เอกสารขาย
สร้าง ค้นหา อ่าน แก้ไข แปลงเอกสาร และเปลี่ยนสถานะใบเสนอราคา ใบกำกับภาษี ใบเสร็จรับเงิน และเอกสารขายอื่น ๆ
เอกสารขายเป็นส่วนที่ใช้งานหนักที่สุดของ API นี้ เป็นจุดที่ระบบ POS แปลงยอดขายของวันเป็นใบกำกับภาษี หรือร้านค้าออนไลน์ออกใบเสร็จรับเงินเมื่อลูกค้าชำระเงิน
เอกสารขายทั้งเจ็ดชนิดใช้ endpoint ชุดเดียวกันและ payload รูปแบบเดียวกัน โดยใช้ documentType เป็นตัวระบุว่ากำลังสร้างเอกสารชนิดใด
| รหัส | เอกสาร |
|---|---|
QT | ใบเสนอราคา |
SO | ใบสั่งขาย |
IV | ใบแจ้งหนี้ / ใบกำกับภาษี |
RE | ใบเสร็จรับเงิน |
CS | ขายสด |
CN | ใบลดหนี้ |
DN | ใบเพิ่มหนี้ |
เอกสารมัดจำ (DP) มีอยู่ใน ABSERP แต่สร้างผ่านขั้นตอนเงินมัดจำในเว็บแอปเท่านั้น ไม่ได้สร้างผ่าน endpoint นี้
สร้างเอกสาร
POST /sales/documents → 201 คืนเอกสาร ตรง ๆ (ไม่ห่อด้วย data)
ต้องระบุ documentType, contactId, issueDate และรายการสินค้าอย่างน้อยหนึ่งรายการ แต่ละรายการต้องมี name และ quantity มากกว่าศูนย์
curl -X POST "$ABSERP_API_URL/openapi/v1/sales/documents" \
-H "Authorization: Bearer $ABSERP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"documentType": "IV",
"contactId": 12,
"issueDate": "2026-09-08",
"dueDate": "2026-10-08",
"vatRate": 7,
"currency": "THB",
"warehouseId": 1,
"note": "คำสั่งซื้อ #A-1099 จากร้านค้าออนไลน์",
"items": [
{
"productId": 5,
"name": "Widget",
"unit": "ชิ้น",
"quantity": 10,
"unitPrice": 300,
"discountPercent": 0,
"sortOrder": 0
}
]
}'ฟิลด์ระดับเอกสาร
| ฟิลด์ | ชนิด | จำเป็น | หมายเหตุ |
|---|---|---|---|
documentType | string | ใช่ | หนึ่งในเจ็ดรหัสด้านบน |
contactId | integer | ใช่ | ลูกค้า ดูผู้ติดต่อ |
issueDate | string | ใช่ | YYYY-MM-DD |
dueDate | string | ไม่ | YYYY-MM-DD |
vatRate | number | ไม่ | เปอร์เซ็นต์ เช่น 7 เว้นไว้หรือใส่ 0 สำหรับเอกสารที่ไม่มี VAT |
discountAmount | number | ไม่ | ส่วนลดระดับเอกสาร เพิ่มจากส่วนลดรายบรรทัด |
currency | string | ไม่ | รหัส 3 ตัวอักษร ถ้าไม่ส่งจะใช้สกุลเงินขององค์กร (THB) |
note | string | ไม่ | แสดงบนเอกสาร |
warehouseId | integer | ไม่ | คลังที่ตัดสต็อก |
project / projectId | string / integer | ไม่ | ระบุโครงการ (ไม่บังคับ) |
whtRate | number | ไม่ | เปอร์เซ็นต์ภาษีหัก ณ ที่จ่าย 0–100 |
whtAmount | number | ไม่ | จำนวนเงินภาษีหัก ณ ที่จ่าย |
bankId | integer | ไม่ | ใช้กับ RE และ CS เท่านั้น — บัญชีที่เงินเข้า ส่งกับชนิดอื่นจะได้ 400 |
items | array | ใช่ | อย่างน้อยหนึ่งรายการ |
ฟิลด์รายบรรทัด
| ฟิลด์ | ชนิด | จำเป็น | หมายเหตุ |
|---|---|---|---|
name | string | ใช่ | ชื่อรายการที่พิมพ์บนเอกสาร |
quantity | number | ใช่ | ต้องมากกว่าศูนย์ |
productId | integer | ไม่ | เชื่อมบรรทัดกับสินค้า เพื่อให้สต็อกและต้นทุนเคลื่อนไหว |
productAltUnitId | integer | ไม่ | ขายด้วยหน่วยทางเลือก เช่น กล่องละ 12 ชิ้น |
description | string | ไม่ | คำอธิบายบรรทัดที่สอง |
unit | string | ไม่ | ข้อความอิสระ เช่น ชิ้น |
unitPrice | number | ไม่ | ค่าเริ่มต้น 0 |
discountPercent | number | ไม่ | 0–100 |
discountAmount | number | ไม่ | ส่วนลดเป็นจำนวนเงินของบรรทัดนี้ |
remark | string | ไม่ | หมายเหตุภายใน |
sortOrder | integer | ไม่ | ลำดับการพิมพ์ |
บรรทัดที่ไม่ระบุ productId คือบรรทัดข้อความอิสระ จะแสดงบนเอกสารและรวมในยอด แต่ไม่ตัดสต็อก เหมาะกับค่าบริการหรือค่าขนส่ง
ข้อมูลที่คืนกลับ
{
"id": 41,
"documentNumber": "IV-2026-0001",
"documentType": "IV",
"contactId": 12,
"contactName": "บริษัท เอซีเอ็มอี จำกัด",
"contactTaxId": "0105551234567",
"status": "draft",
"canConvert": true,
"issueDate": "2026-09-08",
"dueDate": "2026-10-08",
"subtotalAmount": 3000,
"discountAmount": 0,
"totalAmount": 3000,
"vatRate": 7,
"vatAmount": 210,
"grandTotal": 3210,
"paidAmount": 0,
"outstandingAmount": 3210,
"currency": "THB",
"warehouseId": 1,
"whtRate": 0,
"whtAmount": 0,
"items": [
{
"id": 88,
"productId": 5,
"name": "Widget",
"unit": "ชิ้น",
"quantity": 10,
"unitPrice": 300,
"discountPercent": 0,
"discountAmount": 0,
"totalPrice": 3000,
"sortOrder": 0
}
],
"createdAt": 1757289600
}สังเกตว่าอะไรที่เซิร์ฟเวอร์เป็นเจ้าของและคุณไม่ต้องส่ง
documentNumberมาจากกฎการออกเลขที่ขององค์กรสำหรับเอกสารชนิดนั้น (คำนำหน้า จำนวนหลัก การรีเซ็ตรายปี/รายเดือน) คุณกำหนดเองไม่ได้- ยอดทั้งหมด —
subtotalAmount,vatAmount,grandTotal,outstandingAmount— คำนวณจากรายการ ให้ส่งราคาและจำนวน แล้วอ่านยอดกลับมา ไม่ต้องคำนวณเอง statusเริ่มที่draftเสมอ
ค้นหาเอกสาร
GET /sales/documents → แบ่งหน้า คืน ตรง ๆ
| พารามิเตอร์ | หมายเหตุ |
|---|---|
type | รหัสชนิดเอกสารหนึ่งค่า |
status | กรองตามสถานะ |
contactId | รหัสลูกค้า |
fromDate / toDate | YYYY-MM-DD รวมวันปลายทั้งสองข้าง อ้างอิงวันที่เอกสาร |
search | ค้นจากเลขที่เอกสารและชื่อผู้ติดต่อ |
refDocumentId | เอกสารที่อ้างอิงเอกสารนี้ |
sourceDocumentId | เอกสารที่แปลงมาจากเอกสารนี้ |
consolidatedDocumentId | เอกสารที่ถูกรวมเข้าเอกสารนี้ |
page / limit | ดูการแบ่งหน้า |
curl -X GET "$ABSERP_API_URL/openapi/v1/sales/documents?type=IV&status=sent&fromDate=2026-09-01&toDate=2026-09-30&limit=100" \
-H "Authorization: Bearer $ABSERP_API_KEY"{
"items": [ { "id": 41, "documentNumber": "IV-2026-0001", "grandTotal": 3210 } ],
"pagination": {
"previousPage": null,
"currentPage": 1,
"nextPage": null,
"totalPages": 1,
"totalRows": 1
}
}แถวในรายการมีฟิลด์เหมือนกับการอ่านรายละเอียด รวมถึง items ควรใช้ตัวกรองขอเฉพาะข้อมูลที่ต้องการ ดีกว่าการดึงมาทั้งหมดแล้วกรองฝั่งไคลเอนต์ เพราะประหยัดโควตากว่ามาก
อ่านเอกสารรายฉบับ
GET /sales/documents/{id} → คืนเอกสาร ตรง ๆ
404 หมายถึงไม่พบเอกสารนั้น ในองค์กรนี้ ซึ่งเป็นคำตอบเดียวกับที่คุณจะได้เมื่อถาม id ของคนอื่น โดยเจตนา
แก้ไขเอกสาร
PATCH /sales/documents/{id} → 200 { "success": true, "message": "..." }
แก้ได้เฉพาะเอกสารสถานะ draft เมื่อเอกสารเดินหน้าไปแล้ว — ส่งแล้ว ชำระแล้ว หรือแปลงแล้ว — API จะตอบ 400 พร้อม errorCode: "InvalidStatusTransition" การแก้เอกสารที่ออกไปแล้วให้ทำตามหลักบัญชี คือออกใบลดหนี้ ไม่ใช่แก้ประวัติ
เปลี่ยนสถานะ
PATCH /sales/documents/{id}/status → 200 { "success": true, "message": "Status updated" }
curl -X PATCH "$ABSERP_API_URL/openapi/v1/sales/documents/41/status" \
-H "Authorization: Bearer $ABSERP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "sent", "remark": "ส่งอีเมลให้ลูกค้าแล้ว" }'| ฟิลด์ | ชนิด | หมายเหตุ |
|---|---|---|
status | string | สถานะเป้าหมาย ต้องเป็นการเปลี่ยนที่อนุญาตจากสถานะปัจจุบัน |
remark | string | ไม่บังคับ เก็บไว้ในประวัติสถานะ |
confirmNegativeStock | boolean | ไม่บังคับ ใส่ true เพื่อยอมรับการตัดสต็อกที่ทำให้ยอดติดลบ |
เอกสารแต่ละชนิดมีชุดการเปลี่ยนสถานะที่อนุญาตต่างกัน ดูข้อมูลอ้างอิงชนิดเอกสาร การเปลี่ยนที่ไม่อนุญาตจะได้ 400 พร้อม errorCode: "InvalidStatusTransition" และไม่มีอะไรเปลี่ยน
GET /sales/documents/{id}/timelineไม่เปิดให้ใช้ผ่าน Open API (ใช้ได้ในเว็บแอปเท่านั้น) สถานะปัจจุบันของเอกสารมีอยู่ในทุกการอ่าน และประวัติสถานะทั้งหมดดูได้ใน ABSERP หากต้องการร่องรอยการตรวจสอบฝั่งคุณ ให้บันทึกการเปลี่ยนสถานะที่คุณสั่งผ่านPATCH /sales/documents/{id}/statusไว้เอง
แปลงเอกสาร
POST /sales/convert → 201 คืนเอกสาร ฉบับใหม่ ตรง ๆ
curl -X POST "$ABSERP_API_URL/openapi/v1/sales/convert" \
-H "Authorization: Bearer $ABSERP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "fromId": 41, "toType": "RE" }'toType รับค่า SO, IV, CS, RE, CN หรือ DN ระบบจะคัดลอกรายการ ลูกค้า และยอดเงินไปให้ เอกสารใหม่ได้เลขที่ของตัวเอง และเอกสารทั้งสองยังเชื่อมโยงกัน คุณค้นเอกสารลูกได้ภายหลังด้วย sourceDocumentId
นี่คือเส้นทางปกติของงานขาย QT → SO → IV → RE ควรใช้การแปลงเอกสารแทนการสร้างใหม่ เพื่อให้สายเอกสารตรวจสอบย้อนหลังได้และไม่นับยอดซ้ำ
ข้อมูลสำหรับพิมพ์เอกสาร
GET /sales/documents/{id}/print-data คืนข้อมูลเป็น JSON ไม่ใช่ไฟล์ PDF ประกอบด้วยตัวเอกสารเอง (โครงสร้างเดียวกับการอ่าน GET /sales/documents/{id}) รวมกับ organizationName และ organizationLogo สำหรับใช้จัดหน้าเอกสาร
curl -X GET "$ABSERP_API_URL/openapi/v1/sales/documents/41/print-data" \
-H "Authorization: Bearer $ABSERP_API_KEY"การจัดหน้าเป็นไฟล์ PDF จริงเป็นหน้าที่ของฝั่งไคลเอนต์ — เรียก endpoint นี้เพื่อเอาข้อมูลไปเรนเดอร์เป็น PDF เอง (เช่นด้วย jspdf, @react-pdf/renderer หรือไลบรารีที่เทียบเท่าฝั่งของคุณ) ไม่มี endpoint ใดใน Open API ที่คืนไฟล์ PDF โดยตรง
ลบเอกสาร
DELETE /sales/documents/{id} → 204 No Content ไม่มี body
การลบมีไว้สำหรับความผิดพลาดที่พบทันที เช่น เอกสารร่างที่ยังไม่มีใครเห็น เอกสารที่ออกไปแล้วควรยกเลิก (void) หรือออกใบลดหนี้แทน เพื่อให้เลขที่เอกสารและบัญชียังสมบูรณ์
รหัสข้อผิดพลาด
นอกจากรหัสทั่วไปในข้อผิดพลาดและการแบ่งหน้า รหัสต่อไปนี้เฉพาะกับเอกสารขาย
| สถานะ | รหัส | สาเหตุ |
|---|---|---|
| 404 | SalesDocumentNotFound | ไม่พบ :id ในองค์กรนี้ |
| 404 | CustomerNotFound | ไม่พบ contactId ในองค์กรนี้ |
| 400 | BankNotFound | ไม่พบ bankId ในองค์กรนี้ |
| 400 | BankNotAllowedForDocumentType | ส่ง bankId กับชนิดเอกสารที่ไม่ใช่ RE/CS |
| 400 | InvalidStatusTransition | การเปลี่ยนสถานะใน PATCH .../status ไม่ใช่การเปลี่ยนที่อนุญาตจากสถานะปัจจุบัน ดูข้อมูลอ้างอิงชนิดเอกสาร |
| 400 | SalesDocumentCannotConvert | ชนิดหรือสถานะของเอกสารต้นทางไม่อนุญาตให้ POST /sales/convert |
| 400 | SalesDocumentInvalidConversionTarget | toType ไม่ใช่ปลายทางที่อนุญาตสำหรับชนิดของเอกสารต้นทาง |
| 400 | SalesDocumentHasOutstandingDeposits | การเปลี่ยนสถานะจะทำให้มีเงินมัดจำค้างอยู่โดยไม่มีที่ให้นำไปใช้ |
| 400 | SalesDocumentHasReservedDeposits | แก้ไขเอกสารร่างที่มีการแปลงเอกสารจองเงินมัดจำไว้แล้ว ให้ลบเอกสารนี้แล้วแปลงเอกสารต้นทางใหม่แทน |
| 400 | NoDefaultWarehouse | เอกสารที่ตัดสต็อกไม่มี warehouseId และองค์กรยังไม่ได้ตั้งค่าคลังเริ่มต้น |
| 409 | SalesDocumentAlreadyConsolidated | เอกสารนี้ถูกรวมเข้ากับเอกสารอื่นไปแล้ว |
| 422 | NegativeStockBlocked | การยืนยันจะทำให้สต็อกติดลบ และนโยบายองค์กรบล็อกกรณีนี้ |
| 422 | NegativeStockWarnConfirmRequired | เหมือนกัน แต่องค์กรตั้งไว้แค่เตือน ให้ส่งซ้ำพร้อม confirmNegativeStock: true |
ขั้นตอนถัดไป
อ่านต่อที่ เอกสารซื้อ