Group fields with section and column breaks: Details (two columns), Items with
right-aligned totals, Print Settings (two columns), and Status (two columns).
* feat: block sales invoice submit when customer overdue exceeds threshold
Adds an opt-in, per-customer Overdue Billing Threshold. When enabled in
Accounts Settings, submitting a Sales Invoice is blocked if the customer's
overdue amount exceeds their threshold, unless the current user holds a
configured bypass role. Modeled on the existing credit limit feature.
- Accounts Settings (Credit Limits tab): enable toggle + bypass role.
- Per-customer threshold on the Customer Credit Limit table, shown only
when the feature is enabled via a property setter (same mechanism as
subscription / accounting dimension sections). Table relabeled to
"Credit & Overdue Limits".
- Overdue is read live from the ledger via get_outstanding_invoices
(payments already netted), summing Sales Invoices past their due date.
- Enforced in Sales Invoice on_submit, after the credit-limit check;
returns are exempt.
- validate_credit_limit_on_change no longer trips when a row sets only
the overdue threshold (credit_limit = 0).
Fixes#52960
* fix: compute overdue amount in company currency and format with fmt_money
get_customer_overdue_amount now sums GL Entry debit - credit grouped per
invoice, which is always booked in company currency, instead of using
get_outstanding_invoices which returns the receivable-account currency.
The threshold is in company currency, so the previous comparison could mix
currencies for customers with a foreign-currency receivable account. This
mirrors how get_customer_outstanding computes the figure for the existing
credit-limit check.
The blocking message now formats both amounts with fmt_money using the
company currency.
Adds a test asserting a 100 USD invoice at a conversion rate of 50 is
counted as 5000 in company currency.
* refactor: drop redundant threshold coercion and dead test cleanup
- Coerce the overdue threshold with flt() once when reading it, instead of
calling flt() on it at each of the three use sites.
- Remove a no-op set_overdue_billing_threshold() call in the feature-disabled
block (the threshold was already set to that value) and the trailing reset,
which is dead since each test is rolled back.
No behaviour change.
* fix: compute overdue amount from payment terms, matching the Overdue status
The overdue amount keyed on Sales Invoice.due_date, which set_due_date() sets
to the LAST payment term. An invoice whose first term was past due and unpaid
was therefore counted as zero, even though ERPNext already shows it as Overdue
in the invoice list. The gate and the UI could disagree.
get_customer_overdue_amount now follows the same rule as is_overdue(): per
invoice, the amount that has fallen due (sum of payment schedule terms past
their due date) minus what has been paid, clamped to the outstanding balance.
Invoices without a schedule (POS, opening) still fall back to the invoice due
date, mirroring is_overdue()'s own guard.
The ledger stays the source of truth for what is unpaid: the outstanding per
invoice is still SUM(debit) - SUM(credit) from GL Entry. base_payment_amount is
always stored in company currency, so no currency conversion is needed and the
comparison against the threshold stays consistent.
Adds a test covering a two-term invoice: only the past-due term counts, and
paying it off clears the overdue amount.
* feat: honour the overdue billing threshold set on the customer group
The threshold lives on Customer Credit Limit, which is also rendered on
Customer Group. A threshold set there was stored but never evaluated, so the
configuration was a silent no-op.
get_overdue_billing_threshold now reads the customer's row and falls back to
its customer group, mirroring get_credit_limit. The group's
bypass_credit_limit_check is deliberately not consulted: it is labelled for the
credit limit check at sales order and is unrelated to overdue billing.
get_customer_group_details also dropped the threshold when copying group rows
onto a customer, because it copied a single hardcoded field per table. It now
copies a list of fields per table, so credit_limit and overdue_billing_threshold
both carry over.
Add a "Hide Item Quantity in Print" option (Amount basis only) that omits the
qty and rate columns from the printed proforma, for a clean value-based
document that shows only item and amount.
In Amount basis, both qty and amount are now user-entered and the rate is
derived from them (rate = amount / qty). Previously qty was forced to the
ordered qty, which ignored an edited qty when switching basis.
- Pre-fill each line with the remaining (ordered minus already proformed) qty
and amount, so the default no longer trips the excess warning
- Place the Proforma Invoice action after the standard Create options
The work_order link filter in Stock Entry passed the string
`tabWork Order`.produced_qty as a filter value. It was never a real
column comparison: db_query coerces string values on numeric fields
with flt(), so the condition silently degraded to qty > 0, and on
backends that don't coerce text to numeric (postgres) such filters
fail with InvalidTextRepresentation.
Move the condition into a whitelisted search query that compares the
columns properly (qty > produced_qty), mirroring pick_list's
get_pending_work_orders.
Show a non-blocking notice below the item table when a line's total proforma
quantity (or amount) — this proforma plus already-issued ones — exceeds the
Sales Order line's ordered value. It updates live as the qty/amount or the
basis changes, and the user can still create the proforma.
Issued-proforma qty/amount are aggregated per line on demand (cancelled
proformas excluded); nothing is stored on the Sales Order.
set_transit_warehouse re-tested from_warehouse inside a guard that
already requires it, so the Company branch of its ternary was
unreachable: with no default on the source warehouse the field stayed
empty and Company.default_in_transit_warehouse was silently ignored.
Try the warehouse's default first, then the company's.
Cancelling a proforma should void it, not erase history.
- Persist the Cancelled status on cancel (db_set) and keep the PDF attached
- Proforma tab now lists cancelled proformas with a red status badge, so the
voided document and its PDF stay reachable for audit
Let a proforma be created by editing item amount instead of quantity, for
value/advance-style proformas.
- "Based On" (Quantity | Amount) on the proforma and the create dialog
- Amount basis keeps the ordered qty and derives a rate so the line totals
the entered amount; the PDF renders the same in-memory Sales Order copy
- Proforma Invoice Item now stores rate and amount
Remove the pending/proforma-qty machinery: it only fit staged, incremental
proformas and misrepresented the common whole-order / re-issued cases.
- Drop proforma_qty from Sales Order Item and its submit/cancel write-back
- Drop pending-qty aggregation and the over-qty soft warning
- The create dialog now pre-fills the ordered qty (editable down)
Cover partial proforma being non-blocking on delivery/billing,
pending-qty aggregation with cancelled proformas excluded, tax scaling
to the partial qty, over-qty as a soft warning, and the settings gate.
- Create > Proforma Invoice dialog with naming series, print format and
letter head selectors, and an item-wise pending-qty grid
- Proforma tab listing issued proformas with inline view/email actions,
shown only once at least one proforma exists
- Register the client script and add the connections dashboard link
Workspace re-export in #56864 duplicated every link in the Home and
Projects workspaces, so desk renders each link twice. Same issue as
55afd95b20. Bumped modified so existing sites re-sync.
- Submittable, non-accounting Proforma Invoice + Proforma Invoice Item
child doctype (in_create; posts no GL/stock, stores item + qty only)
- Server API: pending-qty aggregation per Sales Order line (issued
proformas only), make_proforma_invoice (sole creation path, gated on
the settings toggle), PDF rendered from an in-memory qty-adjusted copy
of the Sales Order and attached, send_proforma_email
- Non-blocking proforma_qty write-back to the Sales Order on submit/cancel
* feat: book Expenses Added To Stock GL entries for Stock Entry, Stock Reconciliation and LCV
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: make stock expense GL booking configurable via Accounts Settings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: skip stock expense booking for unconfigured companies, check flag once per compose
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The transfer flow ignored Consider Minimum Order Qty twice: the JS
handler force-reset the checkbox before fetching items, and the
purchase remainder left after allocating transfers from other
warehouses was never raised to min_order_qty (the check runs on the
total requirement before the split).
Drop the JS reset and apply min order qty to the purchase remainder,
in stock UOM before the purchase UOM conversion.
Two simultaneous allocations for the same item can both claim the same stock
on postgres: the picked-items locking read cannot see the rows another
in-flight creation is inserting, while MariaDB's gap locks make the creations
take turns. Advisory-gate set_item_locations per item (sorted against
deadlocks) so the second allocation waits, then subtracts the first's claim.
MariaDB unchanged.
Same hasattr pattern as repost_gate: an ERPNext ahead of its frappe build keeps
the status-quo serialization-failure retries instead of failing every stock
submission on postgres.
The for_update read in _ensure_idle_system only blocks new GL inserts on
MariaDB, via the gap lock it takes; a postgres row lock never blocks inserts,
so the guard silently degraded to the 5-minute recency check. LOCK TABLE IN
EXCLUSIVE MODE blocks writers (not readers) until the rename commits and NOWAIT
keeps the wait=False fail-fast, feeding the existing QueryTimeoutError path.
Postgres locking reads never see rows a concurrent transaction is inserting
(MariaDB's gap locks block the insert, then its locking reads return the fresh
row), so two concurrent writers for the same (item, warehouse) compute from the
same stale previous SLE and the loser overwrites Bin with a wrong absolute qty.
Today only the REPEATABLE READ serialization-failure retry catches this; the
gate makes correctness lock-based, covers the empty-history first-transaction
case (nothing exists to row-lock), and keeps negative-stock validation accurate
against concurrently inserted SLEs. Taken at the top of make_sl_entries (sorted
pairs, before the future_sle_exists cache warms) and in
update_entries_after.__init__ for the repost paths; re-entrant, released at
commit. MariaDB paths unchanged.