945 Technical Manual

945 Development Concept

  • ออกแบบระบบโดยเน้นการคงการทำงานด้านบัญชีตามมาตรฐาน Odoo ให้มากที่สุดโดยอิงหลักการ Odoo + OCA + Custom

  • ส่วนโมดูลเพิ่มเติม เน้นการใช้งาน OCA modules ก่อนหลังจากนั้นจึงใช้ Customer Codes ดูคำบรรยายของแต่ละโมดูลเสริมได้จาก Modulesx

  • การยิง API มีการยิงเข้า 2 จุดหลัก คือเข้าที่หน้าต่าง Sales Order และเข้าที่หน้าต่าง 945 Operations โดยการยิง Reference API สามารถดูได้ที่ APIs

  • การยิง API เข้ามาในระบบจะเป็นการยิงเข้า Job Queue เสมอเพื่อให้ระบบมีความเร็วและรับโหลดได้มาก

  • กรณียิง API เข้า Sales Order ในบางประเภทจะมีการ Compute Action เพื่อทำการปรับปรุงรายการให้เข้ากับแบบแผนการคำนวนของ 945 และใช้ Sale Automatic Workflow เพื่อทำการวิ่งเอกสารต่อไปยัง Account Invoice เพื่อทำการบันทึกบัญชี (ตามหลัก Odoo ปกติ)

  • กรณียิง API เข้า 945 Operations (ซึ่งมีหลาย windows/model ในการรับข้อมูล) จะมีการสร้าง Journal ด้วย Account Move Template

  • ในกรณีมีการคำนวนที่่มีความซับซ้อน ระบบจะเรียกใช้งาน Compute Helper

  • มี Unit Test สำหรับ process ที่สำคัญ

Odoo + OCA + 945

  1. Odoo Core

  2. Odoo Enterprise version 13

  3. Odoo Community Association (OCA)

  4. Customer Codes (945)

../_images/eco.png

> Modules

OCA Modules

  • l10n_th_xxx (โมดูลที่เริ่มต้นด้วย)

    เกี่ยวกับภาษีไทย - WHT, VAT, Undue VAT, Tax Reports.

  • queue_job

    Base module สำหรับ Queue Job ถูกเรียกใช้โดย sunteen_api ในการเปลี่ยน function ให้เป็นแบบ queue @job_auto_delay

  • sale_automatic_workflow, sale_automatic_workflow_job

    โมดูลเสริมเพื่อให้ workflow จาก Sales Order -> Invoice เกิดขึ้นโดยอัตโนมัติ

  • account_mass_reconcile

    โมดูลเสริมเพื่อให้ผู้ใช้งานทางบัญชีสามารถจับคู่เคลียร์ได้่ง่ายขึ้น โดยตั้งค่าการเคลียร์แบบอัตโนมัติได้

  • account_move_template, account_move_template_enhanced

    ช่วยในการสร้าง Template สำหรับการบันทึกบัญชี เรียกใช้โดย Operations ต่างๆ

  • account_payment_term_extension

    เพิ่ม Payment Term ตามโจทย์ของ 945 เช่นเอกสารในอาทิตย์นี้ทั้งหมดให้ไปกองวันครบกำหนดที่ศูกร์หน้า เป็นต้น

945 Modules

  • sunteen

    โมดูลหลักสำหรับ Business Process ของ 945 ทั้งหมด เช่น eCommerce, Express.

  • sunteen_install

    ทำหน้าที่่เรียกการติดตั้งโมดูลทั่งหมดที่ต้องใช้ใน 945 โดยตัวเองไม่ได้มี logic ใดๆ

  • sunteen_coa, sunteen_data, sunteen_form

    ทำหน้าที่โหลดข้อมูลที่ต้องใช้งาน เช่น Chart of Account, Move Templates, การตั้งค่าต่างๆ และแบบฟอร์ม

  • sunteen_mass_reconcile

    เสริมการทำงานของ account_mass_reconcile เพื่อให้จับคู่เคลียร์ผ่าน Parcel Number ได้

  • sunteen_test

    โมดูลเสริมสำหรับการทำ Unit Test

> 945 APIs

ตัวอย่างการใช้งาน APIs ระบบ 945

eCommerce

  • eCommerce - Standard

  • eCommerce - Consignment Fix GP

  • eCommerce - Consignment VAR GP

eCommerce - Standard

API 1 (sale.order)

{
    "payload": {
        "status": "out of delivery",  # Status: out of delivery
        "partner_id": 1,  # Customer (res.partner)
        "sunteen_dealer_id": 2,  # Dealer (res.partner)
        "workflow_process_id": "eCommerce Standard",  # Automatic Workflow (sale.workflow.process)
        "date_order": "2020-01-31",  # Order Date
        "transaction_date": "",  # Transaction Date
        "sunteen_payment_method_id": 3,  # Payment Method (sunteen.payment.method)
        "sunteen_payment_provider_id": 34,  # Payment Provider (res.partner)
        "total": 500.0,  # Total
        "order_line": [  # Sale Order Line
            {
                "sunteen_parcel_number": "TDZ001",  # Parcel Number
                "sunteen_partner_id": 14,  # Transporter (res.partner)
                "sunteen_customer": "CUSTOMER1",  # Customer
                "product_id": 16,  # Product (product.product)
                "product_uom_qty": 1,  # Quantity
                "dealer_price_unit": 100.0,  # Dealer Unit Price
            },
            {
                "sunteen_parcel_number": "TDZ001",
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 17,
                "product_uom_qty": 2,
                "dealer_price_unit": 200.0,
            },
            {
                "sunteen_parcel_number": "TDZ001",  # Dealer Amount Difference Line
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 18,
                "product_uom_qty": 1,
                "dealer_price_unit": 20.0,  # API sent +, odoo change to -
            },
        ]
    },
    "auto_create": {    # Optional for auto create master data
        "partner_id": [
            {"name": "Customer 1", "ref": "CUSTOMER1"}
        ],
        "product_id": [
            {"name": "Product 1", "default_code": "PRODUCT1"}
        ]
    }
}

