The Float control parses values with the field precision, falling back
to the global float precision when the docfield declares none
(frappe ControlFloat.parse / get_precision). On a site with float
precision 2, a fetched UOM factor of 0.453592292 was written back to
the model as 0.45, silently corrupting every derived quantity by 0.8
percent. A ratio must not inherit display precision meant for
quantities, so declare the same precision 9 the UOM Conversion Factor
master already uses on every transaction-level conversion_factor
field.
* feat(accounts): split exchange gain and exchange loss accounts
Add optional Exchange Gain Account and Exchange Loss Account fields on
Company. When set, realized FX gain/loss from settling an invoice in a
foreign currency (via Payment Entry, Payment Reconciliation, or a
Journal-Entry-based advance) books to the matching account instead of
the single Exchange Gain/Loss account. Either field left blank falls
back to the existing Exchange Gain/Loss account, so companies that
don't configure the new fields are unaffected.
New companies get "Exchange Gain" and "Exchange Loss" accounts
auto-created in their chart of accounts and auto-assigned to the new
fields, same as the existing Exchange Gain/Loss account provisioning.
The Payment Reconciliation tool's per-allocation "Difference Account"
override in its reconcile dialog continues to work as before; the
split accounts only change the computed default shown there.
* test(account_balance): account for new Exchange Gain account in income report
The new auto-provisioned Exchange Gain account under Indirect Income
now shows up in the Income root type report for _Test Company 2.
---------
Co-authored-by: test <test@test.com>
set_dynamic_labels() unconditionally forced update_stock's hidden
property based only on is_debit_note/has_subcontracted, overwriting
whatever Customize Form had set on every refresh. OR it with the
field's original (property-setter-driven) hidden value instead.
* feat: validate stock value and stock closing entry before period closing
* fix: do not accept scoped stock closing entries as period closing prerequisite
* feat: seed batch valuation from stock closing balance and freeze closed-period stock
* fix(subscription): don't reactivate a cancelled subscription
set_subscription_status() unconditionally set status to Active once
there was no outstanding invoice, even if the subscription had been
intentionally cancelled. Paying off an invoice issued before
cancellation (directly, or via the Payment Entry -> refresh hook)
flipped a Cancelled subscription back to Active while cancelation_date
stayed set.
process()'s cancel_at_period_end check compared posting_date against
getdate(self.end_date), and getdate(None) returns today, so an empty
end_date was silently treated as "cancel now" on every scheduler run.
Combined with the reactivation bug, this let a cancelled subscription
toggle Cancelled -> Active on each run and generate another invoice at
the next period boundary.
Fixes#57761
* fix(test): compare normalized dates in subscription cancellation test
cancelation_date read straight off an unsaved in-memory doc is a
string from nowdate(), but the same field comes back as a
datetime.date after reload(). Wrap both sides in getdate() so the
comparison isn't type-sensitive.
* fix(accounts): key the payment ledger CTEs on account, not Max(account)
QueryPaymentLedger builds two CTEs -- voucher amount and outstanding -- and
joins them on account among other columns. Both sides selected Max(account)
while grouping without it, so the join key was an aggregate over two different
row sets. A voucher posting ledger entries against two party accounts could
have the two sides pick different accounts, the join miss, and the outstanding
come back NULL. Max() over text is a sort, so which account wins is also
collation-dependent, and the engines sort text differently.
Group both CTEs by account instead. That makes the join key a real column and
scopes each Sum() to a single account -- so amount_in_account_currency is no
longer summed across accounts that may not share a currency. Row shape only
changes for a voucher that genuinely spans two party accounts for one party,
where today's single row is already an arbitrary pick over mixed currencies.
cost_center and remarks stay descriptive but genuinely vary per entry, and were
aggregated independently, so they could be stitched together from different
entries into a row that was never posted. They now come off one real entry,
picked by Min(name) -- Payment Ledger Entry declares no autoname rule, so
frappe names it by hash, and those are lower-case, which keeps the pick free of
the collation divergence.
* test(accounts): cover payment ledger metadata coherence
A Journal Entry posting two receivable lines for one customer with different
cost centers and remarks. Whatever row the ledger returns, its cost center and
remarks must be a pair that was actually posted. Guards the fixture itself, so
it cannot pass by posting only one distinct pair.
* test(accounts): cover the account-keyed payment ledger aggregation
The coherence test posts both party lines to one account, so it exercises the
representative-row metadata but not the account-keyed grouping or the CTE join.
Adds a Journal Entry posting to two receivable accounts for one customer and
asserts each account comes back as its own row, with its own amount and a
non-null outstanding.
* fix(accounts): take POS summary warehouse and cost centre from one item line
Both describe an item line, not the invoice, and an invoice can carry several.
They were aggregated independently per invoice, so the report could show a
warehouse from one line beside a cost centre from another -- a pair that was
never posted.
The warehouse then becomes an outer grouping key, so the pick is not merely a
label: it decides how rows are partitioned across owner/date and therefore what
each row totals. Max() over text is a sort, and MariaDB folds case while
PostgreSQL orders by byte value, so the two engines can partition differently.
Take both off one real line instead, and the mode of payment off one real
payment line for the same reason. Sales Invoice Item is hash-named and Sales
Invoice Payment declares no autoname rule, so frappe hash-names it too -- which
keeps Min(name) free of the collation divergence that sorting text has.
* test(accounts): cover POS summary warehouse/cost-centre coherence
The existing tests post a single item line, so they cannot see this. Adds an
invoice with two lines whose warehouse and cost centre are deliberately
crossed: the higher warehouse sits on the line with the lower cost centre, so
an independently aggregated pair belongs to neither line.
* fix(accounts): pick the POS summary representative by idx, not by hash
Min(name) selected whichever child row happened to have the lowest hash, which
is arbitrary and turns on something unrelated to the data. Min(idx) selects the
first line the user actually entered: an integer, so the pick is free of
collation, and it is meaningful rather than incidental.
The join moves to (parent, idx), which is unique per parent.
Filter Accounts Receivable and AR Summary on the Sales Invoice's own
sales_partner instead of the customer's default_sales_partner, and read
the Sales Partner column from the invoice. Returns are attributed to the
invoice they settle, matching how the Sales Person filter works.
When a plan is selected in the Subscription's Plans table, the Subscription's
accounting dimensions (cost center and any custom dimensions) auto-fill from the
plan, falling back to the plan item's company default (selling cost center for a
Customer, buying for a Supplier). Only empty fields are filled. Stale async
responses are ignored so a quick re-pick of the plan can't be overwritten.
Companies now get their Stores warehouse as Default Warehouse, so item
warehouse resolution succeeds where it previously came back empty.
test_internal_pr_reference cleared inter_company_reference and asserted a
ValidationError, but no validation covers that field - the mapper already
sets it. It was incidentally catching 'Row #1: Warehouse is mandatory for
stock Item' from the blank target warehouse, so the assertion never tested
what it claimed. Dropped it; the delivery_note_item assertion below still
covers the reference linkage.
test_inter_company_transaction_without_default_warehouse now establishes its
own premise by clearing the company's default warehouse instead of relying
on it being unset. Its failure previously skipped the teardown that restores
frappe.local.enable_perpetual_inventory, which db rollback cannot undo, which
in turn broke two later inter-company tests.
`get_linked_dunnings_as_per_state` joins Dunning to its Overdue Payment child
table without DISTINCT. When a Sales Invoice has more than one overdue
installment, its Dunning holds one Overdue Payment row per installment, so the
query returns the same Dunning name once per row.
`update_linked_dunnings` then loads that Dunning name into a separate document
object for each duplicate row and saves each one. The first save bumps the
`modified` timestamp, so the second (now stale) save fails with
`TimestampMismatchError` ("Document has been modified after you have opened
it"). The error is raised on the Dunning while the user is submitting a Payment
Entry, making it confusing, and payments for such invoices cannot be posted at
all.
Add DISTINCT so each linked Dunning is returned (and saved) exactly once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pricing Rule: log a broken condition instead of dropping the rule
- Payment Request: log gateway validation failures
- Supplier Quotation from RFQ portal: let errors surface instead of
returning None to a dead submit button
- Bank Transaction Rule: drop the pointless try/except around the
on_trash unlink; a failed write already aborts the delete
- Statement of Accounts: the CC handler was unreachable
(frappe.get_value returns None, it does not raise); drop it and
filter out users without an email
- Bank Statement Import: 'Bank Account' at column 0 is falsy, so the
column was appended instead of filled