← Back to Posable Office

Return of a POS (POPC) Sale in Back-Office — Plan v3

The plan now only keeps the register empty on Back-Office (BO) documents and makes sure nothing breaks because of that. It then answers your 5 questions: what happens in POS (POPC), the End Shift and cash, how the credit invoice looks in POS (POPC), and which store the credit invoice belongs to. The server fix for the empty register is done (not deployed). The rules and the POS (POPC) app change are not built yet.

13 to 14 Oct 2026 · Branch Redy-20261005-Sales-Return · Company OHY33941 (company id 12), dev data · REAL = read from your database or code. EXAMPLE = made up to explain. NOT CHECKED = I did not verify it.

Sections 1  What we fix: the register stays empty 2  What happens in POS (POPC) when POS sells and BO returns 3  End Shift and cash: should BO block these returns? 4  How the credit invoice looks in POS (POPC) 5  Which store the credit invoice belongs to 6  Everything affected, and the work plan 7  Risks and what is not checked 8  Decisions (decided 14 Oct 2026)
What this plan does, in short
  1. The problem. BO can now return sales that were made at POS (POPC). The credit invoice BO writes has no register and no shift. This broke the POS (POPC) sync (error 500), and the POS (POPC) app can crash when you open that credit invoice.
  2. Server: accept an empty register (done, not deployed). The sync accepts a document with no register.
  3. POS (POPC) app: no crash, show "Back office" (to build). The sale preview shows POS : Back office and does not crash. Needs a new POS (POPC) build.
  4. BO: write the right link ids (to build). BO writes the right link ids, so POS (POPC) can connect the return to its sale and show the right quantity left. Without it a double refund is possible.
  5. One rule. BO cannot refund a POS (POPC) sale in Cash. A BO return is in no shift, so a cash refund makes End Shift short by that amount. Card, e-wallet and "No Refund From Store" are allowed, with a warning. If the store has no such method left (like SC0D), BO says "Return it at POS (POPC)".
  6. Store of the credit invoice (your decision, 14 Oct). The BO user can change the store on the Refund step. It starts on the original sale's store. Stock, refund methods and the document number follow the chosen store.
  7. Release gate. The switch that allows these returns is already ON in your code. The rule must be merged together with it.
  8. Not proven. Nothing was run on a POS (POPC) device. Receipt print, X-report, e-invoice and the Y and Z reports are not checked.

Decided on 14 Oct 2026: D1 to D7 use my picks (D4 is your choice). Nothing left to decide before the build, except the open checks in section 7. See section 8.

The short answers to your 5 points

1. Plan scope is only "register stays empty": the server accepts it, and the POS (POPC) app stops crashing on it. No fake register, no new data.

2. After BO returns a POS (POPC) sale, the POS (POPC) syncs the return and shows the sale as Returned. Found in your real data: two link ids written by BO do not match what POS (POPC) looks for, so POS (POPC) cannot connect the return to its sale. It should still show the right "left to return" quantity, but this must be fixed and tested (section 2).

3. Yes, it is better to block it. A cash refund in BO takes cash out of the drawer without the shift knowing, so End Shift will be short. At your store the only refund method BO can use is Cash, so blocking Cash means POS (POPC) sales are returned at POS (POPC). BO does not stop the return at the start: the user picks the items first and meets the Cash block at the refund step. Details in section 3.

4. The credit invoice shows in the POS (POPC) Sale History as a "Credit Invoice" with no register and no shift. Opening it can crash today. After the fix it should read "Back office" (section 4).

5. Today BO always gives the credit invoice the same store as the original sale. POS (POPC) does the opposite: it uses the store of the device doing the return. Both ways have effects on stock, sync and reports. Your decision (14 Oct): BO lets the user change the store, starting on the original store (section 5).

Release gate (found in review): the switch is already ON in the code