API2 (sunteen.delivery.complete):

{
    "payload": {
        "status": "completed",  # Status : completed/return
        "partner_id": 1,  # Customer (res.partner)
        "sunteen_dealer_id": 2,  # Dealer (res.partner)
        "workflow_process_id": "eCommerce Standard",  # Automatic Workflow (sale.workflow.process)
        "date_order": "2020-01-31",  # Order Date
        "transaction_date": "",  # Transaction Date
        "sunteen_payment_method_id": 3,  # Payment Method (sunteen.payment.method)
        "sunteen_payment_provider_id": 34,  # Payment Provider (res.partner)
        "total": 500.0,  # Total
        "order_line": [  # Sale Order Line
            {
                "sunteen_parcel_number": "TDZ001",  # Parcel Number
                "sunteen_partner_id": 14,  # Transporter (res.partner)
                "sunteen_customer": "CUSTOMER1",  # Customer
                "product_id": 16,  # Product (product.product)
                "product_uom_qty": 1,  # Quantity
                "dealer_price_unit": 100.0,  # Dealer Unit Price
            },
            {
                "sunteen_parcel_number": "TDZ001",
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 17,
                "product_uom_qty": 2,
                "dealer_price_unit": 200.0,
            },
            {
                "sunteen_parcel_number": "TDZ001",  # Dealer Amount Difference Line
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 18,
                "product_uom_qty": 1,
                "dealer_price_unit": 20.0,  # API sent +, odoo change to -
            },
        ]
    },
    "auto_create": {  # Optional for auto create master data
        "partner_id": [
            {"name": "Customer 1", "ref": "CUSTOMER1"}
        ],
        "product_id": [
            {"name": "Product 1", "default_code": "PRODUCT1"}
        ]
    }
}

eCommerce - Consignment Fix GP

API1 (sunteen.record.transportation.cost):

{
    "payload": {
        "status": "out of delivery",  # Status: out of delivery
        "partner_id": 1,  # Customer=Consignor (res.partner)
        "sunteen_dealer_id": 2,  # Dealer (res.partner)
        "workflow_process_id": "eCommerce Consignment Fix GP",  # Automatic Workflow (sale.workflow.process)
        "date_order": "2020-01-31",  # Order Date
        "transaction_date": "",  # Transaction Date
        "sunteen_payment_method_id": 3,  # Payment Method (sunteen.payment.method)
        "sunteen_payment_provider_id": 34,  # Payment Provider (res.partner)
        "total": 500.0,  # Total
        "order_line": [  # Sale Order Line
            {
                "sunteen_parcel_number": "TDZ001",  # Parcel Number
                "sunteen_partner_id": 14,  # Transporter (res.partner)
                "sunteen_customer": "CUSTOMER1",  # Customer
                "product_id": 16,  # Product (product.product)
                "product_uom_qty": 1,  # Quantity
                "dealer_price_unit": 100.0,  # Dealer Unit Price
            },
            {
                "sunteen_parcel_number": "TDZ001",
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 17,
                "product_uom_qty": 2,
                "dealer_price_unit": 200.0,
            },
            {
                "sunteen_parcel_number": "TDZ001",  # Dealer Amount Difference Line
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 18,
                "product_uom_qty": 1,
                "dealer_price_unit": 20.0,  # API sent +, odoo change to -
            },
        ]
    },
    "auto_create": {  # Optional for auto create master data
        "partner_id": [
            {"name": "Customer 1", "ref": "CUSTOMER1"}
        ],
        "product_id": [
            {"name": "Product 1", "default_code": "PRODUCT1"}
        ]
    }
}

API2 (sale.order):

{
    "payload": {
        "status": "completed",  # Status: completed/return
        "partner_id": 1,  # Customer=Consignor (res.partner)
        "sunteen_dealer_id": 2,  # Dealer (res.partner)
        "workflow_process_id": "eCommerce Consignment Fix GP",  # Automatic Workflow (sale.workflow.process)
        "date_order": "2020-01-31",  # Order Date
        "transaction_date": "",  # Transaction Date
        "sunteen_payment_method_id": 3,  # Payment Method (sunteen.payment.method)
        "sunteen_payment_provider_id": 34,  # Payment Provider (res.partner)
        "total": 500.0,  # Total
        "order_line": [  # Sale Order Line
            {
                "sunteen_parcel_number": "TDZ001",  # Parcel Number
                "sunteen_partner_id": 14,  # Transporter (res.partner)
                "sunteen_customer": "CUSTOMER1",  # Customer
                "product_id": 16,  # Product (product.product)
                "product_uom_qty": 1,  # Quantity
                "dealer_price_unit": 100.0,  # Dealer Unit Price
            },
            {
                "sunteen_parcel_number": "TDZ001",
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 17,
                "product_uom_qty": 2,
                "dealer_price_unit": 200.0,
            },
            {
                "sunteen_parcel_number": "TDZ001",  # Dealer Amount Difference Line
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 18,
                "product_uom_qty": 1,
                "dealer_price_unit": 20.0,  # API sent +, odoo change to -
            },
        ]
    },
    "auto_create": {  # Optional for auto create master data
        "partner_id": [
            {"name": "Customer 1", "ref": "CUSTOMER1"}
        ],
        "product_id": [
            {"name": "Product 1", "default_code": "PRODUCT1"}
        ]
    }
}

