The Sales Order check and the line rate and amount lived only in
make_proforma_invoice, so a proforma inserted through the REST API could
be submitted against a cancelled Sales Order, with lines from another
order or a stale amount. validate() now enforces them for every path.
The create dialog now shows each line's Sales Order description for
editing. The proforma line stores it, falling back to the Sales Order
description, and the print shows it under the item name.
Cancelling a Sales Order first needs its proformas cancelled, and the
tab then vanished, hiding the cancelled proformas that the list keeps
for audit. Show the list read-only instead.
A proforma is only created from its Sales Order, but a cancelled one
showed Amend to Administrator, and the amended copy could be saved
outside that path.
* feat(manufacturing): show stock UOM on blanket order items
A Blanket Order row's qty is in the item's stock UOM (orders made from
it use the stock UOM), but the row never said which unit that was.
Fetch the stock UOM onto the row and backfill existing rows.
* feat(manufacturing): close individual blanket order items
Use the row-level Close Items and Reopen Items actions from #57596 on
Blanket Order. A row can be closed while part of its qty is still
unordered. Closing every row closes the Blanket Order, and reopening a
row reopens it.
on_item_close_status_change becomes optional, like validate_item_close,
since a Blanket Order has no progress fields to recalculate.
* test(manufacturing): closing blanket order items
* feat(manufacturing): skip closed blanket order items when ordering
A closed row is left out when creating an order from the Blanket Order,
and out of the row picker and the item details lookup for that item.
Saving or submitting an order against it fails, and so does raising a
linked row's qty with Update Items.
* test(manufacturing): closed blanket order items are not ordered
* feat(manufacturing): show row state on blanket order items
Mark each Blanket Order row the way Purchase Order marks its rows: gray
when closed, green when fully ordered, orange while qty is still
pending.
* feat(manufacturing): close and re-open blanket orders
Blanket Order had no status field, so a finished or cancelled agreement
looked the same as an active one and could still be ordered against.
Add a status (Draft, Submitted, Closed, Cancelled) driven by the status
map, with Close and Re-open buttons under Status. A closed Blanket Order
hides its Create buttons. The patch backfills status from docstatus.
* test(manufacturing): blanket order status through close, re-open and cancel
* feat(manufacturing): stop ordering against a closed blanket order
Hiding the Create button is not enough: a Sales or Purchase Order can
still pick the Blanket Order on its rows. A closed Blanket Order is now
left out of the row picker and the item details lookup, and making,
saving or submitting an order against it fails. Update Items fails
when it raises the qty of a row linked to one.
* test(manufacturing): closed blanket order blocks new orders and qty increases
* perf(manufacturing): check closed blanket order on the loaded document
validate_against_blanket_order already loads each Blanket Order, so read
its status from there instead of querying it again per Blanket Order.
make_order and Update Items use the same method.
* fix(manufacturing): check every order row linked to a blanket order
A Sales or Purchase Order row could keep its Blanket Order link with
Against Blanket Order unticked, for example through the API or an
import. Validation skipped that row, so it could exceed the allowance or
order against a closed Blanket Order, yet its qty still counted as
ordered. Check every linked row, the same rows that ordered qty counts.
* test(manufacturing): linked order row is checked without against blanket order
* fix(manufacturing): lock the blanket order while an order is checked against it
Saving an order read the Blanket Order without a lock, so another
transaction could close it, or close one of its rows, between the check
and the commit. Load it FOR UPDATE, which locks the Blanket Order and
its rows until the order is saved; Close and Close Items wait for it.
Blanket Orders are locked in name order so two orders cannot deadlock.
The lock also serialises the existing allowance check. Update Items
takes the same lock.
* fix(manufacturing): check blanket order expiry in update items
Update Items checked only whether the Blanket Order was closed. Its To
Date can be edited after submit, so a submitted order dated after the
new To Date could still grow. Keep the order-level checks in one method,
validate_can_be_ordered, used by make_order, order validation and
Update Items.
* test(manufacturing): update items cannot raise qty after blanket order expires
* feat(stock): GL-only reposting from Stock and Account Value Comparison report (backport #59127)
* fix: take GL repost posting date from the voucher's stock ledger and skip GL-only rows
* fix: negative stock value for moving average item with mixed batchwise valuation (#59099)
* fix: negative stock value for moving average item with mixed batchwise valuation
* chore: remove redundant docstring
* test: restore frappe flags in a finally block
(cherry picked from commit 262fdf3e69)
* perf: skip legacy batch ledger lookups when no legacy entry exists (backport #59110) (#59111)
* perf: skip legacy batch ledger lookups when no legacy entry exists (#59110)
* perf: skip legacy batch ledger lookups when no legacy entry exists
(cherry picked from commit 0130d287f6)
* chore: fix conflicts
---------
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit 4af1b0db4e)
* perf: probe legacy batch ledgers on the batch_no index
The batch_no, item_code, warehouse index was dropped in v15, so the item and
warehouse probe fell back to the item ledger and read every row to look at
batch_no. Probe the batches the aggregates already filter on instead, which
the batch_no index covers.
---------
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Node unit tests evaluate the dialog script with stubbed frappe globals and
cover preserving pre-registered types, filtering them per transaction,
injecting their dialog fields, the submit and full-page return contracts
and rejecting unregistered types. yarn test:js runs them and a js unit
tests job runs them on every PR.
Also restores the track_voucher call on the full-page path that the merge
with develop dropped, and registers the after-submit reconciliation hooks
for every registered voucher type.
* fix(stock): return material that a document rejected in full
A receipt may leave the accepted quantity at zero when the whole row is
rejected, and so may an invoice that updates stock. Neither could be sent back:
the return mapper kept only rows with an accepted quantity, so it produced a
document with no rows at all, and the checks that a return carries something
looked at the accepted quantity alone.
Carry a row that has a rejected quantity, and count that quantity as material
going back.
* test(stock): cover the return of a receipt that rejected every unit
* fix(stock): send rejected material back at the rate it came in at
Material a transfer rejected in full went back to the in-transit warehouse with
no value at all, and the invoice, having no accepted quantity to price, fell
back to a single unit's rate.
Three things stood in the way of simply reversing the rate the material came in
at. A return of a transfer never read that rate. The value going back counted
the accepted quantity alone. And the rate was looked up against the accepted
warehouse, which had received nothing.
Ten units received at 100 into the rejected warehouse now go back as 1000, and
the entries say the same as the stock.
* test(accounts): cover the return of a transfer that rejected every unit
* feat(stock): move rejected material of an internal transfer on an invoice too
An invoice that updates stock moves the same material as a receipt, so it was
left with the fault the layers below fixed for the receipt: the rejected
material stayed in the in-transit warehouse and was counted in the rejected
warehouse as well.
Its entries had no accounting for rejected material at all, which is why those
layers stopped at the receipt. The entry that books the rejected warehouse
serves both cases now: an internal transfer credits the in-transit warehouse for
the accepted and the rejected material together, and an invoice that bills the
rejected qty keeps its cost on the supplier entry, as before.
`is_internal_receipt` covers an invoice that updates stock, so the qty, the
packages, cancelling and returning behave as they do on a receipt.
* test(accounts): cover an internal transfer invoice that rejects material
* fix(stock): validate a rejected package against the rejected quantity
On an invoice row the quantity field was forced to `stock_qty` for every
package, so the package of rejected material was compared with the accepted
quantity and a partly rejected internal transfer could not be saved.
Keep the override for the accepted package only, and let an explicitly passed
field through untouched.
* test(accounts): cover rejected batch material on an internal transfer invoice
* fix(accounts): let a stock updating invoice reject every unit of a row
A receipt may leave the accepted quantity at zero when the whole row is
rejected. An invoice that updates stock moves the same material, but the row was
refused with "Quantity for Item cannot be zero".
Share the receipt test with `is_internal_receipt()` so both exemptions follow
one rule.
* test(accounts): cover a stock updating invoice that rejects a whole row
* fix(stock): count a rejected package in the units a package counts in
A package holds stock units, but the package of rejected material was compared
with the rejected quantity as the row states it. A row of one box of twelve was
refused for holding twelve units, and resizing such a package cut it to the
number of boxes.
Read the row the way the package was built, so a rejected quantity is carried
through the conversion factor like every other quantity.
* test(stock): cover rejected material of a transfer bought in another unit
* feat(accounts): bill the rejected quantity on a stock updating invoice
Both rejected material settings are written for the receipt flow. The receipt
books rejected material against Stock Received But Not Billed, and the invoice
mapped from it carries the received qty with nothing rejected, so the supplier
pays for every unit received and that account clears.
An invoice that moves stock itself has no receipt to do that. It bills the
accepted qty alone, so the setting that asks for rejected material to be valued
had nothing to back the value it asked for, and the layer below zeroes it.
Such an invoice bills the received qty now when the setting is on, spreads the
valuation over the same qty, and debits the rejected warehouse from its own
entries, so the supplier entry carries the cost. An internal transfer bills
nothing of the sort: its material is paid for by the warehouse it came out of.
* test(accounts): cover the rejected quantity billed on a stock updating invoice
* fix(accounts): tell the form whether the rejected quantity is billed
The form read the two settings off the document, where they never appear: a
doctype settings map drives the settings panel of a form, it does not put those
fields on the document. The amount of a row stayed at the accepted qty until the
document was saved and the server worked it out again.
Both settings go into the boot now, and the form reads them from there. An
internal transfer is left alone, as it is on the server.
* test(accounts): give the project purchase cost test its own payable account
The test bills in USD but let the payable account be chosen for it, so it passed
only when an earlier test had left a payable account in that currency behind.
* test(accounts): read the invoices back before cancelling them
Submitting an invoice against a project writes to it again, so the copy the test
holds is already behind and cancelling it fails on the timestamp.
* test(accounts): restore the buying settings the tests change
Both tests put the settings back by hand, one of them to a value the site never
had, so every test that ran afterwards saw the rejected quantity billed. Let the
suite save and restore them.
* fix(accounts): spread a discount over the quantity the invoice bills
An invoice that bills the rejected quantity carries an amount for every unit
received, but the net rate was still divided by the accepted quantity alone. Six
accepted and four rejected at 100 with a tenth off gave a net rate of 150 and
wrote that to the item as its last purchase rate.
Divide by the quantity the amount was built from. Nothing changes for a document
that does not bill the rejected quantity.
* test(accounts): cover a discount on an invoice that bills the rejected quantity
* fix(accounts): book the rejected warehouse from the stock it received
The entry was written only while the setting that bills the rejected quantity
was on, so an invoice reposted after that setting changed lost the entry while
keeping the supplier credit that paid for the material, and reposting failed on
the difference. The stock the invoice moved is what the entry records, so read
that instead. Material that nothing paid for carries no value and still books
nothing.
A return that stands on its own compared what the supplier was credited with the
stock of the accepted warehouse alone, and booked the rejected material a second
time as a variance. Count both warehouses, since both come back.
* test(accounts): cover the rejected warehouse after a repost and on a return
* docs(buying): say where the rejected quantity is billed
An invoice that updates stock bills the rejected quantity too, with no receipt
in front of it.
* docs(buying): drop the list of documents from the billing description
The purchase cycle says it.
* fix(stock): stop valuing rejected material on a stock updating invoice
`set_valuation_rate_for_rejected_materials` is written for the receipt flow. A
receipt books rejected material against Stock Received But Not Billed, and the
invoice mapped from it carries the received qty with nothing rejected, so the
supplier pays for all of it and that account clears.
An invoice that moves stock itself has no receipt to do that. It bills the
accepted qty alone, while the setting still gave its rejected material the
invoice rate, so the rejected warehouse received stock value that nothing paid
for and no entry backed.
One predicate now answers whether rejected material carries value, for plain
rows and for rows tracked by a package alike. Material of an internal transfer
always does, since its value was credited out of the in-transit warehouse. A
receipt follows the setting. A stock updating invoice does not, until it bills
that material.
* test(accounts): cover rejected material value on a stock updating invoice
* fix(stock): read the transit test from where this layer keeps it
The layer below asks the package itself whether the material came from an
in-transit warehouse. Here that question is answered by the buying settings.
* docs(buying): say where rejected material is valued
The setting reaches every document of the purchase cycle that receives material,
not the receipt alone.
* fix(stock): deduct rejected qty from the in-transit warehouse
On an internal transfer the receipt took only the accepted qty out of the
in-transit warehouse, while the rejected qty was booked into the rejected
warehouse, so the rejected material was counted in both.
It also carried stock value, because an internal transfer anchors every inward
entry to the rate of the delivery note, but the rejected warehouse got no
accounting entry unless Buying Settings asked for one. The stock value and the
account value then disagreed.
The entry for the in-transit warehouse covers the accepted and the rejected qty
now, and the rejected warehouse is booked whatever that setting says, since the
value came out of the in-transit warehouse either way.
* test(stock): cover rejected qty on an internal transfer receipt
* fix(stock): let rejected serial and batch material leave the in-transit warehouse
A serial or batch item rejected on an internal transfer could not be received at
all. The package for the in-transit warehouse is copied from the delivery note,
and the copy was never resized, because the check compared a positive qty against
the negative total of an outgoing package.
The package of a row follows the split now. A row that rejects material carries
the package of its accepted warehouse, holding the accepted material alone,
which is the entry it belongs to and the total the desk sets its accepted qty
from. A row that rejects nothing keeps the package of the in-transit warehouse it
came out of. Editing the split moves the package from one to the other, and a row
that accepts nothing carries no package at all.
The entry for the in-transit warehouse gets a package of its own, holding the
accepted and the rejected material together. A landed cost voucher or a repost
reuses it rather than building a second one, which would make the batch qty count
the material twice.
Rejected material of an internal transfer keeps its rate, since its value was
credited out of the in-transit warehouse; refusing it a rate left the difference
to be written off. Cancelling reverses that warehouse with the package its entry
posted, after the rejected warehouse, so serial numbers are not put back and
taken out again. A return builds an inward package covering both.
The resize also fixes an ordinary partial receipt of a tracked item bought in
another UOM: the package is sized in stock UOM, which is what the row is
validated against.
* test(stock): cover rejected serial and batch material on an internal transfer
* fix(stock): build the package of rejected material on an internal transfer
A receipt of an internal transfer builds one package and stops there, so a
tracked row that rejects material had nothing to say where that material came
from. The desk offers no field for it either, and the receipt could not be
submitted: the entry for the in-transit warehouse was handed the package of the
accepted warehouse.
The row takes a package of its own for the rejected material now, built from what
the delivery note put in the in-transit warehouse. Moving the package of a row
between the two warehouses also reads that delivery note package, instead of the
package it happens to hold, which no longer covers the qty once the split changes.
* test(stock): cover the package built for rejected batch material
* fix(stock): empty the in-transit warehouse when every unit is rejected
A receipt that rejects the whole qty left the material in the in-transit
warehouse and added it to the rejected warehouse as well, because the entries of
a row were made only when there was an accepted qty. They are made from the qty
that leaves the source warehouse now, so a row with no accepted qty is posted
like any other. One gate replaces two nested ones, which moves the body of the
loop out by a level; read the diff with whitespace ignored.
Such a row also carried no valuation rate, since the rate of an internal transfer
is taken from the accepted qty alone, and the rejected warehouse was then debited
without a matching credit. The rate falls back to the rejected qty.
Returning that material from the rejected warehouse left the in-transit warehouse
holding the qty at no value and wrote the value off: the return has no delivery
note reference, so its entry for that warehouse got no rate, and the entry
against it was suppressed because rejected material normally carries none. It
takes the rate of the return now, and the value of the source warehouse is signed
rather than absolute, so a return debits the warehouse the material returns to.
* test(stock): cover an internal transfer with every unit rejected
* fix(stock): keep resizing the package of a row whose qty changed
The package of rejected material was built between the two branches that build
and resize the package of a row, which left the resize attached to it. A row
whose qty changed after its package was built stopped being resized, and the
receipt was refused for the qty it no longer had.
* test(stock): state the rejected valuation setting the transfer test relies on
* fix(stock): keep a charge off the rejected material of a transfer
A landed cost voucher rebuilds the receipt with the charge spread over the
material it accepted, and the package of rejected material was then valued at
that same rate. Three units rejected out of a transfer worth 100 each came to
351.43 after a charge of 120, and the difference was credited to Cost of Goods
Sold to make the entries balance.
Rejected material of a transfer keeps the value it arrived in transit with. The
share of the charge that would have sat on it is expensed instead.
* test(stock): cover a charge on a transfer that rejected material
* fix(stock): let rejected serial and batch material leave the in-transit warehouse
A serial or batch item rejected on an internal transfer could not be received at
all. The package for the in-transit warehouse is copied from the delivery note,
and the copy was never resized, because the check compared a positive qty against
the negative total of an outgoing package.
The package of a row follows the split now. A row that rejects material carries
the package of its accepted warehouse, holding the accepted material alone,
which is the entry it belongs to and the total the desk sets its accepted qty
from. A row that rejects nothing keeps the package of the in-transit warehouse it
came out of. Editing the split moves the package from one to the other, and a row
that accepts nothing carries no package at all.
The entry for the in-transit warehouse gets a package of its own, holding the
accepted and the rejected material together. A landed cost voucher or a repost
reuses it rather than building a second one, which would make the batch qty count
the material twice.
Rejected material of an internal transfer keeps its rate, since its value was
credited out of the in-transit warehouse; refusing it a rate left the difference
to be written off. Cancelling reverses that warehouse with the package its entry
posted, after the rejected warehouse, so serial numbers are not put back and
taken out again. A return builds an inward package covering both.
The resize also fixes an ordinary partial receipt of a tracked item bought in
another UOM: the package is sized in stock UOM, which is what the row is
validated against.
* test(stock): cover rejected serial and batch material on an internal transfer
* fix(stock): build the package of rejected material on an internal transfer
A receipt of an internal transfer builds one package and stops there, so a
tracked row that rejects material had nothing to say where that material came
from. The desk offers no field for it either, and the receipt could not be
submitted: the entry for the in-transit warehouse was handed the package of the
accepted warehouse.
The row takes a package of its own for the rejected material now, built from what
the delivery note put in the in-transit warehouse. Moving the package of a row
between the two warehouses also reads that delivery note package, instead of the
package it happens to hold, which no longer covers the qty once the split changes.
* test(stock): cover the package built for rejected batch material
* fix(stock): keep resizing the package of a row whose qty changed
The package of rejected material was built between the two branches that build
and resize the package of a row, which left the resize attached to it. A row
whose qty changed after its package was built stopped being resized, and the
receipt was refused for the qty it no longer had.
* fix(stock): keep a charge off the rejected material of a transfer
A landed cost voucher rebuilds the receipt with the charge spread over the
material it accepted, and the package of rejected material was then valued at
that same rate. Three units rejected out of a transfer worth 100 each came to
351.43 after a charge of 120, and the difference was credited to Cost of Goods
Sold to make the entries balance.
Rejected material of a transfer keeps the value it arrived in transit with. The
share of the charge that would have sat on it is expensed instead.
* test(stock): cover a charge on a transfer that rejected material
* fix(stock): deduct rejected qty from the in-transit warehouse
On an internal transfer the receipt took only the accepted qty out of the
in-transit warehouse, while the rejected qty was booked into the rejected
warehouse, so the rejected material was counted in both.
It also carried stock value, because an internal transfer anchors every inward
entry to the rate of the delivery note, but the rejected warehouse got no
accounting entry unless Buying Settings asked for one. The stock value and the
account value then disagreed.
The entry for the in-transit warehouse covers the accepted and the rejected qty
now, and the rejected warehouse is booked whatever that setting says, since the
value came out of the in-transit warehouse either way.
* test(stock): cover rejected qty on an internal transfer receipt
* test(stock): state the rejected valuation setting the transfer test relies on
* refactor(selling): remove ensure delivery based on produced serial no
the ensure_delivery_based_on_produced_serial_no checkbox on sales order
item only ever validated itself at sales order save: serialized item,
active bom, and the same setting on every row of an item. nothing
downstream read the flag, so a delivery note or stock updating sales
invoice could ship any serial no and submit silently. the field promised
a guarantee it never enforced.
remove the field, its typing entry and validate_serial_no_based_delivery.
stock reservation on the sales order and work order is the supported way
to hold produced stock for an order. the database column is left in place
by migrate, so no data is dropped.
* chore(stock): remove dead reserved serial and batch no validators
validate_reserved_serial_nos and validate_reserved_batch_nos lost their
only callers in a20951e1cd ("fix: reserved serial nos validation"), which
moved serial level reservation checks into serial and batch bundle but
left both functions in stock ledger. drop them along with the three
imports that only they used.
* fix(stock): seed bin values when cancelling a stock voucher
cancellation flags every sle of the voucher before update_entries_after
runs, so get_sle_against_current_voucher returns nothing and the seeding
added in #57380 never fires. prev_sle_dict stays empty, update_bin()
writes nothing, and the bin keeps the stock value and valuation rate it
had before the cancellation while its quantity is restored.
seed from the args when the query comes back empty, leaving the existing
anchor in place whenever a live entry shares the posting datetime.
* test(stock): cover bin stock value after cancelling a transfer
a transfer between two warehouses that both hold stock, then cancelled:
both bins must return to their previous quantity, valuation rate and
stock value. fails on develop with 500.0 != 1000.
Saving a Customer runs create_primary_address, which calls frappe.set_value on the
linked Address to ensure is_primary_address. That is a full document save, so
ERPNextAddress.on_update fires and writes the address display back to
Customer.primary_address with update_modified=True. The Customer row's modified
moves after the document has already been written, so the form keeps the older
timestamp and the next save from the same form fails check_if_latest with
TimestampMismatchError.
primary_address is a denormalized display cache, not a user edit, so writing it
must not move the optimistic lock timestamp.
* fix: do not zero out backdated stock at a stock reco adjustment entry
* fix: keep a stock reco adjustment entry value-only on cancel and refresh
* fix: read stock reco adjustment rows once and value them from the ledger
Other apps can register additional "Create Voucher" document types in
erpnext.accounts.bank_reconciliation.voucher_types with their own dialog
fields, applicability check and creation call. Payment Entry and Journal
Entry now go through the same registry, which also removes the duplicated
create and edit-in-full-page call blocks.
perf(stock): chunk the serial and batch entry backfill patch (#59076)
* perf(stock): chunk the serial and batch entry backfill patch
* fix(stock): support postgres in the serial and batch entry backfill patch
(cherry picked from commit 86fe0c1f4b)
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
* fix: use one formula environment in validator and engine
* test: cover shared formula environment
* fix: use distinct dummy values when test-evaluating formulas
* fix: reject line references that can't be used in a formula
* fix: drop the undefined-reference check
* fix: normalise line references before validating
* fix: normalise formulas on save instead of during validation
* fix: escape validation messages where they are rendered
* fix: strip the formula in the engine instead of relying on the validator
* fix: ignore division by zero when test-evaluating formulas
* fix(stock): use stored posting_datetime for repost boundary
get_stock_ledger_entries re-derived posting_datetime from posting_date and
posting_time on every call, discarding the stored value its callers pass in.
when a row's stored posting_datetime differs from that pair, the replay window
is built from the wrong instant: the row falls outside the range filter and is
never recomputed, while get_previous_sle still selects it as the opening
balance and reuses its stale qty_after_transaction. every later entry inherits
the error, leaving bin qty adrift from the sum of its ledger.
derive the boundary only when the caller has not supplied one.
* fix(stock): match current voucher sle on stored posting_datetime
get_sle_against_current_voucher selected rows with an equality check against a
posting_datetime re-derived from posting_date and posting_time. a row whose
stored posting_datetime differs from that pair matches nothing, so reposting
the voucher silently processes zero entries and the row can never be corrected
through its own voucher.
read the timestamp from the stored row when the sle is known, and derive it
only as a fallback.
* test(stock): cover repost with diverged posting_datetime
add a repack scenario whose incoming entry stores a posting_datetime one
microsecond before its own posting_time. asserts the voucher lookup still
finds that entry, and that reposting replays it instead of reusing its stale
qty_after_transaction, which otherwise left bin qty at 115 against 615 of
recorded movements.
* fix(stock): consume batch slots newest first for LIFO items
The valuation method decided which end of the queue an issue consumed
from, but only for stock carrying no batch or serial number. Batch slots
were always consumed from the head, so a LIFO item reported its oldest
stock as still on hand when it had been issued.
Slots of one batch valued batchwise share a date, so the direction of
the walk cannot change what they report. Slots pooled across batches
carry the date of the batch that filled them, and there the wrong stock
aged.
Pass the valuation method through to the batch walk and read the queue
from the tail for a LIFO item, as the untagged walk already does.
* test(stock): cover LIFO consumption of pooled batch slots
Issue against the newer of two pooled batches on a LIFO item and assert
the September slot is consumed rather than the January one.
* fix(stock): scope stock ageing batch and serial age to the warehouse
The first inward posting date of a batch or serial number was cached
under the identity alone, so the age of a row depended on which stock
ledger entries the filters let the report scan.
A batch received into WH A and transferred to WH B aged from the WH A
receipt in an unfiltered run, but from the transfer date once a
warehouse filter was applied. Same stock, same warehouse, same to date,
two different ages.
Key the cache on the warehouse as well. Repeated receipts of one batch
into one warehouse still age from the first of them, and a transfer now
restarts the clock in the destination warehouse, as it already does for
stock that carries no batch or serial number.
* test(stock): cover warehouse scoped batch age in stock ageing
A batch received into one warehouse and transferred to another aged
from the first receipt in an unfiltered run and from the transfer once
the warehouse filter narrowed the scan. Assert both runs report the
transfer date.
* test(stock): cover warehouse scoped serial age in stock ageing
The cached date is keyed on the warehouse for serial numbers as well as
batches, and only the batch half was covered. Assert a serial
transferred between warehouses ages from the transfer in both a full
and a warehouse filtered scan.
Insert the batch fixture with ignore_if_duplicate instead of checking
for it first.
* test: stop four tests from passing without running
Three advisory-lock tests return early on MariaDB:
if frappe.db.db_type != "postgres":
return
A bare return reports the test as passed, so the MariaDB CI job shows
green for a test it never ran. skipTest reports it as skipped.
test_stock_reco_with_opening_stock_with_diff_inventory returned early
when the custom "Plant" DocType already existed. DocType creation is
DDL and survives the test transaction, so the test ran once on a fresh
site and silently did nothing on every run after that. Create the
DocType only when it is missing and let the test run either way.
Its closing loop also asserted inside an if/elif over the ledger rows,
which verified nothing if the dimension came back unset. Compare the
whole {plant: qty} mapping instead.
* test: give the job card validator tests a real job card
Both tests looked for a submitted Job Card left behind by another test
and returned when they did not find one:
jc_name = frappe.db.get_value("Job Card", {"docstatus": 1})
if not jc_name:
return # skip if no job cards in test data
Run in isolation they asserted nothing and still reported a pass, and
they were the only coverage for validate_job_card_fg_item and
validate_job_card_item.
Move them to test_job_card.py, where the Work Order and BOM fixtures
that produce Job Cards already live, and build the Job Card in the test.
The finished-good case needs a card that carries one, so it goes through
a track_semi_finished_goods BOM. Both now assert on the message text, and
both fail if the validator body is removed.
Four tests in test_stock_entry.py each cover only an early-return guard:
def test_validate_job_card_item_skips_when_no_job_card(self):
se = frappe.new_doc("Stock Entry")
se.job_card = None
se.validate_job_card_item() # must not raise
That exercises `if not self.job_card: return` and nothing else. The
mismatch tests next to them already cover the behaviour these validators
actually implement.
Three tests in test_payment_request.py assert against their own mock.
_is_v2_gateway delegates to payments.utils.is_v2_gateway; all three mock
that delegate to return False and then assert the result is False, for
inputs (None, "", "NonExistentGateway12345") that take an identical code
path. The mock decides the outcome, so the assertion holds regardless of
what ERPNext does. The three tests covering the real branches --
delegation, a False delegate, and the exception fallback -- are kept.
Rapid successive edits to Net Purchase Amount could fire overlapping
set_finance_book calls; if an older request's response arrived after a
newer one, it could overwrite Finance Books with values computed from
a stale amount. Now the callback only applies a response if the fields
it was based on still match the form's current values.
Previously, checking "Calculate Depreciation" (or picking the Item)
before typing in "Net Purchase Amount" left the Finance Books table
empty, because the depreciation schedule was only built at the moment
those fields already had values. Entering the amount afterward only
updated existing Finance Books rows, so an empty table stayed empty.
Now, entering the amount also builds Finance Books from scratch if it
was left empty, regardless of the order fields were filled in.
* fix: include rejected qty in Purchase Receipt billing base
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: per billed stays 100% for fully rejected receipt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(accounts): skip received/rejected qty checks on non-stock return
A Purchase Invoice without Update Stock writes no Stock Ledger Entry, so
received_qty and rejected_qty on its rows move no stock and bill no
amount. The server never derives or validates them either:
validate_accepted_rejected_qty only runs when update_stock is set.
validate_quantity still counted both columns against the source invoice.
Whatever value the form last wrote to the read-only received_qty was
tallied as returned, so a partial return that lowered qty locked out the
rest of the invoice with StockOverReturnError.
Restrict the two columns to documents that actually carry an
accepted/rejected split: Purchase Receipt, Subcontracting Receipt, and a
Purchase Invoice with Update Stock. qty stays validated in every case, so
the billed quantity is still capped at what the source invoice billed.
* test(accounts): cover partial returns of a non-stock invoice
Fails before the previous commit with StockOverReturnError on the second
return, because the stale received_qty carried by the first return is
tallied as a full return of the invoice.
* refactor(accounts): pass frm into the Purchase Invoice hide_fields
hide_fields took a doc but reached for cur_frm to get the grid and to
refresh, so it only worked on whichever form happened to be current. Take
frm instead: all three callers already have one.
frm.toggle_display replaces the hide_field / unhide_field globals, which
resolve the docfield through cur_frm the same way. var becomes let/const.
No change in behaviour.
* fix(accounts): hide stock columns without Update Stock
Received Qty, Rejected Qty and the warehouse section were shown on any
return, including one that updates no stock. received_qty is read-only
there and rejected_qty moves neither stock nor billed amount, so the grid
offered values the user could not correct and the form could not keep in
step with qty.
Show the group only when the invoice updates stock, matching an ordinary
Purchase Invoice. Nothing on these rows needs a warehouse either:
validate_warehouse only checks the warehouses that are set.
a dunning was resolved as soon as the invoiced sum was settled, because the
status was derived from the invoice outstanding alone. paying an invoice
without the interest and fee therefore closed the dunning and lost the
interest: a fresh dunning finds nothing overdue to charge it on.
the dunning amount is never a receivable, it only reaches the ledger as a
negative deduction on a payment entry made from the dunning. link that row
to the dunning so what has been collected is known, and resolve a dunning
only once the invoiced sum and the dunning amount are both paid. a dunning
resolved by hand keeps its status, so waiving the interest stays possible.
the deduction is a company currency field, so book and measure the dunning
amount through base_dunning_amount instead of the transaction currency one.
an interest-only payment leaves every invoice outstanding untouched, so
update the linked dunnings from the payment entry itself instead of relying
on the outstanding amount to change. such a payment also has to be built
from what is left to collect, not from the totals the dunning was raised
with, which are stale by then.
* fix(stock): distribute additional costs when incoming items have no value
when every incoming row has zero basic amount, for example a raw material
purchased at zero rate, distribute_additional_costs returned early and left
additional_cost at 0 on the finished item. the charges were never capitalised
into valuation and the balancing debit stayed in stock adjustment instead of
reaching stock in hand.
the gl composer had its own quantity fallback for the same case, but it divided
by the qty of every row rather than the incoming ones, so a manufacture entry
booked only a fraction of the cost to the expense account, and it apportioned
by qty while valuation apportions by stock qty, which split rows of differing
conversion factors two different ways.
both sides now take the rows, the basis and its total from a single
get_additional_cost_allocation, falling back to transfer_qty so valuation and gl
cannot disagree. the basis is unchanged whenever the incoming rows carry value.
* test(stock): cover additional cost distribution for zero valued items
adds qty based distribution cases for manufacture and material receipt, a
manufacture entry asserting the whole cost reaches its expense account, and a
conversion factor case asserting the gl split matches the valuation split.
updates test_total_basic_amount_zero, which asserted the cost landing in stock
adjustment rather than being capitalised.
* fix(manufacturing): share transfer stock across Production Plan rows
Fetch available stock once per item for the total demand and consume it across rows. Combine batch splits into one transfer row per source warehouse and demand. Round transfer quantities to the plan item precision so the shared balance never leaves a float residue as an extra row.
* test(manufacturing): cover shared transfer stock across Production Plan rows
* fix(manufacturing): apply MOQ once across Production Plan rows
Apply Minimum Order Qty after stock and transfer allocation, grouped by item, warehouse, request type, supplier and Sales Order. Material Requests and Purchase Orders are raised per Sales Order and a Purchase Order rejects an item below its minimum, so each Sales Order buys at least the minimum or is covered by the surplus of an earlier one. The surplus is the quantity actually purchased beyond demand, so purchase UOM and whole-number rounding carry forward.
* test(manufacturing): cover MOQ once across Production Plan rows
* fix(banking): reset scroll on searching accounts
* fix(banking): show only past dates in date filter
* fix(banking): clean up line heights and remove beta badge
* fix(banking): show accurate count of import progress
fix(banking): show latest 20 imports instead of 10
* fix(banking): layout sizing needs to be preserved on page change
* fix(banking): cleaner bank balance UI
* fix(banking): correctly parse Cr/Dr values in statement importer
* Update banking/src/components/features/BankReconciliation/BankBalance.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix: user not able to set valuation rate zero in stock reco
* fix: wrong difference amount when valuation rate is zero
* fix: blank valuation rate should not be treated as a change
* fix(bank reconciliation): match Payment Entries on the bank-side amount
get_pe_matching_query() ranked and filtered on pe.paid_amount while the
match card displayed pe.base_paid_amount_after_tax, so the amount used for
the exact match never matched the amount shown.
Both now use the amount that actually hits the bank account, in that
account's currency: received_amount_after_tax when the bank account is
paid_to (deposit) and paid_amount_after_tax when it is paid_from
(withdrawal). This is the same convention as the Bank Reconciliation
Statement report and matches the bank GL entry that reconciliation
allocates against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(bank reconciliation): cover bank-side amount matching
Two cases the previous behaviour got wrong or could regress on:
- A deposit from an internal transfer where the paid and received sides
differ by a charge. The match must show, and compare against, the
amount that reached this bank account.
- A withdrawal, which still matches on the paid side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(stock): subtract stock qty of same-document rows from batch availability
filter_batches subtracted a row's transaction-UOM qty from batch quantities that
are in the stock UOM, so a row in an alternate UOM freed less of the batch than
it consumes and the auto-pick could assign a batch that cannot cover the new
row.
* test(stock): cover batch availability with alternate UOM rows
* fix(stock): assign batch_no only when the first batch covers the full qty
The auto-pick loop reduced the requested qty per batch and left the last
visited batch on the row, so a qty spanning batches got a batch that could
not fulfil it and failed at submit with a misleading negative-stock error.
Assign the first batch in pick order only when it alone covers the qty.
Otherwise leave batch_no empty so the auto-created Serial and Batch Bundle
splits the qty across batches at submit.
Batches are queried without qty so filter_batches subtracts rows already in
the document from the uncapped batch quantities. Querying on a copy also
stops get_auto_batch_nos from clearing warehouse on the kwargs later used to
pick serial nos.
Fixes#58640
* test(stock): cover batch auto-pick when qty spans batches
* fix(stock): pick serial nos across batches when no batch covers the qty
With batch_no left empty for a qty that spans batches, the serial pick for a
serialised and batched item filtered on [None] and returned nothing, leaving
the row with neither identity. Skip the batch filter when there is no batch so
the serial nos are picked in the configured order across batches; the bundle
built at submit derives each serial's batch.
* test(stock): cover serial pick across batches for batched serial items
* fix: production plan summary report tree structure and quantities
* feat: production plan visualizer page
* feat: single screen production plan visualizer with material readiness
* fix: show live stock and received status for production plan materials
* fix: remove duplicate border under production plan visualizer header
* fix: drop page head border on production plan visualizer
* fix: add horizontal margin to production plan visualizer
* fix: apply record level permissions and resolve shared material owners
* fix: list shared raw materials under every finished good that needs them
* feat: open linked documents in a side panel from the visualizer
* fix: never fall back to stored qty when warehouse stock is not readable
* fix: include directly consuming finished goods in material ownership
* fix: show each finished good's own share of shared material demand
* fix: match production plan quantities and labels in the visualizer
* fix: resolve nested sub assembly owners when parent link is missing
* fix: keep every matching owner when resolving sub assemblies by item code
* feat: flat work order list in place of the items to manufacture tree
* fix: flatten items to manufacture rows without changing the table design
* fix: align table numbers, units and progress cells
* fix: scope nested owner resolution to the same sales order
* fix: keep quantity columns numeric and move uom to the item line
* fix: recover all finished goods for consolidated sub assembly rows
* fix: scope raw material owners to the same sales order
* feat: batch split operation to produce child batches per piece
* fix: single input validation, weight conserving lineage, cancel cleanup and naming race for batch split
* fix: delete cancelled batch split bundle along with unused child batches
* fix: retain all child batches when any sibling of a split bundle is in use
* fix: run batch split cancel cleanup only for batch split entries
* fix: make batch split flag read only on stock entry type
* fix: restrict cancel cleanup to child batches minted by the cancelled entry
* refactor: name child batches from item batch series and retain them on cancel
* feat: batch split tree report for parent to child batch traceability
* refactor: source each piece wholly from a single parent batch
* fix: weight per piece sizes the child batches instead of scaling raw material consumption
* fix: apportion child batch lineage proportionally to parent batch quantities
* fix: cap child batch lineage at the whole piece capacity of each parent batch
* fix: exclude batches of cancelled split entries from the batch split tree
* feat: alternative finished goods conversion against work order
* fix: tighten validations for finished goods conversion
* fix: postgres compatible lock and qty checks post transfer qty for fg conversion
* fix: default single alternative item and hide Change Finished Item button without alternatives
* feat: option to skip delivery note for service items in sales order
* fix: reset stale skip delivery flags when setting is disabled
* fix: clear stale skip delivery note flag for non-sales order types
* fix: reset auto skip delivery flags on switch to maintenance order
* refactor: replace sales order skip_delivery_note with item level skip_delivery
* chore: drop skip delivery migration patch
* fix: honor legacy skip_delivery_note flag instead of data migration
fix(accounts): resolve subscription plans for any reference doctype and require read permission
get_subscription_details() was hardcoded to only resolve plans for
Sales Invoice, but is_a_subscription in make_payment_request() was set
for any reference doctype with a `subscription` field. Since Purchase
Invoice also has this field (supplier-side subscriptions), creating a
Payment Request against a subscription-linked Purchase Invoice set
is_a_subscription=1 with an empty subscription_plans table.
get_subscription_details() is also whitelisted with no permission
check, letting any logged-in user query which Subscription/plan/qty is
linked to an arbitrary Sales Invoice or Purchase Invoice.
Make plan resolution generic (guarded by Meta.has_field so doctypes
without a subscription field never hit a nonexistent column), derive
is_a_subscription from the resolved plans so the two can't disagree,
and add a frappe.has_permission read check before returning any data.
In a multi-currency Internal Transfer, the paid-vs-received difference was
booked entirely to Exchange Gain/Loss, so a bank charge entered as a deduction
pushed the Difference Amount non-zero and blocked submission. The exchange
gain/loss row now absorbs only the residual after user-entered deductions,
letting a Bank Charges row and the Exchange Gain/Loss row coexist and net to
zero.
* fix(stock): carry accounting dimensions from Landed Cost Voucher charges into GL entries
* feat(stock): add accounting dimension fields to Landed Cost Taxes and Charges
The charge row had no dimension fields, so a dimension marked mandatory for
Profit and Loss accounts could not be supplied anywhere on the voucher.
Add the accounting dimensions section, cost center and project, and register
the doctype in accounting_dimension_doctypes so custom dimension fields are
created on it. The section and column break are required for that hook to
place the generated fields correctly.
Cost center deliberately omits the ":Company" default used by Purchase Taxes
and Charges: this child table is also the additional costs table on Stock
Entry and Subcontracting Receipt, and auto-filling it there would change
existing postings.
* refactor(stock): group landed cost charges by expense account and dimensions
get_item_account_wise_lcv_entries keyed its inner map by expense account
alone, so two charge rows posting to the same account - whether in one voucher
or across vouchers - were merged. Amounts accumulated correctly but any
per-row context was lost to whichever row was seen first.
Key the grouping by (expense account, dimension values) and return a list of
charges per receipt item, each carrying its own dimensions, so rows that
differ only by dimension stay distinct.
Dimensions resolve from the charge row first, then the voucher item row.
Blanks are left blank so the GL composers can fall back to the receipt item
and receipt document as before.
* refactor(accounts): allow explicit accounting dimensions on add_gl_entry
get_gl_dict derives dimensions from the parent document and the item row, and
reads only custom dimensions off the item - never cost center or project.
Callers that need to set a dimension from some other source had no way to do
so except by building the args dict by hand.
Add a dimensions argument that is merged into the entry before get_gl_dict is
called, and thread it through the StockController and BaseGLComposer wrappers.
* fix(stock): carry landed cost charge dimensions onto the GL entries
Landed cost charges are posted into the receipt document's ledger, and their
expense account is a Profit and Loss account. Until now the entry took its
dimensions from the receipt item, which cannot know about a voucher created
after it was submitted, so a dimension mandatory for P&L accounts failed.
Take cost center, project and custom dimensions from the charge row, falling
back to the receipt item and receipt document when the row leaves them blank.
Only the leg posting to the charge account is affected; the reclass leg keeps
the item's dimensions so it still nets against the base item entry.
Also skip charges that prorate to zero, and hoist the landed cost lookup in
the Purchase Receipt composer out of the item loop - it was reloading every
voucher once per item.
* fix(stock): report missing mandatory dimensions on the Landed Cost Voucher row
Submitting a voucher re-makes the receipt document's GL entries, so a missing
mandatory dimension surfaced as a GL Entry error naming an account, raised
from the middle of update_landed_cost, with nothing pointing at the row that
caused it.
Check the charge rows during validate instead, against both the mandatory
for P&L / Balance Sheet flags and the per-account Accounting Dimension Filter,
and name the row, the dimension and the account in the message.
The check resolves values through the same fallback chain the GL composers
use, so it does not reject a voucher that would have posted successfully.
* test(stock): cover accounting dimensions on landed cost vouchers
Covers the charge row reaching the GL entry, cost center and project
overriding the receipt item, the blank row still falling back to it, and two
charge rows - and two vouchers - on the same expense account with different
dimensions staying separate entries.
Also covers the mandatory P&L dimension being satisfied from the charge row,
the missing one being reported on the voucher, dimensions surviving a repost,
and each dimension netting to zero on cancellation.
* refactor(lcv): apply custom dimension overrides via .update()
---------
Co-authored-by: nareshkannasln <nareshkannashanmugam@gmail.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
* fix: production plan scheduling edge cases
* fix: per-supplier schedule dates and item-wise amended row mapping
* fix: field-based matching for amended production plan rows
* fix: item-level lead time fallback for unconfigured suppliers
* fix: unambiguous amended row pairing and zero-day lead time fallback
* fix: clear sub assembly and material rows on production plan cancel
* fix(stock): reset bin when no stock ledger entries remain
update_bin() only writes bins reachable through prev_sle_dict, and that
dict is empty once the last live sle for an item and warehouse is
cancelled or deleted. actual_qty is still recomputed, but stock_value
and valuation_rate stay stale and a repost cannot heal them, so bin
totals drift permanently from the stock balance.
zero those bins after the normal update, guarded by a re-check that no
live sle exists. also drop the prev_sle_dict seeding added earlier in
initialize_previous_data, which never took effect because
initialize_reposting() discards the dict before update_bin() reads it.
* test(stock): cover bin reset when ledger is empty
three cases that all leave an item and warehouse with no live sle:
cancelling the only voucher, deleting it with delete_linked_ledger_entries
on, and reposting over an already emptied ledger. each asserts actual_qty,
valuation_rate and stock_value are all zero.
refresh() loops over every row in the accounts child table and calls
set_exchange_rate() for each one. That function unconditionally ended
with frm.refresh_field("accounts"), rebuilding the whole grid (header,
pagination, current page) on every single row. For large child tables
this makes opening the form scale badly with row count.
Use grid.refresh_row(cdn) instead, which only re-renders the row that
actually changed and is a no-op for rows outside the current page.
Measured on a 1000-row Journal Entry: ~8.5s to first rendered row and
~7.9s of blocked main thread before this fix, ~2.3s and ~1.9s after.
* fix(manufacturing): cap job card completed qty by previous operation and show process loss on finish dialog
* fix: revert job card finish dialog changes
* test: cover the rows that have nothing left to order in the mrp report
a row whose requirement is already met by stock or by an order placed earlier
fails the order it is selected for, takes the rows beside it down with it, and
what rounding leaves behind of it is ordered as if it were a real quantity. the
work order made from a row of the schedule also has to keep the work in progress
warehouse the company keeps for it.
* fix: skip covered rows when ordering from the mrp report
a row whose requirement is already met by stock or by orders that were placed
earlier nets down to a required qty of zero. making an order from it threw
"Qty To Manufacture cannot be 0", and since nothing caught it, none of the other
selected rows were created either. such rows are now left alone, and selecting
only covered rows says so instead of failing.
the quantity ordered stays the one that is still needed. taking the planned qty
instead would order everything that stock and the open orders already cover. it
is read at the precision an order stores it in, so what is left of a covered row
after all the subtracting does not become an order line of its own.
* feat: stock availability insight on pick list
* fix: show holding pick lists inline in stock availability dialog
* fix: dashboard layout for stock availability dialog
* fix: reword stock release hint in availability dialog
* fix: tree layout for stock held by section
* fix: escape values in blocking pick lists table
* feat: cross-plan load and overlap validation in production plan scheduling
* fix: row-level job card exclusion and locking read in schedule overlap check
* fix: operation-level job card exclusion and workstation locking in capacity check
* fix: keep plan schedule load when job cards carry no booked time
* fix: qty-coverage based job card exclusion for plan schedule load
the batch selector silently overwrote the item qty with the bundle total,
so editing a row qty in the dialog changed the delivered qty without any
warning. prompt for confirmation when the rows do not add up to the qty
to fetch, and only proceed if the user agrees.
The section is marked collapsible with no condition, so it always
rendered collapsed. When the transaction currency differs from the
company currency the exchange rate is relevant and was hidden behind
a click.
Adds collapsible_depends_on so the section starts expanded whenever
the transaction currency differs from the company currency, and stays
collapsed otherwise.
Replaces the hand-rolled item_prices.html table with frappe.ui.EmbeddedList,
the same primitive the proforma list uses. Drops the custom markup and styles.
The 10-row cap and the "View All Prices" link stay: the query fetches 11 rows
to return 10 plus a has_more flag, and the link routes to the Item Price list
filtered by item.
* feat: capacity aware scheduling for production plan
* fix: do not apply incomplete schedule proposals
* fix: lock plan re-scheduling once work orders exist
* test: concurrent jobs across multiple machines with job capacity
* chore: fix linter and semgrep issues
* fix: readable subject for production plan schedule entries
* fix: persist computed start for item rows without explicit dates
* fix: block manual creation of production plan schedule entries
* chore: replace em-dashes with hyphens in design doc
* fix: cleared item-wise dates no longer constrain the schedule
* chore: format test file
The report only narrowed by sales person when the filter was set, so a user
restricted to a Sales Person saw every row once the filter was cleared.
Resolve the permitted Sales Persons from user permissions and apply them on
top of the filter. Each Sales Team parent type is matched against its own
applicable_for scope, so a permission scoped to one doctype cannot authorise
rows through the other. Descendants are already expanded by
get_user_permissions, so Hide Descendants is respected. Gated to Receivable,
since the class is shared with Accounts Payable.
The detailed-view chart collapsed every row into a single "today" column
and was additionally capped at 10 points, so the chart never matched the
report's date filters or the table data.
Two causes in get_detailed_view_chart_data:
1. `row.deliver_date` was a typo for `row.delivery_date` (the name used
everywhere else in this report). On a frappe._dict the missing
attribute resolves to None, so `getdate(None)` returned today and the
past-date filter silently compared every row against today instead of
its own delivery date.
2. A hard `if i == 10: break` truncated the chart to 10 date buckets.
Use the correct field name and drop the cap. The null check now runs
before the date comparison, since `getdate(None)` returning today meant
the original ordering could never filter a null delivery_date out.
Fixes#52632
The qty-sync fix corrects allocation going forward, but receipts billed
before it can keep understated billed_amt, per_billed, and status. The
earlier repair patch only selects over-billed PO items, so it never picks
these up.
Recompute every candidate PO item (multiple submitted receipts, PO-level
invoicing, no invoice-created receipts). update_billed_amount_based_on_po
only writes rows whose recomputed value differs, so already-correct items
are untouched and the patch stays idempotent. This also converges receipts
left with direct-only billed_amt by last-event-wins overwrites.
The amount-capped allocation branch reduced the remaining PO-invoiced
amount but left the invoiced qty untouched. A later receipt entering the
qty-proportional branch then divided by the stale qty and was under-billed:
PO 10 x 500, PO-level PI for 5 (2500), PR1 qty 3 with 500 billed directly
consumes 1000 (pool 2500 -> 1500, qty stuck at 5), PR2 qty 3 got
1500 * 3/5 = 900 instead of its full 1500. Scale the remaining qty by the
consumed fraction so both stay proportional.
Follow-up to #58021.
Move the insert-time check from before_insert to validate. before_insert
runs before set_new_name, so the validation message rendered the
warehouse name as None. validate runs after naming and only applies to
new documents via is_new().
Resolve inheritance through the parent's lft/rgt bounds instead of the
request-cached warehouse account map. The cached map can be stale within
a request (a parent created moments earlier is missing from it), which
made get_warehouse_account trigger a full nested-set rebuild_tree and
could falsely reject a child whose parent carries a valid account.
rebuild_tree enables auto_commit_on_many_writes, which must not run
inside a document insert.
A Purchase Receipt row created from a Purchase Invoice carries both
purchase_order_item and purchase_invoice_item, and its billed_amt is pinned
to the row amount by update_billing_status. Redistributing the PO-invoiced
pool over such rows zeroes the invoice-created receipt and flips it from
Completed to To Bill, so the repair leaves those PO Items untouched.
enable_auto_reserve_stock ran at the end of validate, after
make_packing_list. On a new Sales Order saved with auto_reserve_stock
enabled, packed item rows were built while the parent reserve_stock
flag was still unset, so they never inherited it. Since the stamping in
packed_item.py is gated on doc.is_new(), later saves could not repair
the rows either; only the client-side toggle could. Move the
auto-enable before packing list generation so packed rows are stamped
on first save.
sales invoice's update_current_stock ran one bin query per item row and one
per packed row. delivery note already batched the same work by warehouse, so
lift that into get_bin_qty_map in stock/utils.py and have both call it.
also batch the per-batch expiry_date lookup in get_batches_by_oldest, and drop
three now-unused per-row setters: delivery note's set_actual_qty (already dead
before this change), sales invoice item's set_actual_qty and packed item's
set_actual_and_projected_qty.
three cases: multiple warehouses get a column each plus the total qty
column, a selected group warehouse still expands to its children, and
multiple items report side by side while unselected items stay out.
the item and warehouse filters took one value at a time, so comparing a
few warehouses meant re-running the report for each one.
both are multiselectlist now, matching the stock balance report. the
warehouse column list unions the subtree of every selected warehouse,
and get_items passes a list through instead of wrapping it. plain string
values still work, so saved filters and existing callers are unaffected.
on_update reran toggle_hide_tax_id, toggle_editable_rate_for_bundle_items
and toggle_discount_accounting_fields on every save, rewriting 11
property setters and clearing the meta cache of five doctypes.
Gate each toggle on has_value_changed. Fresh installs save the settings
with pure defaults (set_single_defaults), so the gated-off state must
match the JSON schema: align sales_invoice.json and
sales_invoice_item.json with the values every saved site already has —
tax_id printed when hide_tax_id is off, discount accounts hidden while
discount accounting is disabled. Packed Item rate already matches.
When a Purchase Invoice is raised directly from a Purchase Order (po_detail
set, pr_detail null), update_billed_amount_based_on_po distributes the billed
amount across the PO's Purchase Receipts in FIFO order.
The proportional branch, taken when the invoiced qty exceeds a single
receipt's qty, computed each receipt's share but never deducted the consumed
amount/qty from the running po_billed_amt_details total. As a result every
subsequent receipt was billed against the same amount again, so the receipts
together showed more billed amount than was actually invoiced. A receipt with
no invoice truly against it could reach 100% billed and become Completed,
dropping out of pending-invoice reports.
Deduct the consumed billed_amt and billed_qty in the proportional branch,
mirroring the existing else branch, so each receipt only consumes what is
left. Add a regression test covering a PO invoice spanning two receipts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every Selling Settings save reran set_by_naming_series for Customer and
every Buying Settings save reran it for Supplier, rewriting the
naming_series property setters with their cache clears and running the
naming_series backfill UPDATE on the master table.
Gate both on has_value_changed, following Stock Settings. Naming
behaviour is unaffected: Customer.autoname and Supplier.autoname read
the master-name default, which is still set on every save.
Both sales partner summary suites created identical submitted, draft,
cancelled, and returned transactions per doctype. Run both reports
against one fixture set and receive stock only for Delivery Note and
POS Invoice, the doctypes that consume it.
Every Stock Settings save rewrote the Item naming property setters and
the barcode visibility property setters. make_property_setter without a
doctype fans out to every doctype that has the fieldname and clears each
doctype's cache, and set_by_naming_series also backfills tabItem.
Gate both on has_value_changed. Item naming behaviour is unaffected: it
reads the item_naming_by default, which is still set on every save.
* feat(accounts): add Bank Charges account for Payment Entry deductions
Add an optional Bank Charges Account field on Company. When a Payment
Entry has a difference between the paid and received amount (e.g. a
same-currency Internal Transfer where the bank deducted a fee), that
amount now books to the Bank Charges account in the deductions table
instead of always going to the Exchange Gain/Loss account. Left blank,
behavior is unchanged.
Mirrors the resolution on both the server (set_exchange_gain_loss) and
client (set_exchange_gain_loss_deduction) so the deduction row is
pre-filled consistently before and after save. A user's manual account
edit on an existing deduction row is preserved across recalculation,
same as before this change.
* fix(accounts): only route Payment Entry difference to Bank Charges for same-currency transfers
Cross-currency Payment Entries were also matching the unconditional
bank_charges_account precedence, misrouting a genuine exchange
gain/loss into the Bank Charges account. Only prefer Bank Charges
Account when paid_from and paid_to share a currency; cross-currency
differences continue to book to Exchange Gain/Loss Account.
* test(payment_entry): assert against actual exchange gain/loss account, not a hardcoded name
CI failed: _Test Company's exchange_gain_loss_account is auto-provisioned
as "Exchange Gain/Loss - _TC" by the standard chart of accounts, not the
"_Test Exchange Gain/Loss - _TC" account used only by a sibling test.
* fix(accounts): auto-set Bank Charges Account from chart of accounts default
The standard chart of accounts already ships a "Bank Charges" ledger
account, but set_default_accounts() never picked it up into the
Company's bank_charges_account field, unlike its write_off_account and
exchange_gain_loss_account siblings. New and existing companies now
get it auto-populated the same way.
---------
Co-authored-by: test <test@test.com>
Static filters with no doc-dependent values belong on the field
definition, not in JS. Matches the existing pattern used for
Warehouse/Item link_filters elsewhere (e.g. job_card_item.json,
product_bundle_item.json).
The Create Job Card dialog on Work Order lists only pending operations,
so the row idx sent to make_job_card is the dialog's position, not the
Work Order Operation idx. create_job_card stamped that dialog idx into
operation_row_id, and get_required_items then matched raw materials of
whichever operation held that idx originally.
Resolve idx server-side from the Work Order Operation row that
get_operation_details already looks up by name.
Fixes https://github.com/frappe/erpnext/issues/57985
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.
When the work order transfers material against Job Card, the Start Job
and Complete Job actions (and the whitelisted start_timer and
complete_job_card methods behind them) accepted work before any
Material Transfer for Manufacture existed; the transfer gate only fired
on job card submission.
Run validate_transfer_qty on both actions, and drop the finished_good
escape in materials_ready so the dashboard hides the buttons while
transfer is pending. Job cards that skip material transfer, corrective
job cards, and work orders transferring against Work Order are exempt,
as on submit.
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.
validate_party_frozen_disabled only enforces Customer/Supplier/Employee,
so passing opportunity_from straight through silently no-op'd for Lead
and Prospect. Made the Customer-only scope explicit instead of relying
on that implicit fallthrough.
Lead.disabled is not enforced anywhere else in the codebase (lead_query,
the picker used for this same field, only filters status/docstatus), so
deliberately not extending validation to Lead-sourced Opportunities.
Request for Quotation overrides validate() entirely and never calls
super().validate(), so it never goes through AccountsController's
party validation. Suppliers also sit in a child table, so the shared
PartyValidator wouldn't have caught it anyway (it only checks a single
top-level party field). A disabled or frozen Supplier could be added
to an RFQ and the RFQ submitted without any warning.
Also filters the suppliers grid's supplier Link field to disabled=0,
matching the same client-side fix applied to Opportunity's party_name.
Opportunity inherits TransactionBase instead of AccountsController, so
it never ran validate_party_frozen_disabled like Quotation, Sales Order
and Sales Invoice do. A disabled Customer could be saved as an
Opportunity's party and only get caught later at Quotation stage.
Also fixes the party_name Link query on the client: it referenced
erpnext.queries.customer, which was never defined, so disabled
customers showed up in the picker.
`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.
* feat: sync serial no status from stock ledger in Stock Qty vs Serial No Count report
* fix: pick last bundle move in SQL ordered by posting datetime and SLE creation
* fix: derive synced serial no status from stock ledger helper and validate sync args
Existing submitted BOMs may carry operations without a finished good,
and no migration repairs them. Exempting every semi FG job card from
the transfer check let such a card submit after a partial transfer.
Exempt only cards that skip material transfer; legacy cards with
transfer enabled keep the strict transferred qty check.
Every generated entry copied each Job Card Item's full required_qty in
the skip-transfer and BOM-backflush paths, so two entries for one job
card consumed the requirement twice. Scale the rows to the share of
production this entry accounts for and cap them at the requirement
still unconsumed, dropping rows that have nothing left. An entry whose
materials are exhausted then fails the existing at-least-one-raw-material
check instead of minting finished goods from nothing.
Saving a submitted manufacture entry to change an allowed field re-ran
the pending production cap with a manufactured aggregate that already
includes the entry itself, so the save was rejected against the
post-entry remainder. Quantities are not editable after submit, so the
check has nothing to protect there.
The WIP warehouse change also removed the Target Warehouse exemption
for semi FG orders, but those may validly carry the target on each
operation instead. Restore the exemption in the form and the submit
check; the WIP warehouse requirement stays.
After a partial entry booked the job card's full process loss, the
next generated entry was sized qty-to-produce minus manufactured only.
It exceeded the pending production cap, so Make Stock Entry could not
finish the card. Subtract the consumed loss when sizing the entry.
Entries from operations without their own BOM carry no For Quantity,
so the finished-good reconciliation cannot run for them and a draft
created before other entries were submitted could still over-produce.
Validate every job-card manufacture entry against the job card
directly: finished goods plus process loss must fit in what the job
card still has left to produce after earlier submitted entries.
The finished_good derivation ran in validate_semi_finished_goods,
after set_materials_based_on_operation_bom had already expanded
operation BOM materials. A single-pass insert-and-submit (API or
import) with bom_no set but finished_good empty skipped the expansion,
persisting a submitted BOM without the referenced components. The
derivation also let a final operation inherit another item from its
bom_no, so downstream job cards would produce the wrong item.
Move the derivation into set_operation_finished_goods, called before
the expansion, prefer the BOM's own item for the final operation, and
reject a final operation whose FG item is not the BOM's item.
get_item_details returns the whole Item document, so the dialog row's
name became the item code. get_item_data then matched that item code
against every Components row regardless of operation, so adding an item
already used by another operation silently updated that row's qty
instead of appending one for the target operation — which stayed empty
and failed 'please add raw materials or set a BOM' on submit.
Match the existing row by item code within the same operation: same
operation updates the qty, any other match appends a new row.
set_process_loss_qty stamped MAX(process_loss_qty) across every
operation of the work order onto each manufacture entry. With semi
finished goods tracking, one operation's process loss leaked into the
entries of every other operation: validate_fg_completed_qty then
rejected the entry when it had a BOM, or the wrong loss was recorded
silently when it did not, double-counting the loss across operations.
When the entry belongs to a job card, use that job card's loss net of
what its earlier entries already booked. The MAX fallback stays for
work-order level entries without a job card.
Fixesfrappe/erpnext#57892
When a previous operation manufactured less than the current job card
is completing, the error always said 'Submit the manufacturing entry
for the operation first' — even when the entry was already submitted
and the missing quantity was booked as process loss, which made the
advice a dead end.
Sum the process loss of the previous operation's job cards alongside
the manufactured quantity. When manufactured + process loss covers the
requested quantity, say the shortfall is process loss so the user
knows to reduce the completed quantity; keep the submit-first message
for genuinely pending manufacturing entries.
Work orders with track_semi_finished_goods were exempt from the
Work-in-Progress Warehouse requirement in three places: the field's
mandatory_depends_on, the fg_warehouse reqd toggle in the form script,
and validate_warehouse on submit.
The exemption was misleading. The flow still transfers materials to a
WIP warehouse when 'Skip Material Transfer' is unchecked: operations
default their WIP warehouse from the work order, and
set_default_warehouse silently restores the company default after the
user clears the field. Make the field genuinely required instead of
pretending it is optional.
validate_transfer_qty uses an empty finished_good to detect legacy job
cards, and unlike validate_semi_finished_goods it ignores
skip_material_transfer. A job card tracking semi finished goods whose
operation had no finished_good fell into the legacy branch and could
not be submitted even with 'Skip Material Transfer' checked on the
work order.
Return early for semi FG job cards; validate_semi_finished_goods
already enforces the transfer requirement for them and honours
skip_material_transfer.
A BOM with track_semi_finished_goods enabled could be saved with no
finished_good on any operation: validate_semi_finished_goods only
checked that one row had 'Is Final Finished Good' set, and a list
containing None passed the emptiness check.
Such a BOM breaks every downstream step. The work order copies the
empty finished_good into its operations, job cards inherit it, and
Make Stock Entry finally fails with 'Item None not found' because the
manufacture entry has no production item.
Derive the finished good where it is unambiguous: an operation that
references a BOM produces that BOM's item, and the final operation
produces the BOM's own item. Otherwise require it on the row, since
each operation's job card books its output through it.
When the in-memory running rate is zero, the fallback went through
get_incoming_rate, whose previous-SLE lookup matches the same
posting_datetime and can land on a sibling line of the voucher being
replayed. Replace it with get_previous_sle_of_current_voucher excluding
the current voucher, keeping the get_valuation_rate chain when no
previous entry exists. get_incoming_rate is no longer used in this
module.
Reposting a return that removes most of the stock across several lines
of the same item must keep every line at the running average and produce
identical results on a second repost. Before the fix the first repost
already drifted, seeding each line from a sibling row of the same
voucher.
During repost, a return line with recalculate_rate resolved its moving
average rate through get_incoming_rate -> get_previous_sle, which matches
posting_datetime <= and orders by creation desc. For a multi-line return
of the same item, every line shares one posting_datetime, so the query
landed on a sibling line of the same voucher whose stored valuation_rate
was still the previous repost run's output, not the rate before the
voucher.
Each repost run therefore re-seeded the voucher from its own prior
output. The error gain per run is (qty returned at the stale rate) /
(qty remaining after the return), so whenever a return removes most of
the stock the loop diverges instead of converging, alternating sign and
growing until stock_value overflows decimal(21,9) and the repost dies
with 'Out of range value for column stock_value'.
Use the in-memory running valuation rate that update_entries_after
already tracks for the warehouse at this point in the repost. It is the
authoritative pre-entry state, is immune to sibling rows, and makes the
repost idempotent. The database lookup is kept only as a fallback for a
zero in-memory rate, preserving the existing zero-rate fallback chain.
A minimum order qty defined in stock UOM often has no exact
representation in the purchase UOM, so the smallest valid order slightly
exceeds the minimum. Surface that overage on the Purchase Order with a
toast on first save when an item's ordered stock qty is above its
minimum by less than one purchase-UOM step, so the buyer sees the
marginal increase before sending the order. Sub-precision dust stays
silent.
Covers both rounding brackets, an exactly representable conversion, the
no-minimum path, and the ceiling through the plan items and materials
from other locations flows.
A Production Plan with Consider Minimum Order Qty raises the requirement
to the item's minimum in stock UOM, then converts it to the purchase UOM
with round-to-nearest. Nearest rounding can land below the minimum it
just applied: min order qty 50000 with purchase UOM conversion factor
453.592292197 becomes 110.231, which is 49999.932 in stock UOM, and the
mapped Purchase Order is then rejected by validate_minimum_order_qty.
When the minimum binds and the nearest-rounded value dips below it,
quantize to the smallest representable purchase-UOM quantity whose stock
equivalent meets the minimum, using Decimal grid-ceiling arithmetic.
110.232 converts to 50000.386: demand stays as planned and the overage
is order-unit granularity, the standard MRP lot-sizing outcome. Ordinary
conversions keep the historical round-to-nearest behavior.
The Address & Contact cards now show these details and mark which record
is primary, so the section below only repeats it. Values are still stored
and Customer.search_fields keeps working, since search reads the column
rather than the form.
Depends on frappe/frappe#41600.
constcontent=_("Below is a list of all accounting entries posted against the bank account {0} between {1} and {2}.",[`<strong>${bankAccount?.account}</strong>`,`<strong>${formattedFromDate}</strong>`,`<strong>${formattedToDate}</strong>`])
constcontent=_("Below is a list of all entries posted against the bank account {0} which have not been cleared till {1}.",[`<strong>${bankAccount?.account}</strong>`,`<strong>${formatDate(dates.toDate)}</strong>`])
constcontent=_("Below is a list of all bank transactions imported in the system for the bank account {0} between {1} and {2}.",[`<strong>${bankAccount?.account_name}</strong>`,`<strong>${formattedFromDate}</strong>`,`<strong>${formattedToDate}</strong>`])
constentriesContent=_("Entries below have a posting date after {0} but the clearance date is before {1}.",[`<strong>${formattedToDate}</strong>`,`<strong>${formattedToDate}</strong>`])
"description":"System will use the latest saved Currency Exchange rate on or before the transaction date, however old it is. <br>\nUncheck to ignore rates older than Stale Days and fetch a fresh rate from the exchange rate provider instead.",
"fieldname":"allow_stale",
"fieldtype":"Check",
"in_list_view":1,
"label":"Allow Stale Exchange Rates"
"label":"Allow Stale Exchange Rates",
"show_description_on_click":1
},
{
"default":"1",
@@ -222,7 +226,8 @@
"description":"The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
"fieldname":"over_billing_allowance",
"fieldtype":"Currency",
"label":"Over Billing Allowance (%)"
"label":"Over Billing Allowance (%)",
"non_negative":1
},
{
"default":"1",
@@ -278,10 +283,10 @@
},
{
"default":"0",
"description":"Block a new Sales Invoice when the customer's overdue amount exceeds the Overdue Limit set on the customer.",
"description":"Enabling this option prevents the creation of a new Sales Invoice when the customer has an overdue limit set and their outstanding overdue amount exceeds that limit.",
"fieldname":"enable_overdue_billing_threshold",
"fieldtype":"Check",
"label":"Restrict Customer Over Billing"
"label":"Prevent Sales Invoice when Customer is Overdue"
frappe.throw(_("Pricing Rule {0} is disabled").format(frappe.bold(self.pricing_rule)))
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.