On 13 Oct 2026 you asked me to turn on BO returns of POS (POPC) sales. It is on in the working tree (SOURCES_RETURNABLE = (SOURCE_TYPE_BO, "POS") in sales_commit_repository.py:40) and is not committed. The rules R1 to R5 below are not built yet. If this branch is merged before them, BO can refund a POS (POPC) sale in Cash and End Shift is short. So R1 to R5 must go in the same merge as the switch (decision D6).

Reviews done on this page

13 Oct: two reviewers checked the page against the real code and your dev database. They found 1 critical problem and several high or medium ones. One was a code bug: the server fix for the register was incomplete (section 1).

14 Oct, after you dropped rule R2: one more reviewer found 4 high and 10 medium or low points. They were leftovers of R2, mockup details that did not match the real POS (POPC) code, and one missing piece of work on the BO return form. All are fixed below.

1What we fix: the register stays empty

The problem: After you returned a POS (POPC) sale in BO and synced, POS (POPC) showed this error:

POST /api/v1/sales/get-sales — 500 — 2 validation errors for SalesHeaderInfo: register_id Input should be a valid integer (input_value=None); register_code Input should be a valid string (input_value=None)
1. POS (POPC) asks"Give me the sales of my store."
→
2. Server findsPOS (POPC) sales and BO documents of that store (it holds back only drafts, rejected, voided and approved invoices).
→
3. Server checks each one"Every sale must name a register." The BO return names none.
→
4. The whole sync stopsOne document blocks all the others.

BO has no POS (POPC) device, so every BO document has an empty register and an empty shift. In your company that is 40 BO documents that sync (24 invoices, 12 orders, 4 returns). It is an old problem: your BO return was just the latest one inside the sync window.

What this plan changes

PartChangeState
Server (v2-sellable-api)register_id (and register_code on the header) become optional in six models in sales_model.py: the header, SalesDetailInfo, SalesItemTaxInfo, SalesPaymentInfo, SalesPromoDiscInfo and SalesParentChildInfo. The first fix covered only the header, so the sync would have failed again on the next list. Found in review and fixed.Done, not deployed · 3 tests pass
POS (POPC) appOpening a sale with no register must not crash. Show "Back office". sale_utils.dart:189 does registerId! then .first (called from sale_detail_controller.dart:222 and :818); sale_history_controller.dart:570-571 has the same pattern.To build · needs a new POS (POPC) build
BO dataNothing for the register. Register and shift stay empty on BO documents.No change

Why the register is not filled in: other brands do the same (Shopify, Square, Toast keep the device empty and use a "source" field; you already have one, source_type). A fake id would still crash POS (POPC). A real "BO_REG" register per store would cost one register row per store and may use up the plan's register limit. Both are kept in the box at the end of this page in case you want them.

2What happens in POS (POPC) when POS sells and BO returns

This is the real case from your data. The sale was made at POS (POPC) store SC0D (Pedro Lazada), register REG-01, shift 2807.

Step by step (REAL)

#WhoWhat happens
1POS (POPC)Sells 3 × BEV-004 by card: SI-8/10/261013122636417, RM 314.80, shift 2807.
2BO userReturns 2 units. BO writes the credit invoice CI-8/BO/261013052505369: store SC0D, no register, no shift, one payment of −RM 209.90 Cash.
3BOThe original sale changes to Returned. Its line shows qty 3, returned 2 (stored as −2).
4BOAt Complete Refund the 2 units go back into stock.
5POS (POPC) syncsIt receives the updated original sale and the new credit invoice. With the server fix there is no error.

What POS (POPC) then does