eCommerce - Consignment Var GP

API1 (sunteen.record.transportation.cost):

{
    "payload": {
        "status": "out of delivery",  # Status: out of delivery
        "partner_id": 1,  # Customer=Consignor (res.partner)
        "sunteen_dealer_id": 2,  # Dealer (res.partner)
        "workflow_process_id": "eCommerce Consignment Var GP",  # Automatic Workflow (sale.workflow.process)
        "date_order": "2020-01-31",  # Order Date
        "transaction_date": "",  # Transaction Date
        "sunteen_payment_method_id": 3,  # Payment Method (sunteen.payment.method)
        "sunteen_payment_provider_id": 34,  # Payment Provider (res.partner)
        "total": 500.0,  # Total
        "order_line": [  # Sale Order Line
            {
                "sunteen_parcel_number": "TDZ001",  # Parcel Number
                "sunteen_partner_id": 14,  # Transporter (res.partner)
                "sunteen_customer": "CUSTOMER1",  # Customer
                "product_id": 16,  # Product (product.product)
                "product_uom_qty": 1,  # Quantity
                "dealer_price_unit": 100.0,  # Dealer Unit Price
            },
            {
                "sunteen_parcel_number": "TDZ001",
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 17,
                "product_uom_qty": 2,
                "dealer_price_unit": 200.0,
            },
            {
                "sunteen_parcel_number": "TDZ001",  # Dealer Amount Difference Line
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 18,
                "product_uom_qty": 1,
                "dealer_price_unit": 20.0,  # API sent +, odoo change to -
            },
        ]
    },
    "auto_create": {  # Optional for auto create master data
        "partner_id": [
            {"name": "Customer 1", "ref": "CUSTOMER1"}
        ],
        "product_id": [
            {"name": "Product 1", "default_code": "PRODUCT1"}
        ]
    }
}

API2 (sale.order):

{
    "payload": {
        "status": "completed",  # Status: completed/return
        "partner_id": 1,  # Customer=Consignor (res.partner)
        "sunteen_dealer_id": 2,  # Dealer (res.partner)
        "workflow_process_id": "eCommerce Consignment Var GP",  # Automatic Workflow (sale.workflow.process)
        "date_order": "2020-01-31",  # Order Date
        "transaction_date": "",  # Transaction Date
        "sunteen_payment_method_id": 3,  # Payment Method (sunteen.payment.method)
        "sunteen_payment_provider_id": 34,  # Payment Provider (res.partner)
        "total": 500.0,  # Total

        "order_line": [  # Sale Order Line
            {
                "sunteen_parcel_number": "TDZ001",  # Parcel Number
                "sunteen_partner_id": 14,  # Transporter (res.partner)
                "sunteen_customer": "CUSTOMER1",  # Customer
                "product_id": 16,  # Product (product.product)
                "product_uom_qty": 1,  # Quantity
                "dealer_price_unit": 100.0,  # Dealer Unit Price
            },
            {
                "sunteen_parcel_number": "TDZ001",
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 17,
                "product_uom_qty": 2,
                "dealer_price_unit": 200.0,
            },
            {
                "sunteen_parcel_number": "TDZ001",  # Dealer Amount Difference Line
                "sunteen_partner_id": 14,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 18,
                "product_uom_qty": 1,
                "dealer_price_unit": 20.0,  # API sent +, odoo change to -
            },
        ]
    },
    "auto_create": {  # Optional for auto create master data
        "partner_id": [
            {"name": "Customer 1", "ref": "CUSTOMER1"}
        ],
        "product_id": [
            {"name": "Product 1", "default_code": "PRODUCT1"}
        ]
    }
}

Express

  • Express - Standard

Express - Standard

API1 (sale.order):

{
    "payload": {
        "status": "out of delivery",  # Status: out of delivery
        "partner_id": 1,  # Customer (res.partner)
        "sunteen_merchant_id": 33,  # Merchant (res.partner)
        "sunteen_merchant_level": "ecom",  # Merchant Level: ecom/agent/franchise
        "sunteen_merchant_ownership": "my945",  # Merchant Ownership: my945/others
        "workflow_process_id": "Express Standard",  # Automatic Workflow (sale.workflow.process)
        "date_order": "2020-01-31",  # Order Date
        "transaction_date": "",  # Transaction Date
        "total": 500.0,  # Total
        "order_line": [  # Sale Order Line
            {
                "sunteen_parcel_number": "TDZ001",  # Parcel Number
                "sunteen_partner_id": 14,  # Transporter (res.partner)
                "sunteen_customer": "CUSTOMER1",  # Customer
                "product_id": 16,  # Product (product.product)
                "product_uom_qty": 1,  # Quantity
                "dealer_price_unit": 100.0,  # Dealer Unit Price
                "type": "normal",  # Type: normal, cod
                "cod_amount": 0,  # COD Amount

            },
            {
                "sunteen_parcel_number": "TDZ002",
                "sunteen_partner_id": 15,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 17,
                "product_uom_qty": 2,
                "dealer_price_unit": 200.0,
                "type": "cod",
                "cod_amount": 20,
            },
        ]
    },
    "auto_create": {  # Optional for auto create master data
        "partner_id": [
            {"name": "Customer 1", "ref": "CUSTOMER1"}
        ],
        "product_id": [
            {"name": "Product 1", "default_code": "PRODUCT1"}
        ]
    }
}

