Skip to content

fix(offers): apply transaction-level promotional schemes in POS#257

Merged
engahmed1190 merged 4 commits into
developfrom
fix/transaction-level-promotional-schemes
May 13, 2026
Merged

fix(offers): apply transaction-level promotional schemes in POS#257
engahmed1190 merged 4 commits into
developfrom
fix/transaction-level-promotional-schemes

Conversation

@engahmed1190
Copy link
Copy Markdown
Contributor

Summary

  • Fix: Promotional Schemes with Apply On = Entire Transaction (e.g. "spend X get free product Y") were silently ignored in POS. pos_next.api.invoices.apply_offers() only called ERPNext's per-item engine (apply_pricing_rule) and never the transaction-level engine (apply_pricing_rule_on_transaction), so transaction-scoped Promotional Schemes never fired regardless of cart contents.
  • Now: apply_offers() also builds a transient (never-saved) Sales Invoice and runs ERPNext's transaction-level engine on it, merging any free-item rows into the existing free_items response under the same (item_code, pricing_rule) dedup key as the per-item path.
  • Print template: the POS Next Receipt Jinja template now annotates is_free_item rows with a green (FREE) badge and renders the rate column as qty × FREE / IQD 0.00, matching the cart and offline-receipt treatment so customers and cashiers see consistent labeling on server-rendered prints (browser + silent QZ Tray paths).

Reproduction (before fix)

On dev-miraaya.kcsc.com.jo, with Promotional Scheme gift1 (Pricing Rule PRLE-0120) configured as:

  • Apply On: Entire Transaction
  • Discount Type: Free Item "Gift 1" × 1
  • Minimum Amount: IQD 149,000

Adding 3× Iconic London Radiance Booster - Honey Glow (cart total 180,000 IQD, above threshold) showed the Offers panel with badge "Will apply when eligible" but never added the free item line.

