Compare commits

..

1413 Commits

Author SHA1 Message Date
Mihir Kandoi
b3d55db893 fix(stock): return material that a document rejected in full (#59280)
* 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
2026-09-22 20:32:13 +05:30
Mihir Kandoi
876bf0b7f7 fix(stock): move rejected material of an internal transfer on an invoice too (#59260)
* 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
2026-09-22 20:32:13 +05:30
Mihir Kandoi
16b1be814c fix(accounts): bill the rejected quantity on a stock updating invoice (#59258)
* 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.
2026-09-22 20:32:12 +05:30
Mihir Kandoi
ecc643fde0 fix(stock): stop valuing rejected material on a stock updating invoice (#59257)
* 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.
2026-09-22 20:32:12 +05:30
Mihir Kandoi
df3f952fac fix(stock): empty the in-transit warehouse when every unit is rejected (#59256)
* 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
2026-09-22 20:32:12 +05:30
Mihir Kandoi
726c92f8ae fix(stock): let rejected serial and batch material leave the in-transit warehouse (#59252)
* 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
2026-09-22 20:32:11 +05:30
Mihir Kandoi
c0f13b01de fix(stock): deduct rejected qty from the in-transit warehouse (#59251)
* 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
2026-09-22 20:32:11 +05:30
rohitwaghchaure
23afc786bb feat: use serial no wise valuation switch on item (backport #59082) (#59182)
feat: use serial no wise valuation switch on item (#59082)
2026-09-22 14:00:42 +00:00
Mihir Kandoi
3bf3bc709f fix(stock): carry the batch of a delivery into the in-transit warehouse (#59264) 2026-09-22 18:17:14 +05:30
Pandiyan P
102842253b fix(stock): use net purchase values in landed cost vouchers (#59274)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-09-22 18:05:16 +05:30
Sudharsanan Ashok
e814d13126 fix(accounts): keep price list within user permissions (#59231)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-09-22 18:04:26 +05:30
Sudharsanan Ashok
7f608f31eb fix(stock): keep mapped cost center in stock entry items (#59286) 2026-09-22 18:02:53 +05:30
Sudharsanan Ashok
f990df9deb refactor(selling): remove ensure delivery based on produced serial no (#59217)
* 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.
2026-09-22 17:54:54 +05:30
Sudharsanan Ashok
62cb38d36b fix(stock): seed bin values when cancelling a stock voucher (#59250)
* 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.
2026-09-22 17:53:07 +05:30
Mihir Kandoi
8d6ee86ba3 fix: don't bump Customer timestamp when caching primary address (#59285)
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.
2026-09-22 12:10:21 +00:00
Khushi Rawat
e3fc1790d4 Merge pull request #59262 from aerele/fix-reverse-je-mandatory-fields
fix(journal-entry): carry reference fields into reverse journal entry
2026-09-22 17:15:30 +05:30
rohitwaghchaure
719b53f2ad fix: do not zero out backdated stock at a stock reco adjustment entry (#59269)
* 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
2026-09-22 16:38:51 +05:30
Pandiyan P
79fd06a1ba fix(crm): keep opportunity type default untranslated (#59277) 2026-09-22 11:06:45 +00:00
Khushi Rawat
5ebee655b7 Merge pull request #59180 from khushi8112/pos-price-list-exchange-rate
fix: refresh price list exchange rate when currency changes
2026-09-22 15:29:36 +05:30
Mihir Kandoi
9f2b05c103 fix: skip serial nos with no stock when reconciling specific serial nos (#59266) 2026-09-22 14:31:14 +05:30
ervishnucs
9e01992217 fix(journal-entry): carry reference fields into reverse journal entry 2026-09-22 13:25:11 +05:30
Shllokkk
5ebc52c95c Merge pull request #59240 from Shllokkk/asset-depr-schedule-company-filter
fix: asset field unfilterable on manual Asset Depreciation Schedule
2026-09-22 12:36:50 +05:30
ruthra kumar
86eb9c6204 Merge pull request #59230 from ruthra-kumar/more_changes_to_pcv_using_mapreduce
refactor(pcv): status on list view, progress update on UI
2026-09-22 12:19:05 +05:30
Afsal Syed
893fc75df4 fix(stock): apply transit warehouse filter to target warehouse in items table (#59192) 2026-09-22 06:48:07 +00:00
ruthra kumar
f2279a139f refactor(pcv): status on list view, progress update on UI 2026-09-22 11:50:28 +05:30
Mihir Kandoi
30e0382aa9 feat(selling): group Sales Order Analysis by item (#59236) 2026-09-22 11:09:20 +05:30
Mihir Kandoi
d2b1965cc8 fix(stock): compare transit quantities in stock UOM (#59202) 2026-09-22 11:08:09 +05:30
Shllokkk
a931c7d2f4 fix: remove company link filter on asset field in Asset Depreciation Schedule 2026-09-22 03:36:40 +05:30
Nikhil Kothari
3c251254b8 feat: update icons and documentation links for DocTypes and reports (#59235)
* feat: update icons and documentation links for DocTypes

* feat: add documentation links in ERPNext

* fix: migrate to newer Lucide icons
2026-09-21 12:32:05 +00:00
Shllokkk
04c949a662 fix: use correct exchange rate for multi-currency bank reconciliation payments (#59144)
* fix: use correct exchange rate for multi-currency bank reconciliation payments

* test: exchange rate for multi-currency bank reconciliation payment
2026-09-21 17:30:25 +05:30
Nishka Gosalia
5d00fec1c2 Merge pull request #59177 from nishkagosalia/stock-unreserve-issue
fix: unreserving stock failing in work order
2026-09-21 14:20:18 +05:30
Raffael Meyer
bb16dca4f7 ci: bump po-review-action to v1.2.0 (#59224) 2026-09-21 08:19:34 +00:00
Shllokkk
79fdc8add3 Merge pull request #59198 from Shllokkk/gp-returned-invoices-company-filter
perf: filter by company in get_returned_invoice_items
2026-09-21 13:43:30 +05:30
Sudharsanan Ashok
6e87694bbe fix(stock): honour pick manually when mapping a pick list (#59220) 2026-09-21 13:32:58 +05:30
rohitwaghchaure
6eda8c8c62 fix: write off stranded stock value only when the warehouse is empty (#59218) 2026-09-21 13:29:57 +05:30
Khushi Rawat
fb69724cda Merge pull request #59128 from aerele/fix/coupon-code-validation
fix: validate coupon pricing rules and validity dates
2026-09-21 12:46:57 +05:30
Mihir Kandoi
dd72c2688b chore(desk): hide CRM and Support on the dock (#59208) 2026-09-21 04:03:46 +00:00
MochaMind
8fd0175b06 chore: update POT file (#59201) 2026-09-20 21:44:23 +02:00
Shllokkk
1e35af3abe perf: filter by company in get_returned_invoice_items 2026-09-20 01:33:39 +05:30
Afsal Syed
db6e089109 fix(stock): validate warehouse account belongs to selected company (#59191) 2026-09-19 13:06:06 +05:30
Khushi Rawat
a2481e9390 Merge pull request #59181 from aerele/fix/accounts-reports-revaluation-filter-labels
fix(accounts): clarify revaluation journal filters
2026-09-18 17:53:02 +05:30
khushi8112
221baabf9e test: cover price list exchange rate currency changes 2026-09-18 17:47:34 +05:30
khushi8112
8b59d66f94 fix: refresh price list exchange rate when currency changes 2026-09-18 17:34:55 +05:30
pandiyan
dc4b390a9b fix(accounts): clarify revaluation journal filters 2026-09-18 17:26:10 +05:30
nishkagosalia
ff70aa6a33 fix: unreserving stock failing in work order 2026-09-18 17:12:01 +05:30
Pandiyan P
4bbd07944d fix(stock): respect manual picking when mapping sales orders (#59170) 2026-09-18 16:31:56 +05:30
Pandiyan P
622b7e0caa fix(stock): forward company to outward batch lookup (#59160) 2026-09-18 14:36:49 +05:30
Khushi Rawat
3d1ec937cf Merge pull request #59142 from aerele/fix/shipping-rule-account-company
fix: validate shipping rule account company
2026-09-18 14:06:59 +05:30
Pandiyan P
6cc500e28d fix(stock): continue putaway allocation after undersized whole-uom rules (#59155) 2026-09-18 07:44:21 +00:00
Khushi Rawat
40ab41278e Merge pull request #59081 from aerele/fix/bank-reconciliation-date-validation
fix(accounts): validate bank reconciliation date ranges
2026-09-18 13:07:24 +05:30
mergify[bot]
4197932f60 perf(stock): chunk the serial and batch entry backfill patch (backport #59076) (#59151)
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>
2026-09-18 11:47:42 +05:30
rohitwaghchaure
aebb160e76 feat: mark update_serial_batch_entries patch as never skippable (#59147) 2026-09-17 16:52:16 +00:00
Sudharsanan Ashok
5d5eeb5e02 fix(manufacturing): include the mrp bucket that ends on to_date (#59143) 2026-09-17 13:21:10 +00:00
pandiyan
022ee73d78 test: cover shipping rule account company validation 2026-09-17 18:31:38 +05:30
pandiyan
340feb94a1 fix: validate shipping rule account company 2026-09-17 18:31:38 +05:30
Mihir Kandoi
4f312fcba4 docs(postgres): a representative row must not be picked by Min(name) (#59135) 2026-09-17 08:40:46 +00:00
Mihir Kandoi
f3c2a836eb fix(stock): break the disassembly representative tie on a date, not a name (#59131) 2026-09-17 08:39:01 +00:00
Mihir Kandoi
f97660d10e fix(manufacturing): take the job card secondary UOM off the representative line (#59129) 2026-09-17 08:35:36 +00:00
Mihir Kandoi
00b7e6e9eb fix(stock): reserve product bundle components from a Pick List (#59134) 2026-09-17 13:58:02 +05:30
Mihir Kandoi
0f66c41819 fix(buying): take the requested-item UOM pair off one line (#59132) 2026-09-17 08:26:34 +00:00
Mihir Kandoi
2aab7f4f72 fix(accounts): take the POS summary labels off one invoice, not a text sort (#59130) 2026-09-17 08:23:12 +00:00
Mihir Kandoi
228bbe845f fix(controllers): report a Prospect quotation's territory (#59133) 2026-09-17 08:21:59 +00:00
pandiyan
3384c1939b test: cover coupon pricing rule and date validation 2026-09-17 11:54:06 +05:30
pandiyan
14275e4c01 fix: validate coupon pricing rules and validity dates 2026-09-17 11:31:59 +05:30
Pandiyan P
60913b722a fix: retain bom components without warehouse stock (#59116)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-09-17 11:08:07 +05:30
Mihir Kandoi
0e31182dca perf(stock): skip the negative batch scan when nothing is negative (#59061) 2026-09-17 11:07:27 +05:30
Mihir Kandoi
ded6df3614 fix(selling): reset ordered_qty when a cancelled Sales Order is unlinked from its Purchase Orders (#59120) 2026-09-17 11:05:11 +05:30
Mihir Kandoi
cb409d710a refactor(manufacturing): share the bin subquery in BOM Stock Analysis (#59121) 2026-09-17 10:59:02 +05:30
Abdeali Chharchhodawala
4e3e301c90 fix: formula evaluation and line reference validation for FRT (#59084)
* 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
2026-09-17 10:47:37 +05:30
Diptanil Saha
863aa45e51 fix: add permission checks and record-level scoping to whitelisted methods (#59100) 2026-09-17 10:30:47 +05:30
Afsal Syed
1d8ce1ee8c fix(stock): allow zero completed quantity and handle process loss in job cards (#59104) 2026-09-16 20:14:32 +05:30
pandiyan
62d0e2c82e test: cover bank reconciliation date validation 2026-09-16 15:57:50 +05:30
pandiyan
970f32a342 fix: validate bank reconciliation date ranges 2026-09-16 15:57:43 +05:30
Shllokkk
4b858ec0af Merge pull request #59078 from Shllokkk/soa-render-letter-head-template
fix: render letter head jinja in statement of accounts pdf
2026-09-16 15:39:55 +05:30
rohitwaghchaure
f6b907ef9d fix: update modified for last purchase rate (#59044) 2026-09-16 15:23:47 +05:30
Pandiyan P
2273448f1b fix: avoid pricing rule help template errors (#59097) 2026-09-16 07:40:22 +00:00
Pandiyan P
b439bdda68 fix: clear stock value for zero-quantity balances (#58899)
* fix: clear stock value for zero-quantity balances

* test(stock): cover zero-quantity residual stock values
2026-09-16 13:00:27 +05:30
Khushi Rawat
4974372073 Merge pull request #59085 from khushi8112/print-format-classic-item-description
fix: show item description in Classic print formats
2026-09-16 12:18:20 +05:30
Pandiyan P
2ad4a8c4a4 fix(manufacturing): apply mrp manufacturing buffer as a duration (#59007)
* fix(manufacturing): apply mrp manufacturing buffer as a duration

* test(manufacturing): cover mrp buffer durations
2026-09-16 06:22:43 +00:00
Pandiyan P
00ee694444 fix(stock): filter item group defaults by company and non-group records (#58923) 2026-09-16 11:47:51 +05:30
Sudharsanan Ashok
6cee9c330c fix(stock): use stored posting_datetime when reposting stock ledger entries (#58998)
* 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.
2026-09-16 11:45:32 +05:30
khushi8112
f73d818ff4 feat: show item description in Classic print formats
Merge the item description as a secondary line under the item name in the
Item column of all Classic transaction print formats.
2026-09-16 11:36:15 +05:30
Raffael Meyer
29335f56b2 feat(manufacturing): ask for a BOM before creating MRP work orders (#58511)
* feat(manufacturing): ask for a BOM before creating MRP work orders

* test(manufacturing): cover MRP work orders created with a supplied BOM
2026-09-16 01:59:21 +02:00
Raffael Meyer
965124ba7c fix(manufacturing): classify MRP rows by purchase item, not missing BOM (#58510) 2026-09-16 01:43:50 +02:00
Khushi Rawat
2fff457e34 Merge pull request #58688 from krishna-254/fix/issue-55219-bank-reconciliation-document-types
fix: handle missing bank reconciliation document types
2026-09-16 00:01:58 +05:30
Khushi Rawat
8e14cd2394 Merge pull request #58941 from Henil666/fix/asset-depreciation-checkbox-order-clean
fix: asset depreciation table stays empty if you check the box before entering the amount
2026-09-15 23:57:28 +05:30
Khushi Rawat
514c3b45d0 chore: remove unnecessary comments 2026-09-15 23:56:20 +05:30
Shllokkk
da377683d6 fix: render letter head jinja in statement of accounts pdf 2026-09-15 21:52:30 +05:30
Mihir Kandoi
9be19e69b5 fix(buying): show linked record count on Supplier Scorecard connections (#59079) 2026-09-15 13:58:42 +00:00
Mihir Kandoi
41ad83dfb1 fix(ui): do not decorate self-referential links with the row's own title (#59023) 2026-09-15 19:03:03 +05:30
Mihir Kandoi
09dea8b1b5 fix: item selection writes to the child table that fired it (#59068) 2026-09-15 12:13:43 +05:30
Mihir Kandoi
f3dcc6180f fix(stock): value stock entry rows as of the posting date (#59042) 2026-09-15 12:11:21 +05:30
MochaMind
198d40446f fix: sync translations from crowdin (develop) (#59051)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-09-14 19:06:24 +02:00
Mihir Kandoi
5a63b36c3b fix(stock): consume batch slots newest first for LIFO items (#59062)
* 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.
2026-09-14 17:51:15 +05:30
Mihir Kandoi
990a43ed16 fix(stock): scope stock ageing batch and serial age to the warehouse (#59058)
* 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.
2026-09-14 17:51:14 +05:30
Mihir Kandoi
9d3675405a perf(stock): look up stock accounts once per company in the warehouse account map (#59056) 2026-09-14 13:15:12 +05:30
Sudharsanan Ashok
fe25746feb fix(subcontracting): validate project across the subcontracting flow (#58965) 2026-09-13 11:58:56 +05:30
Mihir Kandoi
f5f956c4dd test: stop six tests from passing without running (#59025)
* 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.
2026-09-12 14:23:11 +05:30
Mihir Kandoi
fe9d6e5a57 test: remove seven tests that cannot fail (#59024)
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.
2026-09-12 14:23:11 +05:30
Mihir Kandoi
bec627c3eb refactor(manufacturing): rename secondary item valuation type to "% of Component Cost" (#59021) 2026-09-12 06:55:42 +05:30
Mihir Kandoi
bee358ea25 fix(support): stop the SLA link filter patches from aborting migrate on PostgreSQL (#59018) 2026-09-12 06:20:50 +05:30
Pandiyan P
000dcfc23d feat(stock): show reserved stock in batch balance history (#59008) 2026-09-11 18:21:16 +05:30
Pandiyan P
825d24f406 fix(stock): calculate batch bundle valuation per unit (#58994) 2026-09-11 12:50:49 +00:00
Pandiyan P
e6f431a8d6 fix: prevent disabled items from being used in BOM (#58997)
Co-authored-by: Ajish18 <ajishiyappan1@gmail.com>
2026-09-11 12:20:30 +00:00
Mihir Kandoi
5dfd21cce6 fix(selling): list billable sales orders oldest first (#59010) 2026-09-11 12:05:02 +00:00
Krishna Pramod Shirsath
b85da1628d Merge branch 'develop' into fix/issue-55219-bank-reconciliation-document-types 2026-09-11 16:25:39 +05:30
Shllokkk
6f5f2cfce1 Merge pull request #58979 from Shllokkk/fix-bom-cost-allocation-per-type
fix: coerce cost_allocation_per to float in BOM cost allocation
2026-09-11 14:16:57 +05:30
Mihir Kandoi
142976a829 test(accounts): expect a naming rule counter per resolved prefix (#59000) 2026-09-11 08:12:52 +00:00
Henil
5a8126412a fix(assets): discard stale set_finance_book responses
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.
2026-09-11 13:02:04 +05:30
Diptanil Saha
1dd0b7dc9e fix(regional)!: remove irs_1099_print whitelisted method (#58976) 2026-09-11 09:40:22 +05:30
Diptanil Saha
b481083ff0 fix(accounts): add permission checks on invoice_discounting.get_invoices (#58975) 2026-09-11 09:39:38 +05:30
Raffael Meyer
be8208e7cb fix(stock): recalculate billing status of returned delivery notes (#58953) 2026-09-10 19:56:28 +02:00
Mihir Kandoi
4671d1a665 fix(stock): apply company restriction to Item Price (#58948) 2026-09-10 20:51:48 +05:30
Mihir Kandoi
5f216c5d55 fix(selling): exclude fully billed orders from the invoice picker (#58966) 2026-09-10 20:50:11 +05:30
Shllokkk
4d95a240bb chore: resolve merge conflicts 2026-09-10 19:07:25 +05:30
Diptanil Saha
86821ac6ee fix(accounts): add permission checks on execute_repost_payment_ledger (#58980) 2026-09-10 13:33:38 +00:00
Shllokkk
bc2fa03730 fix: coerce cost_allocation_per to float in BOM cost allocation 2026-09-10 18:53:39 +05:30
Mihir Kandoi
e6b8e90ad9 fix(stock): do not rebuild the Warehouse tree on an account map miss (#58956) 2026-09-10 18:15:28 +05:30
Pandiyan P
d82c35aae9 fix: avoid TypeError on new Work Order when company is not set (#58969)
Co-authored-by: Ajish18 <ajishiyappan1@gmail.com>
2026-09-10 12:33:06 +00:00
Sudharsanan Ashok
b1e99a70ac fix(mrp): align monthly bucket labels with demand dates (#58963) 2026-09-10 12:13:44 +00:00
Mihir Kandoi
d5e63b8a9e fix(subcontracting): cannot select Against Finished Good on customer receipt (#58949) 2026-09-09 18:42:31 +05:30
Pandiyan P
3be0c7801a fix(manufacturing): handle empty raw materials in workstation (#58927) 2026-09-09 18:05:03 +05:30
Pandiyan P
33a066d568 fix(manufacturing): handle empty bom cost allocation (#58939) 2026-09-09 18:04:10 +05:30
ruthra kumar
cf6aeddc29 Merge pull request #58732 from ruthra-kumar/rewriting_process_pcv_on_map_reduce
refactor: rewriting process pcv on mapreduce
2026-09-09 17:28:04 +05:30
ruthra kumar
3f422f8e0d refactor: use qb for estimation and include correction factor 2026-09-09 17:08:40 +05:30
Afsal Syed
f130c64530 fix(stock): set item_name to fetch if empty in packing slip item (#58925) 2026-09-09 11:36:56 +00:00
Henil
5d1ffa7fca fix(assets): build Finance Books even when Calculate Depreciation is checked before Net Purchase Amount is entered
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.
2026-09-09 16:34:36 +05:30
ruthra kumar
681bd2734f refactor: post ledger entries once mapreduce is complete 2026-09-09 16:23:32 +05:30
ruthra kumar
31205c4114 refactor: dashboard for pcv 2026-09-09 16:23:32 +05:30
ruthra kumar
503a80f2c9 refactor: dynamic link to mapreduce and clean on cancel and trash 2026-09-09 16:23:32 +05:30
ruthra kumar
3a8bd852d5 refactor: rebuild pcv on mapreduce (parallelization) 2026-09-09 16:23:31 +05:30
Diptanil Saha
a8ec43bcf8 fix(selling): add email permission check on send_emails (#58935) 2026-09-09 15:37:55 +05:30
Diptanil Saha
e2b2940452 fix(accounts): add permission checks on send_proforma_email (#58933) 2026-09-09 09:53:29 +00:00
Shllokkk
3761eb8cbe fix: % Amount Billed exceeds 100% on fully rejected Purchase Receipts (#58885)
* 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>
2026-09-09 14:58:18 +05:30
Mihir Kandoi
fd492100b0 fix(stock): ignore disabled price lists on transactions (#58926) 2026-09-09 09:15:44 +00:00
Mihir Kandoi
28bd1332de fix(accounts): stop counting received qty on non-stock invoice returns (#58924)
* 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.
2026-09-09 08:55:35 +00:00
Pandiyan P
4b23cee2ea fix(stock): remove empty delivery stops after mapping (#58896)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-09-09 07:24:10 +00:00
Khushi Rawat
bdf124ab06 Merge pull request #58909 from khushi8112/fix/posting-date-confirmation
fix: read posting date confirmation setting from boot data
2026-09-09 12:50:41 +05:30
Pandiyan P
b2bdeaa672 fix: validate shelf life for automatically created expiring batches (#58911)
Co-authored-by: Ajish18 <ajishiyappan1@gmail.com>
2026-09-09 07:07:03 +00:00
khushi8112
bcade8f0ba fix: refresh posting date confirmation setting 2026-09-09 12:36:28 +05:30
Pandiyan P
4c32acf300 fix: reset price list fallback when changing parties (#58893) 2026-09-09 12:26:05 +05:30
khushi8112
db9e93306a fix: read posting date confirmation setting from boot data 2026-09-09 11:52:42 +05:30
Khushi Rawat
e93ca84398 Merge pull request #58227 from aerele/fix/dunning-interest-lost-after-payment
fix: keep a dunning claimable until its interest is paid too
2026-09-09 10:47:07 +05:30
Pandiyan P
afd93cf867 fix(stock): guard inventory dimension lookup without reference document (#58889)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-09-09 07:42:40 +05:30
Vishnu Priya Baskaran
51fb261b6b fix: calculate holiday list totals for half days (#58792)
Co-authored-by: nareshkannasln <nareshkannashanmugam@gmail.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-09-09 07:38:23 +05:30
Pandiyan P
f8c2f3440b fix(accounts): reject disabled price lists in buying and selling (#58891) 2026-09-09 06:57:48 +05:30
Diptanil Saha
991cea5ae2 fix(accounts): add permission checks to Chart of Accounts Importer methods (#58892) 2026-09-09 01:14:27 +05:30
Diptanil Saha
11d2847d3e fix(support): bind issue split to its own timeline (#58890) 2026-09-09 00:26:39 +05:30
Krishna Pramod Shirsath
e0d6d797d7 fix(stock): preserve rates for unsaved mapped rows (#58852)
* fix(stock): preserve rates for unsaved mapped rows

* test(stock): cover mapped delivery note item rates
2026-09-08 18:45:02 +05:30
pandiyan
d5a9d158f9 fix: keep a dunning claimable until its interest is paid too
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.
2026-09-08 17:49:36 +05:30
pandiyan
fa89552d10 test: dunning must stay unpaid when only the invoiced sum is paid
paying a dunned invoice without its interest and fee marks the dunning
resolved, and the interest can no longer be collected.
2026-09-08 17:48:15 +05:30
Krishna Pramod Shirsath
cee9f4949a fix: show cost center for unmatched payments in receivables (#58453) 2026-09-08 16:52:53 +05:30
Vishnu Priya Baskaran
218e7927ff fix: fetch accounts by company (#58523) 2026-09-08 16:47:46 +05:30
Pandiyan P
36a4dfe797 fix(accounts): reject same-account internal transfers (#58529) 2026-09-08 16:29:20 +05:30
Sudharsanan Ashok
e825bb2f74 fix(accounts): round journal entry totals to field precision (#58629) 2026-09-08 16:26:04 +05:30
Pandiyan P
f864333afa fix(stock): recalculate delivery note billing after return (#58869) 2026-09-08 10:51:55 +00:00
Krishna Pramod Shirsath
c412310eb5 fix: use account currency in Bank Reconciliation Statement print (#58692) 2026-09-08 16:14:09 +05:30
Sudharsanan Ashok
1728d1b0f5 fix(stock): distribute additional costs when incoming items have no value (#58842)
* 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.
2026-09-08 16:13:12 +05:30
Lakshit Jain
79fc039092 Merge pull request #58682 from ljain112/fix-tds-subcontracting
fix(subcontracting): add condition to exclude tax withholding accounts in purchase receipt mapping
2026-09-08 14:53:58 +05:30
Vishnu Priya Baskaran
467f54162f fix(gross-profit): include item_name in export (#58631) 2026-09-08 14:43:42 +05:30
Diptanil Saha
f60c349794 fix(buying): restrict linked material requests to permitted documents (#58855) 2026-09-08 14:23:03 +05:30
Pandiyan P
cadc0ca86d fix: persist rendered terms on transaction save (#58858) 2026-09-08 13:28:30 +05:30
Sudharsanan Ashok
2f572b1624 fix(stock): distribute additional costs when incoming items have no value (#58841) 2026-09-08 13:27:41 +05:30
Diptanil Saha
ed368a3e2a fix(accounts): enforce account field allow-list on financial report filters (#58790) 2026-09-08 11:30:26 +05:30
Mihir Kandoi
d64e45582a fix(manufacturing): prevent excess process loss from inflating plan quantities (#58847) 2026-09-08 04:22:02 +00:00
Mihir Kandoi
1615ecd25e fix(manufacturing): account for process loss in Production Plan Work Orders (#58799) 2026-09-08 09:17:38 +05:30
Mihir Kandoi
a83a9af9bc fix(stock): allow parent item groups in filters (#58839) 2026-09-08 09:16:23 +05:30
Nikhil Kothari
f2d72f973d fix(banking): Federal bank dark logo (#58844) 2026-09-07 20:41:47 +00:00
Mihir Kandoi
c613a7f7f0 fix(selling): subtract billed qty for zero-amount Sales Order rows (#58816) 2026-09-07 19:27:00 +05:30
Mihir Kandoi
5cea05b302 fix(manufacturing): share transfer stock across Production Plan rows (#58812)
* 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
2026-09-07 19:04:26 +05:30
Mihir Kandoi
65048863db fix(manufacturing): apply safety stock once across Production Plan rows (#58806)
* fix(manufacturing): apply safety stock once across Production Plan rows

* test(manufacturing): cover safety stock once across Production Plan rows
2026-09-07 19:04:26 +05:30
Mihir Kandoi
b1c0921c2b fix(manufacturing): apply MOQ once across Production Plan rows (#58805)
* 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
2026-09-07 19:04:25 +05:30
Mihir Kandoi
9c4d03f26f fix(manufacturing): sum required quantity when consolidating sub-assemblies (#58807) 2026-09-07 18:59:33 +05:30
Nikhil Kothari
ebe5decb96 fix(banking): UI cleanup and better statement parsing (#58817)
* 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>
2026-09-07 13:10:02 +00:00
rohitwaghchaure
e85e300f8f fix: user not able to set valuation rate zero in stock reco (#58800)
* 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
2026-09-07 12:47:54 +00:00
Mihir Kandoi
683f033c34 fix(selling): fetch orders within billing allowance (#58751) 2026-09-07 17:57:59 +05:30
MochaMind
ec738eec28 fix: sync translations from crowdin (develop) (#58579)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-09-07 13:08:22 +02:00
Pandiyan P
72fa7d0b10 feat: add subcontracting receipt ledger previews (#58698) 2026-09-07 05:49:17 +05:30
Diptanil Saha
223a97904a fix(manufacturing): add permission checks to BOM whitelisted methods (#58789) 2026-09-07 00:49:03 +05:30
Diptanil Saha
7b53402eae fix: add permission checks to whitelisted methods (#58787) 2026-09-06 22:57:15 +05:30
MochaMind
353e3a805f chore: update POT file (#58784) 2026-09-06 15:56:03 +02:00
Mihir Kandoi
c1e0865771 fix(controllers): drop the Check-field guard from dimension search (#58783) 2026-09-06 07:27:26 +00:00
Afsal Syed
71f1edeb1c fix(stock): show actual outgoing rate in ledger preview (#58700) 2026-09-05 18:43:11 +05:30
Shllokkk
135921ca79 Merge pull request #58769 from Shllokkk/fix-profit-and-loss-dashboard-chart
fix: restore hover tooltip on Profit and Loss dashboard chart
2026-09-05 14:35:01 +05:30
Shllokkk
9e9c4b8102 fix: restore hover tooltip on Profit and Loss dashboard chart
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-05 14:22:37 +05:30
Raffael Meyer
5beed5f4f0 fix: resolve code lists by URI and version (#58770) 2026-09-04 20:21:50 +02:00
Nikhil Kothari
04b84ef069 fix(banking): find transfers on the same day (#58766) 2026-09-04 13:16:19 +00:00
Hussain Nagaria
154c6fb943 fix(bank reconciliation): match Payment Entries on the bank-side amount (#57740)
* 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>
2026-09-04 17:20:09 +05:30
Vishnu Priya Baskaran
c3319d74cf fix: add reconciliation after submit logic for bank transactions (#57330)
Co-authored-by: Poovetha <poovethapalanivelu@gmail.com>
2026-09-04 17:05:39 +05:30
Mihir Kandoi
b2233a44d1 revert(manufacturing): remove material coverage changes (#58714) 2026-09-04 14:32:28 +05:30
Pandiyan P
5895ed0ee9 fix: prevent duplicate Batch messages and Project links (#58705) 2026-09-04 14:31:23 +05:30
Abdeali Chharchhodawala
1b7da82669 fix: minor improvements to financial report template validation (#58724)
* fix: address review comments on financial report template validation

* refactor: minor fixes
2026-09-04 14:00:57 +05:30
Pandiyan P
0b1f1d6851 fix: check material request price list permission (#58740) 2026-09-04 13:02:59 +05:30
Pandiyan P
00f04fc084 fix(batch): show Expired status only after expiry date has passed (#58736)
Co-authored-by: Ajish18 <ajishiyappan1@gmail.com>
2026-09-04 12:21:01 +05:30
Krishna Pramod Shirsath
f368a5b64e fix(timesheet): handle empty allowed projects (#58745) 2026-09-04 11:34:29 +05:30
Soham Kulkarni
f8951034be Merge pull request #58729 from sokumon/sidebar-changes
fix: more sidebar improvements
2026-09-03 15:16:36 +05:30
sokumon
56caf3c770 fix: more sidebar improvements 2026-09-03 14:46:30 +05:30
Sumit Jain
3b9535ed91 Merge pull request #58726 from frappe/fix-account-tree
feat(accounts): disabled marker and cost center parity in tree views
2026-09-03 14:05:18 +05:30
Sumit Jain
f8c53be376 fix(accounts): drop unsupported badge theme on account number 2026-09-03 13:53:27 +05:30
Sumit Jain
a1b402020e feat(accounts): enhance account and cost center tree views with additional flags and labels 2026-09-03 13:38:20 +05:30
Diptanil Saha
e3a976f115 fix(setup): strict permissions for transaction deletion record (#58687) 2026-09-03 06:28:23 +00:00
dependabot[bot]
68666c2ed0 chore(deps): bump browserslist from 4.28.4 to 4.28.8 in /banking (#58721)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.4 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.4...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 05:33:45 +00:00
Diptanil Saha
717af27559 ci: authenticate github clones in install.sh (#58718) 2026-09-03 10:50:32 +05:30
Sagar Vora
4944df8733 Merge pull request #58697 from sagarvora/frt-fixes
fix!: improve validation in financial report template
2026-09-02 17:08:04 +05:30
Sagar Vora
19aa3b20e0 fix!: improve validation in financial report template 2026-09-02 16:46:02 +05:30
rohitwaghchaure
09f5e76b77 fix: rearrange fields in BOM Operation (#58683) 2026-09-02 16:44:16 +05:30
rohitwaghchaure
646c7d042d fix: check write permission in whitelisted document methods (#58689)
* fix: check write permission in whitelisted document methods

* fix: keep production plan status roll-up permission agnostic
2026-09-02 15:40:10 +05:30
Shllokkk
3e08a085b9 Merge pull request #58684 from Shllokkk/lead-add-to-prospect-read-permission-check
fix(crm): check read permission on lead in add_lead_to_prospect
2026-09-02 14:54:01 +05:30
Krishna Pramod Shirsath
2918e98a2b fix(stock): allow creating stock closing balances (#58590) 2026-09-02 08:24:46 +00:00
Shllokkk
02fcdc0337 fix(crm): check read permission on lead in add_lead_to_prospect 2026-09-02 13:50:38 +05:30
Krishna Pramod Shirsath
a2071a6fdd fix: filter cancelled BOMs in BOM Stock Analysis (#58647) 2026-09-02 13:33:15 +05:30
ljain112
13031d6d5d fix(subcontracting): add condition to exclude tax withholding accounts in purchase receipt mapping 2026-09-02 13:25:23 +05:30
Krishna Shirsath
d39e677d80 fix: handle missing bank reconciliation document types 2026-09-02 13:21:24 +05:30
Vishnu Priya Baskaran
dbe153a15e fix: include payment deductions in sales/purchase register ledger bal… (#58437) 2026-09-02 12:30:08 +05:30
Vishnu Priya Baskaran
f16f249a38 fix(pos): use company-currency change amount when netting pos gl entries (#58599) 2026-09-02 12:22:24 +05:30
Vishnu Priya Baskaran
9cb736a271 fix(stock): prorate landed cost charge into transaction currency (#58575)
* fix(stock): prorate landed cost charge into transaction currency

* test(stock): assert landed cost charge transaction-currency amount
2026-09-02 12:10:22 +05:30
Shllokkk
c93815b4ae fix: filter fully ordered items when creating RFQ from Material Request (#58534) 2026-09-02 06:15:04 +00:00
Pandiyan P
2b54a581f8 fix(stock): preserve exchange gain loss journals in lcv (#58505) 2026-09-02 11:44:38 +05:30
Mihir Kandoi
199cae9496 fix(stock): subtract stock qty of same-document rows from batch availability (#58669)
* 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
2026-09-02 11:42:37 +05:30
Mihir Kandoi
9261c9b47f fix(stock): assign batch_no only when the first batch covers the full qty (#58668)
* 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
2026-09-02 11:42:37 +05:30
Mihir Kandoi
0230879501 ci: allow shallow fetch when fast-forwarding the cached bench (#58666) 2026-09-02 11:20:21 +05:30
kaulith
56a391c522 fix(crm): validate contact email before saving an email campaign (#58667) 2026-09-02 05:34:22 +00:00
Pandiyan P
e74ab38eeb fix(manufacturing): use item warehouses in production plan work orders (#58663) 2026-09-02 04:34:05 +00:00
Pandiyan P
a9b857bdfe fix(stock): ignore cancelled batch entries in valuation (#58659) 2026-09-01 15:49:51 +00:00
Diptanil Saha
a20fc4f931 fix: add missing perm checks on various whitelisted methods (#58652) 2026-09-01 19:12:21 +05:30
Shllokkk
22e3b87621 Merge pull request #58607 from Shllokkk/trial-balance-party-exclude-zero-rounding
fix: round party balances to currency precision in Trial Balance for Party
2026-09-01 15:18:22 +05:30
Mihir Kandoi
e2f24eea15 feat(buying): track supplier quotation order status (#58572) 2026-09-01 08:18:29 +00:00
Pandiyan P
db52e465d0 fix: handle BOM price list currency update (#58638) 2026-09-01 13:31:37 +05:30
Raffael Meyer
5691a7b3c7 fix: explain missing fields when loading party details or taxes (#58627) 2026-09-01 01:31:06 +02:00
Raffael Meyer
0d78261bdf fix: translate label in party validation (#58624) 2026-09-01 00:47:53 +02:00
Diptanil Saha
120e62f208 fix(crm): add missing permission validation on get_contract_template (#58621) 2026-09-01 00:58:58 +05:30
Diptanil Saha
21a3b1c3dc fix: validate POS Settings invoice and search fields on the server (#58611) 2026-08-31 23:43:35 +05:30
Raffael Meyer
d8432d92c8 fix: do not map the same row twice in "Get Items From" (#58617) 2026-08-31 17:15:22 +02:00
Sumit Jain
31761699ae fix: enhance tree view functionality for accounts and cost centers (#58520) 2026-08-31 19:56:24 +05:30
Pandiyan P
7ecfa6b356 fix(stock): correct reservation and pick list quantities (#58613) 2026-08-31 19:38:31 +05:30
Mihir Kandoi
24209ae699 fix(manufacturing): handle duplicate root BOM items (#58614)
* fix(manufacturing): handle duplicate root BOM items

* test(manufacturing): remove duplicate root item test
2026-08-31 14:04:26 +00:00
Pandiyan P
26d000e15f fix(stock): validate serial batch bundle company (#58608) 2026-08-31 12:08:23 +00:00
Mihir Kandoi
dbae23765e fix: keep closed rows out of Update Items (#58609) 2026-08-31 12:03:00 +00:00
Mihir Kandoi
c755e24731 fix: compare updated item quantities in stock UOM (#58603) 2026-08-31 17:05:30 +05:30
Mihir Kandoi
d5789c2e8b feat: close individual transaction items (#57596) 2026-08-31 16:57:48 +05:30
Pandiyan P
0f14f8050f fix: correct bom sorting and stock translations (#58605) 2026-08-31 11:26:42 +00:00
Shllokkk
b1c7657dfa fix: round party balances to currency precision in Trial Balance for Party 2026-08-31 16:43:31 +05:30
rohitwaghchaure
b36895a4c3 feat: production plan visualizer page and summary report fix (#58541)
* 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
2026-08-31 16:24:12 +05:30
Mihir Kandoi
509501c299 feat(manufacturing): add multi-currency support to Blanket Orders (#58472) 2026-08-31 09:45:34 +00:00
Smit Vora
20ea09b4a0 Merge pull request #58483 from ljain112/opening-taxes
fix(taxes): skip tax addition for invoice created from opening invoice tool
2026-08-31 15:05:12 +05:30
Mihir Kandoi
8ca2905a33 feat: valuation method for BOM secondary items (#58431) 2026-08-31 14:51:45 +05:30
Pandiyan P
9087f2cdba fix: widen item name in stock projected qty (#58598) 2026-08-31 08:32:17 +00:00
Mihir Kandoi
6cca7d670b fix: restore isolated loyalty and subcontracting tests (#58587) 2026-08-31 13:40:14 +05:30
Diptanil Saha
4355f8e60e fix(pos): add permission checks on get_invoices (#58591) 2026-08-31 13:23:59 +05:30
Diptanil Saha
0e4b384af1 fix(accounts): added permission checks on get_available_payment_schedules (#58588) 2026-08-31 07:36:01 +00:00
Mihir Kandoi
83dea1a24e test: prevent update_doctypes from exporting files (#58589) 2026-08-31 07:31:41 +00:00
Aarol D'Souza
6d445d94ee Merge pull request #58586 from AarDG10/patch-erpnext
fix(sms_settings): add patch to pre-fill roles into SMS Settings Role Table
2026-08-31 12:27:32 +05:30
Aarol D'Souza
05e5e50a6f Merge branch 'develop' into patch-erpnext 2026-08-31 11:51:59 +05:30
AarDG10
3501beb2bd fix(sms_settings): add patch to pre-fill roles into SMS Settings Roles Table 2026-08-31 11:38:47 +05:30
Nishka Gosalia
7af1af35cd Merge pull request #58325 from Shllokkk/project-duplicate-read-permission-check
fix(projects): check read permission on source in create_duplicate_project
2026-08-31 11:14:40 +05:30
Mihir Kandoi
86852d954e test: narrow shared fixture hardening (#58581) 2026-08-31 05:26:28 +00:00
Diptanil Saha
d5df40986d fix(timesheet): scoping whitelisted methods output to projects and timesheets that are acccessible to users (#58267) 2026-08-30 23:16:32 +05:30
MochaMind
1cedd1421d chore: update POT file (#58566) 2026-08-30 13:00:36 +02:00
Mihir Kandoi
db56080285 fix(manufacturing): use packed row delivery date (#58568) 2026-08-30 10:44:23 +00:00
Pandiyan P
8b43394533 fix(stock): load available serial no report (#58558) 2026-08-29 20:24:58 +05:30
Diptanil Saha
caf8a36bdb fix(accounts): added permission checks on multiple payment entry whitelisted methods (#58555) 2026-08-29 18:06:25 +05:30
Diptanil Saha
a59792a605 refactor(stock): remove dead warehouse_condition branch on get_stock_ledger_entries (#58552) 2026-08-29 11:39:11 +00:00
Shllokkk
6d13e4a633 Merge pull request #58324 from Shllokkk/crm-note-edit-note-permission-check
fix(crm): check write permission in edit_note
2026-08-28 20:34:38 +05:30
Mihir Kandoi
90ac7db704 test: restore ERPNext test coverage (#58542) 2026-08-28 20:09:03 +05:30
Shllokkk
eb49f51d29 fix(crm): check write permission in edit_note 2026-08-28 17:17:04 +05:30
rohitwaghchaure
0223223385 feat: batch split operation to produce child batches per piece (#58530)
* 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
2026-08-28 17:11:02 +05:30
Diptanil Saha
ca49de633f fix(stock): auto-select batch no before saving transaction records (#58536) 2026-08-28 17:01:23 +05:30
Soham Kulkarni
64186f229f Merge pull request #58476 from sokumon/module-sidebars
feat: erpnext sidebars and workspaces
2026-08-28 16:13:20 +05:30
Afsal Syed
074c84e880 fix: persist redistributed additional costs during stock entry repost (#58433)
* fix: persist redistributed additional costs during stock entry repost

* test: cover additional cost persistence on stock entry recalculation
2026-08-28 15:51:51 +05:30
rohitwaghchaure
1f7f8cd9d3 feat: alternative finished goods conversion against work order (#58479)
* 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
2026-08-28 12:56:06 +05:30
Mihir Kandoi
9160182727 test: improve ERPNext test isolation and runtime (#58507) 2026-08-28 10:41:22 +05:30
sokumon
0985451276 fix: remove the conf file 2026-08-28 02:27:14 +05:30
sokumon
987408f203 chore: some more fixtures 2026-08-28 02:27:14 +05:30
sokumon
2a14b5d953 feat: ship new erpnext workspaces 2026-08-28 02:27:14 +05:30
Diptanil Saha
9018573179 fix(accounts): set pos profile on invoices respecting user permissions (#58508) 2026-08-27 22:06:36 +05:30
Raffael Meyer
e95ca2444c fix: translate doctype in map msg (#58515) 2026-08-27 16:14:49 +00:00
Raffael Meyer
971b6fd49d fix(manufacturing): classify MRP items without a BOM as Purchase (#58509) 2026-08-27 17:55:08 +02:00
rohitwaghchaure
457283f2b0 Revert "refactor(stock): use db.count for the empty ledger check" (#58506)
Revert "refactor(stock): use db.count for the empty ledger check (#58486)"

This reverts commit 2866be2816.
2026-08-27 13:51:49 +00:00
Mihir Kandoi
0849f187e7 fix(projects): ignore cancelled invoices in timesheet portal (#58501) 2026-08-27 12:52:30 +00:00
Pandiyan P
a8ba713f80 fix(selling): check sales order permission before work order creation (#58492) 2026-08-27 12:05:43 +00:00
Pandiyan P
5f99a3418d fix(selling): check quotation write permission before marking lost (#58493) 2026-08-27 12:04:27 +00:00
Mihir Kandoi
b90e3d4656 fix(manufacturing): preserve job card qty in mr transfer (#58482) 2026-08-27 17:26:17 +05:30
Sudharsanan Ashok
2866be2816 refactor(stock): use db.count for the empty ledger check (#58486)
Align the existence check in `reset_bin_without_stock_ledger_entries()` with
the version-15-hotfix backport in #58434, per review feedback there.
2026-08-27 11:07:09 +00:00
Abdeali Chharchhodawala
6842ebb186 fix: improve message formatting and translation for validation issues (#58425) 2026-08-27 12:56:49 +02:00
ljain112
e08a166281 fix(taxes): skip tax addition for invoice created from opening invoice tool 2026-08-27 15:32:39 +05:30
Pandiyan P
4d4cf034b5 fix: clarify duplicate internal party messages (#58469) 2026-08-27 07:38:25 +00:00
Shllokkk
62e6e23581 fix: validate items against source Sales Order in Material Request (#58443) 2026-08-27 12:14:36 +05:30
Pandiyan P
0d90608bc1 fix(manufacturing): account for pending job card qty (#58466) 2026-08-27 12:12:46 +05:30
Pandiyan P
dd033cbc10 fix(stock): guard serial batch editor grid lookup (#58464) 2026-08-27 11:28:02 +05:30
Henil Maru
d6956790d8 fix: Work Order picks wrong Delivery Date when Sales Order has the same item in multiple rows (#58448) 2026-08-26 20:41:14 +05:30
MochaMind
4def9ed20a fix: sync translations from crowdin (develop) (#58458)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-08-26 13:13:08 +00:00
Diptanil Saha
b2489eaf1c ci: ignore python tests on update of **.po (#58457) 2026-08-26 18:29:57 +05:30
MochaMind
88948bab42 fix: sync translations from crowdin (#58349) 2026-08-26 14:44:17 +02:00
Diptanil Saha
98be36ef12 ci: use release token for crowdin translation push (#58454) 2026-08-26 12:16:49 +00:00
Diptanil Saha
687bb9c839 ci: fix crowdin branch (#58452) 2026-08-26 11:38:01 +00:00
rohitwaghchaure
731f03e2f2 feat: option to skip delivery note for service items in sales order (#58297)
* 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
2026-08-26 16:28:31 +05:30
Diptanil Saha
fd2057befa ci: crowdin actions (#58447)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:26:38 +05:30
Jatin3128
4cfa42921f fix(accounts): resolve subscription plans for any reference doctype in Payment Request (#58438)
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.
2026-08-26 16:16:40 +05:30
Jatin3128
8d2aa69e61 feat(accounts): split bank charges from exchange gain/loss on multi-currency transfers (#58071)
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.
2026-08-26 13:16:24 +05:30
Sudharsanan Ashok
3f29cdf8d2 feat(analytics): filter sales and purchase analytics by entity (#58402)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-08-26 13:02:59 +05:30
Aarol D'Souza
ccc19f2554 Merge pull request #58351 from AarDG10/better-err-msg
fix(company): throw if linked to demo_company field
2026-08-26 12:53:01 +05:30
Mihir Kandoi
2ca02fb305 fix(stock): preserve LCV quantity across stock reconciliation (#58309)
Co-authored-by: RamachandranMD <ramachandran.md@finstein.ai>
2026-08-26 12:16:51 +05:30
Vishnu Priya Baskaran
1b81db4754 fix/payment-request-subscription-plans-population (#57494)
* fix(payment-request): populate subscription plans

* test: add coverage for subscription plans in payment request

---------

Co-authored-by: Dharanidharan2813 <dharanidharans1328@gmail.com>
2026-08-26 11:47:04 +05:30
Vishnu Priya Baskaran
918e5a28db fix(stock): carry accounting dimensions from Landed Cost Voucher char… (#56981)
* 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>
2026-08-26 11:27:08 +05:30
Pandiyan P
c940bd1e66 fix(stock): validate serial inventory dimensions (#58394)
* fix(stock): validate serial inventory dimensions

* test(stock): cover serial inventory dimensions
2026-08-26 11:26:27 +05:30
Raffael Meyer
6bdc18b753 fix(asset): skip missing checkbox columns in asset type patch (#58416) 2026-08-26 02:46:20 +02:00
Khushi Rawat
35b3358a5d Merge pull request #58412 from khushi8112/fix-child-table-accounting-dimension
fix(accounts): prevent child table doctypes as accounting dimensions
2026-08-26 00:47:58 +05:30
khushi8112
55dd11f977 test(accounts): cover single doctype and non-scalar dimension safeguards 2026-08-26 00:36:29 +05:30
Shllokkk
6ddff9c386 Merge pull request #58400 from Shllokkk/reorder-email-single-company-perm
fix: send auto reorder email to all managers in single company setup
2026-08-25 17:38:19 +05:30
Raffael Meyer
84a32c40f4 fix(permissions): grant select on link targets to roles with write access (#58334)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:31:12 +02:00
khushi8112
7b32d07d1c fix(accounts): prevent child table doctypes as accounting dimensions 2026-08-25 16:09:59 +05:30
Mihir Kandoi
6a6f2ac127 ci: notify support when a PR is released (#58321) 2026-08-25 15:50:21 +05:30
Shllokkk
a01cc92184 test: auto reorder email reaches managers without company user permission 2026-08-25 15:44:29 +05:30
Shllokkk
ae119b1c29 fix: send auto reorder email to all managers in single company setup 2026-08-25 15:44:03 +05:30
Pandiyan P
ce23fcc055 fix: respect zero currency precision (#58395) 2026-08-25 10:06:06 +00:00
rohitwaghchaure
8b8422662c fix: production plan scheduling edge cases (#58388)
* 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
2026-08-25 14:45:05 +05:30
Sudharsanan Ashok
9cf76c6a68 fix(stock): keep pick list links when refetching stock entry items (#58374) 2026-08-25 07:50:50 +00:00
Sudharsanan Ashok
6fbcfade6c fix(stock): reset bin when no stock ledger entries remain (#58362)
* 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.
2026-08-25 11:39:11 +05:30
Mihir Kandoi
5fa68dd068 fix(manufacturing): preserve job card transfer quantity (#58382) 2026-08-24 21:38:12 +05:30
Pandiyan P
ac2bacd3d0 fix: prevent duplicate supplier quotations from portal (#58377) 2026-08-24 18:32:12 +05:30
Pandiyan P
7ec34bab8d fix: hide supplier name in rfq portal (#58373) 2026-08-24 17:38:49 +05:30
Pandiyan P
75d6183bb6 fix: hide rfq status in supplier portal (#58368) 2026-08-24 16:29:05 +05:30
Vishnu Priya Baskaran
560ed25ff7 fix(italy): handle none price_list_rate in e-invoice xml generation (#58242) 2026-08-24 16:26:01 +05:30
Mohd Haris
ef7461cb5b fix: aggregate child warehouses in Stock Qty vs Serial No Count report (#58134)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-08-24 15:42:09 +05:30
Vishnu Priya Baskaran
abf94bc72d Fix/return qty validation different uom (#58298)
Co-authored-by: Afsal Syed <afsalsyed12@gmail.com>
2026-08-24 15:34:43 +05:30
Pandiyan P
59d80b29c4 fix: render missing terms before printing (#58358) 2026-08-24 14:53:08 +05:30
Mihir Kandoi
7149df398a fix: include time logs ending at midnight in timesheet billing summary (#58355) 2026-08-24 08:16:50 +00:00
Shllokkk
4a43e5203f Merge branch 'develop' into project-duplicate-read-permission-check 2026-08-24 13:14:19 +05:30
Shllokkk
8c8b282a2e fix(projects): check read permission on source in create_duplicate_project 2026-08-24 13:03:09 +05:30
Khushi Rawat
1b51f8cbd6 Merge pull request #58179 from aerele/fix-accounts-payable-supplier-group-filter
fix(accounts): supplier group filter not applied on accounts payable …
2026-08-24 12:22:21 +05:30
AarDG10
aaa3100efc fix(company): throw if linked to demo_company field
Throws an error msg if user is deleting demo company directly.
2026-08-24 11:10:50 +05:30
Raffael Meyer
64486f34e8 ci: bump po-review-action (#58346) 2026-08-23 17:05:22 +00:00
MochaMind
0ea36160fe fix: sync translations from crowdin (#58289) 2026-08-23 18:51:09 +02:00
MochaMind
6ba623e95f chore: update POT file (#58345) 2026-08-23 13:32:17 +02:00
Raffael Meyer
e71e08959f refactor: use Meta.get_translated_label (#58335) 2026-08-22 00:28:55 +02:00
Raffael Meyer
eae36c2d67 fix(i18n): translate get_label results at presentation with DocType context (#57217) 2026-08-21 23:19:36 +02:00
Jatin3128
275844d496 fix(journal-entry): avoid full grid re-render per row in set_exchange_rate (#58328)
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.
2026-08-21 01:57:04 +05:30
Mihir Kandoi
624d402143 fix(projects): respect permissions in timesheet billing summary (#58320) 2026-08-20 09:43:18 +00:00
Mihir Kandoi
2328e6da94 fix: changing product bundle warehouse should change packed items war… (#58316) 2026-08-20 12:35:04 +05:30
Mihir Kandoi
941baffdda fix: correct to and from date filters in timesheet billing summary re… (#58317) 2026-08-20 04:12:30 +00:00
Pandiyan P
7faadb8282 fix: stop doubling totals in timesheet billing summary (#58311) 2026-08-20 02:26:23 +00:00
Mihir Kandoi
e8b214e7d3 chore: remove mistakenly committed file (#58310) 2026-08-19 15:49:02 +00:00
Mihir Kandoi
8320c77694 fix(manufacturing): scope secondary items to job card (#58260) 2026-08-19 20:53:53 +05:30
Mihir Kandoi
696f0df811 fix(stock): update pick list status for product bundles (#58306) 2026-08-19 20:52:47 +05:30
Mihir Kandoi
deeb787f57 fix(stock): support shared target UOM conversions (#58305) 2026-08-19 18:29:59 +05:30
Mihir Kandoi
478a2f4f4b fix(stock): ignore future stock during batch reservation (#58303) 2026-08-19 12:50:26 +00:00
Mihir Kandoi
cfca71782e fix(stock): preserve item UOM conversion factor (#58299) 2026-08-19 11:45:43 +00:00
Mihir Kandoi
f4ef7fdbcc fix(stock): preserve zero return incoming rates (#58302) 2026-08-19 11:45:29 +00:00
Mihir Kandoi
e5cd30c5ab fix(setup): handle Transaction Deletion Record CSV edge cases (#58300)
Co-authored-by: Pedro Barbosa <barbosamaverickv8@gmail.com>
2026-08-19 17:12:16 +05:30
Mihir Kandoi
8eeb0196e4 fix(stock): show backdated batches in stock entry (#58301) 2026-08-19 11:33:31 +00:00
rohitwaghchaure
d07f4bb857 feat: percentage based BOM (#58292)
* feat: percentage based BOM, fixed qty components and component qty tolerance

* fix: mandatory percentages, fixed qty explosion keys and expected qty aggregation

* fix: percentage BOM incompatible with semi FG tracking, fixed qty in production plan explosion

* refactor: remove component_qty_tolerance from Manufacturing Settings

* refactor: drop fixed qty components and consumption tolerance, keep percentage-based BOM only
2026-08-19 17:03:20 +05:30
Mihir Kandoi
2524af4758 fix: new docs should refetch incoming rates (#58097) 2026-08-19 12:36:22 +05:30
Sudharsanan Ashok
3a6e17a03d fix(stock): fetch item stock UOM in stock reconciliation (#58284) 2026-08-19 10:27:07 +05:30
Diptanil Saha
d070a1daa1 Merge pull request #58286 from diptanilsaha/template-text-context-escaping
fix: escape interpolated values in text positions across portal and desk templates
2026-08-19 02:30:23 +05:30
diptanilsaha
8d4c10871d fix: read project from url argument in project portal script 2026-08-19 02:06:25 +05:30
diptanilsaha
449c90f05f fix: escape event subject in crm activities template 2026-08-19 02:06:24 +05:30
diptanilsaha
96f51092a0 fix: escape help article title, description and category on support page 2026-08-19 02:06:22 +05:30
diptanilsaha
02434ee98c fix: escape partner introduction and address details in partner pages 2026-08-19 02:06:21 +05:30
diptanilsaha
09e1c6024d fix: escape project name and task subject in project portal templates 2026-08-19 02:06:20 +05:30
diptanilsaha
e88164e8c7 fix: escape items preview in transaction row template 2026-08-19 02:06:19 +05:30
diptanilsaha
a5aec7ed84 fix: escape party name and contact display on order page 2026-08-19 02:06:17 +05:30
Diptanil Saha
25dfcb17c8 fix: escape on status image for workstations in production status (#58279) 2026-08-18 17:58:23 +00:00
Diptanil Saha
a667cd7cc6 Merge pull request #58273 from diptanilsaha/template-output-escaping
fix: escape interpolated values in portal, print and desk templates
2026-08-18 23:09:09 +05:30
diptanilsaha
93d6ccd041 refactor: build document links with get_form_link in form scripts 2026-08-18 22:49:47 +05:30
diptanilsaha
cfb664804c refactor: build document links with get_form_link in report formatters 2026-08-18 22:49:45 +05:30
diptanilsaha
57c2887ab4 fix: escape attribute values in shared client side templates 2026-08-18 22:49:43 +05:30
diptanilsaha
09eae9886a fix: escape activity type in project dashboard template 2026-08-18 22:49:41 +05:30
diptanilsaha
79284d8430 fix: escape item and document values in manufacturing preview templates 2026-08-18 22:49:39 +05:30
diptanilsaha
ffb7a708e4 fix: escape item, warehouse and label values in form grid templates 2026-08-18 22:49:28 +05:30
diptanilsaha
0541635f48 fix: escape item image url and item name in item image print formats 2026-08-18 22:49:26 +05:30
diptanilsaha
dc7822b319 fix: escape item image url in compact item print description 2026-08-18 22:49:24 +05:30
diptanilsaha
303c4f9081 fix: escape help article route on support page 2026-08-18 22:49:23 +05:30
diptanilsaha
9a4cb48429 fix: escape partner logo url and partner name in partner pages 2026-08-18 22:49:22 +05:30
diptanilsaha
53394478d9 fix: escape pathname and document name in transaction row link 2026-08-18 22:49:10 +05:30
diptanilsaha
0b1dd6cccf fix: encode image url and escape alt text in shared media macros 2026-08-18 22:49:09 +05:30
diptanilsaha
a3032f2f66 fix: serialize document values with tojson in rfq portal script 2026-08-18 22:49:07 +05:30
diptanilsaha
c42d653da5 fix: render terms and conditions through sanitize_html on order page 2026-08-18 22:49:06 +05:30
diptanilsaha
bc0fd611d4 fix: escape forum topics and get started sections on help page 2026-08-18 22:49:05 +05:30
diptanilsaha
5c4fb99f68 fix: urlencode project name in project portal page and script 2026-08-18 22:49:03 +05:30
diptanilsaha
4c0f0299ea fix: escape assigned user full name in project portal row templates 2026-08-18 22:49:02 +05:30
Mihir Kandoi
d91fce90c7 fix: use user data fields hook (#58274) 2026-08-18 16:07:06 +00:00
MochaMind
f6ab73d5ac fix: sync translations from crowdin (#58225) 2026-08-18 10:15:01 +00:00
Mihir Kandoi
27b06fcb61 fix(accounts): match returns to source invoice items (#58250) 2026-08-18 08:14:48 +00:00
rohitwaghchaure
b68324ce78 fix: work order finish dialog with process loss qty from job card (#58256)
* fix(manufacturing): cap job card completed qty by previous operation and show process loss on finish dialog

* fix: revert job card finish dialog changes
2026-08-18 13:26:56 +05:30
Pandiyan P
1fa057b943 fix: skip covered rows when ordering from the mrp report (#58249)
* 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.
2026-08-18 11:22:18 +05:30
rohitwaghchaure
1f83906189 fix: don't set work order status to In Process only due to skip material transfer (#58244) 2026-08-17 18:29:24 +05:30
Mihir Kandoi
d40679cffe fix(accounts): allocate drop-ship cost by invoice quantity (#58226) 2026-08-17 18:04:02 +05:30
rohitwaghchaure
bf1ec51680 Pick list stock availability (#58243)
* 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
2026-08-17 17:58:15 +05:30
Ejaaz Khan
e9c6ec4d33 Merge pull request #58241 from iamejaaz/feat/pf-split-across-pages
feat: allow Terms to split across pages in standard print formats
2026-08-17 16:01:42 +05:30
Ejaaz Khan
bd2a709ed1 feat: allow Terms to split across pages in standard print formats 2026-08-17 15:35:43 +05:30
Mihir Kandoi
baa20e5e49 fix(manufacturing): preserve attribution through consumption (#58146)
* fix(manufacturing): preserve attribution through consumption

* fix(manufacturing): preserve consumed quantity matching

* fix(manufacturing): assign consumption once
2026-08-17 09:57:57 +00:00
Pandiyan P
4dc0b2ea52 fix(manufacturing): fall back to item group defaults for work order warehouses (#58231) 2026-08-17 08:06:01 +00:00
Diptanil Saha
2f82e0dd36 fix(bank_statement_import): add missing permission check on multiple whitelisted methods (#58221) 2026-08-16 17:29:37 +00:00
Diptanil Saha
9a71ab4d79 fix(bank_statement_import): add missing permission check on get_import_status (#58217) 2026-08-16 20:22:44 +05:30
Diptanil Saha
5a69bb9001 fix(crm)!: remove unused get_last_interaction endpoint (#58214) 2026-08-16 19:32:24 +05:30
MochaMind
6c7b6bd87d fix: sync translations from crowdin (#58203) 2026-08-16 18:03:42 +05:30
MochaMind
b082f60e48 chore: update POT file (#58206) 2026-08-16 18:03:12 +05:30
Pandiyan P
9ee41029c8 fix: drop removed Restaurant doctype from sales tax template dashboard (#58191) 2026-08-16 10:50:11 +00:00
Pandiyan P
4a5943e2fb fix: correct Item Group doctype name in item tax template dashboard (#58192) 2026-08-16 16:14:52 +05:30
Shllokkk
8b8223f48b Merge pull request #58198 from Shllokkk/supplier-quotation-comparison-status-filter
feat: add status filter to Supplier Quotation Comparison report
2026-08-16 15:51:51 +05:30
Diptanil Saha
1212a278c6 Merge pull request #58197 from diptanilsaha/fix/get_digest_msg
refactor(email_digest): convert `get_digest_msg` from module function to instance method
2026-08-15 21:05:44 +05:30
diptanilsaha
c6b172999c fix(email_digest): added permission check for get_msg_html 2026-08-15 20:53:38 +05:30
Shllokkk
7cc236d1a8 feat: add status filter to Supplier Quotation Comparison report 2026-08-15 20:51:30 +05:30
Diptanil Saha
d472e17c75 Revert "refactor(email_digest)!: convert get_digest_msg from module function to instance method" (#58196) 2026-08-15 19:59:33 +05:30
Diptanil Saha
4cd8fbf1c0 refactor(email_digest): convert get_digest_msg from module function to instance method (#58193) 2026-08-15 14:10:14 +00:00
Pandiyan P
cc60c80e68 fix: correct is_frozen description on customer and supplier (#58168) 2026-08-15 12:24:12 +00:00
rohitwaghchaure
70a8a2d0c5 feat: validate purchase receipt exchange rate parity on purchase invoice (#58177) 2026-08-15 14:20:55 +05:30
Antoine Maas
a5472b5080 feat: Belgian Charts of Accounts (commercial + non-profit, FR + NL) (#54679)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-08-14 22:06:40 +05:30
MochaMind
4316cf2f65 fix: sync translations from crowdin (#58145) 2026-08-14 20:03:05 +05:30
Mihir Kandoi
ef24974645 fix(stock): restore subcontracting inward warehouse checks (#58183) 2026-08-14 14:26:25 +00:00
Sudharsanan Ashok
fb7c2caf29 fix(stock): honour pick serial / batch based on in the batch selector (#58176) 2026-08-14 19:25:35 +05:30
Pandiyan P
1519770cb0 perf: filter stock projected qty bins by company in sql (#58173) 2026-08-14 19:20:43 +05:30
Pandiyan P
92c372c6a1 fix: rename misspelled delink_refernce_from_voucher (#58169) 2026-08-14 19:19:17 +05:30
ervishnucs
513f19924d fix(accounts): supplier group filter not applied on accounts payable report 2026-08-14 18:02:25 +05:30
Khushi Rawat
0b01b9bcd3 Merge pull request #57838 from khushi8112/hide/redundant-primary-address-contact-fields
refactor(ui): hide the primary address and contact preview fields
2026-08-14 17:32:09 +05:30
Jatin3128
cbf8f61101 fix: rewriting the description of the overdue billing checkbox (#58172)
fix: rewritting the description of the overdue billing checkbox
2026-08-14 11:18:38 +00:00
Nishka Gosalia
525930c5eb Merge pull request #58167 from nishkagosalia/gh-57661
fix: validation for task end date check
2026-08-14 16:22:07 +05:30
nishkagosalia
7c6da80f98 fix: validation for task end date check 2026-08-14 16:05:52 +05:30
rohitwaghchaure
e5c24fb398 feat: cross-plan load and overlap validation in production plan scheduling (#58127)
* 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
2026-08-14 10:24:48 +00:00
Jatin3128
917badb82b fix: renaming the overdue billing checkbox (#58165) 2026-08-14 10:16:47 +00:00
Pandiyan P
10109674a4 feat(selling): add shipping contact person to sales order, delivery note and sales invoice (#58159) 2026-08-14 15:04:31 +05:30
Mihir Kandoi
73957dc84b refactor(selling): reorder sales invoice mapper arguments (#58162) 2026-08-14 14:36:37 +05:30
Diptanil Saha
3558ce3b59 feat(accounts): opt-in 'Consider Accounting Dimension' filter on General Ledger Report (#58156) 2026-08-14 08:12:08 +00:00
Khushi Rawat
7e2edb5c27 Merge pull request #58092 from aerele/fix/disallow-reverse-of-reverse-journal-entry
fix(accounts): disallow reversing a reverse journal entry
2026-08-14 11:30:44 +05:30
Mihir Kandoi
8632119f11 fix(accounts): filter parties by transaction company (#58147) 2026-08-14 11:02:13 +05:30
rohitwaghchaure
9239d1c2a3 fix: ignore historical negative batch stock in outward validation (#58148) 2026-08-14 10:56:27 +05:30
Mihir Kandoi
eae6d10304 fix(manufacturing): preserve alternative material attribution (#58131) 2026-08-14 03:44:39 +00:00
Diptanil Saha
081d269f6d Merge pull request #58141 from diptanilsaha/fix/fcrm_custom_field
fix(crm_settings): create `crm_deal` fields on enabling frappe crm data synchronization
2026-08-13 23:09:17 +05:30
diptanilsaha
9613d72d81 fix: patch to delete the crm_deal custom fields 2026-08-13 22:56:25 +05:30
diptanilsaha
be2dea0ba2 fix(crm_settings): create custom fields for Frappe CRM on enabling synchronization 2026-08-13 22:56:05 +05:30
Nikhil Kothari
0bbaf4da3e chore(deps): update eslint in banking app (#58129) 2026-08-13 12:29:13 +00:00
Pandiyan P
fd728dacca fix(buying): allow purchase returns against a closed purchase order (#58126) 2026-08-13 17:55:41 +05:30
Sudharsanan Ashok
1e583725a1 feat: delivery note billing based on quantity (#56149)
* feat(stock): bill delivery note by qty when invoiced amount is short

* test(stock): add test for qty fallback in delivery note billing
2026-08-13 16:28:35 +05:30
Sudharsanan Ashok
a2976dd29e fix(stock): confirm before changing item qty from the batch selector (#58123)
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.
2026-08-13 16:27:45 +05:30
Khushi Rawat
99cb98d96c Merge pull request #58120 from khushi8112/stale-exchange-rate-description
fix: describe stale exchange rate settings
2026-08-13 15:10:41 +05:30
Mihir Kandoi
1534e2fad6 fix(manufacturing): derive material transfers from actual coverage (#58110) 2026-08-13 15:06:03 +05:30
khushi8112
84cdd0daf0 fix: describe stale exchange rate settings 2026-08-13 14:51:31 +05:30
Jatin3128
4a8342496e fix: keep Currency and Price List section open for foreign currency (#58074)
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.
2026-08-13 12:48:43 +05:30
Khushi Rawat
7212e6d989 Merge pull request #58096 from khushi8112/refactor/item-prices-embedded-list
refactor: use EmbeddedList for item prices
2026-08-13 12:33:51 +05:30
Khushi Rawat
c973de27d4 Merge pull request #58078 from khushi8112/fix/ar-sales-person-user-permission
fix: apply Sales Person user permissions in Accounts Receivable
2026-08-13 11:45:28 +05:30
Khushi Rawat
1fd653b1d3 Merge pull request #57563 from khushi8112/refactor-depreciation-entry
refactor: split make_depreciation_entry into public and internal helpers
2026-08-13 11:38:53 +05:30
khushi8112
51cefeffef refactor: use EmbeddedList for item prices
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.
2026-08-13 11:34:12 +05:30
Mohsin Akhtar
e72aabcdb8 Fix/item description in the item price list (#58084) 2026-08-13 09:41:30 +05:30
rohitwaghchaure
c6d08979d3 feat: capacity aware scheduling for production plan (#58094)
* 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
2026-08-12 12:40:03 +00:00
Mihir Kandoi
43de54b907 fix(work order): exclude corrective transfers from item-level transferred qty (#58091) 2026-08-12 11:11:12 +00:00
pandiyan
9dd37d5f32 fix(accounts): disallow reversing a reverse journal entry
check read permission on the source entry before the guards run, so the reversal relationship is not disclosed to a user who cannot read it.
2026-08-12 15:59:16 +05:30
pandiyan
80422d2108 test(accounts): cover reversal of a reverse journal entry
also assert that a user without read access on the entry gets a permission error instead of the reversal relationship.
2026-08-12 15:59:16 +05:30
Mihir Kandoi
e5a0b5f61e fix(manufacturing): refine corrective job cards (#58079) 2026-08-12 10:13:29 +00:00
Mihir Kandoi
68a30f1568 fix(work order): don't count corrective job card transfers as transferred qty (#58080) 2026-08-12 15:35:55 +05:30
Mihir Kandoi
994703c2f3 Merge pull request #58073 from mihir-kandoi/codex/mrp-detailed-chart-timescale
fix(manufacturing): show full date range in MRP chart
2026-08-12 14:54:15 +05:30
Khushi Rawat
ca1b03cd46 Merge pull request #57820 from aerele/fix-asset-capitalization-grouped-row-valuation
fix(assets): split fifo/lifo rate across grouped stock item rows
2026-08-12 14:34:12 +05:30
khushi8112
8b09ba429e fix: apply Sales Person user permissions in Accounts Receivable
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.
2026-08-12 14:26:10 +05:30
Krishna Pramod Shirsath
18dff27f65 Merge pull request #57995 from krishna-254/fix/available-batch-report-company-filter
fix: filter available batch report by company
2026-08-12 13:19:16 +05:30
Mihir Kandoi
5ad085887d fix(manufacturing): keep MRP chart dates distinct 2026-08-12 12:28:13 +05:30
Krishna Pramod Shirsath
c34e6fba32 Merge branch 'develop' into fix/available-batch-report-company-filter 2026-08-12 12:23:04 +05:30
Krishna Shirsath
484beb9fba fix: filter available batch report by company 2026-08-12 12:18:25 +05:30
Mihir Kandoi
592924cc0d test(manufacturing): cover MRP chart date range 2026-08-12 11:39:53 +05:30
MochaMind
83e33b343c fix: sync translations from crowdin (#58070) 2026-08-11 23:28:18 +00:00
Diptanil Saha
c52552ecfa fix(coa_importer): added server-side validations for importing chart of accounts (#58065) 2026-08-12 03:32:19 +05:30
Vishnu Priya Baskaran
89e2c3c3e5 fix: allow non-admin roles to import chart of accounts (#57454) 2026-08-11 23:26:58 +05:30
Vishnu Priya Baskaran
39403a5ef3 refactor:fix item property updates in POS and transactions, and add styling (#57189)
Co-authored-by: Afsal Syed <afsalsyed12@gmail.com>
2026-08-11 22:50:28 +05:30
soulxone
3c17a604be fix(Material Requirements Planning Report): detailed-view chart timescale
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
2026-08-11 11:55:49 -05:00
Vishnu Priya Baskaran
8802000ba2 fix(consolidated cash flow): correct totals and labels in section foo… (#57336) 2026-08-11 21:58:53 +05:30
Raffael Meyer
e591c7e8b6 fix: mirror rounding adjustment on distributed_discount_amount (#58047) 2026-08-11 18:26:26 +02:00
Mihir Kandoi
a7e5e1209b Merge pull request #58051 from mihir-kandoi/codex/fix-sales-order-item-reset
fix(selling): reset stale item details on item change
2026-08-11 21:05:04 +05:30
Mihir Kandoi
e8c890a844 fix(selling): preserve explicit UOM during item selection 2026-08-11 20:54:04 +05:30
Mihir Kandoi
e6966627b8 Merge pull request #58050 from mihir-kandoi/codex/fix-bom-creator-item-selection
fix(manufacturing): preserve BOM Creator item details
2026-08-11 20:53:31 +05:30
Mihir Kandoi
009961edc7 fix(selling): reset stale item details on item change 2026-08-11 20:41:47 +05:30
Mihir Kandoi
439eb250f5 fix(manufacturing): preserve BOM Creator item details 2026-08-11 20:40:30 +05:30
Mihir Kandoi
7e4e9860ef Merge pull request #58045 from mihir-kandoi/codex/repair-underbilled-pr-status
fix(stock): repair existing underbilled purchase receipts
2026-08-11 19:52:08 +05:30
Mihir Kandoi
620465220b test: cover repair of underbilled receipts from mixed billing 2026-08-11 19:21:38 +05:30
Mihir Kandoi
e99ae3796c fix(stock): repair existing underbilled purchase receipts
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.
2026-08-11 19:21:38 +05:30
Mihir Kandoi
4108be4637 Merge pull request #58036 from mihir-kandoi/codex/fix-disabled-warehouse-account-resolution
fix(stock): validate warehouse accounts when used
2026-08-11 18:54:39 +05:30
Mihir Kandoi
ce847b6c29 Merge pull request #58044 from mihir-kandoi/fix-po-billing-qty-sync-develop
fix: keep PO billed qty in sync when allocating amount to receipts
2026-08-11 18:53:46 +05:30
Mihir Kandoi
031e7c0eb7 test: cover mixed direct and PO-invoice billing across receipts 2026-08-11 18:40:35 +05:30
Mihir Kandoi
7803998fce fix: keep PO billed qty in sync when allocating amount to receipts
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.
2026-08-11 18:39:52 +05:30
Mihir Kandoi
abb23fffdc Merge pull request #58021 from harisansari008/fix-po-invoice-billing-distribution-develop
fix: distribute PO-invoice billed amount across receipts without duplication
2026-08-11 18:36:09 +05:30
Mihir Kandoi
b54a4f6285 test(stock): cover named validation error and same-transaction parent inheritance 2026-08-11 18:26:10 +05:30
Mihir Kandoi
624e97f6c1 chore(stock): drop redundant supplier warehouse comment 2026-08-11 18:26:09 +05:30
Mihir Kandoi
d2b5c6ad40 fix(stock): validate new warehouse inventory account after naming
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.
2026-08-11 18:26:08 +05:30
Mihir Kandoi
0171b07023 Merge pull request #58032 from aerele/perf/batch-bin-lookups
perf: batch per-row bin lookups in sales invoice and delivery note
2026-08-11 18:20:50 +05:30
Mihir Kandoi
13883a00b0 fix(stock): validate warehouse accounts when used 2026-08-11 18:16:47 +05:30
Mihir Kandoi
d34519f536 test: cover repair patch exclusion for invoice-created receipts 2026-08-11 18:16:38 +05:30
Mihir Kandoi
ace4230f97 fix: skip PO items with invoice-created receipts in billing repair patch
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.
2026-08-11 18:16:38 +05:30
Mihir Kandoi
d8d0ea01c4 Merge pull request #58041 from mihir-kandoi/fix-auto-reserve-packed-items
fix: set auto reserve stock flag before packing list generation
2026-08-11 18:07:39 +05:30
Mihir Kandoi
b879c491b0 fix: set auto reserve stock flag before packing list generation
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.
2026-08-11 17:53:47 +05:30
pandiyan
6bf00d3c8b perf: batch per-row bin lookups in sales invoice and delivery note
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.
2026-08-11 17:52:27 +05:30
Mihir Kandoi
27da94610c Merge pull request #58037 from aerele/feat/multi-select-item-warehouse-filters
feat(stock): multi select item and warehouse filters in warehouse wise item balance
2026-08-11 17:04:41 +05:30
Mihir Kandoi
db78dd1ef3 Merge pull request #58033 from mihir-kandoi/gate-selling-toggle-setters
perf: rewrite selling settings toggle setters only on change
2026-08-11 16:42:36 +05:30
Sudharsanan11
eb4bb2cbce test(stock): cover multi select item and warehouse filters in warehouse wise item balance
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.
2026-08-11 16:34:12 +05:30
Sudharsanan11
3c92a9e853 feat(stock): multi select item and warehouse filters in warehouse wise item balance
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.
2026-08-11 16:34:12 +05:30
Mihir Kandoi
6c5aca0b37 Merge pull request #57983 from Jatin3128/fix/disabled-party-validation-opportunity-rfq
fix: block disabled/frozen party on Opportunity and Request for Quotation
2026-08-11 16:31:52 +05:30
Mihir Kandoi
f3a8f02c5f Merge pull request #58031 from mihir-kandoi/gate-naming-property-setters
perf: rewrite customer and supplier naming setters only on change
2026-08-11 16:27:39 +05:30
Mihir Kandoi
27f0ffdde5 Merge pull request #58018 from mihir-kandoi/codex/reduce-test-setup-overhead
perf(tests): reduce repeated setup work
2026-08-11 16:27:15 +05:30
Mihir Kandoi
c9f74e21a0 test: cover selling settings toggle setter gating 2026-08-11 16:22:21 +05:30
Mihir Kandoi
136e00d513 perf: rewrite selling settings toggle setters only on change
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.
2026-08-11 16:21:21 +05:30
Mihir Kandoi
8b7e04eae1 fix(stock): repair duplicated purchase receipt billing 2026-08-11 16:16:53 +05:30
harisansari008
5e33a3c0bf fix: distribute PO-invoice billed amount across receipts without duplication
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>
2026-08-11 16:16:33 +05:30
Diptanil Saha
2f1548c0f8 chore: disable mergifyio queue comment (#58030) 2026-08-11 16:11:31 +05:30
Mihir Kandoi
909e7b1457 test: cover customer and supplier naming setter gating 2026-08-11 16:10:15 +05:30
Mihir Kandoi
94320a9928 perf: rewrite customer and supplier naming setters only on change
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.
2026-08-11 16:09:08 +05:30
Mihir Kandoi
d6e2d7105f perf(tests): run sales partner summary reports on shared fixtures
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.
2026-08-11 16:03:51 +05:30
Mihir Kandoi
7aa6a07e74 test(stock): cover item metadata rewrite gating 2026-08-11 16:03:50 +05:30
Mihir Kandoi
a8e2351169 perf(stock): skip item metadata rewrite when settings unchanged
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.
2026-08-11 16:03:49 +05:30
Jatin3128
58491723e7 feat(accounts): add Bank Charges account for Payment Entry deductions (#57840)
* 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>
2026-08-11 15:47:51 +05:30
Smit Vora
4c5d54096f Merge pull request #57965 from ljain112/set-missing-values
fix: run set_missing_values before creating Purchase Order from MRP report
2026-08-11 15:46:28 +05:30
Sudharsanan Ashok
b5a3815a64 fix(selling): read overdue amount from payment ledger, not gl tags (#57786) 2026-08-11 15:37:17 +05:30
Jatin3128
6b35c51ff1 refactor: move RFQ supplier disabled filter to link_filters
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).
2026-08-11 15:35:28 +05:30
Mihir Kandoi
e258921681 Merge pull request #58020 from mihir-kandoi/codex/fix-driver-supplier-address
fix(setup): fetch driver address by supplier link
2026-08-11 15:17:24 +05:30
Mihir Kandoi
3ffb888d26 fix(setup): fetch driver address by supplier link 2026-08-11 15:09:48 +05:30
Mihir Kandoi
575f34e7c6 Merge pull request #58016 from mihir-kandoi/fix-job-card-operation-row-id
fix: preserve original operation idx in manually created Job Cards
2026-08-11 14:19:25 +05:30
Mihir Kandoi
cb7201cc18 fix: preserve original operation idx in manually created Job Cards
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
2026-08-11 14:05:21 +05:30
Mihir Kandoi
3c000929e2 Merge pull request #58005 from mihir-kandoi/restore-manufactured-qty-check
fix: reinstate mandatory manufactured qty check for manufacture entries
2026-08-11 14:00:05 +05:30
Mihir Kandoi
1543ee9350 test: manufacture entry blocked without manufactured qty 2026-08-11 13:47:26 +05:30
Mihir Kandoi
b6ca708d9f fix: reinstate mandatory manufactured qty check for manufacture entries 2026-08-11 13:46:40 +05:30
Mihir Kandoi
3379907564 Merge pull request #58004 from mihir-kandoi/restore-duplicate-entry-check
fix: reinstate duplicate entry check for manufacture entries
2026-08-11 13:45:07 +05:30
Mihir Kandoi
148d6aad5f Merge pull request #58009 from mihir-kandoi/require-transfer-before-job-card-start
fix: require material transfer before job card start and completion
2026-08-11 13:31:05 +05:30
Mihir Kandoi
e5344cc07e test: overproduction allowance permits a further manufacture entry 2026-08-11 13:27:02 +05:30
Mihir Kandoi
492ee05727 fix: honor overproduction allowance in duplicate entry check
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.
2026-08-11 13:27:02 +05:30
Mihir Kandoi
1665873fc9 test: duplicate manufacture entry against a work order
A second Manufacture stock entry saved while existing entries already
cover the full work order qty must raise DuplicateEntryForWorkOrderError.
2026-08-11 13:27:02 +05:30
Mihir Kandoi
22fa520500 fix: reinstate duplicate entry check for manufacture entries
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.
2026-08-11 13:27:02 +05:30
Mihir Kandoi
fee2672bf9 Merge pull request #58000 from mihir-kandoi/restore-operations-completed-check
fix: reinstate operations completed check for manufacture entries
2026-08-11 13:25:04 +05:30
Mihir Kandoi
704adeb96e Merge pull request #58003 from mihir-kandoi/codex/fix-sales-invoice-return-redelivery
fix(selling): bill re-delivered sales order quantities
2026-08-11 13:20:01 +05:30
Mihir Kandoi
e9533495fc fix: keep job card actions visible for corrective job cards
Corrective job cards regenerate required items but are exempt from the
transfer gate on the server; mirror that exemption in materials_ready.
2026-08-11 13:18:55 +05:30
Mihir Kandoi
c95705dc64 test: job card start and completion blocked until material transfer 2026-08-11 13:14:38 +05:30
Mihir Kandoi
808b2e2984 fix: require material transfer before job card start and completion
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.
2026-08-11 13:08:33 +05:30
Mihir Kandoi
af234a2d16 Merge pull request #58006 from mihir-kandoi/subcontracting-fg-items-list
fix: pass finished goods as list to subcontracting BOM lookup
2026-08-11 12:57:55 +05:30
Mihir Kandoi
3704d1513b Merge pull request #57963 from frappe/codex/fix-bom-explorer-quantities
fix(manufacturing): correct nested BOM Explorer quantities
2026-08-11 12:56:08 +05:30
Mihir Kandoi
40fc6c8b8a fix: pass finished goods as list to subcontracting BOM lookup
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
2026-08-11 12:41:47 +05:30
Mihir Kandoi
9f108e4b75 fix(selling): clamp pending qty to qty field precision 2026-08-11 12:38:26 +05:30
Mihir Kandoi
dd23cf40e1 perf(selling): batch billed quantity lookup 2026-08-11 12:27:22 +05:30
Mihir Kandoi
26a05044c0 fix: link an active non-corrective job card in the operations error
Cancelled, amended, and corrective job cards do not contribute to the
operation's completed qty, so the error should not direct users there.
2026-08-11 12:16:46 +05:30
Mihir Kandoi
114ba42850 fix(selling): bill re-delivered sales order quantities 2026-08-11 12:16:00 +05:30
Mihir Kandoi
fe8bedef93 Merge pull request #57997 from kaulith/fix/bom-item-query-barcode-search
fix: keep item code searchable when a barcode matches the same text
2026-08-11 12:12:07 +05:30
Mihir Kandoi
2d022ffb66 test: manufacture entry blocked until operations are completed 2026-08-11 12:11:19 +05:30
Mihir Kandoi
401eb30963 fix: reinstate operations completed check for manufacture entries
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.
2026-08-11 12:09:33 +05:30
Mihir Kandoi
70f236ae2e Merge pull request #57972 from aerele/fix/workstation-time-in-mins
refactor: drop redundant time_in_mins assignment in workstation complete_job
2026-08-11 12:08:43 +05:30
Kaushal Shriwas
23024d1ea9 test(manufacturing): cover BOM item search when item code collides with a barcode 2026-08-11 11:58:36 +05:30
Kaushal Shriwas
bf5d506637 fix(manufacturing): keep item code searchable when a barcode matches the same text 2026-08-11 11:58:33 +05:30
Mihir Kandoi
3cffeb68e3 refactor: drop redundant time_in_mins assignment in complete_job 2026-08-11 11:56:13 +05:30
Nabin Hait
e4f9c664a8 refactor: stock write-path cleanups (SLE/Bin chokepoint groundwork) (#57980)
* 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.
2026-08-11 11:35:24 +05:30
Jatin3128
8c0a945417 fix: scope Opportunity party validation to Customer only
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.
2026-08-11 11:02:29 +05:30
Diptanil Saha
b55c6d16c9 refactor(queries): using frappe.get_query in get_filtered_child_rows (#57991) 2026-08-10 19:33:29 +00:00
Diptanil Saha
02118e7ba0 fix: preserve custom title on new JV (#57987)
Co-authored-by: rehanrehman389 <rehanrehman389@gmail.com>
2026-08-10 21:27:44 +05:30
Jatin3128
4bf65ffc1d fix: block disabled/frozen suppliers on Request for Quotation
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.
2026-08-10 18:02:34 +05:30
Jatin3128
90937ce6d9 fix: block disabled/frozen customers on Opportunity
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.
2026-08-10 18:02:16 +05:30
pandiyan
422a9161dd fix: convert hours to minutes in workstation complete_job
`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.
2026-08-10 16:02:39 +05:30
Nikhil Kothari
5b2952aa23 chore: update deps in banking app (#57971) 2026-08-10 09:45:11 +00:00
Diptanil Saha
ef1d83c298 fix: field validation and perm checks on get_stock_reservation_entries_for_voucher (#57968) 2026-08-10 09:18:53 +00:00
ljain112
94d363851f fix: run set_missing_values before creating Purchase Order from MRP report 2026-08-10 14:11:41 +05:30
Mihir Kandoi
bf9744e145 fix(manufacturing): correct nested BOM quantities 2026-08-10 13:25:38 +05:30
Shllokkk
a25decfa50 fix: skip incoming rate calc when serial no qty is zero (#57427) 2026-08-10 11:54:49 +05:30
Pandiyan P
399ff463cc fix: re-check future sle before queuing repost on submit (#57664)
* 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>
2026-08-10 11:53:49 +05:30
Khushi Rawat
d618ad24f1 Merge pull request #57822 from Shllokkk/asset-repair-downtime-recalc
fix: keep asset repair downtime in sync with entered dates
2026-08-10 11:50:22 +05:30
Diptanil Saha
e342bf765e fix: escape customer_details on lead creation from appointment (#57947) 2026-08-09 19:14:33 +00:00
Shllokkk
5486fbff03 Merge pull request #57927 from Shllokkk/sales-register-ledger-pos-paid
fix: reflect in-invoice receivable credits in Sales Register ledger view
2026-08-09 23:39:10 +05:30
Shllokkk
45a9294476 test: cover POS-paid invoice in Sales Register ledger view 2026-08-09 20:22:14 +05:30
Shllokkk
40c356d166 fix: reflect in-invoice receivable settlements in Sales Register ledger view 2026-08-09 20:22:12 +05:30
Mihir Kandoi
fe09f45ca7 Merge pull request #57895 from mihir-kandoi/fix-track-sfg-finished-good
fix: track semi finished goods production flow
2026-08-09 20:11:05 +05:30
Mihir Kandoi
0bb322f4d1 Merge pull request #57560 from nishkagosalia/gh-56632
fix: handling negative grand total
2026-08-09 16:45:13 +05:30
Mihir Kandoi
bf13284137 Merge pull request #57917 from mihir-kandoi/fix/italy-customer-name-fields
fix(regional): rename Italy's duplicate Customer name fields
2026-08-09 16:44:54 +05:30
Mihir Kandoi
4089f138f2 fix(controllers): correct negative rate settings link 2026-08-09 16:29:28 +05:30
Mihir Kandoi
110d0a38a6 fix(regional): rename Italy's duplicate Customer name fields
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.
2026-08-09 16:29:17 +05:30
Mihir Kandoi
7d1d5a1309 Merge pull request #57918 from mihir-kandoi/fix/qi-reference-notify-update
fix: sync open reference forms after Quality Inspection submit
2026-08-09 16:04:39 +05:30
Mihir Kandoi
e8a6884d5e test: doc_update published for reference on Quality Inspection submit 2026-08-09 15:53:37 +05:30
Mihir Kandoi
647452c95b fix: sync open reference forms after Quality Inspection updates them
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.
2026-08-09 15:45:07 +05:30
MochaMind
40d59f9a15 chore: update POT file (#57915) 2026-08-09 10:02:29 +00:00
Mihir Kandoi
4a31772f3d Merge pull request #57914 from aerele/fix/outward-bundle-sign
fix: negate stock value difference for outward transfer bundles
2026-08-09 14:37:06 +05:30
pandiyan
eff29565ae fix: negate stock value difference for outward transfer bundles
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.
2026-08-09 13:20:48 +05:30
Mihir Kandoi
691c341edd Merge pull request #57911 from aerele/fix/sales-team-allocated-percentage-float
fix: tolerate floating-point drift in sales team allocated percentage
2026-08-09 12:28:56 +05:30
pandiyan
4afba94d1c test: sales team allocation totalling 100 in floating point
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.
2026-08-09 11:25:24 +05:30
pandiyan
f7b2775829 fix: tolerate floating-point drift in sales team allocated percentage
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.
2026-08-09 11:25:12 +05:30
pandiyan
24de81f9fa test: work order process loss for semi finished goods
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.
2026-08-09 09:45:49 +05:30
pandiyan
0eb61c9fac fix: roll up process loss to the work order for semi finished goods
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.
2026-08-09 09:45:49 +05:30
Mihir Kandoi
529a003ed1 Merge pull request #57832 from nishkagosalia/st-75115
fix: validate webform for project
2026-08-09 09:41:24 +05:30
Nishka Gosalia
53d3ba7a78 Merge pull request #57903 from SuhasBharadwajK/develop
fix: condition check with empty object for falsy case
2026-08-09 00:18:36 +05:30
Diptanil Saha
13d3101f12 fix: set restrict_globals=True in frappe.render_template (#57899) 2026-08-08 23:40:58 +05:30
nishkagosalia
126966d1db fix: validate webform for project 2026-08-08 23:20:14 +05:30
rohitwaghchaure
ca0a5cb67c feat: sync serial no status from stock ledger in Stock Qty vs Serial No Count report (#57863)
* 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
2026-08-08 13:48:13 +00:00
Mihir Kandoi
1478e2a4cb test: transfer qty exemption only applies when material transfer is skipped 2026-08-08 17:37:42 +05:30
Mihir Kandoi
1deae664ce fix: keep the transfer qty check for legacy semi FG cards without an FG item
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.
2026-08-08 17:37:42 +05:30
Mihir Kandoi
8f0617c834 test: partial entries consume exactly the job card's material requirement 2026-08-08 17:37:42 +05:30
Mihir Kandoi
0428cddf5b fix: scale generated raw materials to the manufacture entry's production share
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.
2026-08-08 17:37:42 +05:30
Mihir Kandoi
424a1dfa87 test: update-after-submit save keeps the manufacture entry intact 2026-08-08 17:37:42 +05:30
Mihir Kandoi
bed957fa67 fix: skip the pending production check on update-after-submit saves
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.
2026-08-08 17:37:42 +05:30
Mihir Kandoi
db99657c47 test: target warehouse stays optional for semi FG work orders 2026-08-08 17:37:42 +05:30
Mihir Kandoi
9df527bf3f fix: keep Target Warehouse optional for work orders tracking semi finished goods
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.
2026-08-08 17:37:42 +05:30
Mihir Kandoi
eb7537c8df test: partial manufacture entry then finishing the job card 2026-08-08 17:37:42 +05:30
Mihir Kandoi
b8dd886cd4 fix: generate the next manufacture entry net of booked process loss
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.
2026-08-08 17:37:03 +05:30
Mihir Kandoi
7157e4357b test: stale manufacture draft cannot over-produce without an operation BOM 2026-08-08 17:37:03 +05:30
Mihir Kandoi
94cd27ce5d fix: cap a manufacture entry at the job card's pending production
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.
2026-08-08 17:30:55 +05:30
Mihir Kandoi
9ef386dfd2 test: operation BOM materials expand on single-pass submit, final FG must match the BOM item 2026-08-08 17:29:59 +05:30
Mihir Kandoi
1e2e87daac fix: derive operation FG items before material expansion, keep the final one the BOM's item
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.
2026-08-08 17:29:59 +05:30
Mihir Kandoi
0aec62a8dd test: raw material dialog adds a row for its operation despite duplicates 2026-08-08 17:29:59 +05:30
Mihir Kandoi
24f1f3dea8 fix: add raw material to its operation even when another operation uses the 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.
2026-08-08 17:29:59 +05:30
Mihir Kandoi
5e0f056284 test: manufacture entry keeps process loss scoped to its own operation 2026-08-08 17:29:59 +05:30
Mihir Kandoi
1b335973b7 fix: scope manufacture entry process loss to its own job card
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.

Fixes frappe/erpnext#57892
2026-08-08 17:29:59 +05:30
Mihir Kandoi
335dbdaca4 test: previous operation shortfall from process loss gets the right message 2026-08-08 17:29:59 +05:30
Mihir Kandoi
1e22695eae fix: stop asking for a manufacturing entry when process loss explains the shortfall
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.
2026-08-08 17:29:59 +05:30
Mihir Kandoi
f61f6523b9 test: WIP warehouse required for work orders tracking semi finished goods 2026-08-08 17:29:59 +05:30
Mihir Kandoi
198eb60df7 fix: require WIP warehouse for work orders tracking semi finished goods
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.
2026-08-08 17:29:59 +05:30
Mihir Kandoi
4b3904c6d7 test: semi FG job card is exempt from the legacy transfer qty check 2026-08-08 17:29:59 +05:30
Mihir Kandoi
6c8f0b9b56 fix: don't demand raw material transfer for semi FG job cards on submit
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.
2026-08-08 17:29:59 +05:30
Mihir Kandoi
aed7c70b1c test: BOM tracking semi finished goods rejects operations without FG item 2026-08-08 17:29:59 +05:30
Mihir Kandoi
3497a6a6bf fix: require FG / Semi FG Item on operations when tracking semi finished goods
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.
2026-08-08 17:29:59 +05:30
Mihir Kandoi
11a902eb5f Merge pull request #57887 from mihir-kandoi/fix-repost-ma-return-sibling-rate
fix: repost read stale sibling SLE rate for moving average returns
2026-08-08 12:02:24 +05:30
Mihir Kandoi
b3f97cd389 fix: incorrect entry detection in Stock Ledger Invariant Check (#57886) 2026-08-08 05:31:21 +00:00
Mihir Kandoi
97d41867b5 fix: zero-rate repost fallback could still read sibling SLE
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.
2026-08-08 11:00:47 +05:30
Mihir Kandoi
1a47c61b67 test: repost of multi-line moving average return is idempotent
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.
2026-08-08 10:53:04 +05:30
Mihir Kandoi
b589a4d335 fix: repost read stale sibling SLE rate for moving average returns
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.
2026-08-08 10:51:13 +05:30
Pandiyan P
55fe269046 fix: allow selecting a warehouse for new items in the update items dialog (#57876) 2026-08-07 17:32:29 +00:00
Mihir Kandoi
5822cef1d9 feat(manufacturing): prompt for qty when creating material request from work order (#57855) 2026-08-07 17:25:51 +00:00
Mihir Kandoi
5e372df533 Merge pull request #57883 from mihir-kandoi/fix-pp-min-order-qty-round-up
fix: order smallest purchase UOM qty that meets min order qty
2026-08-07 22:49:55 +05:30
Mihir Kandoi
1ca06024cb test: marginal min order qty overage raises a toast 2026-08-07 22:36:02 +05:30
Mihir Kandoi
1aa6f8d1a7 feat: warn when ordered qty exceeds min order qty only by UOM rounding
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.
2026-08-07 22:36:02 +05:30
Mihir Kandoi
1b2090820a Merge pull request #57873 from mihir-kandoi/fix-conversion-factor-precision
fix: declare precision 9 on all conversion_factor fields
2026-08-07 22:20:57 +05:30
Mihir Kandoi
e4b66dc731 test: min order qty conversion takes the grid ceiling
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.
2026-08-07 22:20:38 +05:30
Mihir Kandoi
ee8eb18daf fix: order smallest purchase UOM qty that meets min order qty
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.
2026-08-07 22:20:30 +05:30
Mihir Kandoi
594ff24dae Merge pull request #57879 from mihir-kandoi/remove-dead-mr-po-mapper
refactor: remove unused make_purchase_order_based_on_supplier
2026-08-07 21:03:07 +05:30
Shllokkk
67f4b7a483 Merge pull request #57880 from Shllokkk/guard-clear-reconciliation-patch
fix: guard reconciliation table deletes when tables are missing
2026-08-07 20:03:19 +05:30
Shllokkk
8a2b2a2b68 fix: guard reconciliation table deletes when tables are missing 2026-08-07 19:33:45 +05:30
Mihir Kandoi
8b2946ca6b refactor: remove unused make_purchase_order_based_on_supplier
Its only caller, Purchase Order's get_items_from_open_material_requests,
was deleted in 91e9867fb1 (refactor: Cleanup buying module forms). The
old dotted path was already broken by the move to mapper.py, so no
external caller can be using it either.
2026-08-07 18:42:00 +05:30
Mihir Kandoi
ca5a673409 fix: round computed conversion factors to field precision
The inverse (1 / value) and intermediate-UOM branches of
get_uom_conv_factor returned raw float quotients like
0.4535922921968971, bypassing the precision the docfields now declare.
Same for the client-side back-calculation from an edited stock qty.
Round both to the UOM Conversion Factor value precision.
2026-08-07 17:47:50 +05:30
Mihir Kandoi
69a35a12cb fix: declare precision 9 on all conversion_factor fields
The Float control parses values with the field precision, falling back
to the global float precision when the docfield declares none
(frappe ControlFloat.parse / get_precision). On a site with float
precision 2, a fetched UOM factor of 0.453592292 was written back to
the model as 0.45, silently corrupting every derived quantity by 0.8
percent. A ratio must not inherit display precision meant for
quantities, so declare the same precision 9 the UOM Conversion Factor
master already uses on every transaction-level conversion_factor
field.
2026-08-07 17:31:32 +05:30
Mihir Kandoi
b3300c20a3 Merge pull request #57866 from krishna-254/fix/product-bundle-item-search
fix: optimize product bundle item search
2026-08-07 16:02:51 +05:30
Mihir Kandoi
4e9b069c20 Merge pull request #57861 from mihir-kandoi/fix-uom-integer-check-dust
fix: UOM whole number check truncated instead of rounding
2026-08-07 14:03:26 +05:30
Mihir Kandoi
e6a6458ebe test: UOM whole number check tolerates conversion dust 2026-08-07 13:49:00 +05:30
Mihir Kandoi
a464a6e4a1 fix: UOM whole number check truncated instead of rounding
cint truncates, so a stock_qty of 1999.9998 (dust from qty times
conversion factor) compared as abs(1999 - 2000.0) > epsilon and was
rejected as fractional even though it rounds to a whole number at
field precision, with the error confusingly printing the rounded
value: 'Quantity (2000.0) cannot be a fraction'. Round to field
precision first, then require the result to be a whole number.
Dust above an integer already passed; this fixes the asymmetry for
dust below.
2026-08-07 13:48:38 +05:30
Mihir Kandoi
a219f890df Merge pull request #57859 from mihir-kandoi/fix-po-min-order-qty-precision
fix: compare ordered qty to min order qty at stock_qty precision
2026-08-07 13:46:55 +05:30
Krishna Shirsath
b3867f1428 fix: optimize product bundle item search 2026-08-07 13:44:57 +05:30
Mihir Kandoi
c652f47931 test: min order qty check tolerates UOM conversion dust 2026-08-07 12:51:40 +05:30
Nishka Gosalia
d9e048ece7 Merge pull request #57845 from nishkagosalia/settings-mapping-cleanup
chore: restructure exported files of doctype settings
2026-08-07 12:46:43 +05:30
Mihir Kandoi
98b7407949 fix: compare ordered qty to min order qty at stock_qty precision
stock_qty is stored as raw qty * conversion_factor, so a UOM-converted
order for exactly the minimum (e.g. LB to Kg) produces values like
1999.999999131832 vs a min_order_qty of 2000 and blocks the Purchase
Order. Round both sides to the stock_qty field precision before
comparing, and show the rounded qty in the error message.
2026-08-07 12:44:28 +05:30
Mihir Kandoi
3f8b263014 Merge pull request #57854 from mihir-kandoi/fix/work-order-create-button-group
fix(UX): group work order actions under the Create menu
2026-08-07 04:22:33 +05:30
MochaMind
523d0e4312 fix: sync translations from crowdin (#57842) 2026-08-06 20:03:30 +02:00
Mihir Kandoi
780ab3ba3e fix(UX): group work order actions under the Create menu
Pick List, Material Request, Material Consumption and Additional
Material Transfer were spread across two standalone buttons and a
separate Make menu. Put them all under a single Create menu, and rename
Create Pick List to Pick List since the menu already says Create.
custom_make_buttons is updated to the new label so the connections
shortcut still finds the button.
2026-08-06 23:02:05 +05:30
Mihir Kandoi
e1815cb5a9 Merge pull request #57851 from mihir-kandoi/fix-production-plan-mr-qty-precision
fix: round Production Plan mr_items quantity to field precision
2026-08-06 21:11:10 +05:30
Mihir Kandoi
75145cc72c test: remaining purchase qty is rounded to field precision
Covers the _add_remaining_purchase_request path: partial stock in
another warehouse is allocated as a transfer and the residual purchase
qty goes through the second rounding site.
2026-08-06 20:59:04 +05:30
Mihir Kandoi
f5157bf3c4 test: mr_items quantity is rounded to field precision 2026-08-06 20:49:42 +05:30
Mihir Kandoi
ffc515f046 fix: round production plan mr_items quantity to field precision
The stock-UOM qty is rounded in _accumulate_so_items, but the purchase
UOM conversion divided it by the conversion factor without re-rounding,
storing values like 5738748.300863984 in mr_items.quantity. The raw
value flowed into Material Request qty and the raw materials CSV, and
make_material_request compares quantity to requested_qty with exact
float equality, so any rounding downstream left dust quantities.
2026-08-06 20:48:42 +05:30
Mihir Kandoi
44260b469f refactor: remove unreachable UOM conversion in production plan
The division by conversion_factor in _adjust_required_qty_for_uom sits
directly after frappe.throw inside the same block, so it can never run.
It has been dead since commit 2a8cd05b44 (#27278) re-indented it into
the throw branch; the actual purchase-UOM conversion happens in
_material_request_item_row via _mr_purchase_conversion_factor.
2026-08-06 20:48:19 +05:30
Jatin3128
96a6db7387 feat(accounts): split exchange gain and exchange loss accounts (#57839)
* feat(accounts): split exchange gain and exchange loss accounts

Add optional Exchange Gain Account and Exchange Loss Account fields on
Company. When set, realized FX gain/loss from settling an invoice in a
foreign currency (via Payment Entry, Payment Reconciliation, or a
Journal-Entry-based advance) books to the matching account instead of
the single Exchange Gain/Loss account. Either field left blank falls
back to the existing Exchange Gain/Loss account, so companies that
don't configure the new fields are unaffected.

New companies get "Exchange Gain" and "Exchange Loss" accounts
auto-created in their chart of accounts and auto-assigned to the new
fields, same as the existing Exchange Gain/Loss account provisioning.

The Payment Reconciliation tool's per-allocation "Difference Account"
override in its reconcile dialog continues to work as before; the
split accounts only change the computed default shown there.

* test(account_balance): account for new Exchange Gain account in income report

The new auto-provisioned Exchange Gain account under Indirect Income
now shows up in the Income root type report for _Test Company 2.

---------

Co-authored-by: test <test@test.com>
2026-08-06 17:36:43 +05:30
Suhas Bharadwaj
e0b9351d49 fix: condition check with empty object for falsy case 2026-08-06 16:38:46 +05:30
nishkagosalia
8096766d71 chore: restructure exported files of doctype settings 2026-08-06 16:28:18 +05:30
rohitwaghchaure
a49fcfe888 fix: purchase return of batchwise valuation batch valued at original receipt rate instead of batch avg rate (#57835)
* fix: use current batch avg rate for outward returns of batchwise valuation batches

* fix: honor zero batch average and avoid duplicate batch classification query
2026-08-06 15:21:44 +05:30
khushi8112
757eaf5fcf refactor(ui): hide the primary address and contact section
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.
2026-08-06 15:16:00 +05:30
Diptanil Saha
12359c36bc Merge pull request #57825 from diptanilsaha/st/72599/arpbmd/pinv
refactor(accounts)!: rework Purchase Invoice hold actions and enforce them on Journal Entry
2026-08-06 13:11:51 +05:30
diptanilsaha
1a8d438b21 test(journal_entry): added test cases for blocked purchase invoices 2026-08-06 12:31:00 +05:30
diptanilsaha
cbafa16fbc fix(journal_entry): validate blocked purchase invoices 2026-08-06 11:56:58 +05:30
diptanilsaha
6c33ede45c refactor(purchase_invoice): expose invoice hold actions as document methods 2026-08-06 11:18:46 +05:30
rohitwaghchaure
742e0e16cf fix: XSS through unescaped work order name in shop floor view (#57826) 2026-08-06 08:53:46 +05:30
Shllokkk
8269f8a362 test: assert asset repair downtime recalculates on date change 2026-08-06 00:33:26 +05:30
Shllokkk
4406bb9068 fix: keep asset repair downtime in sync with entered dates 2026-08-06 00:33:24 +05:30
ervishnucs
2cbc5b89d6 test(assets): cover grouped stock item rows splitting FIFO rate 2026-08-05 19:01:10 +05:30
ervishnucs
a05ec49062 fix(assets): split FIFO/LIFO rate across grouped stock item rows 2026-08-05 19:00:51 +05:30
MochaMind
1ffcfeb11b fix: sync translations from crowdin (#57743) 2026-08-05 15:13:07 +02:00
Henil Maru
8e8ef1602e fix(sales-invoice): respect Customize Form hidden setting on Update Stock (#57818)
set_dynamic_labels() unconditionally forced update_stock's hidden
property based only on is_debit_note/has_subcontracted, overwriting
whatever Customize Form had set on every refresh. OR it with the
field's original (property-setter-driven) hidden value instead.
2026-08-05 18:00:26 +05:30
rohitwaghchaure
d71fc3b774 feat: validate stock value and stock closing entry before period closing (#57811)
* feat: validate stock value and stock closing entry before period closing

* fix: do not accept scoped stock closing entries as period closing prerequisite

* feat: seed batch valuation from stock closing balance and freeze closed-period stock
2026-08-05 15:45:16 +05:30
Mihir Kandoi
8aadffa73c Merge pull request #57810 from aerele/fix/blanket-order-mapped-naming-series
fix: do not copy Blanket Order naming series to the mapped order
2026-08-05 14:12:11 +05:30
pandiyan
7620553418 test: assert mapped order keeps its own naming series 2026-08-05 13:03:10 +05:30
pandiyan
fe7128f02f fix: do not copy blanket order naming series to the mapped order
get_mapped_doc copies every same-named field that is not no_copy, so the
Sales Order / Purchase Order / Quotation created from a Blanket Order
inherited MFG-BLR-.YYYY.- and was named MFG-BLR-2026-00003 instead of
SAL-ORD-2026-00001.

exclude naming_series from the mapping, same as job card does when it
maps to a Purchase Order.
2026-08-05 13:03:10 +05:30
Mihir Kandoi
1f42eb1a3c Merge pull request #57793 from aerele/fix/blanket-order-zero-qty-validation
fix: validate Blanket Order item quantity is greater than zero
2026-08-05 12:11:48 +05:30
rohitwaghchaure
d3a8c329dd fix: incorrect batch-wise valuation rate for entries with same posting datetime (#57803)
fix: incorrect batch-wise valuation rate for entries with same posting datetime (#57794)

* fix: incorrect batch-wise valuation rate for entries with same posting datetime

The tie-breaker in get_batch_no_ledgers compared the bundle's creation
against the SLE's creation. These are different timelines - a bundle can
be created (drafted) much before its SLE (created at submission). For
entries sharing a posting datetime (backdated / amended vouchers), this
mis-ordered the entries against the ledger's replay order (SLE creation),
causing double counting or omission of batch qty / value and runaway
outgoing rates that no repost could heal.

Now the tie is broken using the creation of the bundle's own SLE (same
timeline on both sides). When the valuation runs through the bundle
before its SLE exists, the entry is by definition last in its timestamp
group, so all same-timestamp entries already in the ledger precede it.



* test: batch-wise valuation ordering for same posting datetime entries

Covers both tie-breaking branches of get_batch_no_ledgers:
- submission (pre-insertion) branch: same-timestamp inward at a different
  rate plus a multi-row outward voucher (same item and warehouse), at
  submission and after a backdated repost
- existing-SLE branch: a bundle created after its sibling's SLE, the
  ordering must follow the SLE creation and not the bundle creation

Both tests fail with the previous parent.creation < sle.creation
tie-breaker and pass with the fix.



---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 11:06:30 +05:30
Diptanil Saha
4d511a1521 chore(CODEOWNERS): add @nikkothari22 for banking (#57801) 2026-08-04 19:53:54 +00:00
Diptanil Saha
fc8e2e8627 Merge pull request #57734 from aerele/fix/payment-reconciliation-supplier-gain-loss-sign
fix(payment reconciliation): correct supplier gain/loss posting
2026-08-04 23:55:38 +05:30
Sudharsanan Ashok
0dbe410414 fix(stock): handle multi-item opening balance in Stock Ledger report (#57591)
* fix(stock): handle multi-item opening balance in Stock

* test(stock): add unit test for multi-item Stock Ledger report

---------

Co-authored-by: Afsal Syed <afsalsyed12@gmail.com>
2026-08-04 22:14:16 +05:30
R-Jayaraman
d80b0f67cc test: add regression test for zero quantity Blanket Order 2026-08-04 19:06:25 +05:30
R-Jayaraman
e897c4d82d fix: validate Blanket Order item quantity is greater than zero 2026-08-04 19:05:51 +05:30
Shllokkk
b9dafafeee Merge pull request #57790 from Shllokkk/warehouse-account-override-value-comparison
test: child warehouse account override in stock vs account value comparison
2026-08-04 17:30:28 +05:30
Shllokkk
ef7a3cb4c8 test: child warehouse account override excluded in stock vs account value comparison 2026-08-04 17:03:25 +05:30
Mihir Kandoi
afdb951eb4 Merge pull request #57757 from aerele/fix/opportunity-qty-validation
fix(opportunity): add validation for positive item quantities
2026-08-04 16:52:55 +05:30
R-Jayaraman
69de8f2d62 chore: use flt() in qty check 2026-08-04 16:38:49 +05:30
Mihir Kandoi
8b710ddbf1 Merge pull request #57772 from aerele/fix/party-dashboard-doctype-permission
fix(accounts): skip party dashboard without invoice permission
2026-08-04 16:33:30 +05:30
Jatin3128
0f428ed854 fix(subscription): don't reactivate a cancelled subscription (#57774)
* fix(subscription): don't reactivate a cancelled subscription

set_subscription_status() unconditionally set status to Active once
there was no outstanding invoice, even if the subscription had been
intentionally cancelled. Paying off an invoice issued before
cancellation (directly, or via the Payment Entry -> refresh hook)
flipped a Cancelled subscription back to Active while cancelation_date
stayed set.

process()'s cancel_at_period_end check compared posting_date against
getdate(self.end_date), and getdate(None) returns today, so an empty
end_date was silently treated as "cancel now" on every scheduler run.
Combined with the reactivation bug, this let a cancelled subscription
toggle Cancelled -> Active on each run and generate another invoice at
the next period boundary.

Fixes #57761

* fix(test): compare normalized dates in subscription cancellation test

cancelation_date read straight off an unsaved in-memory doc is a
string from nowdate(), but the same field comes back as a
datetime.date after reload(). Wrap both sides in getdate() so the
comparison isn't type-sensitive.
2026-08-04 15:59:44 +05:30
Mihir Kandoi
3dd01e5120 Merge pull request #57777 from mihir-kandoi/fix/bom-creator-toolbar-actions
fix(manufacturing): reach the whole configurator from tree toolbar actions
2026-08-04 15:38:28 +05:30
Mihir Kandoi
097ce0f348 fix(manufacturing): reach the whole configurator from tree toolbar actions
The toolbar handlers were copied onto view.events as unbound functions, so
`this` inside them was that object literal rather than the BOMConfigurator.
They worked only because the literal also carried `frm`, and broke as soon as
a handler called a method the literal did not list: get_item_code, added when
the tree started keying nodes on the row name, threw
"this.get_item_code is not a function" and killed Add Raw Material, Add Sub
Assembly and Convert to Sub Assembly.

Assign the instance instead of a hand-maintained whitelist. Every method is
reachable, `this.frm` keeps working, and no future method can be forgotten.

Fixes #57773
2026-08-04 15:34:59 +05:30
Diptanil Saha
9ce32fc1da fix(sales_invoice): enable repost on account change of account in payments (#57775) 2026-08-04 09:52:28 +00:00
Sudharsanan11
ed78dd37be fix(accounts): skip party dashboard without invoice permission 2026-08-04 14:10:55 +05:30
Jatin3128
c1717d8689 fix: keep source rate on re-fetch when maintain same rate is enabled (#57479)
* fix: keep source rate on re-fetch when maintain same rate is enabled

With "maintain same rate" on, re-fetching item details on a row mapped from a
source document (e.g. a Purchase Order) pulled the latest Item Price, giving a
rate the document can never be saved with. Skip the price list fetch for such
rows and keep the source rate.

Fixes frappe/erpnext#57436

* fix: keep source rate on bulk apply_price_list when maintain same rate is on

The single-row re-fetch guard skipped the bulk apply_price_list path, so
changing the price list, party, or conversion rate on a mapped transaction
re-fetched current Item Prices and overwrote the mapped rates, breaking the
maintain-same-rate check on save.

Guard apply_price_list_on_item with the same source-row lookup, and resolve the
parent doctype via ctx.parenttype since the bulk path carries the child doctype
in ctx.doctype.

* fix: preserve full source pricing on rate-locked rows

Restoring only price_list_rate on a mapped row dropped any manual discount or
margin, so re-running pricing produced a rate that differed from the source and
still failed the maintain-same-rate check on save.

Copy the source row's whole pricing block (rate, discount, margin) and skip
pricing rules for locked rows, in both get_item_details and the bulk
apply_price_list path.

* fix: pass child_docname in server bulk price apply so the rate lock is reachable

The server-side _apply_price_list builds its item ctx from as_dict(), which omits
the child_docname key the desk (JS) callers add, so the maintain-same-rate lock in
apply_price_list could not match rows in that path. Pass child_docname for
consistency with the desk callers.

* test: cover maintain-same-rate preservation on re-fetch of a discounted row

Reproduces the end-to-end symptom: a mapped Purchase Receipt row with a source
discount (rate != price_list_rate) keeps its rate after a re-fetch, so the
document saves under maintain-same-rate. Covers percentage and amount discounts
via process_item_selection, the server recompute the desk mirrors.

* fix: read the locked rate from the persisted source row

get_rate_locked_source_row returned the mutable target row, so an unsaved rate or
discount edit on a mapped row was preserved on re-fetch instead of the source
pricing, and the document still failed maintain-same-rate on save. Read the
pricing straight from the linked source row in the database, and cover the
edit-then-refresh case with a test.

* fix: permission-check the source row before returning its rate

The rate lock reads the linked source row with a direct db.get_value, which
bypasses permissions on a whitelisted endpoint. Only return the source pricing
when the caller can read the source document, so a crafted request cannot
disclose another document's rate. Covered by a test.

* fix: import make_purchase_receipt from its current mapper module

make_purchase_receipt moved from purchase_order.py to
purchase_order/mapper.py in a develop refactor pulled in by this
branch's merge commit. Two tests added afterwards still imported it
from the old path, failing CI with an ImportError.

* fix: Simplify source retrieval logic in get_item_details

Removed permission check for source parent in get_item_details.py.

* Revert "fix: Simplify source retrieval logic in get_item_details"

This reverts commit 58863805bd.
2026-08-04 13:28:57 +05:30
Khushi Rawat
8cefaa355c Merge pull request #57539 from harisansari008/fix/dunning-timestamp-mismatch-multi-installment
fix: prevent TimestampMismatchError resolving Dunning with multiple overdue installments
2026-08-04 13:02:44 +05:30
Diptanil Saha
4255df05cd Merge pull request #57742 from diptanilsaha/fix/escape_data_in_templates
fix: escape data in multiple templates
2026-08-04 12:11:43 +05:30
ruthra kumar
bca3889f97 Merge pull request #57719 from krishna-254/fix/reversal-journal-entry-custom-remark
Fix/reversal journal entry custom remark
2026-08-04 10:59:46 +05:30
Krishna Shirsath
5e0e9ba668 fix: allow custom remark on reversal journal entry 2026-08-03 17:16:09 +05:30
Mihir Kandoi
e8b16a4228 Merge pull request #57754 from mihir-kandoi/fix/isolate-disabled-attribute-test-fixtures
test: isolate the disabled item attribute test fixtures
2026-08-03 17:01:24 +05:30
Mihir Kandoi
ae6749470f test(stock): isolate the disabled attribute fixtures
The test disabled the shared `Test Size` Item Attribute. On version-15
`FrappeTestCase` rolls back once per class instead of once per test, so the
flag stayed visible for the rest of `TestItem` and broke the seven tests that
build a variant from that attribute.

Build a dedicated attribute and template instead. Nothing the test writes is
reachable from another test, on either branch, so no cleanup is needed.
2026-08-03 16:43:07 +05:30
R-Jayaraman
c47cc37441 fix(opportunity): add validation for positive item quantities 2026-08-03 16:41:43 +05:30
Mihir Kandoi
8db8c6a83d fix(stock): allocate secondary item cost from the consumption entry (#57738)
* fix(stock): allocate secondary item cost from the consumption entry

A secondary item's rate is its BOM share of the cost of the consumed
rows. With Get RM Cost From Consumption Entry enabled the consumption
happens in a separate document, so the Manufacture entry carries no
consumed rows and that cost is zero. The share evaluated to zero, and the
row fell through to the item's own valuation rate.

Only the finished good substituted the consumption entry's cost. Against
a consumption entry of 1000 and a BOM allocating 75% to the finished good
and 25% to scrap, the finished good took its 750 while the scrap took an
unrelated valuation of 100, booking 850 for 1000 consumed.

Derive the allocation base once and use it for both sides.

* test(stock): cover secondary allocation against a consumption entry

A consumption entry of 1000 splits into 750 and 250 by the BOM's shares.
2026-08-03 10:58:23 +00:00
Mihir Kandoi
aef69202ea Merge pull request #57747 from mihir-kandoi/fix/disabled-attribute-blocks-variant-edits
fix: disabled item attribute blocks unrelated edits to existing variants
2026-08-03 16:16:24 +05:30
diptanilsaha
3df596d84e fix: escape data on stock_summary_template 2026-08-03 16:16:07 +05:30
diptanilsaha
10c439ff01 fix: escape item name and item title on item_selector 2026-08-03 16:16:07 +05:30
diptanilsaha
2d387002d9 fix(workstation): escape data on get_workstations 2026-08-03 16:15:59 +05:30
Mihir Kandoi
7d901ed92c fix(stock): treat a 0% BOM cost allocation as no cost (#57736)
* fix(stock): treat a 0% BOM cost allocation as no cost

A BOM splits its raw material cost between the finished good and its
secondary items, and validate_total_cost_allocation holds the two to
100%. An allocation of 0% therefore means the finished good takes
everything and the secondary item carries no cost.

The code read it as no allocation at all. A cost_allocation_per of 0 is
falsy, so the branch was skipped, the row kept a rate of zero, and the
fallback below handed it the item's own valuation rate. Producing 1000 of
raw material into a finished good at 100% and scrap at 0% booked 1000 to
the finished good and another 100 to the scrap.

Apply the BOM's share whatever it is, and mark the rate as derived so the
valuation fallback leaves a deliberate zero alone. rate_derived_from_consumption
becomes has_derived_rate, since it now guards more than the consumption case.

* test(stock): cover a secondary item allocated 0% of the cost

The finished good takes the full 1000 and the scrap row is worth nothing.
2026-08-03 10:42:24 +00:00
Mihir Kandoi
8d5326196e test(stock): cover editing a variant whose attribute is disabled
Assert that a variant saves after its attribute is disabled when the edit
leaves the attribute rows alone, and that changing an attribute value still
throws.
2026-08-03 16:00:27 +05:30
Mihir Kandoi
25cd793617 fix(stock): validate only the variant attributes that changed
Disabling an Item Attribute writes `disabled = 1` into every Item Variant
Attribute row, including the rows on the template. `validate_variant` runs
on every save and walks the whole attribute table, so any later save of an
existing variant re-checked its untouched rows against the now-disabled
template row and threw. `update_variants` hit the same wall, which made a
single template save fail once an attribute was disabled.

The flag exists to keep an attribute out of new variants, not to freeze the
variants that already use it. item.js only reads it to drop the attribute
from the variant creation dialog.

Skip rows that are unchanged since the last save. New and edited rows are
still checked, so a disabled attribute cannot be added to an existing
variant, and the same guard covers the sibling checks for attributes and
values that the template no longer offers.
2026-08-03 15:57:35 +05:30
Mihir Kandoi
7886bd2cab fix(stock): stop treating a Repack secondary item as a finished good (#57735)
* fix(stock): stop treating a Repack secondary item as a finished good

mark_finished_and_secondary_items flagged every incoming Repack row as a
finished item, secondary rows included. Two things followed from that.
The row never reached the secondary-item branch in _set_incoming_item_rate,
so its own cost_allocation_per was never applied, and the BOM's
finished-good percentage was applied to every incoming row rather than
to the finished good alone.

Value was destroyed as a result. Repacking 1000 of raw material under a
BOM that allocates 75% to the finished good and 25% to scrap booked 500
to the finished good and 250 to the scrap: 750 in against 1000 out.

Leave secondary rows unflagged so each side takes the share the BOM
declares.

* test(stock): cover cost allocation for a Repack secondary item

A BOM allocating 75% to the finished good and 25% to scrap must split
1000 of raw material into 750 and 250, leaving no difference.
2026-08-03 10:19:09 +00:00
Mihir Kandoi
be3df759f1 Merge pull request #57732 from mihir-kandoi/fix/secondary-item-without-bom-balances-manufacture
fix(stock): cost a BOM-less secondary item out of the finished good
2026-08-03 15:22:16 +05:30
Mihir Kandoi
7f47361ebd test(stock): cover a secondary item added without a BOM
A Manufacture entry with a raw material worth 1000, a finished good and a
Scrap row typed in the UI must value the scrap at its own rate and take
that value out of the finished good, leaving no difference.
2026-08-03 15:01:08 +05:30
Mihir Kandoi
5e81cd1540 fix(stock): cost a BOM-less secondary item out of the finished good
The legacy scrap checkbox deducted the scrap row's value from the
finished good, so a Manufacture entry balanced. Its replacement, the
Secondary Item Type dropdown, only balances when the row carries a BOM
Secondary Item link, because the cost allocation percentage lives there.
A row typed as Scrap in the UI has no such link, so its value was added
on top of a finished good that already absorbed the whole raw material
cost, and the entry closed with a non-zero difference.

Treat a secondary row with no BOM link the way the legacy scrap item was
treated: deduct its value from the finished good.

The finished good's rate is derived from the other incoming rows, so
those rows must be rated first. Previously the finished good was rated
in row order, ahead of the secondary rows, and picked up their amounts
only on a later validate pass. Rate the finished goods last so a single
pass is correct.
2026-08-03 15:00:41 +05:30
Mihir Kandoi
33ea059018 Merge pull request #57737 from mihir-kandoi/fix/secondary-item-must-not-waive-quality-inspection
fix(stock): stop a secondary item type from waiving quality inspection
2026-08-03 14:59:13 +05:30
Nikhil Kothari
abc3da6b97 fix(banking): fetch company list from DB instead of boot (#57731)
* fix(banking): fetch company list from DB instead of boot

* fix: show error banner for company list fail fetch
2026-08-03 14:11:36 +05:30
Mihir Kandoi
fec5dae639 test(stock): cover inspection on a receipt row typed as a secondary item
The row must be blocked with or without the type set.
2026-08-03 13:55:27 +05:30
Mihir Kandoi
dfec7bd5c7 fix(stock): stop a secondary item type from waiving quality inspection
The inspection skip for secondary rows applied to every purpose, and in
validate_inspection it skipped the row even when the item itself mandated
inspection. Secondary Item Type is only meaningful on the purposes that
produce secondary items, but nothing clears it elsewhere, since
mark_finished_and_secondary_items runs for Manufacture and Repack alone.

A Material Receipt of an item marked Inspection Required Before Purchase
is blocked without an inspection. Setting Secondary Item Type on the row
submitted it clean.

Limit the exemption to the purposes that produce secondary items, and to
other doctypes such as Subcontracting Receipt, which carry the field with
its intended meaning. The client-side mirror is kept in sync.
2026-08-03 13:55:26 +05:30
Sudharsanan11
5442ad4c48 test(payment reconciliation): cover supplier exchange gain posting 2026-08-03 13:40:45 +05:30
Sudharsanan11
4688ddd217 fix(payment reconciliation): correct supplier gain/loss posting 2026-08-03 13:40:45 +05:30
Raffael Meyer
915eef0355 ci: hide eo.po from translation PR review details (#57200) 2026-08-03 07:38:11 +00:00
Mihir Kandoi
947f1b148c Merge pull request #57647 from aerele/fix/sales-zero-qty-return-validation
fix(sales): reject sales returns where every item has zero quantity
2026-08-03 12:56:02 +05:30
R-Jayaraman
732c884633 test(sales): add coverage for zero-qty return rejection
Greptile flagged that the sales-side zero-qty-return fix had no dedicated
test proving the behavior - the existing suite happened to pass, but
nothing specifically asserted that an all-zero return is rejected while
a normal negative-qty return still succeeds.

Adds two tests covering the doctypes that rely entirely on this check
(no other guard covers them for a non-stock-effect return):
- Delivery Note return with qty 0 -> rejected
- Sales Invoice return with qty 0 (no update_stock) -> rejected

POS Invoice is not covered separately here since it always runs with
update_stock=1, which is already guarded by the pre-existing
validate_zero_qty_for_return_invoices_with_stock check regardless of
this fix.
2026-08-03 12:41:38 +05:30
R-Jayaraman
a3e9d13da3 fix(sales): reject sales returns where every item has zero quantity
validate_returned_items() set items_returned=True whenever a row matched
a valid item from the original document, even if its qty was 0. This let
a Sales Invoice, Delivery Note, or POS Invoice return be submitted with
every line at qty=0 - a no-op document with no stock or financial effect
that still consumed a document number and linked back to the original
transaction.

Scoped to the Sales side only: items_returned now flips to True for
Sales Invoice/Delivery Note/POS Invoice only when qty (or received_qty)
is actually negative, so an all-zero sales return correctly hits the
existing "At least one item should be entered with negative quantity"
check. Purchase Invoice, Purchase Receipt, and Subcontracting Receipt
are unchanged.
2026-08-03 12:40:57 +05:30
Mihir Kandoi
b4d73cd934 Merge pull request #57725 from aerele/fix/stock-over-delivery-role-scope
fix(stock): scope over deliver/receive role check to delivery and receipt overflow
2026-08-03 12:39:08 +05:30
Afsal Syed
99630f40eb test(stock): prevent settings leakage in purchase order tests 2026-08-03 12:27:33 +05:30
Afsal Syed
0b271e24b6 test(stock): add test cases verifying stock over delivery role does not bypass order allowance 2026-08-03 12:27:33 +05:30
Afsal Syed
248873034d fix(stock): scope over deliver/receive role check to delivery and receipt overflow 2026-08-03 12:27:33 +05:30
Afsal Syed
446ec6030a fix(stock): validate over delivery/receipt allowance in stock settings 2026-08-03 12:27:33 +05:30
Mihir Kandoi
c020de5a69 Merge pull request #57097 from aerele/fix/qi-reading-number-format
fix(stock): read quality inspection readings in the user's number format
2026-08-03 12:26:18 +05:30
Mihir Kandoi
f03c1311cd fix(controllers): source trend report labels from the master (#57724)
* fix(controllers): source trend report labels from the master

item_name, customer_name, territory and supplier_name are stored on each
transaction and editable, so they are not functionally dependent on the grouped
key and historical documents can hold different values for the same item,
customer or supplier. Aggregating them with Max() is a text sort, and MariaDB
folds case while PostgreSQL orders by byte value, so the two engines can label
the same row differently.

Read each from its master instead. Those values ARE dependent on the grouped
key, so they can be grouped without splitting rows and agree on both engines by
construction rather than by an assumption about the data. Supplier needed no new
join -- the Supplier master was already joined as t3 for supplier_group.

A Quotation's party_name is a dynamic link to either a Customer or a Lead, so
neither master can be joined without dropping the other; there the values come
from correlated subqueries over both, keyed only on the grouped party_name.

Row counts and every numeric total are unchanged. What changes is that a
renamed record now shows its current name rather than whichever historical
snapshot happened to sort highest.

* test(selling): assert which label the trends report returns

The existing tests assert the customer stays one row but never which territory
or name comes back, so a divergence between engines passes unnoticed. Asserts
both equal the Customer master's values while an order stores a different
territory.

* fix(controllers): resolve a Quotation's party label through quotation_to

party_name is a dynamic link, so looking it up in Customer and Lead alone was
wrong twice over: a Quotation raised against a Prospect or a CRM Deal got a
blank label, and when a Lead shared its name with a Customer the Customer-first
lookup returned the wrong record's name and territory.

Resolve through the quotation_to discriminator instead, mirroring
Quotation.set_customer_name -- Customer, Lead (company_name falling back to
lead_name), Prospect, and CRM Deal. The CRM Deal branch is emitted only when its
table exists, since it ships with the CRM app.

quotation_to joins the GROUP BY as well: two parties of different types can
share a name, and merging them into one row was never right.

* style(controllers): name the quotation CASE branches

semgrep's string-concat-in-list flags adjacent string literals inside a list,
since that shape is usually a missing comma rather than deliberate. Bind each
branch to a name first so the concatenation is unambiguous.
2026-08-03 06:52:25 +00:00
Mihir Kandoi
8154c45bf0 fix(accounts): key the payment ledger CTEs on account, not Max(account) (#57720)
* fix(accounts): key the payment ledger CTEs on account, not Max(account)

QueryPaymentLedger builds two CTEs -- voucher amount and outstanding -- and
joins them on account among other columns. Both sides selected Max(account)
while grouping without it, so the join key was an aggregate over two different
row sets. A voucher posting ledger entries against two party accounts could
have the two sides pick different accounts, the join miss, and the outstanding
come back NULL. Max() over text is a sort, so which account wins is also
collation-dependent, and the engines sort text differently.

Group both CTEs by account instead. That makes the join key a real column and
scopes each Sum() to a single account -- so amount_in_account_currency is no
longer summed across accounts that may not share a currency. Row shape only
changes for a voucher that genuinely spans two party accounts for one party,
where today's single row is already an arbitrary pick over mixed currencies.

cost_center and remarks stay descriptive but genuinely vary per entry, and were
aggregated independently, so they could be stitched together from different
entries into a row that was never posted. They now come off one real entry,
picked by Min(name) -- Payment Ledger Entry declares no autoname rule, so
frappe names it by hash, and those are lower-case, which keeps the pick free of
the collation divergence.

* test(accounts): cover payment ledger metadata coherence

A Journal Entry posting two receivable lines for one customer with different
cost centers and remarks. Whatever row the ledger returns, its cost center and
remarks must be a pair that was actually posted. Guards the fixture itself, so
it cannot pass by posting only one distinct pair.

* test(accounts): cover the account-keyed payment ledger aggregation

The coherence test posts both party lines to one account, so it exercises the
representative-row metadata but not the account-keyed grouping or the CTE join.
Adds a Journal Entry posting to two receivable accounts for one customer and
asserts each account comes back as its own row, with its own amount and a
non-null outstanding.
2026-08-03 06:46:04 +00:00
Mihir Kandoi
00d17ca5db test(stock): cover reading number formats end to end
Set the number format on the session user rather than on System Settings: the
code reads the user default, which shadows the global one, so these tests never
exercised the path they were written for. Restoring it in a finally also keeps
a failed assertion from leaving the whole suite in another locale.

Add a table test over every format in NUMBER_FORMAT_MAP, covering the grouped
values and the three formats parse_float used to read as 0, and restore the
formula-based coverage for non-numeric readings.
2026-08-03 12:14:19 +05:30
Mihir Kandoi
5b5f354090 fix(stock): accept every number a reading can be written as
parse_float and is_valid_number each re-derived the number grammar, so the
validator accepted strings flt() cannot parse: str.isdigit() lets superscripts
through and lstrip("+-") lets repeated signs through, both then silently scored
as 0. One parse_reading() returning None when float() refuses the value makes
acceptance and conversion true by construction.

The grammar was also wrong for several formats. Where the group separator is
not a dot, a dot-decimal reading such as 1.15 parsed correctly before and is
accepted again. #,### and #.### report no decimal separator at all, which
rejected every fractional reading outright and, for #.###, reread a stored
1.500 as 1500.0; they now fall back to a dot and give up the grouping that
would collide with it.

Only readings that change are checked, so an inspection entered by a user in
one locale stays saveable and submittable by a user in another, and manual
inspection rows keep the free text they were never parsed for.

NumberFormat replaces get_number_format_info, which frappe drops in v16.
2026-08-03 12:14:19 +05:30
Sudharsanan11
b1f188146e test(stock): cover quality inspection readings in every number format
covers the reported case, a 1,15 reading in the space grouped "# ###,##"
format, which was read as 115 and rejected. also covers the dot grouped
comma format, and asserts that a reading written with the wrong separator,
or one that is not a number at all, is now rejected with an error rather
than read as a different value.
2026-08-03 12:14:19 +05:30
Sudharsanan11
3752be809f test(stock): drop non numeric reading from formula based quality inspection
a numeric reading of "random text" was read as 0 and pulled the mean from
0.6 down to 0.4, which the test then asserted as accepted. such a reading
is now rejected outright, and the test is about formula evaluation, so drop
the row. its assertions are unchanged.
2026-08-03 12:14:19 +05:30
Sudharsanan11
e74c0a3cdb fix(stock): read quality inspection readings in the user's number format
readings are Data fields, so they are parsed server side. parse_float only
swapped the separators for "#.###,##", so in the space grouped "# ###,##"
(polish) a reading of 1,15 was read as 115, fell outside the acceptance
range and silently rejected the inspection. strip whatever the group
separator is and normalise whatever the decimal separator is instead.

it also read the global number format, while the desk formats numbers with
the user's own. a user whose locale differs from the site therefore typed
readings in a format the server did not parse them with. read the user
default, which falls back to the global one.

a reading that is not a valid number in that format is now rejected with an
error instead of being read as a different number.
2026-08-03 12:14:19 +05:30
Mihir Kandoi
d74add35d4 fix(accounts): take POS summary warehouse and cost centre from one item line (#57723)
* fix(accounts): take POS summary warehouse and cost centre from one item line

Both describe an item line, not the invoice, and an invoice can carry several.
They were aggregated independently per invoice, so the report could show a
warehouse from one line beside a cost centre from another -- a pair that was
never posted.

The warehouse then becomes an outer grouping key, so the pick is not merely a
label: it decides how rows are partitioned across owner/date and therefore what
each row totals. Max() over text is a sort, and MariaDB folds case while
PostgreSQL orders by byte value, so the two engines can partition differently.

Take both off one real line instead, and the mode of payment off one real
payment line for the same reason. Sales Invoice Item is hash-named and Sales
Invoice Payment declares no autoname rule, so frappe hash-names it too -- which
keeps Min(name) free of the collation divergence that sorting text has.

* test(accounts): cover POS summary warehouse/cost-centre coherence

The existing tests post a single item line, so they cannot see this. Adds an
invoice with two lines whose warehouse and cost centre are deliberately
crossed: the higher warehouse sits on the line with the lower cost centre, so
an independently aggregated pair belongs to neither line.

* fix(accounts): pick the POS summary representative by idx, not by hash

Min(name) selected whichever child row happened to have the lowest hash, which
is arbitrary and turns on something unrelated to the data. Min(idx) selects the
first line the user actually entered: an integer, so the pick is free of
collation, and it is meaningful rather than incidental.

The join moves to (parent, idx), which is unique per parent.
2026-08-03 06:42:28 +00:00
Mihir Kandoi
bf869c3426 Merge pull request #57718 from frappe/pg-audit/search-ranking-case-insensitive
fix(controllers): restore case-insensitive employee/lead/bom search ranking
2026-08-03 12:08:33 +05:30
Mihir Kandoi
d44ed5357d Merge pull request #57645 from aerele/fix/purchase-zero-qty-return-validation
fix(purchase): reject purchase returns where every item has zero quan…
2026-08-03 11:45:36 +05:30
Mihir Kandoi
1968f06cc8 test(controllers): assert lead search ranking, not just result count
The existing query tests assert only how many rows come back, so an ordering
divergence between engines passes unnoticed. Adds a case-adversarial pair: a
lead whose name starts with the search term in upper case, and one containing
it in lower case later on. The first must rank ahead of the second.
2026-08-03 11:38:58 +05:30
Mihir Kandoi
a7a14c82da fix(controllers): restore case-insensitive employee/lead/bom search ranking
Reapplies #56330, which was reverted by #56389 with no recorded reason and has
been absent since 23 June.

The search filter uses .like(), which frappe renders as ILIKE on PostgreSQL, so
a candidate matches regardless of case. The ranking used a bare Locate(), which
frappe renders as strpos() -- case-sensitive there. A candidate can therefore
pass the filter, score no match in the ranking, fall back to 99999 and sort
last, while MariaDB's case-insensitive LOCATE ranks it first.

Same query, different order on the two engines, and a different result page
once page_len cuts between them.

Lower() both operands, matching the item, project, user and pick list handlers
in this same file, which were already correct.
2026-08-03 11:38:57 +05:30
Mihir Kandoi
282712eec2 Merge pull request #57716 from frappe/pg-audit/collation-representative-lines
fix(postgres): read BOM/SO/MR line columns off one line, not Max()
2026-08-03 09:41:36 +05:30
Mihir Kandoi
c8adf9937b refactor(postgres): memoise representative lines with frappe's request_cache
Three helpers each managed their own dictionary on frappe.local, duplicating
cache lifecycle and key handling. @request_cache does the same thing centrally
and is cleared with the request, so the copies cannot drift apart.

Behaviour is unchanged: the decorator keys on the call arguments, which are the
same tuple each hand-rolled key was built from.
2026-08-03 01:03:11 +05:30
Mihir Kandoi
100d0ee784 test(manufacturing): cover the BOM representative-line pick
A BOM listing one item on two lines, with descriptions and source warehouses
that differ. The second line's description sorts above the first on either
engine, so an aggregated value would win; the row must instead carry the first
line's description together with that same line's warehouse.
2026-08-03 00:56:56 +05:30
Mihir Kandoi
414e6560af fix(postgres): read BOM/SO/MR line columns off one line, not Max()
Max() over a text column is a sort, and the engines sort text differently:
MariaDB's utf8mb4 collations fold case, the CI PostgreSQL orders by byte
value. MAX('abc','ABD') is 'ABD' on MariaDB and 'abc' on PostgreSQL --
confirmed on CI in the probe attached to #56241.

The parity effort wrapped many descriptive columns in Max() on the reasoning
that it returns the value MySQL picked arbitrarily. Where the column is
functionally dependent on the group key that holds and the wrap is a genuine
no-op. Where it genuinely varies -- description, item_name, uom and their
warehouses all describe a LINE, not the item -- it does not: MySQL picked a
row, not a maximum, and the sort now diverges between engines. Aggregating
each column separately can also pair one line's description with another's
warehouse, or a uom with the wrong conversion factor.

Take those columns from a single real line instead, the first by idx.

Only groups built from more than one line need it. Each query now also selects
Count(<line>.name).distinct(), and the representative pass returns immediately
when no group has more than one line -- in that case Max() of a single value
is already exact and collation cannot apply. A BOM with no repeated item
therefore issues no extra query at all, which matters because the explosion
and sub-assembly resolution recurse per sub-BOM. Genuine repeats are memoised
per request.

Sites covered: BOM explosion and sub-item queries, sub-assembly raw materials,
get_bom_items_as_dict, BOM Stock Analysis (both queries), Requested Items to
Order and Receive, Pending SO Items for Purchase Request, and Job Card
secondary items.
2026-08-03 00:56:55 +05:30
Mihir Kandoi
a30f3dde0f Merge pull request #57711 from frappe/pg-audit/purchase-register-add-deduct
fix(accounts): net Add and Deduct tax rows in Purchase Register
2026-08-03 00:52:24 +05:30
Mihir Kandoi
03183fc4d9 fix(stock): take disassembly source columns from one posted line (#57710)
* fix(stock): take disassembly source columns from one posted line

get_items_from_manufacture_stock_entry collapses a work order's Manufacture
entries to one row per item and wrapped fifteen Stock Entry Detail columns in
independent Max() to satisfy Postgres' strict GROUP BY. Those columns describe
a line, not an item, and three sets have to stay together:

  uom only means something beside its conversion_factor
  batch_no and serial_no only beside their warehouse
  is_finished_item decides whether the row is the output or an input

Aggregated separately they can be drawn from different lines. Two Manufacture
entries consuming the same item in Nos and in Box return ("Nos", 5) -- a pair
that was never posted, and one that does not describe the summed quantity.

Keep the sums (and the qty-weighted basic_rate) in the aggregate, and read the
descriptive columns off a single real line: the earliest by Stock Entry
creation then idx. That is what MariaDB returned in practice, it is
deterministic, and it is identical on both engines. Same representative-row
shape already used by BOM Stock Analysis and the sub-assembly queries.

* test(manufacturing): cover disassembly source-row coherence

Two Manufacture entries consume the same raw material in different UOMs, so
the max uom and the max conversion factor come from different lines. Asserts
the returned pair is one that was actually posted. Fails on the previous
per-column Max() with ('Nos', 5.0) not found in {('Nos', 1.0), ('Box', 5.0)}.

* fix(stock): aggregate disassembly quantities in stock UOM
2026-08-03 00:52:24 +05:30
Mihir Kandoi
d5ea0d1f6f fix(manufacturing): stop BOM Stock Analysis inflating both its sums (#57709)
* fix(manufacturing): stop BOM Stock Analysis inflating both its sums

get_bom_data left-joined Bin on item_code alone and then summed over the
result. Bin holds one row per warehouse and BOM Item one row per line, so the
join is a cross product and each SUM counts the other side's rows:

  Sum(qty_consumed_per_unit) x (number of warehouses holding the item)
  Sum(bin.actual_qty)        x (number of BOM lines carrying the item)

A component on two BOM lines, stocked in two warehouses, reported a per-unit
requirement of 10 instead of 5 and available stock of 20 instead of 10 --
wrong on both engines, and wrong in the single-line case too as soon as the
item sits in more than one warehouse.

Aggregate Bin to one row per item_code before joining, so neither sum can see
the other's duplicates. The warehouse filter moves into that subquery; it
previously sat in the outer WHERE against a left-joined column, which
silently made the join inner, so the join is now made inner explicitly when a
warehouse is given to keep items with no bin there excluded as before.

* test(manufacturing): cover the BOM Stock Analysis bin-join cross product

Component on two BOM lines, stocked in two warehouses: the join yields four
rows, so both sums are doubled. Asserts qty_per_unit is the sum of the lines'
own per-unit quantities and actual_qty the real total across warehouses.
Fails on the previous single-query form with 10.0 != 5.0.
2026-08-03 00:52:23 +05:30
Mihir Kandoi
5eabd176f5 fix(manufacturing): compute BOM item amount per line (#57708)
* fix(manufacturing): compute BOM item amount per line

get_bom_items_as_dict groups BOM lines by item_code, so a BOM listing the
same item on more than one line collapses to a single row. The amount column
multiplied the summed quantity by a single line's rate:

    Sum(stock_qty / bom.quantity) * Max(rate) * qty

That is neither line's amount and not their total. The Max() was added to
satisfy Postgres' strict GROUP BY on the assumption that rate is constant per
item, but rate is editable per line.

Fold the rate into the sum so every line contributes its own:

    Sum(stock_qty / bom.quantity * rate) * qty

Identical for the common single-line item, correct for duplicates, and valid
on both engines. Same class as the fix applied to budget_controller's
requested amount.

* test(manufacturing): cover BOM item amount across duplicate lines

A BOM listing the same item twice, once in the stock UOM and once in a UOM
with a conversion factor, gives the two lines different rates (rate is the
valuation rate scaled by the conversion factor). The two lines collapse into
one row in get_bom_items_as_dict, so amount must be the sum of each line's
own qty x rate.

Guards the fixture with an assertion that the two rates actually differ,
so the test cannot pass vacuously. Fails on the previous
Sum(stock_qty) * Max(rate) expression.

* fix(manufacturing): use matching UOM quantity for BOM amount
2026-08-03 00:52:23 +05:30
Mihir Kandoi
8f227ad80e Merge pull request #57715 from frappe/pg-audit/collation-taxonomy
docs(postgres): catalog the collation-dependent text pick
2026-08-02 22:20:20 +05:30
Mihir Kandoi
2c6208ad00 Merge pull request #57714 from frappe/ci/patch-test-stacked-pr-base
ci(patch): fall back to develop when the base ref has no frappe branch
2026-08-02 22:18:50 +05:30
Mihir Kandoi
80ca8b3a25 docs(postgres): catalog the collation-dependent text pick
Max()/Min() over a text column is a sort, and the engines sort text
differently: MariaDB's utf8mb4 collations fold case, PostgreSQL as CI runs it
orders by byte value. MAX('abc','ABD') is 'ABD' on MariaDB and 'abc' on
PostgreSQL, confirmed on CI in the probe attached to #56241.

That makes a Max() over a text column which varies in case within its group a
live parity gap, rather than the arbitrary-pick preservation the wrap is
usually justified as. Where the column is functionally dependent on the group
key it stays a genuine no-op and collation cannot matter, so the rule is
scoped to non-FD columns to keep it a high-precision signal.

Recorded as a fifth second-order trap in the guide and in the Greptile
instructions, including the trap that a local macOS PostgreSQL agrees with
MariaDB here and reports a false all-clear.
2026-08-02 22:08:01 +05:30
Mihir Kandoi
39b6f37a48 ci(patch): use GITHUB_REF instead of rebuilding it from type and name
GITHUB_REF is already the fully qualified ref for both branch and tag events,
so reconstructing refs/heads/$GITHUB_REF_NAME and refs/tags/$GITHUB_REF_NAME
just risks the two drifting apart. Keep the type check, since it still decides
whether the develop fallback applies, and take the ref verbatim.
2026-08-02 22:06:21 +05:30
Mihir Kandoi
28d498012a ci(patch): resolve the frappe ref by type and only fall back for branches
The probe used --heads with a bare name, so it could not describe a tag push
and would have fallen back to develop for one. Resolve a fully qualified ref
from the event instead: the PR base or pushed branch under refs/heads, a tag
under refs/tags, and fail loudly on an unrecognised ref type.

Only branch refs are eligible for the develop fallback. A tag that is absent
from frappe is a real error, not a stacked-PR base, so it still fails.
2026-08-02 22:04:48 +05:30
Mihir Kandoi
ccf54b5881 ci(patch): only fall back when the frappe branch is genuinely absent
The previous `||` treated every fetch failure as a missing branch, so a
transient network or auth error on a base that does exist in frappe would
silently substitute develop and report Patch Test results against the wrong
revision.

Probe with `ls-remote --exit-code` instead: exit 2 means no matching ref, so
fall back; any other non-zero status is a real failure and is re-raised.
2026-08-02 20:45:05 +05:30
Mihir Kandoi
1a83fc516e ci(patch): fall back to develop when the base ref has no frappe branch
The Patch Test fetches the frappe repo using this erpnext PR's base branch
name. For an ordinary PR that is develop, which exists in frappe/frappe. For a
stacked PR the base is an erpnext feature branch with no counterpart there, so
the fetch fails and the step exits 128 before any patch runs:

    fatal: couldn't find remote ref pg-audit/bom-amount-per-line

This affects every stacked PR. It has been latent rather than absent: earlier
stacks passed only because their Patch Test ran while they still targeted
develop, before being retargeted onto the layer below.

Fall back to develop when the base ref does not resolve. Ordinary PRs and
version-branch PRs are unaffected -- their base exists in frappe, so the first
fetch succeeds and the fallback never runs.
2026-08-02 20:26:47 +05:30
MochaMind
78f9be257b chore: update POT file (#57707) 2026-08-02 14:31:32 +02:00
Shllokkk
61b80050d1 Merge pull request #57703 from Shllokkk/create-payment-entries-from-payable-report
feat: validate selection and improve Create Payment Entries dialog
2026-08-02 17:43:17 +05:30
Shllokkk
bb5b71643b feat: show PE count, grand total and draft note in payment dialog 2026-08-02 17:28:51 +05:30
Mihir Kandoi
0b9dd11115 Merge pull request #57699 from mihir-kandoi/codex/fix-shipping-rule-duplicate-taxes
fix: prevent duplicate shipping charges without cost center
2026-08-02 12:18:17 +05:30
Mihir Kandoi
106ecd7120 chore: remove shipping rule comments 2026-08-02 12:03:14 +05:30
Mihir Kandoi
a4134af30b fix: prevent duplicate shipping charges without cost center 2026-08-02 12:01:35 +05:30
Mihir Kandoi
c3cd4f18f2 Merge pull request #57676 from mihir-kandoi/feat/mr-supplier-selection-dialog
feat: select a supplier per item when creating Purchase Orders from a Material Request
2026-08-02 11:55:49 +05:30
Mihir Kandoi
6477709f7c Merge pull request #57674 from mihir-kandoi/fix/uom-conversion-factor-precision
fix: preserve UOM conversion factor precision in transactions
2026-08-02 11:55:13 +05:30
Shllokkk
7fdb768259 feat: validate selection and create draft payment entries synchronously 2026-08-01 19:40:39 +05:30
Mihir Kandoi
07ac4d83ef feat(job_card): print quantities with their stock uom (#57689)
* feat(job_card): carry the stock uom on the job card

Every quantity the job card reports belongs to the item it produces, but the
document had no unit of its own, so messages could only print bare numbers.

Add the Stock UOM field, set from the finished good or the final product, and
backfill the job cards that already exist.

* fix(job_card): print quantities with their unit

A bare 5 in an error says nothing about what was counted. Every message that
reports a quantity now names its unit, taking it from the job card's stock uom,
from the previous operation's finished good when the message compares two
operations, and from the item itself for a raw material transfer.

The completion dialogs read the same unit off the job card.

* refactor(job_card): move the stock uom next to the qty it measures

* fix(job_card): keep the stock uom backfill atomic

Drop the auto commit toggle so the backfill is a single transaction with no
connection flag left behind when it raises, and select the job cards to fill
with an explicit unset filter instead of a value list.
2026-08-01 18:34:31 +05:30
Mihir Kandoi
0ddf72dae9 refactor(job_card): make the completion dialog say what it asks for (#57688)
* refactor(job_card): drop the unused make_finished_good handler

Nothing triggered it and Job Card has no make_finished_good method to call.

* refactor(job_card): make the completion dialog say what it asks for

The dialog qty shares the Qty to Manufacture label with the field on the form
while it means the current cycle only, its title fell back to the generic Enter
Value because frappe.prompt takes four arguments and it was passed five, and
nothing on it stated that the three quantities have to add up.

Name the cycle in the label, title the dialog after the button that opens it,
and describe the split on the fields. Same wording in the shop floor dialog.
2026-08-01 18:34:31 +05:30
Mihir Kandoi
7bffd84482 fix(job_card): reject a completion split that cannot add up (#57687)
* fix(job_card): reject a completion split that cannot add up

The completion dialogs silently dropped a recalculation whose result went
negative, so entering a pending qty larger than what is left of the qty to
manufacture kept the contradiction (3 to manufacture, 3 completed, 2 pending)
and the job card only failed much later, on submission.

Keep the split consistent while it is entered: reset the pending qty when the
qty to manufacture changes, and refuse a completed, pending or process loss qty
that leaves the others negative. complete_job_card validates the same rule, so
the shop floor and the API cannot store a split that will never submit.

Also name the three parts in the submission error instead of calling their sum
the Total Completed Qty, which read as a contradiction of the field itself.

* test(job_card): cover the completion qty split guard
2026-08-01 18:34:30 +05:30
Mihir Kandoi
970039d8ec fix(job_card): leave the pending qty out of the job card's own output (#57686)
* fix(job_card): leave the pending qty out of the job card's own output

Pending qty is the part of a job card handed over to another job card, but the
status and the manufacturing entry still measured the card against its full
for_quantity. A card submitted with 3 completed and 2 pending was stuck at Work
In Progress with no way to change it, and its manufacturing entry was built for
the full 5.

Measure both against for_quantity minus pending qty, so the card reaches To
Manufacture on submission, its manufacturing entry covers the completed qty, and
it is Completed once that qty is manufactured.

* test(job_card): cover a job card completed with a pending qty
2026-08-01 18:34:30 +05:30
Mihir Kandoi
0e1bc58b2e fix(job_card): apply the completion dialog's qty to manufacture (#57685)
* fix(job_card): apply the completion dialog's qty to manufacture

Both the desk dialog and the shop floor session dialog send for_quantity when
completing a job card, but complete_job_card dropped it. Reducing Qty to
Manufacture to 3 on a job card of 5 left for_quantity at 5, so set_process_loss
turned the untouched 2 into process loss on the next save.

The dialog qty covers the current cycle, so add it to the qty already completed
by the earlier cycles of the job card instead of overwriting for_quantity, and
validate the pending qty against the result.

* test(job_card): cover qty to manufacture from the completion dialog

Reducing the dialog qty resizes the job card without inventing process loss, and
a pending qty split across two cycles leaves for_quantity untouched.
2026-08-01 18:34:30 +05:30
Mihir Kandoi
3bd3354152 fix(job_card): require the previous operation to be manufactured (#57684)
* fix(job_card): block next operation until previous operation is manufactured

With track semi finished goods, Work Order Operation completed_qty is set from
the submitted job cards' total completed qty, so a job card of the next
operation could be started and completed even when no Manufacture entry existed
for the previous operation. The semi-finished goods it consumes were never
produced.

Validate the sequence against the qty actually manufactured against the previous
operations' job cards (Manufacture entries / Subcontracting Receipts) when the
work order tracks semi finished goods.

* test(job_card): cover manufactured qty check across previous operations

Work order with operations A and B at sequence 1 and C at sequence 2, tracking
semi finished goods. C stays blocked while A's job card is submitted but its
Manufacture entry is missing, and once A is manufactured for 3, C can only be
completed for 3.
2026-08-01 18:34:29 +05:30
Mihir Kandoi
1a49e73c85 Merge pull request #57681 from Shllokkk/sre-voucher-qty-use-demand
fix: set reservation voucher_qty to voucher demand not reserved qty
2026-08-01 15:42:12 +05:30
Shllokkk
7a97dc3361 test: partial work order reservation records full voucher_qty 2026-08-01 15:29:20 +05:30
Shllokkk
7995bb9960 fix: set reservation voucher_qty to voucher demand not reserved qty 2026-08-01 15:29:20 +05:30
Mihir Kandoi
6be6bf2929 ci: fall back to develop when frappe has no matching branch (#57693)
* ci: fall back to develop when frappe has no matching branch

The frappe branch to install is taken from the pull request's base branch. A
stacked pull request targets another erpnext branch, so the clone fails with
"couldn't find remote ref", no bench is installed, and every job that needs one
fails with it.

Fall back to develop when the base branch does not exist in frappe. An explicit
FRAPPE_BRANCH is left alone, since it can be a commit sha rather than a branch.

* ci: only fall back when frappe is known to lack the branch

git ls-remote --exit-code reports 2 for a branch that is not there and 128 for a
remote it could not reach. Treating both as absence let a transient network or
DNS failure install develop over the branch the pull request was built against.

Fall back on 2 alone and log anything else, so a flaky probe leaves the branch
as it was.
2026-08-01 09:38:14 +00:00
Diptanil Saha
ceefd4add7 Merge pull request #57201 from diptanilsaha/fix/perms_whitelisted_methods
fix: permission checks on various whitelisted methods
2026-08-01 14:46:34 +05:30
Mihir Kandoi
e694d45ed4 Merge pull request #57679 from Shllokkk/reserved-stock-dashboard-net-transferred
fix: exclude transferred and consumed qty from dashboard reserved stock
2026-08-01 14:33:32 +05:30
diptanilsaha
0659bd7049 fix(payment_request): added permission checks on resend_payment_email 2026-08-01 14:28:29 +05:30
diptanilsaha
3b0cbc972e fix(item_variant): added permission checks on enqueue_multiple_variant_creation 2026-08-01 14:28:29 +05:30
diptanilsaha
09d721d1be fix(assets): add permission checks on whitelisted methods on asset_capitalization 2026-08-01 14:28:24 +05:30
Shllokkk
6c36624d91 fix: exclude transferred and consumed qty from dashboard reserved stock 2026-08-01 13:24:46 +05:30
Mihir Kandoi
2e72846670 fix: label the items table in the supplier selection dialog
The grid template always renders its label line, so leaving the table unlabelled
left an empty line hanging above the description.
2026-08-01 09:23:09 +05:30
Mihir Kandoi
44fdf7bea9 fix: keep the bulk supplier field to half the supplier selection dialog
A lone Link field stretched the full width of the dialog, which reads as a
search bar rather than a field. A column break holds it to half.
2026-08-01 09:21:16 +05:30
Mihir Kandoi
e84bf44e51 feat: set one supplier across every item in the supplier selection dialog
A Material Request where few items carry a default supplier meant picking the
same supplier row by row. A Supplier field above the table copies its value
into every row, leaving the exceptions to be corrected by hand.

Both pickers skip suppliers that are disabled or barred from Purchase Orders by
their scorecard standing.
2026-08-01 09:19:49 +05:30
Mihir Kandoi
f0bb70539d fix: warn about existing draft orders before the supplier selection creates more
Creating through the dialog calls the endpoint directly instead of going
through open_mapped_doc, so the draft link guard that every other Create action
runs never fired, and a repeated dialog quietly produced a second set of draft
orders for the same quantity.
2026-08-01 09:16:13 +05:30
Mihir Kandoi
8ffe5ba420 test: reject the same Material Request item twice in one supplier selection 2026-08-01 09:11:37 +05:30
Mihir Kandoi
99d56cc850 fix: reject the same Material Request item twice in one supplier selection
Each row was checked against the pending quantity on its own, so a payload that
listed one item under two suppliers passed both checks and ordered the pending
quantity twice. The dialog cannot produce that, a direct call to the endpoint
can.
2026-08-01 09:11:37 +05:30
Mihir Kandoi
21c6d10ad3 fix: escape item code and UOM in the supplier dialog errors
Desk renders a client side message as HTML, so an Item or UOM whose name holds
markup ran as markup in the buyer's session.
2026-08-01 09:11:37 +05:30
Mihir Kandoi
3856eaa35e fix: open the Purchase Order when the supplier selection creates only one
Naming a single order in a message and leaving the buyer to click it is a step
for nothing. The form opens directly when there is one order; the message stays
for the case it was meant for, several orders at once.
2026-08-01 09:02:05 +05:30
Mihir Kandoi
d233fdf198 test: reject a supplier selection without items 2026-08-01 09:00:12 +05:30
Mihir Kandoi
07445b3675 feat: order only the items ticked in the supplier selection dialog
Every row is ticked when the dialog opens, so the common case of ordering
everything is unchanged, and a buyer who wants a partial order unticks what
should wait. Creating with nothing ticked is rejected.
2026-08-01 09:00:12 +05:30
Mihir Kandoi
5a78e2290a fix: link the item and spell out the unit in the supplier dialog errors
A bare item code left the buyer to find the item themselves, and a bare number
gave no clue what the limit was counted in. Both messages now link the item and
state the pending quantity in bold with its UOM.
2026-08-01 08:57:04 +05:30
Mihir Kandoi
671c289303 test: alert when Required By falls back to today 2026-08-01 08:52:23 +05:30
Mihir Kandoi
53e09dfdd6 feat: alert when Required By falls back to today
Items whose requested date has passed silently got today as Required By, which
is a date the buyer never asked for. A toast now says so.
2026-08-01 08:52:23 +05:30
Mihir Kandoi
d0cae2eb9c feat: show the UOM alongside the quantity in the supplier selection dialog
The quantity is meaningless without the unit it is counted in, which the buyer
had to look up on the Material Request itself.
2026-08-01 08:52:23 +05:30
Mihir Kandoi
6f22551aae fix: list the Purchase Orders created per supplier instead of opening one
Opening one of several created orders hid the rest and moved the buyer off the
Material Request. The created orders are now reported the way Production Plan
reports its documents, as links in a message, and the form stays put.
2026-08-01 08:39:03 +05:30
Mihir Kandoi
15d10bbaf1 test: Required By on Purchase Orders created per supplier
Backdates the Material Request item so the mapper drops its schedule date, and
asserts the created order still saves with today as Required By.
2026-08-01 08:38:42 +05:30
Mihir Kandoi
d05bd80b1e fix: set Required By on Purchase Orders created per supplier
Mapping drops a schedule date that already passed, leaving the buyer to pick a
new one on the Purchase Order form. Nothing fills it in when the orders are
created straight from the supplier selection dialog, so a Material Request
whose required date has gone by failed to save with "Please enter the Required
By".

Items that lose their date now fall back to today, which is the earliest date a
Purchase Order raised today accepts.
2026-08-01 08:38:26 +05:30
Mihir Kandoi
09cfd1fe91 test: quantity handling in the supplier selection dialog
Asserts the requested quantity reaches the Purchase Order item and that rows
without a supplier, or with a quantity that is zero, negative or beyond the
pending quantity, are rejected.
2026-08-01 08:38:15 +05:30
Mihir Kandoi
da83370c5c feat: adjust the ordered quantity in the supplier selection dialog
The dialog prefilled the pending quantity of each Material Request item but
kept it read only, so ordering less than what was requested meant editing the
Purchase Order afterwards.

The quantity is now editable and is validated against the pending quantity of
its Material Request item, both in the dialog and on the server. The requested
quantity is handed to the mapper as the pending quantity of the source row, so
the existing mapping - including the subcontracting conversions - derives the
Purchase Order quantities from it unchanged.
2026-08-01 08:38:10 +05:30
Mihir Kandoi
65be201ed6 test: supplier selection when creating Purchase Orders from Material Request
Covers the default supplier lookup for pending items, the supplier passed
through to a single mapped order, the grouping of items into one order per
supplier, and the failure when an item is sent without a supplier.
2026-07-31 22:22:55 +05:30
Mihir Kandoi
e8df7b4a90 feat: select a supplier per item when creating Purchase Orders from Material Request
Creating a Purchase Order from a Material Request mapped every pending item
into a single order, leaving the buyer to split it by hand whenever the items
came from different vendors.

The Create action now reads the default supplier of each pending item (item,
item group, then brand defaults). When the items resolve to more than one
distinct supplier - including the case where only some of them have a default -
a dialog lists the items with their default supplier prefilled and editable.
Submitting it groups the items by the chosen supplier and creates one draft
Purchase Order per group.

When every item resolves to the same supplier the order is mapped straight
away with that supplier set, and when none of them has a default supplier the
previous behaviour is unchanged.
2026-07-31 22:22:49 +05:30
Mihir Kandoi
f4d70c2d60 test: fractional conversion factor survives Material Request to Purchase Order
Fails before the fix with 0.45 != 0.453592292 on a site with Float
Precision 2, and 0.454 on the default of 3.
2026-07-31 21:54:05 +05:30
Mihir Kandoi
269cc6ee3b fix: preserve UOM conversion factor precision in transactions
calculate_item_values rounds every Float field on an item row to the
site's Float Precision (3 by default), and conversion_factor was one of
them. The factor is a ratio, not a rate: UOM Conversion Factor.value is
stored at precision 9, and Material Request keeps the full value because
it has no currency field and so never runs the calculation.

Mapping a Material Request to a Purchase Order therefore truncated the
factor - 0.453592292 for Pound -> Kg became 0.454 - and stock_qty, which
is recomputed as qty * conversion_factor, drifted from the quantity that
was requested, leaving the Material Request unable to close.

Exclude conversion_factor from the rounded fields on the server and on
the client. Factors below the site precision would otherwise round to
zero outright.
2026-07-31 21:53:59 +05:30
Shllokkk
ebb5d933ea Merge pull request #57668 from Shllokkk/reserve-stock-accept-dict-doc
fix: allow reserving stock from work order dialog
2026-07-31 21:15:49 +05:30
Diptanil Saha
0a047b410a fix(plant_floor): add missing perm check on get_stock_summary (#57667) 2026-07-31 14:52:17 +00:00
Shllokkk
517053bc25 fix: drop row prefix in reserve stock message when row is unknown 2026-07-31 20:12:15 +05:30
Shllokkk
f29c7de0ef fix: accept dict doc when reserving stock for work order 2026-07-31 20:12:15 +05:30
Diptanil Saha
0fdca37506 fix: use payment entry posting date for received amount exchange rate (#57660) 2026-07-31 17:47:43 +05:30
Sudharsanan Ashok
9a4594ac06 fix: resolve default expense account fallback in gl composer (#57433)
fix: update stock variance account logic which defaults to default expense account set in company

Co-authored-by: Afsal Syed <afsalsyed12@gmail.com>
2026-07-31 14:02:09 +05:30
R-Jayaraman
cde2963da1 test(purchase): add coverage for zero-qty return rejection 2026-07-31 13:31:10 +05:30
R-Jayaraman
b63066ed44 fix(purchase): reject purchase returns where every item has zero quantity
validate_returned_items() set items_returned=True whenever a row matched
a valid item from the original document, even if its qty was 0. This let
a Purchase Invoice, Purchase Receipt, or Subcontracting Receipt return be
submitted with every line at qty=0 - a no-op document with no stock or
financial effect that still consumed a document number and linked back
to the original transaction.

Scoped to the Purchase side only: items_returned now flips to True for
Purchase Invoice/Purchase Receipt/Subcontracting Receipt only when qty
(or received_qty) is actually negative, so an all-zero purchase return
correctly hits the existing "At least one item should be entered with
negative quantity" check. Sales Invoice, Delivery Note, and POS Invoice
are unchanged.

Also applies a corresponding check to the item_name-only fallback branch
(for rows without an item_code - Item Code is not mandatory on Purchase
Invoice Item), which previously bypassed this fix entirely and still set
items_returned=True unconditionally regardless of quantity. For that
branch specifically, only qty is checked (not received_qty): with no
linked Item there's no accepted/rejected split, so received_qty carries
no independent meaning and a qty=0 row must be rejected regardless of
its value.
2026-07-31 13:31:01 +05:30
Mihir Kandoi
a6bdf7905e Merge pull request #57650 from aerele/fix/material-transfer-qty-precision
fix: respect quantity precision in material transfer validation
2026-07-31 12:40:15 +05:30
Sudharsanan11
cf72e03f39 test: cover material transfer quantity precision 2026-07-31 12:09:43 +05:30
Sudharsanan11
1ff8bf7971 fix: respect quantity precision in material transfer validation 2026-07-31 12:09:36 +05:30
Jatin3128
fd7765ac02 fix: filter Accounts Receivable by invoice sales partner (#57628)
Filter Accounts Receivable and AR Summary on the Sales Invoice's own
sales_partner instead of the customer's default_sales_partner, and read
the Sales Partner column from the invoice. Returns are attributed to the
invoice they settle, matching how the Sales Person filter works.
2026-07-31 11:42:17 +05:30
Diptanil Saha
9e659938d7 fix(quotation): carry forward communications from opportunity at after_insert (#57639) 2026-07-31 05:05:26 +00:00
ruthra kumar
caac1468b7 Merge pull request #57434 from aerele/pcv-status-update
fix: update doc status in period closing voucher
2026-07-31 10:34:36 +05:30
Mihir Kandoi
41cc4ffeb6 Merge pull request #57606 from aerele/fix/stock-entry-items-add-scio-guard
fix: guard scio row lookup in stock entry items_add
2026-07-31 08:56:06 +05:30
pandiyan
6e444a1832 fix: guard scio row lookup in stock entry items_add
check the result of find() before reading t_warehouse off it. on a
'receive from customer' entry with no row carrying scio_detail, find()
returns undefined and items_add throws a typeerror.

the throw rejects the serially-run handler chain, so the stock entry
controller's own items_add never runs and the new row silently loses
its target warehouse, expense account, cost center and serial/batch
field defaults.

leave t_warehouse unset when no reference row exists, so the rest of
the chain still runs.
2026-07-31 08:54:33 +05:30
rohitwaghchaure
d59c5e36bc feat: status based bar colors in Work Order gantt view (#57634) 2026-07-30 23:24:59 +05:30
rohitwaghchaure
386a4ac1f0 fix: do not fetch a random inventory account when multiple inventory accounts exist (#57626) 2026-07-30 14:13:45 +00:00
nareshkannasln
b3c2ba5381 fix: validate account frozen date 2026-07-30 17:14:44 +05:30
Jatin3128
7febc28ed6 feat: auto-fill subscription accounting dimensions from plan with item fallback (#57615)
When a plan is selected in the Subscription's Plans table, the Subscription's
accounting dimensions (cost center and any custom dimensions) auto-fill from the
plan, falling back to the plan item's company default (selling cost center for a
Customer, buying for a Supplier). Only empty fields are filled. Stale async
responses are ignored so a quick re-pick of the plan can't be overwritten.
2026-07-30 15:07:15 +05:30
Shllokkk
956105579d Merge pull request #57618 from Shllokkk/asset-manual-create-valuation-rate
fix: source manually created asset value from valuation rate
2026-07-30 14:52:50 +05:30
Shllokkk
46e01c2d92 fix: source manually created asset value from valuation rate 2026-07-30 14:25:41 +05:30
Mihir Kandoi
e65e1d3c96 Merge pull request #57616 from mihir-kandoi/fix/item-group-root-seeding
fix: seed standard Item Groups under the existing tree root
2026-07-30 13:24:52 +05:30
Mihir Kandoi
e7088d8981 fix: seed standard Item Groups under the existing tree root
install_fixtures always inserted "All Item Groups" as a parentless group.
On a site where another app had already created the root, ItemGroup.validate
re-parented it, leaving a second group-root that held the standard groups
while the real root held everything else.

This is reproducible with the healthcare app on a non-English site: its
after_install seeds the root as _("All Item Groups"), so a pt-BR site gets
"Todos os Grupos de Itens" as the root before the setup wizard runs. The
split predates #57390 -- the old translated-name lookup resolved to the same
root and produced an identical tree.

Resolve the root once with get_root_of (falling back to the canonical English
name on fresh installs) and use it for the root record's exists-guard and the
standard groups' parent, matching Company.create_default_departments.

Patch merges an already-seeded "All Item Groups" into the root it sits under,
lifting its children and repointing every link.

Closes #57581
2026-07-30 13:08:28 +05:30
Soham Kulkarni
ec02b5fa64 Merge pull request #57614 from sokumon/item-default
fix: unchecking default workspace
2026-07-30 12:39:16 +05:30
sokumon
85fa6596b8 fix: unchecking default workspace 2026-07-30 12:23:00 +05:30
Mihir Kandoi
0a7c8504e6 Merge pull request #57609 from mihir-kandoi/fix/title-field-parity
fix(projects): read the Timesheet label from the employee field
2026-07-30 08:36:31 +05:30
Mihir Kandoi
38e5674ea4 chore(stock): drop the dead title template on Material Request
`set_title()` runs in validate and always fills `title` first, so
`set_title_field()` never renders `{material_request_type}`, and
create_new.js skips defaults for the field `title_field` names. Titles
stay "<type> Request for <items>".
2026-07-30 08:22:01 +05:30
Mihir Kandoi
03d84430b6 fix(projects): read the Timesheet label from the employee field
Timesheet was the only doctype where the {...} template on `title`
actually rendered: `title_field` was `title`, so `set_title_field()`
seeded it from `{employee_name}` on insert. A `default` renders once, so
reassigning a draft left the stored title — and every label derived from
it — on the previous employee, with no way to correct it from the form
because the field is hidden.

Point `title_field` at `employee_name` so the label reads the live field
instead of a copy that drifts. Existing rows need no backfill.
2026-07-30 08:22:00 +05:30
Mihir Kandoi
f71946def7 Merge pull request #57419 from kaulith/fix/update-items-row-removal-permission
fix: don't require cancel and delete perms to remove items via Update Items
2026-07-29 16:42:26 +05:30
Khushi Rawat
4f1adb8a94 Merge pull request #57520 from khushi8112/feature/default-modern-print-formats
feat: default new sites to the Modern with Images print formats
2026-07-29 15:47:08 +05:30
Jatin3128
cfe18e8427 fix: let Purchase Receipt cancel defer to Frappe's linked-document check (#57592)
on_cancel pre-blocked cancellation with its own "Purchase Invoice is
already submitted" guard, duplicating the check Frappe already runs for any
submitted linked document. Drop the guard and the unused check_next_docstatus()
method it mirrored so the receipt defers to the framework: the Cancel All
Documents flow cancels the invoice first and then the receipt, and a direct
cancel is still rejected by Frappe's linked-document check.

Add a regression test that a direct cancel of a receipt with a submitted
invoice is rejected and rolls back, leaving no stray stock or GL entries.
2026-07-29 14:44:32 +05:30
nishkagosalia
136f92db04 fix: handling negative grand total 2026-07-29 14:31:33 +05:30
Jatin3128
6b8b9d3644 test: isolate accounts settings mutation in overdue threshold test (#57441)
* test: isolate accounts settings mutation in overdue threshold test

test_overdue_billing_threshold_on_submit mutated the Accounts Settings singleton
without restoring it, so a failed assertion mid-test leaked
enable_overdue_billing_threshold and the bypass role into later tests that submit
sales invoices. Wrap the mutations in try/finally and restore the originals. Also
assert that a 0 overdue limit on the customer inherits the customer group's limit.

* test: restore credit limits in overdue threshold fallback test
2026-07-29 12:38:33 +05:30
Nabin Hait
b86ad21444 Merge pull request #57314 from krishna-254/fix/skip-italy-einvoice-opening-invoices
fix(italy): skip e-invoicing for opening invoices
2026-07-29 11:08:59 +05:30
Diptanil Saha
372dff2ffa refactor(accounts): repost accounting ledger (#56442)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:51:06 +00:00
Vishnu Priya Baskaran
5125d64b7f fix: clear deferred revenue/expense fields on uncheck (#57140) 2026-07-29 04:18:45 +05:30
Krishna Pramod Shirsath
e0c31f1745 fix: recover failed POS closings (#57203)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-07-28 22:29:47 +05:30
Mihir Kandoi
6d748c5523 Merge pull request #57571 from mihir-kandoi/move-warehouse-defaults-to-company
refactor: move warehouse defaults from Stock Settings to Company
2026-07-28 20:28:55 +05:30
Mihir Kandoi
2095073688 fix: check company permission before reading sample retention stock
validate_sample_quantity and move_sample_to_retention_warehouse are
whitelisted and take company from the caller, which selects whose retention
warehouse gets read. The retained batch qty then reaches the return value and
the max-retained warning, so an authenticated user could probe another
company's stock with a known item and batch.

Gate the shared company -> warehouse resolution on read permission for the
Company, which respects User Permissions. validate_sample_quantity only grew
a company argument in this branch; move_sample_to_retention_warehouse already
took one, so this closes that path too.
2026-07-28 20:15:19 +05:30
Mihir Kandoi
ac477bb33c test: stop relying on companies having no default warehouse
Companies now get their Stores warehouse as Default Warehouse, so item
warehouse resolution succeeds where it previously came back empty.

test_internal_pr_reference cleared inter_company_reference and asserted a
ValidationError, but no validation covers that field - the mapper already
sets it. It was incidentally catching 'Row #1: Warehouse is mandatory for
stock Item' from the blank target warehouse, so the assertion never tested
what it claimed. Dropped it; the delivery_note_item assertion below still
covers the reference linkage.

test_inter_company_transaction_without_default_warehouse now establishes its
own premise by clearing the company's default warehouse instead of relying
on it being unset. Its failure previously skipped the teardown that restores
frappe.local.enable_perpetual_inventory, which db rollback cannot undo, which
in turn broke two later inter-company tests.
2026-07-28 20:06:47 +05:30
Mihir Kandoi
455251abe1 fix: throw when the transaction company has no sample retention warehouse
Item.retain_sample is validated against any company having one configured,
since Item is not company-scoped. The transaction company may still not be,
in which case get_batch_qty received warehouse=None and returned its batch
list, which was then compared numerically -> TypeError.

Resolve company -> retention warehouse through one helper that throws a
clear message instead, covering both the sample stock entry and the
whitelisted quantity validation.
2026-07-28 19:41:29 +05:30
Mihir Kandoi
26613d258e refactor: move warehouse defaults from Stock Settings to Company
Default Warehouse and Sample Retention Warehouse were global singles, so every
consumer had to re-check that the warehouse belonged to the transaction's
company before using it. Both now live on Company, under a new Warehouse
Defaults section that also collects the warehouse fields Company already had.

The company check moves to Company.validate_warehouses, which also rejects
group warehouses for all seven fields — a group or cross-company value there
already failed at SLE time, this just surfaces it at the source.

New companies get their Stores warehouse as Default Warehouse via
create_default_warehouses, replacing the setup-wizard and test-fixture code
that seeded the global.
2026-07-28 19:29:18 +05:30
Mihir Kandoi
c80a848216 Merge pull request #57567 from aerele/fix/stock-balance-for-none-row
fix: guard against None row in get_stock_balance_for
2026-07-28 19:26:26 +05:30
Shllokkk
3f0b894cdf Merge pull request #57566 from Shllokkk/item-def-accounting-desc
fix(item): correct description on deferred revenue/expense
2026-07-28 19:22:27 +05:30
Shllokkk
fa75aa08ab fix(item): correct description on deferred revenue/expense 2026-07-28 19:00:24 +05:30
R-Jayaraman
f9d25bc3d3 fix: guard against None row in get_stock_balance_for
row defaults to None but was dereferenced unconditionally when computing
the incoming rate for batch-tracked items, causing an AttributeError for
any caller (e.g. direct API calls) that omits row while still passing
batch_no. The other two row accesses in this function already guard
against None; this brings the incoming-rate check in line with them.
2026-07-28 18:57:06 +05:30
khushi8112
db7b7bb106 refactor: split make_depreciation_entry into public and internal helpers 2026-07-28 17:52:07 +05:30
Smit Vora
986cea2331 feat: taxable-base resolver hook for custom charge types (#56175) 2026-07-28 17:21:55 +05:30
Mihir Kandoi
0cd95e1995 fix(manufacturing): fall back to UOM Conversion Factor in Production Plan (#57553)
Production Plan read the conversion factor straight off the item's own
UOM child table, so an item with a purchase UOM but no matching row threw
"UOM Conversion factor not found" while Stock Entry silently resolved it
from the item's variant template or the UOM Conversion Factor doctype.
Resolve it the same way, and keep returning None when nothing is
configured anywhere so the missing-setup error still fires.
2026-07-28 11:41:51 +00:00
Shllokkk
5fc20d6b8e fix: respect child warehouse account override in Stock and Account Value Comparison (#57552)
fix: respect child warehouse account override in stock vs account value comparison
2026-07-28 16:51:53 +05:30
Diptanil Saha
5835709402 fix: add permission check for get_item_details (#57515) 2026-07-28 10:38:40 +00:00
Mihir Kandoi
a051a12d9b Merge pull request #57540 from mihir-kandoi/fix-company-mfg-warehouse-filters
fix(setup): scope manufacturing warehouse filters to company
2026-07-28 14:16:44 +05:30
Mihir Kandoi
632113c309 fix(setup): scope manufacturing warehouse filters to company
Default WIP, Finished Goods and Scrap Warehouse fields on Company listed
warehouses of every company. Filter them by the current company and
exclude group warehouses, matching the other warehouse fields.
2026-07-28 14:10:50 +05:30
Mihir Kandoi
91e4a753b3 Merge pull request #57535 from frappe/fix/silently-swallowed-exceptions
fix: stop swallowing exceptions silently in six places
2026-07-28 13:06:46 +05:30
Mohd Haris
06bfc23436 fix: prevent TimestampMismatchError resolving Dunning with multiple overdue installments
`get_linked_dunnings_as_per_state` joins Dunning to its Overdue Payment child
table without DISTINCT. When a Sales Invoice has more than one overdue
installment, its Dunning holds one Overdue Payment row per installment, so the
query returns the same Dunning name once per row.

`update_linked_dunnings` then loads that Dunning name into a separate document
object for each duplicate row and saves each one. The first save bumps the
`modified` timestamp, so the second (now stale) save fails with
`TimestampMismatchError` ("Document has been modified after you have opened
it"). The error is raised on the Dunning while the user is submitting a Payment
Entry, making it confusing, and payments for such invoices cannot be posted at
all.

Add DISTINCT so each linked Dunning is returned (and saved) exactly once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 13:04:31 +05:30
rohitwaghchaure
4a0a1db17e fix: skip stock expense GL entries for non-stock items (#57519)
* fix: skip stock expense gl entries for non stock items

* test: use a leaf expense account for the service item invoice
2026-07-28 13:00:32 +05:30
Mihir Kandoi
85a04772f6 fix: stop swallowing exceptions silently in six places
- Pricing Rule: log a broken condition instead of dropping the rule
- Payment Request: log gateway validation failures
- Supplier Quotation from RFQ portal: let errors surface instead of
  returning None to a dead submit button
- Bank Transaction Rule: drop the pointless try/except around the
  on_trash unlink; a failed write already aborts the delete
- Statement of Accounts: the CC handler was unreachable
  (frappe.get_value returns None, it does not raise); drop it and
  filter out users without an email
- Bank Statement Import: 'Bank Account' at column 0 is falsy, so the
  column was appended instead of filled
2026-07-28 12:49:08 +05:30
Mihir Kandoi
2f07dfc474 Merge pull request #57532 from mihir-kandoi/fix-update-cost-bom-creator-boms
fix(manufacturing): update cost of BOMs created via BOM Creator
2026-07-28 12:46:47 +05:30
Mihir Kandoi
d269c90838 fix(manufacturing): update cost of BOMs created via BOM Creator
`calculate_rm_cost` skipped rate refresh whenever `bom_creator` was set,
so neither the Update Cost button nor the BOM Update Tool could ever
refresh those BOMs. Every BOM in a multi-level tree carries the field, so
whole trees stayed frozen at their creation rates.

The guard replaced the removed `rm_cost_as_per == "Manual"` check in
0b63dbf, on the assumption that BOM Creator rows hold manual rates. They
do not: BOM Creator recomputes every row from `rm_cost_as_per` on save.
2026-07-28 12:33:12 +05:30
Mihir Kandoi
7d44bdbdc6 Merge pull request #57522 from mihir-kandoi/fix-subcontracting-receipt-title
fix(subcontracting): stop the Subcontracting Receipt title from going stale
2026-07-28 12:19:21 +05:30
Mihir Kandoi
8d7ac9f51d Merge pull request #57528 from mihir-kandoi/fix-bom-creator-dup-subassembly
fix(manufacturing): scope BOM Creator tree children to the parent row
2026-07-28 12:18:46 +05:30
Mihir Kandoi
15b7626eed Merge pull request #57521 from mihir-kandoi/fix-semi-fg-produced-qty
fix(manufacturing): sum semi-FG qty across split job cards
2026-07-28 12:14:09 +05:30
Mihir Kandoi
b37152752f fix(manufacturing): scope BOM Creator tree children to the parent row
The BOM Creator tree identified a node by the parent's item code
(fg_item) instead of the specific BOM Creator Item row, so every
occurrence of a repeated sub-assembly shared one child set: expanding
any one of them listed the raw materials of all of them, and deleting
one wiped the raw materials of its siblings.

Key the tree on fg_reference_id and make the node value the row name,
matching the framework convention that a tree node's value is its
docname. Item code now travels as its own field for the label and for
the fg_item argument sent back on add/convert.

Fixes #57311
2026-07-28 11:25:29 +05:30
Mihir Kandoi
bde118e7cf fix(manufacturing): exclude corrective job cards from semi-FG aggregate 2026-07-28 11:09:36 +05:30
Khushi Rawat
56a9ca334b Merge pull request #57451 from khushi8112/feature/request-for-quotation-print-formats
feat: add four Request for Quotation print formats built with the print format builder
2026-07-28 11:06:33 +05:30
Khushi Rawat
aacccfb958 Merge pull request #57450 from khushi8112/feature/quotation-print-formats
feat: add four Quotation print formats built with the print format builder
2026-07-28 11:06:20 +05:30
Khushi Rawat
bd5d6fb9d8 Merge pull request #57449 from khushi8112/feature/pos-invoice-print-formats
feat: add four POS Invoice print formats built with the print format builder
2026-07-28 11:06:06 +05:30
Khushi Rawat
22334def56 Merge pull request #57447 from khushi8112/feature/purchase-invoice-print-formats
feat: add four Purchase Invoice print formats built with the print format builder
2026-07-28 11:05:51 +05:30
Khushi Rawat
5631acc39b Merge pull request #57446 from khushi8112/feature/purchase-order-print-formats
feat: add four Purchase Order print formats built with the print format builder
2026-07-28 11:05:38 +05:30
Khushi Rawat
dd0f763613 Merge pull request #57445 from khushi8112/feature/delivery-note-print-formats
feat: add four Delivery Note print formats built with the print format builder
2026-07-28 11:05:25 +05:30
Khushi Rawat
b71a9b8273 Merge pull request #57437 from khushi8112/feature/sales-order-print-formats
feat: add four Sales Order print formats built with the print format builder
2026-07-28 11:05:12 +05:30
Khushi Rawat
7a76c5f268 Merge pull request #57430 from khushi8112/feature/sales-invoice-print-formats
feat: add four Sales Invoice print formats
2026-07-28 11:04:59 +05:30
Mihir Kandoi
5548f0726a fix(manufacturing): sum semi-FG qty across split job cards
update_semi_finished_good_details assigned the current job card's
manufactured_qty to Work Order.produced_qty instead of accumulating it,
so a second job card on the same operation overwrote the first. Nothing
corrected it afterwards because StatusService.update_work_order_qty
returns early for track_semi_finished_goods work orders, leaving the
work order stuck below its planned qty with no way to progress.

Aggregate manufactured_qty and completed_qty over the operation's
submitted job cards instead.
2026-07-28 11:04:17 +05:30
Mihir Kandoi
543301701e fix(subcontracting): stop storing "{supplier_name}" as the Subcontracting Receipt title
Subcontracting Receipt was left out of #57493. Its title_field is "title",
so the template does get rendered on insert, but never again — the title
goes stale as soon as the supplier changes.

Point title_field at supplier_name like Purchase Receipt, and give the
title field the same shape as its subcontracting siblings. Existing rows
already hold a rendered name, so no data patch is needed.

Also guard the whole class of bug: a "{...}" default on a title field is
only ever rendered when title_field is "title".
2026-07-28 11:00:57 +05:30
khushi8112
3eb924d052 feat: default new sites to the Modern with Images print formats
Point set_default_print_formats() at the new builder-made
"<Doctype> Modern with Images" formats, falling back to the existing
"with Item Image" formats when the new ones aren't present. Only fresh
sites are affected (the existing default-print-format guard is kept).
2026-07-28 11:00:31 +05:30
Shllokkk
14fd045555 Merge pull request #57471 from Shllokkk/routing-hour-rate-operating-cost
fix: update operating cost when propagating workstation hour rate to routing
2026-07-28 10:50:06 +05:30
Mihir Kandoi
43acbae5a1 Merge pull request #57493 from aerele/fix-subcontracting-title-template
fix: stop storing raw title template on subcontracting orders
2026-07-28 10:41:59 +05:30
Shllokkk
a5250d8e80 Merge branch 'develop' into routing-hour-rate-operating-cost 2026-07-28 01:33:50 +05:30
Sudharsanan Ashok
73224d3650 fix(stock): keep manufactured item rate at zero when inputs are free (#57334)
* fix(stock): keep manufactured item rate at zero when inputs are free

when a finished item is produced from raw materials consumed at zero
valuation, the incoming rate fell back to the item's own valuation
rate (or BOM cost), valuing free inputs as output and inflating the fg
value on every production run.

add has_consumption_basis() to detect when the consumed cost is known
even if it is zero (consumed rows present, or a consumption entry
exists for the work order). when it is, skip the get_valuation_rate and
BOM-cost fallbacks so a real cost of zero is preserved.

* test(stock): cover manufacture rate for zero-valued raw materials

- manufacture from a free input keeps fg basic_rate and sle
  incoming_rate/stock_value_difference at zero even when the fg already
  carries a valuation in the target warehouse
- material consumption on with no consumption entry does not fall back
  to bom/price-list rate for free inputs
- zero-valued consumption entry keeps the manufacture entry's fg rate
  at zero
2026-07-27 23:35:00 +05:30
Sudharsanan Ashok
d37e905322 fix(stock): value batched packed-item returns from the original bundle (#57327)
* fix(stock): value batched packed-item returns from the original bundle

when a return delivery note or sales invoice bundle is built via the
use_serial_batch_fields / sle-driven path, its voucher_detail_no keeps the
packed item instead of being remapped to the parent dn/si item. the return
valuation lookup then misses and the bundle values at zero, so the sle
stock_value_difference stays wrong even after a repost.

resolve the original dn/si item via the packed item's parent_detail_docname
when the direct lookup fails, so the return values from the original outward
bundle on both submit and repost.

* test(stock): cover batched packed-item return valuation on repost
2026-07-27 23:33:41 +05:30
Diptanil Saha
28c96999e2 fix(quotation): carry forward communications from opportunity (#57507) 2026-07-27 23:29:57 +05:30
Sudharsanan Ashok
425191e57e fix(stock): narrow legacy serial ledger lookup by item (#57499)
Filter legacy Stock Ledger Entry lookups by item code so the existing
item and warehouse index can reduce rows scanned during serial valuation.
2026-07-27 21:40:44 +05:30
ruthra kumar
05a15dff71 Merge pull request #57500 from frappe/mergify/bp/develop/pr-57484
fix(test): flaky test in exchange rate revaluation (backport #57484)
2026-07-27 17:21:41 +05:30
ruthra kumar
4e77c9d6e6 fix(test): flaky test in exchange rate revaluation
- remove redundant setup on system settings

(cherry picked from commit 484ff8e349)
2026-07-27 11:40:37 +00:00
Raffael Meyer
bdf586c670 fix: error message wording (#57495) 2026-07-27 11:15:18 +00:00
Mihir Kandoi
3f93887ec8 Merge pull request #57492 from frappe/production-plan-recalculate-bin
fix: recalculate whole bin in Production Plan reservation patch
2026-07-27 16:01:04 +05:30
Mihir Kandoi
88b02130e7 fix: recalculate whole bin for Production Plan raw material items
projected_qty is derived from every bin quantity, so refreshing only
reserved_qty_for_production_plan leaves it stale wherever another field
had drifted. Call Bin.recalculate_values() instead.

Renamed so the patch re-runs on sites that already applied
recompute_production_plan_reserved_qty.
2026-07-27 15:26:25 +05:30
Raffael Meyer
8b37c52187 fix(crm): align Opportunity status checks with Quotation statuses (#57489) 2026-07-27 09:20:22 +00:00
Mihir Kandoi
70fa8c0c2a Merge pull request #57485 from frappe/stock-ageing-batch-slot-pooling
fix: pool batch slot values on every run, not only when negative
2026-07-27 13:53:23 +05:30
Mihir Kandoi
545262c5d4 test: assert batch pooling preserves the group total on a repeating rate 2026-07-27 13:42:25 +05:30
Mihir Kandoi
cedaaa3a00 fix: pool batch slot values on every run, not only when negative
A batch is one valuation pool, so any per-slot value difference within a
batch is stale detail from the report's own age slots, not real valuation.
The rebalance only ran when consumption had already driven a slot negative,
so a batch whose receipts landed at different rates kept a skewed split
across age buckets (one bucket free, another double-priced) while the total
stayed correct.

Drop the negative-slot precondition and always spread a batch's pooled value
over its slots in proportion to qty. Redistribution preserves group totals,
so buckets still sum to Stock Balance; only the split across ages changes.
2026-07-27 13:37:08 +05:30
pandiyan
5008e6126f fix: stop storing raw title template on subcontracting orders
subcontracting order and subcontracting inward order carry a hidden
title field defaulting to "{supplier_name}" / "{customer_name}", while
their title_field points at supplier_name / customer_name. document.
set_title_field() substitutes the template only when title_field is
"title", so every record stores the placeholder verbatim.

drop the dead default and hidden flags, move title into the other info
tab to match purchase order, and add a patch to repair existing rows.
2026-07-27 13:18:31 +05:30
Mihir Kandoi
273e9f2431 Merge pull request #57463 from aerele/fix/sco-closed-reserved-qty
fix(subcontracting): release raw-material reservation when closing a subcontracting order
2026-07-27 12:08:37 +05:30
ruthra kumar
d3448ef9a2 Merge pull request #57476 from ruthra-kumar/date_configurable_err_reversal
refactor: configurable date in reverse ERR journals
2026-07-27 11:40:32 +05:30
ruthra kumar
1a558ce641 refactor(test): manually submit reverse err journal 2026-07-27 11:18:41 +05:30
Sudharsanan11
e4b8065a69 test(subcontracting): cover reservation release on closing a subcontracting order
close a partially-received sco with a reserve warehouse and assert the
raw-material reservation is released and projected qty recovers.
2026-07-27 11:13:17 +05:30
Sudharsanan11
db91a79d31 fix(subcontracting): release raw-material reservation when closing a subcontracting order
the bin reserved-qty recalc filtered out closed purchase orders but not
closed subcontracting orders, so closing a partially-received sco kept the
reservation for the unreceived qty and left projected qty understated.
apply the same closed-status filter to the subcontracting order path.
2026-07-27 11:13:17 +05:30
ruthra kumar
0be33e4132 refactor: configurable date in reverse ERR journals 2026-07-27 11:11:38 +05:30
rohitwaghchaure
f077d2edc0 fix: GL entries for purchase expense with LCV (#57475) 2026-07-26 23:08:35 +05:30
MochaMind
fd37bc3ff8 chore: update POT file (#57469) 2026-07-26 16:10:52 +02:00
Shllokkk
371ab1db61 Merge pull request #57443 from Shllokkk/rename-ar-ap-report-filters
fix: rename misleading filter labels in AR/AP reports
2026-07-26 17:16:42 +05:30
Shllokkk
be27a918e6 Merge pull request #57320 from Shllokkk/create-payment-entries-from-payable-report
fix: show create payment entries as an inner button on row selection
2026-07-26 17:14:35 +05:30
Shllokkk
e08e119739 test: assert operating cost is propagated to routing operations 2026-07-26 16:15:53 +05:30
Shllokkk
eb9afa40ea fix: update operating cost when propagating workstation hour rate to routing 2026-07-26 16:03:09 +05:30
Shllokkk
c6a16495c0 Merge pull request #57466 from Shllokkk/routing-hour-rate-operating-cost
fix: recalculate operating cost on hour rate change in routing
2026-07-26 14:06:28 +05:30
Shllokkk
598f6f0f4e fix: recalculate operating cost on hour rate change in routing 2026-07-26 13:11:11 +05:30
mergify[bot]
8c24c5bd68 fix: enable the 'Include Zero Stock Items' filter by default to show zero-stock items in the Stock Balance report (backport #57458) (#57459)
fix: enable the 'Include Zero Stock Items' filter by default to show zero-stock items in the Stock Balance report (#57458)

(cherry picked from commit 4e8f5de5cb)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-07-25 08:27:19 +05:30
Nabin Hait
208a07e19e Merge pull request #57452 from nabinhait/fix-child-qty-rate-type-hint
fix: accept list payload for trans_items in update_child_qty_rate
2026-07-24 21:54:18 +05:30
Shllokkk
f13cd00494 fix: migrate stored AR/AP ageing filter to renamed field 2026-07-24 18:45:07 +05:30
Nabin Hait
d73ff0a2bf fix: accept list payload for trans_items in update_child_qty_rate
The whitelisted endpoint declared trans_items as str, so Frappe's typing
validation raised FrappeTypeError when the client sent the items as a
JSON list. ChildItemUpdater.update already handles both via
frappe.parse_json, so widen the wrapper's hint to str | list.
2026-07-24 17:33:58 +05:30
khushi8112
2d731b3232 fix: remove duplicate supplier name in Bordered format
The right-column Custom HTML block re-displayed the vendor name a
second time. In the source Purchase Order design that block was part
of an address card (name + mailing address together); Request for
Quotation has no per-supplier address field, so after dropping the
address the block became a bare, purposeless repeat of the name
already shown at the top of the left column. Classic, Modern, and
Modern with Images each show it once - Bordered now matches.

Addresses the Greptile review comment on this PR.
2026-07-24 17:26:39 +05:30
khushi8112
fe11252a4d feat: add four Request for Quotation print formats built with the print format builder
Request for Quotation has no pricing fields at all (no rate, amount,
grand_total, in_words), so this is a structurally simplified version
of the Bordered/Classic/Modern/Modern with Images designs rather than
a straight field-swap: Sub Total/Discount/Tax/Grand Total/In Words
sections dropped entirely, item table shows Item/Code/Quantity only.
Supplier fields point at the doctype's own `vendor` field (matching
the existing request_for_quotation_with_item_image standard format's
convention), items table bound to Request for Quotation Item, status
field carries the real RFQ status list, no letter head embedded.
2026-07-24 17:20:28 +05:30
khushi8112
8c3e773f89 feat: add four Quotation print formats built with the print format builder
Adapted from the Sales Order formats: customer_name relabeled to
"Party Name" (Quotation supports both Customer and Lead via
quotation_to), delivery_date mapped to valid_till, items table bound
to Quotation Item, status field carries the real Quotation status
list, no letter head embedded.
2026-07-24 17:09:11 +05:30
khushi8112
f9fe407d42 fix: correct supplier_name/in_words field type across all four formats
Both are Data on the Purchase Invoice doctype, not Small Text
(carried over from the Sales Invoice formats these were adapted from,
where Small Text is correct - Sales Invoice's own doctype defines it
that way). No rendering impact: Data.html and Field.vue never branch
on Small Text vs Data. Addresses the Greptile review comment on
purchase_invoice_bordered.json, and applies the same fix to the other
three formats that had the identical issue.
2026-07-24 16:57:38 +05:30
khushi8112
dc5f445bae fix: correct supplier_name/in_words field type on Purchase Order formats
Both are Data on the Purchase Order doctype; carried over as Small
Text from the Sales Order formats these were adapted from. No
rendering impact - caught via a proactive fieldtype audit after a
Greptile comment on a downstream PR flagged the same pattern on
Purchase Invoice.
2026-07-24 16:57:19 +05:30
khushi8112
8ef7e11332 fix: correct customer_name/in_words field type on Delivery Note formats
Both are Data on the Delivery Note doctype; carried over as Small Text
from the Sales Order formats these were adapted from. No rendering
impact - caught via a proactive fieldtype audit after a Greptile
comment on a downstream PR flagged the same pattern on Purchase
Invoice.
2026-07-24 16:56:39 +05:30
khushi8112
f81a70baff fix: correct customer_name/in_words field type on Sales Order formats
Both are Data on the Sales Order doctype; they were carried over as
Small Text from the Sales Invoice formats they were adapted from,
where that type is correct (Sales Invoice defines them as Small Text).
No rendering impact — caught via a proactive fieldtype audit after a
Greptile comment on a downstream PR flagged the same pattern on
Purchase Invoice.
2026-07-24 16:55:23 +05:30
khushi8112
b025a5f8c2 feat: add four POS Invoice print formats built with the print format builder
Adapted from the Sales Invoice formats: nearly identical field shape
(customer_name, address_display, company_address_display, posting_date,
due_date, total, in_words all exist natively on POS Invoice), so only
the items table (bound to POS Invoice Item), status field options, and
title text needed changing. Each format correctly relabels as "Credit
Note" instead of "POS Invoice" when is_return is set. Field types for
customer_name/in_words corrected to Data to match POS Invoice's own
doctype definition (Sales Invoice defines these as Small Text; POS
Invoice does not). No letter head embedded.
2026-07-24 16:54:16 +05:30
khushi8112
a6829f64da feat: add four Purchase Invoice print formats built with the print format builder
Adapted from the Sales Invoice formats for the buying side: customer
fields swapped for supplier fields, items table bound to Purchase
Invoice Item, status field carries the real Purchase Invoice status
list. Each format correctly relabels as "Debit Note" instead of
"Purchase Invoice" when is_return is set, mirroring the Credit Note
labeling on the Sales Invoice formats. No letter head embedded.
2026-07-24 16:44:40 +05:30
khushi8112
273eea8034 feat: add four Purchase Order print formats built with the print format builder
Adapted from the Sales Order formats for the buying side: customer
fields swapped for supplier fields, delivery_date mapped to
schedule_date (Required By), items table bound to Purchase Order
Item, status field carries the real Purchase Order status list, no
letter head embedded.
2026-07-24 16:33:21 +05:30
khushi8112
0dbedbfb25 feat: add four Delivery Note print formats built with the print format builder
Adapted from the Sales Order formats: transaction_date/delivery_date
mapped to Delivery Note's own posting_date and po_no (customer's PO
number), items table bound to Delivery Note Item, status field carries
the real Delivery Note status list, no letter head embedded.
2026-07-24 16:18:35 +05:30
khushi8112
ef50991d18 fix: correct Sales Order print format bindings and tidy the Modern footer
- items table options: Sales Invoice Item -> Sales Order Item (all four formats)
- status field options: replace the Sales Invoice status list with the actual
  Sales Order statuses (Modern, Modern with Images)
- drop the redundant document-number field from the Modern footer; the header
  already shows it
2026-07-24 15:38:21 +05:30
Shllokkk
e99425b7c4 fix: rename misleading filter labels in AR/AP reports 2026-07-24 15:32:51 +05:30
Mihir Kandoi
3e15fe0dfd Merge pull request #57435 from aerele/fix-party-dashboard-user-permission
fix(accounts): respect user permissions in party dashboard company list
2026-07-24 15:00:00 +05:30
khushi8112
df6c98a187 feat: add four Sales Order print formats built with the print format builder
Adapted from the new Sales Invoice Bordered/Classic/Modern/Modern with
Images formats: invoice-specific fields (posting_date, due_date, the
Sales Invoice/Credit Note title) mapped to their Sales Order
equivalents, no letter head embedded.
2026-07-24 13:08:05 +05:30
Deepesh Garg
a04725eb04 Merge pull request #57296 from deepeshgarg007/user_change
fix: Ignore permission while deleting user permission
2026-07-24 12:29:58 +05:30
pandiyan
903c87bcaa fix: respect user permissions in party dashboard company list
use frappe.get_list instead of frappe.get_all in get_dashboard_info so
the company list honors user permissions. previously, a party with
invoices across multiple companies would raise "User don't have
permissions to select/read this account" for users restricted to a
subset of companies, since get_party_account was called for companies
the user could not access.

fixes frappe/erpnext#57428
2026-07-24 12:22:08 +05:30
Deepesh Garg
314796633f Merge branch 'develop' into user_change 2026-07-24 12:14:34 +05:30
khushi8112
15dee35adc fix: address review feedback on new Sales Invoice print formats
- drop dev-site-specific letter_head references so a fresh install picks its own default
- add missing row_condition on Modern with Images' tax repeater
- standardize Sub Total on total (matches summed item amounts) and In Words on the transaction-currency field across all four formats
2026-07-24 11:37:18 +05:30
khushi8112
4c83b7c5f1 feat: add four Sales Invoice print formats built with the print format builder 2026-07-24 10:52:40 +05:30
Khushi Rawat
5bf1c506a5 Merge pull request #57382 from khushi8112/fix/mt940-per-transaction-reference
fix: map MT940 per-transaction reference from :61: customer_reference
2026-07-24 10:36:35 +05:30
Kaushal Shriwas
87a403ae01 test(selling): deactivate leaked sales order workflow before item removal test 2026-07-23 21:19:52 +05:30
Kaushal Shriwas
5b1c4d22e0 test(selling): cover item removal without cancel and delete perms 2026-07-23 20:42:56 +05:30
Kaushal Shriwas
86ba4395fb fix(selling): don't require cancel and delete perms to remove items via Update Items 2026-07-23 20:42:45 +05:30
Mihir Kandoi
ba593f918a Merge pull request #57335 from aerele/fix/report-date-validation-dry
refactor: reuse shared date range validation across reports
2026-07-23 19:54:34 +05:30
Nabin Hait
50e361e10d Merge pull request #57263 from nabinhait/proforma-invoice
feat(selling): Proforma Invoice against Sales Order
2026-07-23 19:48:44 +05:30
Mihir Kandoi
e6ddb91ce5 Merge pull request #57410 from mihir-kandoi/revert-default-warehouse-names
revert: canonical default warehouse names (#57392, #57409)
2026-07-23 19:41:05 +05:30
Nabin Hait
0b71c943c1 fix(selling): don't email cancelled proformas or copy their PDF
Address review findings:
- send_proforma_email rejects non-issued proformas, and the tab suppresses the
  action for cancelled rows, so a voided document can't be sent to a customer
- mark proforma_pdf as no_copy and disable amendment (a proforma is created
  only from a Sales Order), so a copied proforma can't carry the original's PDF
  and number
2026-07-23 19:17:21 +05:30
Nabin Hait
e4fb5ed3c4 fix(selling): guard proforma against unsubmitted SO and missing PDF
Address review findings:
- make_proforma_invoice: reject a non-submitted Sales Order (the whitelisted
  endpoint was previously only JS-gated on docstatus)
- send_proforma_email: throw a clear error when the attached PDF File is missing
  instead of passing a null fid to sendmail
2026-07-23 18:59:08 +05:30
ervishnucs
170803b041 refactor: reuse shared date range validation across reports 2026-07-23 18:31:50 +05:30
Mihir Kandoi
e4561b1431 Merge pull request #57412 from aerele/customer-pick-list
fix: map pick list customer to delivery note when no sales order
2026-07-23 18:24:19 +05:30
Mihir Kandoi
bf309f65d4 Merge pull request #57413 from aerele/fix-batch-oldest-expiry-typeerror
fix: typeerror in get_batches_by_oldest for mixed batch expiry
2026-07-23 18:23:10 +05:30
pandiyan
62c9f8ee3e fix: typeerror in get_batches_by_oldest for mixed batch expiry
sort on (expiry is none, expiry) so a null expiry_date is never
order-compared against a datetime.date, which raised typeerror in
python 3 when a warehouse held both dated and never-expiring batches.
2026-07-23 17:48:22 +05:30
R-Jayaraman
aff10d3a37 fix: map pick list customer to delivery note when no sales order 2026-07-23 17:41:03 +05:30
Mihir Kandoi
63f6f1808f Revert "Merge pull request #57392 from mihir-kandoi/fix/canonical-default-warehouse-names"
This reverts commit 5fa242ec93, reversing
changes made to a81524ea14.
2026-07-23 17:09:53 +05:30
Mihir Kandoi
a8e74df6f7 Revert "Merge pull request #57409 from mihir-kandoi/chore/drop-warehouse-name-translation-marking"
This reverts commit ab16fd0a98, reversing
changes made to 5fa242ec93.
2026-07-23 17:09:53 +05:30
Mihir Kandoi
ab16fd0a98 Merge pull request #57409 from mihir-kandoi/chore/drop-warehouse-name-translation-marking
refactor: drop translation marking from default warehouse names
2026-07-23 16:56:50 +05:30
Mihir Kandoi
2ea63ce709 refactor: drop translation marking from default warehouse names
warehouse_name is a Data field shown as stored, and everywhere else
users see the docname, which carries the company abbr ("Stores - F")
and is never translated - a translated bare "Stores" has no display
use, so the identity _ marking from #57392 serves nothing. The
exists-guard keeps the runtime session-translation match that protects
legacy sites from duplicate English warehouses.
2026-07-23 16:45:57 +05:30
Mihir Kandoi
5fa242ec93 Merge pull request #57392 from mihir-kandoi/fix/canonical-default-warehouse-names
fix: create default warehouses with untranslated names
2026-07-23 16:17:04 +05:30
Mihir Kandoi
a81524ea14 Merge pull request #57400 from aerele/fix-address-missing-company-address-field
fix: guard against missing is_your_company_address custom field on ad…
2026-07-23 16:10:24 +05:30
Mihir Kandoi
2f008ea846 Merge pull request #57403 from mihir-kandoi/fix-stock-ageing-batch-pool-rebalance
fix: rebalance batch slot values at the pooled rate when driven negative
2026-07-23 16:09:19 +05:30
Mihir Kandoi
194e1df5cc fix: rebalance batch slot values at the pooled rate when driven negative
A batch is one valuation pool, so consumption is valued at the pooled
rate while slots may carry stale intra-batch detail (e.g. units
reconciled at zero and later merged). Consuming such a slot leaves a
negative value on positive qty. Spread the pool value across the
batch's slots when that happens; non-batchwise slots pool per
warehouse.
2026-07-23 15:56:42 +05:30
Mihir Kandoi
4911484349 Merge pull request #57399 from mihir-kandoi/production-plan-reserve-bom-qty
fix: Production Plan raw material qty calculation and bin reservation
2026-07-23 15:27:21 +05:30
Mihir Kandoi
a370c9a348 fix: AttributeError in sufficient sub assembly warning 2026-07-23 15:16:13 +05:30
Mihir Kandoi
276f69498a fix: safety stock and MOQ over-ordering in Production Plan raw materials 2026-07-23 15:16:11 +05:30
Mihir Kandoi
c0cb783603 fix: reserve full BOM consumption for Production Plan raw materials 2026-07-23 15:16:11 +05:30
Mihir Kandoi
9ec2262d09 Merge pull request #57398 from mihir-kandoi/item-group-get-root-of
refactor: use get_root_of for Item Group root resolution
2026-07-23 15:01:16 +05:30
Mihir Kandoi
3b3e7b5ea4 refactor: use get_root_of for Item Group root resolution
Replaces the hand-rolled parentless-group query from #57390 with the
framework helper, matching sibling tree doctypes like Customer Group.
2026-07-23 14:48:37 +05:30
Mihir Kandoi
da8bf368da Merge pull request #57387 from mihir-kandoi/drop-job-card-operation-row-number
refactor: drop unused operation_row_number field from Job Card
2026-07-23 14:42:07 +05:30
rohitwaghchaure
8c0ec3c179 fix: Incorrect creation time at the time cancelling an entry causing an issue especially same posting datetime (#57380)
* fix: shift same-timestamp sibling SLEs when cancelling an entry

update_qty_in_future_sle compared against the reversal SLE's own
creation and skipped same-posting_datetime siblings on cancel, leaving
their qty_after_transaction stale and causing false negative stock
errors.

* fix: revert update_qty_in_future_sle cancel tie-break, it double-counted
2026-07-23 09:07:33 +00:00
Mihir Kandoi
53ae349b5a Merge pull request #57390 from mihir-kandoi/fix-item-group-root-lookup
fix: do not translate root Item Group lookup key
2026-07-23 14:22:31 +05:30
Mihir Kandoi
002ca6d5c1 Merge pull request #57388 from mihir-kandoi/mapper-dict-target-doc
fix: accept dict target_doc in mapper endpoints
2026-07-23 14:22:06 +05:30
pandiyan
ea3ed8b836 fix: guard against missing is_your_company_address custom field on address 2026-07-23 14:06:23 +05:30
Mihir Kandoi
861c6b16be fix: create default warehouses with untranslated names
Company.create_default_warehouses stored warehouse_name through the
session _(), so a site set up in a non-English language keeps its
default warehouses under translated names. The opening-stock fallback
in item.py then looks up {"warehouse_name": _("Stores")} from an
arbitrary session and misses the warehouse whenever the lookup
session's language differs from the creation session's.

Create the default warehouses with canonical English names, matching
the identity-_ convention install_fixtures uses for other default
records. The exists-guard also matches the session translation so a
re-run on a legacy site does not insert English duplicates next to
translated warehouses.

The "Stores" fallback lookup moves to get_stores_warehouse, which
tries the canonical name first and falls back to the session
translation so legacy sites keep resolving their translated warehouse.
The setup-wizard and test-bootstrap lookups drop _() since they now
run after English creation (install_fixtures already used identity _,
so its lookup silently missed translated warehouses before this
change).

Same bug class as #57345.
2026-07-23 14:05:22 +05:30
Mihir Kandoi
d197d5685e fix: resolve Item Group tree root structurally
A name lookup misses roots created under a translated name (pre-#49875
setups) or renamed roots. Resolve the first parentless group by lft and
guard against self-parenting when the root itself is saved.
2026-07-23 14:03:54 +05:30
Shllokkk
f2496d2e2c fix: respect selected BOM when creating work order for variant item (#57358) 2026-07-23 13:49:34 +05:30
Mihir Kandoi
9c8d9ac42b fix: validate supplied operation_id belongs to work order operation 2026-07-23 13:23:15 +05:30
Mihir Kandoi
2551543703 fix: accept dict target_doc in mapper endpoints
Native JSON request bodies deliver target_doc as a parsed dict, which the
str | Document type hints rejected, breaking every Get Items From button.
Unify all whitelisted mapper endpoint hints to str | dict | Document | None.
Needs frappe#41190 so get_mapped_doc converts the dict target.
2026-07-23 13:20:40 +05:30
Mihir Kandoi
553336fa6f fix: resolve or require operation_id server-side 2026-07-23 13:17:49 +05:30
Mihir Kandoi
7239e6ed8c fix: do not translate root Item Group lookup key
_("All Item Groups") resolves in the session language, so for
non-English users the db.exists lookup missed the root (stored in
English) and new groups were saved parentless, becoming uneditable
second roots.

Closes #57345
2026-07-23 13:17:48 +05:30
Mihir Kandoi
cff05687d9 Merge pull request #57389 from mihir-kandoi/fix/translated-default-record-lookups
fix: do not translate default record lookup keys
2026-07-23 13:06:35 +05:30
Mihir Kandoi
50de87c9ea fix: clear stale operation_id and guard async row selection 2026-07-23 13:00:10 +05:30
Mihir Kandoi
8cd36f60ab fix: prompt for operation row when operation repeats in work order 2026-07-23 12:52:27 +05:30
Mihir Kandoi
9b7c36f3d9 fix: do not translate default record lookup keys
Company.create_default_departments named and looked up the root
Department via _("All Departments"), which resolves in the session
language. A site set up in a non-English language stores the root
translated, and a company created later from a session in another
language misses it and inserts a second root, corrupting the tree.
Resolve the root once with get_root_of (falling back to the canonical
English name on fresh installs) and reuse it for the root record, the
exists-guard and the child departments' parent.

Also stop translating lookups of records install_fixtures stores under
English names: Price List "Standard Selling" and Print Headings
"Credit Note" / "Debit Note".

Same class as the root Item Group fix (#57386, issue #57345).
2026-07-23 12:51:30 +05:30
Mihir Kandoi
c93b63d757 Merge pull request #57383 from mihir-kandoi/company-restriction-permlevel
fix: gate company restriction fields behind permlevel 1
2026-07-23 12:40:16 +05:30
Mihir Kandoi
d71681a99e refactor: drop unused operation_row_number field from Job Card 2026-07-23 12:37:29 +05:30
Mihir Kandoi
92b62402be Merge pull request #57384 from pratheep-bit/fix/fr-corr-001-reorder-item
fix: log exception instead of swallowing in notify_errors
2026-07-23 12:34:24 +05:30
Jatin3128
a47f25896b feat: make Shipping Rule Cost Center optional with company default fallback (#57355)
feat(shipping-rule): make cost center optional with company default fallback

Cost Center on Shipping Rule is no longer mandatory. When left blank, the
applied shipping tax row falls back to the company default cost center,
avoiding the 'Cost Center is required for Profit and Loss account' error on
submit. The rule's project is also applied to the tax row.
2026-07-23 07:00:23 +00:00
Mihir Kandoi
4bb63cca10 test: probe permlevel visibility via a value field only
get_permitted_fieldnames never lists Table fields, so allowed_companies
cannot be asserted through it; the write-reset assertions already cover
that field.
2026-07-23 12:26:59 +05:30
Mihir Kandoi
41fc23b9b2 fix: drop Customer's unused level-1 Sales User read grant
Makes visibility uniform across the three masters: only the
master-manager role can see or edit company restriction fields.
2026-07-23 12:12:49 +05:30
Khushi Rawat
45553e46ec Merge branch 'develop' into fix/mt940-per-transaction-reference 2026-07-23 12:10:38 +05:30
srujan00123
551d709c64 fix: map MT940 per-transaction reference from :61: customer_reference
The mt940 library exposes ``transaction_reference`` from the :20: tag,
which is the statement-level reference and identical for every
transaction in a statement. The bank-statement-to-CSV conversion was
using it verbatim, so every imported row ended up with the same
reference, making reconciliation impossible.

Read the per-transaction reference from ``customer_reference`` on the
:61: tag instead. Handle two edge cases:

- **Overflow >16 chars.** When a bank emits a single-line :61: whose
  reference exceeds 16 characters, the mt940 regex splits the tail into
  ``extra_details``. Gate the rejoin to cases where
  ``customer_reference`` is exactly at the 16-char MT940 cap; below
  that, ``extra_details`` is genuine supplementary information and must
  not be appended.
- **``NONREF`` sentinel.** The MT940 standard marker for "no customer
  reference". Check it against the un-concatenated value so that a
  ``NONREF`` customer reference with populated ``extra_details`` still
  falls back to ``bank_reference`` instead of returning a junk
  ``NONREFsomething`` value.

Also switch the Description column to ``transaction_details`` (the :86:
tag content) so rows carry their real narrative instead of the mostly
empty :61: supplementary field.
2026-07-23 12:10:20 +05:30
Mihir Kandoi
565220ebe5 fix: gate company restriction fields behind permlevel 1
Only the master-manager role of each doctype (Item Manager, Sales Master
Manager, Purchase Master Manager) can view and edit restrict_to_companies
and allowed_companies. Customer reuses its existing level-1 permission
rows; Item and Supplier get a new level-1 row.
2026-07-23 12:07:16 +05:30
Nabin Hait
7b93252621 Merge pull request #57299 from nabinhait/feat/warn-existing-draft-links
feat: warn when a draft linked document already exists
2026-07-23 10:38:45 +05:30
Nabin Hait
7e0c81391b test: use a role-less user for the permission check
test@example.com carries System Manager in the frappe fixtures, so on a
fresh CI site it can read Delivery Note and the lookup legitimately
returns the draft. test1@example.com has no roles, making the
no-permission assertion environment-independent.
2026-07-23 10:27:41 +05:30
Pratheep Selvam
4830bc51c1 fix: log exception instead of swallowing in notify_errors 2026-07-23 09:23:39 +05:30
Mihir Kandoi
5dab7b9928 Merge pull request #57361 from mihir-kandoi/batch-prefill-stock-controller
refactor: move new-doc route options to StockController
2026-07-22 18:01:56 +05:30
Mihir Kandoi
551559e804 refactor: move new-doc route options to StockController
set_route_options_for_new_doc lived in TransactionController, so doctypes
extending StockController directly (Stock Reconciliation, Stock Entry) missed
the Batch/SABB prefill or duplicated it locally. Move it to StockController
and call it from onload_post_render so all descendants inherit it.

- Batch quick entry from Stock Reconciliation items now prefills Item
- SABB route options unified: warehouse || s_warehouse || t_warehouse,
  so transaction doctypes now also prefill warehouse
- Stock Entry's duplicate handler removed; its onload_post_render now
  calls super
2026-07-22 17:58:38 +05:30
Mihir Kandoi
3d46f7eba2 Merge pull request #57357 from mihir-kandoi/posting-datetime-desc
chore: better description for posting datetime for naming series field
2026-07-22 15:40:24 +05:30
Mihir Kandoi
dcfe1706db Merge pull request #57356 from mihir-kandoi/company-restriction-link-scan-perf
perf: scan link fields once per table in company restriction check
2026-07-22 15:34:49 +05:30
Mihir Kandoi
deafe79dc9 chore: better description for posting datetime for naming series field 2026-07-22 15:28:56 +05:30
Mihir Kandoi
a22b10e5eb perf: scan link fields once per table instead of per row
Meta.get_link_fields rescans the field list on every call; hoisting it
out of the row loop cuts the reference walk on a 100-row invoice from
2.8ms to 0.1ms.
2026-07-22 15:21:51 +05:30
Nabin Hait
01f7e45058 Merge branch 'develop' into feat/warn-existing-draft-links 2026-07-22 15:18:39 +05:30
Mihir Kandoi
ae9bb27436 Merge pull request #57353 from mihir-kandoi/fix/reserved-batch-precision
fix: get reserved batch qty precision from settings
2026-07-22 15:11:11 +05:30
Mihir Kandoi
2401b04090 fix: get reserved batch qty precision from settings 2026-07-22 14:58:02 +05:30
Mihir Kandoi
e147f6be98 Merge pull request #57352 from mihir-kandoi/company-restriction-transaction-enforcement
fix: enforce company restrictions at transaction level
2026-07-22 14:52:54 +05:30
Mihir Kandoi
e3ec8d2975 refactor: exempt system doctypes via in_create flag
Doctypes marked In Create (GL Entry, Stock Ledger Entry, Bin, ledger
entries) are system-created by definition, so derive their exemption
from meta instead of listing them.
2026-07-22 14:40:27 +05:30
Mihir Kandoi
1982816a70 refactor: enforce company restrictions on any doctype with a Company link
Replace the manually maintained transaction allowlist with a wildcard
validate hook: any doctype carrying a Company link field is checked, so
new doctypes are covered automatically. System-managed doctypes (ledger
entries, reposts, bundles, bins, POS consolidation, bank feeds) are
exempted so cancel, repost and reconciliation of documents created
before a restriction changed keep working; that guarantee is pinned by
a cancel-after-restriction test.
2026-07-22 14:36:48 +05:30
Mihir Kandoi
ce01fa0e34 fix: cover manufacturing, logistics, asset and service doctypes
Extend company restriction enforcement to the remaining user-entered
transactions (BOM, Work Order, Job Card, Production Plan, Pick List,
Blanket Order, asset and maintenance documents). Ledger and repost
doctypes stay excluded so cancelling or reposting older documents
keeps working after a restriction changes.
2026-07-22 14:28:13 +05:30
Mihir Kandoi
01892c2e26 refactor: hook validate_allowed_companies instead of calling per master
Item, Customer and Supplier each imported and called it in their
validate; register it once in doc_events instead.
2026-07-22 14:17:32 +05:30
Mihir Kandoi
88b8ce3888 fix: enforce company restrictions at transaction level
Restrict to Companies only filtered list views and document reads, and
only for users with Company user permissions. Any user could still use
a master restricted to Company A in a Company B transaction, and users
without Company user permissions bypassed the feature entirely.

Validate on save of transactions that every linked Item, Customer and
Supplier allows the transaction company, and filter item link queries
by the transaction company so restricted items don't show up in the
item selector.
2026-07-22 14:13:22 +05:30
Nabin Hait
c522a1fdae fix: disable pagination in draft link lookup
frappe.get_list defaults to 20 rows; child-table joins can produce
duplicate parent names that fill the window and hide further drafts.
Also clarify the check-ordering regression test.
2026-07-22 12:06:00 +05:30
Nishka Gosalia
eafd43769b Merge pull request #57333 from nishkagosalia/fix-settings-map
fix: settings map cleanup
2026-07-22 11:09:23 +05:30
Jatin3128
1029cd988a fix(accounts receivable): made territory field multi select (#57322) 2026-07-22 10:53:17 +05:30
Diptanil Saha
8cb96496ec fix(payments): ensure payments app installed on the site in payment_app_import_guard (#57342) 2026-07-21 21:21:40 +00:00
Diptanil Saha
6d31af3a52 chore: remove apiclient (#57339) 2026-07-21 20:15:25 +00:00
nishkagosalia
721fd56013 fix: settings map cleanup 2026-07-21 19:51:05 +05:30
Krishna Shirsath
f328018bfb fix(italy): skip e-invoicing for opening invoices 2026-07-21 16:48:25 +05:30
Mihir Kandoi
a008be7f0f Merge pull request #57316 from mihir-kandoi/fix-stock-ageing-reco-revaluation
fix: rescale stock ageing FIFO slot values on stock reconciliation
2026-07-21 15:45:51 +05:30
Mihir Kandoi
ed855c3823 fix: resolve float precision before streaming stock ledger entries
get_single_value inside _revalue_reconciled_batch_slots runs while
rows stream through the unbuffered cursor on MariaDB, killing the
active iterator. Resolve it once in generate() with the other
prefetches.
2026-07-21 14:34:08 +05:30
Shllokkk
4485179375 fix: show create payment entries as an inner button on row selection 2026-07-21 14:22:10 +05:30
Mihir Kandoi
7cba539cb0 fix: use system float precision for batch qty comparison 2026-07-21 14:18:19 +05:30
Mihir Kandoi
4cd1b6a8bf fix: revalue batch reco slots only when the entry covers the full batch
stock_value_difference / qty equals the new batch rate only when the
reco entry carries the entire batch, as the split out/in reco SLEs and
batches reconciled from zero do. Partial direct-batch_no entries mix a
qty delta with existing stock, so their slots keep prior values.

Plain items need no such guard: the valuation engine collapses the
FIFO stack to qty_after * valuation_rate on every reconciliation, so
rescaling remaining slots at the reco rate matches the ledger. Lock
that with a test.
2026-07-21 14:09:28 +05:30
Mihir Kandoi
3ce31be80a fix: rescale batch FIFO slot values on stock reconciliation
Batch items take the batch-slot path, which mirrors the same value
arithmetic: the reco's incoming entry dumps the revaluation remainder
on one slot. Rescale each reconciled batch's slots at its post-reco
rate (stock_value_difference / qty of the incoming bundle entry).
2026-07-21 13:49:21 +05:30
Mihir Kandoi
7a68e8bf4d fix: rescale stock ageing FIFO slot values on stock reconciliation
A reconciliation's stock_value_difference includes the revaluation of
stock already in the FIFO queue, but the whole amount was attached to
the qty-delta slot while older slots kept pre-revaluation values. A
downward revaluation therefore produced negative bucket values in the
Stock Ageing report, and repeated recos let the queue total drift away
from Stock Balance.

Re-derive every slot value as qty * valuation_rate after processing a
reco SLE, since a reconciliation values the entire balance at its rate.
Covers both single-SLE recos and the zero-out/re-add pair that flows
through the transfer bucket.
2026-07-21 13:44:36 +05:30
Shllokkk
98d58bcd6a fix: sync process loss percentage when fg qty changes (#57063) 2026-07-21 07:32:43 +00:00
Henil Maru
83e04dd773 fix: show transaction currency symbol in Payment Request schedule dialog and reference table (#57050)
* fix: show transaction currency symbol in Payment Request schedule dialog and reference table

When company currency (INR) differs from customer currency (USD), the Amount
column in the Select Payment Schedule dialog and the Payment Reference table on
the Payment Request form incorrectly displayed the company currency symbol (₹)
instead of the transaction currency symbol ($).

- Pass `currency` from the parent document on each schedule row returned by
  `get_available_payment_schedules` so the dialog can resolve the symbol.
- Add a hidden `currency` field to the dialog table and set `options: "currency"`
  on `payment_amount` so Frappe renders the correct symbol.
- Propagate `currency` into Payment Reference rows in `set_payment_references`.
- Add a hidden `currency` Link field to the Payment Reference child DocType and
  set `options: "currency"` on its `amount` field so the table renders correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: preserve currency when serializing payment schedule rows

get_available_payment_schedules set `schedule.currency` directly on
the Payment Schedule Document row, but `currency` isn't a field on
that DocType, so the API response serializer stripped it before it
reached the client. The Select Payment Schedule dialog and the
Payment Reference table therefore always fell back to the company
currency symbol, even with the earlier options="currency" changes in
place.

Convert each row to a plain dict via as_dict() first, then set the
currency key on the dict so it survives serialization.

* refactor: source schedule currency in dialog instead of API serializer

get_available_payment_schedules had to convert each child row with
as_dict() and re-attach currency, because currency is not a field on
Payment Schedule and the response serializer drops attributes set on the
Document itself.

The schedule dialog already has the transaction currency on frm.doc, so
set it there and let the API keep returning the schedule rows unchanged.
Payment Reference still stores currency per row.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jatin3128 <jatinsarna8@gmail.com>
Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-07-21 11:58:38 +05:30
Diptanil Saha
8327f19ebf ci: fix zh two_letters_code mapping (#57307) 2026-07-21 01:06:14 +05:30
MochaMind
ab6931279d fix: sync translations from crowdin (#57259) 2026-07-21 00:41:52 +05:30
Soham Kulkarni
9d5e3be0b3 Merge pull request #57305 from frappe/revert-57292-customer
Revert "fix: mark selling as default workspace for customer"
2026-07-21 00:01:04 +05:30
Soham Kulkarni
58b839eb71 Revert "fix: mark selling as default workspace for customer" 2026-07-20 21:44:49 +05:30
Mihir Kandoi
1b36459d2b Merge pull request #57301 from mihir-kandoi/stock-summary-bin-qty-fields
feat(stock): expose all Bin qty fields in Stock Summary and Stock Projected Qty
2026-07-20 20:17:25 +05:30
Mihir Kandoi
e146c318e5 Merge pull request #57300 from mihir-kandoi/bin-recalculate-values
feat: recalculate valuation rate and stock value from Bin
2026-07-20 20:10:07 +05:30
Mihir Kandoi
59c0c15c2e feat(stock): expose all Bin qty fields in Stock Summary and Stock Projected Qty
Stock Summary's sort selector only offered 5 of Bin's 10 qty fields; add
the rest (ordered, requested, planned, reserved for production plan,
reserved stock) and extend get_data's or_filters so bins whose only
nonzero qty is one of the new fields show up when sorted by it. Sort
labels now mirror Bin field labels.

Stock Projected Qty report had a column for every Bin qty field except
reserved_stock; add it.
2026-07-20 20:05:37 +05:30
Mihir Kandoi
49a43aad81 fix: keep Standard Cost stock value in step with the standard rate
Mirrors update_qty's Standard Cost handling and drops fixed test item
names so reruns start from fresh SLE-less items.
2026-07-20 19:59:13 +05:30
Mihir Kandoi
df79e85f53 feat: recalculate valuation rate and stock value from Bin
Renames the Recalculate Bin Qty button to Recalculate Values and sets
valuation_rate and stock_value from the last SLE (0 when none exists).
2026-07-20 19:48:10 +05:30
Nabin Hait
7d351153bb feat: warn when a draft linked document already exists
When creating a follow-up document (SO->DN, PO->PR, PI->Payment Entry,
etc.), warn the user if a draft of the target doctype already linked to
the source document exists, with links to the drafts and the option to
proceed anyway.

The target doctype comes from the make_mapped_doc response via the new
frappe.model.add_mapped_doc_guard hook, so every open_mapped_doc flow is
covered without per-doctype code or method-name inference. The server
lookup walks parent-level and child-table Link / Dynamic Link fields of
the target doctype and queries through frappe.get_list, so role and user
permissions apply and docstatus filtering happens in the query itself.
Payment Entry creation bypasses open_mapped_doc, so its controller runs
the same guard explicitly.
2026-07-20 18:37:24 +05:30
Jatin3128
b917aca361 refactor: clearer labels for the overdue billing control (#57298)
refactor: clearer labels and messages, drop "threshold" wording

User-facing text only, no field or behaviour changes:

- Accounts Settings toggle label -> "Restrict Customer Over Billing".
- Bypass role label -> "Role Allowed to Bypass Over Billing Restriction".
- Customer Credit Limit field label -> "Overdue Limit".
- Rewrote the descriptions and the block message to match and to stop
  saying "threshold".
2026-07-20 18:14:44 +05:30
Mihir Kandoi
f19216979b Merge pull request #57280 from aerele/timesheet_group_by
fix(report): handle nonetype error in timesheet billing summary group…
2026-07-20 17:43:31 +05:30
Deepesh Garg
3b10ff7df7 fix: Ignore permission while deleting user permission 2026-07-20 17:05:21 +05:30
Diptanil Saha
73004c6e4b refactor: rework appointment booking lifecycle and portal verification (#57270)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:35:58 +05:30
Soham Kulkarni
e728c24b80 Merge pull request #57292 from sokumon/customer
fix: mark selling as default workspace for customer
2026-07-20 16:06:09 +05:30
sokumon
873bce3c46 fix: mark selling as default workspace for customer 2026-07-20 15:53:41 +05:30
Poovetha
9a7209e668 fix(report): handle nonetype error in timesheet billing summary grouping logic 2026-07-20 13:39:07 +05:30
rohitwaghchaure
4cdaa8dba6 fix: block changing Stock account type when stock ledger entries exist (#57283) 2026-07-20 13:20:10 +05:30
Mihir Kandoi
276e688949 Merge pull request #57273 from aerele/typo-fix-allow-negative-stock
fix: correct typo in allow_negative_stock parameter
2026-07-20 12:31:13 +05:30
Nishka Gosalia
ccb9b16378 Merge pull request #57274 from nishkagosalia/gh-57206
fix: project % complete field allowing modification when manual method
2026-07-20 11:46:20 +05:30
nishkagosalia
21009c18c0 fix: project % complete field allowing modification when manual method 2026-07-20 11:34:09 +05:30
Afsal Syed
b3a616c328 fix: correct typo in allow_negative_stock parameter 2026-07-20 10:52:00 +05:30
rohitwaghchaure
2eecdc48bf feat: inline serial and batch entries editor (#57216)
* feat: inline serial and batch entries editor in Purchase Receipt

* feat: grid-style UX, deferred saves, scan and range options for inline serial batch editor

* feat: extend inline serial batch editor to all bundle doctypes with auto fetch

* fix: address review comments on inline serial batch editor

* fix: escape untrusted values in inline editor alerts

* fix: clear child bundle reference only when the row owns the bundle

* fix: keep inline serial batch editor disabled on existing sites via patch
2026-07-19 19:21:09 +05:30
MochaMind
ddb094084e chore: update POT file (#57269) 2026-07-19 14:50:34 +02:00
Nabin Hait
e932105ee3 fix(selling): recompute proforma amount for all rows on qty change
Refreshing a single grid row only updates the active row, so the derived amount
for rows after the edited one went stale. Recompute every row's amount from
qty x rate and re-render the grid.
2026-07-19 12:26:34 +05:30
Nabin Hait
654c9e6ad8 style(selling): form-builder layout tweaks and label rename
- Proforma Invoice form: place Grand Total beside Total Quantity (column break)
  and minor field reorder
- Rename the tab button to "New Proforma Invoice"
- Add the Proforma Invoice client-script scaffold
2026-07-19 11:25:40 +05:30
Nabin Hait
7314cedd53 feat(selling): surface proforma settings in Selling Settings tabs
- Move the Proforma Invoice settings section from the Subcontracting Inward tab
  to the Transaction tab
- List Proforma Invoice in the Document Naming tab so its naming series can be
  configured there
2026-07-19 11:21:41 +05:30
Mihir Kandoi
1cd32b9c73 Merge pull request #57258 from mihir-kandoi/per-master-company-restriction 2026-07-18 20:39:13 +05:30
Mihir Kandoi
ea5c648ab0 refactor: gate company restrictions per master via Restrict to Companies checkbox
Replaces the Global Defaults toggle. Each Item/Customer/Supplier now
carries a Restrict to Companies checkbox: the Allowed Companies table
only shows (and is mandatory) when checked, is cleared on uncheck, and
permission filtering, read denial and write validation apply only to
masters that have the checkbox set.
2026-07-18 19:03:31 +05:30
Mihir Kandoi
4d49cfa1de Merge pull request #57256 from mihir-kandoi/fix-multi-batch-serial-stock-reco
fix: scope current serial nos to the selected batch in stock reconciliation
2026-07-18 18:56:15 +05:30
Mihir Kandoi
f43f8f75d0 fix: scope current serial nos to the selected batch in stock reconciliation
get_stock_balance_for fetched serial nos across every batch in the
warehouse, so reconciling one batch of a serial+batch item compared the
selected serials against the pool of all batches and failed whenever
multiple batches existed.
2026-07-18 16:51:46 +05:30
Mihir Kandoi
71ccc8885e Merge pull request #57253 from aerele/fix-pick-list-work-order-transferred-qty-leak
fix: exclude transferred_qty from work order item to pick list item m…
2026-07-18 14:05:55 +05:30
pandiyan
5b36f12596 fix: exclude transferred_qty from work order item to pick list item mapping
get_mapped_doc copies same-named fields by default. work order item's
transferred_qty (cumulative across the whole work order) was leaking into
the new pick list item's transferred_qty (meant to track how much of
that pick list row has been converted into a stock entry, starting at 0).

the leaked value then got subtracted again in
get_pending_transfer_stock_qty(), so every pick list after the first
under-transferred raw materials by whatever was already recorded on the
work order, driving material_transferred_for_manufacturing towards zero
across repeated partial pick-list/finish cycles.

fixes #57236, related to #56596
2026-07-18 13:36:48 +05:30
Mihir Kandoi
ca5bec2b77 Merge pull request #57249 from mihir-kandoi/ppmr
fix: add fetch from in production plan material request child table
2026-07-17 22:19:58 +05:30
Mihir Kandoi
dfc2a411e1 fix: add fetch from in production plan material request child table 2026-07-17 22:08:15 +05:30
kaulith
1aee0df79a fix: force-delete repost data file during cleanup (#57245)
* fix(stock): force-delete repost data file during cleanup

* test(stock): cover repost data file cleanup with attach guard
2026-07-17 22:07:20 +05:30
Mihir Kandoi
e0896c656c Merge pull request #57244 from mihir-kandoi/fix-clear-old-logs-orphan-references
fix: clear linked comments, versions and attachments with old logs
2026-07-17 22:06:23 +05:30
Mihir Kandoi
6a69237130 Update erpnext/utilities/__init__.py
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-07-17 21:53:16 +05:30
Mihir Kandoi
334346e0f8 Merge pull request #57241 from mihir-kandoi/fix-material-request-buying-price-list
fix: validate buying price list on material request and update item rates on change
2026-07-17 21:38:10 +05:30
Mihir Kandoi
3a63f61832 chore: remove unneccessary flt 2026-07-17 21:24:40 +05:30
Mihir Kandoi
1887825ce5 fix: clear linked comments, versions and attachments with old logs
Repost Item Valuation and BOM Update Log cleared old logs with a raw
delete on the parent table, orphaning timeline comments, versions,
attachments and other reference records.

Fixes #57237
2026-07-17 21:24:08 +05:30
Mihir Kandoi
1ef3cd1d3f fix: dont overwrite rate with 0 if not found 2026-07-17 21:23:49 +05:30
Mihir Kandoi
a31119353c Merge pull request #57223 from aerele/project_validation
fix(projects): include on hold status in project filters and reports
2026-07-17 20:46:11 +05:30
Mihir Kandoi
6dcc0cab3a fix: pass ctx keys get_price_list_rate_for reads, skip rate update on insert
update_item_rates passed price_not_uom_dependent, a key
get_price_list_rate_for never reads, and omitted conversion_factor, so a
stock-UOM price was never converted to the row UOM. The function's
(historically misnamed) price_list_uom_dependant ctx key carries the
Price List's price_not_uom_dependent value: truthy returns the found
rate as-is, falsy multiplies by conversion_factor.

Also guard on_update with is_new(): has_value_changed returns True when
there is no doc_before_save, so every first save re-wrote item rates.
2026-07-17 20:44:07 +05:30
Mihir Kandoi
18b15f2ca9 fix: validate buying price list on material request and update item rates on change 2026-07-17 20:44:07 +05:30
Nabin Hait
2a9603fcd6 style(selling): organise Proforma Invoice form into sections
Group fields with section and column breaks: Details (two columns), Items with
right-aligned totals, Print Settings (two columns), and Status (two columns).
2026-07-17 20:12:37 +05:30
Nabin Hait
63607e91fd feat(selling): open the Proforma tab after creating a proforma
Flag the form on create and activate the Proforma tab once the reloaded form
has rendered the list, so the new proforma is shown immediately.
2026-07-17 20:04:18 +05:30
Nabin Hait
853c1d8986 feat(selling): add a New button below the proforma listing
Place a "+ New" button under the Proforma tab list to create another proforma
without leaving the tab.
2026-07-17 19:16:28 +05:30
Jatin3128
a33da337ec feat: block sales invoice submit when customer overdue exceeds threshold (#57230)
* feat: block sales invoice submit when customer overdue exceeds threshold

Adds an opt-in, per-customer Overdue Billing Threshold. When enabled in
Accounts Settings, submitting a Sales Invoice is blocked if the customer's
overdue amount exceeds their threshold, unless the current user holds a
configured bypass role. Modeled on the existing credit limit feature.

- Accounts Settings (Credit Limits tab): enable toggle + bypass role.
- Per-customer threshold on the Customer Credit Limit table, shown only
  when the feature is enabled via a property setter (same mechanism as
  subscription / accounting dimension sections). Table relabeled to
  "Credit & Overdue Limits".
- Overdue is read live from the ledger via get_outstanding_invoices
  (payments already netted), summing Sales Invoices past their due date.
- Enforced in Sales Invoice on_submit, after the credit-limit check;
  returns are exempt.
- validate_credit_limit_on_change no longer trips when a row sets only
  the overdue threshold (credit_limit = 0).

Fixes #52960

* fix: compute overdue amount in company currency and format with fmt_money

get_customer_overdue_amount now sums GL Entry debit - credit grouped per
invoice, which is always booked in company currency, instead of using
get_outstanding_invoices which returns the receivable-account currency.
The threshold is in company currency, so the previous comparison could mix
currencies for customers with a foreign-currency receivable account. This
mirrors how get_customer_outstanding computes the figure for the existing
credit-limit check.

The blocking message now formats both amounts with fmt_money using the
company currency.

Adds a test asserting a 100 USD invoice at a conversion rate of 50 is
counted as 5000 in company currency.

* refactor: drop redundant threshold coercion and dead test cleanup

- Coerce the overdue threshold with flt() once when reading it, instead of
  calling flt() on it at each of the three use sites.
- Remove a no-op set_overdue_billing_threshold() call in the feature-disabled
  block (the threshold was already set to that value) and the trailing reset,
  which is dead since each test is rolled back.

No behaviour change.

* fix: compute overdue amount from payment terms, matching the Overdue status

The overdue amount keyed on Sales Invoice.due_date, which set_due_date() sets
to the LAST payment term. An invoice whose first term was past due and unpaid
was therefore counted as zero, even though ERPNext already shows it as Overdue
in the invoice list. The gate and the UI could disagree.

get_customer_overdue_amount now follows the same rule as is_overdue(): per
invoice, the amount that has fallen due (sum of payment schedule terms past
their due date) minus what has been paid, clamped to the outstanding balance.
Invoices without a schedule (POS, opening) still fall back to the invoice due
date, mirroring is_overdue()'s own guard.

The ledger stays the source of truth for what is unpaid: the outstanding per
invoice is still SUM(debit) - SUM(credit) from GL Entry. base_payment_amount is
always stored in company currency, so no currency conversion is needed and the
comparison against the threshold stays consistent.

Adds a test covering a two-term invoice: only the past-due term counts, and
paying it off clears the overdue amount.

* feat: honour the overdue billing threshold set on the customer group

The threshold lives on Customer Credit Limit, which is also rendered on
Customer Group. A threshold set there was stored but never evaluated, so the
configuration was a silent no-op.

get_overdue_billing_threshold now reads the customer's row and falls back to
its customer group, mirroring get_credit_limit. The group's
bypass_credit_limit_check is deliberately not consulted: it is labelled for the
credit limit check at sales order and is unrelated to overdue billing.

get_customer_group_details also dropped the threshold when copying group rows
onto a customer, because it copied a single hardcoded field per table. It now
copies a list of fields per table, so credit_limit and overdue_billing_threshold
both carry over.
2026-07-17 18:58:10 +05:30
Nabin Hait
1da4657530 feat(selling): option to hide item qty on amount-based proforma print
Add a "Hide Item Quantity in Print" option (Amount basis only) that omits the
qty and rate columns from the printed proforma, for a clean value-based
document that shows only item and amount.
2026-07-17 17:44:04 +05:30
Nabin Hait
2242f1b230 feat(selling): make qty editable in amount-based proforma
In Amount basis, both qty and amount are now user-entered and the rate is
derived from them (rate = amount / qty). Previously qty was forced to the
ordered qty, which ignored an edited qty when switching basis.
2026-07-17 17:37:01 +05:30
rohitwaghchaure
40f861c0a0 fix: parallel reposting stalls between scheduler ticks (#57220) 2026-07-17 16:27:27 +05:30
Poovetha
7248961568 fix(projects): add project filter 2026-07-17 15:03:25 +05:30
Poovetha
79e5ccd370 test(projects): add test to ensure on hold project retains status 2026-07-17 15:03:25 +05:30
Poovetha
51a9fc0316 fix(projects): include on hold status in project filters and reports 2026-07-17 15:03:25 +05:30
Nabin Hait
77540403ab feat(selling): prefill remaining proforma qty/amount and reorder Create button
- Pre-fill each line with the remaining (ordered minus already proformed) qty
  and amount, so the default no longer trips the excess warning
- Place the Proforma Invoice action after the standard Create options
2026-07-17 14:11:26 +05:30
Mihir Kandoi
3f53475cad Merge pull request #57233 from mihir-kandoi/fix-stock-entry-pending-work-order-query
fix: replace column-literal work order filter with server-side query
2026-07-17 14:05:05 +05:30
Mihir Kandoi
eed7c98b30 fix: replace column-literal work order filter with server-side query
The work_order link filter in Stock Entry passed the string
`tabWork Order`.produced_qty as a filter value. It was never a real
column comparison: db_query coerces string values on numeric fields
with flt(), so the condition silently degraded to qty > 0, and on
backends that don't coerce text to numeric (postgres) such filters
fail with InvalidTextRepresentation.

Move the condition into a whitelisted search query that compares the
columns properly (qty > produced_qty), mirroring pick_list's
get_pending_work_orders.
2026-07-17 13:54:08 +05:30
Nabin Hait
4177101ac0 feat(selling): warn when total proforma exceeds the ordered qty/amount
Show a non-blocking notice below the item table when a line's total proforma
quantity (or amount) — this proforma plus already-issued ones — exceeds the
Sales Order line's ordered value. It updates live as the qty/amount or the
basis changes, and the user can still create the proforma.

Issued-proforma qty/amount are aggregated per line on demand (cancelled
proformas excluded); nothing is stored on the Sales Order.
2026-07-17 13:42:11 +05:30
Mihir Kandoi
d8eb029284 Merge pull request #57228 from mihir-kandoi/fix-transit-warehouse-company-fallback
fix: fall back to the company in-transit warehouse
2026-07-17 13:06:54 +05:30
Mihir Kandoi
920e64ded0 fix: fall back to the company in-transit warehouse
set_transit_warehouse re-tested from_warehouse inside a guard that
already requires it, so the Company branch of its ternary was
unreachable: with no default on the source warehouse the field stayed
empty and Company.default_in_transit_warehouse was silently ignored.
Try the warehouse's default first, then the company's.
2026-07-17 13:05:29 +05:30
Nabin Hait
f711375885 feat(selling): keep cancelled proformas visible with their PDF
Cancelling a proforma should void it, not erase history.

- Persist the Cancelled status on cancel (db_set) and keep the PDF attached
- Proforma tab now lists cancelled proformas with a red status badge, so the
  voided document and its PDF stay reachable for audit
2026-07-17 12:50:44 +05:30
Nabin Hait
473c655cb2 feat(selling): add amount-based proforma option
Let a proforma be created by editing item amount instead of quantity, for
value/advance-style proformas.

- "Based On" (Quantity | Amount) on the proforma and the create dialog
- Amount basis keeps the ordered qty and derives a rate so the line totals
  the entered amount; the PDF renders the same in-memory Sales Order copy
- Proforma Invoice Item now stores rate and amount
2026-07-17 12:50:44 +05:30
Nabin Hait
f9a09e1b3d refactor(selling): drop proforma quantity tracking
Remove the pending/proforma-qty machinery: it only fit staged, incremental
proformas and misrepresented the common whole-order / re-issued cases.

- Drop proforma_qty from Sales Order Item and its submit/cancel write-back
- Drop pending-qty aggregation and the over-qty soft warning
- The create dialog now pre-fills the ordered qty (editable down)
2026-07-17 11:51:03 +05:30
Nabin Hait
b0c53ac5a4 test(selling): add Proforma Invoice tests
Cover partial proforma being non-blocking on delivery/billing,
pending-qty aggregation with cancelled proformas excluded, tax scaling
to the partial qty, over-qty as a soft warning, and the settings gate.
2026-07-17 11:45:49 +05:30
Nabin Hait
fdc8879ce1 feat(selling): wire Proforma Invoice into Sales Order form
- Create > Proforma Invoice dialog with naming series, print format and
  letter head selectors, and an item-wise pending-qty grid
- Proforma tab listing issued proformas with inline view/email actions,
  shown only once at least one proforma exists
- Register the client script and add the connections dashboard link
2026-07-17 11:45:49 +05:30
Diptanil Saha
9fdb6dd875 Merge pull request #57224 from diptanilsaha/fix/dunning_type_validation
fix: added missing validations for `Dunning Type`
2026-07-17 10:45:40 +05:30
diptanilsaha
2325068b19 test: added tests for Dunning Type validation 2026-07-17 10:35:10 +05:30
diptanilsaha
c8e7674d63 fix: added missing validations for Dunning Type 2026-07-17 10:35:04 +05:30
Mihir Kandoi
0cba26c608 Merge pull request #57225 from mihir-kandoi/fix-duplicate-workspace-links
fix: remove duplicate links from home and projects workspaces
2026-07-17 08:05:53 +05:30
Mihir Kandoi
7fc43aba72 fix: remove duplicate links from home and projects workspaces
Workspace re-export in #56864 duplicated every link in the Home and
Projects workspaces, so desk renders each link twice. Same issue as
55afd95b20. Bumped modified so existing sites re-sync.
2026-07-17 07:55:17 +05:30
Mihir Kandoi
fc50e23daa Merge pull request #57194 from aerele/fix-operation-batch-size-flag-n-plus-one
fix: batch operation batch-size flag lookups to avoid n+1 query in wo…
2026-07-17 07:47:59 +05:30
pandiyan
6500752440 fix: batch operation batch-size flag lookups to avoid n+1 query in work order operations 2026-07-16 23:35:45 +05:30
MochaMind
2b4fc02c81 fix: sync translations from crowdin (#57188) 2026-07-16 18:01:04 +02:00
Shllokkk
76298d9bee Merge pull request #57198 from Shllokkk/strip-account-number-coa-importer
fix: strip account number when building account name in COA importer
2026-07-16 20:50:58 +05:30
Nishka Gosalia
5fc03a116a feat: map settings for DocTypes to show on settings dialog (#57025)
fix: mapping settings for DocType settings
2026-07-16 19:30:27 +05:30
Nabin Hait
65db3e374b feat(selling): add Proforma Invoice print format
Jinja print format on Sales Order, rendered against the in-memory
qty-adjusted copy so taxes and totals reflect the partial quantity.
2026-07-16 16:51:56 +05:30
Nabin Hait
07756f2bec feat(selling): add Proforma Invoice doctype and server API
- Submittable, non-accounting Proforma Invoice + Proforma Invoice Item
  child doctype (in_create; posts no GL/stock, stores item + qty only)
- Server API: pending-qty aggregation per Sales Order line (issued
  proformas only), make_proforma_invoice (sole creation path, gated on
  the settings toggle), PDF rendered from an in-memory qty-adjusted copy
  of the Sales Order and attached, send_proforma_email
- Non-blocking proforma_qty write-back to the Sales Order on submit/cancel
2026-07-16 16:51:48 +05:30
Nabin Hait
fbdc1f5b1f feat(selling): add proforma invoice settings and tracking field
- Selling Settings: "Enable Proforma Invoice" toggle (opt-in) and a
  default proforma print format
- Sales Order Item: non-blocking proforma_qty counter
2026-07-16 16:51:37 +05:30
Diptanil Saha
ac68db3fa6 refactor(dunning): converted get_dunning_letter_text to doc method and restrict_globals on render_template (#57205) 2026-07-16 16:06:13 +05:30
rohitwaghchaure
7b517a4e64 feat: book Expenses Added To Stock GL entries for stock vouchers (configurable) (#57190)
* feat: book Expenses Added To Stock GL entries for Stock Entry, Stock Reconciliation and LCV

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: make stock expense GL booking configurable via Accounts Settings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: skip stock expense booking for unconfigured companies, check flag once per compose

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 10:04:45 +00:00
Mihir Kandoi
a25acee43f Merge pull request #57179 from aerele/link-portal-users-to-contact-from-relevent-doctype
feat(stock): automatically link portal users to their associated contact profiles for customers and suppliers
2026-07-16 15:23:49 +05:30
Afsal Syed
9ae2069bd9 test(stock): add portal user contact link verification for customer and supplier 2026-07-16 15:11:28 +05:30
Afsal Syed
337a06dfb6 feat(stock): automatically link portal users to their associated contact profiles for customers and suppliers 2026-07-16 15:11:28 +05:30
Mihir Kandoi
103f3e50a7 Merge pull request #57202 from mihir-kandoi/pg-read-committed-gates
fix(stock): serialize postgres stock writes per (item, warehouse); block GL inserts during account rename
2026-07-16 14:10:56 +05:30
Mihir Kandoi
d0010fda68 Merge pull request #57204 from mihir-kandoi/fix-production-plan-min-order-qty
fix: consider min order qty in the purchase/transfer flow of production plan
2026-07-16 13:28:55 +05:30
Mihir Kandoi
448316fe8e test: assert row count in the min order qty split scenario 2026-07-16 12:56:11 +05:30
Mihir Kandoi
2f8d588093 fix: consider min order qty in the purchase/transfer flow of production plan
The transfer flow ignored Consider Minimum Order Qty twice: the JS
handler force-reset the checkbox before fetching items, and the
purchase remainder left after allocating transfers from other
warehouses was never raised to min_order_qty (the check runs on the
total requirement before the split).

Drop the JS reset and apply min order qty to the purchase remainder,
in stock UOM before the purchase UOM conversion.
2026-07-16 12:46:21 +05:30
Mihir Kandoi
b100e6d414 fix(stock): serialize pick list allocation per item on postgres
Two simultaneous allocations for the same item can both claim the same stock
on postgres: the picked-items locking read cannot see the rows another
in-flight creation is inserting, while MariaDB's gap locks make the creations
take turns. Advisory-gate set_item_locations per item (sorted against
deadlocks) so the second allocation waits, then subtracts the first's claim.
MariaDB unchanged.
2026-07-16 09:58:09 +05:30
Mihir Kandoi
897eca895a fix(stock): fall back gracefully when transaction_advisory_lock is unavailable
Same hasattr pattern as repost_gate: an ERPNext ahead of its frappe build keeps
the status-quo serialization-failure retries instead of failing every stock
submission on postgres.
2026-07-16 09:32:03 +05:30
Mihir Kandoi
35a9d7b09c fix(accounts): block GL Entry inserts during account rename on postgres
The for_update read in _ensure_idle_system only blocks new GL inserts on
MariaDB, via the gap lock it takes; a postgres row lock never blocks inserts,
so the guard silently degraded to the 5-minute recency check. LOCK TABLE IN
EXCLUSIVE MODE blocks writers (not readers) until the rename commits and NOWAIT
keeps the wait=False fail-fast, feeding the existing QueryTimeoutError path.
2026-07-16 09:21:17 +05:30
Mihir Kandoi
9cfdb482fc fix(stock): serialize stock writes per (item, warehouse) with a txn advisory lock on postgres
Postgres locking reads never see rows a concurrent transaction is inserting
(MariaDB's gap locks block the insert, then its locking reads return the fresh
row), so two concurrent writers for the same (item, warehouse) compute from the
same stale previous SLE and the loser overwrites Bin with a wrong absolute qty.
Today only the REPEATABLE READ serialization-failure retry catches this; the
gate makes correctness lock-based, covers the empty-history first-transaction
case (nothing exists to row-lock), and keeps negative-stock validation accurate
against concurrently inserted SLEs. Taken at the top of make_sl_entries (sorted
pairs, before the future_sle_exists cache warms) and in
update_entries_after.__init__ for the repost paths; re-entrant, released at
commit. MariaDB paths unchanged.
2026-07-16 09:21:07 +05:30
Shllokkk
6f6ec3072a Merge branch 'develop' into strip-account-number-coa-importer 2026-07-16 00:18:46 +05:30
Shllokkk
cbe406ee2a fix: strip account number when building account name in COA importer 2026-07-16 00:17:35 +05:30
Mihir Kandoi
7fe4dc1367 Merge pull request #56746 from frappe/mergify/configuration-deprecated-update
ci(mergify): upgrade configuration to current format
2026-07-15 12:53:07 +05:30
Mihir Kandoi
d5126dcad5 Merge pull request #56884 from aerele/fix/v16-report-date-guard
fix: validate mandatory date filters in reports
2026-07-15 12:51:29 +05:30
Diptanil Saha
fee3a6e0fd fix(accounts): update AU standard chart of accounts (#57145)
Co-authored-by: Jebajebas <jeba.j@arus.co.in>
2026-07-15 12:22:02 +05:30
Diptanil Saha
72b72a81fa fix(project): improved access control for project users (#56675)
* fix: permission check for `get_task_html` and `get_timesheet_html`

* fix(project): enabled project access control for users without `Projects User` Role

* fix(portal): validate user permissions for project portal

* fix: patch to add docshare for the project users

* fix(patch): selecting correct column on the query

* fix(project): grant access to all the current users for new project

* fix(portal): fixed condition to display timesheets on project

* test(portal): add access control tests for project user

* fix(project): using `frappe.has_permission` instead of `self.has_permission` to validate user permissions

* fix(project): granting docshare access for every ProjectUser

Roles for an User can be removed any time or an User Permission can be added which might restrict the access to the Project.

* fix(patch): create docshare documents for non-cancelled projects and users who have no docshare documents

* test(project): removed `test_control_access_does_not_touch_users_with_real_permission`
2026-07-15 12:20:27 +05:30
rohitwaghchaure
e99966a38e fix: skip redundant reposting of dependent items (#57092)
* fix: skip redundant reposting of dependent items

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: use earliest cascade datetime and batch repost item lookup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 12:09:34 +05:30
Mihir Kandoi
1d6edf9674 fix: name every conflicting voucher in the reserved batch error (#57174)
* fix: name every conflicting voucher in the reserved batch error

* fix: exclude fully-delivered reservations from the conflict message

* fix: round outstanding qty guard consistently with the conflict gate
2026-07-15 06:13:32 +00:00
Mihir Kandoi
67587a79e9 Merge pull request #57169 from mihir-kandoi/fix-shared-reserved-batch-delivery
fix: allow delivery when a batch is reserved across multiple sales orders
2026-07-15 11:10:58 +05:30
Mihir Kandoi
2310c4c005 fix: allow delivery when a batch is reserved across multiple sales orders
validate_reserved_batches compared the voucher's own qty against the
remaining batch qty, so delivering one order's reserved unit threw
Reserved Batch Conflict whenever the remainder exactly matched another
order's reservation. Compare the remaining batch qty against the
aggregated outstanding reserved qty (qty - delivered_qty) of other
vouchers instead, excluding reservations the voucher itself delivers.
2026-07-15 10:59:52 +05:30
Mihir Kandoi
576a5d26df Merge pull request #57137 from aerele/projects_status
feat: add on hold status to project
2026-07-15 10:51:28 +05:30
Mihir Kandoi
b6b19790d2 Merge pull request #57164 from mihir-kandoi/gh57158
fix: set correct currency in supplier quotation net rate field
2026-07-15 10:46:32 +05:30
Mihir Kandoi
117bb912cb Merge pull request #57163 from mihir-kandoi/hide-job-card-poi
fix: hide job card field in purchase order item
2026-07-15 10:42:58 +05:30
Mihir Kandoi
27672851cd fix: set correct currency in supplier quotation net rate field 2026-07-15 10:34:07 +05:30
Mihir Kandoi
f44bcae47d fix: hide job card field in purchase order item 2026-07-15 10:31:07 +05:30
Mihir Kandoi
71f843e5bf Merge pull request #57154 from aerele/fix-production-plan-bom-warehouse-n-plus-one
fix: batch BOM source warehouse lookup in get_production_items to avo…
2026-07-14 22:18:58 +05:30
pandiyan
4705909cee fix: batch BOM source warehouse lookups to avoid n+1 queries in production plan work order creation 2026-07-14 22:07:52 +05:30
Nabin Hait
04f75cc64f Merge pull request #56983 from nabinhait/fix-flaky-usd-exchange-rate-tests
test: seed current-dated USD↔INR exchange rate to fix flaky currency tests
2026-07-14 18:13:28 +05:30
Nabin Hait
5133ba47b7 fix: make currency exchange truly idempotent against any pre-existing state
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-07-14 18:02:59 +05:30
rohitwaghchaure
1fd2faa68d fix: permission issue (#57112) 2026-07-14 12:22:48 +00:00
Soham Kulkarni
4d2b603ba6 Merge pull request #57134 from sokumon/merge-workspaces
fix: merge erpnext workspaces
2026-07-14 17:10:28 +05:30
Poovitha Palanivelu
672fadaa78 feat: add on hold status to project 2026-07-14 16:43:32 +05:30
Mihir Kandoi
d7f4524cdd refactor: convert Hide Currency Symbol in Global Defaults to a Check field (#57135) 2026-07-14 11:10:07 +00:00
Mihir Kandoi
876adcb535 Merge pull request #57129 from SandraFrappe/fix/purchase-order-duplicate-material-request-item
fix: prevent duplicate material request items in purchase order
2026-07-14 16:31:26 +05:30
Mihir Kandoi
b2ec906ff3 test: remove test 2026-07-14 16:18:53 +05:30
sokumon
f2e8c7b664 fix: add sequence for erpnext 2026-07-14 16:05:44 +05:30
Mihir Kandoi
b6cce627a8 feat: company-wise restriction for Item, Customer and Supplier masters (#57124) 2026-07-14 10:30:47 +00:00
SandraFrappe
2d6f89a7f5 fix: prevent duplicate material request items in purchase order 2026-07-14 14:32:06 +05:30
sokumon
ac99d28100 chore: merge erpnext workspaces 2026-07-14 14:27:39 +05:30
Mihir Kandoi
823dbb7a2b Merge pull request #57127 from mihir-kandoi/naming-posting-date-default-on
feat: make naming series based on posting datetime on by default on n…
2026-07-14 13:49:34 +05:30
Mihir Kandoi
7db93d8b19 feat: make naming series based on posting datetime on by default on new sites 2026-07-14 13:38:41 +05:30
Diptanil Saha
d82e6c4f12 fix(accounts): added permission checks on get_account_balances_coa (#57107) 2026-07-14 07:26:43 +00:00
Smit Vora
7f4be47d43 Merge pull request #56912 from ljain112/refactor-subcon
refactor: move functionality in postprocess for mapped doc
2026-07-14 11:53:58 +05:30
Khushi Rawat
7371df129f Merge pull request #57111 from khushi8112/remove-dead-dashboard-fixtures
chore: remove dead assets dashboard_fixtures with broken imports
2026-07-14 11:53:07 +05:30
khushi8112
14a15cc6f9 chore: remove dead assets dashboard_fixtures with broken imports (#57079)
erpnext.accounts.dashboard_fixtures and erpnext.buying.dashboard_fixtures
were removed in 2020 when dashboards were exported to JSON fixtures.
The assets module's dashboard_fixtures.py was left behind unreferenced;
its dashboard, charts and number cards already exist as exported JSON.
2026-07-14 11:40:34 +05:30
Mihir Kandoi
f4f9030437 Merge pull request #57114 from frappe/mergify/bp/develop/pr-57113
fix(stock): set stock_uom on transferred Stock Reservation Entries (backport #57113)
2026-07-14 11:39:00 +05:30
Mihir Kandoi
1d5ec14530 Merge pull request #57116 from aerele/get_bundle_wise_serial_nos-db-params-limit
fix(stock): fix sqlparse token limit in get_bundle_wise_serial_nos
2026-07-14 11:29:34 +05:30
PranavDarade
e321e95e59 fix(stock): set stock_uom on transferred Stock Reservation Entries
StockReservation.transfer_reservation_entries_to() created the transferred SREs without copying stock_uom, in both the entries_to_reserve dict and the extra-items fallback. get_items_to_reserve() already selects the item's stock_uom, so entry.stock_uom is used.

On sites with a global default stock_uom (e.g. "Nos"), frappe's _set_defaults() backfilled the blank field, so the transfer silently stored the wrong UOM for any item whose stock UOM is not the default. On sites without that default the SRE's validate_mandatory() raised "Stock UOM is required", aborting Work Order submission for the Subcontracting Inward Order / Production Plan flows.

(cherry picked from commit 5991ecfa3d)
2026-07-14 11:27:07 +05:30
S Sakthivel Murugan
3e8784f596 fix: validate mandatory date filters in reports 2026-07-14 11:24:51 +05:30
Afsal Syed
e748bf512b test(stock): add unit test for get_bundle_wise_serial_nos query 2026-07-14 11:18:17 +05:30
Afsal Syed
4544a6c935 fix(stock): fix sqlparse token limit in get_bundle_wise_serial_nos 2026-07-14 11:18:17 +05:30
Mihir Kandoi
336970aa5a Merge pull request #57115 from mihir-kandoi/fix/supplier-scorecard-month-end-idempotency
fix: duplicate scorecard period when supplier is created on a month end
2026-07-14 10:55:59 +05:30
Mihir Kandoi
3aafda331b fix: duplicate scorecard period when supplier is created on a month end
make_all_scorecards' dedup query used strict bounds, so a single-day
period (supplier created on a month's last day -> start == end under
"Per Month") never matched its own window and was re-created on every
call. The daily refresh_scorecards job would insert a duplicate
submitted period each day for such suppliers, and
test_make_all_scorecards_is_idempotent fails on any date where
nowdate() - 75 days lands on a month end — both nightly server suites
went red on 2026-07-14 (75 days after April 30).

Inclusive bounds cannot false-match adjacent periods: each next period
starts at end_date + 1, so closed intervals never touch.
2026-07-14 10:20:41 +05:30
Mihir Kandoi
39b5c6ba2a Merge pull request #57101 from aerele/fix/job-card-work-order-transferred-qty
fix(manufacturing): preserve job card transferred quantity
2026-07-13 21:06:28 +05:30
Mihir Kandoi
3dba2d23b8 Merge pull request #57099 from aerele/fix-stock-entry-bom-query-redundancy
perf: avoid redundant bom cost_allocation_per query per finished item…
2026-07-13 21:05:13 +05:30
Mihir Kandoi
b23224b3d6 Merge pull request #57091 from aerele/fix/so-qty-company-warehouse
fix(stock): show qty (company) and qty (warehouse) in sales transactions
2026-07-13 21:03:07 +05:30
Mihir Kandoi
c0147e5a2e Merge pull request #57089 from aerele/fix-pick-list-barcode-scan-max-qty
fix: allow barcode scan to add and increment items in pick list
2026-07-13 21:02:08 +05:30
Sudharsanan11
51f9b70bfa test(manufacturing): cover transferred quantity across job cards 2026-07-13 19:40:20 +05:30
Sudharsanan11
951e432a1b fix(manufacturing): preserve job card transferred quantity 2026-07-13 19:40:20 +05:30
pandiyan
feb750f082 perf: avoid redundant bom cost_allocation_per query per finished item row in stock entry 2026-07-13 18:29:23 +05:30
Diptanil Saha
9c353741ad fix(tnc): using get_cached_doc to retrieve template for get_terms_and_conditions and permission checks (#57096) 2026-07-13 11:24:56 +00:00
Sudharsanan11
4e5e1f6596 test(stock): assert qty (company) and qty (warehouse) on item details
covers sales order, quotation, sales invoice, delivery note and
purchase order, asserting actual_qty from the row warehouse and
company_total_stock across all warehouses of the company.
2026-07-13 14:37:23 +05:30
Sudharsanan11
ab30bab6cb fix(stock): show qty (company) and qty (warehouse) in sales transactions
company was passed to get_bin_details only for purchase order, so
company_total_stock was never returned for sales order, quotation,
sales invoice and delivery note and the qty (company) column always
read zero. pass ctx.company for every doctype, which also drops the
dependency on doc being supplied.

on the client, set_actual_qty copied only actual_qty out of the
response, so qty (company) never refreshed on a warehouse change. use
frm.call with child so every bin field is applied, pass
include_child_warehouses to match the server, and include quotation.
2026-07-13 14:37:23 +05:30
pandiyan
3ece4a615d fix: allow barcode scan to add and increment items in pick list
- allow new rows on scan when pick manually is enabled, since only
  then are scanned rows not subject to being overridden by
  set_item_locations on save
- stop capping picked qty at the default demand qty (1) for rows
  added by the scanner itself, so repeat scans of the same barcode
  keep incrementing the row instead of failing with "maximum
  quantity scanned"
- ignore barcode uom when matching an existing row if new rows
  aren't allowed, since there's no alternate-uom row to fall back to
2026-07-13 13:54:54 +05:30
ruthra kumar
33abc53d7a Merge pull request #56817 from Soham-ambibuzz/philipinnes_localization_coa_v3
feat: restructure Philippines chart of accounts with amortization sup…
2026-07-13 12:56:31 +05:30
ruthra kumar
47341ff55c Merge pull request #56628 from aerele/multi-currency
fix(journal-entry): fetch outstanding on foreign currency
2026-07-13 12:35:15 +05:30
ruthra kumar
7144e5b29a Merge pull request #56458 from Shllokkk/psoa-restrict-jinja-globals
fix: restrict jinja globals in process statement of accounts templates
2026-07-13 12:34:04 +05:30
ruthra kumar
f0326aa6cf Merge pull request #56902 from frappe/fix-budget-variance-chart-month-shift
fix(budget-variance): correct month shift in comparison chart
2026-07-13 12:31:27 +05:30
Khushi Rawat
2580601937 Merge pull request #57071 from iamejaaz/fix-letterhead-company-guard
fix: guard company logo lookup in default letterheads
2026-07-13 11:58:31 +05:30
Khushi Rawat
14dac6ecc4 Merge pull request #55276 from aerele/fix/asset-repair-fully-depreciated
fix(asset): allow asset repair creation for fully depreciated assets
2026-07-13 02:09:13 +05:30
Mihir Kandoi
e8a61ecfbb Merge pull request #57073 from mihir-kandoi/gh57072
fix: make represents company field in purchase invoice ignore user pe…
2026-07-13 00:08:24 +05:30
Mihir Kandoi
6729a53fee fix: make represents company field in purchase invoice ignore user permissions 2026-07-12 23:56:23 +05:30
MochaMind
612a3f20d8 chore: update POT file (#57067) 2026-07-12 20:25:54 +02:00
Ejaaz Khan
e39ca72997 fix: set explicit table and logo widths in grey letterhead 2026-07-12 22:03:32 +05:30
Ejaaz Khan
23c09fe0f3 fix: guard company logo lookup in default letterheads 2026-07-12 21:17:42 +05:30
Nabin Hait
e1e56b6920 test: adjust currency tests for deterministic seeded exchange rate
Seeding a current-dated USD->INR rate makes get_exchange_rate resolve
62.9 on today() instead of hitting the live API, which exposed three
tests that implicitly relied on a different/undefined current rate:

- customer: dropped its own colliding current-dated seed (ignored via
  ignore_if_duplicate, and its cleanup deleted the shared seed) and now
  asserts the quotation resolves the seeded rate via get_exchange_rate.
- exchange_rate_revaluation: the revalued rate (62.9) is now below the
  booked 80, so the revaluation is a loss (debited) rather than a gain;
  derive the gain/loss column from the sign instead of assuming a gain.
- purchase_invoice: the receipt rate was an accidental tuple (70,) that
  got discarded and recomputed to the seed; set explicit rates with the
  receipt above the invoice so the stock exchange difference is a credit,
  matching the asserted column.
2026-07-12 21:12:29 +05:30
Nabin Hait
7928608b0f Merge pull request #56973 from nabinhait/refactor-stock-gl-composer
refactor(stock): de-conditionalize BaseStockGLComposer via subclass hooks
2026-07-12 20:32:49 +05:30
Mihir Kandoi
4a160c10f8 Merge pull request #57070 from mihir-kandoi/fix-dimension-search-check-field
fix: accounting dimension search matching unrelated records
2026-07-12 20:26:26 +05:30
Mihir Kandoi
38e8c3b1fe fix: accounting dimension search matching unrelated records
#56871 routed any searchfield without a DocField meta — including
"name", which get_search_fields() always appends — into the
`= cint(txt)` branch. For non-numeric search text cint() yields 0, so
`is_group = 0` (Cost Center) matched every leaf record on both
engines, and `name = 0` matched every non-numeric name on MariaDB.

Skip Check fields from or_filters entirely — a checkbox can't match
search text — and keep LIKE for everything else, including "name".
2026-07-12 18:37:51 +05:30
Mihir Kandoi
8db4f82af5 Merge pull request #57069 from mihir-kandoi/fix-job-card-pick-list-project
fix(stock): propagate project from job card to stock entry
2026-07-12 17:51:26 +05:30
Mihir Kandoi
72492f5da2 fix(stock): propagate project from job card to stock entry 2026-07-12 17:32:30 +05:30
mergify[bot]
45102e12cc feat: explain FIFO allocation of fixed Discount Amount on Sales Order (backport #56436) (#57062)
* feat: explain FIFO allocation of fixed Discount Amount on Sales Order (#56436)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
(cherry picked from commit 62fed1d562)

# Conflicts:
#	erpnext/selling/doctype/sales_order/sales_order.json

* chore: resolved conflicts

---------

Co-authored-by: Mohammad Umair Sayed <umair_sayyed@yahoo.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-11 17:57:49 +00:00
Raghav Ruia
b96d6e2a93 fix: remove incorrect Payable account_type from Customer Deposits in Philippines CoA (#57018) 2026-07-11 23:09:14 +05:30
Pandiyan P
ad17efe243 fix(accounts): retain invoice table on opening invoice creation error (#56353)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-07-11 13:17:28 +00:00
Pandiyan P
0524a235d7 fix: update events order by date asc (#56963)
Co-authored-by: nareshkannasln <nareshkannashanmugam@gmail.com>
2026-07-11 18:24:49 +05:30
Nikhil Kothari
d449ad3b3f fix(banking): allow negative balance in bank statement import (#56959) 2026-07-11 12:28:03 +00:00
Vishnu Priya Baskaran
9b66382463 fix(financial_statement): render columnar financial statements instea… (#56921) 2026-07-11 16:09:26 +05:30
Vishnu Priya Baskaran
a721ad3948 fix(payment reconciliation): read user permissions from current session user (#56782) 2026-07-11 16:08:27 +05:30
Vishnu Priya Baskaran
199eeff22c fix: map stock_qty in apply_price_list_on_item (#56869) 2026-07-11 15:03:49 +05:30
Nabin Hait
c5bc17b884 Merge branch 'develop' into fix-flaky-usd-exchange-rate-tests 2026-07-11 14:29:22 +05:30
Nabin Hait
df60d61402 Merge branch 'develop' into refactor-stock-gl-composer 2026-07-11 14:28:52 +05:30
Nabin Hait
0264202b42 Merge pull request #56978 from nabinhait/refactor-ac-services
refactor(accounts): extract deferred-accounting and document-schedule from AccountsController
2026-07-11 14:28:16 +05:30
MochaMind
4b6860de62 fix: sync translations from crowdin (#57010)
* fix: Swedish translations

* fix: Bosnian translations
2026-07-11 00:33:45 +02:00
Shllokkk
3cabf13abe Merge pull request #57045 from Shllokkk/work-order-mapper-method-path
fix: use correct mapper path for make_work_orders call
2026-07-10 21:01:06 +05:30
Shllokkk
394c9d80f9 fix: use correct mapper path for make_work_orders call 2026-07-10 20:59:08 +05:30
Mihir Kandoi
ad9430d17e Merge pull request #57035 from aerele/perf-bom-default-accounts-lookup
perf(bom): batch default account/cost-center/warehouse lookups in bom…
2026-07-10 17:15:01 +05:30
Vishnu Priya Baskaran
a30f72dae1 fix: fetch payment entry reference amounts from invoice (#56928) 2026-07-10 17:01:49 +05:30
Mihir Kandoi
51d7bf64a8 Merge pull request #57031 from mihir-kandoi/fix-job-card-pick-list-transfer
fix(stock): link job card in stock entry created from pick list
2026-07-10 16:54:31 +05:30
Mihir Kandoi
bf4e1a3ae0 Merge pull request #57033 from aerele/perf-bom-raw-material-warehouse-lookup
perf(stock): avoid n+1 queries for work order item source warehouse
2026-07-10 16:37:44 +05:30
Mihir Kandoi
2c03894e00 fix(stock): batch job card item lookup and honour semi_fg_bom
Fetch job_card_item for all pick list locations in one query instead
of one per row, and prefer the job card's semi_fg_bom over the work
order BOM, mirroring the direct Job Card -> Stock Entry mapper.
2026-07-10 16:36:44 +05:30
pandiyan
e88e63976a perf(bom): batch default account/cost-center/warehouse lookups in bom explosion 2026-07-10 16:35:43 +05:30
pandiyan
3e4d5e6745 perf(stock): avoid n+1 queries for work order item source warehouse
hoist the invariant work order lookup and batch-fetch work order item
source warehouses once instead of querying per raw material row in
get_bom_raw_materials
2026-07-10 16:03:18 +05:30
Mihir Kandoi
bdbb8481b0 fix(stock): link job card in stock entry created from pick list
A Stock Entry created from a Pick List against a job card's Material
Request never set job_card, job_card_item, fg_completed_qty or the
'Material Transfer for Manufacture' purpose, so the Job Card did not
recognize the transfer and blocked submission. The WIP warehouse was
also not populated.

Route such pick lists through a job-card-aware branch mirroring the
direct Material Request -> Stock Entry mapper, and set the purpose to
'Material Transfer for Manufacture' in the work order branch so the
WO -> MR -> Pick List flow updates the work order too.
2026-07-10 15:20:28 +05:30
ruthra kumar
4aabe3347f Merge pull request #56801 from ruthra-kumar/reversing_exchange_rate_revaluation
refactor: reversing exchange rate revaluation journals
2026-07-10 11:49:41 +05:30
Sudharsanan Ashok
9cb6610b9e fix(stock): correct stock ageing value for moving average and lifo items (#56693)
* fix(stock): recompute moving average item slots

* test(stock): add test to validate the stock value of moving average items

* fix(stock): support lifo valuation in stock ageing report

lifo items were aged as fifo (oldest consumed first), so the report kept the
newest lots on hand and reported the wrong stock value and average age. prefetch
each item's valuation method (it can't be resolved mid-stream without breaking the
unbuffered cursor) and consume from the tail for lifo items. also reuse that shared
lookup in the moving average revaluation pass. scoped to plain items; batch, serial
and same-voucher repack legs stay on fifo.

* test(stock): add test for lifo consumption in stock ageing report
2026-07-10 11:34:47 +05:30
ruthra kumar
65775e59a1 refactor(test): for reverse journals as well 2026-07-10 11:34:30 +05:30
ruthra kumar
6838242063 refactor: handle reverse ERR journals in AR / AP report 2026-07-10 10:38:54 +05:30
ruthra kumar
a0b14c0607 refactor: reversal capability on exchange rate revaluation 2026-07-10 10:38:52 +05:30
ruthra kumar
6a4c5b6062 refactor: add payment ledger to ignore link 2026-07-10 08:27:28 +05:30
Vishnu Priya Baskaran
38da3fc76e fix: display outstanding amount using company default currency (#56785)
Co-authored-by: S Sakthivel Murugan <s.sakthivelmurugan2003@gmail.com>
2026-07-10 00:57:20 +05:30
Mihir Kandoi
271f8cf1ca Merge pull request #56948 from aerele/feat/production-plan-group-rm-warehouse
feat(manufacturing): allow group warehouse for raw material availability in production plan
2026-07-09 20:52:18 +05:30
Mihir Kandoi
1eed00f5d4 Merge pull request #56428 from raghavisruia/item-allow-negative-stock-confirmation
feat: confirmation dialog when enabling negative stock on Item
2026-07-09 20:52:03 +05:30
Mihir Kandoi
4fbb20fbd9 Merge pull request #56255 from akhtarmohsin/fix-variant-of-filter-item-list
Fix Variant Of filter to show only template items
2026-07-09 20:41:54 +05:30
Mihir Kandoi
8583f0ba05 Merge pull request #56979 from harisansari008/fix/bom-routing-update-operations
fix: update BOM operations when routing is changed
2026-07-09 20:41:12 +05:30
Mihir Kandoi
1b6d3ee870 Merge pull request #56561 from aerele/fix/report-currency
fix: use company currency instead of global default in report
2026-07-09 20:38:55 +05:30
Sudharsanan11
2247ef9a50 test(manufacturing): add production plan group warehouse tests
verify child-stock aggregation, transfer sourcing from child warehouses,
that a for warehouse outside the group is rejected, and that a for
warehouse is required when raw materials are fetched.
2026-07-09 20:37:04 +05:30
Sudharsanan11
0574a0d95e feat(manufacturing): allow group warehouse for raw material availability in production plan
add an optional raw material group warehouse on production plan. when set,
raw material availability is checked across its child warehouses (bin rows
aggregated), while material is still received into for warehouse. for
warehouse is restricted to a child of the group and required when raw
materials are fetched; a group warehouse can never reach a material request
line. when the group is left blank, availability falls back to for warehouse
and the previous flow.
2026-07-09 20:37:04 +05:30
Sudharsanan11
b625525b03 fix(manufacturing): accept plain dict for doc in get_items_for_material_requests
the whitelisted endpoint typed doc as str | frappe._dict | Document, so a
json request body (a plain dict) failed pydantic type validation. widen to
str | dict | Document, matching the convention used elsewhere in erpnext.
2026-07-09 20:37:04 +05:30
Mihir Kandoi
87e7e53a6a Merge pull request #56932 from aerele/fix/work-order-planned-date-validation
fix: validate planned end date is not before planned start date in wo…
2026-07-09 20:34:57 +05:30
Mihir Kandoi
97533812e7 Merge pull request #56925 from aerele/fix/batch-bin-lookup-delivery-note
perf: batch bin lookups in delivery note stock update
2026-07-09 20:34:37 +05:30
Mihir Kandoi
508f03a73e Merge pull request #56923 from aerele/fix/reorder-item-warehouse-lookup-perf
perf: avoid per-row Warehouse doc fetches in auto reorder job
2026-07-09 20:33:56 +05:30
Mihir Kandoi
8dd56ecba5 Merge pull request #56913 from aerele/fix/trend-report-column-translations
fix: make trend report based-on and group-by column labels translatable
2026-07-09 20:33:13 +05:30
Mihir Kandoi
d6682703fb Merge pull request #56909 from aerele/fix/sync-variant-item-code-on-abbr-rename
fix(stock): rename variant item_code/item_name when attribute abbreviation changes
2026-07-09 20:32:50 +05:30
Mihir Kandoi
015fb61b10 Merge pull request #56984 from aerele/fix/apply-price-list-doc-dict
fix(stock): repair client calls broken by type-hint and module refactors
2026-07-09 20:31:24 +05:30
Sudharsanan11
4d629df299 fix(stock): point stock entry client calls at services module
The stock_entry_handler modules were moved to services; update the
retention, expired-batch and subcontract call paths in the client
so the whitelisted methods resolve again.
2026-07-09 18:52:31 +05:30
Sudharsanan11
5956d3e092 fix(stock): accept dict for doc arg in apply_price_list
The type-hint refactor rejected the doc dict sent by the form
controller; broaden the annotation to match ctx and fix the
cts= kwarg typo in transaction_base.
2026-07-09 18:48:01 +05:30
Nabin Hait
d387155e16 test: seed current-dated USD<->INR exchange rate in bootstrap
Tests that create USD documents dated today() (e.g. Sales Order in
test_advance_payment_ledger_entry, USD BOM in test_routing) rely on
get_exchange_rate() finding a USD->INR Currency Exchange record. The
only seeded records are dated 2016, so the lookup misses and falls back
to an external API that is blocked in CI, returning 0. That surfaces as
"Exchange Rate is mandatory" on Sales Order validation and a
ZeroDivisionError in BOM.get_routing (hour_rate / conversion_rate).

Whether it passes depends on which shard incidentally committed the
2016 records first, making it an order-dependent flake that unrelated
PRs trip by shifting test distribution.

Seed today()-dated USD<->INR rates once in BootStrapTestData so the
lookup resolves deterministically without the external API. Rates
mirror the latest Currency Exchange test_records to keep cost
calculations unchanged.
2026-07-09 18:26:14 +05:30
Mohd Haris
758a837de4 fix: update BOM operations when routing is changed
The routing field handler only fetched operations from the routing when
the operations table was empty. When a new BOM version is created (via
"New Version"), operations are copied from the source BOM, so selecting a
different routing left the old operations in place - both in the form and
after saving.

Drop the `!frm.doc.operations.length` guard from the routing handler so
that (re)selecting a routing always refetches the operations from that
routing via the existing get_routing method, which clears and repopulates
the operations table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:01:17 +05:30
Nabin Hait
174027bd57 refactor(accounts): extract deferred-accounting and document-schedule out of AccountsController
Continues the AccountsController service decomposition (Phase 5).

- Add accounts/services/deferred_accounting.py with DeferredAccountingService
  owning the deferred revenue/expense validations (income/expense account
  defaulting and service start/end date checks).
- Move the document-schedule orchestration (validate_all_documents_schedule
  and the invoice/non-invoice variants) into PaymentScheduleService, where
  they already delegated, removing the controller-to-service round trip.
- Update the three validate() call sites; keep
  validate_auto_repeat_subscription_dates on the controller (still called by
  buying/selling controllers).

No behavior change. accounts_controller.py 1818 -> 1745 lines.
2026-07-09 17:55:19 +05:30
Pandiyan P
8b3caeb578 fix(stock): pick list serial batch posting date (#56957)
* fix(stock): fall back to current date/time for serial and batch bundle posting datetime

Pick List has no posting_date/posting_time fields, so creating or updating a
Serial and Batch Bundle from a Pick List row crashed with
"TypeError: combine() argument 1 must be datetime.date, not None". Fall back
to today/now when the parent voucher doesn't carry its own posting date.

Fixes #56951

* fix(stock): accept a plain dict for add_serial_batch_ledgers' doc and child_row

The whitelisted add_serial_batch_ledgers only converted child_row into an
attribute-accessible frappe._dict when it arrived as a JSON string, and doc's
type hint only allowed Document | str. Frappe's JSON API delivers both as
plain dicts (see frappe.app.make_form_dict, which parses the request body
with orjson and only wraps the top-level dict, not nested values), so every
real request was rejected before the handler body ever ran: first with a
FrappeTypeError on doc, and once that's fixed, with an AttributeError on
child_row.serial_and_batch_bundle. parse_json already wraps a plain dict in
frappe._dict (and leaves a real Document instance untouched), so routing
child_row through it unconditionally fixes both.
2026-07-09 17:28:03 +05:30
Pandiyan P
e2178a5f19 feat(manufacturing): create material request for raw materials from work order (#56961)
* feat(manufacturing): create material request for raw materials from work order

allow raising a material transfer request directly from a work order,
mirroring the existing job card flow, so stores can fulfil it into wip
before the actual stock entry happens

* test(manufacturing): cover work order material request flow

verify the material request created from a work order carries the
right bom/purpose onto the resulting stock entry, and that the work
order status still moves to in process on a partial material-request
transfer
2026-07-09 17:27:21 +05:30
Nabin Hait
798680d2d5 refactor(stock): de-conditionalize BaseStockGLComposer via subclass hooks
Remove per-doctype branching from the shared stock GL composer so each
voucher owns its own behavior:

- Move the Stock Reconciliation voucher-detail synthesis out of
  BaseStockGLComposer.get_voucher_details into a
  StockReconciliationGLComposer.get_voucher_details override.
- Replace the hardcoded doctype allow-list in check_expense_account with
  an overridable class attribute enforce_pl_expense_account (default
  True). Vouchers that post the difference to a balance-sheet account
  (Stock Entry, Stock Reconciliation, Delivery Note) set it False.
- Add DeliveryNoteGLComposer to own the P&L-exempt rule and wire
  DeliveryNote.get_gl_entries to it.

No change to GL output; behavior is relocated, not altered.
2026-07-09 17:25:32 +05:30
mergify[bot]
d418dd9e70 fix(patch): moved create_company_custom_fields from pre_model_sync to post_model_sync (backport #56962) (backport #56965) (#56970)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-07-09 10:36:50 +00:00
Diptanil Saha
1885f9fb35 Merge pull request #56674 from diptanilsaha/fix/frappe_crm_sync
fix(crm_settings): skip allowed users check when frappe crm is installed locally
2026-07-09 15:30:46 +05:30
MochaMind
49228b458d fix: sync translations from crowdin (#56943) 2026-07-09 15:30:26 +05:30
Khushi Rawat
e3fb1340e1 Merge pull request #56964 from khushi8112/fix/depr-schedule-precision-match
fix: match depreciation schedule rows at currency precision to avoid duplicate JEs
2026-07-09 14:43:13 +05:30
khushi8112
947ed5dfe1 fix: match depreciation schedule rows at currency precision to avoid duplicate JEs 2026-07-09 14:14:44 +05:30
diptanilsaha
0f987d7135 chore: patch to clear out allowed users on crm_settings if frappe crm is installed on the site 2026-07-09 12:37:47 +05:30
diptanilsaha
2de423e225 fix(frappe_crm_api): handle failure for after_app_install and after_app_uninstall 2026-07-09 12:37:47 +05:30
diptanilsaha
c86aa2d6fe feat(crm_settings): auto-update crm sync settings on frappe crm install and uninstall 2026-07-09 12:37:41 +05:30
diptanilsaha
41badb3d74 fix(crm_settings): skip allowed users check when frappe crm is installed locally 2026-07-09 12:01:49 +05:30
Abdeali Chharchhodawala
95e2ce6d85 feat: add grouping by dimension functionality in financial reports (#54650)
Co-authored-by: Copilot <copilot@github.com>
2026-07-08 19:04:09 +05:30
Raffael Meyer
c12e3fba5e feat(sla): filter service level agreement link by document type (#56954) 2026-07-08 13:47:57 +02:00
Smit Vora
e7f81de159 Merge pull request #54855 from Abdeali099/growth-view-with-cfs-54675
refactor(financial-report): fix row transformation for growth calculations
2026-07-08 16:13:44 +05:30
Abdeali Chharchhoda
aad287d09e fix: enhance growth view filtering by validating period keys 2026-07-08 16:03:28 +05:30
Abdeali Chharchhoda
4c7499600c fix: update formatting of growth view for FS report 2026-07-08 16:03:28 +05:30
Abdeali Chharchhoda
698876672d refactor: simple utility for growth value computation for custom FS report 2026-07-08 16:03:28 +05:30
Abdeali Chharchhoda
c179460c98 refactor(financial-report): fix row transformation for growth calculations 2026-07-08 16:03:28 +05:30
Diptanil Saha
95212e5738 fix(tnc): get_terms_and_conditions render_template with safe_exec (#56944) 2026-07-08 00:18:35 +00:00
Diptanil Saha
8a940af7e1 fix: added permission checks on various whitelisted functions (#56745)
* fix: added permission checks on various whitelisted functions

* fix: permission checks on `get_party_account` and using "select" `ptype`

* test(`BootStrapTestData`): assign `Accounts User` role to User `test@example.com`
2026-07-08 05:29:52 +05:30
Shllokkk
c94973a932 Merge pull request #56926 from Shllokkk/pricing-rule-template-variant-validation
fix: validate template and its variant in the same Pricing Rule
2026-07-07 19:59:10 +05:30
Dany Robert
be10c8ced9 fix: precision issue causing reconciliation error (#54043)
* fix: precision issue causing reconciliation error

* chore: code styling changes

* test: precision causing reconciliation failure

* fix: enhance payment reconciliation tests for floating-point precision

* fix(test): incorrect assertion on status

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2026-07-07 19:44:35 +05:30
pandiyan
2ec780cb35 fix: validate planned end date is not before planned start date in work order 2026-07-07 16:34:58 +05:30
MochaMind
eb37d3b14b fix: sync translations from crowdin (#56919) 2026-07-07 12:59:22 +02:00
Shllokkk
a88048b378 fix: validate template and its variant in the same Pricing Rule 2026-07-07 13:55:22 +05:30
pandiyan
5da878d25f perf: batch bin lookups in delivery note stock update
update_current_stock() in delivery_note.py used to call
frappe.db.get_value("Bin", ...) separately for every row in items and
every row in packed_items - so a delivery note with 200 items and 200
packed items made 400 separate database calls on every save.

now it groups item codes by warehouse and fetches bin data with one
query per distinct warehouse, then assigns actual_qty/projected_qty to
each row from that result - same values as before, far fewer database
calls, and no cross-product over-fetch across warehouses.
2026-07-07 13:00:08 +05:30
pandiyan
6beb3d2509 perf: avoid per-row Warehouse doc fetches in auto reorder job
get_item_warehouse_projected_qty ran an uncached frappe.get_doc per Bin
row to walk the warehouse parent chain, re-fetching the same ancestors
for every item sharing a warehouse. Preload the warehouse parent map
once and walk it in memory instead.
2026-07-07 11:43:52 +05:30
pandiyan
015fa68fc0 fix: make trend report based-on and group-by column labels translatable
based_wise_columns_query() and group_wise_column() in trends.py built
column labels as raw strings, so "Item", "Item Name", "Customer",
"Supplier", "Territory", "Currency", etc. never went through _() and
stayed in English regardless of the user's language, unlike the
period and total columns right next to them which were already
wrapped correctly.
2026-07-06 18:41:06 +05:30
S Sakthivel Murugan
b72ecdda0d test: add regression test for trends chart total row 2026-07-06 17:54:25 +05:30
S Sakthivel Murugan
e6f9149ad7 fix: use company currency instead of global default in report 2026-07-06 17:54:25 +05:30
ljain112
0691c7c7bc refactor: move functionality in postprocess for mapped doc 2026-07-06 17:41:56 +05:30
rohitwaghchaure
607f0e943f fix: workspace for stock and manufacturing (#56906) 2026-07-06 11:48:15 +00:00
MochaMind
be93530b5f fix: sync translations from crowdin (#56866) 2026-07-06 10:49:55 +02:00
pandiyan
e718a70b26 test: cover variant item_code/item_name rename on abbreviation change
Add regression coverage for the new abbreviation-rename propagation:
a simple item_code rename, item_name derived from a template whose
item_name differs from its item_code, and a manually customized
item_name getting rebuilt rather than left stale.
2026-07-06 14:15:40 +05:30
pandiyan
c0cfe5f363 fix: rename variant item_code/item_name when attribute abbreviation changes
Item Attribute abbreviations only got baked into a variant's item_code
and item_name at creation time (make_variant_item_code returns early
once item_code is set). Renaming an abbreviation afterwards left every
existing variant stuck with the stale code, silently out of sync with
its own attribute.

Detect abbreviation renames on Item Attribute save, find every variant
using the affected value, and rebuild+rename its item_code via
frappe.rename_doc so linked records follow along. item_name is rebuilt
in lockstep from the template's item_name, even if it had since been
customized, since both fields are meant to be derived from the same
abbreviation.
2026-07-06 14:15:29 +05:30
S Sakthivel Murugan
c7774a95e5 fix(asset): allow asset repair creation for fully depreciated assets 2026-07-06 14:13:06 +05:30
MochaMind
0688cedba2 chore: update POT file (#56899) 2026-07-05 20:29:59 +02:00
Mohsin Akhtar
41c00de4d3 Merge branch 'develop' into fix-variant-of-filter-item-list 2026-07-05 23:27:46 +05:30
Mohsin Akhtar
54da9fc27a fix: update modified timestamp in item.json 2026-07-05 23:26:06 +05:30
Mihir Kandoi
8aeca5922c Merge pull request #56905 from mihir-kandoi/pg-lock-races-and-advisory-valuation
fix(stock): close postgres locking races; gate batch valuation with a txn advisory lock
2026-07-05 22:42:18 +05:30
Mihir Kandoi
2ec469257e perf(stock): gate batch valuation with a txn advisory lock on postgres
On postgres, outward batch valuation row-locked the item's ENTIRE SLE / Serial and Batch Entry history (a separate plain SELECT ... FOR UPDATE per site, since FOR UPDATE is invalid with GROUP BY there). That writes a lock marker (xmax + WAL) on every historical tuple per outward movement - write amplification that grows with history forever - and locks nothing at all when the history is empty (negative-stock edge: two concurrent outwards don't serialize).

Replace all four history-wide postgres lock statements with one frappe.db.transaction_advisory_lock(("batch-valuation", item_code, warehouse)) at the top of BatchNoValuation.calculate_avg_rate's outward branch - every valuation read (bundle + the three deprecated paths) is downstream of it. Released at commit/rollback, participates in deadlock detection, and serializes regardless of history size, so the empty-history edge is closed by construction. Batch qty updates iterate in sorted order so concurrent vouchers lock Batch rows in the same sequence.

MariaDB is unchanged: it keeps the original grouped FOR UPDATE row locks (and its gap locks). Requires frappe#40621.

Test: outward delivery of a batched item must leave the xact advisory lock visible in pg_locks for the submitting transaction.
2026-07-05 22:31:45 +05:30
Mihir Kandoi
db58858c68 fix(stock): close postgres lock-then-read races in pick list and stock reservation
Postgres has no gap locks, so the lock-then-read pattern (plain SELECT ... FOR UPDATE before a grouped read) only serializes on rows that already exist. Two sites had reachable races where the lock set is empty or disjoint:

- Pick list: two pick lists against the same SO item submitted concurrently lock only docstatus=1 rows, so with no previously-submitted picks their lock sets are disjoint and both pass validate_picked_qty (over-pick; picked_qty last-writer-wins). Gate on the referenced Sales Order Item / Packed Item rows, which always exist.
- Stock reservation: the first concurrent reservations for an (item, warehouse) find no SRE rows to lock, so both pass and reserved qty can exceed actual. Gate on the Bin row, which exists once there is stock.

MariaDB is unchanged (its gap locks already serialize both; the gates are postgres-only). Also: ORDER BY on the small-set postgres lock selects for deterministic lock order, and the repost pre-lock in get_future_stock_vouchers selects a constant instead of shipping every matching SLE name to the client.
2026-07-05 22:31:29 +05:30
Nabin Hait
8abcb7decc Merge pull request #56301 from frappe/chore/payment-entry-purchase-coverage
test: purchase-side Payment Entry allocation coverage
2026-07-05 19:39:38 +05:30
Mihir Kandoi
c68bf89924 Merge pull request #56903 from mihir-kandoi/pg-second-order-groupby-docs
docs: catalog second-order GROUP BY traps (wrap-is-the-bug classes)
2026-07-05 19:11:14 +05:30
Mihir Kandoi
0922d85bf0 docs: catalog second-order GROUP BY traps (wrap-is-the-bug classes)
A full audit of the loose-GROUP-BY fixes found four recurring mistakes in the fixes themselves: incoherent Max/Min pairs over coupled columns, NULL-skipping Max on discriminators, Sum(x)*Max(y) fabricated arithmetic, and wrong-bound picks. Add them to the compatibility catalog (new 3.1) and to the Greptile review instructions so future PRs get flagged.
2026-07-05 19:00:47 +05:30
Mohd Haris
48418eadb0 fix(budget-variance): correct month shift in comparison chart
The Budget Variance Report chart plotted the actual expense one month
earlier than the table (e.g. July actual shown under June).

build_comparison_chart_data() collected budget columns using
fieldname.startswith("budget_"). The dimension column "budget_against"
also matches that prefix, so it was added as an extra leading entry to
budget_fields and labels, while actual_fields had no such leading entry.
This shifted every actual value one position ahead of its label.

Skip the "budget_against" dimension column so budget/actual values and
labels stay aligned per month.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:06:43 +05:30
rohitwaghchaure
7844905e21 fix: dark theme shop floor (#56887) 2026-07-05 16:33:08 +05:30
Mihir Kandoi
00e6b44701 Merge pull request #56897 from mihir-kandoi/pg-requested-items-required-date
fix(buying): show earliest schedule date as required date
2026-07-05 15:45:52 +05:30
Mihir Kandoi
e3a38e3733 Merge pull request #56896 from mihir-kandoi/pg-production-planning-arrival-qty
fix(manufacturing): scope Production Planning arrival qty to open POs
2026-07-05 15:43:04 +05:30
Mihir Kandoi
1d7bb19620 Merge pull request #56898 from mihir-kandoi/pg-trends-empty-row-guard
fix(controllers): guard empty group-by detail row in trends
2026-07-05 15:39:53 +05:30
Mihir Kandoi
8486578c05 Merge pull request #56894 from mihir-kandoi/pg-procurement-tracker-coherent-row
fix(buying): make Procurement Tracker rows coherent PO lines
2026-07-05 15:39:33 +05:30
Mihir Kandoi
df82c7ac1c Merge pull request #56893 from mihir-kandoi/pg-bom-groupby-phantom-pair
fix(manufacturing): keep bom_no/is_phantom_item pair coherent in get_bom_items_as_dict
2026-07-05 15:38:29 +05:30
Mihir Kandoi
4cb622b8e3 Merge pull request #56895 from mihir-kandoi/pg-budget-requested-amount-per-row
fix(controllers): compute budget requested amount per row
2026-07-05 15:37:56 +05:30
Mihir Kandoi
a5d0b25ac4 test: assert earliest schedule date wins for duplicate MR item rows 2026-07-05 15:32:02 +05:30
Mihir Kandoi
2a6bc517bc fix: exclude fully received PO lines from arrival qty and date 2026-07-05 15:29:18 +05:30
Mihir Kandoi
5d56a04a84 fix(controllers): guard empty group-by detail row in trends
With an explicit GROUP BY, a zero-match detail query returns no rows (the old bare aggregate returned one all-NULL row), so row1[0][0] would raise IndexError. Skip the entry instead.
2026-07-05 15:10:32 +05:30
Mihir Kandoi
b5aee6a9cd fix(buying): show earliest schedule date as required date
When a Material Request lists the same item on multiple rows, the consolidated row showed Max(schedule_date), understating urgency. Use Min - the earliest date the item is needed.
2026-07-05 15:10:17 +05:30
Mihir Kandoi
efd777a1a8 fix(manufacturing): scope Production Planning arrival qty to open POs
arrival_qty summed the all-time ordered qty of every submitted PO line for the item+warehouse, so it grew monotonically with purchase history. Sum the pending qty (qty - received_qty) on open POs instead, and take the earliest schedule date from the same scope.
2026-07-05 15:10:03 +05:30
Mihir Kandoi
e52b1d6cff fix(controllers): compute budget requested amount per row
get_requested_amount multiplied the pooled pending qty of all matching Material Request items by a single Max(rate), fabricating the total whenever rates differ and biasing it upward - making false Budget Exceeded stops more likely. Sum (stock_qty - ordered_qty) * rate per row instead, matching get_ordered_amount.
2026-07-05 15:09:49 +05:30
Mihir Kandoi
39d2a62692 fix(buying): make Procurement Tracker rows coherent PO lines
get_po_entries aggregated every non-key column with Max() over (PO, material_request_item), which could stitch values from different PO lines into a row that never existed (one line's item_code with another's qty and amount). Select one representative line per group instead: a subquery picks Min(child.name) per group under the same filters and the outer query reads all columns bare from that line. Row count is unchanged.
2026-07-05 15:09:21 +05:30
Mihir Kandoi
db5ab9fb35 fix(manufacturing): keep bom_no/is_phantom_item pair coherent in get_bom_items_as_dict
When a BOM lists the same item twice (one line phantom via sub-BOM P, one non-phantom via sub-BOM N), grouping by item_code and aggregating bom_no and is_phantom_item with independent Max() could pair one line's phantom flag with the other line's bom_no, exploding the wrong sub-BOM or silently dropping a direct requirement. Group by the pair instead so each line keeps a coherent (bom_no, is_phantom_item); the consumer accumulates duplicate keys and explodes phantom rows with their own qty. Same fix as 41da9eb7fc, which missed this single-level path.
2026-07-05 15:08:58 +05:30
Nabin Hait
17851ceaae Merge pull request #56877 from frappe/chore/test-appointment-booking-settings
test: add coverage for Appointment Booking Settings
2026-07-05 13:16:15 +05:30
Nabin Hait
d2f0ec883e Merge pull request #56880 from frappe/chore/test-email-campaign
test: add coverage for Email Campaign
2026-07-05 13:16:06 +05:30
Nabin Hait
86435bc961 Merge pull request #56891 from frappe/fix/company-coa-test-abbr-collision
test: fix flaky test_coa_based_on_country_template (abbreviation collision)
2026-07-05 12:57:43 +05:30
Nabin Hait
b418df595e Merge branch 'develop' into chore/test-appointment-booking-settings 2026-07-05 12:50:53 +05:30
Nabin Hait
70e1acdb6d Merge branch 'develop' into chore/test-email-campaign 2026-07-05 12:50:47 +05:30
Nabin Hait
2959bfcafa Merge pull request #56888 from frappe/chore/test-lower-deduction-certificate
test: add coverage for Lower Deduction Certificate
2026-07-05 12:50:05 +05:30
Nabin Hait
94afc2a05b Merge pull request #56889 from frappe/chore/test-italy-utils
test: add coverage for Italy e-invoice utility helpers
2026-07-05 12:49:54 +05:30
Nabin Hait
e82a9b9c3b Merge pull request #56890 from frappe/chore/test-import-supplier-invoice
test: add coverage for Import Supplier Invoice
2026-07-05 12:49:31 +05:30
Nabin Hait
df4fc6b186 test: use a unique company abbreviation to fix COA-template flake 2026-07-05 12:44:31 +05:30
Nabin Hait
e86f6fc89c Merge pull request #56849 from frappe/chore/test-stock-closing-entry
fix: Stock Closing Entry duplicate check misses contained date ranges
2026-07-05 12:18:35 +05:30
Nabin Hait
11023511ae Merge pull request #56856 from frappe/chore/test-job-card-dark-paths
test: cover Job Card quantity, docstatus and capacity logic
2026-07-05 12:18:23 +05:30
Nabin Hait
58ee02780f Merge pull request #56883 from nabinhait/sherlock/fix-dunning-outstanding-currency
fix: use transaction-currency outstanding on Dunning for foreign-currency invoices
2026-07-05 12:17:27 +05:30
Nabin Hait
8d289dfe52 Merge pull request #56881 from frappe/chore/test-campaign
fix: Campaign with a naming series breaks the UTM Campaign link
2026-07-05 12:16:50 +05:30
Nabin Hait
616ceb8126 test: add coverage for Import Supplier Invoice validation and country lookup 2026-07-05 12:16:09 +05:30
Nabin Hait
f2f1b2597d test: add coverage for Italy e-invoice utility helpers 2026-07-05 12:13:48 +05:30
Nabin Hait
c293cb8871 test: add coverage for Lower Deduction Certificate date validation 2026-07-05 12:11:34 +05:30
rohitwaghchaure
29be72fae4 fix: warning message for new item standard cost (#56885) 2026-07-04 23:29:32 +05:30
Nabin Hait
a9bb6b31df fix: use transaction-currency outstanding on Dunning for foreign-currency invoices
When a Sales Invoice is in a foreign currency (e.g. USD) but the receivable
account is in the company currency (e.g. INR), `outstanding_amount` on the
invoice is stored in the party account currency (INR). `postprocess_dunning`
was copying that value directly into the Dunning's Overdue Payment row, which
is expected to carry the transaction-currency (USD) amount.

The fix: when `party_account_currency != currency`, use
`payment_schedule[0].outstanding` (already maintained in transaction currency)
instead of `outstanding_amount`.

Closes #56006
2026-07-04 19:45:44 +05:30
Nabin Hait
2b1e922183 fix: don't hijack another Campaign's UTM mirror when display names collide 2026-07-04 19:42:13 +05:30
Nabin Hait
486a1c78b0 Merge pull request #56882 from frappe/fix/pos-invoice-reset-mop-attributeerror
fix: reset_mode_of_payments raises AttributeError on a POS Invoice
2026-07-04 19:37:40 +05:30
Nabin Hait
ca9dcbf2d7 fix: reuse the existing UTM Campaign mirror when campaign_name is edited 2026-07-04 18:45:58 +05:30
Nabin Hait
c969ee7bef test: use separate documents for the invalid and valid slot cases 2026-07-04 18:44:04 +05:30
Nabin Hait
a1f6ae56ff fix: removed unused import
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-04 18:39:52 +05:30
Nabin Hait
14c1b02025 Merge pull request #56879 from frappe/chore/test-contract-template
test: add coverage for Contract Template
2026-07-04 17:58:32 +05:30
Nabin Hait
3b335db64c Merge pull request #56878 from frappe/chore/test-crm-settings
test: add coverage for CRM Settings
2026-07-04 17:58:18 +05:30
Nabin Hait
99ed620dad fix: reset_mode_of_payments raises AttributeError on POS Invoice 2026-07-04 17:54:46 +05:30
Nabin Hait
e5b7c3f98c test: cover Job Card quantity, docstatus and capacity-overlap logic 2026-07-04 17:51:52 +05:30
Nabin Hait
10c6cda6db fix: detect contained/enclosing date ranges in Stock Closing Entry duplicate check 2026-07-04 17:47:22 +05:30
Nabin Hait
4b4afe12df Merge pull request #56832 from frappe/chore/test-repost-payment-ledger
test: add coverage for Repost Payment Ledger
2026-07-04 17:45:10 +05:30
Nabin Hait
9fdcfd5f58 fix: link UTM Campaign to the Campaign's document name, not campaign_name 2026-07-04 17:40:16 +05:30
Nabin Hait
5eaafd3025 test: add coverage for Campaign naming and UTM mirroring 2026-07-04 17:33:35 +05:30
Nabin Hait
79c359ef5d test: add coverage for Email Campaign date and recipient validation 2026-07-04 17:30:46 +05:30
Nabin Hait
34f3870f2a test: add coverage for Contract Template validation and rendering 2026-07-04 17:28:28 +05:30
Nabin Hait
7f903b63dd test: add coverage for CRM Settings sync and contact-us guards 2026-07-04 17:26:58 +05:30
Nabin Hait
1a2e3e03b3 test: add coverage for Appointment Booking Settings slot validation 2026-07-04 17:25:20 +05:30
Nabin Hait
4c26ec8cd9 test: make add_manually test distinguish manual mode from auto-loading 2026-07-04 16:48:52 +05:30
Nabin Hait
f68f53dec0 test: cover on-cutoff boundary in voucher loading 2026-07-04 16:48:52 +05:30
Nabin Hait
9901746e02 test: add coverage for Repost Payment Ledger 2026-07-04 16:48:52 +05:30
Nabin Hait
ae0cfbd3e1 test: assert the saved closing entry exists rather than a truthy name 2026-07-04 16:48:51 +05:30
Nabin Hait
e37ceb5f69 test: cover Stock Closing Entry duplicate date-range validation 2026-07-04 16:48:51 +05:30
Nabin Hait
807c52e32b Merge pull request #56855 from frappe/chore/test-stock-reservation-entry-dark-paths
test: cover Stock Reservation Entry validations and helper
2026-07-04 16:43:55 +05:30
Nabin Hait
38b91a2800 Merge pull request #56846 from frappe/chore/test-bisect-accounting-statements
test: add coverage for Bisect Accounting Statements
2026-07-04 16:43:28 +05:30
Nabin Hait
0f3b77a344 Merge pull request #56841 from frappe/chore/test-exchange-rate-revaluation
test: cover Exchange Rate Revaluation validation and gain/loss paths
2026-07-04 16:42:22 +05:30
ruthra kumar
66e70e312d Merge pull request #56852 from ruthra-kumar/race_condition_in_process_pcv
fix: race condition in process pcv
2026-07-04 16:22:16 +05:30
Mihir Kandoi
ac57e389a4 Merge pull request #56871 from mihir-kandoi/cast-error-postgres
fix: type cast error on postgres
2026-07-04 14:24:20 +05:30
Mihir Kandoi
093bbb07a7 fix: type cast error on postgres 2026-07-04 14:11:29 +05:30
Mihir Kandoi
57e7ceae24 Merge pull request #56859 from aerele/fix/item-form-permission-errors
Fix(stock): item form permission errors
2026-07-04 14:07:54 +05:30
rohitwaghchaure
8093e44746 feat: shop floor interface for operators (#55551)
* feat: shop floor interface for operators

* fix: documentation

* fix: UI/UX for shop floor

* fix: shop floor query and OEE edge cases from review

- Push the draft / To Manufacture condition into the Job Card query
  (or_filters) so a busy workstation's submitted history cannot fill
  the row limit and hide active drafts
- Clamp the OEE quality factor at zero when process loss exceeds
  completed qty

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:31:37 +05:30
Nabin Hait
55c27d3dde Merge pull request #56853 from frappe/chore/test-payment-entry-dark-paths
test: cover untested Payment Entry field validations
2026-07-04 11:52:16 +05:30
Nabin Hait
b85776c00b Merge pull request #56843 from frappe/chore/test-share-transfer
test: cover Share Transfer consistency validations
2026-07-03 23:44:40 +05:30
Nabin Hait
0f7ee3a843 Merge pull request #56844 from frappe/chore/test-process-statement-of-accounts
test: cover Process Statement Of Accounts validation
2026-07-03 23:44:29 +05:30
Nabin Hait
e546132ac3 Merge pull request #56845 from frappe/chore/test-chart-of-accounts-importer
test: add coverage for Chart of Accounts Importer parsing
2026-07-03 23:44:20 +05:30
Nabin Hait
24a13d16bb Merge pull request #56847 from frappe/chore/test-asset-capitalization
test: cover Asset Capitalization row validations
2026-07-03 23:43:52 +05:30
Nabin Hait
17e7f91690 Merge pull request #56850 from frappe/chore/test-packing-slip
test: cover Packing Slip package-number and item validations
2026-07-03 23:43:43 +05:30
Nabin Hait
ef7fb1084d Merge pull request #56854 from frappe/chore/test-serial-batch-bundle-dark-paths
test: cover Serial and Batch Bundle helpers and validations
2026-07-03 23:43:04 +05:30
Nabin Hait
18d16fa5cf Merge pull request #56851 from frappe/chore/test-email-digest
test: cover Email Digest date-window calculations
2026-07-03 23:42:25 +05:30
Nikhil Kothari
dc09362454 fix: replace all old icons (#56864) 2026-07-03 23:03:28 +05:30
Nabin Hait
2000a9db36 Merge pull request #56842 from frappe/chore/test-bank-reconciliation-tool
test: cover Bank Reconciliation Tool date filter and message helper
2026-07-03 22:08:23 +05:30
Nabin Hait
dd35d977f7 Merge pull request #56829 from frappe/chore/test-process-subscription
test: add coverage for Process Subscription
2026-07-03 22:06:19 +05:30
Mihir Kandoi
4545dd939a Merge pull request #56837 from aerele/fix/manufacture-stock-entry-cost-center-default
fix: remove company default on cost center in stock entry detail
2026-07-03 21:11:52 +05:30
rohitwaghchaure
7b0c35caaf fix: auto fetch serial no from previous operation output (#56445)
* fix: auto fetch serial no from previous operation output

* fix: order by

* fix: warehouse for operations
2026-07-03 20:23:33 +05:30
rohitwaghchaure
341a07dffa fix: restrict state-changing whitelisted endpoints to POST (#56858)
Add methods=["POST"] to 50 whitelisted functions that create or modify
documents (get_doc followed by insert/save/submit), so they can no
longer be invoked via GET requests.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:47:13 +05:30
pandiyan
ef794f390c fix: skip item prices tab render for users without item price read access 2026-07-03 18:25:08 +05:30
pandiyan
8c7b2f4d3c fix: clear stray permission message when item dashboard has no warehouse access 2026-07-03 18:25:01 +05:30
rohitwaghchaure
9c911438f1 fix: do not rebook standard cost variance on non-update-stock purchase invoice (#56799) 2026-07-03 17:17:38 +05:30
ruthra kumar
a9ffdac806 chore: linter fix 2026-07-03 17:16:06 +05:30
ruthra kumar
dbc409736a refactor(test): row name based utility methods 2026-07-03 17:16:03 +05:30
Nabin Hait
008742bdbe test: exercise every mandatory field in the Stock Reservation Entry check 2026-07-03 17:08:27 +05:30
Nabin Hait
ed77392741 test: also accept a mid-range rounding loss allowance 2026-07-03 17:07:31 +05:30
Nabin Hait
3240411876 test: drop unused timedelta import 2026-07-03 17:05:53 +05:30
Nabin Hait
8f96e5f2aa test: replace lambda with nested def (ruff E731) 2026-07-03 17:03:51 +05:30
Nabin Hait
7b2f38cd6f test: cover Stock Reservation Entry validations and helper 2026-07-03 16:23:52 +05:30
Nabin Hait
8456e88d93 test: cover Serial and Batch Bundle helpers and in-memory validations 2026-07-03 16:20:41 +05:30
ruthra kumar
21f4603144 refactor: prevent whole table scan while scheduling next date
- helps in concurrency isolation
2026-07-03 16:19:35 +05:30
Nabin Hait
e0ea8eee1a test: cover untested Payment Entry field validations 2026-07-03 16:15:24 +05:30
Nabin Hait
c5ab9958ff test: cover Email Digest date-window calculations 2026-07-03 16:01:10 +05:30
Nabin Hait
113d914b9c test: cover Packing Slip package-number and item validations 2026-07-03 15:59:13 +05:30
Nabin Hait
ebd8547629 test: cover Asset Capitalization row validations 2026-07-03 15:54:17 +05:30
ruthra kumar
7e4045e828 fix: prevent repeatable read related concurrency errors
Process Period Closing Voucher and Process Period Closing Voucher
Details are trackers how the jobs are processed. Keep transactions on
them very short.
2026-07-03 15:28:28 +05:30
ruthra kumar
ff6881764b fix: race condition and repeatable read in process pcv
- Update using child table name to avoid scanning whole table, which
eventually leads to mariadb 1020 (REPEATABLE READ).
 - Avoid race condition in final summarization
2026-07-03 15:28:26 +05:30
Nabin Hait
28367f75e9 test: add coverage for Bisect Accounting Statements bisection 2026-07-03 14:57:47 +05:30
Nabin Hait
740c5a07ff test: add coverage for Chart of Accounts Importer parsing 2026-07-03 14:55:31 +05:30
Nabin Hait
3dfb3f385b test: cover Process Statement Of Accounts validation defaults 2026-07-03 14:53:22 +05:30
Nabin Hait
dae90e90df test: cover Share Transfer consistency validations 2026-07-03 14:50:52 +05:30
Nabin Hait
d1d592cf0c test: cover bank reconciliation date filter and auto-reconcile message 2026-07-03 14:48:21 +05:30
Nabin Hait
65500d5102 test: cover Exchange Rate Revaluation validation and gain/loss paths 2026-07-03 14:45:06 +05:30
Nabin Hait
23e3dd94c0 Merge pull request #56831 from frappe/chore/test-process-payment-reconciliation
fix: Process Payment Reconciliation drops bank/cash and cost center filters
2026-07-03 14:31:17 +05:30
Nabin Hait
6255d99fda Merge pull request #56827 from frappe/chore/test-subscription-plan
fix: Subscription Plan Monthly Rate under-bills across a year boundary
2026-07-03 14:30:25 +05:30
Nabin Hait
81d5eac0ea Merge pull request #56824 from frappe/chore/test-journal-entry-template
fix: Journal Entry Template rows must belong to its company
2026-07-03 14:29:28 +05:30
Nabin Hait
0b20438da9 test: mirror subscription test setup for known settings 2026-07-03 14:24:58 +05:30
Nabin Hait
8632019d2a Merge pull request #56830 from frappe/chore/test-account-closing-balance
test: add coverage for Account Closing Balance
2026-07-03 14:22:40 +05:30
Nabin Hait
c9960b4d51 fix: carry bank/cash account and cost center into Payment Reconciliation 2026-07-03 14:16:46 +05:30
Nabin Hait
2cc02e61d9 fix: validate Journal Entry Template rows belong to its company 2026-07-03 14:15:19 +05:30
Nabin Hait
6fc28edde9 Merge pull request #56828 from frappe/chore/test-cashier-closing
test: add coverage for Cashier Closing
2026-07-03 14:08:55 +05:30
Nabin Hait
196730c535 fix: bill all months across a year boundary in Monthly Rate plans 2026-07-03 14:08:20 +05:30
Nabin Hait
4d39f698bd Merge pull request #56823 from frappe/chore/test-party-link
test: add coverage for Party Link
2026-07-03 14:07:04 +05:30
Nabin Hait
0a7abe7144 Merge pull request #56822 from frappe/chore/test-mode-of-payment
test: add coverage for Mode of Payment
2026-07-03 14:06:50 +05:30
pandiyan
a168bb7ea4 test: cover cost center fallback to item group default in manufacture entry
the existing test_cost_center_for_manufacture only checks a raw material
row against an item-level override, which is set independently of the
":company" default guard and never exercised the bug.
2026-07-03 14:06:22 +05:30
pandiyan
edfa0a7a1d fix: remove company default on cost center in stock entry detail
the ":company" default pre-filled every row before set_default_cost_center()
ran, so its "if not row.cost_center" guard was always false and the
project/item group/brand priority chain in get_default_cost_center()
never ran.
2026-07-03 14:06:22 +05:30
Nabin Hait
5888cdf3a0 Merge pull request #56821 from frappe/chore/test-item-tax-template
test: add coverage for Item Tax Template
2026-07-03 14:04:03 +05:30
Nabin Hait
a1f413e8a8 Merge pull request #56820 from frappe/chore/test-monthly-distribution
test: add coverage for Monthly Distribution
2026-07-03 14:03:36 +05:30
Nabin Hait
cd167bdd40 Merge pull request #56819 from frappe/chore/test-bank-guarantee
test: add coverage for Bank Guarantee
2026-07-03 14:02:53 +05:30
Mihir Kandoi
5d48c44bbb Merge pull request #56826 from frappe/fix/stock-ledger-invariant-check-report
fix: FIFO queue checks and incorrect entries filter in stock ledger reports
2026-07-03 13:19:29 +05:30
rohitwaghchaure
ecc8ec672b fix: replay immutable SLE qty for serial/batch bundle valuation (#56814) 2026-07-03 12:15:07 +05:30
Mihir Kandoi
3b1e57966e test: drop redundant cleanup, db rolls back after each test 2026-07-03 12:12:46 +05:30
Nabin Hait
974571aba7 test: guard account lookups and cover dropped pr_instance filters 2026-07-03 12:08:11 +05:30
Nabin Hait
7d917e497a test: assert account-currency sums carry through the merge 2026-07-03 12:06:56 +05:30
Nabin Hait
e041e33860 test: reload invoice for outstanding and cover equal-time boundary 2026-07-03 12:06:20 +05:30
Nabin Hait
832b5a56bf test: lock current cross-year monthly-rate underbilling value 2026-07-03 12:05:31 +05:30
Nabin Hait
abded56174 test: guard account lookup and lock missing company-check behaviour 2026-07-03 12:04:42 +05:30
Nabin Hait
6f866545b9 test: complete supplier-primary assertions and lock uniqueness gap 2026-07-03 12:03:56 +05:30
Nabin Hait
147e1539dc test: guard account lookup and lock dead POS guard behaviour 2026-07-03 12:02:50 +05:30
Nabin Hait
9980d47524 test: lock current end-date behaviour and assert persisted state 2026-07-03 12:00:45 +05:30
Nabin Hait
97794b7ded test: add coverage for Process Payment Reconciliation 2026-07-03 11:41:21 +05:30
Mihir Kandoi
ef5f47fafd fix: address review comments
- restore mutated SLE after test via addCleanup
- explicit return False in has_difference
- comment the fifo_stock_diff guard for non-queue predecessors
2026-07-03 11:39:51 +05:30
Nabin Hait
c51edbd88e test: add coverage for Account Closing Balance 2026-07-03 11:39:50 +05:30
Nabin Hait
745f657a0e test: add coverage for Process Subscription 2026-07-03 11:37:02 +05:30
Nabin Hait
5c87e2e398 test: add coverage for Cashier Closing 2026-07-03 11:34:27 +05:30
Nabin Hait
3167e8ba77 test: add coverage for Subscription Plan 2026-07-03 11:31:30 +05:30
Mihir Kandoi
94ab09e4a3 fix: FIFO queue checks and incorrect entries filter in stock ledger reports
- 'Show Incorrect Entries' always returned an empty result (regression
  from #43619); now returns entries from one row before the first
  incorrect one
- FIFO queue columns were computed for serialized/batched SLEs that
  don't maintain a stock queue, showing false differences; left empty
  for such rows
- compare value/valuation differences at currency precision, qty at
  float precision
2026-07-03 11:29:44 +05:30
Nabin Hait
83d821d8c4 test: add coverage for Journal Entry Template 2026-07-03 11:28:56 +05:30
Nabin Hait
22dc51a57a test: add coverage for Party Link 2026-07-03 11:25:32 +05:30
Nabin Hait
df54382727 test: add coverage for Mode of Payment 2026-07-03 11:23:08 +05:30
Nabin Hait
ccd2aae481 test: add coverage for Monthly Distribution 2026-07-03 11:18:11 +05:30
Nabin Hait
41000ea109 test: add coverage for Bank Guarantee 2026-07-03 11:14:54 +05:30
Nabin Hait
7d8d1eaec7 test: submit overpaid payment for GL coverage and sync received_amount 2026-07-02 14:15:38 +05:30
mergify[bot]
0d8c65a013 ci(mergify): upgrade configuration to current format 2026-07-01 20:50:03 +00:00
SowmyaArunachalam
07f641c48c fix(journal-entry): fetch outstanding on foreign currency 2026-06-29 21:48:38 +05:30
Raghav Ruia
69d5d2bbc1 refactor: extract negative stock confirmation into shared util
Deduplicate the identical confirmation dialog used by Item and Stock
Settings into erpnext.utils.confirm_negative_stock, and collapse the
message into a single translatable string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:38:23 +05:30
Shllokkk
ecb6d48ec0 fix: restrict jinja globals in process statement of accounts templates 2026-06-25 14:48:02 +05:30
Raghav Ruia
2bf9fcb817 feat: confirmation dialog when enabling negative stock on Item
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:06:30 +05:30
Nabin Hait
ac8b3f18c7 test: add purchase-side Payment Entry allocation coverage
- pay multiple purchase invoices with a single Payment Entry
- unallocated (advance) amount when a supplier payment is overpaid
- allocating more than a purchase invoice's outstanding amount is rejected
2026-06-22 14:13:18 +05:30
Mohsin Akhtar
2433129850 fix: show only template items in Variant Of filter 2026-06-21 16:10:43 +05:30
1441 changed files with 644625 additions and 287957 deletions

View File

@@ -60,10 +60,13 @@ Flag a changed query that uses any of these:
check_field, True)`, `doc.db_set(field, False)`, or `frappe.qb.update(dt).set(check_field, True)`
emit `SET col = true`, which PostgreSQL rejects on a `smallint`/`Check` column
(`column is of type smallint but expression is of type boolean`). Pass `1`/`0`.
- **`.like()`/`.ilike()` (or raw `LIKE`) on a NON-text column** — `idx`, `docstatus`, a date, etc.
frappe maps `.like()` → `ILIKE`, and PostgreSQL has no `bigint ILIKE text` operator (`operator
does not exist: bigint ~~* unknown`). Cast the column to text first — **`Cast_(col, "varchar")`**,
not `Cast(col, "char")` (see below). MariaDB coerces the int implicitly, so the cast is a no-op there.
- **A direct `.like()`/`.ilike()` on a pypika field (or raw `LIKE`) on a NON-text column** — `idx`,
`docstatus`, a date, etc. frappe maps `.like()` → `ILIKE`, and PostgreSQL has no `bigint ILIKE text`
operator (`operator does not exist: bigint ~~* unknown`). Cast the column to text first —
**`Cast_(col, "varchar")`**, not `Cast(col, "char")` (see below). MariaDB coerces the int
implicitly, so the cast is a no-op there. A `["like", …]` filter passed to `get_all`/`get_list`/
`qb.get_query`/`reportview` needs no cast: the framework casts non-text fields itself
(frappe/frappe#42449).
- **`CAST(… AS CHAR)` / `Cast(x, "char")`** — on PostgreSQL bare `CHAR` is `character(1)`, so
`CAST(12 AS CHAR)` → `'1'` (silently truncates multi-digit values); MariaDB gives the full string.
Use `VARCHAR` / `Cast_(x, "varchar")`.
@@ -150,6 +153,48 @@ Add it only when it is functionally dependent on the existing select columns; ot
SQL `ORDER BY` and **sort in Python** (`key=str.casefold`, per §2) so the distinct row set is
unchanged.
### 3.1 Second-order traps — when the `Max()`/`Min()` wrap itself is the bug
The wrap is only a no-op when the column is provably single-valued per group (**"`Max()` means
provably constant"**). When the column can genuinely vary, the wrap is a decision, and a full
audit of these fixes found four recurring mistakes:
- **Incoherent pair** — two semantically-coupled columns (a flag + a link:
`is_phantom_item` + `bom_no`; a discriminator + its value) aggregated with *independent*
`Max()`/`Min()` can pair values from **different rows** — a chimera row that never existed.
MariaDB's loose pick was at least row-coherent. Fix: group by the pair (when consumers
tolerate the extra rows), or select one **representative row** (`Min(child.name)` subquery +
join-back) so every column comes from the same line.
- **NULL-skipping** — `MAX`/`MIN` ignore NULLs, so `Max()` over a mostly-NULL discriminator
(an `original_item`-style column) *deterministically* returns the non-NULL value where
MariaDB could return NULL — deterministically wrong where the old behavior was only
intermittently wrong. Flag it wherever "no value" is a meaningful state (fallback gates,
dict keys).
- **Fabricated arithmetic** — `Sum(x) * Max(y)` where `y` varies within the group invents a
number no row ever had (and `Max` biases it upward) — poisonous when it feeds validation,
budgets, valuation, or GL/stock values. Fix per-row: `Sum(x * y)`.
- **Collation-dependent pick (text columns)** — `Max()`/`Min()` over text is a *sort*, and the two
engines sort text differently: MariaDB's `utf8mb4` collations fold case, PostgreSQL (as CI runs
it) orders by byte value. `MAX('abc', 'ABD')` is `ABD` on MariaDB and `abc` on PostgreSQL. So a
`Max()` over a text column that varies **in case** within its group is a live P2 divergence, not
the arbitrary-pick preservation the wrap is usually justified as. Confirmed on CI; see #56241.
Note a local macOS PostgreSQL gives a **false all-clear** — its collation happens to agree with
MariaDB on case. Fix: take a representative row rather than sorting text.
**Picking that row is the hard part.** `Min(name)` is still a text sort: `autoname="hash"` is
not reliably lower case, because `_get_timestamp_prefix()` prepends `get_trace_id()[-1:]`
un-lowered and a client-supplied `X-Frappe-Request-Id` can put an upper case `A-F` there. A
non-text key (`Min(idx)`) works only where it is **unique within the group** and the join-back
carries the **full group key** — a date is usually neither, and joining on a duplicated value
turns one group into several rows (§3). Otherwise select the row in Python, sorting with
`key=str.casefold` so the order matches MariaDB's collation without depending on the database's.
- **Wrong bound** — where the value has a semantic, pick the bound deliberately:
`Min(schedule_date)` for a "required by", `Min(idx)` for first-line ordering, a qty-weighted
average for a rate. A blind `Max` can understate urgency or overstate a figure.
Review heuristic: **if choosing between `Max` and `Min` would change the answer, the column is
not functionally dependent** — wrapping either is the wrong fix. Group by it, restructure, or
pick a bound for a stated reason, and cover the varying-group case with a test.
---
## 4. False positives — do NOT flag these
@@ -157,8 +202,9 @@ unchanged.
These are auto-handled by the framework and are **not** breaks:
- **`.like()` / `["like", …]`** already renders as `ILIKE` on PostgreSQL — not a
case-sensitivity bug. *(Exception: `.like()` on a **non-text** column — `idx`, `docstatus` —
is a hard break, `bigint ILIKE`; see §1.)*
case-sensitivity bug. A `["like", …]` filter on a **non-text** field is also cast to text by
the framework. *(Exception: a direct `.like()` on a **non-text** pypika field — `idx`,
`docstatus` — is a hard break, `bigint ILIKE`; see §1.)*
- **Raw `ifnull(...)`** inside `frappe.db.sql()` is rewritten to `coalesce(...)` on all engines.
- **Backticks**, **`LOCATE`**, **`REGEXP`** / **`.regexp()`** in raw SQL are auto-translated on
PostgreSQL (`REGEXP` → `~*`). **But `RLIKE` / `.rlike()` is NOT translated** — that one is a

View File

@@ -4,9 +4,59 @@ set -e
cd ~ || exit
# Authenticate git against github.com with the job token: anonymous git-over-HTTPS from the
# runners gets throttled to a 401, which kills whichever clone is in flight — the frappe fetch
# below, or payments under `bench get-app`. See the PR description.
#
# A credential helper rather than a url.insteadOf rewrite, because `git clone` PERSISTS a
# rewritten URL into the new repo's .git/config: an insteadOf would leave the token sitting in
# apps/payments/.git/config on the runner. A helper is consulted only when github.com actually
# challenges, and leaves the stored remote URL untouched. Passing it through GIT_CONFIG_* keeps
# the token out of ~/.gitconfig too, and child processes inherit it (bench shells out to git).
ci_github_token=${CI_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}
if [ -n "$ci_github_token" ]; then
export CI_GITHUB_TOKEN="$ci_github_token"
export GIT_CONFIG_COUNT=3
# Reset first: git runs EVERY configured helper and calls `store` on them after a successful
# auth, so a `credential.helper=store` inherited from the image's gitconfig would write the
# token to ~/.git-credentials. An empty value clears the list before ours is added.
export GIT_CONFIG_KEY_0="credential.helper"
export GIT_CONFIG_VALUE_0=""
export GIT_CONFIG_KEY_1="credential.https://github.com.username"
export GIT_CONFIG_VALUE_1="x-access-token"
export GIT_CONFIG_KEY_2="credential.https://github.com.helper"
# Single-quoted: $CI_GITHUB_TOKEN is expanded by the shell git runs the helper in, so the
# token is read from the environment at call time and never stored anywhere. Answering only
# `get` makes the helper inert for git's `store`/`erase` calls.
export GIT_CONFIG_VALUE_2='!f() { test "$1" = get && echo "password=$CI_GITHUB_TOKEN"; }; f'
fi
# Whatever happens, never sit on a credential prompt: fail fast and legibly instead.
export GIT_TERMINAL_PROMPT=0
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
frappecommitish=${FRAPPE_BRANCH:-}
# A stacked pull request targets another erpnext branch, which has no counterpart in frappe.
# Fall back to develop so the bench is still installed. An explicit FRAPPE_BRANCH is trusted as
# given, since it can be a commit sha rather than a branch.
if [ -z "$frappecommitish" ]; then
frappecommitish=$githubbranch
# git ls-remote --exit-code reports 2 for a branch that is not there and 128 for a remote it
# could not reach. Only the first one is proof of absence; keep the branch on anything else so
# a flaky probe cannot install an unrelated frappe.
probe=0
git ls-remote --exit-code --heads "https://github.com/${frappeuser}/frappe" "$frappecommitish" >/dev/null 2>&1 || probe=$?
if [ "$probe" -eq 2 ]; then
echo "frappe has no branch ${frappecommitish}, falling back to develop"
frappecommitish=develop
elif [ "$probe" -ne 0 ]; then
echo "could not reach frappe to check for branch ${frappecommitish} (git ls-remote exited ${probe}), keeping it"
fi
fi
db_host=${DB_HOST:-"127.0.0.1"}
db_user_host=${DB_USER_HOST:-"localhost"}
wkhtmltox_deb=${WKHTMLTOX_DEB:-"/tmp/wkhtmltox.deb"}
@@ -168,7 +218,7 @@ restore_warm_bench() {
# Phase 1 already fetched ~/frappe to the exact live develop SHA. Fetch that commit
# straight from it (bench init names the remote 'upstream', not 'origin', and points
# it at this local clone — so a plain `git fetch origin` does not work).
git fetch --no-tags "$HOME/frappe" HEAD || exit 1
git fetch --no-tags --update-shallow "$HOME/frappe" HEAD || exit 1
git checkout --force FETCH_HEAD || exit 1
); then
echo "Fast-forward to ${frappe_sha} failed; falling back to full init"

View File

@@ -0,0 +1,51 @@
name: Download translations from Crowdin
on:
schedule:
- cron: "0 4 * * 1"
workflow_dispatch:
concurrency:
group: crowdin-download
cancel-in-progress: false
permissions:
contents: read
jobs:
download-translations:
name: Download translations into ${{ matrix.branch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branch: ["develop", "version-16-hotfix"]
steps:
- name: Checkout ${{ matrix.branch }}
uses: actions/checkout@v6
with:
ref: ${{ matrix.branch }}
fetch-depth: 0
persist-credentials: false
- name: Download translations and open PR
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
with:
config: crowdin.yml
upload_sources: false
upload_translations: false
download_translations: true
crowdin_branch_name: "[frappe.erpnext] ${{ matrix.branch }}"
skip_ref_checkout: true
localization_branch_name: l10n_crowdin_${{ matrix.branch }}
create_pull_request: true
pull_request_base_branch_name: ${{ matrix.branch }}
commit_message: "fix: sync translations from crowdin"
pull_request_title: "fix: sync translations from crowdin (${{ matrix.branch }})"
pull_request_labels: "translation, skip-release-notes"
pull_request_reviewers: barredterra
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -0,0 +1,54 @@
name: Upload main.pot to Crowdin
on:
push:
branches:
- develop
- version-16-hotfix
paths:
- "erpnext/locale/main.pot"
workflow_dispatch:
concurrency:
group: crowdin-upload-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
jobs:
upload-sources:
name: Upload sources from ${{ github.ref_name }}
runs-on: ubuntu-latest
steps:
- name: Checkout ${{ github.ref_name }}
uses: actions/checkout@v6
- name: Restore Crowdin cache
uses: actions/cache/restore@v6
with:
path: .crowdin
key: crowdin-${{ github.ref_name }}
restore-keys: crowdin-${{ github.ref_name }}-
- name: Upload main.pot to Crowdin
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
with:
config: crowdin.yml
upload_sources: true
upload_translations: false
download_translations: false
create_pull_request: false
crowdin_branch_name: "[frappe.erpnext] ${{ github.ref_name }}"
upload_sources_args: "--cache"
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Save Crowdin cache
uses: actions/cache/save@v6
if: always()
with:
path: .crowdin
key: crowdin-${{ github.ref_name }}-${{ github.run_id }}

View File

@@ -0,0 +1,45 @@
name: Notify Support on PR release
on:
issue_comment:
types: [created]
permissions: {}
jobs:
notify-support:
if: >-
github.event.issue.pull_request
&& github.event.comment.user.id == 28699486
&& contains(github.event.comment.body, 'This PR is included in version')
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Notify support.frappe.io
env:
COMMENT_ID: ${{ github.event.comment.id }}
PR_NUMBER: ${{ github.event.issue.number }}
REPOSITORY: ${{ github.repository }}
SUPPORT_FRAPPE_AUTH: ${{ secrets.SUPPORT_FRAPPE_AUTH }}
run: |
payload="$(
jq -n \
--arg repository "$REPOSITORY" \
--argjson pr_number "$PR_NUMBER" \
--argjson comment_id "$COMMENT_ID" \
'{
repository: $repository,
pr_number: $pr_number,
comment_id: $comment_id
}'
)"
curl --fail-with-body \
--retry 3 \
--retry-all-errors \
--request POST \
--header "Authorization: $SUPPORT_FRAPPE_AUTH" \
--header "Content-Type: application/json" \
--data "$payload" \
"https://support.frappe.io/api/method/notify_pr_release"

View File

@@ -121,6 +121,8 @@ jobs:
env:
DB: mariadb
TYPE: server
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
- name: Run Patch Tests
run: |
@@ -171,7 +173,30 @@ jobs:
update_to_version 16 3.14
echo "Updating to latest version"
git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
fallback_to_develop=0
if [ -n "${GITHUB_BASE_REF:-}" ]; then
frappe_ref="refs/heads/$GITHUB_BASE_REF"
fallback_to_develop=1
elif [ "${GITHUB_REF_TYPE:-}" = "branch" ]; then
frappe_ref="$GITHUB_REF"
fallback_to_develop=1
elif [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
frappe_ref="$GITHUB_REF"
else
echo "Unsupported GitHub ref type: '${GITHUB_REF_TYPE:-unset}'"
exit 1
fi
ls_remote_status=0
git -C "apps/frappe" ls-remote --exit-code upstream "$frappe_ref" >/dev/null \
|| ls_remote_status=$?
if [ "$ls_remote_status" -eq 2 ] && [ "$fallback_to_develop" -eq 1 ]; then
echo "frappe has no '$frappe_ref'; falling back to develop"
frappe_ref=refs/heads/develop
elif [ "$ls_remote_status" -ne 0 ]; then
exit "$ls_remote_status"
fi
git -C "apps/frappe" fetch --depth 1 upstream "$frappe_ref"
git -C "apps/frappe" checkout -q -f FETCH_HEAD
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"

View File

@@ -22,4 +22,6 @@ jobs:
pull-requests: write
steps:
- uses: alyf-de/po-review-action@v1.1.0
- uses: alyf-de/po-review-action@57fff275f4a0518a2ca55869ec6776fa3813b3d5 # v1.2.0
with:
hidden-po-files: eo.po

View File

@@ -129,6 +129,8 @@ jobs:
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
- name: Run Tests
run: |

View File

@@ -13,6 +13,7 @@ on:
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
- '**.po'
permissions:
contents: read

View File

@@ -13,6 +13,7 @@ on:
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
- '**.po'
schedule:
# Run everday at midnight UTC / 5:30 IST
- cron: "0 0 * * *"
@@ -101,6 +102,8 @@ jobs:
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
DB_HOST: 127.0.0.1
DB_USER_HOST: '%'
WKHTMLTOX_DEB: /tmp/wkhtmltox.deb

View File

@@ -103,6 +103,8 @@ jobs:
DB: postgres
TYPE: server
FRAPPE_BRANCH: develop
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
BENCH_CACHE_DIR: /home/runner/bench-cache
- name: Warm up test data

File diff suppressed because one or more lines are too long

View File

@@ -88,7 +88,8 @@ pull_request_rules:
actions:
merge:
method: squash
commit_message_template: |
{{ title }} (#{{ number }})
{{ body }}
commit_message_format:
title: pr-title
body: pr-body
merge_queue:
queue_controls_comment: false

View File

@@ -7,6 +7,7 @@ erpnext/accounts/ @ruthra-kumar
erpnext/assets/ @khushi8112
erpnext/regional @ruthra-kumar
erpnext/selling @ruthra-kumar
banking/ @nikkothari22
erpnext/buying/ @rohitwaghchaure @mihir-kandoi
erpnext/maintenance/ @rohitwaghchaure @mihir-kandoi

View File

@@ -24,10 +24,10 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.20",
"frappe-react-sdk": "^1.17.0",
"frappe-react-sdk": "^1.17.1",
"fuse.js": "^7.3.0",
"jotai": "^2.20.1",
"jotai-family": "^1.0.2",
"jotai": "^2.20.2",
"jotai-family": "^1.1.0",
"lodash.isplainobject": "^4.0.6",
"lucide-react": "^1.14.0",
"radix-ui": "^1.6.1",
@@ -39,7 +39,7 @@
"react-hook-form": "^7.75.0",
"react-hotkeys-hook": "^5.3.2",
"react-markdown": "^10.1.0",
"react-router": "^8.1.0",
"react-router": "^8.3.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"safe-expr-eval": "^1.0.4",
@@ -48,18 +48,18 @@
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.16"
"vite": "^8.2.1"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@eslint/js": "^9.39.5",
"@types/node": "^25.3.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.1",
"eslint": "^10.8.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.62.1"
"typescript-eslint": "^8.67.0"
}
}

View File

@@ -9,6 +9,7 @@ import Fuse from "fuse.js"
import { ChevronDownIcon } from "lucide-react"
import { useLayoutEffect, useMemo, useRef, useState } from "react"
import { FormControl } from "../ui/form"
import useResetScrollOnSearch from "@/hooks/useResetScrollOnSearch"
export interface AccountsDropdownProps {
@@ -104,6 +105,10 @@ const AccountsDropdown = ({ root_type, report_type, account_type, value, onChang
const buttonRef = useRef<HTMLButtonElement>(null)
// Searching replaces the grouped list with a short result list, so pin the scroll back to
// the top - otherwise the auto-selected first result can be out of view.
const listRef = useResetScrollOnSearch(search)
const [width, setWidth] = useState(320)
useLayoutEffect(() => {
@@ -153,7 +158,7 @@ const AccountsDropdown = ({ root_type, report_type, account_type, value, onChang
<PopoverContent className="p-0" style={{ minWidth: width }} align="start">
<Command shouldFilter={false} className="w-full">
<CommandInput placeholder={_("Search account...")} onValueChange={setSearch} value={search} />
<CommandList>
<CommandList ref={listRef}>
<CommandEmpty>{_("No accounts found.")}</CommandEmpty>
{recommendedAccounts.length > 0 && (

View File

@@ -10,6 +10,7 @@ import { ChevronDownIcon, ExternalLink } from "lucide-react";
import { Button } from "../ui/button";
import { cn } from "@/lib/utils";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../ui/command";
import useResetScrollOnSearch from "@/hooks/useResetScrollOnSearch";
import _ from "@/lib/translate";
import ErrorBanner from "../ui/error-banner";
import MarkdownRenderer from "../ui/markdown";
@@ -149,6 +150,10 @@ const LinkFieldCombobox = ({
const buttonRef = useRef<HTMLButtonElement>(null)
// Results change as the search runs, so pin the scroll back to the top to keep the
// auto-selected first result in view.
const listRef = useResetScrollOnSearch(searchInput)
const [width, setWidth] = useState(320)
useLayoutEffect(() => {
@@ -264,7 +269,7 @@ const LinkFieldCombobox = ({
{error && <ErrorBanner error={error} />}
<Command shouldFilter={false} className="w-full">
<CommandInput placeholder={placeholder} onValueChange={setSearchInput} />
<CommandList>
<CommandList ref={listRef}>
<CommandEmpty>{isLoading ? _("Loading...") : _("No results found.")}</CommandEmpty>
<CommandGroup>
{items?.map((result) => (
@@ -272,7 +277,7 @@ const LinkFieldCombobox = ({
<span className="font-medium">
{result.label || result.value}
</span>
{result.description && <span className="text-xs text-ink-gray-5">
{result.description && <span className="text-p-xs text-ink-gray-5">
<MarkdownRenderer content={result.description} />
</span>}
</CommandItem>

View File

@@ -6,13 +6,13 @@ import { Progress } from "@/components/ui/progress"
import { useGetAccountClosingBalance, useGetAccountClosingBalanceAsPerStatement, useGetAccountOpeningBalance, useGetUnreconciledTransactions } from "./utils"
import { flt, formatCurrency } from "@/lib/numbers"
import { Skeleton } from "@/components/ui/skeleton"
import { StatContainer, StatLabel, StatValue } from "@/components/ui/stats"
import { Edit, Info, Trash2 } from "lucide-react"
import { H4, Paragraph } from "@/components/ui/typography"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
import { getCompanyCurrency } from "@/lib/company"
import _ from "@/lib/translate"
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { formatDate } from "@/lib/date"
import { Form } from "@/components/ui/form"
@@ -26,50 +26,109 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { toast } from "sonner"
import ErrorBanner from "@/components/ui/error-banner"
const BankBalance = () => {
const useBankCurrency = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
return bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '')
}
/**
* One line of the balance summary - label on the left, figure right-aligned.
*
* `items-baseline` keeps the figure on the label's FIRST line, so a row carrying a `subLabel`
* (the statement row's "As of <date>" note) doesn't centre its value against both lines.
*/
const BalanceRow = ({ label, info, subLabel, emphasis, children }: {
label: React.ReactNode
info?: React.ReactNode
subLabel?: React.ReactNode
emphasis?: boolean
children: React.ReactNode
}) => (
<div className="flex items-baseline justify-between gap-3">
<span className="flex min-w-0 flex-col gap-1.5">
<span className={cn("flex items-center gap-1 whitespace-nowrap text-xs text-ink-gray-6",
emphasis && "font-medium text-ink-gray-7")}>
{label}
{info}
</span>
{subLabel}
</span>
<div className="flex flex-col items-end">{children}</div>
</div>
)
/**
* Type styles for a figure. Shared so an interactive figure can put them on the <button>
* ITSELF rather than on a nested span: Tailwind's preflight sets `font: inherit` on buttons,
* which resets line-height too, so a button wrapping a `text-sm` span gets a taller strut than
* the span and the row grows - visible as extra space above a baseline-aligned row.
*/
const BALANCE_VALUE_CLASSES = "font-numeric text-sm tabular-nums text-ink-gray-8"
const BalanceValue = ({ children, emphasis, tone, className }: { children: React.ReactNode, emphasis?: boolean, tone?: 'red', className?: string }) => (
<span className={cn(BALANCE_VALUE_CLASSES,
emphasis && "font-semibold",
tone === 'red' && "text-ink-red-3",
className)}>
{children}
</span>
)
const BalanceSkeleton = () => <Skeleton className="h-4 w-24 rounded-sm" />
/**
* Balances and progress for the selected bank account, laid out like the totals block of an
* invoice. This sits beside the bank picker rather than in a row of its own (saves vertical
* space) and outside the picker's horizontal scroll area, so the figures being reconciled
* against can never scroll out of view.
*/
const BankAccountBalancePanel = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
if (!bankAccount) {
return null
}
return (
<div className="flex justify-between">
<div className="w-[80%] flex flex-wrap justify-between gap-2 pe-8 border-e-border border-e">
<OpeningBalance />
<ClosingBalance />
<ClosingBalanceAsPerStatement />
<Difference />
</div>
<ReconcileProgress />
return (
<div className="flex w-72 shrink-0 flex-col justify-center gap-2.5 border-s border-outline-gray-2 ps-4">
{/* Names the account these figures belong to - the picker scrolls, so the
highlighted card can't be relied on as the referent. */}
<span
className="truncate text-xs font-medium text-ink-gray-7"
title={bankAccount.account_name}>
{bankAccount.account_name}
</span>
<OpeningBalanceRow />
<SystemClosingBalanceRow />
<StatementClosingBalanceRow />
<Separator />
<DifferenceRow />
<ReconciledRow />
</div>
)
}
const OpeningBalance = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const OpeningBalanceRow = () => {
const currency = useBankCurrency()
const { data, isLoading } = useGetAccountOpeningBalance()
return <StatContainer className="min-w-48">
<StatLabel>{_("Opening Balance")}</StatLabel>
{isLoading ? <Skeleton className="w-[150px] h-5 rounded-sm" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
</StatContainer>
return <BalanceRow label={_("Opening Balance")}>
{isLoading ? <BalanceSkeleton /> : <BalanceValue>{formatCurrency(flt(data?.message, 2), currency)}</BalanceValue>}
</BalanceRow>
}
const ClosingBalance = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const SystemClosingBalanceRow = () => {
const currency = useBankCurrency()
const { data, isLoading } = useGetAccountClosingBalance()
return (
<StatContainer className="min-w-48">
<div className="flex items-start gap-1">
<StatLabel>
{_("Closing Balance as per system")}
</StatLabel>
<BalanceRow
label={_("Closing (system)")}
info={
<HoverCard openDelay={100}>
<HoverCardTrigger>
<Info className="size-3.5 text-ink-gray-6 -mt-px" />
<Info className="size-3.5 text-ink-gray-6" />
</HoverCardTrigger>
<HoverCardContent className="w-96" align="start" side="right">
<H4 className="text-base">{_("Closing balance as per system")}</H4>
@@ -84,15 +143,111 @@ const ClosingBalance = () => {
</Paragraph>
</HoverCardContent>
</HoverCard>
</div>
{isLoading ? <Skeleton className="w-[150px] h-5 rounded-sm" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
</StatContainer>
}
>
{isLoading ? <BalanceSkeleton /> : <BalanceValue>{formatCurrency(flt(data?.message, 2), currency)}</BalanceValue>}
</BalanceRow>
)
}
const Difference = () => {
const StatementClosingBalanceRow = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const currency = useBankCurrency()
const dates = useAtomValue(bankRecDateAtom)
const setValue = useSetAtom(bankRecClosingBalanceAtom(bankAccount?.name ?? ''))
const { data, isLoading } = useGetAccountClosingBalanceAsPerStatement({
onSuccess: (data) => {
if (data?.message && data?.message?.balance) {
setValue({
value: data?.message?.balance,
stringValue: data?.message?.balance.toString()
})
}
}
})
const isDateSame = data?.message?.date === dates.toDate
// The server uses the returned date to distinguish an unset balance from a saved zero.
const hasBalance = Boolean(data?.message?.date)
const [isOpen, setIsOpen] = useState(false)
const tooltip = hasBalance
? _("Click to change the closing balance as per statement")
: _("Click to set the closing balance as per statement")
return (
<BalanceRow
label={_("Closing (statement)")}
// The pencil sits beside the label, mirroring the info icon on the row above, so
// the figure stays a plain right-aligned number in line with every other row.
info={
<Tooltip>
<TooltipTrigger asChild>
{/* `p-0`: Tailwind's preflight gives buttons `appearance: button` but
doesn't reset padding, so a bare button picks up the UA's ~1px 6px
and knocks this row out of step with its neighbours. */}
<button
type='button'
aria-label={tooltip}
onClick={() => setIsOpen(true)}
className="cursor-pointer p-0 text-ink-gray-5 transition-colors hover:text-ink-gray-7">
<Edit className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
}
subLabel={!isDateSame && data?.message.date
? <span className="whitespace-nowrap text-2xs font-medium text-ink-red-3">
{_("As of {0}", [formatDate(data?.message?.date ?? '', 'Do MMM YYYY')])}
</span>
: undefined}
>
{/* Deliberately NOT a flex container: a flex box's baseline doesn't resolve to its
text, so the row's `items-baseline` couldn't line this up with the label. As a
plain inline button its baseline is the figure's own, like every other row.
"Set" gets the same treatment as a figure - it stands in for one. */}
{isLoading
? <BalanceSkeleton />
: <Tooltip>
<TooltipTrigger asChild>
{/* The figure styles live on the button itself - see
BALANCE_VALUE_CLASSES. `p-0` because preflight leaves the UA's
button padding in place. */}
<button
type='button'
aria-label={tooltip}
onClick={() => setIsOpen(true)}
className={cn(BALANCE_VALUE_CLASSES,
"cursor-pointer p-0 underline decoration-outline-gray-5 decoration-dashed underline-offset-4",
"transition-colors hover:decoration-ink-gray-8")}>
{hasBalance ? formatCurrency(flt(data?.message?.balance, 2), currency) : _("Set")}
</button>
</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>}
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="min-w-xl">
<ClosingBalanceForm
defaultBalance={data?.message?.balance ?? 0}
date={dates.toDate}
bankAccount={bankAccount}
onClose={() => setIsOpen(false)}
/>
</DialogContent>
</Dialog>
</BalanceRow>
)
}
const DifferenceRow = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const currency = useBankCurrency()
const { data, isLoading } = useGetAccountClosingBalance()
@@ -102,16 +257,15 @@ const Difference = () => {
const isError = difference !== 0
return <StatContainer className="w-fit text-end sm:min-w-56">
<StatLabel className="text-end">{_("Difference")}</StatLabel>
{isLoading ? <Skeleton className="w-[150px] h-5 self-end rounded-sm" /> : <StatValue className={isError ? 'text-ink-red-3 font-numeric' : 'font-numeric'}>
{formatCurrency(difference,
bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))
}</StatValue>}
</StatContainer>
return <BalanceRow label={_("Difference")} emphasis>
{isLoading
? <BalanceSkeleton />
: <BalanceValue emphasis tone={isError ? 'red' : undefined}>{formatCurrency(difference, currency)}</BalanceValue>}
</BalanceRow>
}
const ReconcileProgress = () => {
/** Reconciliation progress through the selected date range: a count plus a slim bar. */
const ReconciledRow = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
@@ -132,75 +286,14 @@ const ReconcileProgress = () => {
const progress = (totalCount ? reconciledCount / totalCount : 0) * 100
return <div className="w-[18%] flex flex-col gap-1 items-end">
<div className="w-full">
<Progress
value={progress}
max={100}
size="md"
label="Progress"
hint
hintText={`${reconciledCount} / ${totalCount} ${_("reconciled")}`} />
</div>
return <div className="flex flex-col gap-1.5">
<BalanceRow label={_("Reconciled")}>
<BalanceValue>{reconciledCount} / {totalCount ?? 0}</BalanceValue>
</BalanceRow>
<Progress value={progress} max={100} size="sm" />
</div>
}
const ClosingBalanceAsPerStatement = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const setValue = useSetAtom(bankRecClosingBalanceAtom(bankAccount?.name ?? ''))
const { data, isLoading } = useGetAccountClosingBalanceAsPerStatement({
onSuccess: (data) => {
if (data?.message && data?.message?.balance) {
setValue({
value: data?.message?.balance,
stringValue: data?.message?.balance.toString()
})
}
}
})
const isDateSame = data?.message?.date === dates.toDate
const [isOpen, setIsOpen] = useState(false)
return <StatContainer className="min-w-48">
<StatLabel>{_("Closing Balance as per statement")}</StatLabel>
<div className="flex flex-col gap-2 items-start">
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-4 underline cursor-pointer underline-offset-6" role="button">
{isLoading ? <Skeleton className="w-[150px] h-5 rounded-sm" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message?.balance, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
<Edit className="w-4 h-4" />
</div>
</TooltipTrigger>
<TooltipContent>
{_("Click to set the closing balance as per statement")}
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent className="min-w-xl">
<ClosingBalanceForm
defaultBalance={data?.message?.balance ?? 0}
date={dates.toDate}
bankAccount={bankAccount}
onClose={() => setIsOpen(false)}
/>
</DialogContent>
</Dialog>
{!isDateSame && data?.message.date && <span className="text-xs font-medium text-ink-red-3">{_("As of {0}", [formatDate(data?.message?.date ?? '', 'Do MMM YYYY')])}</span>}
</div>
</StatContainer>
}
const ClosingBalanceForm = ({ defaultBalance, date, bankAccount, onClose }: { defaultBalance: number, date: string, bankAccount: SelectedBank | null, onClose: VoidFunction }) => {
const { mutate } = useSWRConfig()
@@ -302,7 +395,7 @@ const ClosingBalancesList = ({ bankAccount, date }: { bankAccount: SelectedBank
return <div>
<Separator className="my-8" />
<p className="text-sm text-center">{_("Balances as per bank statement before {0}", [formatDate(date, 'Do MMM YYYY')])}</p>
<p className="text-p-sm text-center pb-2">{_("Balances as per bank statement before {0}", [formatDate(date, 'Do MMM YYYY')])}</p>
<Table>
<TableHeader>
<TableRow>
@@ -331,4 +424,4 @@ const ClosingBalancesList = ({ bankAccount, date }: { bankAccount: SelectedBank
}
export default BankBalance
export default BankAccountBalancePanel

View File

@@ -205,9 +205,9 @@ const BankClearanceSummaryView = () => {
const content = _("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>`])
return <div className="space-y-4 py-2">
return <div className="flex min-h-0 flex-1 flex-col space-y-4 py-2">
<div>
<div className="shrink-0">
<span className="text-p-sm">
<MarkdownRenderer content={content} />
</span>
@@ -220,8 +220,9 @@ const BankClearanceSummaryView = () => {
data={data.message.result}
columns={clearanceColumns}
getRowId={(row) => `${row.payment_entry}-${row.posting_date}`}
maxHeight="calc(100vh - 200px)"
scrollAreaClassName="min-h-[calc(100vh-200px)]"
className="min-h-0 flex-1"
maxHeight="none"
scrollAreaClassName="flex-1"
emptyState={_("No rows to display.")}
/>
) : null}

View File

@@ -74,7 +74,10 @@ const BankPicker = ({ className }: { className?: string }) => {
}
return (
<div
className={cn("flex gap-3 items-stretch w-full overflow-x-auto pe-4",
// No trailing padding: it would sit inside the fade region, so the mask would
// spend itself on empty space and the last card would stop short of the balance
// panel instead of fading towards it. The column gap provides the separation.
className={cn("flex gap-3 items-stretch w-full overflow-x-auto scroll-fade-x",
banks?.length > 4 ? 'pb-2' : '', className,
)}
style={{
@@ -108,12 +111,12 @@ const BankPickerItem = ({ bank }: { bank: SelectedBank }) => {
role="button"
title={`Select ${bank.account_name}`}
onClick={onSelect}
className={cn('rounded-md border border-outline-gray-1 max-w-60 min-w-60 p-2 overflow-hidden cursor-pointer',
// `shrink-0`: this is a horizontally scrolling row, so cards keep their own width
// instead of being compressed to fit the container.
className={cn('w-60 shrink-0 rounded-md border border-outline-gray-1 p-2 overflow-hidden cursor-pointer transition-colors',
isSelected ? 'border-outline-gray-5 bg-surface-gray-1' : 'hover:bg-surface-gray-1'
)}
>
<BankLogo bank={bank} className="mb-2" />
<div className="flex flex-col gap-1">

View File

@@ -5,107 +5,179 @@ import { AVAILABLE_TIME_PERIODS, formatDate, getDatesForTimePeriod, TimePeriod }
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { ChevronDownIcon, ChevronLeftIcon, ChevronRight } from 'lucide-react'
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Command, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { parse } from "chrono-node"
import { Calendar } from '@/components/ui/calendar'
import useFiscalYear from '@/hooks/useFiscalYear'
import dayjs from 'dayjs'
import _ from '@/lib/translate'
import { useDirection } from '@/components/ui/direction'
import useResetScrollOnSearch from '@/hooks/useResetScrollOnSearch'
const DATE_FORMAT = 'YYYY-MM-DD'
/** Current fiscal year plus this many previous ones, for quarter/year options. */
const PREVIOUS_FISCAL_YEARS = 2
type DateOption = {
/** Stable id - used as the cmdk value and the React key. */
key: string
label: string
translatedLabel: string
fromDate: string
toDate: string
format: string
/** Extra terms to match against, beyond the labels and dates. */
keywords?: string[]
/** Whether to show this option when the search box is empty. */
isDefault?: boolean
}
/**
* Fiscal years keep the same month/day boundaries year on year, so previous years can be
* derived by subtracting whole years instead of fetching them. Works for both Jan-Dec and
* Apr-Mar style fiscal years.
*/
const fiscalYearLabel = (start: dayjs.Dayjs, end: dayjs.Dayjs) =>
start.year() === end.year() ? `${start.year()}` : `${start.year()}-${end.year()}`
const BankRecDateFilter = () => {
const [bankRecDate, setBankRecDate] = useAtom(bankRecDateAtom)
const { data: fiscalYear } = useFiscalYear()
const { fiscalYear } = useFiscalYear()
const timePeriodOptions = useMemo(() => {
const standardOptions = AVAILABLE_TIME_PERIODS.map((period) => {
const today = useMemo(() => dayjs().format(DATE_FORMAT), [])
const allOptions = useMemo(() => {
const standardOptions: DateOption[] = AVAILABLE_TIME_PERIODS.map((period) => {
const dates = getDatesForTimePeriod(period)
return {
key: period,
label: period,
translatedLabel: dates.translatedLabel ?? _(period),
fromDate: dates.fromDate,
toDate: dates.toDate,
format: dates.format,
translatedLabel: dates.translatedLabel
isDefault: true,
}
})
if (fiscalYear?.message) {
// For a fiscal year, we need to replace "Last Year", "This Year", and add options for quarters
const fiscalYearStart = fiscalYear.message.year_start_date
const fiscalYearEnd = fiscalYear.message.year_end_date
const q1 = {
label: `Q1: ${fiscalYear.message.name}`,
translatedLabel: `${_("Q1")}: ${fiscalYear.message.name}`,
fromDate: fiscalYearStart,
toDate: dayjs(fiscalYearStart).add(3, 'month').format('YYYY-MM-DD'),
format: 'MMM YYYY'
}
const q2 = {
label: `Q2: ${fiscalYear.message.name}`,
translatedLabel: `${_("Q2")}: ${fiscalYear.message.name}`,
fromDate: dayjs(fiscalYearStart).add(3, 'month').format('YYYY-MM-DD'),
toDate: dayjs(fiscalYearStart).add(6, 'month').format('YYYY-MM-DD'),
format: 'MMM YYYY'
}
const q3 = {
label: `Q3: ${fiscalYear.message.name}`,
translatedLabel: `${_("Q3")}: ${fiscalYear.message.name}`,
fromDate: dayjs(fiscalYearStart).add(6, 'month').format('YYYY-MM-DD'),
toDate: dayjs(fiscalYearStart).add(9, 'month').format('YYYY-MM-DD'),
format: 'MMM YYYY'
}
const q4 = {
label: `Q4: ${fiscalYear.message.name}`,
translatedLabel: `${_("Q4")}: ${fiscalYear.message.name}`,
fromDate: dayjs(fiscalYearStart).add(9, 'month').format('YYYY-MM-DD'),
toDate: fiscalYearEnd,
format: 'MMM YYYY'
}
const thisYear = {
label: `This Fiscal Year`,
translatedLabel: `${_("This Fiscal Year")}`,
fromDate: fiscalYearStart,
toDate: fiscalYearEnd,
format: 'MMM YYYY'
}
const lastYear = {
label: `Last Fiscal Year`,
translatedLabel: `${_("Last Fiscal Year")}`,
fromDate: dayjs(fiscalYearStart).subtract(1, 'year').format('YYYY-MM-DD'),
toDate: dayjs(fiscalYearEnd).subtract(1, 'year').format('YYYY-MM-DD'),
format: 'MMM YYYY'
}
// Sort the options so that we get "This Month", "Last Month", quarters, fiscal year, then the rest of the standard options
const topRankedItems = standardOptions.filter((option) => {
return option.label === "This Month" || option.label === "Last Month"
})
const bottomRankedItems = standardOptions.filter((option) => {
return option.label !== "This Month" && option.label !== "Last Month"
})
return [...topRankedItems, q1, q2, q3, q4, thisYear, lastYear, ...bottomRankedItems]
if (!fiscalYear) {
return standardOptions
}
return standardOptions
const currentStart = dayjs(fiscalYear.year_start_date)
const currentEnd = dayjs(fiscalYear.year_end_date)
const quarterOptions: DateOption[] = []
const fiscalYearOptions: DateOption[] = []
// Static literals so the translation extractor can find them.
const quarterLabels = [_("Q1"), _("Q2"), _("Q3"), _("Q4")]
for (let yearsAgo = 0; yearsAgo <= PREVIOUS_FISCAL_YEARS; yearsAgo++) {
const start = currentStart.subtract(yearsAgo, 'year')
const end = currentEnd.subtract(yearsAgo, 'year')
// Keep the real name for the current year; derive it for the earlier ones.
const yearLabel = yearsAgo === 0 ? fiscalYear.name : fiscalYearLabel(start, end)
for (let quarter = 0; quarter < 4; quarter++) {
const quarterStart = start.add(quarter * 3, 'month')
// End the day before the next quarter starts, clamped to the fiscal year end
// so a short fiscal year can't spill over.
const nextQuarterStart = start.add((quarter + 1) * 3, 'month')
const quarterEnd = nextQuarterStart.subtract(1, 'day').isAfter(end)
? end
: nextQuarterStart.subtract(1, 'day')
if (quarterStart.isAfter(end)) continue
quarterOptions.push({
key: `Q${quarter + 1}-${yearLabel}`,
label: `Q${quarter + 1}: ${yearLabel}`,
translatedLabel: `${quarterLabels[quarter]}: ${yearLabel}`,
fromDate: quarterStart.format(DATE_FORMAT),
toDate: quarterEnd.format(DATE_FORMAT),
format: 'MMM YYYY',
keywords: ['quarter', `q${quarter + 1}`, yearLabel],
// Only the current fiscal year's quarters clutter the default list;
// older ones stay searchable.
isDefault: yearsAgo === 0,
})
}
const label = yearsAgo === 0
? 'This Fiscal Year'
: yearsAgo === 1
? 'Last Fiscal Year'
: `FY ${yearLabel}`
fiscalYearOptions.push({
key: `fiscal-year-${yearLabel}`,
label,
translatedLabel: yearsAgo <= 1 ? _(label) : `${_("FY")} ${yearLabel}`,
fromDate: start.format(DATE_FORMAT),
toDate: end.format(DATE_FORMAT),
format: 'MMM YYYY',
keywords: ['fiscal year', yearLabel],
isDefault: yearsAgo <= 1,
})
}
// "This Month"/"Last Month" first, then quarters and fiscal years, then the rest.
const topRanked = standardOptions.filter((o) => o.label === 'This Month' || o.label === 'Last Month')
const bottomRanked = standardOptions.filter((o) => o.label !== 'This Month' && o.label !== 'Last Month')
return [...topRanked, ...quarterOptions, ...fiscalYearOptions, ...bottomRanked]
}, [fiscalYear])
// Reconciliation only looks backwards, so a period that hasn't started is never useful.
const selectableOptions = useMemo(
() => allOptions.filter((option) => option.fromDate <= today),
[allOptions, today],
)
const [open, setOpen] = useState(false)
const [value, setValue] = useState("")
// We filter ourselves (`shouldFilter={false}`) so that the parsed-date suggestion can be a
// real CommandItem alongside the predefined options, and keyboard navigation covers both.
const filteredOptions = useMemo(() => {
const query = value.trim().toLowerCase()
if (!query) {
return selectableOptions.filter((option) => option.isDefault)
}
const tokens = query.split(/\s+/)
return selectableOptions.filter((option) => {
const haystack = [
option.label,
option.translatedLabel,
...(option.keywords ?? []),
option.fromDate,
option.toDate,
].join(' ').toLowerCase()
return tokens.every((token) => haystack.includes(token))
})
}, [selectableOptions, value])
const parsedOption = useMemo(() => parseDateRange(value), [value])
// Filtering shortens the list, so pin the scroll back to the top to keep the
// auto-selected first option in view.
const listRef = useResetScrollOnSearch(value)
// Don't show a parsed suggestion that duplicates an option already in the list.
const showParsedOption = parsedOption
&& !filteredOptions.some((o) => o.fromDate === parsedOption.fromDate && o.toDate === parsedOption.toDate)
const timePeriod: TimePeriod | string = useMemo(() => {
if (bankRecDate.fromDate && bankRecDate.toDate) {
// Check if the from and to dates match any predefined time period
for (const period of timePeriodOptions) {
for (const period of allOptions) {
if (period.fromDate === bankRecDate.fromDate && period.toDate === bankRecDate.toDate) {
return period.label;
}
@@ -114,10 +186,11 @@ const BankRecDateFilter = () => {
} else {
return "Date Range";
}
}, [bankRecDate.fromDate, bankRecDate.toDate, timePeriodOptions]);
}, [bankRecDate.fromDate, bankRecDate.toDate, allOptions]);
const handleTimePeriodChange = (fromDate: string, toDate: string) => {
setBankRecDate({ fromDate, toDate })
setValue("")
setOpen(false)
}
@@ -130,7 +203,9 @@ const BankRecDateFilter = () => {
const direction = useDirection()
const RangeArrow = direction === 'ltr'
? <ChevronRight className='text-[12px] text-ink-gray-5/70' />
: <ChevronLeftIcon className='text-[12px] text-ink-gray-5/70' />
return <div className='flex items-center'>
<Popover open={open} onOpenChange={setOpen}>
@@ -141,30 +216,57 @@ const BankRecDateFilter = () => {
size='md'
className='rounded-e-none border-e-0'
role="combobox">
{timePeriodOptions.find((period) => period.label === timePeriod)?.translatedLabel ?? _(timePeriod)}
{allOptions.find((period) => period.label === timePeriod)?.translatedLabel ?? _(timePeriod)}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent className="w-84 p-1" align='start'>
<Command>
<Command shouldFilter={false}>
<CommandInput placeholder="e.g. Last 3 weeks" onValueChange={setValue} value={value} />
<CommandList className='max-h-fit'>
<CommandEmpty className='text-start p-2 hover:bg-surface-gray-1'>
<EmptyState onSelect={handleTimePeriodChange} value={value} />
</CommandEmpty>
{timePeriodOptions.map((period) => (
<CommandItem key={period.label} className='flex justify-between' onSelect={() => handleTimePeriodChange(period.fromDate, period.toDate)}>
<span>
{period.translatedLabel ?? _(period.label)}
</span>
<span className='text-xs text-ink-gray-5 flex items-center gap-1 text-end whitespace-nowrap'>
{formatDate(period.fromDate, period.format)} {direction === 'ltr' ? <ChevronRight className='text-[12px] text-ink-gray-5/70' /> : <ChevronLeftIcon className='text-[12px] text-ink-gray-5/70' />} {formatDate(period.toDate, period.format)}
</span>
</CommandItem>
))}
<CommandInput placeholder={_("e.g. Last 3 weeks, Q1, May 2025")} onValueChange={setValue} value={value} />
<CommandList ref={listRef} className='max-h-80'>
{showParsedOption && parsedOption && (
<CommandGroup heading={_("Matched date")}>
<CommandItem
value='parsed-date-range'
className='flex justify-between'
onSelect={() => handleTimePeriodChange(parsedOption.fromDate, parsedOption.toDate)}>
<span className='max-w-[45%] truncate'>{value}</span>
<span className='text-xs text-ink-gray-5 flex items-center gap-1 text-end whitespace-nowrap'>
{parsedOption.fromDate === parsedOption.toDate
? formatDate(parsedOption.fromDate, 'Do MMM YYYY')
: <>{formatDate(parsedOption.fromDate, 'Do MMM YY')} {RangeArrow} {formatDate(parsedOption.toDate, 'Do MMM YY')}</>}
</span>
</CommandItem>
</CommandGroup>
)}
{filteredOptions.length > 0 && (
<CommandGroup>
{filteredOptions.map((period) => (
<CommandItem
key={period.key}
value={period.key}
className='flex justify-between'
onSelect={() => handleTimePeriodChange(period.fromDate, period.toDate)}>
<span>
{period.translatedLabel}
</span>
<span className='text-xs text-ink-gray-5 flex items-center gap-1 text-end whitespace-nowrap'>
{formatDate(period.fromDate, period.format)} {RangeArrow} {formatDate(period.toDate, period.format)}
</span>
</CommandItem>
))}
</CommandGroup>
)}
{!showParsedOption && filteredOptions.length === 0 && (
<div className='p-2 text-sm text-ink-gray-5'>
{_("No results found")}
</div>
)}
</CommandList>
</Command>
@@ -199,77 +301,97 @@ const BankRecDateFilter = () => {
}
const referentialKeywords = ["last", "this", "next", "previous"]
const EmptyState = ({ onSelect, value }: { onSelect: (fromDate: string, toDate: string) => void, value: string }) => {
const dates = useMemo(() => {
if (value) {
// Try parsing the value
const parsedDate = parse(value, undefined, { forwardDate: false })
/** chrono exposes `knownValues` on ParsingComponents but doesn't type it publicly. */
const knownValuesOf = (components: unknown): Record<string, number> =>
(components as { knownValues?: Record<string, number> })?.knownValues ?? {}
if (parsedDate && parsedDate.length > 0) {
const startDate = parsedDate[0].start.date()
const endDate = parsedDate[0].end?.date()
/**
* How far back a parsed date must move to land in the past. Reconciliation only ever looks
* backwards, so an ambiguous input that chrono resolves into the future - "December" typed in
* September, or a bare weekday like "Friday" - is pulled to its most recent past occurrence.
* An explicitly stated year is respected; a range that is still future gets discarded later.
*
* This returns a shift rather than a date so that a range can be moved as a single unit -
* shifting its start and end independently would distort or invert it.
*/
const pastShift = (date: Date, knownValues: Record<string, number>) => {
const today = dayjs()
let candidate = dayjs(date)
if (!endDate) {
const today = new Date()
// If today is greater than the start date, use today as the end date
if (startDate.getTime() > today.getTime()) {
return { fromDate: today, toDate: startDate }
} else {
// Check if the user only wants a specific month like "May 2025"
// If the "known values" just has month and year, then we need to get the first day of the month and the last day of the month
// @ts-expect-error - "Known Values" is available in the start "ParsingComponents"
if (parsedDate[0].start.knownValues?.month && !parsedDate[0].start.knownValues?.day) {
return {
fromDate: startDate,
toDate: dayjs(startDate).endOf('month').toDate()
}
// @ts-expect-error - "Known Values" is available in the start "ParsingComponents"
} else if (parsedDate[0].start.knownValues?.month && parsedDate[0].start.knownValues?.day && !referentialKeywords.some(keyword => value.toLowerCase().includes(keyword))) {
// If month and day is known, then we should not assume that the user wants to get everything until today
return {
fromDate: startDate,
toDate: startDate,
}
}
return {
fromDate: startDate,
toDate: today
}
}
} else {
return { fromDate: startDate, toDate: endDate }
}
}
}
}, [value])
const onClick = (fromDate: Date, toDate: Date) => {
onSelect(formatDate(fromDate, 'YYYY-MM-DD'), formatDate(toDate, 'YYYY-MM-DD'))
if (!candidate.isAfter(today, 'date') || knownValues.year !== undefined) {
return { amount: 0, unit: 'year' as const }
}
const isEqual = dates?.fromDate && dates?.toDate && dayjs(dates.fromDate).isSame(dates.toDate, 'date')
// A bare weekday repeats weekly, everything else (month/day) repeats yearly.
const unit = knownValues.weekday !== undefined && knownValues.day === undefined
? 'day' as const
: 'year' as const
const step = unit === 'day' ? 7 : 1
let amount = 0
return <div>
{dates ?
<div className='flex gap-2 items-center justify-between cursor-pointer' onClick={() => onClick(dates.fromDate, dates.toDate)}>
<span className='text-sm text-ink-gray-5 max-w-[30%]'>
{value}
</span>
{isEqual ? <span className='text-xs text-ink-gray-5 text-balance flex items-center gap-1'>
{formatDate(dates.fromDate, 'Do MMM YYYY')}
</span> :
<span className='text-xs text-ink-gray-5 flex items-center gap-1'>
{formatDate(dates.fromDate, 'Do MMM YY')} <ChevronRight size='16' className='text-ink-gray-5' /> {formatDate(dates.toDate, 'Do MMM YY')}
</span>}
</div> :
<span className='text-sm text-ink-gray-5'>
No results found
</span>
}
</div>
for (let i = 0; i < 200 && candidate.isAfter(today, 'date'); i++) {
candidate = candidate.subtract(step, unit)
amount += step
}
return { amount, unit }
}
export default BankRecDateFilter
/**
* Parse free text into a past date range, or return undefined when it can't be parsed or
* resolves entirely into the future.
*/
const parseDateRange = (value: string): { fromDate: string, toDate: string } | undefined => {
if (!value.trim()) return undefined
const parsedDate = parse(value, undefined, { forwardDate: false })
if (!parsedDate || parsedDate.length === 0) return undefined
const result = parsedDate[0]
const startKnownValues = knownValuesOf(result.start)
// Anchor the shift on the start and apply it to both ends, so an explicit range like
// "1st Sept to 30th Sept" keeps its shape instead of having only its end rolled back.
const shift = pastShift(result.start.date(), startKnownValues)
const startDate = dayjs(result.start.date()).subtract(shift.amount, shift.unit).toDate()
const endDate = result.end
? dayjs(result.end.date()).subtract(shift.amount, shift.unit).toDate()
: undefined
const today = new Date()
let range: { fromDate: Date, toDate: Date }
if (endDate) {
const endKnownValues = knownValuesOf(result.end)
// chrono ends "Apr 2025 to Jun 2025" on the 1st of June, but the user means all of it.
const rangeEnd = endKnownValues.month && !endKnownValues.day
? dayjs(endDate).endOf('month').toDate()
: endDate
range = { fromDate: startDate, toDate: rangeEnd }
} else if (startKnownValues.month && !startKnownValues.day) {
// The user only wants a specific month like "May 2025" - span the whole month
range = { fromDate: dayjs(startDate).startOf('month').toDate(), toDate: dayjs(startDate).endOf('month').toDate() }
} else if (startKnownValues.month && startKnownValues.day && !referentialKeywords.some(keyword => value.toLowerCase().includes(keyword))) {
// If month and day is known, then we should not assume that the user wants to get everything until today
range = { fromDate: startDate, toDate: startDate }
} else {
range = { fromDate: startDate, toDate: today }
}
// A range that hasn't started yet is never useful for reconciliation. A range that merely
// ends in the future is kept as typed, the same way "This Month" spans the whole month.
if (dayjs(range.fromDate).isAfter(today, 'date')) return undefined
if (dayjs(range.toDate).isBefore(range.fromDate, 'date')) {
range = { fromDate: range.toDate, toDate: range.fromDate }
}
return {
fromDate: dayjs(range.fromDate).format(DATE_FORMAT),
toDate: dayjs(range.toDate).format(DATE_FORMAT),
}
}
export default BankRecDateFilter

View File

@@ -191,9 +191,9 @@ const BankReconciliationStatementView = () => {
const content = _("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>`])
return <div className="space-y-4 py-2">
return <div className="flex min-h-0 flex-1 flex-col space-y-4 py-2">
<div>
<div className="shrink-0">
<span className="text-p-sm">
<MarkdownRenderer content={content} />
</span>
@@ -201,16 +201,18 @@ const BankReconciliationStatementView = () => {
{error && <ErrorBanner error={error} />}
{data && <SummarySection data={data} />}
{data && <div className="shrink-0"><SummarySection data={data} /></div>}
{data && data.message.result.length > 0 && (
<div className="space-y-2">
<p className="text-ink-gray-5 text-sm">{_("Bank Reconciliation Statement")}</p>
<div className="flex min-h-0 flex-1 flex-col space-y-2">
<p className="shrink-0 text-ink-gray-5 text-sm">{_("Bank Reconciliation Statement")}</p>
<ListView
data={statementRows}
columns={statementColumns}
getRowId={(row) => row.payment_entry}
maxHeight="min(70vh, 640px)"
className="min-h-0 flex-1"
maxHeight="none"
scrollAreaClassName="flex-1"
emptyState={_("No entries with a payment document in this list.")}
/>
</div>

View File

@@ -245,9 +245,9 @@ const BankTransactionListView = () => {
const content = _("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>`])
return <div className="space-y-2 py-2">
return <div className="flex min-h-0 flex-1 flex-col space-y-2 py-2">
<div className="flex gap-2 justify-between items-center">
<div className="flex shrink-0 gap-2 justify-between items-center">
<span className="text-p-sm">
<MarkdownRenderer content={content} />
</span>
@@ -278,8 +278,9 @@ const BankTransactionListView = () => {
data={filteredResults}
columns={transactionColumns}
getRowId={(row) => row.name}
maxHeight="calc(100vh - 200px)"
scrollAreaClassName="min-h-[calc(100vh-200px)]"
className="min-h-0 flex-1"
maxHeight="none"
scrollAreaClassName="flex-1"
emptyState={<Empty>
<EmptyMedia>
<ListIcon />

View File

@@ -19,13 +19,22 @@ import {
import { cn } from "@/lib/utils"
import _ from "@/lib/translate"
import { selectedBankAccountAtom } from "./bankRecAtoms"
import { useFrappeGetDocList } from "frappe-react-sdk"
import ErrorBanner from "@/components/ui/error-banner"
const CompanySelector = ({ onChange }: { onChange?: (company: string) => void }) => {
const [open, setOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options = window.frappe?.boot?.docs?.filter((doc: Record<string, any>) => doc.doctype === ":Company").map((company: Record<string, any>) => company.name) || []
const { data: companies, error } = useFrappeGetDocList("Company", {
limit: 0,
fields: ["name"],
}, 'company_list', {
revalidateOnFocus: false,
revalidateOnReconnect: false,
})
const options = companies?.map((company: { name: string }) => company.name) || []
const setSelectedCompany = useSetAtom(selectedCompanyAtom)
const setSelectedBankAccount = useSetAtom(selectedBankAccountAtom)
@@ -42,6 +51,10 @@ const CompanySelector = ({ onChange }: { onChange?: (company: string) => void })
}
}
if (error) {
return <ErrorBanner error={error} />
}
return (<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button

View File

@@ -181,9 +181,9 @@ const IncorrectlyClearedEntriesView = () => {
const entriesContent = _("Entries below have a posting date after {0} but the clearance date is before {1}.", [`<strong>${formattedToDate}</strong>`, `<strong>${formattedToDate}</strong>`])
return <div className="space-y-4 py-2">
return <div className="flex min-h-0 flex-1 flex-col space-y-4 py-2">
<div>
<div className="shrink-0">
<span className="text-p-sm">
<MarkdownRenderer content={content} />
<br />
@@ -198,13 +198,15 @@ const IncorrectlyClearedEntriesView = () => {
{error && <ErrorBanner error={error} />}
{data && data.message.result.length > 0 && (
<div className="space-y-2">
<p className="text-ink-gray-5 text-sm">{_("Incorrectly cleared entries as per the report.")}</p>
<div className="flex min-h-0 flex-1 flex-col space-y-2">
<p className="shrink-0 text-ink-gray-5 text-sm">{_("Incorrectly cleared entries as per the report.")}</p>
<ListView
data={data.message.result}
columns={incorrectlyClearedColumns}
getRowId={(row) => `${row.payment_entry}-${row.posting_date}`}
maxHeight="min(70vh, 640px)"
className="min-h-0 flex-1"
maxHeight="none"
scrollAreaClassName="flex-1"
emptyState={_("No rows to display.")}
/>
</div>

View File

@@ -37,7 +37,7 @@ import { Link } from "react-router"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { InputGroup, InputGroupAddon, InputGroupText } from "@/components/ui/input-group"
const MatchAndReconcile = ({ contentHeight }: { contentHeight: number }) => {
const MatchAndReconcile = () => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
if (!selectedBank) {
@@ -52,15 +52,15 @@ const MatchAndReconcile = ({ contentHeight }: { contentHeight: number }) => {
}
return <>
<div className={`flex items-start space-x-2`} >
<div className="flex-1">
<H4 className="text-sm font-medium">{_("Unreconciled Transactions")}</H4>
<UnreconciledTransactions contentHeight={contentHeight} />
<div className="flex min-h-0 flex-1 items-stretch space-x-2" >
<div className="flex min-h-0 flex-1 flex-col">
<H4 className="shrink-0 text-sm font-medium">{_("Unreconciled Transactions")}</H4>
<UnreconciledTransactions />
</div>
<Separator orientation="vertical" style={{ minHeight: `${contentHeight}px` }} />
<div className="flex-1 px-1">
<H4 className="text-sm font-medium">{_("Match or Create")}</H4>
<VouchersSection contentHeight={contentHeight} />
<Separator orientation="vertical" className="self-stretch" />
<div className="flex min-h-0 flex-1 flex-col px-1">
<H4 className="shrink-0 text-sm font-medium">{_("Match or Create")}</H4>
<VouchersSection />
</div>
</div>
<TransferModal />
@@ -69,16 +69,19 @@ const MatchAndReconcile = ({ contentHeight }: { contentHeight: number }) => {
</>
}
/** TanStack requires `estimateSize` for initial scroll range; `measureElement` on each row sets the real height. */
/**
* TanStack requires `estimateSize` for initial scroll range; `measureElement` on each row sets
* the real height. The scroll container fills its flex parent rather than taking a pixel
* height - the virtualizer observes its own rect, so it stays correct across resizes and any
* layout change above it.
*/
function VirtualizedListBody<T>({
items,
height,
getItemKey,
children,
estimateSize = 74,
}: {
items: T[]
height: number
getItemKey: (item: T, index: number) => string | number
children: (item: T, index: number) => React.ReactNode
estimateSize?: number
@@ -100,8 +103,7 @@ function VirtualizedListBody<T>({
return (
<div
ref={scrollRef}
className="overflow-auto contain-strict"
style={{ height }}
className="min-h-0 flex-1 overflow-auto contain-strict"
>
<div
className="relative w-full"
@@ -123,7 +125,7 @@ function VirtualizedListBody<T>({
)
}
const UnreconciledTransactions = ({ contentHeight }: { contentHeight: number }) => {
const UnreconciledTransactions = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const currency = bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '')
@@ -187,14 +189,13 @@ const UnreconciledTransactions = ({ contentHeight }: { contentHeight: number })
}
const hasFilters = search !== '' || typeFilter !== 'All' || amountFilter.value !== 0
const listHeight = contentHeight - 72
if (isLoading) {
return <UnreconciledTransactionsLoadingState />
}
return <div className="space-y-1">
<div className="flex py-2 w-full gap-2">
return <div className="flex min-h-0 flex-1 flex-col space-y-1">
<div className="flex py-2 w-full gap-2 shrink-0">
<InputGroup variant='outline'>
<label className="sr-only">{_("Search transactions")}</label>
@@ -278,7 +279,6 @@ const UnreconciledTransactions = ({ contentHeight }: { contentHeight: number })
<VirtualizedListBody
items={results}
height={listHeight}
estimateSize={74}
getItemKey={(transaction) => transaction.name}
>
@@ -381,7 +381,7 @@ const UnreconciledTransactionItem = ({ transaction }: { transaction: Unreconcile
}
const VouchersSection = ({ contentHeight }: { contentHeight: number }) => {
const VouchersSection = () => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
const selectedTransactions = useAtomValue(bankRecSelectedTransactionAtom(selectedBank?.name || ''))
@@ -402,8 +402,8 @@ const VouchersSection = ({ contentHeight }: { contentHeight: number }) => {
return <OptionsForMultipleTransactions transactions={selectedTransactions} />
}
return <div style={{ minHeight: contentHeight }} className="mt-2">
<OptionsForSingleTransaction transaction={selectedTransactions[0]} contentHeight={contentHeight} />
return <div className="mt-2 flex min-h-0 flex-1 flex-col">
<OptionsForSingleTransaction transaction={selectedTransactions[0]} />
</div>
}
@@ -535,11 +535,11 @@ const OptionsForMultipleTransactions = ({ transactions }: { transactions: Unreco
}
const OptionsForSingleTransaction = ({ transaction, contentHeight }: { transaction: UnreconciledTransaction, contentHeight: number }) => {
const OptionsForSingleTransaction = ({ transaction }: { transaction: UnreconciledTransaction }) => {
const { setTransferModalOpen, setRecordPaymentModalOpen, setRecordJournalEntryModalOpen } = useKeyboardShortcuts()
return <div className="flex flex-col gap-3">
return <div className="flex min-h-0 flex-1 flex-col gap-3">
<TooltipProvider>
<div className="flex items-center justify-between pt-2">
<div className="flex gap-4 justify-center">
@@ -602,7 +602,7 @@ const OptionsForSingleTransaction = ({ transaction, contentHeight }: { transacti
</div>
</TooltipProvider>
{transaction.matched_transaction_rule && <RuleAction transaction={transaction} />}
<VouchersForTransaction transaction={transaction} contentHeight={contentHeight} />
<VouchersForTransaction transaction={transaction} />
</div>
}
@@ -774,12 +774,11 @@ const RuleAction = ({ transaction }: { transaction: UnreconciledTransaction }) =
)
}
const VouchersForTransaction = ({ transaction, contentHeight }: { transaction: UnreconciledTransaction, contentHeight: number }) => {
const VouchersForTransaction = ({ transaction }: { transaction: UnreconciledTransaction }) => {
const { data: vouchers, isLoading, error } = useGetVouchersForTransaction(transaction)
const voucherList = vouchers?.message ?? []
const listHeight = contentHeight - 120
if (error) {
return <ErrorBanner error={error} />
@@ -801,8 +800,8 @@ const VouchersForTransaction = ({ transaction, contentHeight }: { transaction: U
</div>
}
return <div className="relative space-y-2">
<div className="flex items-center gap-2 text-sm text-ink-gray-5">
return <div className="relative flex min-h-0 flex-1 flex-col space-y-2">
<div className="flex shrink-0 items-center gap-2 text-sm text-ink-gray-5">
<Separator className="flex-1" />
<span>or</span>
<Separator className="flex-1" />
@@ -818,7 +817,6 @@ const VouchersForTransaction = ({ transaction, contentHeight }: { transaction: U
</Empty>}
<VirtualizedListBody
items={voucherList}
height={listHeight}
estimateSize={121}
getItemKey={(voucher) => voucher.name}
>

View File

@@ -59,8 +59,8 @@ const SelectedTransactionDetails = ({ transaction, showAccount = false, account
</div>
</div>
<div className='flex flex-col gap-1'>
<span className='text-sm'>{transaction.description}</span>
{transaction.reference_number ? <span className='text-sm text-ink-gray-5'>{_("Ref")}: {transaction.reference_number}</span> : null}
<span className='text-p-sm'>{transaction.description}</span>
{transaction.reference_number ? <span className='text-p-sm text-ink-gray-5'>{_("Ref")}: {transaction.reference_number}</span> : null}
{showAccount && account ? <span className='text-sm text-ink-gray-5'>{_("GL Account")}: {account}</span> : null}
</div>

View File

@@ -490,7 +490,7 @@ const RecommendedTransferAccount = ({ transaction, onAccountChange }: { transact
<Calendar size='16px' />
<span className='text-sm'>{formatDate(data.message.date, 'Do MMM YYYY')}</span>
</div>
<span className='text-sm line-clamp-1' title={data.message.description}>{data.message.description}</span>
<span className='text-p-sm line-clamp-1' title={data.message.description}>{data.message.description}</span>
</div>
</div>
</div>

View File

@@ -231,7 +231,7 @@ export const BANK_LOGOS: { keywords: string[], logo: string, locale?: string[],
{
keywords: ['Federal Bank'],
logo: 'Federal_Bank.png',
logoDark: 'Federal_Bank-dark.png',
logoDark: 'Federal_Bank-Dark.png',
locale: ['India']
},
{

View File

@@ -83,10 +83,13 @@ const StatementDetails = ({ data }: Props) => {
}
// `progress` is a percentage (drives the bar); `current`/`total` are actual counts.
const [progress, setProgress] = useState(0)
const [imported, setImported] = useState({ current: 0, total: 0 })
useFrappeEventListener("bank-rec-statement-import-progress", (event) => {
setProgress(event.progress)
setImported({ current: event.current ?? 0, total: event.total ?? 0 })
})
const file_name = data.doc.file.split("/").pop() ?? ""
@@ -112,7 +115,9 @@ const StatementDetails = ({ data }: Props) => {
{data.doc.status === 'Completed' ? <Badge theme='green'>{_("Completed")}</Badge> :
<Button onClick={onImport} disabled={loading || data.final_transactions?.length === 0} size='sm' type='button'>
{loading ? <Loader2Icon className='size-4 animate-spin' /> : null}
{loading ? _("Importing...") : _("Import {0} transactions", [data.final_transactions?.length?.toString() || "0"])}</Button>
{loading ? _("Importing...") : data.final_transactions?.length === 1
? _("Import 1 transaction")
: _("Import {0} transactions", [data.final_transactions?.length?.toString() || "0"])}</Button>
}
</div>
<div className='flex items-start gap-4'>
@@ -129,7 +134,9 @@ const StatementDetails = ({ data }: Props) => {
</div>
{progress > 0 && <div className='flex flex-col gap-2'><Progress value={progress} max={100} size="lg" />
<span className='text-sm'>{_("Importing {0} transactions", [progress.toString()])}
<span className='text-sm'>{imported.total === 1
? _("Importing 1 transaction")
: _("Importing {0} of {1} transactions", [imported.current.toString(), imported.total.toString()])}
</span>
</div>}

View File

@@ -387,7 +387,7 @@ function ListViewInner<TData>({
)}
role="columnheader"
>
<div className="min-w-0 flex-1 truncate">
<div className="min-w-0 flex-1 truncate leading-snug">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}

View File

@@ -1,7 +1,9 @@
import { useAtomValue } from "jotai"
import { atomWithStorage } from "jotai/utils"
export const selectedCompanyAtom = atomWithStorage<string>('bank-rec-selected-company', window.frappe?.boot?.user?.defaults?.company || '')
export const selectedCompanyAtom = atomWithStorage<string>('bank-rec-selected-company', window.frappe?.boot?.user?.defaults?.company || '', undefined, {
getOnInit: true,
})
export const useCurrentCompany = () => {
const selectedCompany = useAtomValue(selectedCompanyAtom)

View File

@@ -1,13 +1,58 @@
import { useFrappeGetCall } from "frappe-react-sdk"
import { useMemo } from "react"
import dayjs from "dayjs"
import { useCurrentCompany } from "./useCurrentCompany"
const useFiscalYear = () => {
return useFrappeGetCall("erpnext.accounts.utils.get_fiscal_year", undefined, 'fiscal_year', {
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false
})
export type FiscalYear = {
name: string
year_start_date: string
year_end_date: string
}
export default useFiscalYear
/**
* The fiscal year containing today, for the currently selected company.
*
* `company` matters in multi-company setups, where fiscal years can be restricted to
* specific companies. `date` matters because without it `get_fiscal_year` returns the newest
* fiscal year in the system (they're ordered by start date, descending) - which may be one
* created in advance for a year that hasn't started.
*/
const useFiscalYear = () => {
const company = useCurrentCompany()
const { data, ...rest } = useFrappeGetCall<{ message: FiscalYear | [string, string, string] | false }>(
"erpnext.accounts.utils.get_fiscal_year",
{
date: dayjs().format("YYYY-MM-DD"),
company,
as_dict: 1,
// Return nothing instead of throwing/msgprinting when no fiscal year covers today.
raise_on_missing: 0,
verbose: 0,
},
company ? `fiscal_year_${company}` : null,
{
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false
}
)
// get_fiscal_year returns a dict with as_dict, a (name, start, end) tuple without it, and
// false when there's no match - normalise all three.
const fiscalYear = useMemo<FiscalYear | undefined>(() => {
const message = data?.message
if (!message) return undefined
if (Array.isArray(message)) {
const [name, year_start_date, year_end_date] = message
return { name, year_start_date, year_end_date }
}
return message
}, [data])
return { fiscalYear, ...rest }
}
export default useFiscalYear

View File

@@ -0,0 +1,23 @@
import { useLayoutEffect, useRef } from "react"
/**
* Pins a scrollable list back to the top whenever the search term changes.
*
* Dropdowns that do their own filtering (`shouldFilter={false}`) swap a long list for a much
* shorter one while the scroll container keeps its previous offset - which can leave the
* auto-selected first item scrolled out of view.
*
* Returns a ref to attach to the scroll container (e.g. `CommandList`).
*/
const useResetScrollOnSearch = (search: string) => {
const listRef = useRef<HTMLDivElement>(null)
// Layout effect so the reset lands before paint, avoiding a visible jump.
useLayoutEffect(() => {
listRef.current?.scrollTo({ top: 0 })
}, [search])
return listRef
}
export default useResetScrollOnSearch

View File

@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "./styles/scroll-fade.css";
@font-face {
font-family: InterVariable;

View File

@@ -1,4 +1,4 @@
import BankBalance from "@/components/features/BankReconciliation/BankBalance"
import BankAccountBalancePanel from "@/components/features/BankReconciliation/BankBalance"
import BankPicker from "@/components/features/BankReconciliation/BankPicker"
import BankRecDateFilter from "@/components/features/BankReconciliation/BankRecDateFilter"
import BankTransactionUnreconcileModal from "@/components/features/BankReconciliation/BankTransactionUnreconcileModal"
@@ -9,10 +9,9 @@ import ActionLog from "@/components/features/ActionLog/ActionLog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { TooltipProvider } from "@/components/ui/tooltip"
import _ from "@/lib/translate"
import { lazy, Suspense, useLayoutEffect, useRef, useState } from "react"
import { lazy, Suspense } from "react"
import { AlertTriangleIcon, CheckCircleIcon, HomeIcon, LandmarkIcon, ListIcon, Loader2Icon, ScrollTextIcon, ShuffleIcon } from "lucide-react"
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb"
import { Badge } from "@/components/ui/badge"
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import { Button } from "@/components/ui/button"
import { useAtomValue } from "jotai"
@@ -25,23 +24,13 @@ const IncorrectlyClearedEntries = lazy(() => import('@/components/features/BankR
const BankReconciliation = () => {
const [headerHeight, setHeaderHeight] = useState(0)
const ref = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (ref.current) {
setHeaderHeight(ref.current.clientHeight)
}
}, [])
const remainingHeightAfterTabs = window.innerHeight - headerHeight - 220
return (
<div>
<div className="p-4 flex-col gap-4 md:flex hidden">
<div ref={ref} className="flex flex-col gap-4">
<div className="flex justify-between">
{/* The page owns the viewport height and the tabs/lists below fill what's left, so
the virtualizers size themselves from layout instead of a measured pixel value. */}
<div className="px-2 pt-1 flex-col gap-4 md:flex hidden h-dvh">
<div className="flex flex-col gap-4 shrink-0">
<div className="flex justify-between shrink-0">
<div className="flex items-center gap-6">
<Breadcrumb>
<BreadcrumbList>
@@ -54,7 +43,7 @@ const BankReconciliation = () => {
<BreadcrumbItem>
<BreadcrumbPage>
<div className="flex gap-1 items-center">
{_("Banking")} <Badge theme="violet" variant="subtle">{_("Beta")}</Badge>
{_("Banking")}
</div>
</BreadcrumbPage>
@@ -71,10 +60,8 @@ const BankReconciliation = () => {
<BankRecDateFilter />
</div>
</div>
<BankPicker />
<BankBalance />
</div>
<BankRecTabs remainingHeightAfterTabs={remainingHeightAfterTabs} />
<BankRecWorkspace />
<BankTransactionUnreconcileModal />
</div>
<div className="md:hidden flex h-screen items-center justify-between">
@@ -104,42 +91,53 @@ const BankReconciliation = () => {
)
}
const BankRecTabs = ({ remainingHeightAfterTabs }: { remainingHeightAfterTabs: number }) => {
const BankRecWorkspace = () => {
const selectedBankAccount = useAtomValue(selectedBankAccountAtom)
if (!selectedBankAccount) {
return null
}
return <Tabs defaultValue="Match and Reconcile">
<TabsList>
<TabsTrigger value="Match and Reconcile"><ShuffleIcon /> {_("Match and Reconcile")}</TabsTrigger>
<TabsTrigger value="Bank Reconciliation Statement"><ScrollTextIcon /> {_("Bank Reconciliation Statement")}</TabsTrigger>
<TabsTrigger value="Bank Transactions"><ListIcon />{_("Bank Transactions")}</TabsTrigger>
<TabsTrigger value="Bank Clearance Summary"><CheckCircleIcon />{_("Bank Clearance Summary")}</TabsTrigger>
<TabsTrigger value="Incorrectly Cleared Entries"><AlertTriangleIcon /> {_("Incorrectly Cleared Entries")}</TabsTrigger>
</TabsList>
<TabsContent value="Match and Reconcile">
<MatchAndReconcile contentHeight={remainingHeightAfterTabs} />
</TabsContent>
<Suspense fallback={
<div className="flex items-center justify-center p-16">
<Loader2Icon className="size-6 animate-spin text-muted-foreground" />
return <Tabs defaultValue="Match and Reconcile" className="min-h-0 flex-1 gap-4">
{/* Picker + tab strip stack on the left, balance panel beside them - the tab strip
fills height the panel needs anyway, so it costs no row of its own. The picker
scrolls horizontally (`min-w-0` lets it shrink so its overflow-x engages) while
the panel stays put, so the figures never scroll away. */}
{/* No gap here: the panel's own `border-s ps-4` supplies the separation, and a gap
would leave dead space the picker's edge fade can't reach. */}
<div className="flex shrink-0 items-stretch">
<div className="flex min-w-0 flex-1 flex-col justify-between gap-3">
<BankPicker />
{selectedBankAccount && <TabsList>
<TabsTrigger value="Match and Reconcile"><ShuffleIcon /> {_("Match and Reconcile")}</TabsTrigger>
<TabsTrigger value="Bank Reconciliation Statement"><ScrollTextIcon /> {_("Reconciliation Statement")}</TabsTrigger>
<TabsTrigger value="Bank Transactions"><ListIcon />{_("Transactions")}</TabsTrigger>
<TabsTrigger value="Bank Clearance Summary"><CheckCircleIcon />{_("Clearance Summary")}</TabsTrigger>
<TabsTrigger value="Incorrectly Cleared Entries"><AlertTriangleIcon /> {_("Incorrectly Cleared")}</TabsTrigger>
</TabsList>}
</div>
}>
<TabsContent value="Bank Reconciliation Statement">
<BankReconciliationStatement />
{selectedBankAccount && <BankAccountBalancePanel />}
</div>
{selectedBankAccount && <>
<TabsContent value="Match and Reconcile" className="flex min-h-0 flex-col">
<MatchAndReconcile />
</TabsContent>
<TabsContent value="Bank Transactions">
<BankTransactions />
</TabsContent>
<TabsContent value="Bank Clearance Summary">
<BankClearanceSummary />
</TabsContent>
<TabsContent value="Incorrectly Cleared Entries">
<IncorrectlyClearedEntries />
</TabsContent>
</Suspense>
<Suspense fallback={
<div className="flex items-center justify-center p-16">
<Loader2Icon className="size-6 animate-spin text-muted-foreground" />
</div>
}>
<TabsContent value="Bank Reconciliation Statement" className="flex min-h-0 flex-col">
<BankReconciliationStatement />
</TabsContent>
<TabsContent value="Bank Transactions" className="flex min-h-0 flex-col">
<BankTransactions />
</TabsContent>
<TabsContent value="Bank Clearance Summary" className="flex min-h-0 flex-col">
<BankClearanceSummary />
</TabsContent>
<TabsContent value="Incorrectly Cleared Entries" className="flex min-h-0 flex-col">
<IncorrectlyClearedEntries />
</TabsContent>
</Suspense>
</>}
</Tabs>
}

View File

@@ -226,7 +226,7 @@ const StatementImportLog = () => {
field: "creation",
order: "desc"
},
limit: 10
limit: 20
}, bankAccount ? undefined : null, {
revalidateOnFocus: false
})

View File

@@ -0,0 +1,94 @@
/* Scroll-edge fade mask for horizontal scroll containers (the bank picker strip).
Ported from Raven's `scroll-fade-x`; imported by index.css, since Tailwind processes
`@utility` in imported files the same as in the entry file.
The scroll-timeline keyframes reveal each edge's fade only when there IS content to scroll
in that direction - no fade on the left edge when scrolled fully left, none on the right at
the end. `@property` makes the fade animate smoothly rather than jumping.
Without scroll-timeline support (Firefox) there is deliberately NO fade at all: the fade
vars stay at their 0px initial value and the gradient stops collapse to the edges. A static
both-edges fallback was tried in Raven and removed - on a container with nothing to scroll
it dimmed the edges anyway, promising content that didn't exist. */
@property --scroll-fade-l {
/* length-percentage, NOT length: the fade size is min(12%, …) - a percentage. A <length>
property rejects that value and reverts to initial-value (0px), zeroing the fade. */
syntax: "<length-percentage>";
inherits: false;
initial-value: 0px;
}
@property --scroll-fade-r {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0px;
}
@keyframes scroll-fade-reveal-l {
from {
--scroll-fade-l: 0px;
}
to {
--scroll-fade-l: var(--_scroll-fade-size-l);
}
}
@keyframes scroll-fade-reveal-r {
from {
--scroll-fade-r: var(--_scroll-fade-size-r);
}
to {
--scroll-fade-r: 0px;
}
}
@utility scroll-fade-x {
--_scroll-fade-size-l: var(--scroll-fade-l-size,
var(--scroll-fade-size, min(12%, calc(var(--spacing, 0.25rem) * 10))));
--_scroll-fade-size-r: var(--scroll-fade-r-size,
var(--scroll-fade-size, min(12%, calc(var(--spacing, 0.25rem) * 10))));
/* Eased (smoothstep) alpha ramp, sampled finely so it reads as a smooth curve, NOT fading
all the way to transparent: the edge floors at 0.25 (content dims, never vanishes), ramping
up to a full 1 for the body. The opaque end MUST be 1 or everything would be permanently
dimmed. Stops collapse to the edge when the size animates to 0, so the true first/last card
is never dimmed at rest. Tune the floor - higher (~0.4) = subtler, lower (~0.1) = stronger. */
--scroll-fade-inline: linear-gradient(to right,
rgba(0, 0, 0, 0.25) 0,
rgba(0, 0, 0, 0.282) calc(var(--scroll-fade-l, 0px) * 0.125),
rgba(0, 0, 0, 0.367) calc(var(--scroll-fade-l, 0px) * 0.25),
rgba(0, 0, 0, 0.487) calc(var(--scroll-fade-l, 0px) * 0.375),
rgba(0, 0, 0, 0.625) calc(var(--scroll-fade-l, 0px) * 0.5),
rgba(0, 0, 0, 0.763) calc(var(--scroll-fade-l, 0px) * 0.625),
rgba(0, 0, 0, 0.883) calc(var(--scroll-fade-l, 0px) * 0.75),
rgba(0, 0, 0, 0.968) calc(var(--scroll-fade-l, 0px) * 0.875),
rgba(0, 0, 0, 1) var(--scroll-fade-l, 0px),
rgba(0, 0, 0, 1) calc(100% - var(--scroll-fade-r, 0px)),
rgba(0, 0, 0, 0.968) calc(100% - var(--scroll-fade-r, 0px) * 0.875),
rgba(0, 0, 0, 0.883) calc(100% - var(--scroll-fade-r, 0px) * 0.75),
rgba(0, 0, 0, 0.763) calc(100% - var(--scroll-fade-r, 0px) * 0.625),
rgba(0, 0, 0, 0.625) calc(100% - var(--scroll-fade-r, 0px) * 0.5),
rgba(0, 0, 0, 0.487) calc(100% - var(--scroll-fade-r, 0px) * 0.375),
rgba(0, 0, 0, 0.367) calc(100% - var(--scroll-fade-r, 0px) * 0.25),
rgba(0, 0, 0, 0.282) calc(100% - var(--scroll-fade-r, 0px) * 0.125),
rgba(0, 0, 0, 0.25) 100%);
-webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));
mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));
-webkit-mask-composite: source-in;
mask-composite: intersect;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
@supports (animation-timeline: scroll()) {
animation:
scroll-fade-reveal-l 1ms ease-in-out,
scroll-fade-reveal-r 1ms ease-in-out;
animation-timeline: scroll(self x), scroll(self x);
animation-range:
0 var(--scroll-fade-reveal, calc(var(--spacing, 0.25rem) * 24)),
calc(100% - var(--scroll-fade-reveal, calc(var(--spacing, 0.25rem) * 24))) 100%;
animation-fill-mode: both;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,5 @@
preserve_hierarchy: true
files:
- source: /erpnext/locale/main.pot
translation: /erpnext/locale/%two_letters_code%.po
pull_request_title: "fix: sync translations from crowdin"
pull_request_labels:
- translation
- skip-release-notes
pull_request_reviewers:
- barredterra # change to your GitHub username if you copied this file
commit_message: "fix: %language% translations"
append_commit_message: false
languages_mapping:
two_letters_code:
pt-BR: pt_BR

View File

@@ -6,88 +6,148 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_outstanding_reference_documents,
get_payment_entry,
)
from erpnext.utilities.bulk_transaction import transaction_processing
@frappe.whitelist(methods=["POST"])
def create_payment_entries(
grouped_invoices: str | list | None = None,
ungrouped_invoices: str | list | None = None,
):
def create_payment_entries(invoices: str | list | None = None):
"""Create draft Payment Entries from AP report invoice selection."""
frappe.has_permission("Payment Entry", "create", throw=True)
grouped_invoices = [d for d in frappe.parse_json(grouped_invoices or "[]") if d.get("voucher_no")]
ungrouped_invoices = [d for d in frappe.parse_json(ungrouped_invoices or "[]") if d.get("voucher_no")]
if not grouped_invoices and not ungrouped_invoices:
names = [d["voucher_no"] for d in frappe.parse_json(invoices or "[]") if d.get("voucher_no")]
if not names:
frappe.throw(_("No Purchase Invoices selected"))
if ungrouped_invoices:
data = [{"name": d["voucher_no"]} for d in ungrouped_invoices]
transaction_processing(data, "Purchase Invoice", "Payment Entry")
payable, excluded = _partition_payable_invoices(names)
if not payable:
frappe.throw(_("None of the selected invoices are payable"))
if grouped_invoices:
groups = {}
for d in grouped_invoices:
key = (d["supplier"], d["party_account"])
groups.setdefault(
key, {"supplier": d["supplier"], "party_account": d["party_account"], "vouchers": []}
)["vouchers"].append(d["voucher_no"])
# invoices sharing a (supplier, payable account) are combined into one Payment Entry
groups = {}
for d in payable:
key = (d["supplier"], d["party_account"])
groups.setdefault(
key, {"supplier": d["supplier"], "party_account": d["party_account"], "vouchers": []}
)["vouchers"].append(d["voucher_no"])
frappe.msgprint(
_("Started a background job to create {0} Grouped Payment Entries").format(len(groups))
)
frappe.enqueue(
make_grouped_payment_entries,
queue="long",
timeout=1500,
groups=list(groups.values()),
)
def make_grouped_payment_entries(groups):
created, failed = 0, 0
for group in groups:
supplier = group["supplier"]
try:
frappe.db.savepoint("bulk_pe")
pe = _build_grouped_payment_entry(supplier, group["party_account"], group["vouchers"])
if not pe:
frappe.db.rollback(save_point="bulk_pe")
failed += 1
frappe.log_error(
title=_("Bulk Payment Entry skipped for {0}").format(supplier),
message=_(
"No outstanding invoices found for the selected vouchers in account {0}"
).format(group["party_account"]),
)
continue
pe.flags.ignore_validate = True
pe.set_title_field()
pe.insert(ignore_mandatory=True)
for group in groups.values():
if _create_payment_entry(group):
created += 1
except Exception:
frappe.db.rollback(save_point="bulk_pe")
else:
failed += 1
frappe.log_error(title=_("Bulk Payment Entry creation failed for {0}").format(supplier))
message = _("Created {0} draft Grouped Payment Entries").format(created)
message = _("Created {0} draft Payment Entries").format(created)
if excluded:
message += "" + _("{0} excluded (not payable)").format(len(excluded))
if failed:
message += "" + _("{0} skipped (see Error Log)").format(failed)
message += "" + _("{0} failed (see Error Log)").format(failed)
frappe.msgprint(message, title=_("Bulk Payment Entries"), indicator="green")
frappe.publish_realtime(
"msgprint",
{"message": message, "title": _("Bulk Payment Entries"), "indicator": "green"},
user=frappe.session.user,
after_commit=True,
@frappe.whitelist()
def get_payable_invoices(invoices: str | list | None = None):
"""Return the live payable subset of the selected invoices for the report dialog."""
frappe.has_permission("Payment Entry", "create", throw=True)
names = [d["voucher_no"] for d in frappe.parse_json(invoices or "[]") if d.get("voucher_no")]
payable, excluded = _partition_payable_invoices(names)
currency = None
if payable:
company = frappe.get_cached_value("Purchase Invoice", payable[0]["voucher_no"], "company")
currency = frappe.get_cached_value("Company", company, "default_currency")
return {"payable": payable, "excluded": excluded, "currency": currency}
def _partition_payable_invoices(names):
"""Split submitted Purchase Invoices into payable ones and excluded ones (with reason).
Returns are debit notes, internal transfers are inter-company, and non-positive
outstanding means already settled — none are valid targets for a supplier payment.
"""
if not names:
return [], []
rows = frappe.get_list(
"Purchase Invoice",
filters={"name": ["in", names], "docstatus": 1},
fields=[
"name",
"supplier",
"credit_to",
"outstanding_amount",
"conversion_rate",
"is_return",
"is_internal_supplier",
],
limit_page_length=0,
)
payable, excluded = [], []
for r in rows:
if r.is_return:
excluded.append({"voucher_no": r.name, "reason": _("Debit Note")})
elif r.is_internal_supplier:
excluded.append({"voucher_no": r.name, "reason": _("Internal Transfer")})
elif flt(r.outstanding_amount) <= 0:
excluded.append({"voucher_no": r.name, "reason": _("Already Paid")})
else:
payable.append(
{
"voucher_no": r.name,
"supplier": r.supplier,
"party_account": r.credit_to,
"outstanding": flt(r.outstanding_amount) * flt(r.conversion_rate or 1),
}
)
# names not returned were cancelled/deleted or no longer readable after the report loaded
found = {r.name for r in rows}
for name in names:
if name not in found:
excluded.append({"voucher_no": name, "reason": _("Not available")})
return payable, excluded
def _create_payment_entry(group):
supplier = group["supplier"]
try:
frappe.db.savepoint("bulk_pe")
if len(group["vouchers"]) == 1:
pe = _build_single_payment_entry(group["vouchers"][0])
else:
pe = _build_grouped_payment_entry(supplier, group["party_account"], group["vouchers"])
if not pe:
frappe.db.rollback(save_point="bulk_pe")
frappe.log_error(
title=_("Bulk Payment Entry skipped for {0}").format(supplier),
message=_("No outstanding amount for the selected invoice(s)."),
)
return False
pe.flags.ignore_validate = True
pe.set_title_field()
pe.insert(ignore_mandatory=True)
return True
except Exception:
frappe.db.rollback(save_point="bulk_pe")
frappe.log_error(title=_("Bulk Payment Entry creation failed for {0}").format(supplier))
return False
def _build_single_payment_entry(name):
pe = get_payment_entry("Purchase Invoice", name)
# guard against a stale report row: nothing to allocate means the invoice is already settled
if not pe.references or not any(flt(r.allocated_amount) for r in pe.references):
return None
return pe
def _build_grouped_payment_entry(supplier, party_account, names):
name_set = set(names)
pe = get_payment_entry("Purchase Invoice", names[0])
pe.set("references", [])
@@ -101,8 +161,9 @@ def _build_grouped_payment_entry(supplier, party_account, names):
}
)
# get_negative_outstanding_invoices ignores the vouchers filter, so bound refs to the selection
for r in refs:
if r.voucher_type != "Purchase Invoice":
if r.voucher_type != "Purchase Invoice" or r.voucher_no not in name_set:
continue
pe.append(
"references",

View File

@@ -17,7 +17,7 @@ class ERPNextAddress(Address):
def link_address(self):
"""Link address based on owner"""
if self.is_your_company_address:
if self.get("is_your_company_address"):
return
return super().link_address()
@@ -28,7 +28,9 @@ class ERPNextAddress(Address):
self.is_your_company_address = 1
def validate_reference(self):
if self.is_your_company_address and not [row for row in self.links if row.link_doctype == "Company"]:
if self.get("is_your_company_address") and not [
row for row in self.links if row.link_doctype == "Company"
]:
frappe.throw(
_(
"Address needs to be linked to a Company. Please add a row for Company in the Links table."
@@ -45,14 +47,29 @@ class ERPNextAddress(Address):
super().on_update()
address_display = get_address_display(self.as_dict())
filters = {"customer_primary_address": self.name}
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
for customer_name in customers:
frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display)
customers = frappe.db.get_all(
"Customer", filters={"customer_primary_address": self.name}, pluck="name"
)
for customer in customers:
frappe.db.set_value(
"Customer", customer, "primary_address", address_display, update_modified=False
)
@frappe.whitelist()
def get_shipping_address(company: str, address: str | None = None):
# `company` is caller supplied and this returns that company's own registered address with every
# field. `select` rather than `read` on Company: Delivery, Maintenance, Purchase Manager and
# Stock Manager all fill in transactions that ask for this while holding no Company `read` row.
frappe.has_permission("Company", ptype="select", throw=True)
# and scope it to the caller's own Company restrictions, which costs nobody who has none
from erpnext.stock.doctype.company_restriction.company_restriction import get_allowed_companies
allowed_companies = get_allowed_companies(frappe.session.user, "Address")
if allowed_companies and company not in allowed_companies:
frappe.throw(_("Not permitted for {0}").format(company), frappe.PermissionError)
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],
@@ -69,4 +86,6 @@ def get_shipping_address(company: str, address: str | None = None):
if address:
address_as_dict = address[0]
name, address_template = get_address_templates(address_as_dict)
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict)
return address_as_dict.get("name"), frappe.render_template(
address_template, address_as_dict, restrict_globals=True
)

View File

@@ -9,7 +9,7 @@
"idx": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2025-12-19 12:37:31.673782",
"modified": "2026-09-04 12:37:31.673782",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Profit and Loss",
@@ -17,7 +17,6 @@
"owner": "Administrator",
"report_name": "Profit and Loss Statement",
"roles": [],
"show_values_over_chart": 1,
"timeseries": 0,
"type": "Line",
"use_report_chart": 1,

View File

@@ -24,7 +24,7 @@ def get(
heatmap_year: str | None = None,
):
if chart_name:
chart = frappe.get_doc("Dashboard Chart", chart_name)
chart = frappe.get_doc("Dashboard Chart", chart_name, check_permission="read")
else:
chart = frappe._dict(frappe.parse_json(chart))
timespan = chart.timespan
@@ -46,6 +46,9 @@ def get(
if not account:
frappe.throw(_("Account filter not set!"))
# authorise the account itself, as get_balance_on() does; doc= brings User Permissions with it
frappe.has_permission("Account", doc=account, throw=True)
if not to_date:
to_date = nowdate()
if not from_date:

View File

@@ -7,6 +7,7 @@
"description": "Heads (or groups) against which Accounting Entries are made and balances are maintained.",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/chart-of-accounts",
"engine": "InnoDB",
"field_order": [
"properties",
@@ -122,6 +123,7 @@
"description": "Setting Account Type helps in selecting this Account in transactions.",
"fieldname": "account_type",
"fieldtype": "Select",
"in_preview": 1,
"in_standard_filter": 1,
"label": "Account Type",
"oldfieldname": "account_type",
@@ -199,11 +201,11 @@
"options": "Account Category"
}
],
"icon": "fa fa-money",
"icon": "vault",
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-04-14 18:14:42.202065",
"modified": "2026-09-03 12:59:42.190900",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -264,6 +266,46 @@
{
"role": "HR Manager",
"select": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Manufacturing User",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -121,6 +121,7 @@ class Account(NestedSet):
self.validate_account_currency()
self.validate_root_company_and_sync_account_to_children()
self.validate_receivable_payable_account_type()
self.validate_stock_account_type_change()
def validate_parent_child_account_type(self):
if self.parent_account:
@@ -212,6 +213,36 @@ class Account(NestedSet):
frappe.msgprint(msg)
self.add_comment("Comment", msg)
def validate_stock_account_type_change(self):
doc_before_save = self.get_doc_before_save()
if not (doc_before_save and doc_before_save.account_type == "Stock"):
return
if self.account_type == "Stock":
return
if self.stock_ledger_entry_exists():
frappe.throw(
_(
"The account type of {0} cannot be changed from {1} because stock ledger entries exist against it."
).format(frappe.bold(self.name), frappe.bold(_("Stock")))
)
def stock_ledger_entry_exists(self):
from erpnext.stock import get_warehouse_account_map
warehouse_account = get_warehouse_account_map(self.company)
warehouses = [wh for wh, details in warehouse_account.items() if details.account == self.name]
if not warehouses:
return False
return bool(
frappe.db.count(
"Stock Ledger Entry",
filters={"warehouse": ("in", warehouses), "is_cancelled": 0},
)
)
def validate_root_details(self):
doc_before_save = self.get_doc_before_save()
@@ -472,24 +503,21 @@ class Account(NestedSet):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_parent_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
Account = frappe.qb.DocType("Account")
search_field_obj = getattr(Account, searchfield)
query = (
frappe.qb.from_(Account)
.select(Account.name)
.where(Account.is_group == 1)
.where(Account.docstatus != 2)
.where(Account.company == filters["company"])
.where(search_field_obj.like(f"%{txt}%"))
.order_by(Account.name)
.limit(page_len)
.offset(start)
return frappe.get_list(
"Account",
filters=[
["is_group", "=", 1],
["docstatus", "!=", 2],
["company", "=", filters["company"]],
[searchfield, "like", f"%{txt}%"],
],
fields=["name"],
order_by="name",
limit_start=start,
limit_page_length=page_len,
as_list=True,
)
return query.run(as_list=1)
def get_account_currency(account):
"""Helper function to get account currency"""
@@ -659,8 +687,15 @@ def _ensure_idle_system():
last_gl_update = None
try:
# We also lock inserts to GL entry table with for_update here.
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
if frappe.db.db_type == "postgres":
# The MariaDB branch blocks new GL inserts via the gap lock its for_update read takes;
# a postgres row lock never blocks inserts, so take an EXCLUSIVE table lock instead --
# writers block until the rename commits, readers don't. NOWAIT mirrors wait=False.
frappe.db.sql("LOCK TABLE `tabGL Entry` IN EXCLUSIVE MODE NOWAIT")
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified")
else:
# We also lock inserts to GL entry table with for_update here.
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
except frappe.QueryTimeoutError:
# wait=False fails immediately if there's an active transaction.
last_gl_update = add_to_date(None, seconds=-1)
@@ -689,9 +724,12 @@ def get_company_default_account_fields():
"stock_delivered_but_not_billed": "Stock Delivered But Not Billed Account",
"stock_adjustment_account": "Stock Adjustment Account",
"write_off_account": "Write Off Account",
"bank_charges_account": "Bank Charges Account",
"default_discount_account": "Default Payment Discount Account",
"unrealized_profit_loss_account": "Unrealized Profit / Loss Account",
"exchange_gain_loss_account": "Exchange Gain / Loss Account",
"exchange_gain_account": "Exchange Gain Account",
"exchange_loss_account": "Exchange Loss Account",
"unrealized_exchange_gain_loss_account": "Unrealized Exchange Gain / Loss Account",
"round_off_account": "Round Off Account",
"default_deferred_revenue_account": "Default Deferred Revenue Account",

View File

@@ -52,6 +52,42 @@ frappe.treeview_settings["Account"] = {
],
root_label: "Accounts",
get_tree_nodes: "erpnext.accounts.utils.get_children",
get_label: function (node) {
// clean display name — the account number renders as a badge (see
// onrender) instead of being glued into the name
return frappe.utils.escape_html(node.data.account_name || node.title || node.label);
},
onrender: function (node) {
if (node.is_root || !node.data) return;
const flags = [];
if (node.data.account_number) {
flags.push(frappe.ui.badge({ label: node.data.account_number }));
}
const company = frappe.treeview_settings["Account"].treeview?.page?.fields_dict?.company?.get_value();
const company_currency = company && erpnext.get_currency(company);
if (
node.data.account_currency &&
company_currency &&
node.data.account_currency !== company_currency
) {
flags.push(frappe.ui.badge({ label: node.data.account_currency, theme: "blue" }));
}
if (node.data.freeze_account === "Yes") {
flags.push(
frappe.ui.badge({
label: __("Frozen"),
icon: "lock",
title: __("Frozen - entries restricted"),
theme: "orange",
})
);
}
erpnext.utils.render_tree_node_flags(node, flags);
},
on_node_render: function (node, deep) {
const render_balances = () => {
for (let account of cur_tree.account_balance_data) {
@@ -232,7 +268,7 @@ frappe.treeview_settings["Account"] = {
frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree;
if (treeview.can_create) {
treeview.page.set_primary_action(
__("New"),
{ label: __("Add Account"), short_label: __("Add") },
function () {
let root_company = treeview.page.fields_dict.root_company.get_value();
if (root_company) {
@@ -243,13 +279,14 @@ frappe.treeview_settings["Account"] = {
treeview.new_node();
}
},
"add"
"plus"
);
}
},
toolbar: [
{
label: __("Add Child"),
icon: "plus",
condition: function (node) {
return (
frappe.boot.user.can_create.indexOf("Account") !== -1 &&
@@ -272,6 +309,7 @@ frappe.treeview_settings["Account"] = {
return !node.root && frappe.boot.user.can_read.indexOf("GL Entry") !== -1;
},
label: __("View Ledger"),
icon: "book-open",
click: function (node, btn) {
frappe.route_options = {
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
@@ -286,6 +324,106 @@ frappe.treeview_settings["Account"] = {
},
btnClass: "hidden-xs",
},
{
// same label and mechanism as the Account form's Actions button:
// NOT frappe's generic rename (Allow Rename stays off) — this is
// ERPNext's controlled update that rebuilds the derived
// "number - name - abbr" document name
label: __("Update Account Name / Number"),
icon: "text-cursor-input",
condition: function (node) {
return !node.is_root && frappe.model.can_write("Account");
},
click: function (node) {
const dialog = new frappe.ui.Dialog({
title: __("Update Account Number / Name"),
fields: [
{
fieldtype: "Data",
fieldname: "account_name",
label: __("Account Name"),
reqd: 1,
default: node.data.account_name,
},
{
fieldtype: "Data",
fieldname: "account_number",
label: __("Account Number"),
default: node.data.account_number,
},
],
primary_action_label: __("Update"),
primary_action(values) {
dialog.hide();
frappe.dom.freeze(__("Updating {0}", [node.label]));
frappe.call({
method: "erpnext.accounts.doctype.account.account.update_account_number",
args: {
name: node.label,
account_name: values.account_name,
account_number: values.account_number,
},
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees["Account"];
node.parent_node && treeview.tree.load_children(node.parent_node);
},
always: function () {
frappe.dom.unfreeze();
},
});
},
});
dialog.show();
},
},
{
label: __("Convert to Group"),
icon: "folder-tree",
condition: function (node) {
return !node.is_root && !node.expandable && frappe.model.can_write("Account");
},
click: function (node) {
erpnext.accounts.convert_tree_node("Account", node, "convert_ledger_to_group");
},
},
{
label: __("Convert to Non-Group"),
icon: "file-text",
condition: function (node) {
// only on groups the user has opened and found empty — a
// group with children can't convert, so don't offer it
return (
!node.is_root &&
node.expandable &&
node.loaded &&
!node.$ul.children().length &&
frappe.model.can_write("Account")
);
},
click: function (node) {
erpnext.accounts.convert_tree_node("Account", node, "convert_group_to_ledger");
},
},
],
extend_toolbar: true,
};
frappe.provide("erpnext.accounts");
// shared by the Account and Cost Center tree views (defined in both files,
// whichever loads first wins): run the doctype's whitelisted convert method,
// then re-render the branch so the node's group/leaf state updates
erpnext.accounts.convert_tree_node =
erpnext.accounts.convert_tree_node ||
function (doctype, node, method) {
frappe.call({
method: "run_doc_method",
args: { dt: doctype, dn: node.label, method: method },
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees[doctype];
node.parent_node && treeview.tree.load_children(node.parent_node);
frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" });
},
});
};

View File

@@ -102,6 +102,8 @@ def identify_is_group(child):
def get_chart(chart_template: str | None, existing_company: str | None = None):
chart = {}
if existing_company:
frappe.has_permission("Company", doc=existing_company, throw=True)
return get_account_tree_from_existing_company(existing_company)
elif chart_template == "Standard":

View File

@@ -24,7 +24,8 @@
"account_number": "11530"
},
"account_number": "115",
"is_group": 1
"is_group": 1,
"account_type": "Bank"
},
"Trade Receivables": {
"Trade Debtors": {
@@ -529,6 +530,13 @@
"account_number": "630",
"is_group": 1
},
"Accrued Manufacturing Expenses": {
"Accrued Expenses - Manufacturing": {
"account_number": "63510"
},
"account_number": "635",
"is_group": 1
},
"account_number": "63",
"is_group": 1
},
@@ -814,4 +822,4 @@
"root_type": "Expense"
}
}
}
}

View File

@@ -179,6 +179,9 @@
},
"Impairment": {
"account_category": "Operating Expenses"
},
"Exchange Loss": {
"account_category": "Operating Expenses"
}
},
"root_type": "Expense"
@@ -196,6 +199,10 @@
"account_type": "Income Account"
},
"Indirect Income": {
"Exchange Gain": {
"account_type": "Income Account",
"account_category": "Other Operating Income"
},
"account_type": "Income Account",
"is_group": 1
},

View File

@@ -22,12 +22,12 @@
"account_type": "Cash"
},
"Petty Cash Fund": {
"account_number": "1200",
"account_number": "1110",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Petty Cash Fund": {
"account_number": "1201",
"account_number": "1111",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
@@ -35,10 +35,16 @@
}
},
"Bank Accounts": {
"account_number": "1102",
"account_number": "1200",
"is_group": 1,
"root_type": "Asset",
"account_type": "Bank"
"account_type": "Bank",
"Cash in Bank - Checking Account": {
"account_number": "1201",
"is_group": 0,
"root_type": "Asset",
"account_type": "Bank"
}
},
"Advances to Officers & Employees": {
"account_number": "1290",
@@ -104,25 +110,20 @@
"account_number": "1511",
"is_group": 0,
"root_type": "Asset"
},
"Factory Overhead Variance": {
"account_number": "1512",
"is_group": 0,
"root_type": "Asset"
}
},
"Finished Goods": {
"account_number": "1520",
"account_number": "1540",
"is_group": 1,
"root_type": "Asset",
"Finished Goods Inventory": {
"account_number": "1531",
"account_number": "1541",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock"
},
"Inventory in Transit": {
"account_number": "1532",
"account_number": "1542",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock Adjustment"
@@ -268,7 +269,7 @@
"root_type": "Asset"
}
},
"System Development": {
"Intangible Assets": {
"account_number": "1940",
"is_group": 1,
"root_type": "Asset",
@@ -277,6 +278,17 @@
"is_group": 0,
"root_type": "Asset"
}
},
"Accumulated Amortization - Intangible Assets": {
"account_number": "1950",
"is_group": 1,
"root_type": "Asset",
"Accum Amortization - System Development": {
"account_number": "1951",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
}
}
}
},
@@ -406,8 +418,7 @@
"Customer Deposits": {
"account_number": "2500",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
"root_type": "Liability"
}
},
"Non Current Liabilities": {
@@ -563,6 +574,28 @@
"is_group": 0,
"root_type": "Income"
}
},
"Exchange Gain": {
"account_number": "6030",
"is_group": 1,
"root_type": "Income",
"Exchange Gain - Detail": {
"account_number": "6031",
"is_group": 0,
"root_type": "Income",
"account_type": "Indirect Income"
}
},
"Gain on Asset Disposal": {
"account_number": "6040",
"is_group": 1,
"root_type": "Income",
"Gain on Asset Disposal - Detail": {
"account_number": "6041",
"is_group": 0,
"root_type": "Income",
"account_type": "Indirect Income"
}
}
}
},
@@ -575,7 +608,7 @@
"is_group": 1,
"root_type": "Expense",
"Cost of Goods Sold": {
"account_number": "5010",
"account_number": "5002",
"is_group": 0,
"root_type": "Expense",
"account_type": "Cost of Goods Sold"
@@ -828,20 +861,61 @@
"root_type": "Expense"
}
},
"Stock Adjustment": {
"Other Expenses": {
"account_number": "5200",
"is_group": 1,
"root_type": "Expense",
"Bank Charges": {
"account_number": "5201",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
},
"Interest Expenses Bank": {
"account_number": "5202",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
},
"Write Off": {
"account_number": "5203",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
},
"Exchange Loss": {
"account_number": "5204",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
},
"Loss on Asset Disposal": {
"account_number": "5205",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
}
},
"Provision For Income Tax": {
"account_number": "5300",
"is_group": 0,
"root_type": "Expense",
"account_type": "Tax"
},
"Stock Adjustment": {
"account_number": "5400",
"is_group": 0,
"root_type": "Expense",
"account_type": "Stock Adjustment"
},
"Round Off": {
"account_number": "5300",
"account_number": "5500",
"is_group": 0,
"root_type": "Expense",
"account_type": "Round Off"
},
"Expenses Included In Valuation": {
"account_number": "5400",
"account_number": "5600",
"is_group": 0,
"root_type": "Expense",
"account_type": "Expenses Included In Valuation"

View File

@@ -138,6 +138,7 @@ def get():
_("Gain/Loss on Asset Disposal"): {"account_category": "Other Operating Income"},
_("Impairment"): {"account_category": "Operating Expenses"},
_("Tax Expense"): {"account_category": "Tax Expense"},
_("Exchange Loss"): {"account_category": "Operating Expenses"},
},
"root_type": "Expense",
},
@@ -149,6 +150,7 @@ def get():
_("Indirect Income"): {
_("Interest Income"): {"account_category": "Investment Income"},
_("Interest on Fixed Deposits"): {"account_category": "Investment Income"},
_("Exchange Gain"): {"account_category": "Other Operating Income"},
"is_group": 1,
},
"root_type": "Income",

View File

@@ -233,6 +233,7 @@ def get():
},
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
_("Exchange Loss"): {"account_number": "5226", "account_category": "Operating Expenses"},
"account_number": "5200",
},
"root_type": "Expense",
@@ -250,6 +251,10 @@ def get():
"account_number": "4220",
"account_category": "Investment Income",
},
_("Exchange Gain"): {
"account_number": "4230",
"account_category": "Other Operating Income",
},
"is_group": 1,
"account_number": "4200",
},

View File

@@ -306,6 +306,31 @@ class TestAccount(ERPNextTestSuite):
acc.account_currency = "USD"
self.assertRaises(frappe.ValidationError, acc.save)
def test_stock_account_type_change_with_ledger_entries(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
stock_account = get_warehouse_account(frappe.get_doc("Warehouse", warehouse))
make_stock_entry(
item_code="_Test Item",
target=warehouse,
company=company,
qty=5,
basic_rate=100,
)
account = frappe.get_doc("Account", stock_account)
self.assertEqual(account.account_type, "Stock")
account.account_type = ""
self.assertRaises(frappe.ValidationError, account.save)
account.reload()
account.account_name = f"{account.account_name} Updated"
account.save() # non-type change stays allowed
def test_account_balance(self):
from erpnext.accounts.utils import get_balance_on

View File

@@ -40,6 +40,7 @@
}
],
"grid_page_length": 50,
"icon": "folder-tree",
"index_web_pages_for_search": 1,
"links": [
{

View File

@@ -148,7 +148,7 @@
"precision": "9"
}
],
"icon": "fa fa-list",
"icon": "scale",
"in_create": 1,
"links": [],
"modified": "2025-08-22 19:13:50.400404",

View File

@@ -1,10 +1,59 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
aggregate_with_last_account_closing_balance,
generate_key,
)
from erpnext.tests.utils import ERPNextTestSuite
def entry(**overrides):
row = {"debit": 0, "credit": 0, "debit_in_account_currency": 0, "credit_in_account_currency": 0}
row.update(overrides)
return row
class TestAccountClosingBalance(ERPNextTestSuite):
pass
"""The closing-balance snapshot is built by merging this period's entries with the
previous period's. These lock the merge/key logic that drives that carry-forward."""
def test_matching_entries_are_summed(self):
# this is how a prior-period balance carries forward into the current one
merged = aggregate_with_last_account_closing_balance(
[
entry(account="Cash - _TC", debit=100, debit_in_account_currency=100),
entry(
account="Cash - _TC",
debit=50,
credit=20,
debit_in_account_currency=50,
credit_in_account_currency=20,
),
],
[],
)
self.assertEqual(len(merged), 1)
row = next(iter(merged.values()))
self.assertEqual(row["debit"], 150)
self.assertEqual(row["credit"], 20)
# the account-currency columns are accumulated in the same pass
self.assertEqual(row["debit_in_account_currency"], 150)
self.assertEqual(row["credit_in_account_currency"], 20)
def test_entries_are_kept_separate_per_dimension(self):
merged = aggregate_with_last_account_closing_balance(
[
entry(account="Cash - _TC", cost_center="CC1", debit=100, debit_in_account_currency=100),
entry(account="Cash - _TC", cost_center="CC2", debit=40, debit_in_account_currency=40),
],
[],
)
self.assertEqual(len(merged), 2)
def test_period_closing_flag_is_part_of_the_key(self):
# a P&L reversal (flag 0) and a closing-account entry (flag 1) for the same
# account must not merge, so the flag has to distinguish their keys
key_reversal, _ = generate_key(entry(account="Sales - _TC", is_period_closing_voucher_entry=0), [])
key_closing, _ = generate_key(entry(account="Sales - _TC", is_period_closing_voucher_entry=1), [])
self.assertNotEqual(key_reversal, key_closing)

View File

@@ -16,6 +16,8 @@ frappe.ui.form.on("Accounting Dimension", {
return {
filters: {
name: ["not in", invalid_doctypes],
istable: 0,
issingle: 0,
},
};
});

View File

@@ -3,6 +3,7 @@
"autoname": "field:label",
"creation": "2019-05-04 18:13:37.002352",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounting-dimensions",
"engine": "InnoDB",
"field_order": [
"document_type",
@@ -49,6 +50,7 @@
"options": "Accounting Dimension Detail"
}
],
"icon": "layers",
"links": [],
"modified": "2024-03-27 13:05:56.890002",
"modified_by": "Administrator",

View File

@@ -60,6 +60,14 @@ class AccountingDimension(Document):
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)
meta = frappe.get_meta(self.document_type)
if meta.istable or meta.issingle:
frappe.throw(
_(
"{0} cannot be used as an accounting dimension as it is not a standalone document type."
).format(frappe.bold(self.document_type))
)
exists = frappe.db.get_value("Accounting Dimension", {"document_type": self.document_type}, ["name"])
if exists and self.is_new():
@@ -215,8 +223,11 @@ def delete_accounting_dimension(doc):
frappe.clear_cache(doctype=doctype)
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def disable_dimension(doc: str):
# toggle_disabling rewrites a Custom Field site-wide, so demand the write that configures dimensions
frappe.has_permission("Accounting Dimension", "write", throw=True)
if frappe.in_test:
toggle_disabling(doc=doc)
else:
@@ -359,3 +370,13 @@ def create_accounting_dimensions_for_doctype(doctype):
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)
def get_dimension_fieldname(dim_doctype: str) -> str:
"""
Return the `GL Entry` fieldname for a given dimension.
"""
if dim_doctype in ("Cost Center", "Project"):
return frappe.scrub(dim_doctype)
return frappe.db.get_value("Accounting Dimension", {"document_type": dim_doctype}, "fieldname")

View File

@@ -51,6 +51,23 @@ class TestAccountingDimension(ERPNextTestSuite):
self.assertEqual(gle.get("department"), "_Test Department - _TC")
self.assertEqual(gle1.get("department"), "_Test Department - _TC")
def test_child_table_not_allowed_as_dimension(self):
dimension = frappe.get_doc({"doctype": "Accounting Dimension", "document_type": "Sales Team"})
self.assertRaises(frappe.ValidationError, dimension.insert)
def test_single_doctype_not_allowed_as_dimension(self):
dimension = frappe.get_doc({"doctype": "Accounting Dimension", "document_type": "Selling Settings"})
self.assertRaises(frappe.ValidationError, dimension.insert)
def test_non_scalar_dimension_value_skipped_in_gl_dict(self):
si = create_sales_invoice(do_not_save=1)
si.department = "_Test Department - _TC"
self.assertEqual(si.get_gl_dict({}).get("department"), "_Test Department - _TC")
si.department = ["_Test Department - _TC"]
self.assertNotIn("department", si.get_gl_dict({}))
def test_mandatory(self):
location = frappe.get_doc("Accounting Dimension", "Location")
location.dimension_defaults[0].mandatory_for_bs = True

View File

@@ -6,7 +6,7 @@ frappe.ui.form.on("Accounting Dimension Filter", {
let help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<p>
<i class="fa fa-hand-right"></i>
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
{{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}}
</p>
</td></tr>

View File

@@ -3,6 +3,7 @@
"autoname": "format:{accounting_dimension}-{#####}",
"creation": "2020-11-08 18:28:11.906146",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounting-dimension-filter",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -99,6 +100,7 @@
"label": "Fieldname"
}
],
"icon": "funnel",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-08 14:13:22.203011",

View File

@@ -3,6 +3,7 @@
"autoname": "field:period_name",
"creation": "2018-04-13 18:50:14.672323",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounting-period",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -77,6 +78,7 @@
"options": "Role"
}
],
"icon": "calendar-range",
"links": [],
"modified": "2026-03-09 17:15:33.577217",
"modified_by": "Administrator",

View File

@@ -3,6 +3,7 @@
"creation": "2013-06-24 15:49:57",
"doctype": "DocType",
"document_type": "Other",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounts-settings",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -22,6 +23,8 @@
"allow_multi_currency_invoices_against_single_party_account",
"confirm_before_resetting_posting_date",
"preview_mode",
"stock_expense_section",
"book_stock_expense_gl_entries",
"analytics_section",
"enable_discounts_and_margin",
"enable_accounting_dimensions",
@@ -76,6 +79,8 @@
"over_billing_allowance",
"credit_controller",
"role_allowed_to_over_bill",
"enable_overdue_billing_threshold",
"role_allowed_to_bypass_overdue_billing",
"column_break_11",
"assets_tab",
"asset_settings_section",
@@ -91,13 +96,14 @@
"column_break_25",
"reports_tab",
"remarks_section",
"general_ledger_remarks_length",
"receivable_payable_remarks_length",
"disable_include_dimensions",
"column_break_lvjk",
"general_ledger_remarks_length",
"accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method",
"default_ageing_range",
"column_break_ntmi",
"receivable_payable_remarks_length",
"legacy_section",
"ignore_is_opening_check_for_reporting",
"tab_break_dpet",
@@ -195,10 +201,12 @@
},
{
"default": "1",
"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",
@@ -218,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",
@@ -272,6 +281,21 @@
"label": "Role Allowed to over bill ",
"options": "Role"
},
{
"default": "0",
"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": "Prevent Sales Invoice when Customer is Overdue"
},
{
"depends_on": "eval:doc.enable_overdue_billing_threshold",
"description": "Users with this role can still submit invoices for customers who have crossed their Overdue Limit.",
"fieldname": "role_allowed_to_bypass_overdue_billing",
"fieldtype": "Link",
"label": "Role Allowed to Bypass Over Billing Restriction",
"options": "Role"
},
{
"fieldname": "period_closing_settings_section",
"fieldtype": "Section Break"
@@ -456,7 +480,7 @@
{
"fieldname": "remarks_section",
"fieldtype": "Section Break",
"label": "Remarks Column Length"
"label": "General Ledger Report"
},
{
"default": "0",
@@ -530,7 +554,7 @@
{
"fieldname": "accounts_receivable_payable_tuning_section",
"fieldtype": "Section Break",
"label": "Accounts Receivable / Payable Tuning"
"label": "Accounts Receivable / Payable Report"
},
{
"fieldname": "legacy_section",
@@ -757,15 +781,33 @@
"description": "Changing the account in any transaction of the DocTypes listed below will trigger a repost. To prevent reposting, remove the relevant DocType from the list.",
"fieldname": "column_break_mfor",
"fieldtype": "Column Break"
},
{
"fieldname": "stock_expense_section",
"fieldtype": "Section Break",
"label": "Stock Expense Accounting"
},
{
"default": "0",
"description": "Books Purchase Expense and Expenses Added To Stock account pairs against stock value. On enabling this, the accounts become mandatory in Company or Item Defaults for Purchase Receipt, Purchase Invoice, Stock Entry, Stock Reconciliation and Landed Cost Voucher",
"fieldname": "book_stock_expense_gl_entries",
"fieldtype": "Check",
"label": "Book Stock Expense GL Entries"
},
{
"default": "0",
"fieldname": "disable_include_dimensions",
"fieldtype": "Check",
"label": "Disable \"Consider Accounting Dimension\" Filter"
}
],
"grid_page_length": 50,
"icon": "icon-cog",
"icon": "settings",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-06-24 12:59:41.868865",
"modified": "2026-09-04 10:08:30.115003",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -62,6 +62,7 @@ class AccountsSettings(Document):
book_asset_depreciation_entry_automatically: DF.Check
book_deferred_entries_based_on: DF.Literal["Days", "Months"]
book_deferred_entries_via_journal_entry: DF.Check
book_stock_expense_gl_entries: DF.Check
book_tax_discount_loss: DF.Check
calculate_depr_using_total_days: DF.Check
check_supplier_invoice_uniqueness: DF.Check
@@ -71,12 +72,14 @@ class AccountsSettings(Document):
default_ageing_range: DF.Data | None
delete_linked_ledger_entries: DF.Check
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
disable_include_dimensions: DF.Check
enable_accounting_dimensions: DF.Check
enable_common_party_accounting: DF.Check
enable_discounts_and_margin: DF.Check
enable_fuzzy_matching: DF.Check
enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
enable_overdue_billing_threshold: DF.Check
enable_party_matching: DF.Check
enable_subscription: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
@@ -96,6 +99,7 @@ class AccountsSettings(Document):
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
repost_allowed_types: DF.Table[RepostAllowedTypes]
role_allowed_to_bypass_overdue_billing: DF.Link | None
role_allowed_to_over_bill: DF.Link | None
role_to_notify_on_depreciation_failure: DF.Link | None
role_to_override_stop_action: DF.Link | None
@@ -151,6 +155,10 @@ class AccountsSettings(Document):
toggle_subscription_sections(not self.enable_subscription)
clear_cache = True
if old_doc.enable_overdue_billing_threshold != self.enable_overdue_billing_threshold:
toggle_overdue_billing_threshold_field(not self.enable_overdue_billing_threshold)
clear_cache = True
if clear_cache:
frappe.clear_cache()
@@ -196,8 +204,8 @@ class AccountsSettings(Document):
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
frappe.throw(
_("You cannot enable both the settings '{0}' and '{1}'.").format(
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
frappe.bold(self.meta.get_translated_label("add_taxes_from_item_tax_template")),
frappe.bold(self.meta.get_translated_label("add_taxes_from_taxes_and_charges_template")),
),
title=_("Auto Tax Settings Error"),
)
@@ -214,6 +222,13 @@ class AccountsSettings(Document):
set_allow_on_submit_for_dimension_fields(doctypes)
@frappe.whitelist(methods=["POST"])
def get_posting_date_confirmation() -> int:
return cint(
frappe.db.get_single_value("Accounts Settings", "confirm_before_resetting_posting_date", cache=False)
)
def toggle_accounting_dimension_sections(hide):
accounting_dimension_doctypes = frappe.get_hooks("accounting_dimension_doctypes")
for doctype in accounting_dimension_doctypes:
@@ -242,6 +257,10 @@ def toggle_subscription_sections(hide):
create_property_setter_for_hiding_field(doctype, "subscription_section", hide)
def toggle_overdue_billing_threshold_field(hide):
create_property_setter_for_hiding_field("Customer Credit Limit", "overdue_billing_threshold", hide)
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,

View File

@@ -1,9 +1,15 @@
import frappe
from erpnext.accounts.doctype.accounts_settings.accounts_settings import get_posting_date_confirmation
from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsSettings(ERPNextTestSuite):
def test_posting_date_confirmation_uses_current_setting(self):
for enabled in (0, 1, 0):
frappe.db.set_single_value("Accounts Settings", "confirm_before_resetting_posting_date", enabled)
self.assertEqual(get_posting_date_confirmation(), enabled)
def test_stale_days(self):
cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
cur_settings.allow_stale = 0

View File

@@ -4,6 +4,7 @@
"creation": "2024-10-16 16:57:12.085072",
"doctype": "DocType",
"engine": "InnoDB",
"icon": "book-open",
"is_submittable": 1,
"field_order": [
"company",

View File

@@ -6,6 +6,7 @@
"creation": "2018-04-07 16:59:59.496668",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -100,8 +101,9 @@
"read_only": 1
}
],
"icon": "landmark",
"links": [],
"modified": "2024-03-27 13:06:36.896195",
"modified": "2026-08-21 23:11:39.423431",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank",
@@ -118,11 +120,20 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"role": "Accounts Manager",
"select": 1
},
{
"role": "Accounts User",
"select": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -5,6 +5,7 @@
"creation": "2017-05-29 21:35:13.136357",
"doctype": "DocType",
"document_type": "Setup",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank-account",
"engine": "InnoDB",
"field_order": [
"account_name",
@@ -227,6 +228,7 @@
}
],
"grid_page_length": 50,
"icon": "credit-card",
"links": [
{
"group": "Transactions",
@@ -269,7 +271,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2026-04-11 19:46:27.609994",
"modified": "2026-08-21 23:11:39.585456",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
@@ -299,6 +301,22 @@
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -107,7 +107,7 @@ def get_party_bank_account(party_type, party):
)
def get_default_company_bank_account(company, party_type, party):
def get_default_company_bank_account(company, party_type, party, ignore_permissions=True):
default_company_bank_account = frappe.db.get_value(party_type, party, "default_bank_account")
if default_company_bank_account:
if company != frappe.get_cached_value("Bank Account", default_company_bank_account, "company"):
@@ -118,6 +118,14 @@ def get_default_company_bank_account(company, party_type, party):
"Bank Account", {"company": company, "is_company_account": 1, "is_default": 1}
)
if not ignore_permissions:
default_company_bank_account = (
default_company_bank_account
if default_company_bank_account
and frappe.get_cached_doc("Bank Account", default_company_bank_account).has_permission("select")
else None
)
return default_company_bank_account
@@ -188,7 +196,7 @@ def get_closing_balance_as_per_statement(bank_account: str, date: str):
return {"balance": 0, "date": None}
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def set_closing_balance_as_per_statement(bank_account: str, date: str | datetime.date, balance: float):
"""
Set the closing balance as per statement for a bank account and date

View File

@@ -44,6 +44,7 @@
}
],
"grid_page_length": 50,
"icon": "wallet",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-06-16 22:17:48.007982",

View File

@@ -18,6 +18,7 @@
"unique": 1
}
],
"icon": "credit-card",
"links": [],
"modified": "2024-03-27 13:06:37.221876",
"modified_by": "Administrator",

View File

@@ -18,6 +18,7 @@
"unique": 1
}
],
"icon": "credit-card",
"links": [],
"modified": "2024-03-27 13:06:37.347035",
"modified_by": "Administrator",

View File

@@ -87,7 +87,7 @@
}
],
"hide_toolbar": 1,
"icon": "fa fa-check",
"icon": "badge-check",
"idx": 1,
"issingle": 1,
"links": [],

View File

@@ -4,29 +4,18 @@
import frappe
from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
)
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed, if_lending_app_not_installed
class TestBankClearance(ERPNextTestSuite):
def setUp(self):
frappe.clear_cache()
create_warehouse(
warehouse_name="_Test Warehouse",
properties={"parent_warehouse": "All Warehouses - _TC"},
company="_Test Company",
)
create_item("_Test Item")
create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company")
make_bank_account()
add_transactions()
@@ -139,11 +128,8 @@ def add_transactions():
def make_payment_entry():
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
supplier = create_supplier(supplier_name="_Test Supplier")
pi = make_purchase_invoice(
supplier=supplier.name,
supplier="_Test Supplier",
supplier_warehouse="_Test Warehouse - _TC",
expense_account="Cost of Goods Sold - _TC",
uom="Nos",
@@ -158,10 +144,6 @@ def make_payment_entry():
def make_pos_sales_invoice():
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
@@ -170,13 +152,13 @@ def make_pos_sales_invoice():
)
mode_of_payment.save()
customer = make_customer(customer="_Test Customer")
mode_of_payment = frappe.get_doc("Mode of Payment", "Wire Transfer")
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank Clearance - _TC")
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
si = create_sales_invoice(
customer="_Test Customer", item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1
)
si.set("payments", [])
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 1000})
si.insert()

View File

@@ -5,6 +5,7 @@
"creation": "2016-12-17 10:43:35.731631",
"doctype": "DocType",
"document_type": "Document",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank-guarantee",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -216,6 +217,7 @@
}
],
"grid_page_length": 50,
"icon": "shield-check",
"is_submittable": 1,
"links": [],
"modified": "2026-05-25 18:12:10.768835",

View File

@@ -60,6 +60,9 @@ def get_voucher_details(bank_guarantee_type: str, reference_name: str):
if not isinstance(reference_name, str):
raise TypeError("reference_name must be a string")
# the form is the boundary, not the referenced order: an order guard would break one of the two roles
frappe.has_permission("Bank Guarantee", throw=True)
fields_to_fetch = ["grand_total"]
if bank_guarantee_type == "Receiving":
@@ -70,4 +73,14 @@ def get_voucher_details(bank_guarantee_type: str, reference_name: str):
doctype = "Purchase Order"
fields_to_fetch.append("supplier")
# and scope the referenced order to the caller's own Company restrictions, which costs nobody
# who has none
from erpnext.stock.doctype.company_restriction.company_restriction import get_allowed_companies
allowed_companies = get_allowed_companies(frappe.session.user, "Bank Guarantee")
if allowed_companies:
company = frappe.db.get_value(doctype, reference_name, "company")
if company and company not in allowed_companies:
frappe.throw(_("Not permitted for {0}").format(company), frappe.PermissionError)
return frappe.db.get_value(doctype, reference_name, fields_to_fetch, as_dict=True)

View File

@@ -1,8 +1,76 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.utils import flt
from erpnext.accounts.doctype.bank_guarantee.bank_guarantee import get_voucher_details
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite
BANK = "_Test BG Bank"
class TestBankGuarantee(ERPNextTestSuite):
pass
"""Bank Guarantee records a guarantee issued/received against a customer or
supplier. validate() needs a party; on_submit() needs the bank details filled in."""
def setUp(self):
frappe.set_user("Administrator")
if not frappe.db.exists("Bank", BANK):
frappe.get_doc({"doctype": "Bank", "bank_name": BANK}).insert()
def make_bg(self, **args):
args = frappe._dict(args)
doc = frappe.new_doc("Bank Guarantee")
doc.bg_type = args.bg_type or "Receiving"
doc.amount = args.amount if args.amount is not None else 1000
doc.start_date = args.start_date or "2026-06-01"
if args.end_date:
doc.end_date = args.end_date
doc.customer = args.get("customer", "_Test Customer")
doc.supplier = args.get("supplier")
# fields on_submit requires — present by default, cleared per-test to assert the guard
doc.bank_guarantee_number = args.get("bank_guarantee_number", "BG-001")
doc.name_of_beneficiary = args.get("name_of_beneficiary", "Test Beneficiary")
doc.bank = args.get("bank", BANK)
return doc
def test_validate_requires_customer_or_supplier(self):
doc = self.make_bg(customer=None)
self.assertRaises(frappe.ValidationError, doc.insert)
def test_submit_requires_guarantee_number(self):
doc = self.make_bg(bank_guarantee_number="")
doc.insert()
self.assertRaises(frappe.ValidationError, doc.submit)
def test_submit_requires_beneficiary_name(self):
doc = self.make_bg(name_of_beneficiary="")
doc.insert()
self.assertRaises(frappe.ValidationError, doc.submit)
def test_submit_requires_bank(self):
doc = self.make_bg(bank="")
doc.insert()
self.assertRaises(frappe.ValidationError, doc.submit)
def test_valid_guarantee_submits(self):
doc = self.make_bg()
doc.insert()
doc.submit()
self.assertEqual(frappe.db.get_value("Bank Guarantee", doc.name, "docstatus"), 1)
def test_get_voucher_details_for_receiving(self):
so = make_sales_order()
details = get_voucher_details("Receiving", so.name)
self.assertEqual(details.customer, so.customer)
self.assertEqual(flt(details.grand_total), flt(so.grand_total))
def test_end_date_before_start_date_is_not_validated(self):
# SUSPECTED BUG: validate() never checks that end_date >= start_date, so a
# guarantee that expires before it starts saves cleanly. Locking the current
# (wrong) behaviour so a future fix that adds the check trips this test.
doc = self.make_bg(start_date="2026-06-30", end_date="2026-06-01")
doc.insert()
self.assertTrue(frappe.db.exists("Bank Guarantee", doc.name))

View File

@@ -68,6 +68,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frappe.msgprint(__("Please select Bank Account"));
return;
}
frm.events.validate_dates(frm);
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
args: {
@@ -82,7 +83,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
});
frm.add_custom_button(__("Get Unreconciled Entries"), function () {
frm.trigger("make_reconciliation_tool");
return frm.trigger("make_reconciliation_tool");
});
frm.change_custom_button_type(__("Get Unreconciled Entries"), null, "primary");
@@ -106,7 +107,24 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.trigger("get_account_opening_balance");
},
validate_dates(frm) {
const from_date = frm.doc.filter_by_reference_date
? frm.doc.from_reference_date
: frm.doc.bank_statement_from_date;
const to_date = frm.doc.filter_by_reference_date
? frm.doc.to_reference_date
: frm.doc.bank_statement_to_date;
if (from_date && to_date && from_date > to_date) {
frappe.throw(
frm.doc.filter_by_reference_date
? __("From Reference Date cannot be greater than To Reference Date")
: __("From Date cannot be greater than To Date")
);
}
},
make_reconciliation_tool(frm) {
frm.events.validate_dates(frm);
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
frm.trigger("get_cleared_balance").then(() => {

View File

@@ -116,6 +116,7 @@
}
],
"hide_toolbar": 1,
"icon": "arrow-left-right",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Max, Sum
from frappe.utils import cint, create_batch, flt
from frappe.utils import cint, create_batch, flt, getdate
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
@@ -54,6 +54,8 @@ def get_bank_transactions(
all_transactions: bool = False,
):
# returns bank transactions for a bank account
validate_date_range(from_date, to_date)
filters = []
filters.append(["bank_account", "=", bank_account])
filters.append(["docstatus", "=", 1])
@@ -116,7 +118,7 @@ def get_account_balance(bank_account: str, till_date: str | date, company: str):
return flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def update_bank_transaction(
bank_transaction_name: str, reference_number: str, party_type: str | None = None, party: str | None = None
):
@@ -146,7 +148,7 @@ def update_bank_transaction(
)[0]
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_journal_entry_bts(
bank_transaction_name: str,
reference_number: str | None = None,
@@ -305,7 +307,7 @@ def create_journal_entry_bts(
return reconcile_vouchers(bank_transaction_name, vouchers, is_new_voucher=True)
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_payment_entry_bts(
bank_transaction_name: str,
reference_number: str | None = None,
@@ -500,7 +502,7 @@ def create_bulk_internal_transfer(bank_transaction_names: list[str | int], bank_
return output
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_internal_transfer(
bank_transaction_name: str | int,
posting_date: str | date,
@@ -790,6 +792,7 @@ def create_bulk_payment_entry_and_reconcile(
"deposit",
"withdrawal",
"bank_account",
"company",
"currency",
"unallocated_amount",
"date",
@@ -824,11 +827,7 @@ def create_bulk_payment_entry_and_reconcile(
"paid_from": paid_from,
"paid_to": paid_to,
"paid_amount": bank_transaction.unallocated_amount,
"base_paid_amount": bank_transaction.unallocated_amount,
"received_amount": bank_transaction.unallocated_amount,
"base_received_amount": bank_transaction.unallocated_amount,
"target_exchange_rate": 1,
"source_exchange_rate": 1,
"reference_date": bank_transaction.date,
"posting_date": bank_transaction.date,
"reference_no": (bank_transaction.reference_number or bank_transaction.description or "")[
@@ -837,6 +836,8 @@ def create_bulk_payment_entry_and_reconcile(
}
)
set_multi_currency_amounts(payment_entry_doc)
payment_entry_doc.insert()
payment_entry_doc.submit()
@@ -875,6 +876,7 @@ def create_payment_entry_and_reconcile(bank_transaction_name: str | int, payment
"doctype": "Payment Entry",
}
)
set_multi_currency_amounts(payment_entry)
payment_entry.insert()
payment_entry.submit()
transaction = reconcile_vouchers(
@@ -897,6 +899,33 @@ def create_payment_entry_and_reconcile(bank_transaction_name: str | int, payment
}
def set_multi_currency_amounts(pe):
"""Set real exchange rates when the bank and party accounts differ in currency."""
company_currency = frappe.get_cached_value("Company", pe.company, "default_currency")
pe.paid_from_account_currency = frappe.get_cached_value("Account", pe.paid_from, "account_currency")
pe.paid_to_account_currency = frappe.get_cached_value("Account", pe.paid_to, "account_currency")
pe.source_exchange_rate = (
1.0
if pe.paid_from_account_currency == company_currency
else get_exchange_rate(pe.paid_from_account_currency, company_currency, pe.posting_date)
)
pe.target_exchange_rate = (
1.0
if pe.paid_to_account_currency == company_currency
else get_exchange_rate(pe.paid_to_account_currency, company_currency, pe.posting_date)
)
# derive the party-side amount from the authoritative bank-side amount; Payment Entry books any
# rounding residual to Exchange Gain/Loss during validation (set_exchange_gain_loss)
if pe.payment_type == "Receive" and pe.source_exchange_rate:
base_amount = flt(pe.received_amount) * pe.target_exchange_rate
pe.paid_amount = flt(base_amount / pe.source_exchange_rate, pe.precision("paid_amount"))
elif pe.payment_type == "Pay" and pe.target_exchange_rate:
base_amount = flt(pe.paid_amount) * pe.source_exchange_rate
pe.received_amount = flt(base_amount / pe.target_exchange_rate, pe.precision("received_amount"))
@frappe.whitelist(methods=["GET"])
def search_for_transfer_transaction(transaction_id: str | int):
"""
@@ -912,7 +941,7 @@ def search_for_transfer_transaction(transaction_id: str | int):
days = frappe.db.get_single_value("Accounts Settings", "transfer_match_days")
if not days:
if days is None:
days = 3
min_date = frappe.utils.add_days(date, -days)
@@ -962,9 +991,10 @@ def auto_reconcile_vouchers(
from_date: str | date | None = None,
to_date: str | date | None = None,
filter_by_reference_date: bool | None = None,
from_reference_date: bool | None = None,
to_reference_date: str | None = None,
from_reference_date: str | date | None = None,
to_reference_date: str | date | None = None,
):
validate_date_range(from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date)
bank_transactions = get_bank_transactions(bank_account)
if len(bank_transactions) > 10:
@@ -1057,7 +1087,7 @@ def get_auto_reconcile_message(partially_reconciled, reconciled):
return alert_message, indicator
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def reconcile_vouchers(bank_transaction_name: str | int, vouchers: str | list, is_new_voucher: bool = False):
# updated clear date of all the vouchers based on the bank transaction
vouchers = frappe.parse_json(vouchers)
@@ -1079,10 +1109,11 @@ def get_linked_payments(
from_date: str | date | None = None,
to_date: str | date | None = None,
filter_by_reference_date: bool | None = None,
from_reference_date: bool | None = None,
to_reference_date: str | None = None,
from_reference_date: str | date | None = None,
to_reference_date: str | date | None = None,
):
# get all matching payments for a bank transaction
validate_date_range(from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
@@ -1102,6 +1133,23 @@ def get_linked_payments(
return subtract_allocations(gl_account, matching)
def validate_date_range(
from_date,
to_date,
filter_by_reference_date=False,
from_reference_date=None,
to_reference_date=None,
):
if cint(filter_by_reference_date):
from_date, to_date = from_reference_date, to_reference_date
message = _("From Reference Date cannot be greater than To Reference Date")
else:
message = _("From Date cannot be greater than To Date")
if from_date and to_date and getdate(from_date) > getdate(to_date):
frappe.throw(message)
def subtract_allocations(gl_account, vouchers):
"Look up & subtract any existing Bank Transaction allocations"
copied = []
@@ -1138,6 +1186,7 @@ def check_matching(
from_reference_date=None,
to_reference_date=None,
):
document_types = document_types or []
exact_match = True if "exact_match" in document_types else False
common_filters = frappe._dict(
@@ -1336,9 +1385,11 @@ def get_pe_matching_query(
ref_condition = pe.reference_no == transaction.reference_number
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
amount_equality = pe.paid_amount == transaction.unallocated_amount
amount_field = pe.received_amount_after_tax if account_from_to == "paid_to" else pe.paid_amount_after_tax
amount_equality = amount_field == transaction.unallocated_amount
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
amount_condition = amount_equality if exact_match else pe.paid_amount > 0.0
amount_condition = amount_equality if exact_match else amount_field > 0.0
party_condition = (
(pe.party_type == transaction.party_type) & (pe.party == transaction.party) & pe.party.isnotnull()
@@ -1355,7 +1406,7 @@ def get_pe_matching_query(
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
ConstantColumn("Payment Entry").as_("doctype"),
pe.name,
pe.base_paid_amount_after_tax.as_("paid_amount"),
amount_field.as_("paid_amount"),
pe.reference_no,
pe.reference_date,
pe.party,

View File

@@ -2,18 +2,26 @@
# See license.txt
from unittest.mock import patch
import frappe
from frappe import qb
from frappe.utils import add_days, today
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
auto_reconcile_vouchers,
create_bulk_payment_entry_and_reconcile,
create_payment_entry_and_reconcile,
get_auto_reconcile_message,
get_bank_transactions,
get_linked_payments,
)
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite
RATE_METHOD = "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_exchange_rate"
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
@@ -97,3 +105,248 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
# assert API output post reconciliation
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
self.assertEqual(len(transactions), 0)
def make_bank_transaction(self, date, deposit=100, withdrawal=0):
return (
frappe.get_doc(
{
"doctype": "Bank Transaction",
"date": date,
"deposit": deposit,
"withdrawal": withdrawal,
"bank_account": self.bank_account,
"currency": "INR",
}
)
.save()
.submit()
)
def get_matching_payment_entries(self, bank_transaction, exact_match=False):
document_types = ["payment_entry", "exact_match"] if exact_match else ["payment_entry"]
vouchers = get_linked_payments(
bank_transaction,
document_types,
from_date=add_days(today(), -1),
to_date=today(),
)
return [v for v in vouchers if v.get("doctype") == "Payment Entry"]
def test_get_bank_transactions_excludes_dates_after_to_date(self):
self.make_bank_transaction(date=today())
names = [t.name for t in get_bank_transactions(self.bank_account, to_date=add_days(today(), -1))]
self.assertEqual(names, [])
def test_get_linked_payments_without_document_types(self):
bank_transaction = self.make_bank_transaction(date=today())
self.assertEqual(get_linked_payments(bank_transaction.name), [])
def test_rejects_reversed_date_ranges(self):
from_date, to_date = today(), add_days(today(), -1)
with self.assertRaisesRegex(frappe.ValidationError, "From Date cannot be greater than To Date"):
get_bank_transactions(self.bank_account, from_date, to_date)
with self.assertRaisesRegex(
frappe.ValidationError, "From Reference Date cannot be greater than To Reference Date"
):
auto_reconcile_vouchers(
self.bank_account,
filter_by_reference_date=True,
from_reference_date=from_date,
to_reference_date=to_date,
)
transaction = self.make_bank_transaction(date=today())
with self.assertRaisesRegex(
frappe.ValidationError, "From Reference Date cannot be greater than To Reference Date"
):
get_linked_payments(
transaction.name,
["payment_entry"],
filter_by_reference_date=True,
from_reference_date=from_date,
to_reference_date=to_date,
)
def test_deposit_matches_amount_received_in_bank_account(self):
# money leaves another bank account and lands here minus a charge, so the two sides differ
payment = frappe.get_doc(
{
"doctype": "Payment Entry",
"payment_type": "Internal Transfer",
"company": self.company,
"posting_date": today(),
"paid_from": "_Test Bank - _TC",
"paid_to": self.bank,
"paid_amount": 3537.64,
"received_amount": 3460.52,
"reference_no": "TRF-001",
"reference_date": today(),
}
)
payment.set_missing_values()
payment.set_exchange_rate()
payment.set_amounts()
payment.deductions[-1].account = "_Test Exchange Gain/Loss - _TC"
payment.deductions[-1].cost_center = "_Test Cost Center - _TC"
payment = payment.save().submit()
transaction = self.make_bank_transaction(date=today(), deposit=3460.52)
# the received side is what reached this bank account, so that is what is shown
matches = self.get_matching_payment_entries(transaction.name)
self.assertEqual([m["name"] for m in matches], [payment.name])
self.assertEqual(matches[0]["paid_amount"], 3460.52)
# and what the exact match compares against
exact_matches = self.get_matching_payment_entries(transaction.name, exact_match=True)
self.assertEqual([m["name"] for m in exact_matches], [payment.name])
def test_withdrawal_matches_amount_paid_from_bank_account(self):
payment = create_payment_entry(
company=self.company,
payment_type="Pay",
party_type="Supplier",
party="_Test Supplier",
paid_from=self.bank,
paid_to="Creditors - _TC",
paid_amount=1250,
)
payment = payment.save().submit()
transaction = self.make_bank_transaction(date=today(), deposit=0, withdrawal=1250)
exact_matches = self.get_matching_payment_entries(transaction.name, exact_match=True)
self.assertEqual([m["name"] for m in exact_matches], [payment.name])
self.assertEqual(exact_matches[0]["paid_amount"], 1250)
def test_auto_reconcile_message_for_no_matches(self):
message, indicator = get_auto_reconcile_message([], [])
self.assertEqual(indicator, "blue")
self.assertIn("No matches", message)
def test_auto_reconcile_message_counts_and_pluralizes(self):
# reconciled count is reported and the indicator turns green
message, indicator = get_auto_reconcile_message([], ["t1", "t2"])
self.assertEqual(indicator, "green")
self.assertIn("2 Transaction(s) Reconciled", message)
# partially-reconciled label is singular for one, plural for many
singular, _ = get_auto_reconcile_message(["p1"], [])
self.assertIn("1 Transaction Partially Reconciled", singular)
plural, _ = get_auto_reconcile_message(["p1", "p2"], [])
self.assertIn("2 Transactions Partially Reconciled", plural)
def test_multi_currency_pay_converts_and_balances(self):
# withdrawal from an INR bank paying a USD supplier; rate 3.0 makes 100/3 non-exact
self.enable_multi_currency_setup()
pe = self.reconcile_new_payment(
self.make_multi_currency_txn(withdrawal=100),
payment_type="Pay",
party_type="Supplier",
party=self.supplier,
party_account=self.creditors_usd,
paid_from=self.bank,
paid_to=self.creditors_usd,
rate=3.0,
)
self.assertEqual(pe.docstatus, 1) # submits despite the rounding residual
self.assertEqual((pe.source_exchange_rate, pe.target_exchange_rate), (1.0, 3.0))
self.assertEqual((pe.paid_amount, pe.received_amount), (100, 33.33)) # bank side kept, 100/3
self.assertEqual(pe.difference_amount, 0)
# Payment Entry auto-books the rounding residual to Exchange Gain/Loss
self.assertTrue(pe.deductions[0].is_exchange_gain_loss)
self.assertEqual(pe.deductions[0].amount, 0.01) # 100 - 33.33 * 3
def test_multi_currency_receive_converts_and_balances(self):
# deposit into an INR bank from a USD customer; the party side must convert
self.enable_multi_currency_setup()
pe = self.reconcile_new_payment(
self.make_multi_currency_txn(deposit=100),
payment_type="Receive",
party_type="Customer",
party=self.customer,
party_account=self.debtors_usd,
paid_from=self.debtors_usd,
paid_to=self.bank,
rate=3.0,
)
self.assertEqual(pe.docstatus, 1)
self.assertEqual((pe.source_exchange_rate, pe.target_exchange_rate), (3.0, 1.0))
self.assertEqual((pe.received_amount, pe.paid_amount), (100, 33.33)) # bank side kept, 100/3
self.assertEqual(pe.difference_amount, 0)
def test_multi_currency_bulk_pay_converts_and_balances(self):
# the bulk path builds the Payment Entry itself, so it must convert too
self.enable_multi_currency_setup()
txn = self.make_multi_currency_txn(withdrawal=100)
with patch(RATE_METHOD, return_value=3.0):
result = create_bulk_payment_entry_and_reconcile(
[txn.name], "Supplier", self.supplier, self.creditors_usd
)
pe = frappe.get_doc("Payment Entry", result[0]["payment_entry"].name)
self.assertEqual(pe.docstatus, 1)
self.assertEqual(pe.target_exchange_rate, 3.0)
self.assertEqual((pe.paid_amount, pe.received_amount), (100, 33.33))
self.assertEqual(pe.difference_amount, 0)
def enable_multi_currency_setup(self):
# USD party/accounts + a company gain/loss account to absorb rounding residuals
self.company_abbr = "_TC"
self.create_supplier(supplier_name="_Test Supplier USD", currency="USD")
self.create_customer(customer_name="_Test Customer USD", currency="USD")
self.create_usd_payable_account()
self.create_usd_receivable_account()
self.set_party_account("Supplier", self.supplier, self.creditors_usd)
if not frappe.db.get_value("Company", self.company, "exchange_gain_loss_account"):
frappe.db.set_value(
"Company", self.company, "exchange_gain_loss_account", "Exchange Gain/Loss - _TC"
)
def set_party_account(self, party_type, party, account):
doc = frappe.get_doc(party_type, party)
if not any(row.company == self.company for row in doc.accounts):
doc.append("accounts", {"company": self.company, "account": account})
doc.save()
def make_multi_currency_txn(self, withdrawal=0, deposit=0):
return (
frappe.get_doc(
{
"doctype": "Bank Transaction",
"date": today(),
"withdrawal": withdrawal,
"deposit": deposit,
"bank_account": self.bank_account,
"currency": "INR",
"reference_number": "TEST-FX-REF",
}
)
.save()
.submit()
)
def reconcile_new_payment(
self, txn, *, payment_type, party_type, party, party_account, paid_from, paid_to, rate
):
# mimics the /banking frontend, which sends a hardcoded 1:1 rate
payment_entry_doc = {
"payment_type": payment_type,
"company": self.company,
"party_type": party_type,
"party": party,
"party_account": party_account,
"paid_from": paid_from,
"paid_to": paid_to,
"paid_amount": txn.unallocated_amount,
"received_amount": txn.unallocated_amount,
"source_exchange_rate": 1,
"target_exchange_rate": 1,
"posting_date": today(),
"reference_no": f"TEST-FX-{payment_type}",
"reference_date": today(),
}
with patch(RATE_METHOD, return_value=rate):
result = create_payment_entry_and_reconcile(txn.name, payment_entry_doc)
return frappe.get_doc("Payment Entry", result["payment_entry"].name)

View File

@@ -225,6 +225,7 @@
}
],
"hide_toolbar": 1,
"icon": "file-down",
"links": [],
"modified": "2026-06-19 14:18:00.000000",
"modified_by": "Administrator",

View File

@@ -142,9 +142,34 @@ def preprocess_mt940_content(content: str) -> str:
return processed_content
@frappe.whitelist()
MT940_CUSTOMER_REFERENCE_MAX_LEN = 16
def get_transaction_reference(txn_data: dict) -> str:
"""Extract the per-transaction reference from an MT940 :61: tag.
The mt940 library exposes ``transaction_reference`` from the :20: tag, which is the
statement-level reference and identical for every transaction in a statement. The
real per-transaction reference is ``customer_reference`` (with any overflow captured
into ``extra_details`` when a bank emits a single-line :61: longer than 16 chars).
"""
customer_reference = (txn_data.get("customer_reference") or "").strip()
if len(customer_reference) == MT940_CUSTOMER_REFERENCE_MAX_LEN:
customer_reference += (txn_data.get("extra_details") or "").strip()
if customer_reference and customer_reference.upper() != "NONREF":
return customer_reference
return (txn_data.get("bank_reference") or "").strip() or (
txn_data.get("transaction_reference") or ""
).strip()
@frappe.whitelist(methods=["POST"])
def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
doc = frappe.get_doc("Bank Statement Import", data_import)
doc.check_permission("write")
_file_doc, content = get_file(mt940_file_path)
@@ -189,8 +214,8 @@ def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
deposit = amount_value if amount_value > 0 else ""
withdrawal = abs(amount_value) if amount_value < 0 else ""
description = txn.data.get("extra_details") or ""
reference = txn.data.get("transaction_reference") or ""
description = txn.data.get("transaction_details") or txn.data.get("extra_details") or ""
reference = get_transaction_reference(txn.data)
currency = txn.data.get("currency", "")
writer.writerow([date_str, deposit, withdrawal, description, reference, doc.bank_account, currency])
@@ -211,26 +236,30 @@ def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
def get_preview_from_template(
data_import: str, import_file: str | None = None, google_sheets_url: str | None = None
):
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
import_file, google_sheets_url
)
bsi = frappe.get_doc("Bank Statement Import", data_import)
bsi.check_permission()
return bsi.get_preview_from_template(import_file, google_sheets_url)
@frappe.whitelist()
def form_start_import(data_import: str):
job_id = frappe.get_doc("Bank Statement Import", data_import).start_import()
return job_id is not None
bsi = frappe.get_doc("Bank Statement Import", data_import)
bsi.check_permission("write")
return bsi.start_import()
@frappe.whitelist()
def download_errored_template(data_import_name: str):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.check_permission()
data_import.export_errored_rows()
@frappe.whitelist()
def download_import_log(data_import_name: str):
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
bsi = frappe.get_doc("Bank Statement Import", data_import_name)
bsi.check_permission()
return bsi.download_import_log()
def is_mt940_format(content: str) -> bool:
@@ -307,7 +336,7 @@ def add_bank_account(data, bank_account):
bank_account_loc = loc
for row in data[1:]:
if bank_account_loc:
if bank_account_loc is not None:
row[bank_account_loc] = bank_account
else:
row.append(bank_account)
@@ -369,6 +398,7 @@ def get_import_status(docname: str):
import_status = {}
data_import = frappe.get_doc("Bank Statement Import", docname)
data_import.check_permission()
import_status["status"] = data_import.status
logs = frappe.get_all(
@@ -407,6 +437,11 @@ def get_import_logs(docname: str):
@frappe.whitelist()
def upload_bank_statement(**args):
# The only caller is the Bank Reconciliation Tool's "Upload Bank Statement" button, whose
# callback routes straight into a new Bank Statement Import form — so `create` is exactly the
# right to require, and both doctypes are System Manager only, which makes it loser-free.
frappe.has_permission("Bank Statement Import", "create", throw=True)
args = frappe._dict(args)
bsi = frappe.new_doc("Bank Statement Import")

View File

@@ -1,7 +1,10 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
import mt940
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
get_transaction_reference,
is_mt940_format,
preprocess_mt940_content,
)
@@ -188,6 +191,135 @@ class TestBankStatementImport(ERPNextTestSuite):
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
def test_get_transaction_reference_uses_customer_reference(self):
"""Per-transaction reference must come from :61: customer_reference, not :20:."""
self.assertEqual(
get_transaction_reference(
{"customer_reference": "UPI-100000000001", "transaction_reference": "STMTREF12345"}
),
"UPI-100000000001",
)
def test_get_transaction_reference_rejoins_overflow(self):
"""When a bank emits a single-line :61: with >16-char reference, the regex
splits the tail into extra_details. We must rejoin them."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "NEFTINW-12345678",
"extra_details": "90",
"transaction_reference": "STMTREF12345",
}
),
"NEFTINW-1234567890",
)
def test_get_transaction_reference_falls_back_to_bank_reference_on_nonref(self):
"""NONREF is the MT940 'no customer reference' sentinel; prefer bank_reference."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "NONREF",
"bank_reference": "1234567890123456",
"transaction_reference": "STMTREF12345",
}
),
"1234567890123456",
)
def test_get_transaction_reference_falls_back_to_bank_reference_on_nonref_with_extra_details(self):
"""NONREF sentinel must trigger the bank_reference fallback even when
extra_details is populated. Without the 16-char gate, the old naive concat
would produce a junk reference like 'NONREFsome info' and bypass the check."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "NONREF",
"extra_details": "some info",
"bank_reference": "1234567890123456",
"transaction_reference": "STMTREF12345",
}
),
"1234567890123456",
)
def test_get_transaction_reference_does_not_append_extra_details_below_16_chars(self):
"""When customer_reference is below the 16-char cap, extra_details is a
genuine supplementary-info field from :61: — not overflow — and must not
be appended to the reference."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "TBMS-123456789",
"extra_details": "note field",
"transaction_reference": "STMTREF12345",
}
),
"TBMS-123456789",
)
def test_get_transaction_reference_keeps_noref_literal(self):
"""Bare 'NOREF' (without bank_reference) stays as-is; still better than the
statement-level reference which is identical across all transactions."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "NOREF",
"bank_reference": None,
"transaction_reference": "STMTREF12345",
}
),
"NOREF",
)
def test_mt940_parse_per_transaction_reference_mapping(self):
"""End-to-end: every transaction in a statement must get its own distinct
reference from :61: customer_reference, never the statement-level :20: reference."""
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
:20:STMTREF12345
:25:1234567890
:28C:12345/1
:60F:C250716INR88123,38
:61:2509280928D5000,00NMSCUPI-100000000001
:86:UPI/TEST PAYEE ONE/111111111111/TestApp
:61:2509190919D2606,00NMSCUPI-100000000002
:86:UPI/TEST PAYEE TWO/222222222222/TestApp
:61:2509190919D900,00NMSCUPI-100000000003
:86:UPI/TEST PAYEE THREE/333333333333/TestApp
:61:2508140814D5000,00NMSCUPI-100000000004
:86:UPI/TEST PAYEE FOUR/444444444444/TestApp
:61:2508060806D2000,00NMSCUPI-100000000005
:86:UPI/TEST PAYEE FIVE/555555555555/TestApp
:61:2508030803D1066,00NMSC123456789012
:86:PCD/1234/TEST MERCHANT/01234567890123/12:00
:61:2507310731D305,62NMSCTBMS-123456789
:86:Chrg: Debit Card Annual Fee 1234 for 2025
:61:2507240724C1,00NMSCNEFTINW-1234567890
:86:NEFT TEST123456789 TEST SERVICES
:61:2507170717C100000,00NMSCNOREF
:86:BY CLG INST 123456/01-01-25/TESTBANK/TESTCITY
:62F:C250930INR100000,00
-}"""
transactions = list(mt940.parse(preprocess_mt940_content(mt940_content)))
references = [get_transaction_reference(t.data) for t in transactions]
self.assertEqual(
references,
[
"UPI-100000000001",
"UPI-100000000002",
"UPI-100000000003",
"UPI-100000000004",
"UPI-100000000005",
"123456789012",
"TBMS-123456789",
"NEFTINW-1234567890",
"NOREF",
],
)
# No transaction should carry the statement-level reference from :20:
self.assertNotIn("STMTREF12345", references)
def test_preprocess_mt940_content_whitespace_variants(self):
"""Test handling of whitespace and different line endings"""
# Test with trailing spaces

View File

@@ -54,7 +54,6 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Closing Balance",
"non_negative": 1,
"options": "currency"
},
{
@@ -189,9 +188,10 @@
}
],
"grid_page_length": 50,
"icon": "file-clock",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-05-08 17:55:25.615942",
"modified": "2026-07-09 17:55:25.615942",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import Log",

View File

@@ -375,8 +375,7 @@ class BankStatementImportLog(Document):
table["column_mapping"] = guess_column_mapping_by_content(table["rows"])
final_transactions, table["date_format"], table["amount_format"] = build_table_transactions(table)
# Tables with no detectable transactions (ads, summaries, headers) start excluded.
table["included"] = bool(final_transactions)
table["included"] = should_include_table(table, final_transactions)
self.pdf_tables = json.dumps(tables)
return tables
@@ -542,6 +541,8 @@ class BankStatementImportLog(Document):
"bank-rec-statement-import-progress",
{
"progress": round(progress / total_transactions * 100),
"current": progress,
"total": total_transactions,
},
doctype="Bank Statement Import Log",
docname=self.name,
@@ -551,13 +552,14 @@ class BankStatementImportLog(Document):
"bank-rec-statement-import-progress",
{
"progress": 100,
"current": total_transactions,
"total": total_transactions,
},
doctype="Bank Statement Import Log",
docname=self.name,
)
if self.closing_balance and self.closing_balance > 0 and self.end_date:
if self.closing_balance is not None and self.end_date:
set_closing_balance_as_per_statement(
self.bank_account, frappe.utils.getdate(self.end_date), self.closing_balance
)
@@ -821,6 +823,15 @@ def compute_final_transactions(transaction_rows: list, date_format: str, amount_
"""Pure version of the final-transaction builder (date normalized, amount split)."""
final_transactions = []
# Which marker does this statement actually write? A statement that only ever says "Cr"
# is marking the credits as its exceptions, so an unmarked row is a withdrawal; one that
# only ever says "Dr" means the opposite. With both markers present an unmarked row is
# genuinely undetermined, so it stays a withdrawal.
unmarked_is_deposit = False
if amount_format == 'Amount column has "CR"/"DR" values':
markers = {get_amount_cr_dr_marker(row.get("amount")) for row in transaction_rows}
unmarked_is_deposit = markers - {None} == {"dr"}
def parse_amount(transaction_row: dict):
if amount_format == "Separate columns for withdrawal and deposit":
return get_float_amount(transaction_row.get("withdrawal")), get_float_amount(
@@ -829,44 +840,43 @@ def compute_final_transactions(transaction_rows: list, date_format: str, amount_
if amount_format == 'Amount column has "CR"/"DR" values':
amount = transaction_row.get("amount")
marker = get_amount_cr_dr_marker(amount)
# The marker carries the direction, so the amount's own sign is ignored.
signed_amount = get_float_amount(amount) or 0
# If the amount column has CR/DR in it - we should remove any signs (negative or positive) from the amount
float_amount = abs(get_float_amount(amount) or 0)
if "cr" in amount.lower():
return 0, float_amount
else:
return float_amount, 0
if marker:
return (0, abs(signed_amount)) if marker == "cr" else (abs(signed_amount), 0)
# An unmarked row takes the opposite direction to the marker this statement
# uses. A negative amount reverses that again (a refund).
is_deposit = unmarked_is_deposit
if signed_amount < 0:
is_deposit = not is_deposit
return (0, abs(signed_amount)) if is_deposit else (abs(signed_amount), 0)
# `or 0` below: get_float_amount returns None for an unparseable cell, and a blank
# transaction-type cell comes through as None. Both used to raise.
if amount_format == "Amount column has positive/negative values":
amount = get_float_amount(transaction_row.get("amount", "0"))
amount = get_float_amount(transaction_row.get("amount", "0")) or 0
if amount > 0:
return 0, abs(amount)
else:
return abs(amount), 0
transaction_type = str(transaction_row.get("debit_credit") or "").strip().lower()
amount = abs(get_float_amount(transaction_row.get("amount", "0")) or 0)
if amount_format == 'Transaction type column has "CR"/"DR" values':
transaction_type = transaction_row.get("debit_credit")
amount = get_float_amount(transaction_row.get("amount", "0"))
if "cr" in transaction_type.lower():
return 0, abs(amount)
else:
return abs(amount), 0
# "credit" contains "cr". "debit" does not contain "dr", so it correctly falls
# through to the withdrawal side.
return (0, amount) if "cr" in transaction_type else (amount, 0)
if amount_format == 'Transaction type column has "C"/"D" values':
transaction_type = transaction_row.get("debit_credit")
amount = get_float_amount(transaction_row.get("amount", "0"))
if transaction_type.lower().strip() == "c":
return 0, abs(amount)
else:
return abs(amount), 0
return (0, amount) if transaction_type == "c" else (amount, 0)
if amount_format == 'Transaction type column has "Deposit"/"Withdrawal" values':
transaction_type = transaction_row.get("debit_credit")
amount = get_float_amount(transaction_row.get("amount", "0"))
if "deposit" in transaction_type.lower():
return 0, abs(amount)
else:
return abs(amount), 0
return (0, amount) if "deposit" in transaction_type else (amount, 0)
return 0, 0
@@ -910,6 +920,26 @@ def build_table_transactions(table: dict):
return final_transactions, date_format, amount_format
def should_include_table(table: dict, final_transactions: list) -> bool:
"""
Whether a freshly extracted PDF table should START as included - only the default state
of the checkbox, which the user can change afterwards.
It must have yielded transactions, and it must have a Description column mapped. A
transaction table always carries a narration; the summary boxes printed around it -
payment due, credit limit, reward points - are dates and figures only. Otherwise the
HDFC credit-card "Payment Due Date / Total Dues / Minimum Amount Due" box parses as one
transaction and imports a phantom row.
A description is NOT needed to import (it is not mandatory on Bank Transaction), so a
bank that omits narration still works - its table just starts unticked.
"""
if not final_transactions:
return False
return any(column.get("maps_to") == "Description" for column in table.get("column_mapping", []))
def _clean_cell(cell) -> str:
"""Normalize a pdfplumber cell: None -> '', collapse wrapped newlines, strip."""
if cell is None:
@@ -1055,6 +1085,43 @@ def get_float_amount(amount):
return amount
# A "CR"/"DR" marker on the amount itself, at either end: "2,378.00Cr", "Cr 100",
# "INR 50.90 Cr.", "DR 1,234.50".
# `(?![a-zA-Z])` rather than `\b` on the leading form: there is no word boundary between
# the "r" of "Cr100" and the digit, but there IS one inside "CREDIT" and "DRAFT".
AMOUNT_CR_DR_PATTERN = re.compile(r"^\s*(cr|dr)(?![a-zA-Z])\.?|(?:^|[\s\d.)])(cr|dr)\b\.?\s*$", re.IGNORECASE)
def get_amount_cr_dr_marker(amount) -> str | None:
"""
Return "cr" or "dr" if the amount cell carries a direction marker of its own, else None.
What is left after removing the marker has to look like an amount - it must hold a digit
and at most a short currency token - so that text which merely starts or ends with the
letters is not read as a marker. That guard is what separates "Cr 100" from a
description that bled into the amount column, like "Dr Smith Clinic 500".
"""
if not isinstance(amount, str):
return None
match = AMOUNT_CR_DR_PATTERN.search(amount)
if not match:
return None
# Only the marker itself is removed - the surrounding character the pattern needed to
# anchor on (a digit, say) stays part of the remainder.
group = 1 if match.group(1) else 2
start, end = match.span(group)
remainder = amount[:start] + amount[end:]
if not any(char.isdigit() for char in remainder):
return None
if sum(char.isalpha() for char in remainder) > 3:
return None
return match.group(group).lower()
def get_file_properties(transactions: list):
"""
From the transaction rows, try to figure out the following:
@@ -1075,6 +1142,8 @@ def get_file_properties(transactions: list):
'Transaction type column has "C"/"D" values': 0,
}
amount_column_has_cr_dr = False
for transaction in transactions:
date_format = transaction.get("date_format")
@@ -1092,33 +1161,40 @@ def get_file_properties(transactions: list):
if not amount:
continue
if isinstance(amount, str) and ("cr" in amount.lower() or "dr" in amount.lower()):
debit_credit = str(transaction.get("debit_credit") or "").strip().lower()
# One vote per row, most specific signal first. Order matters: "withdrawal" contains
# "dr", so it must be matched before the loose cr/dr check or a Deposit/Withdrawal
# column reads as CR/DR. "debit" needs listing because, unlike "credit", it does not
# contain "dr". The final else means every row votes, even an unrecognised type.
if get_amount_cr_dr_marker(amount):
amount_column_has_cr_dr = True
amount_format_frequency['Amount column has "CR"/"DR" values'] += 1
# Check if there's a debit_credit column containing "cr"/"dr"
if transaction.get("debit_credit", None):
if (
"cr" in transaction.get("debit_credit", "").lower()
or "dr" in transaction.get("debit_credit", "").lower()
):
amount_format_frequency['Transaction type column has "CR"/"DR" values'] += 1
elif (
"deposit" in transaction.get("debit_credit", "").lower()
or "withdrawal" in transaction.get("debit_credit", "").lower()
):
amount_format_frequency['Transaction type column has "Deposit"/"Withdrawal" values'] += 1
elif (transaction.get("debit_credit", "").lower().strip() == "c") or (
transaction.get("debit_credit", "").lower().strip() == "d"
):
amount_format_frequency['Transaction type column has "C"/"D" values'] += 1
# Else assume that the amount is expressed as positive/negative value
elif "deposit" in debit_credit or "withdrawal" in debit_credit:
amount_format_frequency['Transaction type column has "Deposit"/"Withdrawal" values'] += 1
elif debit_credit in ("c", "d"):
amount_format_frequency['Transaction type column has "C"/"D" values'] += 1
elif any(token in debit_credit for token in ("cr", "dr", "debit")):
amount_format_frequency['Transaction type column has "CR"/"DR" values'] += 1
else:
# Nothing said which direction this is, so assume the amount carries the sign.
amount_format_frequency["Amount column has positive/negative values"] += 1
most_common_date_format = max(date_format_frequency, key=date_format_frequency.get)
most_common_amount_format = max(amount_format_frequency, key=amount_format_frequency.get)
# With no votes at all (no rows, or every amount blank) max() would return whichever key
# happens to be first in the dict. Say what we mean instead.
if not amount_format_frequency[most_common_amount_format]:
most_common_amount_format = "Amount column has positive/negative values"
# A CR/DR amount column is proved by a single marker, not by a majority: both formats
# describe the same column, and an unmarked row is only the default direction, not
# evidence against the notation. Statements mark just the exceptions - one HDFC
# credit-card page has 18 rows and a single "50.90Cr".
if amount_column_has_cr_dr and most_common_amount_format == "Amount column has positive/negative values":
most_common_amount_format = 'Amount column has "CR"/"DR" values'
return most_common_date_format, most_common_amount_format

View File

@@ -11,12 +11,14 @@ from erpnext.accounts.doctype.bank_statement_import_log.bank_statement_import_lo
detect_column_mapping,
detect_header_row,
extract_pdf_tables,
get_amount_cr_dr_marker,
get_float_amount,
get_statement_details,
guess_column_mapping_by_content,
reextract_pdf_table,
set_header_index,
set_pdf_table_header,
should_include_table,
update_column_mapping,
update_pdf_tables,
)
@@ -124,6 +126,184 @@ class TestBankStatementImportLog(ERPNextTestSuite, AccountsTestMixin):
self.assertIsNone(get_float_amount("ABCD"))
self.assertIsNone(get_float_amount("****"))
# ------------------------------------------------------------------ #
# Amount format detection
# ------------------------------------------------------------------ #
def test_amount_cr_dr_marker(self):
"""The marker is read at either end of the cell, but only next to the amount."""
for amount in ("2,378.00Cr", "50.90 CR", "INR 50.90 Cr.", "1000cr", "5cr", "(100) Cr"):
self.assertEqual(get_amount_cr_dr_marker(amount), "cr", amount)
for amount in ("2,378.00Dr", "50.90 DR", "1000dr", "-100 Dr"):
self.assertEqual(get_amount_cr_dr_marker(amount), "dr", amount)
# Some banks put the marker in front of the digits instead.
for amount in ("Cr 100", "Cr100", "CR INR 100", "cr 0.00"):
self.assertEqual(get_amount_cr_dr_marker(amount), "cr", amount)
for amount in ("Dr 100", "Dr100", "Dr. 1,234.50"):
self.assertEqual(get_amount_cr_dr_marker(amount), "dr", amount)
for amount in ("100.00", "-2,000.00", "INR 25,236.00", "", None, 100.0):
self.assertIsNone(get_amount_cr_dr_marker(amount), amount)
# Text that merely starts or ends with the letters must not be read as a marker, or
# a description that bled into the amount column would reclassify the statement.
for amount in (
"CREDIT CARD PAYMENT 500",
"DRAFT 100",
"Dr Smith Clinic 500",
"DR AMBEDKAR ROAD BRANCH 500",
"500 CRC",
"Cheque Dr",
"Cr",
):
self.assertIsNone(get_amount_cr_dr_marker(amount), amount)
def test_sparsely_marked_cr_dr_amount_column(self):
"""One marker is enough to prove a CR/DR amount column - it is not a majority vote.
A real HDFC credit-card page carries 18 rows and a single "50.90Cr": the unmarked
rows are ordinary purchases, and only the exceptions are marked. A frequency vote
therefore picked "positive/negative" 17-1 and imported that lone credit as a debit.
"""
doc = self._create_bank_statement_import_log(
[
["Date", "Transaction Description", "Amount (in Rs.)"],
["21/07/2026", "ITC MAURYA NEW DELHI", "2,495.00"],
["22/07/2026", "ZOMATO LIMITED Gurugram", "1,288.68"],
["23/07/2026", "SWIGGY Bangalore", "532.00"],
["26/07/2026", "SWIGGY Bangalore", "1,043.00"],
["27/07/2026", "PETRO SURCHARGE WAIVER", "50.90Cr"],
]
)
self.assertEqual(doc.detected_amount_format, 'Amount column has "CR"/"DR" values')
# Only "Cr" appears, so it is the marked exception and unmarked rows are debits.
self.assertEqual(doc.total_credits, 50.90)
self.assertEqual(doc.total_credit_transactions, 1)
self.assertEqual(doc.total_debits, 5358.68)
self.assertEqual(doc.total_debit_transactions, 4)
def test_dr_only_statement_treats_unmarked_rows_as_deposits(self):
"""The mirror image of a Cr-only statement: only withdrawals are marked.
The unmarked default cannot be hardcoded to the debit, because which side gets
marked varies by bank. It is derived from the markers the statement actually uses -
here only "Dr" appears, so "Dr" is the exception and everything unmarked is a
deposit.
"""
doc = self._create_bank_statement_import_log(
[
["Date", "Narration", "Amount"],
["01/04/2026", "ATM WITHDRAWAL", "2,000.00Dr"],
["03/04/2026", "SALARY", "20,000.00"],
["05/04/2026", "INTEREST", "150.00"],
]
)
self.assertEqual(doc.detected_amount_format, 'Amount column has "CR"/"DR" values')
self.assertEqual(doc.total_debits, 2000.0)
self.assertEqual(doc.total_debit_transactions, 1)
self.assertEqual(doc.total_credits, 20150.0)
self.assertEqual(doc.total_credit_transactions, 2)
def test_leading_cr_dr_markers(self):
"""Some banks print the marker in front of the amount."""
doc = self._create_bank_statement_import_log(
[
["Date", "Narration", "Amount"],
["01/04/2026", "ATM WITHDRAWAL", "Dr 2,000.00"],
["03/04/2026", "SALARY", "Cr 20,000.00"],
]
)
self.assertEqual(doc.detected_amount_format, 'Amount column has "CR"/"DR" values')
self.assertEqual(doc.total_debits, 2000.0)
self.assertEqual(doc.total_credits, 20000.0)
def test_partially_marked_cr_dr_amount_column(self):
"""A CR/DR amount column stays CR/DR even when some rows carry no marker.
Every unmarked row used to also vote for "positive/negative", so an ordinary
statement with a few unmarked rows was detected as positive/negative and a
"2000.00Dr" was then imported as a deposit.
"""
doc = self._create_bank_statement_import_log(
[
["Date", "Narration", "Amount", "Balance"],
["01/04/2026", "OPENING FEE", "100.00", "9,900.00"],
["03/04/2026", "SALARY", "20000.00Cr", "29,900.00"],
["05/04/2026", "ATM WDL", "2000.00Dr", "27,900.00"],
]
)
self.assertEqual(doc.detected_amount_format, 'Amount column has "CR"/"DR" values')
# Both markers appear, so an unmarked row is undetermined and stays a debit.
self.assertEqual(doc.total_debits, 2100.0)
self.assertEqual(doc.total_debit_transactions, 2)
self.assertEqual(doc.total_credits, 20000.0)
self.assertEqual(doc.total_credit_transactions, 1)
def test_deposit_withdrawal_type_column(self):
"""The word Withdrawal contains "dr", so a loose CR/DR check claims this column first.
It then reads "Deposit" (which has no "cr" in it) as a withdrawal, flipping the
direction of every credit in the statement.
"""
doc = self._create_bank_statement_import_log(
[
["Date", "Narration", "Transaction Type", "Amount"],
["01/04/2026", "ATM WDL", "Withdrawal", "2,000.00"],
["03/04/2026", "SALARY", "Deposit", "20,000.00"],
["05/04/2026", "ATM WDL", "Withdrawal", "500.00"],
]
)
self.assertEqual(
doc.detected_amount_format, 'Transaction type column has "Deposit"/"Withdrawal" values'
)
self.assertEqual(doc.total_debits, 2500.0)
self.assertEqual(doc.total_debit_transactions, 2)
self.assertEqual(doc.total_credits, 20000.0)
self.assertEqual(doc.total_credit_transactions, 1)
def test_unrecognised_type_column_falls_back_to_signed_amount(self):
"""An unrecognised transaction type must not stop the amount being read.
No tally was incremented for these rows, so max() returned the first key -
"Separate columns for withdrawal and deposit" - and, with no such columns in the
file, every amount came through as None.
"""
doc = self._create_bank_statement_import_log(
[
["Date", "Narration", "Transaction Type", "Amount"],
["01/04/2026", "ATM WDL", "NEFT", "-2,000.00"],
["03/04/2026", "SALARY", "IMPS", "20,000.00"],
]
)
self.assertEqual(doc.detected_amount_format, "Amount column has positive/negative values")
self.assertEqual(doc.total_debits, 2000.0)
self.assertEqual(doc.total_credits, 20000.0)
def test_blank_transaction_type_cell(self):
"""A blank type cell used to raise - `None.lower()` - instead of parsing the row."""
doc = self._create_bank_statement_import_log(
[
["Date", "Narration", "Transaction Type", "Amount"],
["01/04/2026", "ATM WDL", "Dr", "2,000.00"],
["03/04/2026", "SALARY", "Cr", "20,000.00"],
["05/04/2026", "UNKNOWN", None, "500.00"],
]
)
self.assertEqual(doc.detected_amount_format, 'Transaction type column has "CR"/"DR" values')
# The unmarked row has no direction of its own, so it counts as a withdrawal.
self.assertEqual(doc.total_debits, 2500.0)
self.assertEqual(doc.total_credits, 20000.0)
# ------------------------------------------------------------------ #
# PDF statement import
# ------------------------------------------------------------------ #
@@ -159,7 +339,8 @@ class TestBankStatementImportLog(ERPNextTestSuite, AccountsTestMixin):
else:
table["header_index"] = None
table["column_mapping"] = guess_column_mapping_by_content(table["rows"])
table["included"] = True
final_transactions, _df, _af = build_table_transactions(table)
table["included"] = should_include_table(table, final_transactions)
return table
def test_pdf_multi_page_kept_separate_and_unioned(self):
@@ -197,6 +378,74 @@ class TestBankStatementImportLog(ERPNextTestSuite, AccountsTestMixin):
final, _df, _af = build_table_transactions(ad_table)
self.assertEqual(final, [])
def test_pdf_summary_box_not_auto_included(self):
"""A summary box that happens to parse as one transaction must not start included.
The "Payment Due Date / Total Dues / Minimum Amount Due" block on an HDFC
credit-card statement has a date column and a figures column, so it yields a single
transaction - the due date and the minimum amount - and used to import as a phantom
row. What it does not have, and a real transaction table always does, is a narration.
"""
summary_box = {
"header_index": 1,
"rows": [
["Statement Date:17/08/2025", "Card No: 4341 55XX XXXX 2754", ""],
["Payment Due Date", "Total Dues", "Minimum Amount Due"],
["06/09/2025", "73,200.00", "3,660.00"],
["Credit Limit", "Available Credit Limit", "Available Cash Limit"],
["", "32,800", ""],
],
"column_mapping": [
{"index": 0, "header_text": "Payment Due Date", "variable": "a", "maps_to": "Date"},
{"index": 1, "header_text": "Total Dues", "variable": "b", "maps_to": "Do not import"},
{"index": 2, "header_text": "Minimum Amount Due", "variable": "c", "maps_to": "Amount"},
],
}
final, _df, _af = build_table_transactions(summary_box)
# It really does parse as a transaction - that is why the previous check missed it.
self.assertEqual(len(final), 1)
self.assertFalse(should_include_table(summary_box, final))
# The transaction table beside it, which does carry a narration, still starts included.
transactions = self._auto_map(
{
"rows": [
["Date", "Transaction Description", "Amount (in Rs.)"],
["21/07/2025", "ITC MAURYA NEW DELHI", "2,495.00"],
["27/07/2025", "PETRO SURCHARGE WAIVER", "50.90Cr"],
]
}
)
self.assertTrue(transactions["included"])
def test_pdf_table_without_description_still_importable(self):
"""No narration column means "starts unticked", NOT "cannot be imported".
`description` is not mandatory on Bank Transaction, so a bank that omits narration
must still import once the user ticks the table.
"""
table = {
"header_index": 0,
"rows": [
["Date", "Amount", "Balance"],
["01/04/2025", "500.00", "9,500.00"],
["03/04/2025", "20000.00", "29,500.00"],
],
"column_mapping": [
{"index": 0, "header_text": "Date", "variable": "a", "maps_to": "Date"},
{"index": 1, "header_text": "Amount", "variable": "b", "maps_to": "Amount"},
{"index": 2, "header_text": "Balance", "variable": "c", "maps_to": "Balance"},
],
}
final, _df, _af = build_table_transactions(table)
self.assertFalse(should_include_table(table, final))
# The transactions themselves are intact and importable.
self.assertEqual(len(final), 2)
self.assertEqual([t["date"] for t in final], ["2025-04-01", "2025-04-03"])
def test_headerless_content_mapping(self):
"""Without a header row, columns are guessed from their contents."""
rows = [

View File

@@ -4,6 +4,7 @@
"autoname": "naming_series:",
"creation": "2018-10-22 18:19:02.784533",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank-transaction",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -277,6 +278,7 @@
}
],
"grid_page_length": 50,
"icon": "arrow-left-right",
"is_submittable": 1,
"links": [],
"modified": "2026-04-11 20:41:15.124085",

View File

@@ -397,7 +397,7 @@ def unreconcile_transaction(transaction_name: str | int):
frappe.get_doc(voucher["doctype"], voucher["name"]).cancel()
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def unreconcile_transaction_entry(bank_transaction_id: str | int, voucher_type: str, voucher_id: str | int):
"""
Removes a single payment entry from a bank transaction - for example only undoing one voucher instead of undoing the entire transaction

View File

@@ -11,6 +11,11 @@ from frappe.utils.dateutils import parse_date
@frappe.whitelist()
def upload_bank_statement():
# Parsing a statement is the first step of creating Bank Transactions from it, so that is the
# right to require. Both functions in this file are reached only over HTTP — nothing in the tree
# calls either — so there is no caller to break.
frappe.has_permission("Bank Transaction", "create", throw=True)
if getattr(frappe, "uploaded_file", None):
with open(frappe.uploaded_file, "rb") as upfile:
fcontent = upfile.read()
@@ -34,8 +39,14 @@ def upload_bank_statement():
return {"columns": columns, "data": data}
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_bank_entries(columns: str, data: str | list, bank_account: str):
# insert()/submit() below already enforce this per document, but only after the per-row loop has
# read the Bank Account and its Bank mapping and written an Error Log for every rejected row —
# so check once up front rather than failing row by row.
frappe.has_permission("Bank Transaction", "create", throw=True)
frappe.has_permission("Bank Account", doc=bank_account, throw=True)
header_map = get_header_mapping(columns, bank_account)
success = 0

View File

@@ -23,8 +23,6 @@ from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed
class TestBankTransaction(ERPNextTestSuite):
def setUp(self):
make_pos_profile()
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
uniq_identifier = frappe.generate_hash(length=10)
gl_account = create_gl_account("_Test Bank " + uniq_identifier)
@@ -32,6 +30,7 @@ class TestBankTransaction(ERPNextTestSuite):
gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
)
make_pos_profile()
add_transactions(bank_account=bank_account)
add_vouchers(gl_account=gl_account)
@@ -47,7 +46,7 @@ class TestBankTransaction(ERPNextTestSuite):
from_date=bank_transaction.date,
to_date=utils.today(),
)
self.assertEqual(linked_payments[0]["party"], "Conrad Electronic")
self.assertIn("Conrad Electronic", [payment["party"] for payment in linked_payments])
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self):

View File

@@ -140,6 +140,7 @@
}
],
"grid_page_length": 50,
"icon": "zap",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-04-11 21:36:26.754667",

View File

@@ -157,12 +157,9 @@ class BankTransactionRule(Document):
"""
Delete the matched rule from the bank transaction
"""
try:
frappe.db.set_value(
"Bank Transaction", {"matched_transaction_rule": self.name}, "matched_transaction_rule", None
)
except Exception:
pass
frappe.db.set_value(
"Bank Transaction", {"matched_transaction_rule": self.name}, "matched_transaction_rule", None
)
def after_delete(self):
"""

Some files were not shown because too many files have changed in this diff Show More