API2 (sunteen.express.delivery.complete):

{
    "payload": {
        "status": "completed",  # Status : completed/return
        "partner_id": 1,  # Customer (res.partner)
        "sunteen_merchant_id": 33,  # Merchant (res.partner)
        "sunteen_merchant_level": "ecom",  # Merchant Level: ecom/agent/franchise
        "sunteen_merchant_ownership": "my945",  # Merchant Ownership: my945/others
        "workflow_process_id": "Express Standard",  # Automatic Workflow (sale.workflow.process)
        "date_order": "2020-01-31",  # Order Date
        "transaction_date": "",  # Transaction Date
        "total": 500.0,  # Total
        "order_line": [  # Sale Order Line
            {
                "sunteen_parcel_number": "TDZ001",  # Parcel Number
                "sunteen_partner_id": 14,  # Transporter (res.partner)
                "sunteen_customer": "CUSTOMER1",  # Customer
                "product_id": 16,  # Product (product.product)
                "product_uom_qty": 1,  # Quantity
                "dealer_price_unit": 100.0,  # Dealer Unit Price
                "type": "normal",  # Type: normal, cod
                "cod_amount": 0,  # COD Amount
                "status": "completed",  # Status: completed/return

            },
            {
                "sunteen_parcel_number": "TDZ002",
                "sunteen_partner_id": 15,
                "sunteen_customer": "CUSTOMER1",
                "product_id": 17,
                "product_uom_qty": 2,
                "dealer_price_unit": 200.0,
                "type": "cod",
                "cod_amount": 20,
                "status": "completed",
            },
        ]
    },
    "auto_create": {  # Optional for auto create master data
        "partner_id": [
            {"name": "Customer 1", "ref": "CUSTOMER1"}
        ],
        "product_id": [
            {"name": "Product 1", "default_code": "PRODUCT1"}
        ]
    }
}

Note

สำหรับ eCommerce การส่งข้อมูลจะเป็น 1 API ต่อ 1 Parcel Number ส่วน Express จะเป็น 1 API ต่อ หลาย Parcel Number

> Job Queue

Job Queue คืออะไร

Job Queue คือการหน่วงการทำงานของ process โดยแทนที่จะทำทันที ก็จะหน่วงและทำในภายหลัง

เช่น หากระบบต้นทางต้องการยิง API เข้ามา 1,000 รายการ ถ้าไม่มี Job Queue ระบบต้นทางจะต้องรอจนกว่ารายการก่อนหน้าจะทำงานเสร็จ จึงจะยิงรายการถัดมาได้ ถ้า server ไม่สามารถทำงานได้เร็วพอก็จะเกิดปัญหาได้ ถ้าใช้ Job Queue ระบบต้นทางสามารถยิงข้อมูลเข้ามาได้ทั้งหมดอย่างรวดเร็วโดยไม่โหลด server (เพราะยังไม่เกิดการทำงานจริง)

ข้อมูลการทำงานทั้งหมดจะเข้ามาพักที่ table queue.job ของ Odoo ก่อน แล้วจึงทยอยทำงานพร้อมกันตามจำนวน workers

ข้อดีอีกประการหนึ่ง จากการที่ข้อมูลการทำงานได้เข้ามาพักไว้ก่อน ไม่ว่าการทำงานจะสำเร็จหรือไม่ ก็จะสามารถตรวจเช็คได้ในภายหลังที่ Odoo

ข้อดีของ Job Queue

  1. เก็บรายการที่ยิงเข้ามาใน database table queue.job (ตั้งเวลาลบทิ้งได้หลังจากทำงานเสร็จและเวลาผ่านไปนาน)

  2. Jobrunner: ทำงานกับ queue ที่เข้ามาได้อย่างทันที

  3. Channels: แบ่งการทำงานของ workers ตาม channel, sub-channel ได้ เพื่อจำกัด worker บางตัวให้ทำงานบางอย่างไม่ปนกัน (ไม่แย่ง CPU)

  4. Retries: มีการ retry งานที่ failed (บางครั้ง faile เพราะ update รายการเดียวกันพร้อมกัน ซึ่งทำอีกครั้งก็จะผ่านไปได้)

  5. Retry Pattern: เช่น ให้ retry 3 ครั้งแรกห่างกัน 10 วินาที retry 5 ครั้งต่อไป ทุกๆ 1 นาที

Note

สำหรับ 945 เราได้ตั้งค่าการทำงานแบบทั่วๆไป อนาคตสามารถเปลี่ยนแปลงได้ตามความเหมาะสม

Configurations

เพื่อให้ Job Queue ทำงานได้ ต้องมีการตั้งค่าที่ odoo config file.

Using the Odoo configuration file:
[options]
(...)
workers = 6
server_wide_modules = web,queue_job

(...)
[queue_job]
channels = root:2

Note

ตัวอย่างนี้ให้ใช้ worker 2 ตัวจากทั้งหมด 6 ตัว ทำงานกับ channel = root

Usage