QuestionAnswerHow sure
Does the sync store the credit invoice?Yes. An empty register or shift does not stop it. It only fails if the offline sale id, document number, type, source type or date is empty, and BO fills those.Read in code (sales_http_sync.dart, sale_header_table.dart). Not run.
Does the original sale show as Returned?Yes, the newer server copy overwrites the old one.Read in code (sale_header_dao.dart:63-79). Not run.
Can the cashier return the same 2 units again at POS (POPC)?It should not be possible: POS (POPC) shows 1 left. It reads the returned quantity that came from the server (see the box below). If that quantity is not synced, the cashier could refund twice.Open risk — must be tested on a device
Does the credit invoice appear in Sale History?Yes (section 4).Read in code.
Found in your real data: two link ids do not match (REAL)

POS (POPC) joins a return to its sale using ids. I compared what BO wrote with what POS (POPC) looks for:

LinkWhat BO wroteWhat it should beMatch?
Return link row, "child" ide3c01777-…the credit invoice's own id 70d26d65-…No
Return link row, "parent" id5e095b52-…the original sale's id 5e095b52-…Yes
Credit invoice line, "refers to original line" ida733f460-…the original line's id b3f9f045-…No

What it means: POS (POPC) finds the returns of a sale by joining the link row's child id to a sale it has. That join finds nothing, so POS (POPC) cannot list the credit invoice under its sale. For "how many are left" it then falls back to the returned quantity on the sale line (sale_return_exchange_utils.dart:242-272), which BO did update (returned = −2). So the left-to-return quantity should still be right, but only by luck of the fallback.

Cause (found in review, read in code):

Fix (small, in BO): write the real ids into the link row and into the credit invoice lines.

Both ids must ship togetherIf only the link row is fixed, POS (POPC) starts to find the credit invoice under its sale and stops using the fallback. But the line lookup still finds nothing. The returned quantity then reads 0 and POS (POPC) shows "3 left" instead of 1. That allows a double refund. Test: sync a BO return and check POS (POPC) lists the credit invoice under its sale and shows 1 left.

The 4 BO returns already written keep their random ids. They rely on the fallback. One fix: a one-off update of those rows. I will not touch old data without your OK (decision D3).

3End Shift and cash: should BO block these returns?

Your question: End Shift will not tally if the BO return is paid in cash. Is it better not to let BO refund a POS (POPC) sale in cash? My answer: yes.

Why the shift does not tally (REAL: your shift 2807)

Assumption: a BO cash refund is paid from the POS (POPC) cash drawer. If your shop pays BO refunds from an office or petty-cash box, the drawer is not short. Please confirm.

