fix(offers): apply transaction-level promotional schemes in POS#257
Merged
Conversation
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.
edb69b2 to
a01c2d7
Compare
…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>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
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.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 existingfree_itemsresponse under the same(item_code, pricing_rule)dedup key as the per-item path.is_free_itemrows with a green (FREE) badge and renders the rate column asqty × 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 RulePRLE-0120) configured as: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
frappe.client.geton PRLE-0120 → rule config valid (correct company, currency IQD, valid_from, pos_only=1, min_amt=149000, free_item="Gift 1").erpnext.accounts.doctype.pricing_rule.pricing_rule.apply_pricing_rulewith the cart payload →free_item_data: [](per-item engine doesn't match Transaction rules).pos_next.api.invoices.apply_offers→{"items": [...]}only — nofree_items, noapplied_pricing_ruleskeys.grep -rn apply_pricing_rule_on_transaction pos_next/→ zero hits.What changed
pos_next/api/invoices.py_evaluate_transaction_offers()helper that constructs a transientSales Invoicedocument, callsapply_pricing_rule_on_transaction(doc), harvests appended free-item rows. Also tops uprule_mapwith activeapply_on="Transaction"Pricing Rules so the existing early-return path doesn't drop them. Result is merged intofree_items_mapwithsetdefault(per-item path wins on collision since it carries full discount metadata).pos_next/pos_next/print_format/pos_next_receipt/pos_next_receipt.jsonitem.is_free_item: item name suffixed with green (FREE) badge; rate column showsqty × FREE / IQD 0.00in green.modifiedtimestamp bumped.Why the transient
frappe.new_doc("Sales Invoice")rather than a_dictapply_pricing_rule_for_free_items()(ERPNextpricing_rule/utils.py:705) callsdoc.append("items", free_item), whichfrappe._dictdoes not support. The doc is constructed in memory and discarded — never.insert()or.save()— so no DB side effects.doc.is_pos = 1activates pos_next's monkey-patchedget_other_conditions(pos_next/overrides/pricing_rule.py:67), sopos_only=1rules continue to be honored.Why this approach
processFreeItems()inPOS/src/stores/posCart.js:391already consumesfree_items.InvoiceCart.vue), payment dialog (PaymentDialog.vue), and offline custom receipt (printInvoice.js) already renderis_free_itemcorrectly — only the server-rendered Jinja template needed updating.Test plan
/api/method/pos_next.api.invoices.apply_offerswith 3× IQD 60,000 cart item andselected_offers=["PRLE-0120"]→ response containsfree_items: [{item_code: "Gift 1", qty: 1, pricing_rules: "PRLE-0120", is_free_item: 1, ...}]andapplied_pricing_rules: ["PRLE-0120"]./printview?format=POS%20Next%20Receipt): receipt shows "Gift 1 (FREE)" header, rate column "1 × FREE", amount "IQD 0.00" in green; TOTAL still correct.silentPrintInvoice().buildReceiptDocumentHTML) still prints (FREE) annotation correctly.Out of scope
apply_offers(no existing test file — follow-up).apply_on=Transaction,price_or_product_discount="Price"); they setdoc.discount_amounton 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