../_images/job_queue1.png
  1. Job Menu: แต่ละ API ที่ถูกยิงเข้ามาจะเข้ามาสร้าง Job โดยจะเริ่มต้นที่ status = Pending

  2. Job Status: Pending = รอ, Enqueue = เข้าคิว, Started = เริ่มทำงาน, Done = เสร็จ, Failed = ไม่สำเร็จ

  3. Task: รายละเอียดของ function call ที่จะทำ โดยมี input data ทั้งหมดที่ต้องใช้

  4. Function / Channel: Function นี้ทำงานผ่าน Channel ใหน เช่น root.sunteen_api ซึ่งเป็น sub-channel ของ root (จะใช้ร worker ่วมกับ root หากไม่ประกาศแยกไว้)

  5. Retry: จำนวนครั้งที่ retry

  6. Result: ผลลัพธ์ที่ได้ หากเกิด Error จะแสดงเป็นข้อความ exception และเปลี่ยนสถานะเป็น status = Failed (และ retry)

> Sale Automatic Workflow

Sale Automatic Workflow คืออะไร

Menu: Sales > Configuration > Automatic Workflow > Automatic Workflow

Sale Automatic Workflow คือ กระบวนการการดำเนินงานโดยอัตโนมัติที่เกี่ยวข้องกับการขาย ตัวอย่างกระบวนการการดำเนินงานที่เกี่ยวข้องกับการขาย เช่น การ Confirm Sale Order, การสร้าง Invoice จาก Sale Order และการ Validate Invoice ของ Sale Order เป็นต้น โดยกระบวนการการดำเนินงานโดยอัตโนมัติที่เกี่ยวข้องกับการขาย สามารถทำงานได้ใช้โมดูล sale_automatic_workflow และ sale_automatic_workflow_job ของ OCA

โมดูล sale_automatic_workflow เป็นโมดูลที่ใช้ในการสร้างและตั้งค่าการทำงานของ Automatic Workflow ที่จะนำมาใช้ในกระบวนการขาย ซึ่งเหมาะสำหรับระบบที่มีการทำงานโดยการ Interface ผ่าน API เข้ามาที่ Sale Order ใน Odoo

โมดูล sale_automatic_workflow_job เป็นโมดูลที่รวมการทำงานของ sale_automatic_workflow และ job_queue เข้าด้วยกัน ทำให้ระบบสามารถจัดลำดับการทำงานของ Automatic Workflow ได้

Configurations

การตั้งค่า Sale Automatic Workflow แบ่งออกเป็น 3 ส่วน ดังนี้

  1. Order Configuration

  2. Workflow Options

  3. 945 Workflow Options

Order Configuration

  • Shipping Policy คือ วิธีการจัดส่งสินค้า ซึ่งแบ่งได้ดังนี้
    • Deliver all products at once คือ ส่งสินค้าทั้งหมดที่ลูกค้าต้องการในครั้งเดียว

    • Deliver each product when available คือ ส่งสินค้าเท่าที่มีในคลัง(ตามที่ระบุในระบบ)

  • Sales Team คือ ทีมที่ขายสินค้าได้

Workflow Options

  • Validate Order คือ ต้องการให้ Confirm Sale Oder หรือไม่ โดยสามารถจำกัด Domain ของ Sale Order ที่ต้องการ Confirm ได้ที่ Order Filter

  • Confirm and Transfer Picking คือ ต้องการให้ส่งของและ Validate Delivery Order หรือไม่

  • Create Invoice คือ ต้องการให้สร้าง Invoice หรือไม่ โดยสามารถจำกัด Domain ของ Sale Order ที่ต้องการสร้าง Invoice ได้ที่ Create Invoice Filter

  • Validate Invoice คือ ต้องการให้ Validate Invoice หรือไม่ โดยสามารถจำกัด Domain ของ Sale Order ที่ต้องการ Validate ได้ที่ Validate Invoice Filter

  • Sale Done คือ ต้องการ Locked Sale Order หลังจาก Confirm Sale Oder หรือไม่

  • Force Invoice Date คือ ต้องการให้ Invoice Date ใน Invoice เป็นวันเดียวกับ Order Date ใน Sale Order หรือไม่

  • Invoice Service on delivery คือ ต้องการให้บันทึกจำนวน Deliveried ของสินค้าประเภทบริการใน Sale Order หลังจากสร้าง Invoice หรือไม่

  • Sales Journal คือ Journal ที่จะใช้ใน Invoice

  • Warning Message คือ ข้อความที่จะขึ้นแจ้งเตือน เมื่อเลือก Automatic Workflow นี้ ใน Sale Order

945 Workflow Options

  • Allow Compute Sale Order Line คือ ต้องการให้คำนวณรายได้อื่นๆ เช่น รายได้ค่าขนส่ง ใน Sale Order หรือไม่

  • Record Transportation Cost คือ ต้องการสร้าง Transportation Cost (TC) หรือไม่

  • Record Delivery Complete คือ ต้องการสร้าง Delivery Complete (DC) หรือไม่

  • Consignment Payable คือ วิธีการคำนวณเจ้าหนี้การค้าฝากขาย ซึ่งจะเท่ากับ Account Payable Transfer (APT) โดยสามารถเลือกได้ ดังนี้
    • None คือ ไม่คำนวณ Consignment Payable

    • Fixed Cost คือ คำนวณโดยที่ทราบต้นทุนที่แน่นอน เช่น เมื่อขายสินค้า A ได้ จะต้องจ่ายเงินให้ผู้ฝากขาย 10 บาท/หน่วย

    • Variance Cost คือ คำนวณโดยที่ไม่ทราบต้นทุนที่แน่นอน เช่น เมื่อขายสินค้า B ได้ ผู้ฝากขายจะได้รับเงินตามรายได้หลังหักค่าใช้จ่ายต่างๆแล้ว