End Shift adds up only documents that carry that shift's number. A BO return has none, so it is left out of every shift, and it is not taken off the original sale's shift. (Checked in the database functions, functions_pg.sql:1868-2465, and by running the system's own shift report on your shift.)

Shift 2807 (REG-01)Amount
Opening float (inferred: expected cash 1,105.95 − cash sales 1,005.95)100.00
Cash sales (4 sales)1,005.95
Card sales (1 sale)314.80
System expects cash in drawer1,105.95
BO cash refund (not in any shift)−209.90
Cashier counts (1,105.95 − 209.90)896.05 → short by 209.90

What happens in each case

Refund paid byDrawer cashCard / e-wallet totalNet sales and tax of the shift
Cash, from the drawerShort by the refundCorrectToo high
Card or e-wallet (on the machine / app)CorrectNot correct if compared with the machine (EXAMPLE: system 314.80, machine 104.90)Too high
"No Refund From Store"CorrectCorrectToo high
Same return made at POS (POPC)CorrectCorrectCorrect

Also: the original sale's shift discount line changes sign once the sale becomes Returned (functions_pg.sql:2287-2299), even on a closed shift. And the BO business-day total leaves BO returns out (business_day_repository.py:176-186). Both are report figures, not cash.

What BO can refund with, at your store (REAL, store SC0D)

MethodEnabled for refund at SC0D?Can BO use it?
CashYesYes
Credit Note (store credit)YesNo — the server refuses store credit and gift card (sales_commit_service.py:1747-1757)
Credit / Debit Card, E-wallet PaymentNoNo (not enabled at this store)
Gift CardNoNo
So at SC0D, BO can only refund in Cash. And the POS (POPC) sales in your company are mostly cash: of 1,000+ POS (POPC) sales in company 12, 882 took Cash (plus 13 written as "CASH"), 112 Gift Card, 42 Card, 40 Credit Note, 7 E-wallet (REAL; 15 sales have cash plus another tender). Blocking cash for POS (POPC) sales therefore means these returns are done at POS (POPC), where the refund lands on the right shift.

Proposed rules

  1. R1 — BO cannot refund a POS (POPC) sale in Cash. The cash must be paid at POS (POPC) so the shift's cash count stays right. This is the part that keeps End Shift right. It applies whatever the sale was originally paid with.
  2. R2 — Dropped (your decision, 14 Oct 2026). BO will not stop the return at the start. The user picks the items first and meets the Cash block at the refund step. The number R2 stays unused so the other rule numbers do not change.
  3. R3 — Any other refund method the store allows is allowed for a POS (POPC) sale (card, e-wallet, "No Refund From Store"), with a warning on the refund step that the shift's card or e-wallet total will not show it. This includes a sale that was paid in cash and is refunded by card. That does not touch the cash drawer.
  4. R4 — The server checks every rule too. One place does it: _price_return (sales_commit_service.py:1682). Create, edit, calculate, and the re-check at submit, approve and Complete Refund all go through it. A Cash refund of a POS (POPC) sale is refused with the existing reason REFUND_METHOD_NOT_ALLOWED and its own plain message: "Cash refunds of POS (POPC) sales are paid at POS (POPC)."
  5. R5 — A sale with no usable refund method gets a clear message. Example: any POS (POPC) sale at a store where Cash is the only refund method BO can use, like SC0D (every POS (POPC) sale at SC0D ends here, whatever it was paid with). R5 depends only on the store's refund methods, not on how the sale was paid. The server cannot say "no method left" (it only sees the method the user chose), so the return form shows it. See "Work on the return form" below. Text: "This sale cannot be refunded in Back-Office. Return it at POS (POPC)." Gift Card sales are already refused today (sales_commit_service.py:1739-1744).

How the rules work in detail (added after review)

QuestionRule
Which refund methods does BO list for a POS (POPC) sale?The ones the store has enabled, minus Cash (R1) and minus the ones the server already refuses (store credit, gift card). If none is left, the user gets the R5 message.
Work on the return form (found in review)Today, once items are picked, the Items step already prices the refund with a preview method: a non-cash one if the store has it, otherwise the first one (SalesReturnFormPage.tsx:327-339). At SC0D that is Cash, so with R1 the server would refuse the preview one step early. So the form must (1) leave Cash out of the method list for a POS (POPC) sale (SalesReturnFormPage.tsx:223-231); (2) when the list is empty, skip the preview and show the R5 message on the Refund step; (3) the Items step then shows no refund amounts for such a sale, and the message says why. Decide the exact wording of that Items-step hint when building.
Which sales does this apply to?Every sale that is not made in BO (not only the text "POS"). Company 12 also has 1 sale with source "POPC" and 15 rows with an old document type. They cannot be returned today, but the rule should not depend on the exact word.
Returns already open before the rule?A Draft or For Approval return of a POS (POPC) sale with a Cash refund will be stopped at the next submit or approve, and it keeps holding its quantity. The message must say "Void this return" (void exists). In dev there are 0 such returns. Production: not checked. Run /tmp/open_returns.sql before deploying.

Sample screens (EXAMPLE)

Refund — return of SI-8/10/261013115346074 (POS (POPC) sale at SC0D)
Refund TotalRM 105.00
How is it paid back?nothing available
This sale cannot be refunded in Back-Office. Return it at POS (POPC) so the shift's cash count stays right.
Shown on the refund step, after the user picked the items (R1 and R5). Plain words, says what to do next. No codes.
Refund — return of a POS (POPC) sale paid by card (EXAMPLE: a store where card refund is enabled)
Refund TotalRM 209.90
How is it paid back?Credit / Debit Card ▾
At SC0D card refund is not enabled, so the real sale SI-8/10/261013122636417 could not be returned in BO under these rules (R5). Cash is not listed for a POS (POPC) sale. Refund the card on the machine first, then type its reference. This refund will not show in the POS (POPC) shift's card total.

4How the credit invoice looks in POS (POPC)

What I could read in the POS (POPC) code (not run on a device):

ScreenWhat the user seesBreaks?
Sale History, tabs All / Normal / ReturnThe credit invoice is listed, with document type "Credit Invoice" (sale_history_controller.dart:330-395, sale_constants.dart:178-246)No
Sale History, "this shift" tabNot listed: it has no shift (sale_header_dao.dart:242-258)Hidden, by design
Sale History, search by registerNot listed (sale_header_dao.dart:278-285)Hidden
A row in the listNo visible mark. The "other shift" check (list_sale.dart:354-355) only disables "Change cashier", which a credit invoice never showsNo
Opening the sale detailLooks up the register with registerId! and .firstCrash risk (sale_utils.dart:189)
Detail: "different register / shift" flagsTreated as a different register and shift (sale_detail_controller.dart:230, 799, 817, 936)Logic only; how the screen uses it: NOT CHECKED
Receipt reprint, X-report, payment line, total signNOT CHECKED—

The POS (POPC) does not treat a credit invoice from BO differently from one made at POS (POPC) in these list and filter queries (checked only in sale_history_controller.dart and sale_header_dao.dart).

POS (POPC) screen mockups for an empty register (EXAMPLE, copied from the real screens)

I rebuilt these from the real POS (POPC) code, not from my own idea. The Sale History screen has two parts: a list on the left and a receipt-style preview on the right (list_sale.dart, sale_detail_view.dart). The register is shown only in the preview, as the line "POS : name" (the label text is "POS": PRINT_POS_INFO.REGISTER_NAME in en_US.json). The list does not show the register at all.

Today

List row (real layout)

Date
Time
Check no
Doc type
Status
Cashier#RefAmount
13 Oct 2026
12:26
SI-261013122636417
Sale Invoice
Returned
AnaSI-8/10/261013122636417RM 314.80
13 Oct 2026
13:25
CI-261013052505369
Credit Invoice
BO user (blank if not on this device)CI-8/BO/261013052505369RM 209.90
No register on the list, so nothing breaks here. Column order is approximate and the list has no Qty column in the All and Return tabs: I read the code, not a screenshot.

Preview after tapping the credit invoice

REFUND
Check No : CI-261013052505369
POS : the screen fails before this line
The app looks the register up by id, gets nothing, and the preview stops loading at that step (sale_utils.dart:189). What the screen then shows is NOT CHECKED.

After the fix — option A (my pick): only the preview line changes

REFUND
Check No : CI-261013052505369
POS : Back office
Date : 13 Oct 2026 13:25
#Ref : CI-8/BO/261013052505369
Ori #Ref : SI-8/10/261013122636417 (only after Step 3)
Cashier : name of the BO user
Customer : Walk-In
Same lines as a normal receipt. Only the "POS" value is new. "Ori #Ref" is drawn from the return link row, so it shows only once the link ids are fixed (Step 3). The 4 BO returns already written, including this one, show no "Ori #Ref" unless you choose to update the old rows (D3). BO writes its own user as cashier, so that name shows (sales_commit_service.py:2220-2222). The time is an example (the number in the document name holds 05:25, probably UTC). The items, payment and totals below it are unchanged.

After the fix — option B: also mark the list row

Date
Time
Check no
Doc type
Status
Cashier#RefAmount
13 Oct 2026
13:25
CI-261013052505369
Credit Invoice
Back office
BO user (blank if not on this device)CI-8/BO/261013052505369RM 209.90
One small tag under the document type. Not needed to fix the crash. It makes a Back-Office document easy to spot in the list.
Wording and files

"Back office" needs a new key in assets/languages/en_US.json (for example SALES.BACK_OFFICE), added in the same change. Other languages only if a verified translation exists. Screens touched: sale_utils.dart:189 (lookup returns nothing instead of crashing), sale_detail_controller.dart:229 (sets the name), :817-827 and :937-941 (Return / Edit messages that name the register), sale_history_controller.dart:571. Option B also list_sale.dart.

NOT CHECKED: a real screenshot (I read the code only), receipt print and reprint, and what the Return and Edit buttons show for a BO credit invoice.

5Which store the credit invoice belongs to

You pointed out that the real return happens in BO, possibly for a store other than the one that made the sale. The credit invoice (CI) carries a store, and the stock, the sync and the reports all follow it.

What happens today (REAL)

CICI storeOriginal saleOriginal store
CI-8/BO/261013052505369SC0DSI-8/10/261013122636417SC0D
CI-994/BO/2610080551223587MOMSI-994/BO/2610070353580127MOM

BO has no store field on the return form. It copies the original sale's store (sales_commit_service.py:2182-2192), checks that the user holds that store (:1709), and checks that store's stock location and its refund methods (:1748). POS (POPC) does the opposite: the server overwrites a return's store with the store of the device that makes it (sales_service.py:724-726).

Example (EXAMPLE)

The sale was made at S1 = Pedro Lazada (SC0D). The customer brings the goods to S2 = Sellable KL Sentral (RYS-KL), and a BO user returns it.

A — CI always belongs to the original store (S1) Today

Stock goes back to S1's default location, though the goods are at S2.

Refund uses S1's refund methods and is booked to S1.

Sync: S1 devices get the CI. S2 devices never get it: with the setting IS_ALLOW_EXCHANGE_REFUND_OTHER_STORE on, a device also gets credit invoices whose original sale is in its store, and here that is S1.

Reports: the return sits under S1.

Work: none. Every check already uses S1.

B — CI belongs to the store chosen in BO (S2) Your choice, 14 Oct

Stock goes to S2's default location (the database function follows the CI's store, functions_pg.sql:3629-3660).

