Whether a return row could be closed was decided by the sign of its amount
rather than by anyone's intent: the signed comparison happened to reject it,
and switching to magnitudes happened to allow it. Closing a whole return
document was already allowed, so allowing the row is the consistent choice,
but it should be written down.
`is_item_closable` now says so, and the behaviour is covered on both Delivery
Note and Purchase Receipt returns rather than only the one that was reported.
Also asserts the property worth protecting: closing a row on a return leaves
the original document's returned qty and per_returned untouched.
The server started treating billing amounts as magnitudes so return rows stay
closable, but the client kept the signed comparison. An unbilled return row was
filtered out of the Close Items dialog and hid the button entirely, so the row
the server would accept could not be reached from the form.
The pending amount shown in the dialog had the same flaw and would have
displayed zero outstanding on a row with a full amount left to credit.
Verified on a return Delivery Note: the row now appears with amount -500 and
pending amount 500.
Two issues from review.
The endpoint stored whatever integer the caller passed, so `closed=2` was
treated as closed by every truthy check while `validate_closed_source_items`
looks for exactly 1. A submit-authorized caller could suppress a row from
mapping and still submit an invoice against it. Verified: the value persisted
as 2, truthy checks saw it as closed, the guard query did not find it.
`is_item_closable` compared signed amounts, so an unbilled return row with a
negative amount looked already completed and could not be closed. Verified on a
real return Delivery Note: amount -500, billed_amt 0, predicate false.
Magnitudes are compared instead, matching how the percentage funnel already
treats these fields.
Removes the duplication left over from adding the doctypes one at a time.
`is_bundle_of_closed_row` was copied between the Sales Order and Delivery Note
mappers, differing only in a doctype name; it now derives that from the packed
item's parenttype. `is_item_closable` was identical on Delivery Note and
Purchase Receipt and repeated the billing clause on the order doctypes; billing
is now the default on AccountsController and the orders add their own
fulfilment axis. The close dialog config was written out per doctype, differing
by a qty field, a column label and a sentence, and is now two builders in
erpnext/public/js/utils/item_close.js.
Also stops counting closed rows as committed spend in the budget's ordered
amount, which sums per row but only guarded the parent status.
When every row is closed there is nothing left to measure against, so the
percentage falls back to the whole table and reports what actually happened.
Writing off two unbilled rows leaves per_billed at 0; writing off two rows that
were fully received leaves per_received at 100 and per_billed at 0. A constant
would have been wrong in one direction or the other.
A closed row was counted as fully settled, so closing one of two unbilled
Delivery Note rows pushed per_billed to 50 and the document read "Partially
Billed" with nothing invoiced. The same inflation applied to per_received,
per_delivered and per_picked.
Closed rows now leave the denominator instead of counting as done, so a
percentage stays a true measure of what was received, delivered or billed
against what is still expected. Writing off one of two unbilled rows leaves
per_billed at 0; billing the other takes it to 100. When every row is closed
nothing is outstanding, so the percentage is 100 by definition.
This mirrors how Delivery Note already excludes returned value from the
billing denominator.
Amending a cancelled document no longer inherits its write-offs. Frappe copies
no_copy fields when amending so a document can be corrected and resubmitted,
and unlike billed_amt or received_qty nothing recomputes `closed`, so the flag
would silently keep a row out of billing on the new document.
Extends row level close to the two documents where the goods have already
moved, so closing a row writes off what is left to bill rather than what is
left to fulfil. Nothing is released in Bin.
Delivery Note and Purchase Receipt are billed through their own services
rather than through status_updater, so their invoices declare the row link in
`closed_source_links`. Without it a closed row stayed invoiceable, since the
existing guard only walked status_updater args.
`per_returned` shares the percentage funnel on both doctypes and is excluded
from `SETTLED_BY_CLOSE`: closing a row writes off pending billing, it does not
turn the row into a return.
Closed rows now show a grey indicator in the items grid on all four doctypes.
Purchase Receipt had no indicator formatter at all and gets one.
Extends row level close to Sales Order. Closing a row releases its reserved
qty from Bin, settles its delivery and billing progress, and skips it when
creating a Delivery Note, Sales Invoice, Pick List, Material Request or
drop ship Purchase Order.
A row holding Stock Reservation Entries cannot be closed. Releasing physical
stock is a deliberate act, so the row has to be unreserved first rather than
having its reservations cancelled as a side effect.
Packed Items have no flag of their own and follow the Sales Order Item row
that bundles them, both in the mappers and in the reserved qty rollup.
Closed rows count as picked in the picking percentage, so the Pick List
button stops offering a list that would map no rows.
The close dialog moves to erpnext/public/js/utils/item_close.js and is shared
with Purchase Order, with each doctype supplying its own eligibility rule,
help text and columns.
REOPEN_STATUS now records why the per doctype value matters: Sales Order
re-checks the credit limit only on the literal "Draft", so each doctype reuses
whatever its own Re-open button passes.
Adds a `closed` flag on Purchase Order Item so a single line can be written
off without closing the whole order. Closing a row settles it: its pending
quantity stops holding the order open, its ordered qty is released from Bin,
and it is skipped when creating a Purchase Receipt or Purchase Invoice.
The percentage funnel in StatusUpdater counts a closed row as fully settled,
gated on the progress field so `per_returned` is unaffected — closing writes
off what is pending, it does not turn a row into a return.
Parent and row close stay independent owners: a closed parent does not stamp
its rows, and consumers check both. Closing the last open row closes the
parent; reopening any row reopens it. Reopening a parent whose rows are all
closed is blocked, since it would read as open while every row stayed
suppressed.
Rows that are already received and billed in full cannot be closed, matching
the existing document level gate. Rows received but not yet billed can be,
which writes off the remaining billable amount.
validate_sample_quantity and move_sample_to_retention_warehouse are
whitelisted and take company from the caller, which selects whose retention
warehouse gets read. The retained batch qty then reaches the return value and
the max-retained warning, so an authenticated user could probe another
company's stock with a known item and batch.
Gate the shared company -> warehouse resolution on read permission for the
Company, which respects User Permissions. validate_sample_quantity only grew
a company argument in this branch; move_sample_to_retention_warehouse already
took one, so this closes that path too.
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.
Item.retain_sample is validated against any company having one configured,
since Item is not company-scoped. The transaction company may still not be,
in which case get_batch_qty received warehouse=None and returned its batch
list, which was then compared numerically -> TypeError.
Resolve company -> retention warehouse through one helper that throws a
clear message instead, covering both the sample stock entry and the
whitelisted quantity validation.
Default Warehouse and Sample Retention Warehouse were global singles, so every
consumer had to re-check that the warehouse belonged to the transaction's
company before using it. Both now live on Company, under a new Warehouse
Defaults section that also collects the warehouse fields Company already had.
The company check moves to Company.validate_warehouses, which also rejects
group warehouses for all seven fields — a group or cross-company value there
already failed at SLE time, this just surfaces it at the source.
New companies get their Stores warehouse as Default Warehouse via
create_default_warehouses, replacing the setup-wizard and test-fixture code
that seeded the global.
row defaults to None but was dereferenced unconditionally when computing
the incoming rate for batch-tracked items, causing an AttributeError for
any caller (e.g. direct API calls) that omits row while still passing
batch_no. The other two row accesses in this function already guard
against None; this brings the incoming-rate check in line with them.
Production Plan read the conversion factor straight off the item's own
UOM child table, so an item with a purchase UOM but no matching row threw
"UOM Conversion factor not found" while Stock Entry silently resolved it
from the item's variant template or the UOM Conversion Factor doctype.
Resolve it the same way, and keep returning None when nothing is
configured anywhere so the missing-setup error still fires.
Default WIP, Finished Goods and Scrap Warehouse fields on Company listed
warehouses of every company. Filter them by the current company and
exclude group warehouses, matching the other warehouse fields.
- 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
`calculate_rm_cost` skipped rate refresh whenever `bom_creator` was set,
so neither the Update Cost button nor the BOM Update Tool could ever
refresh those BOMs. Every BOM in a multi-level tree carries the field, so
whole trees stayed frozen at their creation rates.
The guard replaced the removed `rm_cost_as_per == "Manual"` check in
0b63dbf, on the assumption that BOM Creator rows hold manual rates. They
do not: BOM Creator recomputes every row from `rm_cost_as_per` on save.
The BOM Creator tree identified a node by the parent's item code
(fg_item) instead of the specific BOM Creator Item row, so every
occurrence of a repeated sub-assembly shared one child set: expanding
any one of them listed the raw materials of all of them, and deleting
one wiped the raw materials of its siblings.
Key the tree on fg_reference_id and make the node value the row name,
matching the framework convention that a tree node's value is its
docname. Item code now travels as its own field for the label and for
the fg_item argument sent back on add/convert.
Fixes#57311
update_semi_finished_good_details assigned the current job card's
manufactured_qty to Work Order.produced_qty instead of accumulating it,
so a second job card on the same operation overwrote the first. Nothing
corrected it afterwards because StatusService.update_work_order_qty
returns early for track_semi_finished_goods work orders, leaving the
work order stuck below its planned qty with no way to progress.
Aggregate manufactured_qty and completed_qty over the operation's
submitted job cards instead.
Subcontracting Receipt was left out of #57493. Its title_field is "title",
so the template does get rendered on insert, but never again — the title
goes stale as soon as the supplier changes.
Point title_field at supplier_name like Purchase Receipt, and give the
title field the same shape as its subcontracting siblings. Existing rows
already hold a rendered name, so no data patch is needed.
Also guard the whole class of bug: a "{...}" default on a title field is
only ever rendered when title_field is "title".
* fix(stock): keep manufactured item rate at zero when inputs are free
when a finished item is produced from raw materials consumed at zero
valuation, the incoming rate fell back to the item's own valuation
rate (or BOM cost), valuing free inputs as output and inflating the fg
value on every production run.
add has_consumption_basis() to detect when the consumed cost is known
even if it is zero (consumed rows present, or a consumption entry
exists for the work order). when it is, skip the get_valuation_rate and
BOM-cost fallbacks so a real cost of zero is preserved.
* test(stock): cover manufacture rate for zero-valued raw materials
- manufacture from a free input keeps fg basic_rate and sle
incoming_rate/stock_value_difference at zero even when the fg already
carries a valuation in the target warehouse
- material consumption on with no consumption entry does not fall back
to bom/price-list rate for free inputs
- zero-valued consumption entry keeps the manufacture entry's fg rate
at zero
* fix(stock): value batched packed-item returns from the original bundle
when a return delivery note or sales invoice bundle is built via the
use_serial_batch_fields / sle-driven path, its voucher_detail_no keeps the
packed item instead of being remapped to the parent dn/si item. the return
valuation lookup then misses and the bundle values at zero, so the sle
stock_value_difference stays wrong even after a repost.
resolve the original dn/si item via the packed item's parent_detail_docname
when the direct lookup fails, so the return values from the original outward
bundle on both submit and repost.
* test(stock): cover batched packed-item return valuation on repost