* fix(selling): validate Proforma Invoice lines in the controller
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.
* test(selling): Proforma Invoice lines validated on direct insert
* fix(selling): block amending a cancelled Proforma Invoice
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.
* test(selling): reject an amended Proforma Invoice
* fix(selling): keep the Proforma tab on a cancelled Sales Order
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.
* fix(selling): tidy the Proforma Invoice print
Print the customer address under the customer name, and drop the
Against Sales Order row and the not-a-demand-for-payment footer.
* feat(selling): editable item description on Proforma Invoice
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.
* test(selling): edited and default Proforma Invoice line description
* fix(selling): block deleting a proformed Sales Order row via Update Items
Deleting the row left the issued proforma pointing at a Sales Order
line that no longer exists.
* test(selling): Update Items keeps a row with an issued proforma
* fix(selling): decode entities before comparing proforma description
strip_html kept entities, so an item named R&D with the description
R&D printed its name twice. Jinja's striptags also unescapes.
* fix(selling): check issued proformas for all deleted rows in one query
The check ran one query per deleted Sales Order row inside the
validation loop.
* fix(selling): default the proforma PDF to the Proforma Invoice format
With no format picked in the dialog and none set in Selling Settings,
the PDF used the Sales Order's default print format, so it was headed
Sales Order and lacked the proforma number and date.
* feat(selling): create quotation revisions without cancelling
Create > Revision copies a submitted quotation into a draft named after the original with an -R<n> suffix. The next index comes from the existing revisions, as BOM does. The original lists its revisions under Connections.
* test(selling): quotation revisions
* feat(selling): mark quotations inactive
An Is Active flag, like BOM's, lets users retire an older quotation version. Inactive quotations cannot be mapped to a Sales Order or Sales Invoice, show a red Inactive indicator, and no longer count as an active offer for their Opportunity or Lead.
* test(selling): inactive quotations
* feat(selling): block setting inactive quotations as lost
Setting a quotation as Lost also marks its Opportunity as Lost, which is wrong while a newer version of the quotation is still open.
* feat(selling): deactivate older quotation versions on revision submit
Submitting an active revision clears Is Active on the other submitted versions of the same quotation, so only the latest one can be ordered. This mirrors how a new default BOM takes the default from the item's other BOMs.
* test(selling): older quotation versions deactivate on revision submit
* feat(selling): set all quotation versions as lost together
Setting a quotation as Lost now also sets its other submitted versions as Lost, except ones already ordered. Lost Quotations counts only the active version, so a revised deal is counted once, and the list shows Lost ahead of Inactive.
* test(selling): lost quotation versions
* feat(selling): revise only the active quotation version
Create > Revision is hidden on inactive quotations and make_revision rejects them, so new revisions branch from the current version.
* test(selling): inactive quotation cannot be revised
* feat(selling): set only the latest quotation version as lost
Set as Lost moves from the active version to the latest submitted one, active or not. Setting it as Lost makes it the active version and deactivates the others, so Lost Quotations still counts the deal once. Revision Of is indexed, since the latest version is looked up on every form load.
* test(selling): only the latest quotation version can be set as lost
* feat(selling): do not revise lost or ordered quotations
Create > Revision follows the same rule as Sales Order and Update Items: it is hidden on Lost and Ordered quotations, and make_revision rejects them.
* test(selling): lost quotation cannot be revised
* feat(selling): revise only the latest quotation version
Create > Revision now needs the quotation to be both active and the latest submitted version, so an older version that was re-activated by hand cannot branch the revision history. The revise rules move into Quotation.validate_can_be_revised.
* test(selling): only the latest quotation version can be revised
* feat(selling): order quotation versions by transaction date
The latest version is the one with the latest Transaction Date. Creation time only breaks ties between versions dated the same day.
* test(selling): latest quotation version follows transaction date
* feat(selling): block quotation revisions dated before the latest version
A revision cannot have a Transaction Date earlier than any other submitted version of the same quotation. Otherwise the revision becomes active on submit while an older version stays the latest, and no version can be revised.
* test(selling): quotation revision dated before the latest version
* fix(selling): keep a submitted quotation revision the latest version
The revision date check compared dates only, so of two revisions dated the same day, the one created first could be submitted last. It then became the active version while the other stayed the latest, and neither could be revised. The check now uses the same date-then-creation order as the latest version.
* test(selling): older quotation revision submitted after a newer one
* fix(selling): lock the original quotation while naming a revision
Two revisions of the same quotation inserted at once could read the same existing names and pick the same -R suffix. Locking the original quotation row makes the second insert wait and take the next number.
* perf(selling): update other quotation versions in one query
Deactivating or setting the other versions as Lost wrote one row at a time. It now reads the matching versions once and updates them with a single query.
* test(selling): copy the margin test quotation once
Copying an unsaved quotation a second time turns the unset Is Active into 0, so insert kept the quotation inactive and the Sales Order mapper rejected it. One copy leaves the field unset, and insert applies its default.
* perf(selling): clear the cache of updated quotation versions only
A filter-based set_value clears the cache of every Quotation. bulk_update keeps the single query, and the cache is then cleared for the updated versions only.
* fix(selling): block submitting a sales order against an inactive quotation
The Is Active check ran only in the quotation mappers, so a Sales Order with quotation rows added another way could still be submitted against an inactive quotation. The check now also runs when the Sales Order is submitted. Cancelling an existing Sales Order still works.
* test(selling): sales order against an inactive quotation
* feat(selling): lock is active on lost quotations
Is Active is read-only once a quotation is Lost, and the server rejects changes to it. Set as Lost now writes Is Active together with the status, so marking an inactive latest version as Lost still makes it active.
* test(selling): is active locked on a lost quotation
* feat(selling): show is active on submitted quotations only
Is Active only affects submitted quotations, so the checkbox is hidden on new, draft and cancelled ones.
* feat(selling): set any quotation version as lost
Set as Lost is back on every submitted version, active or not. When other versions would also be set as Lost, the user confirms first.
* test(selling): older quotation version set as lost
* feat(selling): revise any quotation version
Create > Revision is back on every submitted version that is not Lost or Ordered, active or not. When newer versions exist, the user confirms first.
* test(selling): older quotation version revised
* fix(selling): keep the quotation create menu primary
The Create menu turned primary only when the Sales Order button was added, so a quotation offering only Revision showed a plain menu.
* feat(selling): list every quotation version under connections
Connections found versions through revision_of, which only matches on the original, so revisions listed nothing. A Quotation count method now adds the other draft and submitted versions by name, so every version links to the rest.
* test(selling): quotation versions listed under connections
* fix(selling): label the quotation connections group versions
The group lists the original and the other revisions, so Versions fits better than Revisions.
* fix(selling): block setting a quotation as lost when a version is ordered
Set as Lost checked only the chosen version for Sales Orders. An older open version could be set as Lost while a newer version was ordered, which counted a won deal as lost.
* test(selling): quotation version set as lost with an ordered version
* fix(selling): set other quotation versions as lost before updating the opportunity
Opportunity and Lead recompute their status from active quotations. When an older version was set as Lost, the newer active version was still open at that point, so the Opportunity stayed at Quotation.
* test(selling): opportunity lost when an older quotation version is set as lost
* fix(selling): keep quotation revisions in the same company and off lost quotations
A revision could move to another company, so updates to its other versions reached quotations of a different company. A draft revision made before the quotation was set as Lost could also still be submitted and reopen the deal.
* test(selling): quotation revision company and lost checks
* fix(selling): list only readable quotation versions under connections
The Connections list named every other version without a permission check. It now uses get_list, as frappe does when it returns linked names.
* feat(selling): keep the party of the original on quotation revisions
A revision must stay with the party of the original quotation. Quotations for a Lead are the exception, so a revision can move to the Customer made from that Lead.
* test(selling): quotation revision party checks
* fix(selling): update opportunity and lead when a quotation is deactivated
Ticking or unticking Is Active by hand left the Opportunity and Lead status unchanged, even though they no longer count inactive quotations. The change now updates them the same way submit and cancel do.
* test(selling): opportunity reopened when its quotation is deactivated
* fix(selling): block submitting a sales order against a lost quotation
A draft Sales Order made before its quotation was set as Lost could still be submitted, which turned the Lost quotation into Ordered.
* test(selling): sales order against a lost quotation
* fix(selling): limit lead quotation revisions to the lead's customer
A revision of a quotation for a Lead could move to any party. It can now only stay with that Lead or move to the Customer created from it.
* test(selling): lead quotation revision moved to its customer
* fix(selling): validate items in the quotation connections count
The whitelisted count method parsed items without checking the result, so other JSON values failed deep inside frappe's counter. It now rejects anything but a list of DocType names.
* fix(selling): name the quotation revision button new version
Create > Revision is now Create > New Version, and its confirmation says version too.
* 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.
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.
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.