ลดหนี้ / คืนสินค้า
เมนูนี้ใช้สร้างใบลดหนี้จากใบแจ้งหนี้ โดยเลือกได้ทั้งการคืนสินค้าจริงและการลดหนี้ทางการเงินอย่างเดียว กฎข้อมูล สิทธิ และเงื่อนไขของ Mobile กับ POS Terminal เหมือนกัน ต่างกันเพียงรูปแบบหน้าจอ
จุดเริ่มต้น
| แอป | จุดเข้าใช้งาน |
|---|---|
| Mobile | CreditNoteList → CreditNoteDetail หรือกดทำใบคืนสินค้าจากใบแจ้งหนี้ |
| POS Terminal | เมนูแบบ split-pane creditNote หรือกด เปิดใบคืนสินค้า จากรายละเอียดใบแจ้งหนี้ |
รายการจะถูกจำกัดตาม branchId และ whGrpCode ที่เลือก รองรับค้นหา กรองสถานะ รีเฟรช และโหลดเพิ่ม โดยค่าเริ่มต้นเปิดตัวกรอง มีการคืนสินค้า ซึ่งส่ง isReturn=1; ปิดตัวกรองนี้เพื่อดูใบลดหนี้ทางการเงินอย่างเดียวด้วย
สีพื้นหลังการ์ดในหน้ารายการ
Mobile และ Terminal ใช้สีพื้นหลังการ์ดใบลดหนี้/คืนสินค้าเหมือนกัน ทั้งคลังหลักและคลังฝาก:
| Status | ความหมาย | สีพื้นหลัง |
|---|---|---|
DRAFT | ร่าง | ฟ้าอ่อน #CBFEFF |
NEW | ใหม่ | ฟ้าอ่อน #CBFEFF |
WAITINGCN | รอ B1 อนุมัติ | เขียวมะนาวอ่อน #CFFD67 |
INVUPDATE | คลังอัปเดต | ม่วงชมพูอ่อน #E7BCE7 |
RECEIVED | ส่ง B1 สำเร็จ | เขียวอ่อน #76FFA3 |
APPROVED / APPROVE | อนุมัติ | ม่วง #BB8AE3 |
CANCELED | ยกเลิก | ส้มอ่อน #FFC385 |
ERRORRECEIVED | ส่ง B1 ไม่สำเร็จ | ส้มอ่อน #FFC385 |
QC_FAILED | ตรวจ QC ไม่ผ่าน | ส้มอ่อน #FFC385 |
| สถานะอื่น / ไม่ระบุ | สีสำรอง | ส้มอ่อน #FFC385 |
ปัจจุบัน QC_FAILED ใช้สีส้มสำรองของหน้ารายการ ยังไม่ได้กำหนดสีเฉพาะ ส่วนปุ่มเต็มความกว้าง ตรวจ QC ไม่ผ่าน ในหน้ารายละเอียดของคลังฝากเป็นสีแดง (#C53030) และกดไม่ได้ ทั้งสองส่วนเป็นคนละองค์ประกอบ เอกสารนี้บันทึกสีที่ใช้อยู่ ไม่ได้เปลี่ยนสีในแอป
ขั้นตอนคืนสินค้าแบบครบถ้วน
- เปิดใบแจ้งหนี้และเริ่มแบบร่างใบคืนสินค้า ระบบคัดลอกข้อมูลใบแจ้งหนี้ ลูกค้า บริษัท คลัง พนักงานขาย และรายการสินค้าในใบแจ้งหนี้มาให้ ใบคืนสินค้าเดิมจะโหลดใบแจ้งหนี้ที่ผูกไว้ซ้ำ เพื่อให้ยังเลือกรายการต้นทางได้
- เลือก เหตุผล 1 ซึ่งจำเป็นก่อนเพิ่มรายการสินค้าและก่อนบันทึกเอกสาร
- เลือกรายการจากใบแจ้งหนี้ ระบบตั้งต้นจำนวน ราคา ยอดเงิน หน่วยนับ คลัง ภาษี และข้อมูลน้ำหนักจากรายการต้นทาง
- ระบุประเภทการคืน จำนวนคืน (
qtyCN) และยอดลดหนี้ (priceCN) ระบบไม่ยอมให้จำนวนคืนมากกว่าจำนวนในรายการต้นทาง หรือยอดลดหนี้มากกว่ายอดเงินของรายการต้นทาง - บันทึกรายการ หากเอกสารยังไม่มี ระบบจะสร้างเป็น
DRAFTก่อน จากนั้นจะรีเฟรชรายการและยอดรวม - หากเป็นการคืนสินค้าจริง (
isReturn = 1) ผู้มีสิทธิ์คลังเลือก Lot/Batch ที่เมนู ตำแหน่งวาง ตัวเลือก Lot มาจากGET /api/invoices/{invoiceId}?includeLot=trueในฟิลด์items[].lotsของรายการต้นทางโดยตรง ระบบจะไม่เรียก/api/products/lotและไม่ตรวจสต็อกคงเหลือปัจจุบัน - เมื่อเลือก Lot ระบบบันทึก
{ lotNo, qty: qtyCN }จำนวนที่บันทึกจึงยึดจำนวนคืนในใบลดหนี้ ไม่ได้ยึดจำนวนที่แสดงของ Lot ในใบแจ้งหนี้ หากมีหลายบรรทัดในใบแจ้งหนี้ที่ใช้รหัสสินค้าเดียวกัน ระบบรวมจำนวนตามเลข Lot เพื่อใช้แสดงตัวเลือกเท่านั้น - เพิ่มรูปหลักฐานหลังเอกสารถูกสร้างแล้ว เมื่อผู้ใช้ยังแก้ไขเอกสารได้ แล้วบันทึกหรือส่งต่อเอกสารตามสิทธิและสถานะด้านล่าง
- ก่อนส่ง B1 รายการที่คืนสินค้าจริงทุกรายการต้องมี Lot และต้องติ๊ก Q1/Q2/Q3 อย่างน้อยหนึ่งข้อ อุณหภูมิถูกเก็บเป็นข้อมูลเอกสาร แต่ฝั่งแอปไม่มีการตรวจช่วงอุณหภูมิเพิ่มเติม
กฎ Lot และจำนวนคืน
| กฎ | พฤติกรรมของแอป |
|---|---|
| แหล่งข้อมูล Lot | เรียกข้อมูลใบแจ้งหนี้ด้วย includeLot=true |
| ตัวเลือก Lot | ใช้ invoice.items[].lots; แสดง lotNo เป็น Batch พร้อมจำนวนในใบแจ้งหนี้ |
| การตรวจสต็อก | ไม่มีการตรวจสต็อกคงเหลือสำหรับ flow คืนสินค้านี้ จึงไม่บล็อกเพราะสต็อกปัจจุบันน้อยกว่า qtyCN |
| จำนวนที่ส่งเมื่อเลือก Lot | ส่งเท่ากับ qtyCN ของรายการใบลดหนี้เสมอ |
| คืนสินค้าจริง | isReturn = 1; ต้องเลือก Lot ก่อนส่ง B1 |
| ลดหนี้อย่างเดียว | isReturn = 0; ไม่ต้องเลือก Lot ก่อนส่ง B1 |
| ตรวจข้อมูลรายการต้นทาง | qtyCN ต้องไม่มากกว่า qty ต้นทาง และ priceCN ต้องไม่มากกว่า amount ต้นทาง |
| หลาย Lot | หน้าจอปัจจุบันเลือก Lot ได้หนึ่ง Lot ต่อหนึ่งรายการ และส่ง lots ที่มีหนึ่งรายการ |
สถานะและการบันทึก
สถานะที่แอปแสดง ได้แก่ DRAFT, NEW, WAITINGCN, INVUPDATE, RECEIVED, APPROVED/APPROVE, CANCELED, ERRORRECEIVED และ QC_FAILED
| ผู้ดำเนินการ / การทำงาน | ผลลัพธ์หรือเงื่อนไข |
|---|---|
| บันทึกเอกสารใหม่ครั้งแรก | สร้างสถานะ DRAFT |
| ผู้ที่ไม่ใช่ฝ่ายคลังบันทึกและมีรายการคืนสินค้า | เปลี่ยนเป็น WAITINGCN |
| ผู้ที่ไม่ใช่ฝ่ายคลังบันทึกและไม่มีรายการคืนสินค้า | เปลี่ยนเป็น NEW |
ฝ่ายคลังบันทึกจาก WAITINGCN | เปลี่ยนเป็น INVUPDATE |
ฝ่ายคลังบันทึกจาก INVUPDATE | คงเป็น INVUPDATE |
| ฝ่ายคลังบันทึกจากสถานะอื่น | ระบบบล็อกและแจ้งให้รอขั้นตอนคลัง |
| ปุ่มส่ง B1 (ทั้งคลังหลักและคลังฝาก) | แสดงเฉพาะ NEW หรือ INVUPDATE และต้องผ่านสิทธิ์เดิมกับเงื่อนไข QC ของคลังฝาก สถานะอื่นซ่อนปุ่มทั้งหมด |
| ส่ง B1 | เรียก B1 แล้วอัปเดตเป็น RECEIVED เมื่อสำเร็จ |
| เคยส่ง B1 แล้ว | ส่งซ้ำไม่ได้เมื่อมี datetimeToB1 |
RECEIVED | การแก้ไขต่าง ๆ เป็นแบบอ่านอย่างเดียวหรือถูกปิดใช้งาน |
เงื่อนไขเหล่านี้ใช้เหมือนกันทั้ง development และ release แล้ว ข้อยกเว้นชั่วคราวที่ซ่อน QC และเปิดส่ง B1 เพื่อทดสอบ error ถูกถอดออกหลังทดสอบเสร็จ รวมถึงการข้าม Lot/Q
ตรวจ QC คลังฝากก่อนส่ง B1
เมื่อ isMain = false, ใบคืนสินค้าเป็น WAITINGCN และ qcStatus = null ผู้มีสิทธิ์ส่ง B1 เดิมจะเห็นปุ่ม ตรวจสอบ QC แทนปุ่มส่ง B1 ระหว่างรอ QC จะซ่อนปุ่มบันทึกด้วย เพื่อป้องกันการบันทึกเปลี่ยนสถานะเป็น INVUPDATE แล้วข้าม QC ส่วนคลังหลักใช้ flow เดิม
ปุ่มและเงื่อนไขล็อกจาก QC ใช้เฉพาะ isMain === false เท่านั้น คลังหลัก (isMain = true) และคลังที่ยังไม่ทราบประเภทจะไม่เข้า flow QC แม้ผล API จะมีฟิลด์ QC มาด้วย
Modal ตรวจสอบ QC มีช่องเหตุผลหลายบรรทัด (ไม่เกิน 500 ตัวอักษร) และปุ่ม ผ่าน / ไม่ผ่าน ถ้าผ่านไม่บังคับเหตุผล ถ้าไม่ผ่านต้องระบุเหตุผลที่ไม่ใช่ข้อความว่าง ระบบส่ง PATCH /api/credit-notes/{id}/qc ด้วย { qcPassed: boolean, qcRemark: string } แล้วโหลดรายละเอียดใบคืนสินค้าใหม่ การตรวจ QC ไม่ได้บันทึกหัวเอกสารหรือส่ง B1 ให้อัตโนมัติ
QC_FAILED เป็นสถานะที่ backend กำหนดใน API ตรวจ QC ไม่ใช่ชื่อสถานะที่แอปตั้งขึ้นเอง เอกสาร API ระบุว่าเมื่อไม่ผ่าน QC ใบคืนสินค้าจะเปลี่ยนเป็น QC_FAILED แอปส่งเฉพาะผลตรวจและเหตุผล แล้วอ่าน qcStatus กับ creditNoteStatus จากรายละเอียดที่โหลดใหม่ ไม่ได้ PATCH creditNoteStatus: "QC_FAILED" เอง
ตัวอย่าง payload เมื่อไม่ผ่าน:
{
"qcPassed": false,
"qcRemark": "สินค้าละลาย บรรจุภัณฑ์ฉีกขาด"
}
ถ้าผ่านโดยไม่ระบุเหตุผล แอปส่ง { "qcPassed": true, "qcRemark": "" }
qcStatus = true: กลับมาแสดงบันทึกตามสิทธิ์เดิม ถ้าโหลดแล้วสถานะยังเป็นWAITINGCNจะยังไม่แสดงส่ง B1 ต้องบันทึกตาม flow คลังให้เป็นINVUPDATEก่อน ปุ่มส่ง B1 แสดงเฉพาะNEWหรือINVUPDATEยังคงตรวจ Lot, Q1/Q2/Q3, การส่งแล้ว และเปลี่ยนสถานะตาม flow เดิมqcStatus = falseหรือสถานะQC_FAILEDจาก API: ซ่อนบันทึกและส่ง B1 แสดงปุ่มสีแดงเต็มความกว้าง ตรวจ QC ไม่ผ่าน เพียงปุ่มเดียวในส่วนดำเนินการ กดไม่ได้ และช่องตรวจคลัง เหตุผล รายการสินค้า และรูปหลักฐานแก้ไขไม่ได้- ป้องกันการส่งผลซ้ำระหว่างรอ API หากส่งไม่สำเร็จจะคงเหตุผลและแสดงข้อความผิดพลาด หาก PATCH สำเร็จแต่โหลดรายละเอียดล้มเหลว ให้โหลดข้อมูลซ้ำโดยไม่ส่งผล QC ซ้ำ และไม่ปลดล็อก B1 จากการคาดเดาผล
Mobile และ Terminal ใช้กฎ/ตัวสร้าง payload ร่วมใน packages/utils/src/creditNoteQc.ts และ Modal ใน packages/rn-ui/src/CreditNoteQcModal.tsx ฟอร์มนี้เป็นของพนักงาน ไม่มีฟอร์ม QC ให้แก้ไขบนจอรอง
สิทธิและเงื่อนไข
ตารางด้านล่างเป็นสิทธิ์พื้นฐานตามบทบาท สำหรับคลังฝาก ระหว่างรอ QC จะซ่อนบันทึกเพิ่ม และเมื่อ QC ไม่ผ่านจะล็อกการแก้ไขในหน้ารายละเอียดและซ่อนบันทึก/ส่ง B1 ส่วนเงื่อนไขลบเอกสารจากหน้ารายการแยกต่างหาก
ค่าด้านล่างเป็น deptCode ทั้งหมด ส่วน user-code ที่ได้รับสิทธิ์ B1 เพิ่มเติมคือ 012142, 012570, 012544, 012051, 012566
| ความสามารถ | สิทธิและเงื่อนไข |
|---|---|
| เปิดเมนูลดหนี้/คืนสินค้า (Mobile/POS Terminal) | -2, 1, 2, 3, 4, 7, 8, 9, 12, 13, 16, 20, 21, 22, 23, 27, 28, 29 หรือ user-code ที่ตั้งค่า override ไว้สำหรับเมนู |
| สร้างจากรายการ Mobile | -2, 1, 2, 4, 7, 8, 9, 12, 16, 22, 28, 29 |
| บันทึกเอกสาร | 7, 8, 9, 12, 22, 28, 29 และสถานะต้องเป็น DRAFT, NEW, WAITINGCN หรือ INVUPDATE |
| เพิ่มรายการ | เอกสารไม่ใช่ RECEIVED และผู้ใช้ปัจจุบันเป็นเจ้าของเอกสาร |
| แก้จำนวนหรือยอดรายการ | เอกสารไม่ใช่ RECEIVED, ต้องเป็นเจ้าของเอกสาร และอยู่ใน 7, 12, 22, 28, 29 |
| ลบรายการ | เอกสารไม่ใช่ RECEIVED และผู้ใช้ปัจจุบันเป็นเจ้าของเอกสาร |
| เลือกตำแหน่งวาง/Lot | เอกสารไม่ใช่ RECEIVED และเป็นรายการคืนสินค้าจริง; แผนก 12, 22, 28 เลือกได้ทุกสาขา ส่วนแผนก 20, 23 เลือกได้เมื่อสาขาที่เลือกมี isMain = false เท่านั้น หรือเป็น user-code override ของ B1; ไม่บังคับว่าเป็นเจ้าของเอกสาร |
| ช่องตรวจรับของคลัง | 8, 9, 12, 22, 28, 29; Q1/Q2/Q3 และอุณหภูมิจะอ่านอย่างเดียวหลัง RECEIVED |
| เพิ่ม/ลบรูปหลักฐาน | เอกสารถูกสร้างแล้วและยังไม่ใช่ RECEIVED |
| ส่ง B1 | เอกสารถูกสร้างแล้ว, สถานะเป็น NEW หรือ INVUPDATE, ไม่ติดเงื่อนไขบล็อกจาก QC ของคลังฝาก, ผู้ใช้เป็น 8, 9, 12, 20, 22, 23, 28, 29 หรือ user-code override ของ B1, ไม่มี datetimeToB1, เลือก Lot ครบทุกรายการคืนสินค้า และมี Q1/Q2/Q3 อย่างน้อยหนึ่งข้อเมื่อมีการคืนสินค้าจริง |
| ลบเอกสารจากรายการ | ห้ามลบเมื่อเป็น RECEIVED, APPROVED หรือ APPROVE; นอกนั้นแผนกยกเลิก -2, 1, 2, 4, 12, 22, 28 ลบได้, สถานะ DRAFT/NEW เจ้าของลบได้, และสถานะ WAITINGCN/INVUPDATE ลบได้ ต้องยืนยันก่อนลบทุกครั้ง |
เมื่อ API ล้มเหลวและแสดง Global Alert กด คัดลอกปัญหาส่งทีม IT เพื่อคัดลอกผลตอบกลับจาก backend พร้อม URL/query/payload และ usercode/deptCode ของผู้ส่งคำขอสำหรับตรวจสอบปัญหา ดู การคัดลอก API error
แผนที่ API
| การทำงาน | API / service |
|---|---|
| รายการใบลดหนี้ | GET /api/credit-notes ผ่าน getCreditNotes พร้อม scope สาขา/คลัง และตัวเลือก status, ค้นหา, isReturn |
| รายละเอียดใบลดหนี้ | GET /api/credit-notes/{creditNoteId} |
| ใบแจ้งหนี้ต้นทางและ Lot | GET /api/invoices/{invoiceId}?includeLot=true |
| สร้าง/แก้ไขหัวเอกสาร | POST /api/credit-notes; PATCH /api/credit-notes/{creditNoteId} |
| สร้าง/แก้ไข/ลบรายการ | /api/credit-notes/{creditNoteId}/items |
| บันทึก Lot ที่เลือก | PATCH /api/credit-notes/{creditNoteId}/items/{itemId}/lots พร้อม lots: [{ lotNo, qty }] |
| รูปหลักฐาน | endpoint สำหรับอัปโหลด/ลบไฟล์ของใบลดหนี้ |
| ผลตรวจ QC | PATCH /api/credit-notes/{id}/qc พร้อม qcPassed และ qcRemark แล้ว GET รายละเอียดใหม่ |
| B1 | POST /api/credit-notes/{creditNoteId}/to-b1 แล้ว PATCH หัวเอกสารเป็น RECEIVED |
ความสอดคล้องของ Mobile และ POS Terminal
ทั้งสองแอปใช้ Lot จากใบแจ้งหนี้ การตรวจรายการต้นทาง การเปลี่ยนสถานะ เงื่อนไขส่ง B1 และสิทธิแผนก/user-code ชุดเดียวกัน Mobile ใช้หน้าจอแบบ stack และ modal ส่วน POS Terminal ใช้ CreditNoteDetailForm ใน split pane โดย POS Terminal เก็บ sourceInvoiceItems ไว้เมื่อเปิดใบคืนสินค้าเดิม เพื่อให้ตัวเลือก Lot ใช้ข้อมูลใบแจ้งหนี้เดียวกัน
ไฟล์ที่เกี่ยวข้องและเกณฑ์ตรวจสอบ
| ส่วนรับผิดชอบ | ไฟล์ |
|---|---|
เงื่อนไขสถานะส่ง B1 (NEW / INVUPDATE) | packages/utils/src/creditNoteB1.ts |
| สถานะ QC คลังฝากและตรวจ payload | packages/utils/src/creditNoteQc.ts |
| Modal QC ร่วม ป้องกันกดซ้ำ และโหลดข้อมูลซ้ำ | packages/rn-ui/src/CreditNoteQcModal.tsx |
| ฟอร์ม / API ของ Mobile | apps/mobile/src/screens/creditNoteStack/creditNoteDetailScreen/index.tsx, apps/mobile/src/services/creditNotes.ts |
| ฟอร์ม / API ของ Terminal | apps/pos-terminal/src/screens/HomeScreen/CreditNoteDetailForm.tsx, apps/pos-terminal/src/services/creditNotes.ts |
| สีรายการ Mobile | apps/mobile/src/screens/creditNoteStack/helpers.ts, apps/mobile/src/ui/colors.ts |
| สีรายการ Terminal | apps/pos-terminal/src/screens/HomeScreen/ListPane.tsx, apps/pos-terminal/src/screens/HomeScreen/config.ts |
รันจาก repository root:
yarn typecheck
yarn workspace @bsr/mobile jest --runInBand --watchman=false __tests__/creditNoteQc.test.ts __tests__/creditNoteQcModal.test.tsx __tests__/creditNoteQcTerminal.test.tsx
- ตรวจปุ่มส่ง B1 ทั้งสองประเภทคลังทุกสถานะ รวมสถานะไม่รู้จัก/ไม่ระบุ ต้องแสดงเฉพาะ
NEWและINVUPDATEตามสิทธิ์เดิม ทั้งปุ่มและ handler ใช้เงื่อนไขเดียวกัน - คลังฝาก
WAITINGCNที่ยังไม่ตรวจ QC ต้องแสดงตรวจ QC และซ่อนบันทึก/ส่ง B1 เมื่อผ่านต้องโหลดรายละเอียดใหม่ ถ้ายังเป็นWAITINGCNให้บันทึกตาม flow คลังเป็นINVUPDATEก่อนแสดงส่ง B1 เมื่อไม่ผ่านฟอร์มต้องถูกล็อกและแสดงปุ่มแดงที่กดไม่ได้ - คลังหลักไม่เข้า QC และไม่ถูกล็อกด้วยผล QC เมื่อกดส่ง B1 ยังคงตรวจ Lot, Q1/Q2/Q3 และ
datetimeToB1เดิม - ตรวจเหตุผลบังคับเฉพาะไม่ผ่าน การเก็บเหตุผลและข้อความ error การกดผ่าน/ไม่ผ่านซ้ำอย่างรวดเร็ว การโหลดใหม่โดยไม่ส่ง PATCH ซ้ำ และการไม่ใช้ผลโหลดเก่าหลังออกจากเอกสารนั้น
- ตรวจปุ่ม QC/บันทึก/B1 ทั้ง
__DEV__ = trueและfalseโดยทั้งสองโหมดต้องไม่ข้ามเงื่อนไขธุรกิจ - ตรวจสีการ์ดในหน้ารายการแยกจากปุ่ม QC ไม่ผ่านในหน้ารายละเอียด
ผลตรวจหลังคืนเงื่อนไขและเปลี่ยนชื่อปุ่ม (6442b189): alertResponseCopy.test.tsx, creditNoteQcTerminal.test.tsx, creditNoteQc.test.ts ผ่านรวม 114 กรณี พร้อม TypeScript ของ Mobile/Terminal และ build Wiki สองภาษา การส่ง API ในเทสต์ใช้ mock