945’s Configurations

ปัจจุบัน 945 มีการใช้ Sale Automatic Workflow ทั้งหมด 4 แบบ ดังนี้

  1. eCommerce Standard

  2. eCommerce Consignment Fix GP

  3. eCommerce Consignment Var GP

  4. Express Standard

eCommerce Standard

../_images/automatic_workflow_ecom_std.png

eCommerce Consignment Fix GP

../_images/automatic_workflow_ecom_consign_fix.png

eCommerce Consignment Var GP

../_images/automatic_workflow_ecom_consign_var.png

Express Standard

../_images/automatic_workflow_express_std.png

> Account Move Template

Account Move Template คืออะไร

Menu: Accounting > Configuration > Accounting > Journal Entry Templates

Account Move Template หรือ Journal Entry Template คือ Template ของการบันทึกบัญชี มีประโยชน์ในการบันทึกบัญชีที่มีรูปแบบของคู่บัญชีเหมือนๆกัน แต่มีจำนวนมาก

Configurations

../_images/account_move_template.png
  • Name คือ ชื่อของ Template

  • Journal คือ สมุดบัญชี

  • Reference คือ ข้อความที่ต้องการให้แสดงใน Reference ของ Journal Entry

../_images/account_move_template_line.png
  • Sequence คือ ลำดับของ line และใช้อ้างอิงในการคำนวณด้วย

  • Label คือ ข้อความที่ใช้อธิบายการบันทึกบัญชีของ line นั้นๆ

  • Account คือ บัญชีที่ต้องการบันทึก

  • Account Opt. คือ บัญชีที่ต้องการบันทึก เมื่อ Amount ติดลบ

  • Partner คือ คู่ค้า

  • Payment Terms คือ ระยะเวลาที่กำหนดในการจ่ายเงิน

Amount

  • Direction คือ ทิศทางในการบันทึกบัญชี ได้แก่ Credit และ Debit

  • Type คือ วิธีการรับข้อมูล Amount มี 2 แบบ คือ Computed และ User Input

  • Note คือ ข้อความที่ต้องการบันทึกเพิ่มเติม

Note

Compute Formula คือ ส่วนที่ไว้ใส่สูตรที่ใช้ในการคำนวณ Amount ของ line นั้น จะปรากฏเมื่อเลือก Type เป็น Computed

Taxes

  • Is a refund? คือ เป็นการคืนเงินหรือไม่

  • Originator Tax คือ ภาษีตั้งต้น

  • Taxes คือ ภาษี

ตัวอย่าง Account Move Template

../_images/deferred_revenue_template.png

วิธีใช้ Account Move Template

1. เรียกใช้งานผ่าน Code

สิ่งที่จำเป็นในการเรียกใช้งาน Account Move Template คือ

  1. สร้าง Record ของ account.move.template.run

  2. เรียกใช้ function generate_move()

ตัวอย่างการเรียกใช้งานผ่าน Code

def process_generate_move(self):
    wiz = self.create_move()
    wizard = self.env[wiz["res_model"]].browse(wiz["res_id"])
    wizard.with_context(wiz["context"]).generate_move()

2. เรียกใช้งานผ่าน UI

วิธีที่ 1

../_images/gen_move_1.gif
  1. เข้าไปที่ Accounting > Accounting > Miscellaneous > Create Entry from Template

  2. เลือก Template ที่ต้องการใช้เป็นแบบในการบันทึกบัญชี

  3. ใส่ข้อมูลที่ต้องการให้แสดงใน Journal Entry

  4. หลังจากใส่ข้อมูลและตรวจสอบข้อมูลเรียบร้อยแล้ว ให้กดที่ปุ่ม CREATE JOURNAL ENTRY เพื่อสร้าง Journal Entry

Note

สามารถ overwrite ข้อมูลใน line ที่เราต้องการได้ โดยการเพิ่มข้อมูลที่ต้องการ overwrite ไปที่ฟิลด์ Overwrite ในรูปแบบของ Dictionary เช่น {“L0”: {“partner_id”: 1, “date_maturity”: “2020-02-02”}}

วิธีที่ 2

../_images/gen_move_2.gif
  1. เข้าไปที่ Accounting > Configuration > Accounting > Journal Entry Templates

  2. เลือก Template ที่ต้องการใช้เป็นแบบในการบันทึกบัญชี

  3. กดที่ปุ่ม GENERATE JOURNAL ENTRY

  4. ใส่ข้อมูลที่ต้องการให้แสดงใน Journal Entry

  5. หลังจากใส่ข้อมูลและตรวจสอบข้อมูลเรียบร้อยแล้ว ให้กดที่ปุ่ม CREATE JOURNAL ENTRY เพื่อสร้าง Journal Entry

> Compute helper

Compute Helper คืออะไร

Menu: 945 > Configurations > Compute > Compute Type / Helper

Compute Helper เกิดขึ้นเนื่องจากการคำนวนเพื่อให้ได้ค่าต่างๆที่เกิดขึ้นในระบบเช่น Service Cost ซึ่งหลังจากที่เราพัฒนาระบบไปได้ซักระยะ พบว่าระบบมีความซับซ้อนเพราะสูตรการคำนวนขึ้นอยู่กับหลายตัวแปร เช่น Partner, Product, Payment Method การฝังกฏการคำนวนไว้ในโค้ด (hard code) จะทำให้การดูแลระบบเป็นไปอย่างยากลำบาก