Refund uses S2's refund methods, booked to S2.

Sync: S2 devices get the CI. S1 devices only with that setting on.

Reports: the return moves to S2 in sales lists, credit-note report and e-invoice. S1's totals no longer show it.

Work: see "What the store picker needs" below.

The two systems disagreeA return of the same sale at another store ends up in different places depending on where it is made: POS (POPC) puts the CI in the device's store, BO puts it in the original store. Your decision: BO lets the user choose the store, starting from the original store. POS (POPC) keeps using the store of the device.

Your decision (14 Oct 2026): the user can change the store, and it starts on the original

  1. Default: the Refund step shows "Return to store" already filled with the store of the original sale. A user who changes nothing gets today's result.
  2. Change: the user may pick another store. Only stores the user has access to are listed. The user must still have access to the original store (as today).
  3. Everything follows the chosen store: the refund methods the store allows, the rule R1 and R5 (they use that store's methods), the stock location, the credit invoice's store, and the store id in the document number.
  4. Stores without a default stock location cannot be picked for a return that puts goods back in stock. The user sees a plain message.
  5. Who may change it: anyone who can create a return and has access to that store. No new permission (decision D8).
Refund — return of SI-8/10/261013122636417 (EXAMPLE)
Return to storePedro Lazada (SC0D) ▾
Starts on the store that made the sale. Change it if the goods come back to another store. Stock and the refund methods follow this store.
Return to storeSellable KL Sentral (RYS-KL) ▾
Different from the store that made the sale (Pedro Lazada). The goods go back into stock at Sellable KL Sentral.
How is it paid back?methods of the chosen store ▾

What the store picker needs (REAL places in the code)

PartChange
RequestAdd an optional store_id to ReturnCalculateRequest (models/sales_creation.py:254-261). Empty means the original store. The create request inherits it.
Price check codeThe code that proves the quoted figures (calculation_token, sales_commit_service.py:243-262) must cover the chosen store, so a store cannot be swapped after pricing.
Access checkKeep the check on the original store (:1709). Add the same check on the chosen store.
Pricing and refund method_price_return reads the store settings (:1728) and the refund method (:1749) of the chosen store. R1 and R5 use the same store.
CreateStock location check (:2152), document number and the store written on the header (:2181-2193) use the chosen store. The replay check (:2132-2146) already reads the stored header.
Edit, submit, approveThe edit path (:2573) and the checks that read the header's own store (:2323, :2411) already follow the header. Check both with a test.
Complete RefundThe stock location check at :2630 must use the header's store, not the original's. Stock then goes to the chosen store (database function functions_pg.sql:3629-3660).
Return formNew "Return to store" field on the Refund step (SalesReturnFormPage.tsx). The method list (useStorePaymentMethods) reads the chosen store. Changing the store clears the chosen method.
TestsDefault = original store; changed store changes stock location, methods and document number; a user without access to the chosen store is refused; a token from another store is refused.
What this changes for the rest of the plan

R5 gets easier, but only when the goods really move. If the original store has Cash as its only refund method (like SC0D), the user can pick a store that has card refund enabled. They must do this only when the customer really returns the goods there. Cash is still blocked (R1).

POS (POPC) devices: the device of the chosen store receives the credit invoice. The device of the original store sees its sale as Returned (the returned quantity syncs), but not the credit invoice, unless the setting IS_ALLOW_EXCHANGE_REFUND_OTHER_STORE is on.

Reports: the return moves to the chosen store in the sales list, the credit-note report and e-invoice. The original store's totals no longer show it. End Shift: unchanged, a BO return is in no shift.

Risk: a user can book stock into the wrong store. The red hint above the field is there for that.

The return form has no store field today (models/sales_creation.py:254-273). NOT CHECKED: whether the credit-note report's store equals the CI header store; the e-invoice store mapping end to end; the full End-of-day roll-up; every database trigger.

6Everything affected, and the work plan

AreaWhat changesRisk
POS sync (get-sales)Accepts an empty register (done, not deployed)Low
POS (POPC) app: Sale History detailNo crash on an empty register; show "Back office"Medium — new build, test on a device
BO: link ids of a returnWrite the real ids so POS (POPC) can connect the CI to its saleMedium — check on a synced device
BO: refund rules R1 to R5 for POS (POPC) salesServer check, plain message, no Cash for a POS (POPC) sale, and changes on the return form (method list, empty list, Items step without figures)Medium
End ShiftNo change to the maths. R1 stops the case that breaks the cash count.Card and net sales stay off
Store of the CIStore picker on the return form, default = original store; server uses the chosen store for access, methods, stock and document number (section 5)Medium — touches create, edit, approve and Complete Refund
Old dataNo change unless you say yes to D3. The 4 BO returns already written keep random link ids and rely on the fallback.Low
BO: return after a POS (POPC) returnReturns made at POS (POPC) are not matched to the original line in BO (all 337 lines have no reference). The quantity left is still right, but the "last return takes the rest" rule and the serial check can miss them. Cash refunds are refused (R1), but any POS (POPC) sale refunded by card, e-wallet or "No Refund From Store" can still hit it.Medium
E-invoice (LHDN)A BO credit invoice of a POS (POPC) sale is new. Not checked.Decide before release

Order of work

  1. Step 1 — Server. Deploy the optional register in v2-sellable-api (all six models). The sync works again. Can go first and alone. Add a test that runs one BO document through the whole get-sales answer.
  2. Step 2 — BO rules R1 to R5. Backend check in _price_return; return form changes (see "Work on the return form"); screen message and hint. Tests: Cash refund refused on a POS (POPC) sale, whatever the sale was paid with; card or "No Refund From Store" allowed where the store enables it; a store whose only method is Cash gets the R5 message and no preview call; BO-made sale unchanged. Must be in the same merge as the switch (D6).
  3. Step 2b — Store picker. The work listed in section 5, "What the store picker needs". It can ship after the release gate (D6): until then the credit invoice uses the original store. Test every row of that table.
  4. Step 3 — BO link ids. Write the real ids into the link row and the credit invoice lines, in one change. Test: the ids match what POS (POPC) looks for.
  5. Step 4 — POS (POPC) app. Safe register lookup, "Back office" label. New build.
  6. Step 5 — Check on a device. After the build: use a store that has card refund enabled and return a POS (POPC) sale there with a card refund in BO; confirm the CI shows in Sale History and opens, the original shows Returned with 1 left, and the cashier cannot return the 2 units again. Run End Shift and confirm the drawer cash tallies and that the card total is off by the refund, as R3 warns.

Rollback: every step is code only. Redeploy the previous build. Returns written with the new ids stay valid.

7Risks and what is not checked

8Decisions (decided 14 Oct 2026)

All decided. The last column is the final answer. D1 to D3 and D5 to D8 use my picks. D4 is your choice (store picker). R2 was dropped by you.
#DecisionOption AOption BDecided
D1No Cash refund in BO for a POS (POPC) sale (R1)Yes — cash refunds are done at POS (POPC)No — accept the shortageYes
D2Card, e-wallet or "No Refund From Store" refunds of a POS (POPC) sale (R3)Allow with a warning, if the store enables the methodBlock tooAllow with a warning, unless your shift compares the card total with the machine
D3Fix the link ids so POS (POPC) can connect a return to its saleNew returns only (old returns, including the real one in section 2, show no "Ori #Ref")New returns and the 4 old rows (needs your OK)New returns now, old rows later
D4Store of the credit invoiceOriginal store only (today)Store chosen in BO, picker starts on the originalB — decided by you on 14 Oct
D8Who may change the storeAnyone who can create a return and has access to that storeA new permission just for thisNo new permission
D5Show BO returns on the shift reportYes, laterNoLater
D6Release gate: when may BO returns of POS (POPC) sales go live?Only in the same merge as R1 to R5Now, rules laterSame merge
D7A POS (POPC) sale at a store whose only refund method is Cash (like SC0D)Tell the user to return it at POS (POPC) (R5)Turn on card refund for the store in settingsR5 message
Options for the register that were considered and not chosen

A — a real "BO_REG" register in each store. Works with no POS (POPC) code change, but needs one register row per store (and for each new store and company), may count against the plan's register limit, may be picked by a cashier, and old BO documents need a database update.

B — a fake register id (0) with no register row. Still crashes POS (POPC): it looks the register up by id and takes the first result of an empty list.

"Code only, no id" does not work: the server needs a number for the id too.

What other brands do: Shopify, Square and Toast keep the device empty and add a "source" field. Lightspeed X-Series requires a register and uses a real register made for web shops (extra registers can cost money). Odoo and Microsoft Dynamics keep back-office orders apart from POS sessions, and a POS refund is written on the POS's own session or terminal.

Source of this page: apps/v2-sellable-bo/docs/modules/sales-return/till-sale-return-plan.html. Facts checked against: db-postgresql/tenants.schema/functions_pg.sql, apps/v2-sellable-api/src/modules/sales/sales_query.py, sales_model.py, sales_service.py, apps/v2-sellable-bo/backend/app/services/sales_commit_service.py, business_day_repository.py, apps/v2-pos-client/client/lib/… (sale_utils, sale_history, sale_detail, sync) and your dev database (read only).