ใบเบิกยืม
ใบเบิกยืมเป็น flow สำหรับเบิกสินค้า/ปลาออกไปก่อนเข้าสู่การขายปกติ จึงคล้ายใบสั่งขายตรงที่เริ่มจากลูกค้า สาขา/คลัง รายการสินค้า จำนวน และภายหลังสามารถแปลงเป็นใบสั่งขายได้ จุดที่ต่างคือประเภทการเบิก เงื่อนไขวันที่มาเบิก/ทะเบียนรถของ SPEC ในสาขาย่อย จำนวนที่คาดว่าจะซื้อ การรับงาน/ตัดสต๊อก การ retry เมื่อ sub-borrow ล้มเหลว และการ convert ไปใบสั่งขาย
จุดเข้าใช้งาน
| แอป | หน้าจอ |
|---|---|
| Mobile | BorrowList -> BorrowDetail |
| POS Terminal | borrowFish -> BorrowDetailForm ใน HomeScreen และรายการใบเบิก/workspace ของ StorefrontPosStack |
ประเภทการเบิก
| Type | ความหมาย |
|---|---|
SPEC | ให้ลูกค้าดู |
MARKET | เดินตลาด |
VIDEO | ถ่ายวีดีโอ |
SEMINAR | สัมมนา |
DESTROY | ทำลาย |
LAB | วิจัย |
ต้องเลือกประเภทก่อนเพิ่มสินค้า ถ้าเป็นสาขาย่อยและจะเปลี่ยนเป็น SPEC แต่มีสินค้าอยู่แล้ว ระบบจะบล็อกและให้ลบสินค้าออกก่อน เพราะ SPEC มีเงื่อนไขเพิ่มเรื่องวันที่มาเบิกและทะเบียนรถลูกค้า
สถานะเอกสาร
| สถานะ API | ข้อความใน UI | ความหมาย |
|---|---|---|
NEW | สร้างใหม่ | Draft แก้ไขได้ตามสิทธิ์เจ้าของเอกสาร |
WAITING | รอดำเนินการ | รอการดำเนินการ |
ACCEPTED | รับงานแล้ว | รับงานแล้ว โดย guard ของปุ่มตรวจ acceptedAt ด้วย |
DONE | เสร็จสิ้น | ดำเนินการเสร็จ แต่ยังมี retry ได้หาก sub-borrow ล้มเหลว |
PARTIAL_CONVERTED | แปลงและรอคืน | แปลงสินค้าบางรายการเป็นใบสั่งขายแล้ว และคืนรายการที่ยังมี lots ได้ |
CONVERTED | แปลงเป็นใบสั่งขายแล้ว | แปลงเป็นใบสั่งขายแล้ว |
CANCELED | ยกเลิก | ยกเลิกเอกสาร |
หลังทำรายการสำเร็จ แอป refresh สถานะจาก backend การแปลงเป็นใบสั่งขายต้องรับงานแล้วและผ่านสิทธิ์ด้านล่าง โดยไม่บังคับสถานะ DONE หลังเรียก /return-items แอปใช้สถานะที่ backend ส่งกลับมา ไม่กำหนดสถานะปลายทางเอง.
Flow การทำงาน
เงื่อนไขหัวเอกสาร
| หัวข้อ | พฤติกรรม |
|---|---|
| ลูกค้า | ใบเบิกเริ่มจากลูกค้าที่เลือก และเก็บ customer code/name |
| สาขาและกลุ่มคลัง | payload ส่ง branchId, whGrpCode, whGrpName จากสาขา/เอกสาร |
| ประเภทการเบิก | ต้องเลือกก่อนเพิ่มสินค้าและก่อน confirm |
| หมายเหตุ | เก็บใน notes และแก้ได้เมื่อ draft ยัง editable |
dateOfPickup | ส่งเฉพาะประเภท SPEC; ถ้าไม่ใช่ SPEC จะส่งเป็น null |
| ทะเบียนรถลูกค้า | สำหรับสาขาย่อย SPEC ต้องกรอกก่อนเพิ่มสินค้า |
| การล็อกวันที่มาเบิก | สาขาย่อย SPEC แก้วันที่ไม่ได้เมื่อมีสินค้าแล้ว |
| วันที่ย้อนหลัง | Mobile บล็อกวันที่มาเบิกที่น้อยกว่าวันปัจจุบัน |
เงื่อนไขสินค้าและจำนวน
กฎสำคัญของสินค้า:
| กฎ | สาขาหลัก | สาขาย่อย |
|---|---|---|
| จำนวนที่คาดว่าจะซื้อ | ไม่บังคับ payload เป็น 0/undefined | บังคับ และต้องมากกว่า 0 |
| จำนวนต่อบรรทัด | เฉพาะ SPEC (ให้ลูกค้าดู) สูงสุด 3 ชิ้น; ประเภทอื่นไม่มี limit ต่อบรรทัด | สูงสุด 5 ชิ้นทุกประเภท |
| จำนวนรายการสินค้า | เฉพาะ SPEC สูงสุด 3 รายการ; ประเภทอื่นไม่มี limit จำนวนรายการ | ไม่มี guard จำกัด 3 รายการใน UI |
SPEC วันที่/ทะเบียนรถ | ไม่ถูกบังคับด้วย branch guard นี้ | ต้องกรอกก่อนเพิ่มสินค้า |
| รูปแบบจำนวน | จำนวนเต็มบวกเท่านั้น | จำนวนเต็มบวกเท่านั้น |
| stock guard | ตอนเพิ่มตรวจ onHand - booking; ตอนแก้ใช้ stock ที่เหลือรวมจำนวนเดิมของ item นั้น | เหมือนกัน |
payload ของ item สร้างจาก product inventory เช่น product code/name, qty, expectedQty, price, amount, weight, warehouse, company, tax/VAT, UOM, weight-base flag, step-price flag และ editPriceBy = null
การแก้ไขสินค้าเดิม
การแก้ item ทำได้เฉพาะตอนเอกสารยังเป็น draft ที่แก้ได้ เมื่อแก้ ระบบโหลด inventory ปัจจุบันจาก product code แล้วคำนวณ maxEditableStock = availableStock + originalQty หากเป็นสินค้าเดิม เพื่อให้ผู้ใช้คงจำนวนเดิมหรือลดจำนวนได้ แต่ยังกันไม่ให้จำนวนสุดท้ายเกิน stock
สำหรับสาขาย่อย expectedQty ยังบังคับมากกว่า 0 และ limit จำนวนต่อบรรทัดยังคงเป็นไม่เกิน 5 ชิ้นทุกประเภท ส่วนสาขาหลักจำกัดไม่เกิน 3 ชิ้นเฉพาะ SPEC; ประเภทอื่นไม่จำกัดจำนวนต่อบรรทัด ทุกประเภทยังต้องเป็นจำนวนเต็มบวกและไม่เกิน stock ที่ใช้ได้
Product Picker และการแสดงสินค้ารายการแรก
- ถ้าใบเบิกมี
borrowIdแล้ว การเพิ่มสินค้าจะไม่แสดง loading แบบเต็มหน้าจอที่บังหน้า detail และ product picker จะยังเปิดอยู่หลังเพิ่มสำเร็จ เพื่อให้เลือกเพิ่มได้ต่อเนื่อง - Draft ใหม่ใช้การ persist สองช่วง: สร้าง header ของใบเบิกก่อน ใช้
borrowIdที่ API คืนกลับมาเป็น identity สำหรับสร้าง item แล้วจึงสร้างสินค้ารายการแรก borrowNoเป็นเลขเอกสารสำหรับแสดงผล ไม่ใช้แทนborrowIdในการสร้าง item- หลังสร้างสินค้ารายการแรก ระบบจะ refresh detail/list ของใบเบิก และ sync route ด้วย identity ที่ได้กลับมา ทำให้เห็น header และสินค้ารายการแรกทันที ไม่ต้องรอเพิ่มสินค้ารายการที่สอง
- Mobile และ POS Terminal ใช้พฤติกรรมการ persist เดียวกัน
Confirm, Accept, Complete, Retry
ตอน confirm ระบบบันทึกหัวเอกสารล่าสุดก่อนเรียก confirmBorrow แล้วล็อกการแก้ draft ตามสถานะ การรับงานจะบันทึก acceptedAt ปุ่มยืนยันคืน/ตัดสต๊อกยังต้องผ่านสิทธิ์ process รับงานแล้ว และมีรายการที่ดำเนินการได้
completeBorrowStock(borrowId, detail) เลือก API ตามลำดับเดียวกันบน Mobile, POS HomeScreen และ Storefront POS:
| ลำดับ | ข้อมูลปัจจุบัน | POST endpoint | Body |
|---|---|---|---|
| 1 | borrowStatus === "PARTIAL_CONVERTED" | /api/borrow/{id}/return-items | ID ของทุกรายการที่ยังมี lots |
| 2 | สถานะอื่นที่ดำเนินการได้ และ hasFailedSubBorrow | /api/borrow/{id}/retry | ไม่มี body |
| 3 | สถานะอื่นที่ดำเนินการได้ และไม่มี failed sub-borrow | /api/borrow/{id}/approve | ไม่มี body |
ตรวจ PARTIAL_CONVERTED ก่อนเสมอ แม้ hasFailedSubBorrow เป็น true และยังคืนได้แม้มี convertedOrderId/convertedAt หากเหลืออย่างน้อยหนึ่งรายการที่มี lots ส่วนสถานะอื่นใช้ guard เดิม: WAITING/ACCEPTED ที่รับงานแล้ว หรือ DONE ที่มี failed sub-borrow และต้องยังไม่ถูกแปลง
ตัวอย่าง: หลังแปลงรายการ 11 และ 12 จะมี lots ว่าง ส่วนรายการ 13 ยังมี lots เมื่อยืนยันคืนจะส่ง:
POST /api/borrow/{id}/return-items
{
"borrowItemIds": [13]
}
Lots มีอยู่แล้วในแต่ละรายการ จึงไม่มีขั้นตอนเลือก lots หรือเลือกรายการคืนเพิ่ม ระบบส่งทุกรายการที่ lots เป็น array ไม่ว่าง ตัดรายการที่แปลงแล้วออก และไม่ส่ง ID ซ้ำ หากไม่มีรายการเหลือหรือมีรายการที่จะคืนแต่ ID ไม่ถูกต้อง ระบบไม่ยิง API เมื่อ API ล้มเหลวจะแสดง error โดยไม่เปลี่ยนไปเรียก /approve หรือ /retry แทน เมื่อสำเร็จจะโหลดรายละเอียดและข้อมูลรายการ/workspace ที่เกี่ยวข้องใหม่.
แปลงเป็นใบสั่งขาย
ปุ่มแปลงเป็นใบสั่งขายต้องผ่านเงื่อนไขครบ:
| เงื่อนไข | ค่าที่ต้องเป็น |
|---|---|
| สาขา | สาขาหลัก (isMainBranch = true) |
| ประเภทใบเบิก | SPEC |
| บันทึกและรับงานแล้ว | มี borrowId และ acceptedAt โดยไม่บังคับสถานะ DONE |
| ยังไม่เคยแปลง | ไม่มี convertedOrderId หรือ convertedAt และไม่ใช่ CONVERTED หรือ PARTIAL_CONVERTED |
| ผู้ใช้ | เจ้าของเอกสาร หรือแผนก -2, 1, 2, 4, 7, 8, 9, 13, 16, 20, 21, 23, 29 |
เมื่อกดแปลง ระบบเปิด BorrowConversionModal ที่ใช้ร่วมกันบน Mobile, POS HomeScreen และ Storefront POS:
- ทุกครั้งที่เปิดเริ่มโดยยังไม่เลือกรายการ กดเลือกทีละรายการหรือ เลือกทั้งหมด ได้ กดเลือกทั้งหมดอีกครั้งเพื่อล้างการเลือก
- แต่ละรายการแสดงเฉพาะชื่อสินค้า รหัสสินค้า และจำนวน มี checkbox/เครื่องหมายถูกและขอบที่เปลี่ยนสีเพื่อบอกว่าเลือกแล้ว พร้อมจำนวนรายการที่เลือก
- ต้องเลือกอย่างน้อยหนึ่งรายการแล้วกด ยืนยัน ระบบตรวจว่ารายการที่เลือกยังอยู่ในข้อมูลปัจจุบัน และส่งเฉพาะ ID ของรายการใบเบิกที่เลือก:
POST /api/borrow/{id}/convert
{
"borrowItemIds": [11, 12]
}
Modal ใช้ขนาดตัวอักษรชื่อสินค้า 14.4 (ลด 10%), padding การ์ด 7 (ลด 50%), gap รายการ/การ์ด 6 และ gap ระหว่างบรรทัด 2 ระหว่างส่งข้อมูล ปุ่มแสดง กำลังแปลง… และล็อกการเลือก การปิด Modal และการส่งซ้ำ หาก API ล้มเหลวจะคงรายการที่เลือกไว้พร้อมแสดง error ใน Modal
เมื่อสำเร็จ ระบบโหลดรายละเอียดใบเบิกใหม่ รวมสถานะจาก backend และ convertedOrderId สถานะ PARTIAL_CONVERTED แสดงข้อความ แปลงและรอคืน ในหน้ารายการ ในหน้ารายละเอียด หากสถานะนี้และ item มี lots ว่าง ไม่ส่งมา หรือเป็น null จะถือว่าแปลงแล้วและแสดงพื้นหลังแดงอ่อน (rgba(239,68,68,0.16)) รายการที่ยังมี lots ใช้พื้นหลังปกติและเข้าสู่ flow คืนรายการที่เหลือ ส่วน lots ว่างในสถานะอื่นไม่เข้าเงื่อนไขพื้นหลังแดง เอกสารที่แปลงบางส่วนแล้วไม่สามารถกดแปลงซ้ำได้
ใน StorefrontPosStack ใบเบิกที่แปลงแล้วจะแสดงปุ่ม ไปที่ใบสั่งขาย ระบบโหลด getOrderDetail(convertedOrderId) ก่อนเปิด product workspace ใน order mode โดยยึดข้อมูลที่ persist ใน order เป็นหลักสำหรับสินค้า การรับ/จัดส่ง ที่อยู่ เส้นทาง พิกัด และ document flags จากนั้นจึงโหลด customer profile ด้วย customerCode เพื่อเติม tier, point, สิทธิประโยชน์ และที่อยู่ fallback หากโหลด profile ไม่ได้ ยังเปิด order จาก customer snapshot ที่บันทึกไว้ได้
ID ของรายการใบเบิก
ตัวสร้าง payload ทั้งสองตัวเลือกค่าแรกที่ไม่ใช่ null/undefined ตามลำดับ itemId → borrowItemId → borrowDetailId → id แปลง string ตัวเลขเป็น number และรับเฉพาะจำนวนเต็มบวกที่เป็น safe integer ไม่ใช้รหัสสินค้าหรือ ID ของหัวใบเบิกแทน ID รายการ การแปลงใบสั่งขายจะปฏิเสธการเลือกว่าง/รายการที่ไม่อยู่ในข้อมูลปัจจุบัน และตัด ID ซ้ำออก.
การพิมพ์
มี 2 เส้นทางพิมพ์:
| เอกสาร | พฤติกรรม |
|---|---|
| Borrow pack | พิมพ์เอกสารใบเบิกฝั่งลูกค้า/แพ็กสินค้า |
| Borrow storage | พิมพ์เอกสารใบเบิกฝั่งคลัง |
ทั้งสองปุ่มต้องมีสินค้าอย่างน้อย 1 รายการ สถานะ PARTIAL_CONVERTED ยังพิมพ์ได้ การพิมพ์ไม่เปลี่ยนสถานะเอกสารเอง
สิทธิ์และ Guard
| สิทธิ์ | กฎ |
|---|---|
| Terminal menu key | borrowFish |
| deptCode ที่เห็นเมนู | -2, 1, 2, 3, 4, 7, 29, 21, 8, 9, 12, 13, 14, 30, 16, 20, 22, 23, 27, 28 |
| สร้างใบเบิก | Admin หรือ dept -2, 1, 2, 4, 7, 8, 9, 13, 16, 21, 29 |
| ยกเลิก/ลบใบเบิก | Admin; เจ้าของเอกสารเมื่อสถานะ NEW; หรือแผนก/ผู้ใช้ที่อนุญาตเมื่อสถานะ NEW หรือ WAITING |
| แผนกยกเลิก | -2, 1, 2, 4, 9; dept 8 และ 22 override ได้ใน NEW/WAITING |
| user code override ยกเลิก | 008045, 012521, 012508, 008051, 012388, 012428, 008060 |
| B1 processing | Admin หรือ dept 2, 8, 12, 22, 28 |
| Owner processing | เจ้าของเอกสาร process ได้ถ้า borrow type ไม่ใช่ DESTROY |
| Processing guard | สาขาหลักที่มีสิทธิ์ B1 หรือเจ้าของเอกสารตามเงื่อนไขด้านบน |
| แก้ draft | สถานะต้องเป็น NEW และผู้ใช้เป็นเจ้าของ draft หรือเอกสารยังไม่ persist |
| แก้ประเภทการเบิก | ทำได้ก่อน persist หรือโดยเจ้าของเอกสาร |
Mobile vs POS Terminal
| หัวข้อ | Mobile | POS Terminal |
|---|---|---|
| การนำทาง | borrow stack list/detail | HomeScreen split-pane menu borrowFish |
| Product flow | borrow detail screen และ product-flow hook | borrow detail form, panels, product picker |
| บันทึกหัวเอกสาร | บันทึกผ่าน borrow service เมื่อ detail เดิมเปลี่ยน | บันทึก raw detail ผ่าน HomeScreen handlers |
| Validation | business rules เดียวกันใน mobile components | business rules เดียวกันใน HomeScreen handlers |
| พิมพ์ | printBorrowPack, printBorrowStorage | ใช้ print utilities เดียวกัน |
| Convert | Modal เลือกสินค้าร่วมกันและส่ง borrowItemIds ที่เลือก | HomeScreen และ Storefront POS ใช้ Modal/contract เดียวกัน; Storefront เปิด convertedOrderId ใน order mode ได้ |
| คืนหลังแปลงบางส่วน | completeBorrowStock ส่ง ID รายการที่เหลือไป /return-items | ทั้งสอง flow ของ Terminal ใช้การเลือก API และกรอง lots เหมือนกัน |
API และ Data Map
| Operation | Service |
|---|---|
| List | getBorrows |
| Detail | getBorrowDetail(borrowId) |
| Create/update/delete header | createBorrow, updateBorrow, deleteBorrow |
| Item CRUD | createBorrowItem, updateBorrowItem, deleteBorrowItem |
| Confirm | confirmBorrow |
| Accept | acceptBorrow |
| Complete/cut stock dispatcher | completeBorrowStock(borrowId, detail) |
| Normal completion | approveBorrow |
| Return remaining items | returnBorrowItems(borrowId, payload) |
| Retry failed sub-borrow | retryBorrow |
| Convert to sales order | convertBorrow(borrowId, payload) |
Developer Handoff Map
| ส่วน | โค้ด |
|---|---|
| Mobile permissions/helpers | apps/mobile/src/utils/borrow.ts |
| Mobile list/detail | apps/mobile/src/screens/borrowStack/borrowListScreen, borrowDetailScreen |
| Mobile product flow | apps/mobile/src/screens/borrowStack/borrowDetailScreen/useBorrowProductFlow.ts |
| Terminal permissions/panels | apps/pos-terminal/src/screens/HomeScreen/BorrowPanels.tsx |
| Terminal detail | apps/pos-terminal/src/screens/HomeScreen/BorrowDetailForm.tsx |
| Terminal handlers | apps/pos-terminal/src/screens/HomeScreen/index.tsx |
| Shared conversion UI | packages/rn-ui/src/BorrowConversionModal.tsx |
| Shared guards, status, item IDs and payloads | packages/utils/src/borrow.ts |
| Payload types | packages/types/src/borrow.ts |
| Storefront flow | apps/pos-terminal/src/screens/StorefrontPosStack/borrowFlow.ts |
| Mobile/shared regression tests | apps/mobile/__tests__/borrowConversionModal.test.tsx, borrowConversionService.test.ts, borrowPartialConversion.test.ts, borrowReturnItemsService.test.ts |
| Storefront regression tests | apps/pos-terminal/src/screens/StorefrontPosStack/__tests__/borrowFlow.test.ts |
| Services | apps/*/src/services/borrow.ts |
| Converted target | เอกสารใบสั่งขายและ order services |