จึงได้มีการสร้างตาราง Mapping ไว้แยกต่างหาก (Compute Helper) เพื่อให้เห็นว่าในแต่ละประเภทของการคำนวน (Compute Type) มีสูตรการคำนวน (Server Action) อย่างไร

ตัวอย่างตามรูปคือตัวช่วยการคำนวนสำหรับฟังก์ชั่น _get_service_cost()

../_images/compute_helper.png

ตัวอย่างสำหรับ _get_service_cost() จะถูกเรียกใช้งานผ่าน compute helper ตามตัวอย่างด้านล่าง

def _get_service_cost(self, payment_method_id, payment_provider_id):
    res = self.env["sunteen.compute.helper"].run(
        self, "_get_service_cost", partner_id=payment_provider_id.id or False,
        payment_method_id=payment_method_id.id or False, input_dict={}
    )
    return res

Note

Compute Helper ทั้งหมดปัจจุบันจะถูกโหลดเข้าระบบด้วยไฟล์ data จาก
  • sunteen/data/sunteen_compute_helper_data.xml

  • sunteen_data/data/sunteen.compute.helper.csv

Server Actions

Menu: Settings > Technical > Actions > Server Actions

ด้วย Compute Helper จะมาเรียก Server Action ต่อ แล้ว Server Action คืออะไร?

Server Action เป็นฟังก์ชั่นมาตรฐานหนึ่งของ Odoo ใช้สำหรับการเขียน scriptlet ด้วย Python สั้นๆได้ โดยใน scriptlet สามารถรับการผ่านค่าต่างๆผ่านตัวแปร Context (เช่น model, active_id และอื่นๆ) โดยหลังจากที่สร้าง scriptlet นี้แล้วสามารถนำมันไปผูกใช้งาน กับโมเดลที่สนใจได้ ในกรณีของ 945 เราสร้าง scriptlet โดยไม่ได้นำไปผูกกับโมเดลใดๆ แต่เรียกใช้โดยตรงผ่านฟัังชั่นดังตัวอย่าง _get_service_cost()

รูปนี้เป็นตัวอย่างการเขียน Server Action ที่ใช้ในการคำนวน Service Cost

../_images/server_action.png

Note

รายละเอียดเพิ่มเติมเกี่ยวกับ Server Action

> Unit Test

What is unit test?

Unit Test คือ script ในการจำลองให้ระบบทำงานผ่านโค้ด มีประโยชน์ในการตรวจสอบระบบที่มีการแก้ไขต่อเนื่องให้แน่ใจว่าสิ่งที่แก้ไม่ไปขัดแย้งกับส่วนเดิม หรือถ้าแย้งกันเราจะได้รู้ว่าระบบทำงานถูกต้องหรือไม่ หรือต้องปรับโค้ดและ test script ให้ถูกต้อง

ในระบบ 945 เรามีการเขียน Unit Test เฉพาะกับการทำงานกับ API และซึ่งมีการบันทึกบัญชีอัตโนมัติ โดยปกติเราจะรัน unit test บน development machine เท่านั้นเพื่อตรวจสอบการทำงานของโค้ดก่อนการ deploy

Test script จะถูกเก็บไว้ที่โมดูล sunteen_test โดยเมื่อจะรัน test สิ่งที่ต้องทำคือการ start Odoo ผ่าน Command Line ด้วย -d <database> -i sunteen_test –test-enable เช่น

> ./odoo-bin -c odoo.conf -d api_odoo -i sunteen_test --test-enable

คำสั่งนี้จะบอกให้ Odoo ทำงานกับ database api_odoo และทำการรันการทดสอบของโมดูล sunteen_test

โดย log ของ command line จะมีผลดังตัวอย่างนี้หากสำเร็จ

 2020-11-19 05:02:50,837 23016 INFO sunteen odoo.addons.sunteen_test.tests.test_ecom_standard_1: Starting TestEcomStandard.test_2_transportation_cost ...
 2020-11-19 05:02:50,958 23016 WARNING sunteen odoo.addons.base.models.res_company: The method '_company_default_get' on res.company is deprecated and shouldn't be used anymore
 2020-11-19 05:02:51,428 23016 INFO sunteen odoo.addons.sunteen_test.tests.test_ecom_standard_1: Starting TestEcomStandard.test_3_invoice_entry ...
 2020-11-19 05:02:51,432 23016 INFO sunteen odoo.modules.module: Ran 3 tests in 1.024s