Verified via API probes

  1. frappe.client.get on PRLE-0120 → rule config valid (correct company, currency IQD, valid_from, pos_only=1, min_amt=149000, free_item="Gift 1").
  2. erpnext.accounts.doctype.pricing_rule.pricing_rule.apply_pricing_rule with the cart payload → free_item_data: [] (per-item engine doesn't match Transaction rules).
  3. pos_next.api.invoices.apply_offers{"items": [...]} only — no free_items, no applied_pricing_rules keys.
  4. grep -rn apply_pricing_rule_on_transaction pos_next/ → zero hits.

What changed

File Change
pos_next/api/invoices.py +161 lines: new _evaluate_transaction_offers() helper that constructs a transient Sales Invoice document, calls apply_pricing_rule_on_transaction(doc), harvests appended free-item rows. Also tops up rule_map with active apply_on="Transaction" Pricing Rules so the existing early-return path doesn't drop them. Result is merged into free_items_map with setdefault (per-item path wins on collision since it carries full discount metadata).
pos_next/pos_next/print_format/pos_next_receipt/pos_next_receipt.json Conditional rendering for item.is_free_item: item name suffixed with green (FREE) badge; rate column shows qty × FREE / IQD 0.00 in green. modified timestamp bumped.

Why the transient frappe.new_doc("Sales Invoice") rather than a _dict

apply_pricing_rule_for_free_items() (ERPNext pricing_rule/utils.py:705) calls doc.append("items", free_item), which frappe._dict does not support. The doc is constructed in memory and discarded — never .insert() or .save() — so no DB side effects. doc.is_pos = 1 activates pos_next's monkey-patched get_other_conditions (pos_next/overrides/pricing_rule.py:67), so pos_only=1 rules continue to be honored.

Why this approach

  • Reuses ERPNext's own engine — no reimplementation of rule matching, qty/amount thresholds, mixed_conditions, recursion, or coupon gating.
  • No frontend changes needed: processFreeItems() in POS/src/stores/posCart.js:391 already consumes free_items.
  • Cart (InvoiceCart.vue), payment dialog (PaymentDialog.vue), and offline custom receipt (printInvoice.js) already render is_free_item correctly — only the server-rendered Jinja template needed updating.

Test plan

  • Direct API smoke: hit /api/method/pos_next.api.invoices.apply_offers with 3× IQD 60,000 cart item and selected_offers=["PRLE-0120"] → response contains free_items: [{item_code: "Gift 1", qty: 1, pricing_rules: "PRLE-0120", is_free_item: 1, ...}] and applied_pricing_rules: ["PRLE-0120"].
  • UI cart: add 3× Iconic London Radiance Booster (total 180,000 IQD); gift1 badge flips from "Will apply when eligible" → applied; cart shows Gift 1 row at IQD 0.00 with FREE badge.
  • Threshold edge: reduce qty to 1 (total 60,000) → Gift 1 row disappears; bump back to 3 → reappears.
  • Regression — per-item rule: cart with item matching PRLE-0117 ("20% Discount", apply_on=Item Code) still gets the 20% discount.
  • No-rule cart: add an item with no matching promotion; response = original items only; no errors logged.
  • Payment dialog: open Checkout — Gift 1 appears with green background and "(Free)" label, Grand Total correct (free item contributes 0).
  • Browser print (/printview?format=POS%20Next%20Receipt): receipt shows "Gift 1 (FREE)" header, rate column "1 × FREE", amount "IQD 0.00" in green; TOTAL still correct.
  • Silent print (QZ Tray) if configured: same content via silentPrintInvoice().
  • Offline receipt regression: finalize sale while offline; custom HTML path (buildReceiptDocumentHTML) still prints (FREE) annotation correctly.
  • Server logs: no entries under "POS Apply Offers (Transaction Rules)" during normal use.

Out of scope

  • Tests for apply_offers (no existing test file — follow-up).
  • Price-discount Transaction rules (apply_on=Transaction, price_or_product_discount="Price"); they set doc.discount_amount on the doc rather than appending an item row. The reported issue is Product-discount; price-discount Transaction rules are a follow-up.

🤖 Generated with Claude Code

engahmed1190 and others added 2 commits May 13, 2026 12:38
ERPNext routes Pricing Rules with apply_on="Transaction" through a
separate engine (apply_pricing_rule_on_transaction) that pos_next
never invoked, so "Entire Transaction" Promotional Schemes — like
"spend X get free product Y" — were silently dropped regardless of
cart contents. apply_offers() now also builds a transient (never-saved)
Sales Invoice and runs ERPNext's transaction-level engine on it,
merging any free-item rows into the existing free_items response under
the same (item_code, pricing_rule) dedup key the per-item path uses.

The POS Next Receipt Jinja template now annotates is_free_item rows
with a green "(FREE)" badge and renders the rate column as
"qty x FREE / IQD 0.00", matching the cart and offline-receipt
treatment so customers and cashiers see consistent labeling on
server-rendered prints (browser + silent QZ Tray paths).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updated the `formatItemsForSubmission` function to synthesize dedicated free item rows when legacy carts indicate free quantities via `free_qty`. This ensures compatibility with ERPNext's Sales Invoice expectations, which require separate rows for free items. Additionally, modified the `usePOSCartStore` to consistently handle free items by adding dedicated rows, improving the overall handling of promotional offers in both online and offline modes.

Refactored the sales invoice logic to merge packed items from free bundles into their corresponding paid rows, preventing duplication and ensuring accurate stock management.
@engahmed1190 engahmed1190 force-pushed the fix/transaction-level-promotional-schemes branch from edb69b2 to a01c2d7 Compare May 13, 2026 12:24
engahmed1190 and others added 2 commits May 13, 2026 18:10
…ws on submit

When a batch-tracked paid item is submitted with batch_no but
use_serial_batch_fields=0 and no pre-created serial_and_batch_bundle,
ERPNext's auto-SBB path triggers SBB validation inside the SLE on_submit
and fails for packed_items of free product bundles with
"The serial and batch bundle X not linked to Sales Invoice Y".

Setting use_serial_batch_fields=1 whenever batch_no or serial_no is set
routes the row through ERPNext's legacy batch fields path and avoids the
broken auto-SBB validation for bundle components.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ked_items

The FE-only fix for SI Item rows is not sufficient when the bundle's
component items themselves are batch- or serial-tracked. ERPNext still
attempts auto-SBB creation for those packed_items at SLE.on_submit time
and the resulting bundle fails validate_voucher_detail_no, throwing
"The serial and batch bundle X not linked to Sales Invoice Y".

Override update_packing_list to mark batch/serial-tracked packed_items
with use_serial_batch_fields=1 so the row goes through ERPNext's legacy
batch fields path and skips the broken auto-bundle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@engahmed1190 engahmed1190 merged commit d42a6b6 into develop May 13, 2026
3 checks passed
@engahmed1190 engahmed1190 mentioned this pull request May 13, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant