Compare already-entered finished good qty against the work order qty plus
the configured overproduction percentage, mirroring the submit-time guard
in work_order/services/status.py, so a save is never rejected that the
submission contract would accept.
The stock_entry.py split (#54466) dropped check_duplicate_entry_for_work_order
and DuplicateEntryForWorkOrderError with no replacement. The Work Order still
throws StockOverProductionError when submitted entries exceed the planned qty,
but nothing blocks saving another Manufacture entry, draft or submitted, once
existing entries already cover the full work order qty.
Restore the validation in the manufacture purpose handler, gated to work
orders without track_semi_finished_goods, matching the pre-split behaviour.
set_service_items_for_finished_goods built a set and passed it to
get_subcontracting_boms_for_finished_goods, whose filter builder only
handles str and list. Whitelist type validation lax-coerces the set to a
list during HTTP requests and tests, hiding the mismatch, but from
console, bench execute or background contexts the set reaches
frappe.get_all verbatim and is inlined into invalid SQL on both MariaDB
and PostgreSQL.
Ref #57996
The stock_entry.py split (#54466) dropped check_if_operations_completed
and OperationsNotCompleteError with no replacement, so a Manufacture or
Material Consumption for Manufacture entry could be submitted against a
work order whose operations (job cards) were never completed.
Restore the validation in the manufacture purpose handler, gated to
work orders without track_semi_finished_goods, which has its own
per-operation enforcement.
* fix: skip zero-qty rows in make_sl_entries instead of reusing the previous entry
A row with zero actual_qty that is not a Stock Reconciliation never gets an
SLE, but the loop body still ran with the previous iteration's sle_doc:
repost_current_voucher and the bin update executed twice for the previous
row, or the whole call crashed with UnboundLocalError when the zero-qty row
came first. Skip such rows entirely.
* refactor: remove dead update_entries_after.update_bin_data
No callers anywhere in the codebase; it duplicates update_bin() with subtly
different semantics (no update_modified) and would only invite accidental
resurrection as a second Bin write path.
* refactor: rename bin.update_qty to update_qty_from_sle
Two unrelated functions circulated under the name update_bin_qty:
bin.update_qty (recomputes quantities from the ledger, aliased on import in
stock_ledger.py) and stock_balance.update_bin_qty (writes caller-supplied
absolute values, imported by six modules). Give the SLE-driven one a name
that states its semantics and drop the alias.
`time_diff_in_hours` returns hours, so `time_in_mins` needs `* 60`, not
`/ 60`. Matches `Job Card.validate_time_log_row`.
No behaviour change: the `doc.save()` on the next line runs Job Card's
`validate`, which recomputes `time_in_mins` correctly before the row is
written. This only stops the expression from reading as a bug.
* test: cover both repost branches and the no-repost case
* fix: queue repost for entries backdated by a concurrent submit
---------
Co-authored-by: nareshkannasln <nareshkannashanmugam@gmail.com>
The Italy regional setup created Custom Fields first_name/last_name on
Customer. Since #46281 added standard quick-entry fields with the same
names, every Italian site carries duplicate field definitions:
- the setup wizard creates the duplicates silently because it skips
validation, and any later Custom Field on Customer then raises
UniqueFieldnameError (#50915)
- without the duplicates, creating an Italian company aborts inside
install_country_fixtures; on MariaDB an interrupted fixture run
persists Custom Field documents whose columns were never added, after
which every Company insert fails with "Unknown column
'fiscal_regime'" (#57215)
Re-land the rename from #50921 (reverted in #53409): the fields become
italy_customer_first_name/italy_customer_last_name and the e-invoice
template reads the new names. The migration patch runs only on sites
with Italy fixtures, re-runs them, explicitly syncs the schema of every
affected doctype (create_custom_fields skips unchanged fields, so its
own schema sync cannot restore missing columns), copies the old column
values wherever the new field is empty (also on sites that removed the
duplicate fields with the documented manual workaround), and deletes
the duplicate Custom Fields last so an interrupted run stays resumable.
The old insert_after anchor "salutation" no longer exists on Customer;
the renamed fields anchor after customer_type.
update_qc_reference() writes the QI link and bumps the reference
document's modified timestamp via raw db writes, which emit no realtime
event. A reference form (Purchase Receipt, Delivery Note, Stock Entry,
Job Card) still open in the browser keeps the old timestamp and fails
the timestamp conflict check on the next save/submit, forcing a manual
refresh after every QI submit/cancel/delete.
Calling notify_update() on the reference publishes the standard
doc_update event, so an open, unedited form silently reloads and syncs
its timestamp. get_lazy_doc skips child table loading since
notify_update only needs the parent row.
make_bundle_for_material_transfer squares stock_value_difference for
outward rows instead of negating it. multiply by -1, matching the qty
negation on the line above.
no behaviour change: set_incoming_rate and calculate_qty_and_amount both
recompute the field from qty * incoming_rate before the bundle is saved.
covers the case where the percentages are correct but the accumulated
sum is 100.00000000000001. two rows can never drift, since the second
reconstructs exactly as 100 - first, so the case needs three rows.
the total of allocated_percentage was compared to 100 with exact float
equality, so a correct allocation could be rejected when the sum drifts
in binary floating point (10.0 + 58.02 + 31.98 -> 100.00000000000001).
round the total to the field precision before comparing, in both
SellingController.calculate_contribution and Customer.validate.
Cover both shapes: a single operation that books the loss itself, and a
chain where an earlier operation books it and the final operation loses
nothing, so the sum over the operations is the only correct source.
update_work_order_qty() returns early when track_semi_finished_goods is
enabled, so set_process_loss_qty() never ran and Work Order.process_loss_qty
stayed at zero even though the job cards and the work order operations had
booked the loss. The work order also never reached the Completed status,
since that needs produced_qty + process_loss_qty to cover the ordered qty.
Calling set_process_loss_qty() from that early return is not enough: the
final operation has no semi finished good bom, so its manufacture entry is
not from a bom, remove_fg_completed_qty() zeroes fg_completed_qty and
update_work_order_qty() is never reached at all.
The manufacture entries cannot be summed either. Each one is reset to
MAX(Work Order Operation.process_loss_qty), so every entry of a multi
operation chain carries the running maximum instead of the loss of its own
operation. Aggregate the operations instead, and refresh the work order from
the job card, which is where the operation loss is written.