feat(PE): carry advance tax through invoice consumption - #54921
feat(PE): carry advance tax through invoice consumption#54921HenningWendtland wants to merge 17 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #54921 +/- ##
===========================================
+ Coverage 79.75% 79.86% +0.11%
===========================================
Files 1170 1170
Lines 128096 128444 +348
===========================================
+ Hits 102167 102586 +419
+ Misses 25929 25858 -71
🚀 New features to boost your workflow:
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughPayment entries now persist per-reference gross allocations (allocated_gross_amount) that include advance taxes marked included_in_paid_amount. Client-side handlers compute a gross/net ratio to preview and refresh per-reference gross amounts after allocations or tax changes. Server-side adds compute_advance_tax_breakdown() and set_allocated_gross_amount(), updates allocation to subtract included taxes before distributing paid amounts, persists unallocated_gross_amount, and emits per-reference advance-tax reversal GL legs. Doctype schemas, invoice controllers, totals logic, utilities, GL cancellation behavior, and tests are updated to be gross-aware. Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsStopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@erpnext/accounts/doctype/payment_entry/payment_entry.js`:
- Around line 1132-1144: In get_gross_net_ratio, stop using tax.tax_amount and
instead use tax.base_tax_amount (like get_included_taxes does), converting
base_tax_amount into the party/account currency by dividing by the appropriate
exchange rate before summing: if frm.doc.payment_type === "Pay" use
target_exchange_rate, if "Receive" use source_exchange_rate (matching the
pattern in calculate_taxes); continue to respect tax.included_in_paid_amount and
tax.add_deduct_tax when accumulating included_taxes, then compute net =
flt(frm.doc.paid_amount) - included_taxes and return net ? paid / net : 1.
In `@erpnext/accounts/doctype/payment_entry/payment_entry.py`:
- Around line 716-778: compute_advance_tax_breakdown is adding company‑currency
tax_amounts to reference (party) amounts causing FX drift; convert advance-tax
shares into the reference/party currency before summing. Update
compute_advance_tax_breakdown (and callers like set_allocated_gross_amount and
get_included_taxes when in_account_currency=True) to, for each tax share
computed from tax.tax_amount, convert that company‑currency value into the
reference/party currency using the appropriate exchange rate helper (e.g.
frappe.get_exchange_rate or your existing payment-entry currency conversion
utility) keyed by company vs reference currency, then distribute/round the
converted shares and use those converted numbers when setting
ref.allocated_gross_amount. Ensure you reference tax.tax_amount and
ref.allocated_amount when performing the conversion so all math is done in the
same currency.
In `@erpnext/accounts/utils.py`:
- Line 804: The unlink flow in remove_ref_doc_link_from_pe() currently removes
reference rows and calls set_amounts() but doesn't recalculate allocated gross,
leaving allocated_gross_amount stale; after the existing set_amounts() call
(inside remove_ref_doc_link_from_pe) invoke
payment_entry.set_allocated_gross_amount() (same method used in the
link/allocate paths) so surviving reference rows get their
allocated_gross_amount recalculated and persisted before returning/committing.
In `@erpnext/controllers/accounts_controller.py`:
- Around line 1531-1533: The allocation logic can produce negative allocations
due to floating-point drift when computing remaining = amount -
advance_allocated_gross; update the block in accounts allocation (variables:
allocated_gross_amount, allocated_amount, advance_allocated_gross) to clamp
remaining to at least 0 before taking min with source_gross (i.e., use remaining
= max(0, amount - advance_allocated_gross) and then allocated_gross_amount =
min(remaining, source_gross)), then compute allocated_amount =
allocated_gross_amount * source_net / source_gross if source_gross else 0 and
add flt(allocated_gross_amount) to advance_allocated_gross.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 8755733d-47ff-4a68-a104-3267413e1abe
📒 Files selected for processing (14)
erpnext/accounts/doctype/payment_entry/payment_entry.jserpnext/accounts/doctype/payment_entry/payment_entry.pyerpnext/accounts/doctype/payment_entry/test_payment_entry.pyerpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.jsonerpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.pyerpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.jsonerpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.pyerpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.jsonerpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.pyerpnext/accounts/general_ledger.pyerpnext/accounts/utils.pyerpnext/controllers/accounts_controller.pyerpnext/controllers/taxes_and_totals.pyerpnext/public/js/controllers/taxes_and_totals.js
I think Coderabbit flags another pre-existing bug here: That taxes are not properly resolved for PEs with taxes |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
erpnext/accounts/doctype/payment_entry/test_payment_entry.py (2)
357-365: 💤 Low valueCapture the
book_advance_payments_in_separate_party_accountflag too, not just the advance account.The try/finally only snapshots
default_advance_received_account. The flag itself is unconditionally reset to0in the finally block, so if a future test ordering (or shared setup) leaves this Company flag enabled, this test will silently flip it off and leak that to subsequent tests. The same gap exists intest_advance_tax_reverses_on_si_cancel(lines 446–454, 514–522).♻️ Symmetric snapshot/restore
- previous_advance_account = frappe.db.get_value("Company", company, "default_advance_received_account") + previous_book_in_separate_account, previous_advance_account = frappe.db.get_value( + "Company", + company, + ["book_advance_payments_in_separate_party_account", "default_advance_received_account"], + ) frappe.db.set_value( "Company", company, { "book_advance_payments_in_separate_party_account": 1, "default_advance_received_account": advance_account, }, ) try: ... finally: frappe.db.set_value( "Company", company, { - "book_advance_payments_in_separate_party_account": 0, + "book_advance_payments_in_separate_party_account": previous_book_in_separate_account, "default_advance_received_account": previous_advance_account, }, )Also applies to: 422-429
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@erpnext/accounts/doctype/payment_entry/test_payment_entry.py` around lines 357 - 365, The finally block only restores previous_advance_account but unconditionally resets book_advance_payments_in_separate_party_account to 0; capture the original flag value (e.g., previous_book_advance_flag = frappe.db.get_value("Company", company, "book_advance_payments_in_separate_party_account")) before setting it, and in the finally restore both values (default_advance_received_account and book_advance_payments_in_separate_party_account) using frappe.db.set_value so the test (including test_advance_tax_reverses_on_si_cancel and other spots where you set book_advance_payments_in_separate_party_account and default_advance_received_account) does not mutate global Company state for subsequent tests.
735-739: 💤 Low valueInline comment seems inverted —
~847is the fixed value, not the pre-fix proportional share.The assertion below expects
so2.allocated_gross_amount ≈ so2_allocated + tax_total≈622.56 + 224.83 ≈ 847.39, which is the post-fix "full tax absorbed" outcome. The pre-fix/buggy stale value is the original proportional share, ≈622.56 + (622.56/1322.56)*224.83≈728.39, not~847. The current wording reads as if~847were the buggy value the new loop avoids, which is backwards.📝 Suggested rewording
- # Surviving row: allocated_amount unchanged, allocated_gross_amount absorbs - # the full tax (it's the only ref left in the breakdown). Without the new - # `frappe.db.set_value` loop in `remove_ref_doc_link_from_pe`, this would - # still be the pre-unlink proportional share (~847 — net + ref's old tax - # slice), not the full gross. + # Surviving row: allocated_amount unchanged, allocated_gross_amount absorbs + # the full tax (~847.39 = 622.56 net + 224.83 full tax) because it's the + # only ref left in the breakdown. Without the new `frappe.db.set_value` + # loop in `remove_ref_doc_link_from_pe`, this would still be the pre-unlink + # proportional share (~728 = net + ref's old tax slice).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@erpnext/accounts/doctype/payment_entry/test_payment_entry.py` around lines 735 - 739, The inline comment in test_payment_entry.py misstates which value is the pre-fix vs post-fix outcome; update the comment near the assertion involving so2.allocated_gross_amount (and variables so2_allocated, tax_total) to say that ~847.39 is the post-fix “full tax absorbed” result and that the pre-fix buggy proportional share was ~728.39, and mention that the frappe.db.set_value loop added to remove_ref_doc_link_from_pe causes the surviving row to absorb the full tax rather than the old proportional slice.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@erpnext/accounts/doctype/payment_entry/payment_entry.js`:
- Around line 1132-1147: The gross/net ratio calculation in get_gross_net_ratio
uses frm.doc.paid_amount (source currency) while included_taxes are converted
using exchange_rate for "Pay", causing mismatch; update the paid value so that
when frm.doc.payment_type === "Pay" you convert paid_amount into the party
(target) currency using the same exchange_rate before computing net (e.g.,
compute paid_in_party = paid_amount / exchange_rate for "Pay"), then use that
converted paid when computing net and returning the ratio.
---
Nitpick comments:
In `@erpnext/accounts/doctype/payment_entry/test_payment_entry.py`:
- Around line 357-365: The finally block only restores previous_advance_account
but unconditionally resets book_advance_payments_in_separate_party_account to 0;
capture the original flag value (e.g., previous_book_advance_flag =
frappe.db.get_value("Company", company,
"book_advance_payments_in_separate_party_account")) before setting it, and in
the finally restore both values (default_advance_received_account and
book_advance_payments_in_separate_party_account) using frappe.db.set_value so
the test (including test_advance_tax_reverses_on_si_cancel and other spots where
you set book_advance_payments_in_separate_party_account and
default_advance_received_account) does not mutate global Company state for
subsequent tests.
- Around line 735-739: The inline comment in test_payment_entry.py misstates
which value is the pre-fix vs post-fix outcome; update the comment near the
assertion involving so2.allocated_gross_amount (and variables so2_allocated,
tax_total) to say that ~847.39 is the post-fix “full tax absorbed” result and
that the pre-fix buggy proportional share was ~728.39, and mention that the
frappe.db.set_value loop added to remove_ref_doc_link_from_pe causes the
surviving row to absorb the full tax rather than the old proportional slice.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 74b962b0-6e77-46f1-b2d3-bca68afb01b8
📒 Files selected for processing (2)
erpnext/accounts/doctype/payment_entry/payment_entry.jserpnext/accounts/doctype/payment_entry/test_payment_entry.py
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
erpnext/accounts/doctype/payment_entry/payment_entry.js (1)
385-412:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winBind
unallocated_gross_amountto party currency in dynamic labels/options.
unallocated_gross_amountis not included in the form-level currency metadata update, so it can display with the wrong currency context on multi-currency entries.Suggested fix
frm.set_currency_labels( - ["total_allocated_amount", "unallocated_amount", "total_taxes_and_charges"], + [ + "total_allocated_amount", + "unallocated_amount", + "unallocated_gross_amount", + "total_taxes_and_charges", + ], party_account_currency ); var currency_field = frm.doc.payment_type == "Receive" ? "paid_from_account_currency" : "paid_to_account_currency"; frm.set_df_property("total_allocated_amount", "options", currency_field); frm.set_df_property("unallocated_amount", "options", currency_field); + frm.set_df_property("unallocated_gross_amount", "options", currency_field); frm.set_df_property("total_taxes_and_charges", "options", currency_field);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@erpnext/accounts/doctype/payment_entry/payment_entry.js` around lines 385 - 412, The dynamic currency binding omits "unallocated_gross_amount", causing it to show the wrong currency; update the two places where currency-linked fields are listed—add "unallocated_gross_amount" to the array passed into frm.set_df_property (the ["total_amount", "outstanding_amount", "allocated_amount", "allocated_gross_amount"] list) and include it in the loop that calls reference_grid.update_docfield_property(fieldname, "options", party_currency) so that "unallocated_gross_amount" is bound to party_currency like the others (refs: frm.set_df_property, party_currency, reference_grid, update_docfield_property).
🧹 Nitpick comments (1)
erpnext/accounts/doctype/payment_entry/test_payment_entry.py (1)
339-429: 💤 Low valueConsider extracting the repeated advance-account company-setting setup/teardown into a helper.
Three of the new tests duplicate the same ~17-line scaffold to flip
book_advance_payments_in_separate_party_account+default_advance_received_account, run the case, and restore infinally. A small context manager (or asetUp/tearDownper test class group) would cut ~50 lines and make the intent of each test stand out. Also worth giving each test its own advance-account name (the consume and cancel tests currently share"Advances Received For Tax Test"), and capturing the originalbook_advance_payments_in_separate_party_accountinstead of hardcoding0on restore — the latter is just inheriting an existing pattern in the file though.♻️ Sketch of a context-manager helper
from contextlib import contextmanager `@contextmanager` def _separate_advance_account(company: str, advance_account: str): prev_flag = cint(frappe.db.get_value("Company", company, "book_advance_payments_in_separate_party_account")) prev_acc = frappe.db.get_value("Company", company, "default_advance_received_account") frappe.db.set_value( "Company", company, { "book_advance_payments_in_separate_party_account": 1, "default_advance_received_account": advance_account, }, ) try: yield finally: frappe.db.set_value( "Company", company, { "book_advance_payments_in_separate_party_account": prev_flag, "default_advance_received_account": prev_acc, }, )Then each test becomes:
advance_account = create_account(..., account_name="Advances Received For SI Consume Tax Test", ...) with _separate_advance_account("_Test Company", advance_account): ...That also gives a natural home for
cint(line 7), which I don't see referenced in any of the new tests right now.Also applies to: 431-522, 731-803
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@erpnext/accounts/doctype/payment_entry/test_payment_entry.py` around lines 339 - 429, Extract the repeated company advance-account setup/teardown into a reusable helper (e.g. a contextmanager named _separate_advance_account(company, advance_account)) and use it from test_advance_tax_reversal_on_si_consume (and the other two tests noted) instead of duplicating the frappe.db.set_value/restore block; the helper should read and save the current book_advance_payments_in_separate_party_account flag and default_advance_received_account (use cint when reading the flag), set the 1/advance_account values, yield, and restore the original values in finally, and update the tests to create unique advance account names via create_account and call the helper around the test logic instead of hardcoding a restore value of 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@erpnext/accounts/doctype/payment_entry/payment_entry.js`:
- Around line 385-412: The dynamic currency binding omits
"unallocated_gross_amount", causing it to show the wrong currency; update the
two places where currency-linked fields are listed—add
"unallocated_gross_amount" to the array passed into frm.set_df_property (the
["total_amount", "outstanding_amount", "allocated_amount",
"allocated_gross_amount"] list) and include it in the loop that calls
reference_grid.update_docfield_property(fieldname, "options", party_currency) so
that "unallocated_gross_amount" is bound to party_currency like the others
(refs: frm.set_df_property, party_currency, reference_grid,
update_docfield_property).
---
Nitpick comments:
In `@erpnext/accounts/doctype/payment_entry/test_payment_entry.py`:
- Around line 339-429: Extract the repeated company advance-account
setup/teardown into a reusable helper (e.g. a contextmanager named
_separate_advance_account(company, advance_account)) and use it from
test_advance_tax_reversal_on_si_consume (and the other two tests noted) instead
of duplicating the frappe.db.set_value/restore block; the helper should read and
save the current book_advance_payments_in_separate_party_account flag and
default_advance_received_account (use cint when reading the flag), set the
1/advance_account values, yield, and restore the original values in finally, and
update the tests to create unique advance account names via create_account and
call the helper around the test logic instead of hardcoding a restore value of
0.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: cc218a3e-1e54-45c9-be61-45182c6bd429
📒 Files selected for processing (5)
erpnext/accounts/doctype/payment_entry/payment_entry.jserpnext/accounts/doctype/payment_entry/payment_entry.jsonerpnext/accounts/doctype/payment_entry/payment_entry.pyerpnext/accounts/doctype/payment_entry/test_payment_entry.pyerpnext/controllers/accounts_controller.py
🚧 Files skipped from review as they are similar to previous changes (1)
- erpnext/accounts/doctype/payment_entry/payment_entry.py
This comment was marked as outdated.
This comment was marked as outdated.
|
bump |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #54921 +/- ##
===========================================
+ Coverage 79.75% 79.86% +0.11%
===========================================
Files 1170 1170
Lines 128096 128444 +348
===========================================
+ Hits 102167 102586 +419
+ Misses 25929 25858 -71
🚀 New features to boost your workflow:
|
This comment was marked as outdated.
This comment was marked as outdated.
Confidence Score: 4/5
Reviews (7): Last reviewed commit: "Merge branch 'develop' into improve-adva..." | Re-trigger Greptile |
…t taxes
`allocate_amount_to_references` distributes the full paid amount across the
reference rows, but `set_unallocated_amount` and `set_difference_amount`
subtract taxes flagged as `included_in_paid_amount`. With such a tax row the
auto-allocation therefore over-allocates by the tax and the entry cannot be
saved ("Difference Amount must be zero").
Subtract the included taxes before distributing. The tax rows are recomputed
first, since `tax_amount` may still be based on the previous paid amount.
`get_included_taxes` gains an `in_account_currency` flag because the amount
being allocated is in party-account currency while the tax rows are stored in
company currency.
When an advance payment carries VAT that is included in the paid amount, the party account only holds the net. Consuming that advance from a Sales Invoice therefore clears the net, leaving the tax as outstanding, and books the tax a second time on the invoice. Track the gross alongside the net: * `allocated_gross_amount` on Payment Entry Reference and the invoice advance rows, `unallocated_gross_amount` on Payment Entry. Gross equals net when there are no advance taxes, so nothing changes for existing entries. * On consumption, reverse each reference's share of the advance tax against the party account, so the receivable clears by the gross. * `set_advances` and `calculate_total_advance` allocate on gross, but only for advances booked in a separate party account: that is the only path with a consumption GL event to post the reversal on. * Partial cancel matches nullable GL columns with IS NULL, as the tax legs carry no party.
7253b16 to
4cdf653
Compare
…urrency GL `compute_advance_tax_breakdown` now reads `base_tax_amount` instead of `tax_amount`. `calculate_taxes` assigns both fields the same company-currency value, so this is behaviour-neutral, but it matches `get_included_taxes` and makes the company-currency basis obvious at the call site. The party-side tax leg was converted back with an unrounded `flt()`, unlike the sibling `allocated_amount` leg. Round it to the reference precision. Adds a multi-currency test asserting the reversal GL itself: a USD advance in an INR company must relieve the receivable by the full gross in party currency and leave the tax account at zero after consumption.
ERPNext keeps inline comments and field descriptions sparse. Drop the ones that restate the code, keep the ones stating something the code cannot. No logic change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tax handlers redistribute the payment because included taxes change how much is left to allocate. That call passes `frappe.flags.allocate_payment_amount`, and the server zeroes every `allocated_amount` when it is falsy, so editing a tax row after fetching references with "Allocate Payment Amount" unchecked wiped the amounts the user had typed. Only redistribute when allocation is automatic; otherwise just refresh the gross preview and the unallocated amount. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`unallocated_gross_amount` grossed the remainder up by `paid_amount / net`, which counts every included tax row. The reference rows gross up by advance tax alone, so with an included `Deduct` row the same money was worth less sitting unallocated than allocated, and an invoice consuming the remainder cleared by the wrong amount. A `Deduct` row is withheld from the payment and already sits inside the net, so it must not gross anything up. Use the advance tax total on both paths, and mirror the same rule in the client-side preview so the value no longer jumps on save. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`set_allocated_gross_amount` converted each tax to party currency before splitting it across references, while the reversal legs split in company currency and convert per account head. On multi-currency entries with more than one reference the two orders disagreed by a cent, so the invoice kept a residual outstanding and never reached `Paid`. Derive both from the same company-currency breakdown. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An included tax is credited to the tax account at payment time, so only the net ever reaches the party account, and the Advance Payment Ledger is built from receivable and payable rows alone. An order paid in full therefore read `advance_paid` short by the tax — 1000 against a grand total of 1190 — and "Get Outstanding Orders" kept offering the remaining 190 on an order the customer had already settled. The gap closed only once an invoice consumed the advance and the reversal leg reached the ledger. Add the advance tax still held on open reference rows to the ledger total. The term is self-cancelling: while a row names the order the ledger is short by exactly that amount, and once an invoice consumes it the row is rewritten to the invoice while the reversal leg posts the same amount against the party. Restricted to payments that book advances in a separate party account, since nothing grosses up elsewhere. Every reader of `advance_paid` compares it against the order's gross total, so the gross is the figure they all want. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reconciliation tool allocates the net it finds in the party ledger. `set_allocated_gross_amount` then grossed that up, so a 1190 advance with 19% included tax settled a 1000 invoice by 1190 and left the customer 190 in credit that nothing could clear. Shrink the party leg instead, so the gross lands on the outstanding and the remainder stays an advance. The outstanding on a row only bounds an allocation the entry has not booked yet, so submitted rows keep theirs. Restrict the gross-up to entries that book advances in a separate party account, since only those reverse the tax onto the party on consumption, and to the reference types that carry it. A plain payment or a journal reference settles by the net, and the grid now says so.
An advance in a separate party account clears its invoice by the gross: the net leaves the advance account and the tax leg follows it onto the receivable. Both move at the payment's rate, while the invoice books at its own, so the difference to correct is on the gross. Computing it on the net left the receivable holding the rate difference on the tax portion — a company-currency balance against a foreign currency party account at zero that no reconciliation could clear.
`refresh` turns `allocate_payment_amount` back on after every load and save, so editing a tax row on a saved draft re-ran the auto-allocation and threw away a hand-made split without a prompt. A rate change only moves the split between net and tax; every reference still owes the same gross. Re-derive each net from the gross it had before the change instead: manual rows survive, auto-allocated rows land where a fresh allocation would put them, and the total stays balanced.
The cap in `set_allocated_gross_amount` also fired when an invoice consumed an advance, where `outstanding_amount` is already net of that advance. An invoice larger than the advance gross then shrank the allocation to the leftover outstanding, stranding the deposit on the advance account and leaving the receivable open. Move it to `update_reference_in_payment_entry`, the only caller that allocates in net against a gross outstanding. Reconciliation is what tells the two paths apart, as it does not pass `outstanding_amount`; multi-currency reconciliation also skips the reference details update, so read what the reference owes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`taxes_changed` derived the gross each reference owes from `allocated_amount` and a ratio read after the edited field had reached the model but before `apply_taxes` refreshed the amounts. Ticking `Included in Paid Amount`, adding a tax row or removing one therefore priced the new inclusion set against the old tax amounts, inflating the gross a little more on every such edit. Read `allocated_gross_amount` instead. The reference row already carries it and keeps it in step, so it is the prior gross without a ratio to get wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An advance settles its reference by the gross, so a net allocation typed on the form can overshoot while still passing the net check against the outstanding: 1100 net at 19% settles 1309 against an order worth 1190, and the order ends up over-paid with no warning. Cap it while the entry is still being saved by its own form. Reconciliation and unlink reach the same function on a stored entry, where the row's outstanding no longer reflects this allocation and capping would under-consume the advance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes #42843
Rebased on current develop. Two commits: the first is an independent bug fix that stands on its own, the second is the feature.
Issue Addressed
In Germany, the EU, and other VAT jurisdictions, taxes are due upon receipt of an advance payment, not only on the invoice that later consumes it. ERPNext's Payment Entry can already carry tax rows, but the rest of the advance-consumption pipeline did not honour them.
Sales Order 1 190 (1 000 net + 19 % VAT), advance payment of 1 190, final invoice 1 190:
developTracing the problem from the end:
allocated_amountis net underincluded_in_paid_amount=1. A parallelallocated_gross_amountonPayment Entry Reference,Sales Invoice AdvanceandPurchase Invoice Advancecarries the gross forward, plusunallocated_gross_amountonPayment Entryfor the remainder a later invoice may consume.allocated_amount, anchored on the net paid amount rather than on the sum of current allocations. A reference's share therefore depends only on its own allocation and survives reference rewriting, partial consumption and unlinking. The rounding remainder lands on the last reference only when the payment is fully allocated.included_in_paid_amounttax row,allocate_amount_to_referencesdistributed the gross whileset_unallocated_amountsubtracted the tax, so the entry over-allocated by exactly the tax and failed validation with Difference Amount must be zero. That is the first commit, and it is independent of everything above.Key Additions
Backend:
PaymentEntry.compute_advance_tax_breakdown— returns{ref_row_name: {account_head: tax_amount}}, the per-reference split of the PE's advance taxes.PaymentEntry.set_allocated_gross_amount— persistsallocated_gross_amountper reference andunallocated_gross_amounton the entry. Called fromvalidate()and fromupdate_reference_in_payment_entryafter the consume rewrites references.PaymentEntry._add_advance_tax_reversal_for_reference— posts the per-account tax-reversal legs on consumption, and emits zero-amount cancel templates so SI/PI cancel and amend reverse them viamake_reverse_gl_entries(partial_cancel=True).set_advances(inaccounts/services/advances.py) andcalculate_total_advance(intaxes_and_totals.py) — drive invoice consumption off gross, with a net fallback.PaymentEntry.allocate_amount_to_references— subtracts included taxes before splitting across references, recomputing the tax rows first since they may still reflect the previous paid amount.Cross-cutting fix:
make_reverse_gl_entries(partial_cancel=True)now matches nullable columns withIS NULLrather than=. The new tax legs carry noparty_type/party, andNULL = NULLis false in SQL, so the cancel update would silently match nothing.Frontend:
refresh_allocated_gross_amountsinpayment_entry.jskeeps the read-only gross preview in sync while editing; the server remains the source of truth at save time.Scope
book_advance_payments_in_separate_party_account). That is the only path with a consumption GL event to post the reversal on, soset_advanceskeeps allocating on net otherwise — behaviour is unchanged for every other company.Deductrows and tax-withholding rows are excluded: they are withheld from the payment rather than collected on top of it.advance_paidstill reports the net, as it comes from the Advance Payment Ledger. Left alone to keep the diff contained.Backwards compatibility
With no advance taxes,
allocated_gross_amount == allocated_amounton every path and no extra GL entries are emitted. Reads use... or allocated_amount, so legacy rows (NULL/0) are handled transparently with no migration step.Screenshots
PE Document with new fields

SI Totals with Total Advance filled from Allocated Gross Advance

PE after SI submission with partial consumption

GL Entries for PE after reposting (includes tax handling now)

GL Entries for PE after cancellation of the SI

Multi-currency
USD advance in a EUR company at 0.9137.
calculate_taxeswritestax_amountandbase_tax_amountwith the same company-currency value, so the breakdown is built in EUR and converted back to the party currency for the receivable leg. The gross/net split stays in USD:Ledger for that entry: receivable credited 1 190,00 USD = 1 087,30 EUR, advance account and tax account both net to zero, total Dr = total Cr.
Out of Scope
Cancelling a Sales Invoice that partially consumed an advance does not bump the allocated amount back up on the Payment Entry reference row. Pre-existing, untouched here, worth its own fix.
Follow-Up PR
Carrying tax rows over automatically from the "Create → Payment" button on SO/PO. Kept separate to hold this diff down; it depends on the gross/net model landing first.
Tests
19 new tests in
test_payment_entry.py: gross computation, proportional split across references, multi-currency in both Receive and Pay direction including the reversal GL itself, tax-reversal GL on consume and on cancel, unlinking, partial and unallocated consumption,Deductand withholding exclusion, and the no-separate-account case.test_payment_entry,test_sales_invoice,test_purchase_invoice,test_payment_reconciliationandtest_unreconcile_paymentwere run against this branch and againstdevelopon the same machine, with identical results. Also verified manually on a German chart of accounts: advance creation from a Sales Order, partial consumption, invoice cancellation, payment cancellation.Update
Three follow-up fixes after self-review:
unallocated_gross_amountis grossed up by advance tax only, like the reference rows, so the same money is worth the same whether it sits allocated or unallocated. ADeductrow is withheld from the payment and already inside the net, so it grosses nothing up.https://docs.frappe.io/wiki/change-requests/6tbus2aj7k