และอาจมีผลลัพธ์แบบนี้หากไม่สำเร็จ

 2020-11-19 04:49:11,697 21813 INFO sunteen odoo.addons.sunteen_test.tests.test_ecom_standard_1: Starting TestEcomStandard.test_4_delivery_complete ...
 2020-11-19 04:49:11,698 21813 INFO sunteen odoo.addons.sunteen_api.models.utils: [sunteen.delivery.complete].create_data(), input: {'payload': {'status': 'out for delivery', 'partner_id': 4960, 'sunteen_dealer_id': 4961, 'name': '55-CP20053042686x', 'workflow_process_id': 'eCommerce Standard', 'date_order': '2020-06-01', 'transation_date': '2020-06-01', 'sunteen_payment_method_id': 1, 'sunteen_payment_provider_id': 10, 'total': 3200.0, 'order_line': [{'product_id': 1323, 'product_uom_qty': 1, 'dealer_price_unit': 1600.0, 'sunteen_parcel_number': 'TDZ20982676x', 'sunteen_partner_id': 10, 'sunteen_customer': 'คันธรส จงอร่ามเรือง(Kantarost)', 'name': 'Colla Tab 10 กระปุก'}, {'product_id': 1324, 'product_uom_qty': 1, 'dealer_price_unit': 1600.0, 'sunteen_parcel_number': 'TDZ20982676x', 'sunteen_partner_id': 10, 'sunteen_customer': 'คันธรส จงอร่ามเรือง(Kantarost)', 'name': 'Aqua S คอลลาเจน 10 แพ็ค'}, {'product_id': 1, 'product_uom_qty': 1, 'dealer_price_unit': 50.0, 'sunteen_parcel_number': 'TDZ20982676x', 'sunteen_partner_id': 10, 'sunteen_customer': 'sudtinee jaithip', 'name': 'Dealer Amount Difference'}]}, 'status': 'completed'}
 2020-11-19 04:49:11,701 21813 INFO sunteen odoo.addons.sunteen_test.tests.test_ecom_standard_1: ======================================================================
 2020-11-19 04:49:11,701 21813 ERROR sunteen odoo.addons.sunteen_test.tests.test_ecom_standard_1: ERROR: TestEcomStandard.test_4_delivery_complete
 Traceback (most recent call last):
   File "/home/kittiu/PycharmProjects/odoo-945-erp/sunteen_test/tests/test_ecom_standard_1.py", line 131, in test_4_delivery_complete
     res = self.DC.create_data(self.ecom_standard_api2_data)
 ...
 ValueError: Wrong value for sunteen.delivery.complete.delivery_status: 'out for delivery'

การเขียน Unit Test

การเขียน Unit Test มีหลักการคือให้มองการทำงานเป็น blackbox เราจะใส่ input data เรียกการทำงาน และประเมินผลที่ได้ และเป้าหมายคือพยายามเขียน script ให้รันผ่านโค้ดให้มากที่สุด โดยเมื่อมีการ start Odoo ด้วย –test-enable ระบบจะเข้าไปหาโฟลเดอร์ tests และไฟล์ที่ชื่อเริ่มต้นด้วย test_xxx.py และจะไปรัน method ที่เริ่มต้นด้วย def test_yyy()

../_images/unit_test.png
  1. ระบบจะมองหาโฟลเดอร์ tests และหารไฟล์ที่เริ่มด้วย test_xxx.py

  2. Inherit Test Class

  3. Setup() คือส่วนที่เอาไว้ initialize ข้อมูลที่ต้องการใช้สำหรับแต่ละการทดสอบ

  4. Test Cases, 1, 2, 3, 4… แล้วแต่เรื่องที่ต้องการ

Note

รายละเอียดเพิ่มเติมเกี่ยวกับ Unit Testing

> SQL Views Editor

หน้าต่าง SQL Views

Menu: Settings > Technical > Database Structure > SQL Views

แทนที่จะเขียน SQL ดิบๆ เพื่อสร้าง Database View โดยตรงที่ระบบฐานข้อมูล เรามีโมดูลช่วยให้การสร้างและดูแล View ง่ายและมีระเบียบขึ้นด้วยหน้าต่าง SQL Views

../_images/1_sql_view_editor.png

สร้าง SQL View

เราสามารถสร้างใหม่และปรับปรุง SQL View ได้จากหน้าต่างนี้

  1. ตั้งชื่อ Name (จะเป็นชื่อเมนู) และชื่อ Technical Name (จะเป็นชื่อ database view)

  2. ชื่อของ database view ที่จะสร้างขึ้น สามารถเรียกใช้งานได้จากระบบ BI อื่นๆ

  3. ลำดับของ UI View ที่จะสร้างขึ้น เป็น View ที่ใช้เพื่อดูข้อมูล, export excel และอื่นๆ

  4. Is Materialized View, สร้าง view แบบ static จาก dynamic sql (ข้อดีคือเร็วเหมือน data table แต่จะไม่ real time เพราะถูก refresh เป็นระยะ)

  5. SQL Query ที่ใช้ในการสร้าง View

Activate View

หลังจากทดสอบ View เสร็จ (Preview SQL Expression) เราสามารถเริ่มใช้งานได้โดย กดปุ่มเหล่านี้

  • [Validate SQL Expression] -> [Create SQL View, Indexes And Model] > [Create UI]

../_images/2_activate_view.png

Open View

คลิกหที่ปุ่ม Open View จากหน้าต่าง SQL View Edit เพื่อเริ่มใช้งานได้ทันที หรือจะเข้ามาทางเมนู Dashboard จากหน้าต่าง Desktop ก็ได้เช่นกัน

../_images/3_open_view.png

Export Excel จาก Tree View โดยการเลือก Column

../_images/4_export_excel.png ../_images/5_excel.png

Refresh Materialized View

หากต้องการ Refresh Data ของ view ที่สร้างขี้นสามารถทำได้โดย

  1. กดปุ่ม Refresh โดยตรง

  2. ดูการตั้งค่าการ Refresh ของ Scheduled Job ของ View นี้

../_images/refresh_view.png

Note

ปกติตั้งไว้ 1 Month สามารถปรับให้ถี่ขึ้นตามต้องการ เช่นวันละ 1 ครั้ง ไม่ควรถี่เกินไปเพราะถ้าข้อมูลเยอะจะใช้เวลานาน