Compare commits

...

431 Commits

Author SHA1 Message Date
Frappe PR Bot
a5de60c357 chore(release): Bumped to Version 16.29.0
# [16.29.0](https://github.com/frappe/erpnext/compare/v16.28.0...v16.29.0) (2026-07-22)

### Bug Fixes

* add fetch from in production plan material request child table ([2852671](2852671cd5))
* added missing validations for `Dunning Type` (backport [#57224](https://github.com/frappe/erpnext/issues/57224)) ([#57227](https://github.com/frappe/erpnext/issues/57227)) ([3948951](3948951904))
* allow delivery when a batch is reserved across multiple sales orders (backport [#57169](https://github.com/frappe/erpnext/issues/57169)) ([56bbca0](56bbca0203))
* block changing Stock account type when stock ledger entries exist (backport [#57283](https://github.com/frappe/erpnext/issues/57283)) ([#57285](https://github.com/frappe/erpnext/issues/57285)) ([a26296c](a26296ca50))
* consider min order qty in the purchase/transfer flow of production plan (backport [#57204](https://github.com/frappe/erpnext/issues/57204)) ([cbd4f93](cbd4f93f3d))
* correct typo in allow_negative_stock parameter ([e2be05e](e2be05e48b))
* dont overwrite rate with 0 if not found ([aa08f75](aa08f753b4))
* exclude fully-delivered reservations from the conflict message ([555c607](555c607f2f))
* exclude transferred_qty from work order item to pick list item mapping ([c466b49](c466b49d09)), closes [#57253](https://github.com/frappe/erpnext/issues/57253) [#57236](https://github.com/frappe/erpnext/issues/57236) [#56596](https://github.com/frappe/erpnext/issues/56596)
* hide job card field in purchase order item ([20f6ac8](20f6ac81b9))
* keep Standard Cost stock value in step with the standard rate ([fe65882](fe65882e59))
* name every conflicting voucher in the reserved batch error ([839fd5e](839fd5e3b6))
* parallel reposting stalls between scheduler ticks (backport [#57220](https://github.com/frappe/erpnext/issues/57220)) ([#57248](https://github.com/frappe/erpnext/issues/57248)) ([a3bfded](a3bfdede06))
* pass ctx keys get_price_list_rate_for reads, skip rate update on insert ([c3aea9c](c3aea9ca9c))
* **payments:** ensure `payments` app installed on the site in `payment_app_import_guard` (backport [#57342](https://github.com/frappe/erpnext/issues/57342)) ([#57344](https://github.com/frappe/erpnext/issues/57344)) ([2c8c076](2c8c076f6e))
* permission issue (backport [#57112](https://github.com/frappe/erpnext/issues/57112)) (backport [#57142](https://github.com/frappe/erpnext/issues/57142)) ([#57184](https://github.com/frappe/erpnext/issues/57184)) ([0817d10](0817d1064c))
* project % complete field allowing modification when manual method (backport [#57274](https://github.com/frappe/erpnext/issues/57274)) ([#57276](https://github.com/frappe/erpnext/issues/57276)) ([0d53f1a](0d53f1adb7))
* **project:** improved access control for project users (backport [#56675](https://github.com/frappe/erpnext/issues/56675)) ([#57181](https://github.com/frappe/erpnext/issues/57181)) ([b1adec7](b1adec7e9e))
* **report:** handle nonetype error in timesheet billing summary grouping logic ([b7cf3bf](b7cf3bf641))
* rescale batch FIFO slot values on stock reconciliation ([6b3b03f](6b3b03fcd8))
* rescale stock ageing FIFO slot values on stock reconciliation ([c9394c0](c9394c030f))
* resolve float precision before streaming stock ledger entries ([0f25254](0f252542c3))
* restrict jinja globals in process statement of accounts templates (backport [#56458](https://github.com/frappe/erpnext/issues/56458)) ([#57232](https://github.com/frappe/erpnext/issues/57232)) ([5f6952b](5f6952b15c))
* revalue batch reco slots only when the entry covers the full batch ([a4bf506](a4bf50656a))
* round outstanding qty guard consistently with the conflict gate ([f3e1b3f](f3e1b3fca7))
* set correct currency in supplier quotation net rate field ([e24ef84](e24ef847dc))
* show transaction currency symbol in Payment Request schedule dialog and reference table (backport [#57050](https://github.com/frappe/erpnext/issues/57050)) ([#57312](https://github.com/frappe/erpnext/issues/57312)) ([429b58b](429b58b833))
* **stock:** correct stock ageing value for moving average and lifo items ([#56693](https://github.com/frappe/erpnext/issues/56693)) ([02549ef](02549ef8c4))
* strip account number when building account name in COA importer ([e1ebfa7](e1ebfa7163))
* sync process loss percentage when fg qty changes ([beeffee](beeffee8f9))
* sync translations from crowdin ([#57187](https://github.com/frappe/erpnext/issues/57187)) ([827831a](827831a247))
* use system float precision for batch qty comparison ([92217b2](92217b2c45))
* validate buying price list on material request and update item rates on change ([025b2f2](025b2f2922))

### Features

* add on hold status to project ([57a49ce](57a49ce168))
* recalculate valuation rate and stock value from Bin ([a38cbfc](a38cbfc88e))
* **stock:** automatically link portal users to their associated contact profiles for customers and suppliers ([134d63d](134d63de78))
* **stock:** expose all Bin qty fields in Stock Summary and Stock Projected Qty ([0bdf258](0bdf258888))
2026-07-22 00:17:03 +00:00
Diptanil Saha
264bfa188b Merge pull request #57324 from frappe/version-16-hotfix
chore: release v16
2026-07-22 05:45:21 +05:30
mergify[bot]
2c8c076f6e fix(payments): ensure payments app installed on the site in payment_app_import_guard (backport #57342) (#57344)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-21 23:12:53 +00:00
mergify[bot]
a0ac2a58dd chore: remove apiclient (backport #57339) (#57341)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-21 20:26:20 +00:00
Shllokkk
5930ce623c Merge pull request #57321 from Shllokkk/sync-process-loss-percentage-v16
fix: sync process loss percentage when fg qty changes
2026-07-21 16:24:52 +05:30
Mihir Kandoi
3a196ae8e7 Merge pull request #57317 from mihir-kandoi/fix-stock-ageing-reco-revaluation-v16
fix: rescale stock ageing FIFO slot values on stock reconciliation (backport #57316)
2026-07-21 15:46:22 +05:30
Shllokkk
beeffee8f9 fix: sync process loss percentage when fg qty changes 2026-07-21 15:11:10 +05:30
Mihir Kandoi
0f252542c3 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:12 +05:30
Mihir Kandoi
92217b2c45 fix: use system float precision for batch qty comparison 2026-07-21 14:18:24 +05:30
Mihir Kandoi
a4bf50656a 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:33 +05:30
Mihir Kandoi
6b3b03fcd8 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:41 +05:30
Mihir Kandoi
c9394c030f 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:45:02 +05:30
mergify[bot]
429b58b833 fix: show transaction currency symbol in Payment Request schedule dialog and reference table (backport #57050) (#57312)
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.



* 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.

---------




(cherry picked from commit 83e04dd773)

Co-authored-by: Henil Maru <henil@frappe.io>
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 12:59:50 +05:30
mergify[bot]
e0d0bf07c8 refactor: rework appointment booking lifecycle and portal verification (backport #57270) (#57295)
* refactor: rework appointment booking lifecycle and portal verification (#57270)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 73004c6e4b)

# Conflicts:
#	erpnext/crm/doctype/appointment/test_appointment.py
#	erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py
#	erpnext/www/book_appointment/index.py

* chore: resolve conflicts

* fix: parse contact as native JSON in create_appointment

version-16-hotfix never received develop's 9955adb2fc, so the backported
tests calling create_appointment with a dict contact crashed on
json.loads. Use frappe.parse_json to accept both str and dict, matching
develop.

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

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 00:17:01 +05:30
mergify[bot]
a26296ca50 fix: block changing Stock account type when stock ledger entries exist (backport #57283) (#57285)
fix: block changing Stock account type when stock ledger entries exist (#57283)

(cherry picked from commit 4cdaa8dba6)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-07-20 22:58:22 +05:30
Mihir Kandoi
8a320fc0af Merge pull request #57302 from frappe/mergify/bp/version-16-hotfix/pr-57300
feat: recalculate valuation rate and stock value from Bin (backport #57300)
2026-07-20 20:40:02 +05:30
Mihir Kandoi
f5bb4052fa Merge pull request #57304 from frappe/mergify/bp/version-16-hotfix/pr-57301
feat(stock): expose all Bin qty fields in Stock Summary and Stock Projected Qty (backport #57301)
2026-07-20 20:39:47 +05:30
Mihir Kandoi
0bdf258888 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.

(cherry picked from commit 59c0c15c2e)
2026-07-20 14:48:22 +00:00
Mihir Kandoi
fe65882e59 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.

(cherry picked from commit 49a43aad81)
2026-07-20 14:40:41 +00:00
Mihir Kandoi
a38cbfc88e 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).

(cherry picked from commit df79e85f53)
2026-07-20 14:40:41 +00:00
Mihir Kandoi
942a8db7fa Merge pull request #57297 from frappe/mergify/bp/version-16-hotfix/pr-57280
fix(report): handle nonetype error in timesheet billing summary group… (backport #57280)
2026-07-20 17:54:31 +05:30
Poovetha
b7cf3bf641 fix(report): handle nonetype error in timesheet billing summary grouping logic
(cherry picked from commit 9a7209e668)
2026-07-20 12:14:13 +00:00
mergify[bot]
0d53f1adb7 fix: project % complete field allowing modification when manual method (backport #57274) (#57276)
Co-authored-by: nishkagosalia <nishka.gosalia@gmail.com>
2026-07-20 17:28:39 +05:30
Mihir Kandoi
fcee4f128e Merge pull request #57282 from frappe/mergify/bp/version-16-hotfix/pr-57273
fix: correct typo in allow_negative_stock parameter (backport #57273)
2026-07-20 12:43:53 +05:30
Afsal Syed
e2be05e48b fix: correct typo in allow_negative_stock parameter
(cherry picked from commit b3a616c328)
2026-07-20 07:01:53 +00:00
MochaMind
a9c3a00dc4 chore: update POT file (#57268) 2026-07-19 14:50:42 +02:00
Mihir Kandoi
50c79c82bb Merge pull request #57257 from aerele/fix-pick-list-work-order-transferred-qty-leak-v16
fix: exclude transferred_qty from work order item to pick list item m…
2026-07-18 16:52:35 +05:30
pandiyan
c466b49d09 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.

backport of #57253

fixes #57236, related to #56596
2026-07-18 16:42:40 +05:30
Mihir Kandoi
406d3b3862 Merge pull request #57252 from frappe/mergify/bp/version-16-hotfix/pr-57249
fix: add fetch from in production plan material request child table (backport #57249)
2026-07-17 22:32:49 +05:30
Mihir Kandoi
2852671cd5 fix: add fetch from in production plan material request child table
(cherry picked from commit dfc2a411e1)
2026-07-17 16:50:49 +00:00
rohitwaghchaure
a3bfdede06 fix: parallel reposting stalls between scheduler ticks (backport #57220) (#57248)
fix: parallel reposting stalls between scheduler ticks (#57220)

(cherry picked from commit 40f861c0a0)
2026-07-17 22:17:47 +05:30
Mihir Kandoi
34a65e5dba Merge pull request #57247 from frappe/mergify/bp/version-16-hotfix/pr-57241
fix: validate buying price list on material request and update item rates on change (backport #57241)
2026-07-17 21:49:56 +05:30
Mihir Kandoi
ec0da0f113 chore: remove unneccessary flt
(cherry picked from commit 3a63f61832)
2026-07-17 16:09:12 +00:00
Mihir Kandoi
aa08f753b4 fix: dont overwrite rate with 0 if not found
(cherry picked from commit 1ef3cd1d3f)
2026-07-17 16:09:12 +00:00
Mihir Kandoi
c3aea9ca9c 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.

(cherry picked from commit 6dcc0cab3a)
2026-07-17 16:09:11 +00:00
Mihir Kandoi
025b2f2922 fix: validate buying price list on material request and update item rates on change
(cherry picked from commit 18b15f2ca9)
2026-07-17 16:09:11 +00:00
mergify[bot]
5f6952b15c fix: restrict jinja globals in process statement of accounts templates (backport #56458) (#57232)
fix: restrict jinja globals in process statement of accounts templates

(cherry picked from commit ecb6d48ec0)

Co-authored-by: Shllokkk <shllokosan23@gmail.com>
2026-07-17 08:20:41 +00:00
mergify[bot]
3948951904 fix: added missing validations for Dunning Type (backport #57224) (#57227)
* fix: added missing validations for `Dunning Type`

(cherry picked from commit c8e7674d63)

* test: added tests for `Dunning Type` validation

(cherry picked from commit 2325068b19)

# Conflicts:
#	erpnext/accounts/doctype/dunning_type/test_dunning_type.py

* chore: resolve conflict

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-07-17 06:57:37 +00:00
Shllokkk
915562c7a8 Merge pull request #57219 from frappe/mergify/bp/version-16-hotfix/pr-57198
fix: strip account number when building account name in COA importer (backport #57198)
2026-07-17 11:39:00 +05:30
Mihir Kandoi
dfd05caeb0 Merge pull request #57017 from frappe/mergify/bp/version-16-hotfix/pr-56693
fix(stock): correct stock ageing value for moving average and lifo items (backport #56693)
2026-07-17 07:49:09 +05:30
MochaMind
827831a247 fix: sync translations from crowdin (#57187) 2026-07-16 18:00:39 +02:00
Shllokkk
e1ebfa7163 fix: strip account number when building account name in COA importer
(cherry picked from commit cbe406ee2a)
2026-07-16 15:21:50 +00:00
mergify[bot]
c055de2da6 refactor(dunning): converted get_dunning_letter_text to doc method and restrict_globals on render_template (backport #57205) (#57214)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-16 11:33:41 +00:00
Mihir Kandoi
c5d94d5fc3 Merge pull request #57211 from frappe/mergify/bp/version-16-hotfix/pr-57179
feat(stock): automatically link portal users to their associated contact profiles for customers and suppliers (backport #57179)
2026-07-16 16:29:56 +05:30
Afsal Syed
74451366c2 test(stock): add portal user contact link verification for customer and supplier
(cherry picked from commit 9ae2069bd9)
2026-07-16 15:46:06 +05:30
Afsal Syed
134d63de78 feat(stock): automatically link portal users to their associated contact profiles for customers and suppliers
(cherry picked from commit 337a06dfb6)
2026-07-16 15:44:45 +05:30
Mihir Kandoi
8946fc41a2 Merge pull request #57208 from mihir-kandoi/fix-production-plan-min-order-qty-v16
fix: consider min order qty in the purchase/transfer flow of production plan (backport #57204)
2026-07-16 13:46:23 +05:30
Mihir Kandoi
cbd4f93f3d fix: consider min order qty in the purchase/transfer flow of production plan (backport #57204)
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 13:36:04 +05:30
mergify[bot]
0817d1064c fix: permission issue (backport #57112) (backport #57142) (#57184)
* fix: permission issue (backport #57112) (#57142)

* fix: permission issue (#57112)

(cherry picked from commit 1fd2faa68d)

# Conflicts:
#	erpnext/controllers/stock_controller.py

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit 6b23b007a4)

# Conflicts:
#	erpnext/controllers/stock_controller.py

* chore: fix conflicts

* chore: fix conflicts

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-07-15 15:05:17 +05:30
mergify[bot]
b1adec7e9e fix(project): improved access control for project users (backport #56675) (#57181)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-15 14:19:07 +05:30
mergify[bot]
caea21208e Revert "chore: remove unused whitelisted method from project" (backport #56660) (#57178)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-15 07:09:36 +00:00
Mihir Kandoi
2808b34d42 Merge pull request #57170 from mihir-kandoi/backport-57169-version-16-hotfix
fix: allow delivery when a batch is reserved across multiple sales orders (backport #57169)
2026-07-15 12:31:39 +05:30
Mihir Kandoi
7ccb2584c4 test: set delivered batch on the row explicitly on v16
The v16 reserved-stock mapper attaches neither a bundle nor row
serial/batch fields when use_serial_batch_fields is enabled, and
update_stock_reservation_entries crashes on the missing bundle (fixed
on develop by 9c5f9218b5, not backported). Deliver via an explicit row
batch_no like the guard test so the bundle is built from row fields
before the reservation update runs.
2026-07-15 12:19:21 +05:30
Mihir Kandoi
f3e1b3fca7 fix: round outstanding qty guard consistently with the conflict gate 2026-07-15 12:00:02 +05:30
Mihir Kandoi
555c607f2f fix: exclude fully-delivered reservations from the conflict message 2026-07-15 12:00:02 +05:30
Mihir Kandoi
7cd7e4ab0f test: deliver reserved batches via bundles on v16
use_serial_batch_fields delivery of reserved stock crashes on v16 with
'Serial and Batch Bundle None not found' (fixed on develop only), so
deliver through auto-created bundles like test_auto_reserve_serial_and_batch.
2026-07-15 12:00:02 +05:30
Mihir Kandoi
839fd5e3b6 fix: name every conflicting voucher in the reserved batch error 2026-07-15 12:00:02 +05:30
Mihir Kandoi
56bbca0203 fix: allow delivery when a batch is reserved across multiple sales orders (backport #57169)
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 12:00:02 +05:30
Mihir Kandoi
478719b828 Merge pull request #57173 from frappe/mergify/bp/version-16-hotfix/pr-57137
feat: add on hold status to project (backport #57137)
2026-07-15 11:29:14 +05:30
Poovitha Palanivelu
57a49ce168 feat: add on hold status to project
(cherry picked from commit 672fadaa78)
2026-07-15 05:47:16 +00:00
Mihir Kandoi
6d47c51c5b Merge pull request #57166 from frappe/mergify/bp/version-16-hotfix/pr-57163
fix: hide job card field in purchase order item (backport #57163)
2026-07-15 11:03:06 +05:30
Mihir Kandoi
6ab1668b37 Merge pull request #57168 from frappe/mergify/bp/version-16-hotfix/pr-57164
fix: set correct currency in supplier quotation net rate field (backport #57164)
2026-07-15 10:58:18 +05:30
Mihir Kandoi
b256c76c3b chore: resolve conflicts 2026-07-15 10:48:51 +05:30
Mihir Kandoi
e24ef847dc fix: set correct currency in supplier quotation net rate field
(cherry picked from commit 27672851cd)
2026-07-15 05:17:11 +00:00
Mihir Kandoi
20f6ac81b9 fix: hide job card field in purchase order item
(cherry picked from commit f44bcae47d)

# Conflicts:
#	erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
2026-07-15 05:13:39 +00:00
Frappe PR Bot
de591661b9 chore(release): Bumped to Version 16.28.0
# [16.28.0](https://github.com/frappe/erpnext/compare/v16.27.0...v16.28.0) (2026-07-15)

### Bug Fixes

* **accounts:** added permission checks on `get_account_balances_coa` (backport [#57107](https://github.com/frappe/erpnext/issues/57107)) ([#57122](https://github.com/frappe/erpnext/issues/57122)) ([e8a5325](e8a532587d))
* allow barcode scan to add and increment items in pick list ([00a646a](00a646ac25))
* apply precision fix inline for v16-hotfix, drop develop-only asset_service refactor ([d3a8e91](d3a8e91cda))
* **asset:** allow asset repair creation for fully depreciated assets ([8658039](8658039e9a))
* **budget-variance:** correct month shift in comparison chart ([703e9a7](703e9a728c))
* guard company logo lookup in default letterheads ([7b54314](7b543142a2))
* **journal-entry:** fetch outstanding on foreign currency ([ec782ee](ec782ee20d))
* **manufacturing:** preserve job card transferred quantity ([d7e9321](d7e9321746))
* match depreciation schedule rows at currency precision to avoid duplicate JEs ([a1fae95](a1fae959ed))
* prevent duplicate material request items in purchase order ([4d951c1](4d951c1cf8))
* set explicit table and logo widths in grey letterhead ([0d80fef](0d80fef3bf))
* **stock:** fix sqlparse token limit in get_bundle_wise_serial_nos ([5a99dd6](5a99dd6016))
* **stock:** set stock_uom on transferred Stock Reservation Entries ([5991ecf](5991ecfa3d))
* **stock:** show qty (company) and qty (warehouse) in sales transactions ([0d9ace7](0d9ace7ab8))
* **tnc:** `get_terms_and_conditions` render_template with `safe_exec` (backport [#56944](https://github.com/frappe/erpnext/issues/56944)) ([#56977](https://github.com/frappe/erpnext/issues/56977)) ([40c85a0](40c85a0087))

### Features

* **balance-sheet:** implement execute_synced_report with full parity to normal report ([636bcbe](636bcbedc0))
* **general-ledger:** implement execute_synced_report with full parity to normal report ([19ec095](19ec095ff8))
* **profit-and-loss:** implement execute_synced_report with full parity to normal report ([21ddb00](21ddb00e20))
* **trial-balance:** implement execute_duckdb with full parity to normal report ([228418b](228418b05f))
* weekly auto-repost of incorrect stock valuation entries (backport [#56637](https://github.com/frappe/erpnext/issues/56637)) ([#56700](https://github.com/frappe/erpnext/issues/56700)) ([bf1b7f2](bf1b7f2bea))
2026-07-15 02:24:51 +00:00
ruthra kumar
9a7e796fd2 Merge pull request #57161 from frappe/version-16-hotfix
chore: release v16
2026-07-15 06:17:14 +05:30
Mihir Kandoi
b1ca9c26cb Merge pull request #57136 from frappe/mergify/bp/version-16-hotfix/pr-57129
fix: prevent duplicate material request items in purchase order (backport #57129)
2026-07-14 16:43:09 +05:30
Mihir Kandoi
4d931a7108 test: remove test
(cherry picked from commit b2ec906ff3)
2026-07-14 11:01:57 +00:00
SandraFrappe
4d951c1cf8 fix: prevent duplicate material request items in purchase order
(cherry picked from commit 2d6f89a7f5)
2026-07-14 11:01:57 +00:00
Mihir Kandoi
3a38316c26 Merge pull request #57126 from aerele/fix/job-card-work-order-transferred-qty-v16
fix(manufacturing): preserve job card transferred quantity
2026-07-14 13:40:29 +05:30
Sudharsanan11
fd6c9a71cd test(manufacturing): cover transferred quantity across job cards 2026-07-14 13:23:49 +05:30
Sudharsanan11
d7e9321746 fix(manufacturing): preserve job card transferred quantity 2026-07-14 13:23:28 +05:30
mergify[bot]
e8a532587d fix(accounts): added permission checks on get_account_balances_coa (backport #57107) (#57122)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-14 13:07:12 +05:30
Mihir Kandoi
8d7fe755e3 Merge pull request #57118 from frappe/mergify/bp/version-16-hotfix/pr-57116
fix(stock): fix sqlparse token limit in get_bundle_wise_serial_nos (backport #57116)
2026-07-14 12:33:43 +05:30
Khushi Rawat
2947f030e8 Merge pull request #57119 from frappe/mergify/bp/version-16-hotfix/pr-57111
chore: remove dead assets dashboard_fixtures with broken imports (backport #57111)
2026-07-14 12:14:47 +05:30
Afsal Syed
cec1e87c90 test(stock): add unit test for get_bundle_wise_serial_nos query
(cherry picked from commit e748bf512b)
2026-07-14 12:13:42 +05:30
khushi8112
345c508be7 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.

(cherry picked from commit 14a15cc6f9)
2026-07-14 06:23:40 +00:00
Afsal Syed
5a99dd6016 fix(stock): fix sqlparse token limit in get_bundle_wise_serial_nos
(cherry picked from commit 4544a6c935)
2026-07-14 06:00:14 +00:00
Mihir Kandoi
3b7467daab Merge pull request #57113 from PranavDarade/fix/transfer-reservation-stock-uom
fix(stock): set stock_uom on transferred Stock Reservation Entries
2026-07-14 10:14:59 +05:30
Khushi Rawat
db737da22e Merge pull request #57077 from frappe/mergify/bp/version-16-hotfix/pr-55276
fix(asset): allow asset repair creation for fully depreciated assets (backport #55276)
2026-07-14 00:24:20 +05:30
Khushi Rawat
138a44e450 Merge pull request #56966 from frappe/mergify/bp/version-16-hotfix/pr-56964
fix: match depreciation schedule rows at currency precision to avoid duplicate JEs (backport #56964)
2026-07-14 00:09:32 +05:30
Diptanil Saha
40c85a0087 fix(tnc): get_terms_and_conditions render_template with safe_exec (backport #56944) (#56977) 2026-07-13 23:22:55 +05:30
Mihir Kandoi
4f59e57151 Merge pull request #57105 from frappe/mergify/bp/version-16-hotfix/pr-57091
fix(stock): show qty (company) and qty (warehouse) in sales transactions  (backport #57091)
2026-07-13 21:27:54 +05:30
Mihir Kandoi
0447a18217 Merge pull request #57103 from frappe/mergify/bp/version-16-hotfix/pr-57089
fix: allow barcode scan to add and increment items in pick list (backport #57089)
2026-07-13 21:27:40 +05:30
PranavDarade
5991ecfa3d 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.
2026-07-13 21:21:03 +05:30
Sudharsanan11
77cca4464d 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.

(cherry picked from commit 4e5e1f6596)
2026-07-13 15:34:10 +00:00
Sudharsanan11
0d9ace7ab8 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.

(cherry picked from commit ab30bab6cb)
2026-07-13 15:34:09 +00:00
pandiyan
00a646ac25 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

(cherry picked from commit 3ece4a615d)
2026-07-13 15:33:11 +00:00
ruthra kumar
aa1d50a914 Merge pull request #57100 from frappe/mergify/bp/version-16-hotfix/pr-56706
refactor: rename synced to snapshot report (backport #56706)
2026-07-13 20:02:49 +05:30
ruthra kumar
c03d115999 refactor: rename feature toggle in report master
(cherry picked from commit 981e90e4da)
2026-07-13 13:17:01 +00:00
ruthra kumar
97a7a2d6bc refactor: rename execute_synced_report to execute_snapshot_report
Match the framework rename of the standard report entry point in the
trial balance, P&L, balance sheet, and general ledger reports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit ba7b6a47c5)
2026-07-13 13:17:01 +00:00
ruthra kumar
d784b9a7be Merge pull request #57098 from frappe/mergify/bp/version-16-hotfix/pr-56304
feat: faster (synced) financial statements using duckdb (backport #56304)
2026-07-13 18:08:15 +05:30
ruthra kumar
88b7a38be4 refactor: synced reports should be enabled on sites based on requirements
(cherry picked from commit 963bbc8729)
2026-07-13 12:16:28 +00:00
ruthra kumar
21ddb00e20 feat(profit-and-loss): implement execute_synced_report with full parity to normal report
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 6a93baacf0)
2026-07-13 12:16:28 +00:00
ruthra kumar
636bcbedc0 feat(balance-sheet): implement execute_synced_report with full parity to normal report
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit bb19540816)
2026-07-13 12:16:27 +00:00
ruthra kumar
19ec095ff8 feat(general-ledger): implement execute_synced_report with full parity to normal report
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 6b4895bcc9)
2026-07-13 12:16:27 +00:00
ruthra kumar
2f6ef7b2ec refactor: DB agnostic method names
(cherry picked from commit f40cd41801)
2026-07-13 12:16:26 +00:00
ruthra kumar
d41b9f11ff refactor: maintain sync dependency in report master
(cherry picked from commit 5c536b8ad1)
2026-07-13 12:16:25 +00:00
ruthra kumar
98a65f7529 refactor(trial-balance): execute_duckdb only reads GL Entry from duckdb
Replaces the previous over-engineered stub with 7 short functions.
Account data, Account Closing Balance, and all metadata come from
frappe.db as normal; only tabGL Entry is read from the duckdb_conn.

Reuses get_opening_balance() for Account Closing Balance unchanged,
reuses all downstream compute helpers (calculate_values, prepare_data,
etc.) unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 55862f98f4)
2026-07-13 12:16:24 +00:00
ruthra kumar
228418b05f feat(trial-balance): implement execute_duckdb with full parity to normal report
Replaces the placeholder stub with 8 focused functions that mirror the
normal execute() flow using parameterized DuckDB SQL queries: account
fetch, period GL entries, opening balances (with Period Closing Voucher
path), and all filters (cost center, project, finance book, accounting
dimensions). Reuses existing pure-Python processing functions unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit b1c8e2cb5c)
2026-07-13 12:16:24 +00:00
ruthra kumar
08e267271e refactor: reports on duckdb
(cherry picked from commit adb768505a)
2026-07-13 12:16:23 +00:00
mergify[bot]
bf1b7f2bea feat: weekly auto-repost of incorrect stock valuation entries (backport #56637) (#56700)
* feat: weekly auto-repost of incorrect stock valuation entries (#56637)

(cherry picked from commit adae0bd732)

# Conflicts:
#	erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py

* chore: fix conflicts

Removed merge conflict markers and cleaned up code.

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-07-13 10:11:12 +00:00
Frappe PR Bot
9d5c7605b8 chore(release): Bumped to Version 16.27.0
# [16.27.0](https://github.com/frappe/erpnext/compare/v16.26.2...v16.27.0) (2026-07-13)

### Bug Fixes

* **`frappe_crm_api`:** handle failure for `after_app_install` and `after_app_uninstall` ([eac3afc](eac3afcd88))
* **accounts:** clear clearance date when amending reconciled voucher ([6f28135](6f281356a6)), closes [#54909](https://github.com/frappe/erpnext/issues/54909)
* **accounts:** retain invoice table on opening invoice creation error (backport [#56353](https://github.com/frappe/erpnext/issues/56353)) ([#57058](https://github.com/frappe/erpnext/issues/57058)) ([4136ca5](4136ca55a7))
* add page numbers to print format footer ([0371e8e](0371e8eaf0))
* added permission checks on various whitelisted functions (backport [#56745](https://github.com/frappe/erpnext/issues/56745)) ([#56946](https://github.com/frappe/erpnext/issues/56946)) ([7ce1289](7ce1289c10))
* address review comments ([056195c](056195ce07))
* auto fetch serial no from previous operation output (backport to v16) ([#56861](https://github.com/frappe/erpnext/issues/56861)) ([17f2de4](17f2de42f3))
* **banking:** allow negative balance in bank statement import (backport [#56959](https://github.com/frappe/erpnext/issues/56959)) ([#57054](https://github.com/frappe/erpnext/issues/57054)) ([835e32c](835e32cec7))
* **banking:** handle blank password protected PDFs and negative amounts in CR/DR columns (backport [#56690](https://github.com/frappe/erpnext/issues/56690)) ([#56694](https://github.com/frappe/erpnext/issues/56694)) ([19c318d](19c318df68))
* **banking:** use custom renderer for translated strings and parser for rules (backport [#56643](https://github.com/frappe/erpnext/issues/56643)) ([#56647](https://github.com/frappe/erpnext/issues/56647)) ([2cb577b](2cb577b912))
* block serialized to non-serialized item change when SABB exists (backport [#56773](https://github.com/frappe/erpnext/issues/56773)) ([#56775](https://github.com/frappe/erpnext/issues/56775)) ([19d03fe](19d03fee46))
* clear stray permission message when item dashboard has no warehouse access ([a39753e](a39753ee08))
* **company:** ignore user permissions for link fields having link to `Account` and `Cost Center` (backport [#56748](https://github.com/frappe/erpnext/issues/56748)) ([#56795](https://github.com/frappe/erpnext/issues/56795)) ([003b655](003b6554c4))
* correct filter handling in Sales Person-wise Transaction Summary + tests (backport [#56783](https://github.com/frappe/erpnext/issues/56783)) ([#56908](https://github.com/frappe/erpnext/issues/56908)) ([24f0989](24f0989ac1))
* **crm_settings:** skip allowed users check when frappe crm is installed locally ([dfe4d5a](dfe4d5ab73))
* display outstanding amount using company default currency (backport [#56785](https://github.com/frappe/erpnext/issues/56785)) ([#57009](https://github.com/frappe/erpnext/issues/57009)) ([bdba4c8](bdba4c8091))
* don't attempt to create SABB for non-serialized / non-batch items (backport [#56627](https://github.com/frappe/erpnext/issues/56627)) ([#56803](https://github.com/frappe/erpnext/issues/56803)) ([8c56a5a](8c56a5ac0c))
* don't treat batch slot at FIFO queue head as qty slot ([20f6dd0](20f6dd0224))
* enforce read permission on ledger preview endpoints ([#57039](https://github.com/frappe/erpnext/issues/57039)) ([5642359](5642359c10))
* fetch payment entry reference amounts from invoice (backport [#56928](https://github.com/frappe/erpnext/issues/56928)) ([#57042](https://github.com/frappe/erpnext/issues/57042)) ([67c85ef](67c85ef0af))
* FIFO queue checks and incorrect entries filter in stock ledger reports ([3571512](3571512101)), closes [#43619](https://github.com/frappe/erpnext/issues/43619)
* **financial_statement:** render columnar financial statements instea… (backport [#56921](https://github.com/frappe/erpnext/issues/56921)) ([#57053](https://github.com/frappe/erpnext/issues/57053)) ([e6a6c13](e6a6c13355))
* for purchases do voucher based reposting (backport [#56601](https://github.com/frappe/erpnext/issues/56601)) ([#56608](https://github.com/frappe/erpnext/issues/56608)) ([e1e6176](e1e6176ddc))
* guard against missing DocType in onboarding steps patch ([#56804](https://github.com/frappe/erpnext/issues/56804)) ([f72289e](f72289e27c))
* **item-attribute:** clear attribute values when marking numeric ([374b340](374b340e73))
* **item:** error on uncommitted input and escape values in variant dialog ([04c834d](04c834d6a9))
* **item:** rework multiple variant dialog for large numeric ranges ([025d0cd](025d0cd7f3))
* make represents company field in purchase invoice ignore user permissions ([4aed77d](4aed77df0c))
* make trend report based-on and group-by column labels translatable ([b8199d8](b8199d88b6))
* map stock_qty in apply_price_list_on_item (backport [#56869](https://github.com/frappe/erpnext/issues/56869)) ([#57052](https://github.com/frappe/erpnext/issues/57052)) ([240fb2c](240fb2c4b8))
* partial delivery note against pick list (backport [#56985](https://github.com/frappe/erpnext/issues/56985)) ([#57006](https://github.com/frappe/erpnext/issues/57006)) ([20255a8](20255a8a7f))
* **patch:** moved create_company_custom_fields from `pre_model_sync` to `post_model_sync` (backport [#56962](https://github.com/frappe/erpnext/issues/56962)) ([#56965](https://github.com/frappe/erpnext/issues/56965)) ([14efd14](14efd14384))
* **payment reconciliation:** honour user permissions on accounting di… ([#56560](https://github.com/frappe/erpnext/issues/56560)) ([8eb92b8](8eb92b8b18))
* precision issue causing reconciliation error ([#54043](https://github.com/frappe/erpnext/issues/54043)) ([20df15b](20df15b3ac))
* prevent repeatable read related concurrency errors ([019b02a](019b02adcd))
* race condition and repeatable read in process pcv ([6d9f5fa](6d9f5fac76))
* recompute transferred qty before deciding work order status ([bc952f4](bc952f40d9))
* remove company default on cost center in stock entry detail ([a1fe6ce](a1fe6cee5d))
* remove incorrect Payable account_type from Customer Deposits in Philippines CoA (backport [#57018](https://github.com/frappe/erpnext/issues/57018)) ([#57061](https://github.com/frappe/erpnext/issues/57061)) ([edfbc71](edfbc71135))
* rename variant item_code/item_name when attribute abbreviation changes ([97cd1e7](97cd1e7144))
* render letter head footer in print formats ([fa4d32d](fa4d32dcdb))
* replay immutable SLE qty for serial/batch bundle valuation (backport [#56814](https://github.com/frappe/erpnext/issues/56814)) ([#56835](https://github.com/frappe/erpnext/issues/56835)) ([2264e25](2264e25cc6))
* resolve conflicts ([3aad7fe](3aad7fee24))
* restore Save button on reverse journal entry (backport [#56770](https://github.com/frappe/erpnext/issues/56770)) ([#56772](https://github.com/frappe/erpnext/issues/56772)) ([4573cd1](4573cd15a9))
* **selling:** update sales order per billed on credit note submission ([224cf19](224cf19f33))
* set conversion_rate on quotation created from customer ([26905bc](26905bc142))
* show only template items in Variant Of filter ([bebe011](bebe011636))
* skip item prices tab render for users without item price read access ([c964811](c964811293))
* skip stock reservation for opted-out production plans ([17598e2](17598e2626)), closes [#56798](https://github.com/frappe/erpnext/issues/56798)
* **stock:** backfill transferred qty for existing pick lists ([903d78c](903d78cc43))
* **stock:** link job card in stock entry created from pick list (backport [#57031](https://github.com/frappe/erpnext/issues/57031)) ([755b9cc](755b9ccbc3))
* **stock:** pick list serial batch posting date ([#57015](https://github.com/frappe/erpnext/issues/57015)) ([7e46be2](7e46be2a33)), closes [#56951](https://github.com/frappe/erpnext/issues/56951)
* **stock:** propagate project from job card to stock entry ([51d2077](51d2077eed))
* **stock:** support quality inspection for stock entry by purpose (backport [#56446](https://github.com/frappe/erpnext/issues/56446)) ([40ca3b5](40ca3b5e5d))
* update BOM operations when routing is changed ([b2e6a39](b2e6a39743))
* update events order by date asc (backport [#56963](https://github.com/frappe/erpnext/issues/56963)) ([#57056](https://github.com/frappe/erpnext/issues/57056)) ([e2fd061](e2fd061b3d))
* update modified timestamp in item.json ([f602ee0](f602ee0e73))
* use live source warehouse valuation for internal transfer purchase receipts (backport [#56431](https://github.com/frappe/erpnext/issues/56431)) ([#56677](https://github.com/frappe/erpnext/issues/56677)) ([1da28f2](1da28f2278))
* validate planned end date is not before planned start date in work order ([745baad](745baad0d1))
* validate reverse GL entries on current date under immutable ledger (backport [#56709](https://github.com/frappe/erpnext/issues/56709)) ([#56752](https://github.com/frappe/erpnext/issues/56752)) ([eea4756](eea4756ce9))
* validate template and its variant in the same Pricing Rule ([159a253](159a2538da))

### Features

* capture user persona during setup (backport [#56705](https://github.com/frappe/erpnext/issues/56705)) ([#56707](https://github.com/frappe/erpnext/issues/56707)) ([d9aa449](d9aa4491f1))
* **crm_settings:** auto-update crm sync settings on frappe crm install and uninstall ([cb2a930](cb2a930a32))
* explain FIFO allocation of fixed Discount Amount on Sales Order ([#56436](https://github.com/frappe/erpnext/issues/56436)) ([62fed1d](62fed1d562))
* **manufacturing:** create material request for raw materials from work order ([#56980](https://github.com/frappe/erpnext/issues/56980)) ([144f92d](144f92d58e))
* **sla:** filter service level agreement link by document type (backport [#56954](https://github.com/frappe/erpnext/issues/56954)) ([#56956](https://github.com/frappe/erpnext/issues/56956)) ([393b5d1](393b5d1f74))
* **stock:** support partial transfer from pick list ([af495ed](af495ed253))

### Performance Improvements

* avoid per-row Warehouse doc fetches in auto reorder job ([39b5e12](39b5e12305))
* batch bin lookups in delivery note stock update ([697e2c0](697e2c0b66))
2026-07-13 07:41:31 +00:00
ruthra kumar
6bd903e45a Merge pull request #57085 from frappe/mergify/bp/version-16-hotfix/pr-56817
feat: restructure Philippines chart of accounts with amortization sup… (backport #56817)
2026-07-13 13:08:42 +05:30
ruthra kumar
5281d538ce Merge pull request #56817 from Soham-ambibuzz/philipinnes_localization_coa_v3
feat: restructure Philippines chart of accounts with amortization sup…
(cherry picked from commit 33abc53d7a)
2026-07-13 07:28:54 +00:00
ruthra kumar
5261566b41 Merge pull request #57081 from frappe/mergify/bp/version-16-hotfix/pr-56902
fix(budget-variance): correct month shift in comparison chart (backport #56902)
2026-07-13 12:55:57 +05:30
ruthra kumar
5bbb71ef76 Merge pull request #57083 from frappe/mergify/bp/version-16-hotfix/pr-56628
fix(journal-entry): fetch outstanding on foreign currency (backport #56628)
2026-07-13 12:50:49 +05:30
Ejaaz Khan
d453781374 Merge pull request #57080 from frappe/mergify/bp/version-16-hotfix/pr-57071
fix: guard company logo lookup in default letterheads (backport #57071)
2026-07-13 12:40:06 +05:30
SowmyaArunachalam
ec782ee20d fix(journal-entry): fetch outstanding on foreign currency
(cherry picked from commit 07f641c48c)
2026-07-13 07:07:59 +00:00
Mohd Haris
703e9a728c 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>
(cherry picked from commit 48418eadb0)
2026-07-13 07:02:18 +00:00
Diptanil Saha
f94eee3197 Merge pull request #56931 from frappe/version-16-hotfix
chore: release v16
2026-07-13 12:09:52 +05:30
Ejaaz Khan
0d80fef3bf fix: set explicit table and logo widths in grey letterhead
(cherry picked from commit e39ca72997)
2026-07-13 06:29:01 +00:00
Ejaaz Khan
7b543142a2 fix: guard company logo lookup in default letterheads
(cherry picked from commit 23c09fe0f3)
2026-07-13 06:29:01 +00:00
Mihir Kandoi
90768c42d8 Merge pull request #57075 from frappe/mergify/bp/version-16-hotfix/pr-57073 2026-07-13 10:46:36 +05:30
ruthra kumar
fd3bebee02 Merge pull request #57023 from frappe/mergify/bp/version-16-hotfix/pr-56801
refactor: reversing exchange rate revaluation journals (backport #56801)
2026-07-13 10:13:57 +05:30
ruthra kumar
431ae6bc93 chore: resolve conflict 2026-07-13 10:02:18 +05:30
Khushi Rawat
9d6f15acd4 Merge branch 'version-16-hotfix' into mergify/bp/version-16-hotfix/pr-56964 2026-07-13 02:10:42 +05:30
S Sakthivel Murugan
8658039e9a fix(asset): allow asset repair creation for fully depreciated assets
(cherry picked from commit c7774a95e5)
2026-07-12 20:40:16 +00:00
Mihir Kandoi
4aed77df0c fix: make represents company field in purchase invoice ignore user permissions
(cherry picked from commit 6729a53fee)
2026-07-12 18:39:28 +00:00
MochaMind
9e95bfe472 chore: update POT file (#57066) 2026-07-12 20:25:28 +02:00
Mihir Kandoi
b6821c0ee5 Merge pull request #57044 from mihir-kandoi/fix-job-card-pick-list-transfer-v16
fix(stock): link job card in stock entry created from pick list (backport #57031)
2026-07-12 17:51:50 +05:30
Mihir Kandoi
51d2077eed fix(stock): propagate project from job card to stock entry 2026-07-12 17:31:22 +05:30
rohitwaghchaure
5642359c10 fix: enforce read permission on ledger preview endpoints (#57039)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-07-12 09:53:17 +00:00
mergify[bot]
4136ca55a7 fix(accounts): retain invoice table on opening invoice creation error (backport #56353) (#57058)
* fix(accounts): retain invoice table on opening invoice creation error (#56353)

Co-authored-by: diptanilsaha <diptanil@frappe.io>
(cherry picked from commit ad17efe243)

# Conflicts:
#	erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py

* chore: resolve conflicts

---------

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-12 07:45:18 +00:00
MochaMind
2e892be1c1 chore: sync translations to version-16-hotfix (#56911) 2026-07-12 13:12:57 +05:30
mergify[bot]
edfbc71135 fix: remove incorrect Payable account_type from Customer Deposits in Philippines CoA (backport #57018) (#57061)
Co-authored-by: Raghav Ruia <168326921+raghavisruia@users.noreply.github.com>
2026-07-11 17:52:48 +00:00
Mohammad Umair Sayed
62fed1d562 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>
2026-07-11 17:41:20 +00:00
mergify[bot]
e2fd061b3d fix: update events order by date asc (backport #56963) (#57056)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
Co-authored-by: nareshkannasln <nareshkannashanmugam@gmail.com>
2026-07-11 18:45:35 +05:30
mergify[bot]
835e32cec7 fix(banking): allow negative balance in bank statement import (backport #56959) (#57054)
fix(banking): allow negative balance in bank statement import (#56959)

(cherry picked from commit d449ad3b3f)

Co-authored-by: Nikhil Kothari <nik.kothari22@live.com>
2026-07-11 18:18:17 +05:30
Diptanil Saha
04d982fa0d Merge branch 'version-16' into version-16-hotfix 2026-07-11 18:17:59 +05:30
mergify[bot]
e6a6c13355 fix(financial_statement): render columnar financial statements instea… (backport #56921) (#57053)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
2026-07-11 10:41:49 +00:00
mergify[bot]
24f0989ac1 fix: correct filter handling in Sales Person-wise Transaction Summary + tests (backport #56783) (#56908)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
2026-07-11 15:28:23 +05:30
mergify[bot]
240fb2c4b8 fix: map stock_qty in apply_price_list_on_item (backport #56869) (#57052)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
2026-07-11 15:26:36 +05:30
Vishnu Priya Baskaran
8eb92b8b18 fix(payment reconciliation): honour user permissions on accounting di… (#56560) 2026-07-11 15:04:44 +05:30
Mihir Kandoi
755b9ccbc3 fix(stock): link job card in stock entry created from pick list (backport #57031)
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 17:46:57 +05:30
mergify[bot]
67c85ef0af fix: fetch payment entry reference amounts from invoice (backport #56928) (#57042)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
2026-07-10 11:43:01 +00:00
Sudharsanan Ashok
02549ef8c4 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

(cherry picked from commit 9cb6610b9e)

# Conflicts:
#	erpnext/stock/report/stock_ageing/stock_ageing.py
2026-07-10 15:40:37 +05:30
mergify[bot]
bdba4c8091 fix: display outstanding amount using company default currency (backport #56785) (#57009)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
Co-authored-by: S Sakthivel Murugan <s.sakthivelmurugan2003@gmail.com>
2026-07-10 15:36:34 +05:30
Mihir Kandoi
c56eb2635a Merge pull request #57024 from aerele/backport-56923-version-16-hotfix
perf: avoid per-row Warehouse doc fetches in auto reorder job
2026-07-10 12:43:56 +05:30
Mihir Kandoi
f96166af5b Merge pull request #57028 from aerele/backport-56913-version-16-hotfix
fix: make trend report based-on and group-by column labels translatable
2026-07-10 12:43:47 +05:30
pandiyan
b8199d88b6 fix: make trend report based-on and group-by column labels translatable
based_wise_columns_query() and group_wise_column() built column labels as
raw strings, so headers like Item, Item Name, Customer, Supplier, and
Territory never went through the _() translation function and stayed in
English regardless of the user's language, while period and total columns
translated fine. Build these as column dicts with an explicit _()-wrapped
label instead, so they're translated the same way as the rest of the report.

(cherry picked from commit 015fa68fc0)
2026-07-10 12:16:45 +05:30
mergify[bot]
20255a8a7f fix: partial delivery note against pick list (backport #56985) (#57006)
* fix: partial delivery note against pick list (#56985)

(cherry picked from commit 53af4d53ef)

# Conflicts:
#	erpnext/stock/doctype/pick_list/test_pick_list.py

* chore: fix conflicts

Refactor tests for pick list to improve clarity and organization.

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-07-10 06:38:02 +00:00
ruthra kumar
4711a28dd0 refactor(test): for reverse journals as well
(cherry picked from commit 65775e59a1)

# Conflicts:
#	erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
2026-07-10 06:20:54 +00:00
ruthra kumar
63e5117182 refactor: handle reverse ERR journals in AR / AP report
(cherry picked from commit 6838242063)
2026-07-10 06:20:53 +00:00
ruthra kumar
051757760f refactor: reversal capability on exchange rate revaluation
(cherry picked from commit a0b14c0607)

# Conflicts:
#	erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
2026-07-10 06:20:53 +00:00
ruthra kumar
277c651a9f refactor: add payment ledger to ignore link
(cherry picked from commit 6a4c5b6062)
2026-07-10 06:20:53 +00:00
pandiyan
39b5e12305 perf: avoid per-row Warehouse doc fetches in auto reorder job
get_item_warehouse_projected_qty() called frappe.get_doc("Warehouse", ...)
inside the per-bin loop to walk up the warehouse hierarchy, re-fetching the
same parent warehouses over and over on sites with nested warehouses. Preload
the warehouse-to-parent mapping with a single query and walk it in-memory
instead, cutting the DB round-trips from O(bins * hierarchy depth) to one
query.

(cherry picked from commit 6beb3d2509)
2026-07-10 11:48:04 +05:30
mergify[bot]
e1e6176ddc fix: for purchases do voucher based reposting (backport #56601) (#56608)
* fix: for purchases do voucher based reposting (#56601)

(cherry picked from commit 5523c15ab8)

* chore: fix type hints

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-07-10 06:13:01 +00:00
Pandiyan P
7e46be2a33 fix(stock): pick list serial batch posting date (#57015)
* 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-10 11:37:50 +05:30
mergify[bot]
2264e25cc6 fix: replay immutable SLE qty for serial/batch bundle valuation (backport #56814) (#56835)
fix: replay immutable SLE qty for serial/batch bundle valuation (#56814)

(cherry picked from commit ecc8ec672b)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-07-10 04:24:03 +00:00
Mihir Kandoi
7f64dfbccc Merge pull request #56988 from frappe/mergify/bp/version-16-hotfix/pr-56909
fix(stock): rename variant item_code/item_name when attribute abbreviation changes (backport #56909)
2026-07-09 21:23:33 +05:30
Mihir Kandoi
d4e16b0228 Merge pull request #56994 from frappe/mergify/bp/version-16-hotfix/pr-56925
perf: batch bin lookups in delivery note stock update (backport #56925)
2026-07-09 21:23:20 +05:30
Mihir Kandoi
3bf8dd7297 Merge pull request #56996 from frappe/mergify/bp/version-16-hotfix/pr-56932
fix: validate planned end date is not before planned start date in wo… (backport #56932)
2026-07-09 21:23:11 +05:30
Mihir Kandoi
50a40a7819 Merge pull request #56836 from aerele/fix/wo-status-partial-pick-v16
fix(manufacturing): update work order status on partial pick-list transfer
2026-07-09 21:22:49 +05:30
Sudharsanan11
dd264506db test(manufacturing): add test to validate the work order status on partial pick-list transfer
Cover the pick-list flow where a stock entry moves only one of the work
order's required items: material_transferred_for_manufacturing stays 0 (min
fraction) while the status must move to "in process".
2026-07-09 21:07:12 +05:30
Mihir Kandoi
2e535042b4 Merge pull request #57000 from frappe/mergify/bp/version-16-hotfix/pr-56255
Fix Variant Of filter to show only template items (backport #56255)
2026-07-09 20:57:40 +05:30
Mihir Kandoi
7dd7d7939f Merge pull request #56998 from frappe/mergify/bp/version-16-hotfix/pr-56979
fix: update BOM operations when routing is changed (backport #56979)
2026-07-09 20:52:54 +05:30
Mohsin Akhtar
f602ee0e73 fix: update modified timestamp in item.json
(cherry picked from commit 54da9fc27a)
2026-07-09 15:13:13 +00:00
Mohsin Akhtar
bebe011636 fix: show only template items in Variant Of filter
(cherry picked from commit 2433129850)
2026-07-09 15:13:13 +00:00
Mohd Haris
b2e6a39743 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>
(cherry picked from commit 758a837de4)
2026-07-09 15:12:09 +00:00
pandiyan
745baad0d1 fix: validate planned end date is not before planned start date in work order
(cherry picked from commit 2ec780cb35)
2026-07-09 15:08:02 +00:00
pandiyan
697e2c0b66 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.

(cherry picked from commit 5da878d25f)
2026-07-09 15:07:00 +00:00
pandiyan
719439f694 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.

(cherry picked from commit e718a70b26)
2026-07-09 15:04:04 +00:00
pandiyan
97cd1e7144 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.

(cherry picked from commit c0cfe5f363)
2026-07-09 15:04:03 +00:00
Mihir Kandoi
8e133fcf03 Merge pull request #56915 from aerele/backport/pick-list-partial-transfer-v16
feat(stock): support partial transfer from pick list
2026-07-09 20:30:56 +05:30
Pandiyan P
144f92d58e feat(manufacturing): create material request for raw materials from work order (#56980)
* feat(manufacturing): create material request for raw materials from work order

* test(manufacturing): cover work order material request flow
2026-07-09 19:43:01 +05:30
Sudharsanan11
903d78cc43 fix(stock): backfill transferred qty for existing pick lists
Pick Lists transferred before this feature have transferred_qty = 0 and
their Stock Entry rows carry no pick_list_item link, so the new
is_fully_transferred check would never fire and, with the old
duplicate-entry guard removed, they could be transferred again. Set
transferred_qty = picked_qty for non-Delivery submitted pick lists that
already have a linked Stock Entry so they stay completed and locked.
2026-07-09 19:20:50 +05:30
Sudharsanan11
6ecbe6fd4b test(stock): add test for partial transfer status from pick list 2026-07-09 19:20:19 +05:30
Sudharsanan11
af495ed253 feat(stock): support partial transfer from pick list
Creating a Stock Entry from a Pick List blocked any further entry
(stock_entry_exists) and flipped the pick list to Completed as soon as
one entry existed, so picked stock could not be transferred in parts.

Track transferred_qty per Pick List Item (summed from submitted Stock
Entry rows via a new pick_list_item link, mirroring delivered_qty), add
a Partially Transferred status, and map each new Stock Entry from the
remaining qty so transfers can continue until fully transferred.
2026-07-09 19:20:19 +05:30
Diptanil Saha
6707d8e0ad Merge pull request #56969 from frappe/mergify/bp/version-16-hotfix/pr-56674
fix(crm_settings): skip allowed users check when frappe crm is installed locally (backport #56674)
2026-07-09 15:52:21 +05:30
diptanilsaha
eb76872da9 chore: patch to clear out allowed users on crm_settings if frappe crm is installed on the site
(cherry picked from commit 0f987d7135)
2026-07-09 10:02:15 +00:00
diptanilsaha
eac3afcd88 fix(frappe_crm_api): handle failure for after_app_install and after_app_uninstall
(cherry picked from commit 2de423e225)
2026-07-09 10:02:14 +00:00
diptanilsaha
cb2a930a32 feat(crm_settings): auto-update crm sync settings on frappe crm install and uninstall
(cherry picked from commit c86aa2d6fe)
2026-07-09 10:02:14 +00:00
diptanilsaha
dfe4d5ab73 fix(crm_settings): skip allowed users check when frappe crm is installed locally
(cherry picked from commit 41badb3d74)
2026-07-09 10:02:14 +00:00
khushi8112
d3a8e91cda fix: apply precision fix inline for v16-hotfix, drop develop-only asset_service refactor 2026-07-09 15:25:01 +05:30
mergify[bot]
14efd14384 fix(patch): moved create_company_custom_fields from pre_model_sync to post_model_sync (backport #56962) (#56965)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-09 09:32:48 +00:00
khushi8112
a1fae959ed fix: match depreciation schedule rows at currency precision to avoid duplicate JEs
(cherry picked from commit 947ed5dfe1)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry/services/asset_service.py
2026-07-09 09:14:03 +00:00
Shllokkk
7301be3e4c Merge pull request #56940 from frappe/mergify/bp/version-16-hotfix/pr-56926
fix: validate template and its variant in the same Pricing Rule (backport #56926)
2026-07-09 01:04:17 +05:30
mergify[bot]
393b5d1f74 feat(sla): filter service level agreement link by document type (backport #56954) (#56956)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-07-08 17:29:33 +02:00
mergify[bot]
7ce1289c10 fix: added permission checks on various whitelisted functions (backport #56745) (#56946)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-08 18:10:27 +05:30
ruthra kumar
b32bfab66f Merge pull request #56938 from frappe/mergify/bp/version-16-hotfix/pr-54043
fix: precision issue causing reconciliation error (backport #54043)
2026-07-07 21:42:23 +05:30
Shllokkk
159a2538da fix: validate template and its variant in the same Pricing Rule
(cherry picked from commit a88048b378)
2026-07-07 14:30:22 +00:00
Dany Robert
20df15b3ac 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>
(cherry picked from commit be10c8ced9)
2026-07-07 14:29:21 +00:00
MochaMind
5c6631f6af chore: update POT file (#56900) 2026-07-05 20:30:21 +02:00
ruthra kumar
d27bc77644 Merge pull request #56876 from frappe/mergify/bp/version-16-hotfix/pr-56852
fix: race condition in process pcv (backport #56852)
2026-07-04 16:32:51 +05:30
ruthra kumar
5a9d40ce04 chore: linter fix
(cherry picked from commit a9ffdac806)
2026-07-04 10:53:20 +00:00
ruthra kumar
9cf7f441fb refactor(test): row name based utility methods
(cherry picked from commit dbc409736a)
2026-07-04 10:53:19 +00:00
ruthra kumar
d759574f9a refactor: prevent whole table scan while scheduling next date
- helps in concurrency isolation

(cherry picked from commit 21f4603144)
2026-07-04 10:53:19 +00:00
ruthra kumar
019b02adcd 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.

(cherry picked from commit 7e4045e828)
2026-07-04 10:53:19 +00:00
ruthra kumar
6d9f5fac76 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

(cherry picked from commit ff6881764b)
2026-07-04 10:53:19 +00:00
Mihir Kandoi
8277d62025 Merge pull request #56870 from frappe/mergify/bp/version-16-hotfix/pr-56859
Fix(stock): item form permission errors (backport #56859)
2026-07-04 14:18:51 +05:30
pandiyan
c964811293 fix: skip item prices tab render for users without item price read access
(cherry picked from commit ef794f390c)
2026-07-04 08:38:42 +00:00
pandiyan
a39753ee08 fix: clear stray permission message when item dashboard has no warehouse access
(cherry picked from commit 8c7b2f4d3c)
2026-07-04 08:38:42 +00:00
Pandiyan P
3c1f163e3b Merge pull request #56863 from frappe/mergify/bp/version-16-hotfix/pr-56837
fix: remove company default on cost center in stock entry detail  (backport #56837)
2026-07-04 12:57:19 +05:30
Pandiyan P
3aad7fee24 fix: resolve conflicts 2026-07-04 12:46:32 +05:30
rohitwaghchaure
17f2de42f3 fix: auto fetch serial no from previous operation output (backport to v16) (#56861) 2026-07-03 17:32:05 +00:00
mergify[bot]
1da28f2278 fix: use live source warehouse valuation for internal transfer purchase receipts (backport #56431) (#56677)
fix: use live source warehouse valuation for internal transfer purchase receipts (#56431)

fix: anchor incoming SLE rate to DN rate for intra-company PR transfers
(cherry picked from commit 35de9deb0a)

Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com>
2026-07-03 22:48:35 +05:30
pandiyan
149af49d01 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.

(cherry picked from commit a168bb7ea4)
2026-07-03 15:43:14 +00:00
pandiyan
a1fe6cee5d 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.

(cherry picked from commit edfa0a7a1d)

# Conflicts:
#	erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
2026-07-03 15:43:14 +00:00
Frappe PR Bot
d1d3b241ae chore(release): Bumped to Version 16.26.2
## [16.26.2](https://github.com/frappe/erpnext/compare/v16.26.1...v16.26.2) (2026-07-03)

### Bug Fixes

* **item-attribute:** clear attribute values when marking numeric ([1b1ea7f](1b1ea7f2aa))
* **item:** error on uncommitted input and escape values in variant dialog ([56d065b](56d065b919))
* **item:** rework multiple variant dialog for large numeric ranges ([6691217](66912173bd))
2026-07-03 10:29:44 +00:00
Mihir Kandoi
9cdaa738f1 Merge pull request #56848 from frappe/mergify/bp/version-16/pr-56742
fix(item): rework multiple variant dialog for large numeric ranges (backport #56741) (backport #56742)
2026-07-03 15:58:17 +05:30
Mihir Kandoi
56d065b919 fix(item): error on uncommitted input and escape values in variant dialog
Address review feedback:
- A typed-but-not-selected value passed validation yet was dropped by
  get_selected_attributes (reads committed pills only). Treat any pending
  input as an error so it is never silently omitted from creation.
- Escape pill / pending values before interpolating them into the HTML
  error message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit d4da9a3d7d)
(cherry picked from commit 04c834d6a9)
2026-07-03 10:25:48 +00:00
Mihir Kandoi
66912173bd fix(item): rework multiple variant dialog for large numeric ranges
The 'Create Multiple Variants' dialog rendered one checkbox per attribute
value and read the numeric config from the variant attribute child row. This
broke in several ways:

- A template whose attribute was made numeric after being added kept
  numeric_values=0 on the child row, so the dialog treated it as non-numeric,
  queried the empty Item Attribute Value table, and showed no values.
- Enumerating a large range (e.g. 1-100000) into checkboxes froze the browser.

Rework the dialog:

- Read numeric_values / from_range / to_range / increment from the Item
  Attribute master, and guard increment > 0.
- Replace the checkbox-per-value list with one MultiSelectPills per attribute,
  with a search placeholder.
- Stop enumerating numeric ranges: preview the first few values and validate
  typed input against the range on demand, so huge ranges stay instant.
- Block variant creation with a modal error if any selected value or pending
  input is invalid (out of range, off-increment, or not a number), so garbage
  like '00A' can't reach creation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 99152b8300)
(cherry picked from commit 025d0cd7f3)
2026-07-03 10:25:48 +00:00
Mihir Kandoi
1b1ea7f2aa fix(item-attribute): clear attribute values when marking numeric
Marking an attribute numeric hides the Item Attribute Values grid but leaves
its rows in the doc, whose mandatory Attribute Value / Abbreviation block the
save client-side before the server can clear them. Clear the table on the
client too so the save goes through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 4afbd4d3d9)
(cherry picked from commit 374b340e73)
2026-07-03 10:25:47 +00:00
Mihir Kandoi
d20b18a209 Merge pull request #56825 from aerele/backport-56806
fix: skip stock reservation for opted-out production plans
2026-07-03 13:56:00 +05:30
Mihir Kandoi
f6916c6f2d Merge pull request #56839 from frappe/mergify/bp/version-16-hotfix/pr-56826
fix: FIFO queue checks and incorrect entries filter in stock ledger reports (backport #56826)
2026-07-03 13:51:41 +05:30
Mihir Kandoi
feb58caf1e test: drop redundant cleanup, db rolls back after each test
(cherry picked from commit 3b1e57966e)
2026-07-03 07:50:22 +00:00
Mihir Kandoi
056195ce07 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

(cherry picked from commit ef5f47fafd)
2026-07-03 07:50:22 +00:00
Mihir Kandoi
3571512101 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

(cherry picked from commit 94ab09e4a3)

# Conflicts:
#	erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py
2026-07-03 07:50:22 +00:00
Khushi Rawat
5034e110cf Merge pull request #56813 from frappe/mergify/bp/version-16-hotfix/pr-56811
fix: render letter head footer in print formats (backport #56811)
2026-07-03 12:11:49 +05:30
Khushi Rawat
b09514465d Merge branch 'version-16-hotfix' into mergify/bp/version-16-hotfix/pr-56811 2026-07-03 11:38:18 +05:30
pandiyan
91a319c9e3 test: cover reserve stock gating on purchase receipt submit
(backport of #56798)

Co-Authored-By: pandiyan <pandiyanpalani37@gmail.com>
2026-07-03 11:06:39 +05:30
pandiyan
17598e2626 fix: skip stock reservation for opted-out production plans
(backport of #56798)

Co-Authored-By: pandiyan <pandiyanpalani37@gmail.com>
2026-07-03 11:06:29 +05:30
mergify[bot]
8c56a5ac0c fix: don't attempt to create SABB for non-serialized / non-batch items (backport #56627) (#56803)
* fix: don't attempt to create SABB for non-serialized / non-batch items (#56627)

* fix: don't attempt to create SABB for non-serialized / non-batch items

* fix(stock): skip serial batch lookup for rows without item code

(cherry picked from commit 5b738b7b0d)

# Conflicts:
#	erpnext/stock/services/serial_batch_bundle_service.py

* chore: resolve conflicts

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-07-03 10:35:43 +05:30
khushi8112
0371e8eaf0 fix: add page numbers to print format footer
(cherry picked from commit 2d0c0a8c09)
2026-07-02 21:13:53 +00:00
khushi8112
fa4d32dcdb fix: render letter head footer in print formats
(cherry picked from commit e60a467972)
2026-07-02 21:13:53 +00:00
Shllokkk
8907e8a492 Merge pull request #56805 from frappe/mergify/bp/version-16-hotfix/pr-56804
fix: guard against missing DocType in onboarding steps patch (backport #56804)
2026-07-03 02:12:52 +05:30
Shllokkk
f72289e27c fix: guard against missing DocType in onboarding steps patch (#56804)
(cherry picked from commit caa4358057)
2026-07-02 14:04:47 +00:00
mergify[bot]
003b6554c4 fix(company): ignore user permissions for link fields having link to Account and Cost Center (backport #56748) (#56795)
* fix(company): ignore user permissions for link fields having link to `Account` and `Cost Center` (#56748)

(cherry picked from commit 9cea43b006)

# Conflicts:
#	erpnext/setup/doctype/company/company.json

* chore: resolves conflict

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-02 12:55:16 +00:00
Mihir Kandoi
0e25e62750 Merge pull request #56712 from aerele/backport-56552-version-16-hotfix
fix: set conversion_rate on quotation created from customer
2026-07-02 15:20:39 +05:30
mergify[bot]
19d03fee46 fix: block serialized to non-serialized item change when SABB exists (backport #56773) (#56775)
* fix: block serialized to non-serialized item change when SABB exists (#56773)

(cherry picked from commit 0e8ae7548d)

# Conflicts:
#	erpnext/stock/doctype/item/item.py
#	erpnext/stock/doctype/item/test_item.py

* chore: fix conflicts

Remove validation for standard cost change and adjust serialized item change validation.

* chore: fix conflicts

Removed test for opening stock with serial and batch numbers.

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-07-02 09:31:16 +00:00
mergify[bot]
4573cd15a9 fix: restore Save button on reverse journal entry (backport #56770) (#56772)
fix: restore Save button on reverse journal entry (#56770)

Reversing a submitted Journal Entry opened a draft with reversal_of set,
which called frm.set_read_only(). That strips the write and submit perms
from frm.perm, so the toolbar never rendered the Save (or later Submit)
button and the reversal could not be saved.

Lock the fields and the accounts grid as read_only instead, leaving perms
intact so Save and Submit still work while nothing stays editable.

Ticket: 72857
(cherry picked from commit 0a05dd4426)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-07-02 13:50:57 +05:30
Nabin Hait
0f9405646b Merge pull request #55970 from frappe/mergify/bp/version-16-hotfix/pr-55947
fix(accounts): clear clearance date when amending reconciled voucher (backport #55947)
2026-07-02 13:22:20 +05:30
Mihir Kandoi
84af478eec Merge pull request #56753 from aerele/backport-56670
fix: recompute transferred qty before deciding work order status
2026-07-02 13:16:16 +05:30
Mihir Kandoi
5b133dcf18 Merge pull request #56764 from frappe/mergify/bp/version-16-hotfix/pr-56757
fix: don't treat batch slot at FIFO queue head as qty slot (backport #56757)
2026-07-02 13:09:15 +05:30
Mihir Kandoi
16bc78834e test: assert full negative batch slot in ageing regression test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 8928b42d5d)
2026-07-02 07:14:31 +00:00
Mihir Kandoi
20f6dd0224 fix: don't treat batch slot at FIFO queue head as qty slot
An incoming SLE without resolvable serial/batch details hit the
negative-head branch in _compute_incoming_stock even when the head was
a batch slot, because flt() on the batch number string returns 0.0.
_add_to_negative_fifo_head then crashed with
"TypeError: can only concatenate str (not 'float') to str".

Guard the branch with is_qty_slot, mirroring the existing check in
_add_transfer_slot_to_fifo_queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c47a95a4d2)
2026-07-02 07:14:31 +00:00
ruthra kumar
d88ca43e52 Merge pull request #56756 from frappe/mergify/bp/version-16-hotfix/pr-56754
refactor: update title for process statement of accounts (backport #56754)
2026-07-02 12:16:01 +05:30
ruthra kumar
ef162f9a9e refactor: update title for process statement of accounts
(cherry picked from commit 04468c3c33)
2026-07-02 06:42:55 +00:00
pandiyan
bc952f40d9 fix: recompute transferred qty before deciding work order status
work order status was decided using a stale transferred-qty value,
computed before the current stock entry's transfer got recomputed.
this left work orders stuck at "not started" for pick-list-driven
transfers, since those entries never set fg_completed_qty and their
transferred qty can only be known from actual item-level transfers.

an earlier attempt fixed this by setting fg_completed_qty from the pick
list's for_qty, but that broke two things tied to fg_completed_qty
being zero: the excess-transfer guard, and the partial-transfer
fraction logic used to avoid marking a work order as fully supplied too
early.

recompute the transferred qty first, then decide status from the fresh
value. revert the fg_completed_qty change since it's no longer needed.
2026-07-02 11:22:57 +05:30
mergify[bot]
eea4756ce9 fix: validate reverse GL entries on current date under immutable ledger (backport #56709) (#56752)
fix: validate reverse GL entries on current date under immutable ledger (#56709)

* fix: validate reverse GL entries on current date under immutable ledger

When Immutable Ledger is enabled, the reverse GL entry is posted on the
current date, but the closed-period checks in make_reverse_gl_entries still
validate against the original (backdated) posting date. This blocks cancelling
a backdated voucher, such as a suspense Journal Entry for a migrated NPA loan,
with a books-closed error even though the reverse entry lands in an open period.

Validate both check_freezing_date and validate_against_pcv against the current
date when Immutable Ledger is enabled. When it is disabled, behaviour is
unchanged.

Follow-up to #55268.

* test: reset frozen till date after reverse entry test

The freeze date set on the company was not reset, so it leaked into the next
test which posts entries in that period. Reset it in a finally block.

* fix: prefer explicit posting_date under immutable ledger

Prefer the posting_date argument before frappe.form_dict and getdate, at both
the validation and the GL entry site, so an explicit date passed by the caller
is honoured and validation still matches the posted date.

(cherry picked from commit cab1b129c0)

Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com>
2026-07-02 04:57:11 +00:00
Mihir Kandoi
8dafd2b684 Merge pull request #56742 from frappe/mergify/bp/version-16-hotfix/pr-56741
fix(item): rework multiple variant dialog for large numeric ranges (backport #56741)
2026-07-02 00:31:03 +05:30
Mihir Kandoi
04c834d6a9 fix(item): error on uncommitted input and escape values in variant dialog
Address review feedback:
- A typed-but-not-selected value passed validation yet was dropped by
  get_selected_attributes (reads committed pills only). Treat any pending
  input as an error so it is never silently omitted from creation.
- Escape pill / pending values before interpolating them into the HTML
  error message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit d4da9a3d7d)
2026-07-01 18:14:18 +00:00
Mihir Kandoi
025d0cd7f3 fix(item): rework multiple variant dialog for large numeric ranges
The 'Create Multiple Variants' dialog rendered one checkbox per attribute
value and read the numeric config from the variant attribute child row. This
broke in several ways:

- A template whose attribute was made numeric after being added kept
  numeric_values=0 on the child row, so the dialog treated it as non-numeric,
  queried the empty Item Attribute Value table, and showed no values.
- Enumerating a large range (e.g. 1-100000) into checkboxes froze the browser.

Rework the dialog:

- Read numeric_values / from_range / to_range / increment from the Item
  Attribute master, and guard increment > 0.
- Replace the checkbox-per-value list with one MultiSelectPills per attribute,
  with a search placeholder.
- Stop enumerating numeric ranges: preview the first few values and validate
  typed input against the range on demand, so huge ranges stay instant.
- Block variant creation with a modal error if any selected value or pending
  input is invalid (out of range, off-increment, or not a number), so garbage
  like '00A' can't reach creation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 99152b8300)
2026-07-01 18:14:18 +00:00
Mihir Kandoi
374b340e73 fix(item-attribute): clear attribute values when marking numeric
Marking an attribute numeric hides the Item Attribute Values grid but leaves
its rows in the doc, whose mandatory Attribute Value / Abbreviation block the
save client-side before the server can clear them. Clear the table on the
client too so the save goes through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 4afbd4d3d9)
2026-07-01 18:14:18 +00:00
ervishnucs
26905bc142 fix: set conversion_rate on quotation created from customer 2026-07-01 22:27:14 +05:30
mergify[bot]
d9aa4491f1 feat: capture user persona during setup (backport #56705) (#56707) 2026-07-01 18:15:55 +05:30
mergify[bot]
19c318df68 fix(banking): handle blank password protected PDFs and negative amounts in CR/DR columns (backport #56690) (#56694)
fix(banking): handle blank password protected PDFs and negative amounts in CR/DR columns (#56690)

* fix(banking): strip signs from amount if column has CR/DR values

* fix(banking): try decrypting PDF with a blank password

(cherry picked from commit 300471da12)

Co-authored-by: Nikhil Kothari <nik.kothari22@live.com>
2026-07-01 08:24:28 +00:00
mergify[bot]
2cb577b912 fix(banking): use custom renderer for translated strings and parser for rules (backport #56643) (#56647)
fix(banking): use custom renderer for translated strings and parser for rules (#56643)

fix(banking): use custom renderer for translated strings and parser for formula evaluation

(cherry picked from commit 8447f551e7)

Co-authored-by: Nikhil Kothari <nik.kothari22@live.com>
2026-07-01 13:31:12 +05:30
mergify[bot]
53c17bf331 chore: update dependencies in banking app (backport #56685) (#56689)
chore: update dependencies in banking app (#56685)

chore: update deps in banking app
(cherry picked from commit 26583ae357)

Co-authored-by: Nikhil Kothari <nik.kothari22@live.com>
2026-07-01 13:30:32 +05:30
Frappe PR Bot
fd00cebbd2 chore(release): Bumped to Version 16.26.1
## [16.26.1](https://github.com/frappe/erpnext/compare/v16.26.0...v16.26.1) (2026-07-01)

### Bug Fixes

* **stock:** support quality inspection for stock entry by purpose (backport [#56446](https://github.com/frappe/erpnext/issues/56446)) ([613b3c1](613b3c16c2))
2026-07-01 07:42:04 +00:00
Mihir Kandoi
fe6c276e72 Merge pull request #56687 from frappe/mergify/bp/version-16/pr-56672
fix(stock): support quality inspection for stock entry by purpose (ba… (backport #56672)
2026-07-01 13:10:18 +05:30
Sudharsanan11
613b3c16c2 fix(stock): support quality inspection for stock entry by purpose (backport #56446)
(cherry picked from commit 40ca3b5e5d)
2026-07-01 07:28:45 +00:00
Mihir Kandoi
e083d195cf Merge pull request #56672 from aerele/backport/qi-stock-entry-by-purpose-v16
fix(stock): support quality inspection for stock entry by purpose (ba…
2026-07-01 12:45:07 +05:30
Mihir Kandoi
7f1ef59dc5 Merge pull request #56671 from aerele/backport/dn-credit-note-per-billed-v16
fix(selling): update sales order per billed on credit note submission
2026-07-01 09:29:38 +05:30
Sudharsanan11
710e021638 test(selling): add test to validate the per billed after credit note submission 2026-07-01 09:06:01 +05:30
Sudharsanan11
224cf19f33 fix(selling): update sales order per billed on credit note submission 2026-07-01 09:06:01 +05:30
Frappe PR Bot
666becc670 chore(release): Bumped to Version 16.26.0
# [16.26.0](https://github.com/frappe/erpnext/compare/v16.25.0...v16.26.0) (2026-07-01)

### Bug Fixes

* add permission checks in whitelisted functions (backport [#53103](https://github.com/frappe/erpnext/issues/53103)) ([#56669](https://github.com/frappe/erpnext/issues/56669)) ([52ea964](52ea9641ce))
* adjust outstanding amount calculation in purchase and sales registers ([1a2a9b6](1a2a9b6cfc))
* allow rename for Quality Inspection Parameter ([410a772](410a772510))
* **asset:** conditionally show Is Fully Depreciated field ([0e730bf](0e730bf1c7))
* carry item-level project to Purchase Receipt GL entries (backport [#56568](https://github.com/frappe/erpnext/issues/56568)) ([#56619](https://github.com/frappe/erpnext/issues/56619)) ([0829cb4](0829cb45a8))
* **controllers:** fix supplier-RFQ portal list query (wrong column + Postgres DISTINCT) ([3abadc7](3abadc7a5f))
* **crm:** using `get_list` instead of `get_all` in `get_opportunities` (backport [#56463](https://github.com/frappe/erpnext/issues/56463)) ([#56466](https://github.com/frappe/erpnext/issues/56466)) ([30ba950](30ba950abd))
* do not allow closing the accounting period for future dates (backport [#56551](https://github.com/frappe/erpnext/issues/56551)) ([#56577](https://github.com/frappe/erpnext/issues/56577)) ([df1b443](df1b4431b6))
* exclude virtual child doctypes from deletion in transaction deletion record ([8a66570](8a665709d2))
* gross profit calculation with rate adjustment entries ([9c5b063](9c5b063884))
* handle missing serial and batch bundle in print format ([c84d4a2](c84d4a282d))
* ignored posting time 00:00:00 in RIV (backport [#56571](https://github.com/frappe/erpnext/issues/56571)) ([#56573](https://github.com/frappe/erpnext/issues/56573)) ([b4d83e5](b4d83e542a))
* job card timer issue (backport [#56405](https://github.com/frappe/erpnext/issues/56405)) ([#56406](https://github.com/frappe/erpnext/issues/56406)) ([54c45d7](54c45d7b22))
* **lead:** added missing read permission check on `get_lead_details` (backport [#56272](https://github.com/frappe/erpnext/issues/56272)) ([#56274](https://github.com/frappe/erpnext/issues/56274)) ([bd54c7f](bd54c7fea8))
* **letter-head:** guard company lookups when doc has no company field ([89059a9](89059a990f))
* link portal address rows to web form ([3a480c0](3a480c08b1))
* manual backport of [#55896](https://github.com/frappe/erpnext/issues/55896) ([490e125](490e125267))
* party aliases should be no copy ([0b42241](0b42241682))
* precision issue causing COGS in inter transfer PR (backport [#56420](https://github.com/frappe/erpnext/issues/56420)) ([#56425](https://github.com/frappe/erpnext/issues/56425)) ([e3958ad](e3958ad7bb))
* remove dead bundle helper call from purchase receipt print format ([2eaa635](2eaa635ab6))
* remove frappe.utils from jinja context in process statement of accounts ([37ec2d0](37ec2d0edd))
* rewrite item rate calculation (backport [#56315](https://github.com/frappe/erpnext/issues/56315)) ([ef3d444](ef3d444a60))
* set mr status to received when per_received is 100 even if per_ordered < 100 ([4181246](41812462b4))
* show contextual balance label on party dashboard for net balances ([a886d0b](a886d0b445))
* skip qty over-allowance check for non-stock items only ([bc313dc](bc313dc09d))
* **stock:** value batch/serial return from ledger when original receipt has no bundle (backport [#56631](https://github.com/frappe/erpnext/issues/56631)) ([#56646](https://github.com/frappe/erpnext/issues/56646)) ([2c18c16](2c18c16be6))
* sync Stock Reconciliation difference amount with GL after reposting (backport [#56574](https://github.com/frappe/erpnext/issues/56574)) ([#56585](https://github.com/frappe/erpnext/issues/56585)) ([e834098](e834098c28))
* update qty in future SLEs when cancelling documents ([#56638](https://github.com/frappe/erpnext/issues/56638)) ([01374db](01374db8da))
* update_qty_in_future_sle skips SLEs with same posting datetime ([#56612](https://github.com/frappe/erpnext/issues/56612)) ([5aa62d1](5aa62d1cda))
* Use correct doctype name for PCV perm-check (backport [#56606](https://github.com/frappe/erpnext/issues/56606)) ([#56611](https://github.com/frappe/erpnext/issues/56611)) ([762ce5c](762ce5c684))
* use correct variable to fetch valuation method ([d2c8df9](d2c8df9451))

### Features

* **accounts:** add configurable job timeout for Process Period Closing Voucher ([d389014](d389014e57))
2026-07-01 03:34:53 +00:00
Diptanil Saha
628b932d55 Merge pull request #56652 from frappe/version-16-hotfix
chore: release v16
2026-07-01 09:03:17 +05:30
Diptanil Saha
b5c09fa993 Merge pull request #56680 from frappe/mergify/bp/version-16-hotfix/pr-56678
fix(gross_profit): correct GP calculation for rate adjustment debit notes (backport #56678)
2026-07-01 08:42:13 +05:30
diptanilsaha
66f99668a9 test(gross_profit): added test cases for rate adjustment entry
(cherry picked from commit 17ef5d6034)
2026-07-01 03:02:44 +00:00
diptanilsaha
9c5b063884 fix: gross profit calculation with rate adjustment entries
(cherry picked from commit b9f330a158)
2026-07-01 03:02:44 +00:00
Sudharsanan11
40ca3b5e5d fix(stock): support quality inspection for stock entry by purpose (backport #56446) 2026-07-01 00:16:21 +05:30
mergify[bot]
52ea9641ce fix: add permission checks in whitelisted functions (backport #53103) (#56669)
* fix: add permission checks in whitelisted functions (#53103)

(cherry picked from commit a6e78c2eea)

# Conflicts:
#	erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py

* chore: resolve conflict

---------

Co-authored-by: Priyal Rawal <135015851+Priyal208@users.noreply.github.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-06-30 16:36:13 +00:00
Diptanil Saha
7396fca4be Merge branch 'version-16' into version-16-hotfix 2026-06-30 22:02:57 +05:30
Mihir Kandoi
38994ec915 Merge pull request #56664 from frappe/mergify/bp/version-16-hotfix/pr-56662
fix: use correct variable to fetch valuation method (backport #56662)
2026-06-30 20:45:25 +05:30
Diptanil Saha
a1412a61c9 refactor: port bulk_transaction_log existence check to the query builder (#56659)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:44:11 +05:30
ruthra kumar
62703de452 Merge pull request #56658 from frappe/mergify/bp/version-16-hotfix/pr-56655
ci: warmup test data along with DB (backport #56655)
2026-06-30 20:37:34 +05:30
Mihir Kandoi
d2c8df9451 fix: use correct variable to fetch valuation method
(cherry picked from commit 1492c9fbc3)
2026-06-30 15:04:46 +00:00
ruthra kumar
05aa68b53b ci: warmup test data along with DB
(cherry picked from commit dcdbf9df17)
2026-06-30 12:31:03 +00:00
Khushi Rawat
169a4c82a4 Merge pull request #56649 from frappe/mergify/bp/version-16-hotfix/pr-56569
fix(asset): conditionally show Is Fully Depreciated field (backport #56569)
2026-06-30 15:55:20 +05:30
Mohd Haris
0e730bf1c7 fix(asset): conditionally show Is Fully Depreciated field
The "Is Fully Depreciated" field was hidden on the Asset form (hidden: 1),
so it could never be set for manually entered existing assets.

Make it visible based on context:
- Existing Asset with Calculate Depreciation off -> visible and editable
- Calculate Depreciation on -> visible but read-only and forced unchecked
  (it is only meaningful for manually entered assets)

The unchecked value is enforced in the form script (immediate feedback on
toggle and on load) and in server-side validate() so it can never be saved
as checked while depreciation is being calculated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit a7c1ebacbe)
2026-06-30 10:05:12 +00:00
mergify[bot]
2c18c16be6 fix(stock): value batch/serial return from ledger when original receipt has no bundle (backport #56631) (#56646)
fix(stock): value batch/serial return from ledger when original receipt has no bundle (#56631)

* fix(stock): value batch/serial return from ledger when original receipt has no bundle

* test(stock): add test to validate the valuation of serial/batch for return when original receipt has no bundle

(cherry picked from commit 6184c057db)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-06-30 15:34:51 +05:30
rohitwaghchaure
01374db8da fix: update qty in future SLEs when cancelling documents (#56638) 2026-06-30 14:27:46 +05:30
Mihir Kandoi
5d8f653476 Merge pull request #56604 from frappe/mergify/bp/version-16-hotfix/pr-56231
fix(controllers): fix supplier-RFQ portal list query (wrong column + Postgres DISTINCT) (backport #56231)
2026-06-30 12:03:21 +05:30
Mihir Kandoi
55716a509f Merge pull request #56636 from frappe/mergify/bp/version-16-hotfix/pr-56621
fix: set mr status to received when per_received is 100 even if per_o… (backport #56621)
2026-06-30 12:01:53 +05:30
MochaMind
c782658056 chore: sync translations to version-16-hotfix (#56617) 2026-06-30 11:24:54 +05:30
pandiyan
41812462b4 fix: set mr status to received when per_received is 100 even if per_ordered < 100
(cherry picked from commit a3c5ef6aa3)
2026-06-30 05:41:09 +00:00
mergify[bot]
0829cb45a8 fix: carry item-level project to Purchase Receipt GL entries (backport #56568) (#56619)
fix: carry item-level project to Purchase Receipt GL entries (#56568)

Purchase Receipt stock and asset GL lines used the item row's cost center
but always fell back to the document-level project, unlike Purchase Invoice
which uses the item-level project. add_gl_entry accepted a project argument
but never wrote it to the GL dict, so the inward, Stock Received But Not
Billed, landed cost, divisional loss, sub-contracting and exchange rate
lines dropped the row's project.

Write project into the GL dict and pass project=item.project on the entries
that were missing it, so project behaves like cost center and matches
Purchase Invoice.

Ticket: 72523
(cherry picked from commit 6f97c7199c)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-06-29 23:30:48 +05:30
mergify[bot]
762ce5c684 fix: Use correct doctype name for PCV perm-check (backport #56606) (#56611)
fix: Use correct doctype name for PCV perm-check (#56606)

closes https://github.com/frappe/erpnext/issues/56593

(cherry picked from commit 747374e767)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2026-06-29 19:04:47 +05:30
rohitwaghchaure
5aa62d1cda fix: update_qty_in_future_sle skips SLEs with same posting datetime (#56612) 2026-06-29 15:38:35 +05:30
Mihir Kandoi
3abadc7a5f fix(controllers): fix supplier-RFQ portal list query (wrong column + Postgres DISTINCT)
rfq_transaction_list had two defects introduced when it was converted to the query
builder:

1. `party.supplier == party[0]` compared supplier to a column literally named "0"
   (a stray index on the DocType, not the intended `parties[0]` value). This renders
   as `supplier = \`0\`` / `supplier = "0"` and errors on BOTH engines
   (MariaDB: Unknown column '0'; Postgres: column "0" does not exist), so the
   supplier portal RFQ list was completely broken.
2. SELECT DISTINCT ordered by `creation`, which is not in the select list. Postgres
   rejects this ("for SELECT DISTINCT, ORDER BY expressions must appear in select list").

Compare against `parties[0]` and add `creation` to the select list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit a7d9078bf4)

# Conflicts:
#	erpnext/controllers/tests/test_website_list_for_contact.py
2026-06-29 08:24:18 +00:00
ruthra kumar
1a348d8e6e Merge pull request #56598 from frappe/mergify/bp/version-16-hotfix/pr-54597
fix: adjust outstanding amount calculation in purchase and sales registers (backport #54597)
2026-06-29 11:30:40 +05:30
ruthra kumar
1eed77e605 Merge pull request #56038 from frappe/mergify/bp/version-16-hotfix/pr-56034
refactor(test): remove custom utility for company creation (backport #56034)
2026-06-29 11:19:49 +05:30
ruthra kumar
490e125267 fix: manual backport of #55896 2026-06-29 11:09:23 +05:30
Sowmiya P K
1a2a9b6cfc fix: adjust outstanding amount calculation in purchase and sales registers
(cherry picked from commit 6115af720b)

# Conflicts:
#	erpnext/accounts/report/purchase_register/purchase_register.py
2026-06-29 10:52:49 +05:30
ruthra kumar
93b28f3337 refactor(test): update assertion for new test records
(cherry picked from commit afeaba5142)
2026-06-29 09:41:48 +05:30
ruthra kumar
1a9fcfb0cc refactor(test): broken test case in accounts controller
(cherry picked from commit 59a69fc497)
2026-06-29 09:41:48 +05:30
ruthra kumar
a8aa73baae refactor(test): remove even more dead code
(cherry picked from commit 3d109571ee)
2026-06-29 09:41:48 +05:30
ruthra kumar
65202d2be1 refactor(tests): reuse persistent master data instead of creating company per test
Replace per-test company creation in setUp() with persistent master data
from BootStrapTestData. Add Test PCV Company to test_records.json so it
becomes a persistent fixture rather than a throwaway created per test run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 6e62750c2f)

# Conflicts:
#	erpnext/accounts/report/financial_ratios/test_financial_ratios.py
2026-06-29 09:41:47 +05:30
MochaMind
1cc2890f3e chore: update POT file (#56591) 2026-06-28 21:26:46 +02:00
mergify[bot]
e834098c28 fix: sync Stock Reconciliation difference amount with GL after reposting (backport #56574) (#56585)
* fix: sync Stock Reconciliation difference amount with GL after reposting (#56574)

* fix: sync Stock Reconciliation difference amount with GL after reposting

* fix: placement of recalculate differece amount function

(cherry picked from commit c7ef42ef98)

# Conflicts:
#	erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
#	erpnext/stock/stock_ledger.py

* chore: fix conflicts

Removed unused import and fixed import order.

* chore: fix conflicts

Refactor update_rate_on_stock_reconciliation to use recalculation method for difference amount.

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-06-27 17:08:54 +05:30
Mihir Kandoi
655d6dac87 Merge pull request #56587 from frappe/mergify/bp/version-16-hotfix/pr-56586
fix: party aliases should be no copy (backport #56586)
2026-06-27 16:44:33 +05:30
Shllokkk
36a9c7a0d3 Merge pull request #56386 from frappe/mergify/bp/version-16-hotfix/pr-56337
fix: show contextual balance label on party dashboard for net balances (backport #56337)
2026-06-27 16:36:07 +05:30
Mihir Kandoi
b35c5c1568 chore: resolve conflicts 2026-06-27 16:34:53 +05:30
Mihir Kandoi
e64e9866fb chore: fix conflicts 2026-06-27 16:34:20 +05:30
mergify[bot]
b292be34cb chore(crm_settings): remove unused delete_custom_fields import (backport #56558) (#56583)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-06-27 16:29:48 +05:30
Mihir Kandoi
0b42241682 fix: party aliases should be no copy
(cherry picked from commit 057af21cd8)

# Conflicts:
#	erpnext/buying/doctype/supplier/supplier.json
#	erpnext/selling/doctype/customer/customer.json
2026-06-27 10:55:31 +00:00
mergify[bot]
df1b4431b6 fix: do not allow closing the accounting period for future dates (backport #56551) (#56577)
fix: do not allow closing the accounting period for future dates (#56551)

(cherry picked from commit 5e60e4faa7)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-06-27 12:04:11 +05:30
mergify[bot]
112b773193 chore: removing controllers from pre-commit eslint hooks exclude list (backport #56575) (#56579)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-06-27 01:37:18 +05:30
mergify[bot]
b4d83e542a fix: ignored posting time 00:00:00 in RIV (backport #56571) (#56573)
fix: ignored posting time 00:00:00 in RIV (#56571)

(cherry picked from commit 31f89b72b4)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-06-26 19:42:45 +05:30
mergify[bot]
9bc240c4f0 ci: bump pre-commit actions to v3.0.1 (backport #56562) (#56567)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-06-26 17:06:08 +05:30
Mihir Kandoi
1b7486f61a Merge pull request #56565 from frappe/mergify/bp/version-16-hotfix/pr-56559
fix: remove dead bundle helper call from purchase receipt print format (backport #56559)
2026-06-26 16:54:11 +05:30
Shllokkk
2eaa635ab6 fix: remove dead bundle helper call from purchase receipt print format
(cherry picked from commit b9f5a77fa7)
2026-06-26 11:07:51 +00:00
Mihir Kandoi
733a837c18 Merge pull request #56550 from frappe/mergify/bp/version-16-hotfix/pr-56127
fix: allow rename for Quality Inspection Parameter (backport #56127)
2026-06-26 15:56:43 +05:30
Shllokkk
a886d0b445 fix: show contextual balance label on party dashboard for net balances
(cherry picked from commit 3251b40365)
2026-06-26 15:03:06 +05:30
Mohd Haris
410a772510 fix: allow rename for Quality Inspection Parameter
The Quality Inspection Parameter DocType did not have `allow_rename`
enabled, so the "Rename" action was hidden from the form's menu
(the 3-dots / ⋮ options). Since the DocType is auto-named from the
`parameter` field (`autoname: field:parameter`), users had no way to
correct or change a parameter's name once created.

Enable `allow_rename` so users can rename a Quality Inspection
Parameter from the form menu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit adfef48a65)
2026-06-26 08:15:17 +00:00
Mihir Kandoi
2404074b9d Merge pull request #56512 from frappe/revert-56500-mergify/bp/version-16-hotfix/pr-56497
Revert "fix: handle missing serial and batch bundle in print format (backport #56497)"
2026-06-26 12:56:19 +05:30
Mihir Kandoi
5f02ff5f8c Revert "fix: handle missing serial and batch bundle in print format (backport #56497)" 2026-06-26 10:08:25 +05:30
rohitwaghchaure
6dad1b1ae6 refactor: move Inventory Dimension mandatory check from field-level to server-side (#56451)
* refactor: move Inventory Dimension mandatory check from field-level to server-side

* refactor: split large function

* fix: greptile issue
2026-06-25 20:10:09 +05:30
Shllokkk
a00874fa95 Merge pull request #56500 from frappe/mergify/bp/version-16-hotfix/pr-56497
fix: handle missing serial and batch bundle in print format (backport #56497)
2026-06-25 19:33:50 +05:30
Shllokkk
c84d4a282d fix: handle missing serial and batch bundle in print format
(cherry picked from commit 548d90df4f)
2026-06-25 13:53:11 +00:00
mergify[bot]
30ba950abd fix(crm): using get_list instead of get_all in get_opportunities (backport #56463) (#56466)
* fix(crm): using `get_list` instead of `get_all` in `get_opportunities` (#56463)

(cherry picked from commit 9b4c8a8d7f)

# Conflicts:
#	erpnext/crm/doctype/prospect/prospect.py

* chore: resolve conflict

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-06-25 10:23:04 +00:00
mergify[bot]
ef3d444a60 fix: rewrite item rate calculation (backport #56315)
Co-authored-by: Harsh Patadia <harsh@Harshs-MacBook-Air.local>
Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com>
Co-authored-by: Smit Vora <mailsmitvora@gmail.com>
2026-06-25 09:50:50 +00:00
Shllokkk
4e94b75b5d Merge pull request #56460 from Shllokkk/psoa-restrict-jinja-context-v16
fix: remove frappe.utils from jinja context in process statement of accounts
2026-06-25 15:17:29 +05:30
mergify[bot]
831d25bed7 ci: bump po review action (backport #56454) (#56461)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-06-25 09:42:32 +00:00
Shllokkk
37ec2d0edd fix: remove frappe.utils from jinja context in process statement of accounts 2026-06-25 14:55:47 +05:30
Khushi Rawat
85ba1231f8 Merge pull request #56444 from frappe/mergify/bp/version-16-hotfix/pr-56432
fix(letter-head): guard company lookups when doc has no company field (backport #56432)
2026-06-25 14:38:08 +05:30
Mihir Kandoi
a24b690a14 Merge pull request #56452 from mihir-kandoi/mariadb-ci-fanout-v16
ci(mariadb): self-hosted fan-out MariaDB CI (v16 backport of #56410)
2026-06-25 14:25:43 +05:30
Mihir Kandoi
8dd37b6df0 ci(mariadb): self-hosted fan-out MariaDB CI
Backport of #56410 to version-16-hotfix. v16 matches develop on Python 3.14 /
Node 24 / --lightmode / payments branch and the
ghcr.io/frappe/erpnext-ci-mariadb:py3.14-node24 image, so the fan-out workflow
and helpers (start-db.sh, hydrate.sh) apply verbatim; the frappe framework
branch resolves automatically from GITHUB_BASE_REF.
2026-06-25 14:15:18 +05:30
Mihir Kandoi
de70f2cceb Merge pull request #56307 from frappe/mergify/bp/version-16-hotfix/pr-56306
fix: link portal address rows to web form (backport #56306)
2026-06-25 13:51:07 +05:30
Ejaaz Khan
89059a990f fix(letter-head): guard company lookups when doc has no company field
(cherry picked from commit 7cb03a427a)

# Conflicts:
#	erpnext/accounts/letter_head/company_letterhead/company_letterhead.json
#	erpnext/accounts/letter_head/company_letterhead___grey/company_letterhead___grey.json
#	erpnext/accounts/letter_head/company_letterhead_report/company_letterhead_report.json
2026-06-25 05:42:22 +00:00
Mihir Kandoi
a9a371e4a4 Merge pull request #56433 from aerele/backport-56376
fix: skip over-allowance qty validation for non-stock items (backport #56335)
2026-06-24 20:03:15 +05:30
mergify[bot]
bd54c7fea8 fix(lead): added missing read permission check on get_lead_details (backport #56272) (#56274)
fix(lead): added missing read permission check on `get_lead_details` (backport #56272)
2026-06-24 19:42:20 +05:30
pandiyan
0c502eaa18 test: add tests for non stock item over billing against so/po 2026-06-24 18:47:59 +05:30
mergify[bot]
e3958ad7bb fix: precision issue causing COGS in inter transfer PR (backport #56420) (#56425)
fix: precision issue causing COGS in inter transfer PR (#56420)

(cherry picked from commit 9b0e1b61f2)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-06-24 18:30:32 +05:30
pandiyan
bc313dc09d fix: skip qty over-allowance check for non-stock items only 2026-06-24 18:12:42 +05:30
Mihir Kandoi
5c716b0547 Merge pull request #56430 from frappe/mergify/bp/version-16-hotfix/pr-55191
refactor(sales_person_wise_transaction_summary): Replace SQL with que… (backport #55191)
2026-06-24 16:43:29 +05:30
Loic Oberle
b3871a212c refactor(sales_person_wise_transaction_summary): Replace SQL with que… (#55191)
(cherry picked from commit df3d0859a1)
2026-06-24 10:44:33 +00:00
Frappe PR Bot
e9a9224ec4 chore(release): Bumped to Version 16.25.0
# [16.25.0](https://github.com/frappe/erpnext/compare/v16.24.0...v16.25.0) (2026-06-24)

### Features

* **accounts:** add configurable job timeout for Process Period Closing Voucher ([e71b066](e71b066eec))
2026-06-24 10:43:40 +00:00
ruthra kumar
b08de1f1e5 Merge pull request #56427 from frappe/mergify/bp/version-16/pr-56418
refactor: configurable timeout on process pcv (backport #56417) (backport #56418)
2026-06-24 16:12:00 +05:30
Mihir Kandoi
18400b58ce Merge pull request #56423 from frappe/mergify/bp/version-16-hotfix/pr-56421
fix: exclude virtual child doctypes from deletion in transaction dele… (backport #56421)
2026-06-24 15:37:18 +05:30
ruthra kumar
e46be25e9f chore: resolve conflicts
(cherry picked from commit df3c821f98)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/patches.txt
2026-06-24 15:36:07 +05:30
ruthra kumar
570c67bb34 refactor: patch, display depends on and json changes
(cherry picked from commit 3da7eefebb)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/patches.txt
(cherry picked from commit c33d7e5d7b)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/patches.txt
2026-06-24 10:00:58 +00:00
ruthra kumar
e71b066eec feat(accounts): add configurable job timeout for Process Period Closing Voucher
Adds a `pcv_job_timeout` Int field (default 3600s) to Accounts Settings
so admins can tune the enqueue timeout for PCV background jobs without
a code change. All three `frappe.enqueue` calls in
`process_period_closing_voucher.py` now read this value at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 13b6c4a165)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
(cherry picked from commit c97be8abe1)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
2026-06-24 10:00:57 +00:00
ruthra kumar
eee826dfb6 Merge pull request #56419 from frappe/mergify/bp/version-16-hotfix/pr-56417
refactor: configurable timeout on process pcv (backport #56417)
2026-06-24 15:30:14 +05:30
Mihir Kandoi
8a665709d2 fix: exclude virtual child doctypes from deletion in transaction deletion record
(cherry picked from commit 8bd8b28207)
2026-06-24 09:48:25 +00:00
ruthra kumar
267086153b chore: resolve conflicts 2026-06-24 15:08:04 +05:30
ruthra kumar
66b28cf456 refactor: patch, display depends on and json changes
(cherry picked from commit 3da7eefebb)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/patches.txt
2026-06-24 07:58:49 +00:00
ruthra kumar
d389014e57 feat(accounts): add configurable job timeout for Process Period Closing Voucher
Adds a `pcv_job_timeout` Int field (default 3600s) to Accounts Settings
so admins can tune the enqueue timeout for PCV background jobs without
a code change. All three `frappe.enqueue` calls in
`process_period_closing_voucher.py` now read this value at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 13b6c4a165)
2026-06-24 07:58:48 +00:00
mergify[bot]
54c45d7b22 fix: job card timer issue (backport #56405) (#56406)
fix: job card timer issue (#56405)

(cherry picked from commit 21541e3ad3)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-06-24 09:21:02 +05:30
Frappe PR Bot
e9a26b5086 chore(release): Bumped to Version 16.24.0
# [16.24.0](https://github.com/frappe/erpnext/compare/v16.23.1...v16.24.0) (2026-06-23)

### Bug Fixes

* add customer type in the list view ([c788106](c788106011))
* add partially transferred status and fix button visibility for partial material transfer on job card ([570ef45](570ef45e46))
* add validation and tests for set_status ([bcd72a7](bcd72a7fec))
* address product bundle review comments ([c066880](c066880978))
* apply docstatus filter to exclude cancelled Work Orders in Serial No ([58d5f39](58d5f39e0a))
* **budget:** ambiguous error message for budget assignment validation (backport [#56390](https://github.com/frappe/erpnext/issues/56390)) ([#56392](https://github.com/frappe/erpnext/issues/56392)) ([2b6f2c2](2b6f2c2f9c))
* clear stale payment rows on non-POS returns so they don't surface in bank reconciliation (backport [#55903](https://github.com/frappe/erpnext/issues/55903)) ([#56171](https://github.com/frappe/erpnext/issues/56171)) ([d363186](d3631860db))
* **coa_importer:** allow importing COA through `import_coa` only for `Accounts Manager` (backport [#56132](https://github.com/frappe/erpnext/issues/56132)) ([#56140](https://github.com/frappe/erpnext/issues/56140)) ([e6e5591](e6e5591088))
* company default handling in purchase transactions made from project ([29323cb](29323cb0b1))
* customer master form cleanup ([8b56b7b](8b56b7ba0e))
* disable is_debit_note while creating credit note ([c7dbedb](c7dbedbfdc))
* disarding stock entry fix ([5372254](537225494c))
* **err:** add missing permission check on `get_account_details` ([88ce356](88ce356d62))
* escape user image url on various templates (backport [#56269](https://github.com/frappe/erpnext/issues/56269)) ([#56271](https://github.com/frappe/erpnext/issues/56271)) ([3b734f4](3b734f4d5d))
* fetch party types based on account type in journal entry and refactor SQL to query builder ([bbb3181](bbb3181c6e))
* honor account freezing date when cancelling vouchers ([f4b827c](f4b827cb3d))
* **journal entry:** validate opening entry against pcv on save ([f8aa4c7](f8aa4c730c))
* lock budget distribution table and guard against null distribution rows ([2b28b7e](2b28b7e694))
* **manufacturing:** make item_code mandatory in Job Card Item ([d40c36a](d40c36a4b1))
* party specific item doesnt work if there are 2 suppliers with same item ([3df7a28](3df7a28476))
* **payment_entry:** recompute base amount when exchange rate changes (backport [#56136](https://github.com/frappe/erpnext/issues/56136)) ([#56398](https://github.com/frappe/erpnext/issues/56398)) ([1d0edf1](1d0edf1b9a))
* placement of fields ([#56257](https://github.com/frappe/erpnext/issues/56257)) ([3f53af8](3f53af8b1f))
* **pos:** remove redundant opening balance dialog onchange handler (backport [#54591](https://github.com/frappe/erpnext/issues/54591)) ([#56403](https://github.com/frappe/erpnext/issues/56403)) ([4555c32](4555c323af))
* preserve stock ageing on non-serial reconciliation ([846e0a9](846e0a9f06))
* removing the document naming series dialog and moving to framework ([97279c7](97279c7e26))
* **report_utils:** remove unnecessary whitelist decorator on `get_invoiced_item_gross_margin` ([bf58393](bf58393fda))
* resolve backport merge conflicts in customer.json ([77121f2](77121f2a41))
* resolve backport merge conflicts in supplier.json ([a80de9b](a80de9bd01))
* **stock:** allow partial raw material picking/transfer from work order ([8e3fbab](8e3fbab94a))
* **stock:** apply precision to the additional cost amount in stock entry ([6ac699d](6ac699d3bb))
* **stock:** define qi exception classes in exceptions file ([42c121a](42c121a750))
* **stock:** enable quality inspection for all Stock Entry purposes ([a631035](a6310351fd))
* **stock:** propagate renamed attribute values to variant items ([dbc831e](dbc831e008))
* **stock:** update transfer status for mixed transfer flows ([84a1a51](84a1a51023))
* **stock:** update variant attributes on value rename ([3110ab1](3110ab1c57))
* **stock:** update voucher valuaion rate in sle (backport [#55960](https://github.com/frappe/erpnext/issues/55960)) ([#56263](https://github.com/frappe/erpnext/issues/56263)) ([82e1221](82e1221dc9))
* submittable product bundle issues ([7a1def0](7a1def07e9))
* supplier master form cleanup ([e5c9e7a](e5c9e7abdc))
* supplier status in list view ([2035fac](2035fac494))
* tax.base_tax_amount as none when payment entry created using API ([43b355e](43b355eaf6))
* type def in get_linked_payments ([#56100](https://github.com/frappe/erpnext/issues/56100)) ([8e21af0](8e21af0a63))
* update reference doctype mapping and field visibility in bank guarantee ([dc9ae20](dc9ae20db8))
* update round off account functions to accept document context for regional overrides (backport [#55758](https://github.com/frappe/erpnext/issues/55758)) ([#55771](https://github.com/frappe/erpnext/issues/55771)) ([f5d05b9](f5d05b969b))
* update weighted average rate calculation to consider returned and consumed quantities ([35e0604](35e06045bd))

### Features

* add batch-level option to allow negative stock for batch ([1f075d4](1f075d4bbf))
* allocate full actual charge to stock items only (e.g. Freight) (backport [#56102](https://github.com/frappe/erpnext/issues/56102)) ([#56222](https://github.com/frappe/erpnext/issues/56222)) ([9469889](9469889bd5))
* **opening invoice creation tool:** add project to opening invoice child row (backport [#54662](https://github.com/frappe/erpnext/issues/54662)) ([#56401](https://github.com/frappe/erpnext/issues/56401)) ([d54938f](d54938fa64))
* party aliases ([768425e](768425ebf1))

### Performance Improvements

* composite index on (serial_no, warehouse, posting_datetime) for Serial and Batch Entry (backport [#56032](https://github.com/frappe/erpnext/issues/56032)) ([#56166](https://github.com/frappe/erpnext/issues/56166)) ([c0dab55](c0dab55fcc))
2026-06-23 21:37:59 +00:00
Diptanil Saha
872c86e223 Merge pull request #56359 from frappe/version-16-hotfix
chore: release v16
2026-06-24 03:06:16 +05:30
mergify[bot]
d54938fa64 feat(opening invoice creation tool): add project to opening invoice child row (backport #54662) (#56401)
Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
2026-06-23 21:07:26 +00:00
mergify[bot]
4555c323af fix(pos): remove redundant opening balance dialog onchange handler (backport #54591) (#56403)
Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
fix(pos): remove redundant opening balance dialog onchange handler (#54591)
2026-06-24 02:24:23 +05:30
mergify[bot]
1d0edf1b9a fix(payment_entry): recompute base amount when exchange rate changes (backport #56136) (#56398)
Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
Co-authored-by: ervishnucs <ervishnucs369@gmail.com>
fix(payment_entry): recompute base amount when exchange rate changes (#56136)
2026-06-24 02:11:35 +05:30
mergify[bot]
2b6f2c2f9c fix(budget): ambiguous error message for budget assignment validation (backport #56390) (#56392)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
Co-authored-by: Wolfram Schmidt <wolfram.schmidt@phamos.eu>
fix(budget): ambiguous error message for budget assignment validation (#56390)
2026-06-24 01:03:05 +05:30
mergify[bot]
00ba64baae feat(crm_settings)!: enable frappe crm data synchronization (backport #56268) (#56384)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-06-23 22:02:23 +05:30
Mihir Kandoi
3dc128881c Merge pull request #56374 from frappe/mergify/bp/version-16-hotfix/pr-56364
fix(manufacturing): make item_code mandatory in Job Card Item (backport #56364)
2026-06-23 19:36:44 +05:30
pandiyan
d40c36a4b1 fix(manufacturing): make item_code mandatory in Job Card Item
The item_code field in the Job Card Item child table was optional,
allowing job cards to be saved without a raw material item linked.
Set reqd=1 in the JSON and update the Python type annotation accordingly.

(cherry picked from commit d7e9a97f8a)
2026-06-23 13:33:00 +00:00
Khushi Rawat
7b50f67b55 Merge pull request #56317 from frappe/mergify/bp/version-16-hotfix/pr-55341
fix: customer master form cleanup (backport #55341)
2026-06-23 16:44:22 +05:30
Khushi Rawat
b40445fe44 Merge pull request #56316 from frappe/mergify/bp/version-16-hotfix/pr-55461
fix: supplier master form cleanup (backport #55461)
2026-06-23 16:37:47 +05:30
khushi8112
77121f2a41 fix: resolve backport merge conflicts in customer.json
Keep both customer_type (list view) and the hotfix-only alias field in
field_order; take the newest modified timestamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:14:24 +05:30
khushi8112
a80de9bd01 fix: resolve backport merge conflicts in supplier.json
Apply form-cleanup label changes while preserving the hotfix-only
no_copy flags and the alias field. Drop removed column_break_44 and
the duplicate is_frozen field_order entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:10:11 +05:30
Nishka Gosalia
be605adbc1 Merge pull request #56351 from frappe/mergify/bp/version-16-hotfix/pr-56350
fix: handling default company in purchase transactions created from project (backport #56350)
2026-06-23 12:40:38 +05:30
nishkagosalia
29323cb0b1 fix: company default handling in purchase transactions made from project
(cherry picked from commit 359717115f)
2026-06-23 07:08:07 +00:00
MochaMind
17324ec45b chore: sync translations to version-16-hotfix (#56321) 2026-06-23 02:16:25 +05:30
mergify[bot]
f5d05b969b fix: update round off account functions to accept document context for regional overrides (backport #55758) (#55771)
Co-authored-by: Lakshit Jain <ljain112@gmail.com>
fix: update round off account functions to accept document context for regional overrides (#55758)
2026-06-22 12:30:09 +00:00
khushi8112
c788106011 fix: add customer type in the list view
(cherry picked from commit 059f560017)

# Conflicts:
#	erpnext/selling/doctype/customer/customer.json
2026-06-22 11:42:59 +00:00
khushi8112
8b56b7ba0e fix: customer master form cleanup
(cherry picked from commit 6f6e17188f)

# Conflicts:
#	erpnext/selling/doctype/customer/customer.json
2026-06-22 11:42:59 +00:00
khushi8112
2035fac494 fix: supplier status in list view
(cherry picked from commit 515983e016)
2026-06-22 11:42:29 +00:00
khushi8112
e5c9e7abdc fix: supplier master form cleanup
(cherry picked from commit 820c0caf88)

# Conflicts:
#	erpnext/buying/doctype/supplier/supplier.json
2026-06-22 11:42:29 +00:00
Mihir Kandoi
b4f2b6cab2 Merge pull request #56313 from mihir-kandoi/backport-55802-version-16-hotfix
fix: backport product bundle issue fixes to v16
2026-06-22 17:04:30 +05:30
Mihir Kandoi
373476042e Merge pull request #56285 from aerele/backport-56204
fix: add partially transferred status and fix button visibility for partial material transfer on job card
2026-06-22 16:47:24 +05:30
Mihir Kandoi
c066880978 fix: address product bundle review comments
(cherry picked from commit d48a1e0d16)
2026-06-22 16:32:42 +05:30
Mihir Kandoi
7a1def07e9 fix: submittable product bundle issues
(cherry picked from commit a218b8db8c)
2026-06-22 16:32:10 +05:30
Nishka Gosalia
0051950afb Merge pull request #56311 from frappe/mergify/bp/version-16-hotfix/pr-56309
fix: Removing the document naming series dialog and moving to framework (backport #56309)
2026-06-22 16:23:42 +05:30
nishkagosalia
97279c7e26 fix: removing the document naming series dialog and moving to framework
(cherry picked from commit aa7402b1e3)
2026-06-22 10:37:41 +00:00
Mihir Kandoi
0be5ba6bac Merge pull request #56305 from frappe/mergify/bp/version-16-hotfix/pr-56300
fix: party specific item doesnt work if there are 2 suppliers with sa… (backport #56300)
2026-06-22 15:54:59 +05:30
Mihir Kandoi
3a480c08b1 fix: link portal address rows to web form
(cherry picked from commit 5008b82f90)
2026-06-22 09:59:53 +00:00
Mihir Kandoi
a67b489d17 test: add test case
(cherry picked from commit 7d205c89ea)
2026-06-22 09:27:41 +00:00
Mihir Kandoi
3df7a28476 fix: party specific item doesnt work if there are 2 suppliers with same item
(cherry picked from commit 98f5116a09)
2026-06-22 09:27:40 +00:00
ruthra kumar
694ebb53ec Merge pull request #56299 from frappe/mergify/bp/version-16-hotfix/pr-55488
fix: add validation and tests for set_status (backport #55488)
2026-06-22 14:09:16 +05:30
Mihir Kandoi
9643720858 Merge pull request #56297 from mihir-kandoi/codex/party-alias-v16
feat: party aliases
2026-06-22 13:54:24 +05:30
Shllokkk
bcd72a7fec fix: add validation and tests for set_status
(cherry picked from commit b5a84c5e65)
2026-06-22 07:51:06 +00:00
Mihir Kandoi
768425ebf1 feat: party aliases
(cherry picked from commit 5e16d41387)
2026-06-22 13:16:56 +05:30
Nabin Hait
5d031c0a04 Merge pull request #56289 from frappe/mergify/bp/version-16-hotfix/pr-56288
ci: Wait for processes to die (backport #56288)
2026-06-22 12:51:28 +05:30
Ankush Menat
476054f684 ci: Wait for processes to die (#56288)
(cherry picked from commit 7256fc98e9)
2026-06-22 06:56:03 +00:00
pandiyan
570ef45e46 fix: add partially transferred status and fix button visibility for partial material transfer on job card 2026-06-22 11:37:41 +05:30
Ravibharathi
2462be0e61 Merge pull request #56200 from frappe/mergify/bp/version-16-hotfix/pr-56155
fix: fetch party types based on account type in journal entry (backport #56155)
2026-06-22 10:06:40 +05:30
mergify[bot]
d3631860db fix: clear stale payment rows on non-POS returns so they don't surface in bank reconciliation (backport #55903) (#56171)
fix: clear stale payment rows on non-POS returns so they don't surface in bank reconciliation (#55903)

(cherry picked from commit 322d4dff25)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
#	erpnext/accounts/doctype/sales_invoice/services/pos.py

Co-authored-by: Jatin3128 <jatinsarna8@gmail.com>
2026-06-22 05:17:33 +05:30
mergify[bot]
3b734f4d5d fix: escape user image url on various templates (backport #56269) (#56271)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix: escape user image url on various templates (#56269)
2026-06-22 03:00:39 +05:30
Mihir Kandoi
f3307b3ca9 Merge pull request #56206 from aerele/backport-#55807
fix(stock): allow partial raw material picking/transfer from work order
2026-06-21 22:23:49 +05:30
mergify[bot]
82e1221dc9 fix(stock): update voucher valuaion rate in sle (backport #55960) (#56263)
fix(stock): update voucher valuaion rate in sle (#55960)

(cherry picked from commit 130c2594e1)

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-06-21 16:41:05 +00:00
Sudharsanan11
4d055d374a test(stock): add test to validate the partial transfer of raw material 2026-06-21 21:57:51 +05:30
Sudharsanan11
8e3fbab94a fix(stock): allow partial raw material picking/transfer from work order 2026-06-21 21:57:51 +05:30
MochaMind
6f9954bb62 chore: update POT file (#56254) 2026-06-21 14:25:56 +02:00
rohitwaghchaure
3f53af8b1f fix: placement of fields (#56257) 2026-06-21 12:00:20 +00:00
rohitwaghchaure
9469889bd5 feat: allocate full actual charge to stock items only (e.g. Freight) (backport #56102) (#56222)
* feat: allocate full actual charge to stock items only (e.g. Freight)

Backport of #56102 to version-16-hotfix. Adapts the GL valuation-tax change
to the inline make_tax_gl_entries in purchase_receipt.py (no services/ refactor
on hotfix) and additionally applies it to purchase_invoice.py.

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

* fix: distribute each Actual valuation charge individually

distribute_actual_tax_amount pooled all "Actual" valuation charges (both
the spread-across-all-items charges and the allocate_full_amount_to_stock_items
freight charges) into single totals before spreading, while the GL path
(get_capitalized_valuation_tax) capitalizes each tax row separately. For
multiple charges over unevenly valued items, pool-then-spread can drift by a
rounding cent from spread-each-then-sum, so a row's item_tax_amount no longer
decomposed exactly into the per-account capitalized GL amounts (the document
total still balanced).

get_tax_details now returns the per-row charge amounts as lists and
distribute_actual_tax_amount spreads each charge on its own, mirroring
get_capitalized_valuation_tax. Per-item valuation now reconciles exactly with
per-account GL credits. Single-charge behaviour is unchanged.

Adds test_multiple_actual_charges_per_item_matches_gl_per_account covering two
freight charges over items of net 100 and 200 (asserts 6.66 / 13.34, which the
old pooled logic would have rounded to 6.67 / 13.33).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 16:21:06 +05:30
Mihir Kandoi
6e61ee8d70 Merge pull request #56201 from aerele/backport-#56077
fix(stock): apply precision to the additional cost amount in stock entry
2026-06-21 13:44:17 +05:30
Mihir Kandoi
dcf076aad6 Merge pull request #56245 from frappe/mergify/bp/version-16-hotfix/pr-56235
refactor(stock): remove dead get_batches() in batch.py (backport #56235)
2026-06-21 13:08:18 +05:30
Mihir Kandoi
edd18fd650 refactor(stock): remove dead get_batches() in batch.py
batch.get_batches(item_code, warehouse, ...) was added by #55647 and has no callers
anywhere in erpnext, frappe, or payments (not whitelisted, not referenced from JS/hooks).
It is also obsolete: it joins Stock Ledger Entry on `batch_no`, which the Serial and
Batch Bundle system no longer populates, so it returns nothing even on MariaDB. Its
query was additionally Postgres-invalid (GROUP BY batch_id with ORDER BY expiry_date/
creation -> GroupingError, since batch_id is not the primary key).

Remove the dead function (and its now-unused CurDate/Sum import) rather than fix a query
that nothing can reach. Live batch-quantity lookups go through get_batch_qty() /
get_auto_batch_nos(), which use the bundle model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 345cbc97e1)
2026-06-21 07:18:19 +00:00
Mihir Kandoi
62209348a4 Merge pull request #56232 from frappe/mergify/bp/version-16-hotfix/pr-56228
fix: disarding stock entry fix (backport #56228)
2026-06-21 10:41:31 +05:30
nishkagosalia
537225494c fix: disarding stock entry fix
(cherry picked from commit debe1855c6)
2026-06-21 04:47:04 +00:00
S Sakthivel Murugan
bbb3181c6e fix: fetch party types based on account type in journal entry and refactor SQL to query builder
(cherry picked from commit 6f225920d0)
2026-06-20 18:32:30 +00:00
Sudharsanan11
20b14395e3 test(stock): add test to validate the precision for additional cost amount 2026-06-20 23:51:16 +05:30
Sudharsanan11
6ac699d3bb fix(stock): apply precision to the additional cost amount in stock entry 2026-06-20 23:50:50 +05:30
mergify[bot]
e6e5591088 fix(coa_importer): allow importing COA through import_coa only for Accounts Manager (backport #56132) (#56140)
* fix(coa_importer): allow importing COA through `import_coa` only for `Accounts Manager` (#56132)

* fix(coa_importer): allow importing COA only for `Accounts Manager`

Co-authored-by: Pratheep S <pratheeps2024@gmail.com>

* fix(coa_importer): check permissions in `unset_existing_data`

---------

Co-authored-by: Pratheep S <pratheeps2024@gmail.com>
(cherry picked from commit 8c1a1aafe6)

# Conflicts:
#	erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py

* chore: resolve conflicts

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-06-20 22:16:16 +05:30
Diptanil Saha
635c51acf7 Merge pull request #56194 from frappe/mergify/bp/version-16-hotfix/pr-56191
fix: added missing permission validation on whitelisted function and removed unnecessary whitelisted decorator (backport #56191)
2026-06-20 20:25:41 +05:30
diptanilsaha
e605675e11 chore: resolve conflicts 2026-06-20 20:07:42 +05:30
diptanilsaha
bf58393fda fix(report_utils): remove unnecessary whitelist decorator on get_invoiced_item_gross_margin
(cherry picked from commit e29535f29c)

# Conflicts:
#	erpnext/accounts/report/utils.py
2026-06-20 20:07:42 +05:30
diptanilsaha
88ce356d62 fix(err): add missing permission check on get_account_details
(cherry picked from commit 9bf1e847d2)
2026-06-20 20:07:37 +05:30
Shllokkk
396feadace Merge pull request #56164 from Shllokkk/honor-account-freezing-date-on-cancel
fix: honor account freezing date when cancelling vouchers
2026-06-20 13:56:12 +05:30
mergify[bot]
c0dab55fcc perf: composite index on (serial_no, warehouse, posting_datetime) for Serial and Batch Entry (backport #56032) (#56166)
* perf: composite index on (serial_no, warehouse, posting_datetime)

(cherry picked from commit b1b6ae98ed)

# Conflicts:
#	erpnext/patches.txt

* chore: fix conflicts

Removed conflicting patch entries and retained relevant ones.

* chore: fix conflicts

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-06-19 12:54:19 +00:00
ruthra kumar
cb47745d8c Merge pull request #56159 from frappe/mergify/bp/version-16-hotfix/pr-55265
fix: update reference doctype mapping and field visibility in bank guarantee (backport #55265)
2026-06-19 17:48:27 +05:30
Shllokkk
f4b827cb3d fix: honor account freezing date when cancelling vouchers 2026-06-19 16:51:54 +05:30
nareshkannasln
dc9ae20db8 fix: update reference doctype mapping and field visibility in bank guarantee
(cherry picked from commit b1de654dfd)
2026-06-19 11:09:38 +00:00
Mihir Kandoi
6cb42ab8b1 Merge pull request #56138 from frappe/mergify/bp/version-16-hotfix/pr-55920
fix: update weighted average rate calculation to consider returned and consumed quantities (backport #55920)
2026-06-19 15:15:45 +05:30
ljain112
35e06045bd fix: update weighted average rate calculation to consider returned and consumed quantities
(cherry picked from commit 35e55d3e13)
2026-06-19 09:17:57 +00:00
Mihir Kandoi
6c37acc180 Merge pull request #55968 from frappe/mergify/bp/version-16-hotfix/pr-55830
fix(stock): enable quality inspection for all Stock Entry purposes (backport #55830)
2026-06-19 12:03:46 +05:30
Sudharsanan11
42c121a750 fix(stock): define qi exception classes in exceptions file 2026-06-19 11:38:25 +05:30
Mihir Kandoi
1e027364e3 chore: resolve conflicts 2026-06-19 11:38:25 +05:30
Sudharsanan11
d2fee32eb3 test(stock): add test to validate the quality inspection for stock entry
(cherry picked from commit 609ccc3cb1)
2026-06-19 11:38:25 +05:30
Smit Vora
21912402c0 Merge pull request #56123 from frappe/mergify/bp/version-16-hotfix/pr-56104
fix: base_tax_amount as none when payment entry created using API (backport #56104)
2026-06-19 09:25:27 +05:30
vorasmit
43b355eaf6 fix: tax.base_tax_amount as none when payment entry created using API
(cherry picked from commit b9b402f2ec)
2026-06-19 03:17:58 +00:00
Mihir Kandoi
175aac4156 Merge pull request #56119 from frappe/mergify/bp/version-16-hotfix/pr-56065
fix(stock): propagate renamed attribute values to variant items (backport #56065)
2026-06-18 23:16:26 +05:30
Mihir Kandoi
30650f298b Merge pull request #56115 from frappe/mergify/bp/version-16-hotfix/pr-56055
fix: disable is_debit_note while creating credit note (backport #56055)
2026-06-18 22:54:51 +05:30
barredterra
3110ab1c57 fix(stock): update variant attributes on value rename
(cherry picked from commit c7acd88742)
2026-06-18 17:06:07 +00:00
barredterra
40110d83c9 test(stock): add cleanup for item attribute value changes in tests
(cherry picked from commit 60f5de7ab8)
2026-06-18 17:06:07 +00:00
barredterra
dbc831e008 fix(stock): propagate renamed attribute values to variant items
(cherry picked from commit 27d574dad5)
2026-06-18 17:06:07 +00:00
Mihir Kandoi
686437bd54 Merge pull request #56117 from frappe/mergify/bp/version-16-hotfix/pr-56098
fix: apply docstatus filter to exclude cancelled Work Orders in Seria… (backport #56098)
2026-06-18 22:32:47 +05:30
pandiyan
58d5f39e0a fix: apply docstatus filter to exclude cancelled Work Orders in Serial No
(cherry picked from commit 3ba8f690a4)
2026-06-18 16:56:46 +00:00
pandiyan
c7dbedbfdc fix: disable is_debit_note while creating credit note
(cherry picked from commit 279c8dea06)
2026-06-18 16:54:21 +00:00
Nikhil Kothari
8e21af0a63 fix: type def in get_linked_payments (#56100) 2026-06-18 14:11:11 +00:00
Shllokkk
87e498cd7d Merge pull request #56088 from Shllokkk/je-pcv-vaidation
fix(journal entry): validate opening entry against pcv on save
2026-06-18 18:04:51 +05:30
Shllokkk
f8aa4c730c fix(journal entry): validate opening entry against pcv on save 2026-06-18 16:56:17 +05:30
rohitwaghchaure
a335838691 Merge pull request #56091 from frappe/mergify/bp/version-16-hotfix/pr-56079
feat: allow negative stock at batch level (backport #56079)
2026-06-18 16:42:32 +05:30
Rohit Waghchaure
1f075d4bbf feat: add batch-level option to allow negative stock for batch
(cherry picked from commit ca07982ee0)
2026-06-18 10:49:45 +00:00
Khushi Rawat
7f441864d6 Merge pull request #56084 from frappe/mergify/bp/version-16-hotfix/pr-56030
fix: lock budget distribution table and guard against null distributi… (backport #56030)
2026-06-18 14:34:18 +05:30
Shllokkk
2b28b7e694 fix: lock budget distribution table and guard against null distribution rows
(cherry picked from commit d37e5cd97d)
2026-06-18 08:28:37 +00:00
Mihir Kandoi
56d9cbabbf Merge pull request #56049 from aerele/backport-56003
fix(stock): update transfer status for mixed transfer flows
2026-06-17 17:20:55 +05:30
pandiyan
4481efec17 test(stock): validate completed status for mixed transfer methods 2026-06-17 16:58:32 +05:30
pandiyan
84a1a51023 fix(stock): update transfer status for mixed transfer flows 2026-06-17 16:58:23 +05:30
Mihir Kandoi
98f45221e6 Merge pull request #56043 from mihir-kandoi/codex/fix-stock-ageing-reco-ageing-v16
fix: preserve stock ageing on non-serial reconciliation
2026-06-17 16:20:49 +05:30
Mihir Kandoi
846e0a9f06 fix: preserve stock ageing on non-serial reconciliation 2026-06-17 15:58:47 +05:30
ruthra kumar
6185507614 Merge pull request #56024 from frappe/mergify/bp/version-16-hotfix/pr-55988
refactor(test): remove dependency on accounts test mixin (backport #55988)
2026-06-17 07:59:00 +05:30
ruthra kumar
d051407126 refactor(test): remove redundant clear method and minor fixes
(cherry picked from commit 004087097c)
2026-06-17 02:11:27 +00:00
ruthra kumar
3d91e021a3 refactor(tests): replace AccountsTestMixin master data setup with direct attribute assignments
All test classes inheriting AccountsTestMixin that called create_company(),
create_item(), create_customer(), create_supplier(), create_usd_receivable_account(),
and create_usd_payable_account() in setUp() now set instance attributes directly
using master data pre-created by BootStrapTestData, eliminating redundant DB
inserts on every test run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 1fda0dfb9b)
2026-06-17 02:11:26 +00:00
Nabin Hait
6f281356a6 fix(accounts): clear clearance date when amending reconciled voucher
The framework ignores `no_copy` while amending, so a reconciled voucher
carried a stale clearance date into its amendment even though the linked
bank transaction gets unreconciled on cancellation. Reset it via a shared
`before_insert` hook on AccountsController.

Fixes #54909

(cherry picked from commit 1a8d73cbbe)
2026-06-16 09:59:41 +00:00
Sudharsanan11
a6310351fd fix(stock): enable quality inspection for all Stock Entry purposes
- Remove `depends_on` restriction from `inspection_required` field so it
  is visible for all Stock Entry purposes, not just Manufacture
- Fix `check_item_quality_inspection` to return items for Stock Entry
  (was returning [] for unknown doctypes, blocking QI creation flow)
- Fix `inspection_type` in transaction.js to be purpose-aware: Manufacture
  and Material Receipt → "Incoming"; all other purposes → "Outgoing"

(cherry picked from commit dceb9a3c6c)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.json
2026-06-16 09:38:17 +00:00
396 changed files with 336793 additions and 108445 deletions

72
.github/helper/hydrate.sh vendored Executable file
View File

@@ -0,0 +1,72 @@
#!/bin/bash
#
# Hydrate a test shard from the setup job's artifact.
#
# The bench (apps, venv, node_modules, sites) is already on disk at ~/frappe-bench — the
# workflow untar'd it from the artifact the setup job built. So there is NO bench init, no
# asset build, and no reinstall here: just bring the DB up on the baked datadir and start redis
# so tests can run. The whole point is that the expensive work happened ONCE in the setup job.
#
set -e
ci_user="${ERPNEXT_CI_USER:-frappe}"
db_host="${DB_HOST:-127.0.0.1}"
# Re-exec as the ci user (uid 1001) so bench/cache ownership matches the artifact, same as
# install.sh. The workflow untar'd as root with -p, so the files are already owned by ci.
if [ "$(id -u)" = "0" ] && [ "${SKIP_SYSTEM_SETUP:-0}" = "1" ] && [ "$ci_user" != "root" ]; then
exec su -m "$ci_user" -s /bin/bash -c \
"ERPNEXT_CI_USER='$ci_user' DB_HOST='$db_host' DB='${DB:-}' bash '$0'"
fi
cd ~/frappe-bench
# Start the DB on the datadir baked into the artifact. It's already populated (the setup job
# reinstalled into this very datadir), so there is NO restore — the server comes up on the
# existing files. This is what replaces the per-shard SQL replay.
bash ~/frappe-bench/start-db.sh
# Bring up redis (lightmode unit tests need cache + queue). In the self-hosted container we use the
# full `bench start` (web/workers too, like install.sh). On the bare GitHub Postgres shard
# `bench start` (honcho) lagged — it blocks the redis procs behind web/worker procs the lightmode
# suite never uses, so the wait below burned its full timeout (~4m). There, start the two redis
# instances directly: fast and deterministic.
if [ "${DB:-mariadb}" = "postgres" ]; then
# Start redis directly as daemons — reliable and persists across steps. Do NOT route it through
# `bench start`: honcho tears the whole process group down if any one Procfile proc dies on the
# bare shard, which took redis with it (redis @ 13000 refused in Run Tests). Keeping redis
# independent is what makes it survive. The web server (for PDF tests) is NOT started here — a
# backgrounded server doesn't survive into the next step; it's started inside the Run Tests step.
for conf in redis_cache redis_queue; do
[ -f ~/frappe-bench/config/$conf.conf ] && redis-server ~/frappe-bench/config/$conf.conf --daemonize yes
done
else
bench start >> ~/frappe-bench/bench_start.log 2>&1 &
fi
# Wait for redis, failing fast instead of silently burning minutes if it never comes up.
cfg=~/frappe-bench/sites/common_site_config.json
if [ -f "$cfg" ]; then
ports=$(python - "$cfg" <<'PY'
import json, re, sys
try:
cfg = json.load(open(sys.argv[1]))
except Exception:
sys.exit(0)
for key in ("redis_cache", "redis_queue"):
m = re.search(r":(\d+)", str(cfg.get(key, "")))
if m:
print(m.group(1))
PY
)
for port in $ports; do
up=0
for _ in $(seq 1 60); do
if (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then exec 3>&- 3<&-; up=1; break; fi
sleep 1
done
[ "$up" = "1" ] || { echo "redis did not come up on port $port"; exit 1; }
done
fi
echo "Hydrated: DB up on baked datadir, redis up — ready for tests."

View File

@@ -7,21 +7,106 @@ cd ~ || exit
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
db_host=${DB_HOST:-"127.0.0.1"}
db_user_host=${DB_USER_HOST:-"localhost"}
wkhtmltox_deb=${WKHTMLTOX_DEB:-"/tmp/wkhtmltox.deb"}
bench_cache_dir=${BENCH_CACHE_DIR:-}
run_as_ci_user_if_needed() {
if [ "$(id -u)" != "0" ] || [ "${SKIP_SYSTEM_SETUP:-0}" != "1" ] || [ "${ERPNEXT_CI_NON_ROOT:-0}" = "1" ]; then
return
fi
local missing_packages=()
if ! command -v pkg-config >/dev/null 2>&1; then
missing_packages+=("pkg-config")
fi
if ! command -v mariadb_config >/dev/null 2>&1 && ! command -v mysql_config >/dev/null 2>&1; then
missing_packages+=("libmariadb-dev")
fi
if ! command -v crontab >/dev/null 2>&1; then
missing_packages+=("cron")
fi
if [ "${#missing_packages[@]}" -gt 0 ]; then
apt-get update
apt-get install -y --no-install-recommends "${missing_packages[@]}"
fi
local ci_user="${ERPNEXT_CI_USER:-frappe}"
if ! id "$ci_user" >/dev/null 2>&1; then
useradd --home-dir "$HOME" --no-create-home --shell /bin/bash "$ci_user"
fi
rm -rf ~/frappe ~/frappe-bench
local ci_dirs=(
"$HOME"
"$GITHUB_WORKSPACE"
"$HOME/.cache"
"${PIP_CACHE_DIR:-$HOME/.cache/pip}"
"${npm_config_cache:-$HOME/.npm}"
"${YARN_CACHE_FOLDER:-$HOME/.cache/yarn}"
"$HOME/.yarn"
"${UV_CACHE_DIR:-$HOME/.cache/uv}"
"$(dirname "$wkhtmltox_deb")"
)
if [ -n "$bench_cache_dir" ]; then
ci_dirs+=("$bench_cache_dir")
fi
# Create + own (non-recursively) the home/cache/workspace dirs before dropping to
# the ci user. We deliberately do NOT wipe the yarn/uv caches here so a persistent
# cache (mounted volume or baked image layer) stays warm across runs.
mkdir -p "${ci_dirs[@]}" "$HOME/.yarn"
chown "$ci_user:$ci_user" "${ci_dirs[@]}" "$HOME/.yarn"
export ERPNEXT_CI_NON_ROOT=1
exec su -m "$ci_user" -s /bin/bash -c "cd '$HOME' && bash '$GITHUB_WORKSPACE/.github/helper/install.sh'"
}
run_as_ci_user_if_needed
run_ci_step() {
local label=$1
shift
echo "::group::${label}"
date -u
local exit_code=0
timeout --foreground "${CI_INSTALL_STEP_TIMEOUT:-1800}" "$@" || exit_code=$?
date -u
echo "::endgroup::"
return "$exit_code"
}
if [ -n "${GITHUB_WORKSPACE:-}" ]; then
git config --global --add safe.directory "$GITHUB_WORKSPACE" || true
git config --global --add safe.directory "$GITHUB_WORKSPACE/.git" || true
fi
rm -rf ~/frappe ~/frappe-bench
# ---------------------------------------------------------------------------
# Phase 1 — parallelise the three slow, independent setup steps:
# a) system packages b) frappe-bench pip install c) frappe git fetch
# ---------------------------------------------------------------------------
sudo apt update
if [ "${SKIP_SYSTEM_SETUP:-0}" != "1" ]; then
sudo apt-get update
# apt remove/install must run sequentially but can overlap with pip and git.
sudo apt remove mysql-server mysql-client
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev &
apt_pid=$!
# apt remove/install must run sequentially but can overlap with pip and git.
sudo apt-get remove -y mysql-server mysql-client
sudo apt-get install -y libcups2-dev redis-server mariadb-client libmariadb-dev &
apt_pid=$!
pip install frappe-bench &
pip_pid=$!
pip install frappe-bench &
pip_pid=$!
else
apt_pid=
pip_pid=
fi
mkdir frappe
(
@@ -32,76 +117,247 @@ mkdir frappe
) &
clone_pid=$!
wait $apt_pid
wait $pip_pid
if [ -n "$apt_pid" ]; then wait $apt_pid; fi
if [ -n "$pip_pid" ]; then wait $pip_pid; fi
wait $clone_pid
pushd frappe
git checkout FETCH_HEAD
popd
frappe_sha=$(git -C frappe rev-parse HEAD)
get_bench_cache_archive() {
if [ -z "$bench_cache_dir" ]; then
return
fi
mkdir -p "$bench_cache_dir"
# Keyed on tool versions only (NOT the frappe SHA): any recent base bench works, because
# restore_warm_bench fast-forwards it to the exact live develop SHA. This is what lets a
# constantly-moving develop still hit the cache.
local cache_key
cache_key=$(
{
uname -m
python --version
node --version
bench --version
} | sha256sum | awk '{print $1}'
)
echo "${bench_cache_dir}/frappe-bench-base-${cache_key}.tar.zst"
}
restore_warm_bench() {
bench_cache_archive=$(get_bench_cache_archive)
[ -n "$bench_cache_archive" ] && [ -f "$bench_cache_archive" ] || return 1
echo "Restoring base bench from ${bench_cache_archive}"
tar --use-compress-program=unzstd -xf "$bench_cache_archive" -C ~ || return 1
[ -d ~/frappe-bench/apps/frappe/.git ] || return 1
mkdir -p ~/frappe-bench/sites ~/frappe-bench/logs
[ -f ~/frappe-bench/sites/apps.txt ] || printf "frappe\n" > ~/frappe-bench/sites/apps.txt
[ -f ~/frappe-bench/sites/common_site_config.json ] || printf "{}\n" > ~/frappe-bench/sites/common_site_config.json
# Fast-forward the restored frappe to the EXACT live develop SHA fetched in phase 1, then
# rebuild only what changed. The editable install means the venv tracks the new code with
# no reinstall. Any failure returns non-zero so the caller falls back to a full bench init.
if ! (
cd ~/frappe-bench/apps/frappe || exit 1
# 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 checkout --force FETCH_HEAD || exit 1
); then
echo "Fast-forward to ${frappe_sha} failed; falling back to full init"
rm -rf ~/frappe-bench
return 1
fi
# Pick up any frappe dependency changes since the base was built (cached → fast if none),
# so a develop commit that bumped requirements doesn't leave a stale venv.
if ! ~/frappe-bench/env/bin/python -m pip install -q -e ~/frappe-bench/apps/frappe; then
echo "frappe dependency refresh failed; falling back to full init"
rm -rf ~/frappe-bench
return 1
fi
( cd ~/frappe-bench && CI=Yes bench build --app frappe ) || { rm -rf ~/frappe-bench; return 1; }
return 0
}
save_warm_bench() {
if [ -z "${bench_cache_archive:-}" ] || [ -f "$bench_cache_archive" ]; then
return
fi
if [ -n "$bench_cache_dir" ] && [ ! -w "$bench_cache_dir" ]; then
echo "Skipping warm bench save because ${bench_cache_dir} is not writable"
return
fi
local tmp_archive
tmp_archive="${bench_cache_archive}.${$}.tmp"
echo "Saving warm bench to ${bench_cache_archive}"
# Keep sites/common_site_config.json (the redis ports live there — dropping it makes the
# restore path fall back to a default redis port that bench start never bound, so reinstall
# fails with "redis ... connection refused"). Only the rebuildable sites/assets is excluded;
# restore_warm_bench runs `bench build` to regenerate it.
tar \
--use-compress-program="zstd -T0 -3" \
--exclude="frappe-bench/logs" \
--exclude="frappe-bench/sites/assets" \
-cf "$tmp_archive" \
-C ~ frappe-bench
mv "$tmp_archive" "$bench_cache_archive"
}
# ---------------------------------------------------------------------------
# Phase 2 — bench init and site setup
# ---------------------------------------------------------------------------
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
install_whktml() {
# Re-use the .deb if the wkhtmltopdf cache step already restored it.
if [ ! -f "$wkhtmltox_deb" ]; then
wget -O "$wkhtmltox_deb" https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
fi
sudo apt-get install -y "$wkhtmltox_deb"
}
if [ "${SKIP_WKHTMLTOX_SETUP:-0}" != "1" ]; then
install_whktml &
wkpid=$!
else
wkpid=
fi
mkdir ~/frappe-bench/sites/test_site
if ! restore_warm_bench; then
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
cd ~/frappe-bench || exit
sed -i 's/watch:/# watch:/g' Procfile
sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
CI=Yes bench build --app frappe
save_warm_bench
fi
if [ -n "$wkpid" ]; then wait $wkpid; fi
mkdir -p ~/frappe-bench/sites/test_site
if [ "$DB" == "mariadb" ];then
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_mariadb.json" ~/frappe-bench/sites/test_site/site_config.json
if [ "$db_host" != "127.0.0.1" ]; then
sed -i "s/\"db_host\": \"127.0.0.1\"/\"db_host\": \"${db_host}\"/" ~/frappe-bench/sites/test_site/site_config.json
fi
else
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_postgres.json" ~/frappe-bench/sites/test_site/site_config.json
fi
if [ "$DB" == "mariadb" ];then
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
for _ in {1..60}; do
if mariadb-admin ping --host "$db_host" --port 3306 -u root -proot --silent; then
break
fi
sleep 1
done
mariadb-admin ping --host "$db_host" --port 3306 -u root -proot --silent
# Belt-and-suspenders: also set performance variables at runtime in case
# MARIADB_EXTRA_FLAGS was not honoured by the container image.
mariadb --host 127.0.0.1 --port 3306 -u root -proot \
mariadb --host "$db_host" --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mariadb --host "$db_host" --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
# Throwaway-DB durability tuning at runtime. (innodb_doublewrite is read-only on MariaDB
# 10.6, so it can't be disabled here — would need a server startup flag.)
mariadb --host "$db_host" --port 3306 -u root -proot \
-e "SET GLOBAL innodb_flush_log_at_trx_commit=0; SET GLOBAL sync_binlog=0;"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
# Opt-in DDL speedup: a shared tablespace avoids a create+fsync per DocType table during
# reinstall — a big win under disk contention. But ROW_FORMAT=DYNAMIC must be accepted in
# the system tablespace on this MariaDB. Enable with CI_INNODB_SHARED_TABLESPACE=1; if
# reinstall then errors on table creation, unset it (off by default — zero risk).
if [ "${CI_INNODB_SHARED_TABLESPACE:-0}" = "1" ]; then
mariadb --host "$db_host" --port 3306 -u root -proot -e "SET GLOBAL innodb_file_per_table=0;"
fi
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
mariadb --host "$db_host" --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'${db_user_host}' IDENTIFIED BY 'test_frappe'"
mariadb --host "$db_host" --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
mariadb --host "$db_host" --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'${db_user_host}'"
mariadb --host "$db_host" --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
fi
if [ "$DB" == "postgres" ];then
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres;
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
# Disposable CI DB: durability off for speed (postgres fsyncs every commit by default, which
# dominates a commit-heavy suite). All reloadable, no restart. The postgres workflow runs a
# service-container DB and never calls start-db.sh, so the flags must be applied here.
echo "travis" | psql -h 127.0.0.1 -p 5432 -U postgres \
-c "ALTER SYSTEM SET synchronous_commit = 'off'" \
-c "ALTER SYSTEM SET fsync = 'off'" \
-c "ALTER SYSTEM SET full_page_writes = 'off'" \
-c "SELECT pg_reload_conf()";
fi
install_whktml() {
# Re-use the .deb if the wkhtmltopdf cache step already restored it.
if [ ! -f /tmp/wkhtmltox.deb ]; then
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
fi
sudo apt install /tmp/wkhtmltox.deb
}
install_whktml &
wkpid=$!
cd ~/frappe-bench || exit
sed -i 's/watch:/# watch:/g' Procfile
sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
run_ci_step "Get payments app" bench get-app payments --branch develop
bench get-app payments --branch develop
bench get-app erpnext "${GITHUB_WORKSPACE}"
# Opt-in: skip building erpnext's frontend assets. Server tests don't need them, but PDF
# tests (print formats) do — they pass only if the PDF renderer ignores missing assets.
# Enable with CI_SKIP_ERPNEXT_ASSETS=1 to test; if PDF tests fail, unset it.
erpnext_get_app_args=()
if [ "${CI_SKIP_ERPNEXT_ASSETS:-0}" = "1" ]; then erpnext_get_app_args=(--skip-assets); fi
run_ci_step "Get erpnext app" bench get-app erpnext "${GITHUB_WORKSPACE}" "${erpnext_get_app_args[@]}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
if [ "$TYPE" == "server" ]; then run_ci_step "Setup dev requirements" bench setup requirements --dev; fi
wait $wkpid
bench start >> ~/frappe-bench/bench_start.log 2>&1 &
bench start &>> ~/frappe-bench/bench_start.log &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes
# Under heavy concurrency, gunicorn's startup can delay redis coming up. reinstall and the
# tests need redis, so wait for it (best-effort, bounded) instead of racing — contention
# then slows the job rather than failing it.
wait_for_redis() {
local cfg=~/frappe-bench/sites/common_site_config.json
[ -f "$cfg" ] || return 0
local ports port
ports=$(python - "$cfg" <<'PY'
import json, re, sys
try:
cfg = json.load(open(sys.argv[1]))
except Exception:
sys.exit(0)
for key in ("redis_cache", "redis_queue"):
match = re.search(r":(\d+)", str(cfg.get(key, "")))
if match:
print(match.group(1))
PY
)
for port in $ports; do
local up=0
for _ in $(seq 1 120); do
if (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
exec 3>&- 3<&-; up=1
break
fi
sleep 1
done
# Fail clearly instead of letting reinstall die later on a vague socket-connection error
# when redis never bound.
[ "$up" = "1" ] || { echo "redis did not come up on port $port"; return 1; }
done
}
wait_for_redis
# Site setup: build the schema (~1000 DocTypes) into the DB. This is the single-threaded-Python
# bottleneck, but the fan-out amortises it — it runs once here in the setup job, and the test
# shards start the DB on the baked datadir instead of repeating the reinstall.
run_ci_step "Reinstall test site" bench --site test_site reinstall --yes

79
.github/helper/start-db.sh vendored Executable file
View File

@@ -0,0 +1,79 @@
#!/bin/bash
#
# Run MariaDB INSIDE the runner container, on a datadir we control. Because the datadir can be
# packaged into the bench artifact, test shards start an already-loaded server instead of
# replaying a SQL dump (the ~60s hydrate restore). Each shard gets its own copy → isolation kept.
#
# CI_DB_DATADIR picks the path:
# - setup job: /home/ci/db-data (OUTSIDE the bench, so install.sh's `rm -rf ~/frappe-bench`
# doesn't wipe it; it's moved into the bench just before packaging)
# - test shard: ~/frappe-bench/mariadb-data (where the artifact untar'd it)
#
# Idempotent: inits a fresh datadir if absent (setup), else starts on the existing one (shards).
#
set -e
ci_user="${ERPNEXT_CI_USER:-frappe}"
# Re-exec as the ci user so mariadbd and the datadir are owned consistently (root mariadbd is
# refused anyway). Mirrors install.sh's user switch.
if [ "$(id -u)" = "0" ] && [ "${SKIP_SYSTEM_SETUP:-0}" = "1" ] && [ "$ci_user" != "root" ]; then
exec su -m "$ci_user" -s /bin/bash -c \
"ERPNEXT_CI_USER='$ci_user' CI_DB_DATADIR='${CI_DB_DATADIR:-}' DB='${DB:-}' bash '$0'"
fi
# --- PostgreSQL (GitHub-hosted CI): run in-runner on a PGDATA so it bakes into the artifact,
# same idea as the mariadb datadir. Trust auth (throwaway CI) skips password setup; durability
# off for speed. Postgres is preinstalled on ubuntu-latest under /usr/lib/postgresql/<ver>/bin.
if [ "${DB:-mariadb}" = "postgres" ]; then
PG_BIN=$(ls -d /usr/lib/postgresql/*/bin 2>/dev/null | sort -V | tail -1)
[ -n "$PG_BIN" ] && export PATH="$PG_BIN:$PATH"
PGDATA="${CI_DB_DATADIR:-$HOME/frappe-bench/pgdata}"
if [ ! -d "$PGDATA/base" ]; then
initdb -D "$PGDATA" -U postgres --auth-local=trust --auth-host=trust >/dev/null
echo "host all all 127.0.0.1/32 trust" >> "$PGDATA/pg_hba.conf"
fi
pg_ctl -D "$PGDATA" -w -o "-p 5432 -c listen_addresses=127.0.0.1 -c unix_socket_directories=$PGDATA -c fsync=off -c synchronous_commit=off -c full_page_writes=off" start
echo "PostgreSQL up in-runner (pgdata=$PGDATA)"
exit 0
fi
# --- MariaDB ---
DATADIR="${CI_DB_DATADIR:-$HOME/frappe-bench/mariadb-data}"
SOCK="$DATADIR/mysqld.sock"
fresh=0
if [ ! -d "$DATADIR/mysql" ]; then
mkdir -p "$DATADIR"
mariadb-install-db --no-defaults --datadir="$DATADIR" \
--auth-root-authentication-method=normal --skip-test-db >/dev/null 2>&1
fresh=1
fi
# Throwaway-CI durability off; bind TCP 127.0.0.1:3306 so bench/install.sh connect as usual.
mariadbd --no-defaults --datadir="$DATADIR" --socket="$SOCK" --pid-file="$DATADIR/mysqld.pid" \
--port=3306 --bind-address=127.0.0.1 \
--innodb-flush-log-at-trx-commit=0 --sync-binlog=0 --skip-log-bin \
> "$HOME/mariadb.log" 2>&1 &
up=0
for _ in $(seq 1 60); do
if mariadb-admin --socket="$SOCK" ping --silent 2>/dev/null; then up=1; break; fi
sleep 1
done
# Fail loudly instead of letting the loop fall through (exit 0 of the last `sleep`) into SQL that
# would error with a vague socket-connection failure.
[ "$up" = "1" ] || { echo "mariadbd did not come up on $SOCK"; cat "$HOME/mariadb.log" 2>/dev/null; exit 1; }
if [ "$fresh" = "1" ]; then
# A fresh datadir has only a password-less root@localhost. Give it the password install.sh
# uses, plus a TCP-reachable root@127.0.0.1, so the rest of install.sh works unchanged.
mariadb --no-defaults --socket="$SOCK" -u root <<'SQL'
ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY 'root';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION;
FLUSH PRIVILEGES;
SQL
fi
echo "MariaDB up in-container (datadir=$DATADIR, fresh=$fresh)"

View File

@@ -21,7 +21,7 @@ jobs:
cache: pip
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.0
uses: pre-commit/action@v3.0.1
semgrep:
name: semgrep

View File

@@ -134,6 +134,7 @@ jobs:
# Resetup env and install apps
pgrep honcho | xargs kill
sleep 10
rm -rf ~/frappe-bench/env
bench -v setup env --python python$2
bench pip install -e ./apps/erpnext

View File

@@ -22,4 +22,4 @@ jobs:
pull-requests: write
steps:
- uses: alyf-de/po-review-action@v1.0.0
- uses: alyf-de/po-review-action@v1.1.0

View File

@@ -31,51 +31,49 @@ on:
permissions:
contents: read
packages: read
concurrency:
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true
# Shared across both jobs. Both run in the SAME CI image so the bench lives at the identical
# path (/home/ci/frappe-bench) on the setup runner and the test shards — that's what makes the
# packaged Python venv portable between them.
env:
TZ: 'Asia/Kolkata'
DEBIAN_FRONTEND: noninteractive
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
ERPNEXT_CI_USER: ci
PIP_CACHE_DIR: /home/ci/.cache/pip
npm_config_cache: /home/ci/.cache/npm
YARN_CACHE_FOLDER: /home/ci/.cache/yarn
UV_CACHE_DIR: /home/ci/.cache/uv
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
TZ: 'Asia/Kolkata'
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
strategy:
fail-fast: false
matrix:
container: [1, 2, 3, 4]
name: Python Unit Tests
services:
mysql:
image: mariadb:10.6
env:
TZ: 'Asia/Kolkata'
MARIADB_ROOT_PASSWORD: 'root'
# Disable durability guarantees that are unnecessary in a throwaway CI container.
# innodb_flush_log_at_trx_commit=0 avoids an fsync on every commit (biggest win).
# sync_binlog=0 skips binary-log syncs; innodb_doublewrite=0 skips the doublewrite buffer.
MARIADB_EXTRA_FLAGS: --innodb-flush-log-at-trx-commit=0 --sync-binlog=0 --innodb-doublewrite=0
ports:
- 3306:3306
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
# Build the bench (clone + pip + yarn + assets) and reinstall test_site ONCE, on a free
# GitHub-hosted runner, then publish the whole bench (with a DB dump baked in) as an artifact.
# The expensive, non-parallelisable work happens here exactly once instead of on every shard.
setup:
name: Build & reinstall (setup)
# Dedicated scale set (fat cpu request) so the build+reinstall runs at full speed, uncontended
# by the many thin test shards. Same CI image + /home/ci path + 127.0.0.1 DB as the shards,
# so the packaged bench (and its venv) transplants cleanly.
runs-on: erpnext-arc-setup
timeout-minutes: 40
container:
image: ghcr.io/frappe/erpnext-ci-mariadb:py3.14-node24
credentials:
username: ${{ secrets.GHCR_USERNAME || github.actor }}
password: ${{ secrets.GHCR_TOKEN || github.token }}
defaults:
run:
shell: bash
steps:
- name: Clone
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Check for valid Python & Merge Conflicts
run: |
python -m compileall -fq "${GITHUB_WORKSPACE}"
@@ -84,53 +82,17 @@ jobs:
exit 1
fi
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
# MariaDB runs in-container on a datadir OUTSIDE the bench, because install.sh's next step
# does `rm -rf ~/frappe-bench`. After the reinstall, the datadir is moved into the bench so
# it ships in the artifact — test shards then start an already-loaded server (no restore).
- name: Start DB
run: bash ${GITHUB_WORKSPACE}/.github/helper/start-db.sh
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Cache wkhtmltopdf
uses: actions/cache@v4
with:
path: /tmp/wkhtmltox.deb
key: wkhtmltox-0.12.6.1-2-jammy-amd64
SKIP_SYSTEM_SETUP: "1"
CI_DB_DATADIR: /home/ci/db-data
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
@@ -139,9 +101,88 @@ jobs:
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
DB_HOST: 127.0.0.1
DB_USER_HOST: '%'
WKHTMLTOX_DEB: /tmp/wkhtmltox.deb
SKIP_SYSTEM_SETUP: "1"
SKIP_WKHTMLTOX_SETUP: "1"
- name: Warm up test data
run: |
su -m "${ERPNEXT_CI_USER:-frappe}" -s /bin/bash <<'EOF'
cd ~/frappe-bench/
bench --site test_site run-tests --lightmode --module erpnext.tests.bootstrap_test_data
EOF
# Clean shutdown (consistent InnoDB datadir), then stage it inside the bench for packaging.
- name: Stop DB and stage datadir
run: |
mariadb-admin -h 127.0.0.1 -P 3306 -u root -proot shutdown || true
for _ in $(seq 1 30); do [ -f /home/ci/db-data/mysqld.pid ] || break; sleep 1; done
# Don't bake a dirty datadir — fail if mariadbd didn't finish stopping, rather than ship
# an inconsistent datadir the shards would have to crash-recover.
[ -f /home/ci/db-data/mysqld.pid ] && { echo "mariadbd did not shut down cleanly"; exit 1; }
mv /home/ci/db-data /home/ci/frappe-bench/mariadb-data
# Package the whole bench (apps, venv, node_modules, sites, the DB dump, and hydrate.sh)
# into one artifact for the test shards to consume.
# Single-node hand-off: stage the bench on a node-local hostPath instead of round-tripping
# through GitHub artifact storage (~60s/shard). Setup and shards share the same disk, so
# the shards just untar it locally. NOTE: this assumes one node — a shard on a different
# node could not read this path (then you'd need GitHub artifacts or an NFS/RWX volume).
- name: Stage bench on node (hostPath)
run: |
cp "${GITHUB_WORKSPACE}/.github/helper/hydrate.sh" /home/ci/frappe-bench/hydrate.sh
cp "${GITHUB_WORKSPACE}/.github/helper/start-db.sh" /home/ci/frappe-bench/start-db.sh
mkdir -p /opt/ci-bench-staging
# self-clean: drop bench tars from runs older than 2h
find /opt/ci-bench-staging -maxdepth 1 -name '*.tar.gz' -mmin +120 -delete 2>/dev/null || true
# Exclude .git/node_modules; the mariadb-data datadir IS included (the pre-loaded DB).
tar czpf "/opt/ci-bench-staging/${GITHUB_RUN_ID}.tar.gz" -C /home/ci \
--exclude='.git' --exclude='node_modules' frappe-bench
ls -lh "/opt/ci-bench-staging/${GITHUB_RUN_ID}.tar.gz"
# Fan-out: each shard downloads the bench, untars it, starts MariaDB on the baked datadir, and
# runs its slice of the suite. No clone, no build, no reinstall, no DB dump restore on the shards.
test:
name: Python Unit Tests
needs: setup
runs-on: erpnext-arc
timeout-minutes: 60
container:
image: ghcr.io/frappe/erpnext-ci-mariadb:py3.14-node24
credentials:
username: ${{ secrets.GHCR_USERNAME || github.actor }}
password: ${{ secrets.GHCR_TOKEN || github.token }}
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
container: [1, 2, 3, 4]
steps:
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
# Read the bench straight from the node-local hostPath the setup job staged it on — no
# GitHub download. -p preserves the ci (uid 1001) ownership so bench runs as ci cleanly.
- name: Untar bench from node (hostPath)
run: |
tar xzpf "/opt/ci-bench-staging/${GITHUB_RUN_ID}.tar.gz" -C /home/ci
ls -ld /home/ci/frappe-bench
- name: Hydrate (start DB on baked datadir + bench start)
run: bash /home/ci/frappe-bench/hydrate.sh
env:
DB_HOST: 127.0.0.1
SKIP_SYSTEM_SETUP: "1"
- name: Run Tests
run: |
su -m "${ERPNEXT_CI_USER:-frappe}" -s /bin/bash <<'EOF'
cd ~/frappe-bench/
coverage_flag=""
if [ "$WITH_COVERAGE" = "true" ]; then coverage_flag="--with-coverage"; fi
@@ -149,10 +190,10 @@ jobs:
--total-builds ${{ strategy.job-total }} \
--build-number ${{ matrix.container }} \
$coverage_flag
EOF
env:
TYPE: server
- name: Show bench output
if: ${{ always() }}
run: cat ~/frappe-bench/bench_start.log || true
@@ -162,11 +203,11 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml
path: /home/ci/frappe-bench/sites/coverage.xml
coverage:
name: Coverage Wrap Up
needs: test
needs: [test]
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
steps:

View File

@@ -48,7 +48,6 @@ repos:
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$

View File

@@ -14,35 +14,35 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/vite": "^4.3.0",
"@tailwindcss/vite": "^4.3.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.24",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-react": "^6.0.3",
"chrono-node": "^2.9.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.20",
"frappe-react-sdk": "^1.15.0",
"frappe-react-sdk": "^1.17.0",
"fuse.js": "^7.3.0",
"jotai": "^2.20.0",
"jotai-family": "^1.0.1",
"jotai": "^2.20.1",
"jotai-family": "^1.0.2",
"lodash.isplainobject": "^4.0.6",
"lucide-react": "^1.14.0",
"radix-ui": "^1.4.3",
"react": "^19.2.6",
"radix-ui": "^1.6.1",
"react": "^19.2.7",
"react-currency-input-field": "^4.0.5",
"react-day-picker": "9.14.0",
"react-dom": "^19.2.6",
"react-dom": "^19.2.7",
"react-dropzone": "^15.0.0",
"react-hook-form": "^7.75.0",
"react-hotkeys-hook": "^5.3.2",
"react-markdown": "^10.1.0",
"react-router": "^7.15.0",
"react-router-dom": "^7.15.0",
"react-router": "^8.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"safe-expr-eval": "^1.0.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.3.0",
@@ -51,15 +51,15 @@
"vite": "^8.0.16"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@eslint/js": "^9.39.4",
"@types/node": "^25.3.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0"
"typescript-eslint": "^8.62.1"
}
}

View File

@@ -1,5 +1,5 @@
import { lazy, useEffect } from 'react'
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
import { BrowserRouter, Navigate, Route, Routes } from 'react-router'
import { FrappeProvider } from 'frappe-react-sdk'
import { Toaster } from '@/components/ui/sonner'
import BankReconciliation from '@/pages/BankReconciliation'

View File

@@ -2,7 +2,6 @@ import { useAtomValue } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import { Paragraph } from "@/components/ui/typography"
import type { ColumnDef } from "@tanstack/react-table"
import { useCallback, useMemo, useState } from "react"
import { useFrappeGetCall, useFrappePostCall, useSWRConfig } from "frappe-react-sdk"
@@ -26,6 +25,7 @@ import { Form } from "@/components/ui/form"
import { useForm } from "react-hook-form"
import { DateField } from "@/components/ui/form-elements"
import { Empty, EmptyMedia, EmptyHeader, EmptyTitle, EmptyDescription } from "@/components/ui/empty"
import MarkdownRenderer from "@/components/ui/markdown"
const BankClearanceSummary = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
@@ -203,14 +203,14 @@ const BankClearanceSummaryView = () => {
[accountCurrency, bankAccount, companyID, mutate, onCopy],
)
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">
<div>
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("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>`])
}} />
</Paragraph>
<span className="text-p-sm">
<MarkdownRenderer content={content} />
</span>
</div>
{error && <ErrorBanner error={error} />}

View File

@@ -18,6 +18,7 @@ import { useMultiFileUploadProgress } from "@/hooks/useMultiFileUploadProgress"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Checkbox } from "@/components/ui/checkbox"
import { ArrowDownRight, ArrowUpRight, Plus, Trash2 } from "lucide-react"
import { evaluateAmountFormula } from "@/lib/amountFormula"
import { flt, formatCurrency } from "@/lib/numbers"
import { cn } from "@/lib/utils"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
@@ -215,38 +216,13 @@ const BankEntryForm = ({ selectedTransaction }: { selectedTransaction: Unreconci
})
} else {
/**
* The debit and credit amounts can also be expressions - like "transaction_amount * 0.5"
* So we need to compute the value of the expression
* We can use the eval function to do this. But we need to expose certain variables to the expression.
* One of them is transaction_amount which is the unallocated amount of the selected transaction
* @param expression - The expression to compute
* @returns The computed value
*/
const computeExpression = (expression: string) => {
const script = `
const transaction_amount = ${selectedTransaction.unallocated_amount ?? 0}
${expression};
`
let value = 0;
try {
value = window.eval(script);
} catch (error: unknown) {
console.error(error);
value = 0;
}
return value;
}
const transactionAmount = selectedTransaction.unallocated_amount ?? 0
if (!acc?.debit && !acc?.credit) {
hasTotallyEmptyRowEarlier = true;
}
const computedDebit = acc?.debit ? flt(computeExpression(acc.debit), 2) : 0
const computedCredit = acc?.credit ? flt(computeExpression(acc.credit), 2) : 0
const computedDebit = acc?.debit ? flt(evaluateAmountFormula(acc.debit, transactionAmount), 2) : 0
const computedCredit = acc?.credit ? flt(evaluateAmountFormula(acc.credit, transactionAmount), 2) : 0
totalDebits = flt(totalDebits + computedDebit, 2)
totalCredits = flt(totalCredits + computedCredit, 2)

View File

@@ -2,7 +2,6 @@ import { useAtomValue } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import { Paragraph } from "@/components/ui/typography"
import { useCallback, useMemo } from "react"
import type { ColumnDef } from "@tanstack/react-table"
import { useFrappeGetCall } from "frappe-react-sdk"
@@ -19,6 +18,7 @@ import _ from "@/lib/translate"
import { toast } from "sonner"
import { useCopyToClipboard } from "usehooks-ts"
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import MarkdownRenderer from "@/components/ui/markdown"
const BankReconciliationStatement = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
@@ -189,14 +189,14 @@ const BankReconciliationStatementView = () => {
return data.message.result.filter((row: BankClearanceSummaryEntry) => Boolean(row.payment_entry))
}, [data])
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">
<div>
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("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>`])
}} />
</Paragraph>
<span className="text-p-sm">
<MarkdownRenderer content={content} />
</span>
</div>
{error && <ErrorBanner error={error} />}

View File

@@ -1,7 +1,6 @@
import { useAtomValue, useSetAtom } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, bankRecUnreconcileModalAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { Paragraph } from "@/components/ui/typography"
import { formatDate } from "@/lib/date"
import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view"
import { formatCurrency, getCurrencyFormatInfo } from "@/lib/numbers"
@@ -23,6 +22,7 @@ import { useCallback, useMemo, useState } from "react"
import { Link } from "react-router"
import { Empty, EmptyTitle, EmptyHeader, EmptyMedia, EmptyDescription, EmptyContent } from "@/components/ui/empty"
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group"
import MarkdownRenderer from "@/components/ui/markdown"
const BankTransactions = () => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
@@ -243,14 +243,14 @@ const BankTransactionListView = () => {
}, [data, search, amountFilter, typeFilter, status])
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">
<div className="flex gap-2 justify-between items-center">
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("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>`])
}} />
</Paragraph>
<span className="text-p-sm">
<MarkdownRenderer content={content} />
</span>
<Button size='md' variant='subtle' asChild>
<Link to="/statement-importer">

View File

@@ -2,7 +2,6 @@ import { useAtomValue } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import { Paragraph } from "@/components/ui/typography"
import type { ColumnDef } from "@tanstack/react-table"
import { useCallback, useMemo } from "react"
import { useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk"
@@ -18,6 +17,7 @@ import { PartyPopper } from "lucide-react"
import ErrorBanner from "@/components/ui/error-banner"
import _ from "@/lib/translate"
import { Empty, EmptyTitle, EmptyDescription, EmptyMedia, EmptyHeader } from "@/components/ui/empty"
import MarkdownRenderer from "@/components/ui/markdown"
const IncorrectlyClearedEntries = () => {
const companyID = useCurrentCompany()
@@ -177,22 +177,22 @@ const IncorrectlyClearedEntriesView = () => {
[accountCurrency, onClearClick],
)
const content = _("This report shows all entries in the system where the <strong>clearance date is before the posting date</strong> which is incorrect.")
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">
<div>
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("This report shows all entries in the system where the <strong>clearance date is before the posting date</strong> which is incorrect.")
}} />
<span className="text-p-sm">
<MarkdownRenderer content={content} />
<br />
{data && data.message.result.length > 0 && <span>
<span dangerouslySetInnerHTML={{
__html: _("Entries below have a posting date after {0} but the clearance date is before {1}.", [`<strong>${formattedToDate}</strong>`, `<strong>${formattedToDate}</strong>`])
}} />
<MarkdownRenderer content={entriesContent} />
<br />
{_("You can reset the clearing dates of these entries here.")}
</span>}
</Paragraph>
</span>
</div>
{error && <ErrorBanner error={error} />}

View File

@@ -11,6 +11,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { H4, Paragraph } from "@/components/ui/typography"
import { today } from "@/lib/date"
import { evaluateAmountFormula } from "@/lib/amountFormula"
import _ from "@/lib/translate"
import { cn } from "@/lib/utils"
import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule"
@@ -445,11 +446,10 @@ const AmountFormulaRenderer = ({ value }: { value?: string }) => {
// If it's a string and cannot be a number, then show it as a formula
if (isNaN(Number(value))) {
let calculatedValue = "";
try {
calculatedValue = window.eval(`const transaction_amount = 200; ${value}`);
calculatedValue = String(evaluateAmountFormula(value ?? "", 200));
} catch (error: unknown) {
console.error(error);
calculatedValue = "Error";

View File

@@ -14,7 +14,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { useFrappeEventListener, useFrappePostCall } from 'frappe-react-sdk'
import { toast } from 'sonner'
import ErrorBanner from '@/components/ui/error-banner'
import { Link, useNavigate } from 'react-router-dom'
import { Link, useNavigate } from 'react-router'
import { useMemo, useState } from 'react'
import { Progress } from '@/components/ui/progress'
import { useSetAtom } from 'jotai'

View File

@@ -0,0 +1,26 @@
import { Parser } from 'safe-expr-eval'
const parser = new Parser()
const PLAIN_NUMBER_PATTERN = /^-?\d+(\.\d+)?$/
export function evaluateAmountFormula(expression: string, transactionAmount: number): number {
const trimmed = expression.trim()
if (!trimmed) {
return 0
}
if (PLAIN_NUMBER_PATTERN.test(trimmed)) {
return Number(trimmed)
}
try {
const result = parser.parse(trimmed).evaluate({ transaction_amount: transactionAmount })
if (typeof result !== 'number' || !Number.isFinite(result)) {
return 0
}
return result
} catch {
return 0
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.23.1"
__version__ = "16.29.0"
def get_default_company(user=None):

View File

@@ -120,6 +120,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:
@@ -208,6 +209,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()

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

@@ -307,6 +307,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

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, nowdate
class OverlapError(frappe.ValidationError):
@@ -36,8 +37,20 @@ class AccountingPeriod(Document):
# end: auto-generated types
def validate(self):
self.validate_dates()
self.validate_overlap()
def validate_dates(self):
if getdate(self.start_date) > getdate(self.end_date):
frappe.throw(_("Start Date cannot be after End Date"))
if getdate(self.end_date) > getdate(nowdate()):
frappe.throw(
_(
"Accounting Period cannot be created for a future date. End Date {0} is after today."
).format(frappe.bold(frappe.format(self.end_date, "Date")))
)
def before_insert(self):
self.bootstrap_doctypes_for_closing()

View File

@@ -3,7 +3,7 @@
import unittest
import frappe
from frappe.utils import add_months, nowdate
from frappe.utils import nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import (
ClosedAccountingPeriod,
@@ -94,7 +94,7 @@ def create_accounting_period(**args):
accounting_period = frappe.new_doc("Accounting Period")
accounting_period.start_date = args.start_date or nowdate()
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.end_date = args.end_date or nowdate()
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})

View File

@@ -10,7 +10,7 @@ frappe.ui.form.on("Accounts Settings", {
},
};
});
if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm);
if (!frm.naming_controller) frm.naming_controller = new frappe.ui.NamingSeriesController(frm);
frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm));
},

View File

@@ -86,6 +86,7 @@
"period_closing_settings_section",
"ignore_account_closing_balance",
"use_legacy_controller_for_pcv",
"pcv_job_timeout",
"column_break_25",
"reports_tab",
"remarks_section",
@@ -611,6 +612,14 @@
"fieldtype": "Check",
"label": "Use legacy controller for Period Closing Voucher"
},
{
"default": "3600",
"depends_on": "eval: !doc.use_legacy_controller_for_pcv",
"description": "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher",
"fieldname": "pcv_job_timeout",
"fieldtype": "Int",
"label": "PCV Job Timeout (seconds)"
},
{
"description": "Users with this role will be notified if the asset depreciation gets failed",
"fieldname": "role_to_notify_on_depreciation_failure",
@@ -748,7 +757,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-06-15 18:26:50.778723",
"modified": "2026-06-24 12:59:41.868865",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -90,6 +90,7 @@ class AccountsSettings(Document):
make_payment_via_journal_entry: DF.Check
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
pcv_job_timeout: DF.Int
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int

View File

@@ -22,11 +22,13 @@ class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
"""
def setUp(self):
self.create_company()
self.create_usd_receivable_account()
self.create_usd_payable_account()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.supplier = "_Test Supplier"
self.item = "_Test Item"
self.cash = "Cash - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
self.creditors_usd = "_Test Payable USD - _TC"
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
"""

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

View File

@@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
frappe.ui.form.on("Bank Guarantee", {
setup: function (frm) {
frm.set_query("reference_doctype", function () {
return {
filters: {
name: ["in", ["Sales Order", "Purchase Order"]],
},
};
});
frm.set_query("bank_account", function () {
return {
filters: {

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"autoname": "ACC-BG-.YYYY.-.#####",
"creation": "2016-12-17 10:43:35.731631",
"doctype": "DocType",
@@ -50,8 +51,7 @@
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType",
"read_only": 1
"options": "DocType"
},
{
"fieldname": "reference_docname",
@@ -60,14 +60,14 @@
"options": "reference_doctype"
},
{
"depends_on": "eval: doc.bg_type == \"Receiving\"",
"depends_on": "eval: doc.reference_doctype == \"Sales Order\"",
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer"
},
{
"depends_on": "eval: doc.bg_type == \"Providing\"",
"depends_on": "eval: doc.reference_doctype == \"Purchase Order\"",
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
@@ -218,10 +218,11 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-08-29 11:52:33.550847",
"modified": "2026-05-25 18:12:10.768835",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Guarantee",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{

View File

@@ -64,7 +64,7 @@ def get_bank_transactions(
filters.append(["date", "<=", to_date])
if from_date:
filters.append(["date", ">=", from_date])
transactions = frappe.get_all(
transactions = frappe.get_list(
"Bank Transaction",
fields=[
"date",
@@ -93,6 +93,7 @@ def get_bank_transactions(
@frappe.whitelist()
def get_account_balance(bank_account: str, till_date: str | date, company: str):
# returns account balance till the specified date
frappe.has_permission("Bank Account", "read", bank_account, throw=True)
account = frappe.db.get_value("Bank Account", bank_account, "account")
filters = frappe._dict(
{

View File

@@ -17,9 +17,10 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.bank = "HDFC - _TC"
self.debit_to = "Debtors - _TC"
bank_dt = qb.DocType("Bank")
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
self.create_bank_account()

View File

@@ -54,7 +54,6 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Closing Balance",
"non_negative": 1,
"options": "currency"
},
{
@@ -191,7 +190,7 @@
"grid_page_length": 50,
"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

@@ -557,7 +557,7 @@ class BankStatementImportLog(Document):
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
)
@@ -829,7 +829,9 @@ 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")
float_amount = get_float_amount(amount)
# 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:
@@ -932,14 +934,18 @@ def extract_pdf_tables(content: bytes, password: str | None = None) -> list[dict
from pypdf import PdfReader
reader = PdfReader(io.BytesIO(content))
if reader.is_encrypted and (not password or not reader.decrypt(password)):
frappe.throw(
_(
"This PDF is password protected. Please set the correct statement password on the"
" Bank Account and try again."
),
title=_("Password Required"),
)
if reader.is_encrypted:
# Try opening the PDF with a password - if no password is provided, try with a blank password
if not password:
password = ""
if not reader.decrypt(password):
frappe.throw(
_(
"This PDF is password protected. Please set the correct statement password on the"
" Bank Account and try again."
),
title=_("Password Required"),
)
text_settings = {"vertical_strategy": "text", "horizontal_strategy": "text"}
tables = []

View File

@@ -26,9 +26,9 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestBankStatementImportLog(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.bank = "HDFC - _TC"
bank_dt = qb.DocType("Bank")
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
self.create_bank_account()

View File

@@ -104,6 +104,36 @@ class TestBankTransaction(ERPNextTestSuite):
self.assertEqual(bank_transaction.unallocated_amount, 1700)
self.assertEqual(bank_transaction.payment_entries, [])
# Amending a reconciled payment entry must not carry over its clearance date
def test_clearance_date_cleared_on_amend(self):
bank_transaction = frappe.get_doc(
"Bank Transaction",
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
vouchers = json.dumps(
[
{
"payment_doctype": "Payment Entry",
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers)
self.assertTrue(frappe.db.get_value("Payment Entry", payment.name, "clearance_date"))
payment.reload()
payment.cancel()
amended = frappe.copy_doc(payment)
amended.amended_from = payment.name
amended.docstatus = 0
amended.insert()
self.assertFalse(amended.clearance_date)
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc(

View File

@@ -9,6 +9,48 @@ from frappe.model.document import Document
from erpnext.accounts.doctype.bank_transaction.bank_transaction import BankTransaction
PLAIN_NUMBER_PATTERN = re.compile(r"^-?\d+(\.\d+)?$")
# Tokens accepted by safe-expr-eval on the frontend (must stay in sync).
ALLOWED_FORMULA_TOKEN = re.compile(r"\s+|transaction_amount|\d+(?:\.\d+)?|[+\-*/%^()]")
PYTHON_ONLY_OPERATORS = ("**", "//")
def _is_expr_eval_formula(formula: str) -> bool:
position = 0
while position < len(formula):
match = ALLOWED_FORMULA_TOKEN.match(formula, position)
if not match:
return False
position = match.end()
return formula.count("(") == formula.count(")")
def validate_amount_formula(formula: str) -> None:
if not formula:
return
stripped = formula.strip()
if PLAIN_NUMBER_PATTERN.match(stripped):
return
if any(operator in stripped for operator in PYTHON_ONLY_OPERATORS):
frappe.throw(_("Invalid debit/credit formula: {0}").format(formula))
if not _is_expr_eval_formula(stripped):
frappe.throw(_("Invalid debit/credit formula: {0}").format(formula))
# expr-eval uses ^ for exponentiation; translate for a smoke-test evaluation only.
python_formula = stripped.replace("^", "**")
try:
result = frappe.safe_eval(python_formula, eval_globals=None, eval_locals={"transaction_amount": 1})
except Exception:
frappe.throw(_("Invalid debit/credit formula: {0}").format(formula))
if not isinstance(result, (int | float)):
frappe.throw(_("Invalid debit/credit formula: {0}").format(formula))
class BankTransactionRule(Document):
# begin: auto-generated types
@@ -86,6 +128,11 @@ class BankTransactionRule(Document):
frappe.throw(
_("The last account row must not have any debit or credit amounts set.")
)
else:
if account.debit:
validate_amount_formula(account.debit)
if account.credit:
validate_amount_formula(account.credit)
# Validate regex
for rule in self.description_rules:

View File

@@ -11,9 +11,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestBankTransactionRule(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.bank = "HDFC - _TC"
self.debit_to = "Debtors - _TC"
self.cash = "Cash - _TC"
bank_dt = qb.DocType("Bank")
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
self.create_bank_account()
@@ -229,3 +231,45 @@ class TestBankTransactionRule(ERPNextTestSuite, AccountsTestMixin):
doc = self._rule("bad_rx", [{"check": "Regex", "value": "["}])
with self.assertRaises(ValidationError):
doc.insert()
def _multiple_accounts_rule(self, prefix: str, accounts, **fields):
return self._rule(
prefix,
[{"check": "Contains", "value": "x"}],
classify_as="Bank Entry",
bank_entry_type="Multiple Accounts",
accounts=accounts,
**fields,
)
def test_validate_bank_entry_multiple_valid_amount_formulas(self):
doc = self._multiple_accounts_rule(
"be_formula",
accounts=[
{"account": self.bank, "debit": "200", "credit": ""},
{"account": self.cash, "debit": "", "credit": "transaction_amount * 0.25"},
{"account": self.cash, "debit": "", "credit": ""},
],
)
doc.insert()
self.assertTrue(doc.name)
def test_validate_bank_entry_multiple_invalid_amount_formulas(self):
malicious_formulas = [
"__import__('os')",
"eval('1+1')",
"open('/etc/passwd')",
"transaction_amount ** 2",
"transaction_amount // 2",
]
for formula in malicious_formulas:
with self.subTest(formula=formula):
doc = self._multiple_accounts_rule(
"be_bad_formula",
accounts=[
{"account": self.bank, "debit": formula, "credit": ""},
{"account": self.cash, "debit": "", "credit": ""},
],
)
with self.assertRaises(ValidationError):
doc.insert()

View File

@@ -136,6 +136,9 @@ function set_total_budget_amount(frm) {
function toggle_distribution_fields(frm) {
const grid = frm.fields_dict.budget_distribution.grid;
frm.set_df_property("budget_distribution", "cannot_add_rows", true);
frm.set_df_property("budget_distribution", "cannot_delete_rows", true);
["amount", "percent"].forEach((field) => {
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
});

View File

@@ -159,9 +159,9 @@ class Budget(Document):
frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
elif account_details.report_type != "Profit and Loss":
frappe.throw(
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
self.account
)
_(
"Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense"
).format(self.account)
)
def set_null_value(self):
@@ -355,8 +355,8 @@ class Budget(Document):
if self.should_regenerate_budget_distribution():
return
total_amount = sum(d.amount for d in self.budget_distribution)
total_percent = sum(d.percent for d in self.budget_distribution)
total_amount = sum(flt(d.amount) for d in self.budget_distribution)
total_percent = sum(flt(d.percent) for d in self.budget_distribution)
if flt(abs(total_amount - self.budget_amount), 2) > 0.10:
frappe.throw(

View File

@@ -18,6 +18,7 @@
"in_list_view": 1,
"label": "Start Date",
"read_only": 1,
"reqd": 1,
"search_index": 1
},
{
@@ -25,26 +26,29 @@
"fieldtype": "Date",
"in_list_view": 1,
"label": "End Date",
"read_only": 1
"read_only": 1,
"reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount"
"label": "Amount",
"reqd": 1
},
{
"fieldname": "percent",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Percent"
"label": "Percent",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-11-03 13:18:28.398198",
"modified": "2026-06-18 11:23:17.669733",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget Distribution",

View File

@@ -15,12 +15,12 @@ class BudgetDistribution(Document):
from frappe.types import DF
amount: DF.Currency
end_date: DF.Date | None
end_date: DF.Date
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
percent: DF.Percent
start_date: DF.Date | None
start_date: DF.Date
# end: auto-generated types
pass

View File

@@ -75,7 +75,10 @@ def validate_company(company):
@frappe.whitelist()
def import_coa(file_name, company):
frappe.only_for("Accounts Manager")
# delete existing data for accounts
frappe.has_permission("Company", "write", company, throw=True)
unset_existing_data(company)
# create accounts
@@ -215,6 +218,7 @@ def build_forest(data):
for row in data:
account_name, parent_account, account_number, parent_account_number = row[0:4]
if account_number:
account_number = cstr(account_number).strip()
account_name = f"{account_number} - {account_name}"
if parent_account_number:
parent_account_number = cstr(parent_account_number).strip()
@@ -451,6 +455,7 @@ def unset_existing_data(company):
fieldnames = get_linked_fields("Account").get("Company", {}).get("fieldname", [])
linked = [{"fieldname": name} for name in fieldnames]
update_values = {d.get("fieldname"): "" for d in linked}
frappe.db.set_value("Company", company, update_values, update_values)
# remove accounts data from various doctypes
@@ -462,8 +467,7 @@ def unset_existing_data(company):
"Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template",
]:
dt = frappe.qb.DocType(doctype)
frappe.qb.from_(dt).where(dt.company == company).delete().run()
frappe.get_query(doctype, delete=True, filters={"company": company}, ignore_permissions=False).run()
def set_default_accounts(company):

View File

@@ -169,23 +169,10 @@ frappe.ui.form.on("Dunning", {
},
get_dunning_letter_text: function (frm) {
if (frm.doc.dunning_type) {
frappe.call({
method: "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
args: {
dunning_type: frm.doc.dunning_type,
language: frm.doc.language,
doc: frm.doc,
},
callback: function (r) {
if (r.message) {
frm.set_value("body_text", r.message.body_text);
frm.set_value("closing_text", r.message.closing_text);
frm.set_value("language", r.message.language);
} else {
frm.set_value("body_text", "");
frm.set_value("closing_text", "");
}
},
frm.call("get_dunning_letter_text").then((r) => {
if (!r.exc) {
frm.refresh_fields();
}
});
}
},

View File

@@ -163,6 +163,46 @@ class Dunning(AccountsController):
"Serial and Batch Bundle",
]
@frappe.whitelist()
def get_dunning_letter_text(self):
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if not self.dunning_type:
return
filters = {"parent": self.dunning_type, "is_default_language": 1}
if self.language:
filters.pop("is_default_language")
filters["language"] = self.language
letter_text = frappe.db.get_value(DOCTYPE, filters, FIELDS, as_dict=True)
if not letter_text:
msg = (
_("Dunning Letter for Dunning Type {0} in language '{1}' not found.").format(
frappe.bold(self.dunning_type), frappe.bold(self.language)
)
if self.language
else _("Dunning Letter for Dunning Type {0} not found.").format(
frappe.bold(self.dunning_type)
)
)
frappe.msgprint(msg, alert=True, indicator="yellow")
self.body_text = (
frappe.render_template(letter_text.body_text, self.as_dict(), restrict_globals=True)
if letter_text
else None
)
self.closing_text = (
frappe.render_template(letter_text.closing_text, self.as_dict(), restrict_globals=True)
if letter_text
else None
)
self.language = letter_text.language if letter_text else self.language
def update_linked_dunnings(doc, previous_outstanding_amount):
if (
@@ -241,35 +281,3 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
& (overdue_payment.sales_invoice == sales_invoice)
)
).run(as_dict=True)
@frappe.whitelist()
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if isinstance(doc, str):
doc = json.loads(doc)
if not language:
language = doc.get("language")
letter_text = None
if language:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
)
if not letter_text:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
)
if not letter_text:
return {}
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}

View File

@@ -3,7 +3,10 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import comma_and
from frappe.utils.jinja import validate_template
class DunningType(Document):
@@ -30,3 +33,134 @@ class DunningType(Document):
def autoname(self):
company_abbr = frappe.get_value("Company", self.company, "abbr")
self.name = f"{self.dunning_type} - {company_abbr}"
def validate(self):
self.validate_dunning_letter_text()
self.validate_income_account()
self.validate_cost_center()
self.set_default_dunning_type()
def validate_dunning_letter_text(self):
self.validate_languages()
self.validate_is_default_language()
self.validate_dunning_letter_text_templates()
def validate_income_account(self):
if not self.income_account:
return
account = frappe.get_cached_doc("Account", self.income_account)
msg = []
if account.company != self.company:
msg.append(
_(
"{0} doesn't belong to Company {1}. Please select an Income Account that belongs to Company {1}."
).format(frappe.bold(self.income_account), frappe.bold(self.company))
)
if account.disabled:
msg.append(
_("{0} is disabled. Please select a valid Income Account.").format(
frappe.bold(self.income_account)
)
)
if account.root_type != "Income":
msg.append(
_("{0} is not an Income Account. Please select a valid Income Account.").format(
frappe.bold(self.income_account)
)
)
if account.is_group:
msg.append(
_("{0} is a group account. Please select a non-group Income Account.").format(
frappe.bold(self.income_account)
)
)
if msg:
frappe.msgprint(
msg,
title=_("Income Account Validation Error"),
as_list=True,
raise_exception=frappe.ValidationError,
)
def validate_cost_center(self):
if not self.cost_center:
return
cost_center = frappe.get_cached_doc("Cost Center", self.cost_center)
msg = []
if cost_center.company != self.company:
msg.append(
_(
"{0} doesn't belong to Company {1}. Please select a Cost Center that belongs to Company {1}."
).format(frappe.bold(self.cost_center), frappe.bold(self.company))
)
if cost_center.disabled:
msg.append(
_("{0} is disabled. Please select an enabled Cost Center.").format(
frappe.bold(self.cost_center)
)
)
if cost_center.is_group:
msg.append(
_("{0} is a group Cost Center. Please select a non-group Cost Center.").format(
frappe.bold(self.cost_center)
)
)
if msg:
frappe.msgprint(
msg,
title=_("Cost Center Validation Error"),
as_list=True,
raise_exception=frappe.ValidationError,
)
def validate_languages(self):
languages = [d.language for d in self.dunning_letter_text]
if len(languages) == len(set(languages)):
return
frappe.throw(_("Duplicate languages found on Dunning Letter Text. Keep only one of them."))
def validate_is_default_language(self):
is_default_language_list = [
d.language for d in self.dunning_letter_text if d.is_default_language == 1
]
if len(is_default_language_list) <= 1:
return
frappe.throw(
_("{0} languages are marked as default languages. Please select only one of them.").format(
comma_and(is_default_language_list, add_quotes=True)
)
)
def validate_dunning_letter_text_templates(self):
for d in self.dunning_letter_text:
if d.body_text:
validate_template(d.body_text, restrict_globals=True)
if d.closing_text:
validate_template(d.closing_text, restrict_globals=True)
def set_default_dunning_type(self):
if self.is_default != 1:
return
frappe.db.set_value(
"Dunning Type",
{"company": self.company, "is_default": 1, "name": ["!=", self.name]},
"is_default",
0,
)

View File

@@ -1,10 +1,200 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
import frappe
from erpnext.tests.utils import ERPNextTestSuite
def make_dunning_type(dunning_type, company="_Test Company", **kwargs):
doc = frappe.new_doc("Dunning Type")
doc.dunning_type = dunning_type
doc.company = company
doc.dunning_fee = kwargs.get("dunning_fee", 100)
doc.rate_of_interest = kwargs.get("rate_of_interest", 5)
doc.is_default = kwargs.get("is_default", 0)
if "income_account" in kwargs:
doc.income_account = kwargs["income_account"]
elif kwargs.get("income_account") is not False:
doc.income_account = "Sales - _TC" if company == "_Test Company" else "Sales - _TC1"
if "cost_center" in kwargs:
doc.cost_center = kwargs["cost_center"]
elif kwargs.get("cost_center") is not False:
doc.cost_center = "Main - _TC" if company == "_Test Company" else "Main - _TC1"
for row in kwargs.get("dunning_letter_text", [{"language": "en", "body_text": "Test body"}]):
doc.append("dunning_letter_text", row)
return doc
class TestDunningType(ERPNextTestSuite):
pass
def test_income_account_must_belong_to_company(self):
doc = make_dunning_type("_Test Dunning Wrong Company Account", income_account="Sales - _TC1")
self.assertRaisesRegex(frappe.ValidationError, "doesn't belong to Company", doc.insert)
def test_income_account_must_not_be_disabled(self):
disabled_account = frappe.get_doc(
{
"doctype": "Account",
"account_name": "_Test Disabled Income Account",
"parent_account": "Direct Income - _TC",
"company": "_Test Company",
"account_type": "Income Account",
"disabled": 1,
}
).insert()
doc = make_dunning_type("_Test Dunning Disabled Account", income_account=disabled_account.name)
self.assertRaisesRegex(frappe.ValidationError, "is disabled", doc.insert)
def test_income_account_must_be_income_type(self):
doc = make_dunning_type("_Test Dunning Non Income Account", income_account="Debtors - _TC")
self.assertRaisesRegex(frappe.ValidationError, "is not an Income Account", doc.insert)
def test_income_account_must_not_be_group(self):
doc = make_dunning_type("_Test Dunning Group Account", income_account="Income - _TC")
self.assertRaisesRegex(frappe.ValidationError, "is a group account", doc.insert)
def test_income_account_is_optional(self):
doc = make_dunning_type("_Test Dunning No Income Account", income_account=False)
doc.insert()
self.assertFalse(doc.income_account)
def test_valid_income_account_passes(self):
doc = make_dunning_type("_Test Dunning Valid Income Account", income_account="Sales - _TC")
doc.insert()
self.assertEqual(doc.income_account, "Sales - _TC")
def test_cost_center_must_belong_to_company(self):
doc = make_dunning_type("_Test Dunning Wrong Company CC", cost_center="Main - _TC1")
self.assertRaisesRegex(frappe.ValidationError, "doesn't belong to Company", doc.insert)
def test_cost_center_must_not_be_disabled(self):
disabled_cc = frappe.get_doc(
{
"doctype": "Cost Center",
"cost_center_name": "_Test Disabled Cost Center",
"parent_cost_center": "_Test Company - _TC",
"company": "_Test Company",
"disabled": 1,
}
).insert()
doc = make_dunning_type("_Test Dunning Disabled CC", cost_center=disabled_cc.name)
self.assertRaisesRegex(frappe.ValidationError, "is disabled", doc.insert)
def test_cost_center_must_not_be_group(self):
doc = make_dunning_type("_Test Dunning Group CC", cost_center="_Test Company - _TC")
self.assertRaisesRegex(frappe.ValidationError, "is a group Cost Center", doc.insert)
def test_cost_center_is_optional(self):
doc = make_dunning_type("_Test Dunning No CC", cost_center=False)
doc.insert()
self.assertFalse(doc.cost_center)
def test_valid_cost_center_passes(self):
doc = make_dunning_type("_Test Dunning Valid CC", cost_center="Main - _TC")
doc.insert()
self.assertEqual(doc.cost_center, "Main - _TC")
def test_duplicate_languages_not_allowed(self):
doc = make_dunning_type(
"_Test Dunning Duplicate Language",
dunning_letter_text=[
{"language": "en", "body_text": "Body one"},
{"language": "en", "body_text": "Body two"},
],
)
self.assertRaisesRegex(frappe.ValidationError, "Duplicate languages found", doc.insert)
def test_unique_languages_allowed(self):
doc = make_dunning_type(
"_Test Dunning Unique Languages",
dunning_letter_text=[
{"language": "en", "body_text": "Body one"},
{"language": "de", "body_text": "Body two"},
],
)
doc.insert()
self.assertEqual(len(doc.dunning_letter_text), 2)
def test_only_one_default_language_allowed(self):
doc = make_dunning_type(
"_Test Dunning Multiple Default Language",
dunning_letter_text=[
{"language": "en", "body_text": "Body one", "is_default_language": 1},
{"language": "de", "body_text": "Body two", "is_default_language": 1},
],
)
self.assertRaisesRegex(
frappe.ValidationError, "languages are marked as default languages", doc.insert
)
def test_single_default_language_allowed(self):
doc = make_dunning_type(
"_Test Dunning Single Default Language",
dunning_letter_text=[
{"language": "en", "body_text": "Body one", "is_default_language": 1},
{"language": "de", "body_text": "Body two", "is_default_language": 0},
],
)
doc.insert()
self.assertEqual(doc.dunning_letter_text[0].is_default_language, 1)
def test_invalid_jinja_template_in_body_text_raises(self):
doc = make_dunning_type(
"_Test Dunning Invalid Body Template",
dunning_letter_text=[{"language": "en", "body_text": "{{ unclosed"}],
)
self.assertRaisesRegex(frappe.ValidationError, "Syntax error in template", doc.insert)
def test_invalid_jinja_template_in_closing_text_raises(self):
doc = make_dunning_type(
"_Test Dunning Invalid Closing Template",
dunning_letter_text=[
{"language": "en", "body_text": "Valid body", "closing_text": "{{ unclosed"}
],
)
self.assertRaisesRegex(frappe.ValidationError, "Syntax error in template", doc.insert)
def test_valid_jinja_template_passes(self):
doc = make_dunning_type(
"_Test Dunning Valid Template",
dunning_letter_text=[
{
"language": "en",
"body_text": "Outstanding amount is {{ outstanding_amount }}",
"closing_text": "Regards, {{ company }}",
}
],
)
doc.insert()
self.assertTrue(doc.name)
def test_set_default_dunning_type_unsets_previous_default(self):
first = make_dunning_type("_Test Dunning Default One", is_default=1)
first.insert()
self.assertEqual(frappe.db.get_value("Dunning Type", first.name, "is_default"), 1)
second = make_dunning_type("_Test Dunning Default Two", is_default=1)
second.insert()
self.assertEqual(frappe.db.get_value("Dunning Type", first.name, "is_default"), 0)
self.assertEqual(frappe.db.get_value("Dunning Type", second.name, "is_default"), 1)
def test_set_default_dunning_type_scoped_per_company(self):
company_1 = make_dunning_type("_Test Dunning Default Co1", is_default=1)
company_1.insert()
company_2 = make_dunning_type(
"_Test Dunning Default Co2",
company="_Test Company 1",
is_default=1,
)
company_2.insert()
self.assertEqual(frappe.db.get_value("Dunning Type", company_1.name, "is_default"), 1)
self.assertEqual(frappe.db.get_value("Dunning Type", company_2.name, "is_default"), 1)

View File

@@ -22,17 +22,27 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
frappe.call({
method: "check_journal_entry_condition",
method: "check_journal_and_reversal",
doc: frm.doc,
callback: function (r) {
if (r.message) {
frm.add_custom_button(
__("Journal Entries"),
function () {
return frm.events.make_jv(frm);
},
__("Create")
);
if (!r.message.journals_posted) {
frm.add_custom_button(
__("Journal Entries"),
function () {
return frm.events.make_jv(frm);
},
__("Create")
);
} else if (!r.message.reversals_posted) {
frm.add_custom_button(
__("Reversal Journal Entries"),
function () {
return frm.events.make_reverse_journal(frm);
},
__("Create")
);
}
}
},
});
@@ -100,6 +110,14 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
},
});
},
make_reverse_journal: function (frm) {
frappe.call({
method: "make_reverse_journal",
doc: frm.doc,
freeze: true,
freeze_message: __("Reversing Journals..."),
});
},
});
frappe.ui.form.on("Exchange Rate Revaluation Account", {

View File

@@ -8,7 +8,7 @@ from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Criterion, Order
from frappe.query_builder.functions import NullIf, Sum
from frappe.utils import flt, get_link_to_form
from frappe.utils import flt, get_link_to_form, nowdate
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
@@ -90,25 +90,31 @@ class ExchangeRateRevaluation(Document):
)
def on_cancel(self):
self.ignore_linked_doctypes = "GL Entry"
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
@frappe.whitelist()
def check_journal_entry_condition(self):
def check_journal_and_reversal(self):
exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
journals_posted = False
reversals_posted = False
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
journals = (
qb.from_(jea)
.select(jea.parent)
qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.name)
.distinct()
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
& (je.reversal_of.isnull()) # omit journals that have reversals
)
.run()
.run(pluck="name")
)
if journals:
gle = qb.DocType("GL Entry")
total_amt = (
@@ -123,12 +129,31 @@ class ExchangeRateRevaluation(Document):
.run()
)
if total_amt and total_amt[0][0] != self.total_gain_loss:
return True
if total_amt and total_amt[0][0] == self.total_gain_loss:
journals_posted = True
else:
return False
journals_posted = False
return True
# reverse journals
reverse_journals = (
qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.name)
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
& (je.reversal_of.notnull())
)
.run(pluck="name")
)
if reverse_journals:
reversals_posted = True
else:
reversals_posted = False
return {"journals_posted": journals_posted, "reversals_posted": reversals_posted}
def fetch_and_calculate_accounts_data(self):
accounts = self.get_accounts_data()
@@ -342,6 +367,7 @@ class ExchangeRateRevaluation(Document):
@frappe.whitelist()
def make_jv_entries(self):
frappe.has_permission("Journal Entry", "write", throw=True)
zero_balance_jv = self.make_jv_for_zero_balance()
if zero_balance_jv:
frappe.msgprint(
@@ -568,6 +594,38 @@ class ExchangeRateRevaluation(Document):
journal_entry.save()
return journal_entry
@frappe.whitelist()
def make_reverse_journal(self):
frappe.has_permission("Journal Entry", "write", throw=True)
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
journals = (
qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.name)
.distinct()
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
& (je.reversal_of.isnull()) # omit journals that have reversals
)
.run(pluck="name")
)
if journals:
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
for x in journals:
reversal = make_reverse_journal_entry(x)
reversal.posting_date = nowdate()
reversal.submit()
frappe.msgprint(
_("Revaluation journal for {0} has been created: {1}").format(
frappe.bold(x), get_link_to_form("Journal Entry", reversal.name)
)
)
def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
"""
@@ -616,6 +674,10 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
def get_account_details(
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None
):
if not account:
return
frappe.has_permission("Account", doc=account, throw=True)
if not (company and posting_date):
frappe.throw(_("Company and Posting Date is mandatory"))

View File

@@ -15,11 +15,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_usd_receivable_account()
self.create_item()
self.create_customer()
self.clear_old_entries()
self.company = "_Test Company"
self.item = "_Test Item"
self.customer = "_Test Customer"
self.cost_center = "Main - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
self.set_system_and_company_settings()
def set_system_and_company_settings(self):
@@ -132,7 +132,8 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
err = err.save().submit()
# Create JV for ERR
self.assertTrue(err.check_journal_entry_condition())
ret = err.check_journal_and_reversal()
self.assertFalse(ret.get("journals_posted"))
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()
@@ -221,7 +222,8 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
err = err.save().submit()
# Create JV for ERR
self.assertTrue(err.check_journal_entry_condition())
ret = err.check_journal_and_reversal()
self.assertFalse(ret.get("journals_posted"))
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()

View File

@@ -71,7 +71,7 @@ frappe.ui.form.on("Journal Entry", {
refresh: function (frm) {
if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) {
frm.set_read_only();
erpnext.journal_entry.lock_reversal_entry(frm);
}
erpnext.toggle_naming_series();
@@ -564,6 +564,13 @@ $.extend(erpnext.journal_entry, {
});
},
lock_reversal_entry: function (frm) {
frm.fields
.filter((field) => field.has_input)
.forEach((field) => frm.set_df_property(field.df.fieldname, "read_only", 1));
frm.set_df_property("accounts", "read_only", 1);
},
set_debit_credit_in_company_currency: function (frm, cdt, cdn) {
var row = locals[cdt][cdn];

View File

@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
validate_docs_for_voucher_types,
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
from erpnext.accounts.general_ledger import validate_opening_entry_against_pcv
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
@@ -131,6 +132,9 @@ class JournalEntry(AccountsController):
if not self.is_opening:
self.is_opening = "No"
if self.is_opening == "Yes":
validate_opening_entry_against_pcv(self.company)
self.clearance_date = None
self.validate_party()
@@ -413,11 +417,12 @@ class JournalEntry(AccountsController):
def update_journal_entry_link_on_depr_schedule(self, asset, je_row):
depr_schedule = get_depr_schedule(asset.name, "Active", self.finance_book)
precision = je_row.precision("debit")
for d in depr_schedule or []:
if (
d.schedule_date == self.posting_date
and not d.journal_entry
and d.depreciation_amount == flt(je_row.debit)
and flt(d.depreciation_amount, precision) == flt(je_row.debit, precision)
):
frappe.db.set_value("Depreciation Schedule", d.name, "journal_entry", self.name)

View File

@@ -1,7 +1,10 @@
frappe.listview_settings["Journal Entry"] = {
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark"],
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark", "reversal_of"],
get_indicator: function (doc) {
if (doc.docstatus === 1) {
if (doc.reversal_of && doc.voucher_type == "Exchange Rate Revaluation") {
return [__("Reversal Of Exchange Rate Revaluation"), "blue"];
}
return [__(doc.voucher_type), "blue", `voucher_type,=,${doc.voucher_type}`];
}
},

View File

@@ -12,10 +12,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.debit_to = "Debtors - _TC"
self.income_account = "Sales - _TC"
self.configure_monitoring_tool()
self.clear_old_entries()
def configure_monitoring_tool(self):
monitor_settings = frappe.get_doc("Ledger Health Monitor")

View File

@@ -24,15 +24,22 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
setTimeout(
() => {
frm.doc.import_in_progress = false;
frm.clear_table("invoices");
frm.refresh_fields();
frm.page.clear_indicator();
frm.dashboard.hide_progress();
if (frm.doc.invoice_type == "Sales") {
frappe.msgprint(__("Opening Sales Invoices have been created."));
if (!data.errors) {
frm.clear_table("invoices");
frm.refresh_fields();
const message =
frm.doc.invoice_type == "Sales"
? __("Opening Sales Invoice(s) have been created.")
: __("Opening Purchase Invoice(s) have been created.");
frappe.show_alert({
message: message,
indicator: "green",
});
} else {
frappe.msgprint(__("Opening Purchase Invoices have been created."));
frm.refresh_fields();
}
},
1500,
@@ -74,29 +81,31 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
},
setup_company_filters: function (frm) {
frm.set_query("cost_center", "invoices", function (doc, cdt, cdn) {
return {
filters: {
company: doc.company,
},
};
frm.events.apply_company_query_filter(frm, "cost_center", "invoices", { is_group: 0 });
frm.events.apply_company_query_filter(frm, "project", "invoices");
frm.events.apply_company_query_filter(frm, "project");
frm.events.apply_company_query_filter(frm, "cost_center", undefined, { is_group: 0 });
frm.events.apply_company_query_filter(frm, "temporary_opening_account", "invoices", {
account_type: "Temporary",
is_group: 0,
});
},
frm.set_query("cost_center", function (doc) {
apply_company_query_filter: function (frm, field_name, child_doctype = null, filters = {}) {
const query = function (doc) {
return {
filters: {
company: doc.company,
...filters,
},
};
});
};
frm.set_query("temporary_opening_account", "invoices", function (doc, cdt, cdn) {
return {
filters: {
company: doc.company,
},
};
});
if (child_doctype) {
frm.set_query(field_name, child_doctype, query);
} else {
frm.set_query(field_name, query);
}
},
company: function (frm) {
@@ -120,11 +129,6 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
},
invoice_type: function (frm) {
$.each(frm.doc.invoices, (idx, row) => {
row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier";
frappe.model.set_value(row.doctype, row.name, "party", "");
frappe.model.set_value(row.doctype, row.name, "party_name", "");
});
frm.clear_table("invoices");
frm.refresh_fields();
frm.trigger("update_party_labels");
@@ -219,7 +223,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool Item", {
});
},
invoices_add: (frm) => {
invoices_add: (frm, cdt, cdn) => {
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
["project", "cost_center"].forEach((fieldname) => {
if (frm.doc[fieldname]) {
frappe.model.set_value(cdt, cdn, fieldname, frm.doc[fieldname]);
} else {
field_copy.push(fieldname);
}
});
frm.script_manager.copy_from_first_row("invoices", row, field_copy);
frm.trigger("update_invoice_table");
},
});

View File

@@ -133,6 +133,17 @@ class OpeningInvoiceCreationTool(Document):
if not row.get(scrub(d)):
frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
self.validate_temporary_opening_account(row)
def validate_temporary_opening_account(self, row):
account_type = frappe.get_cached_value("Account", row.temporary_opening_account, "account_type")
if account_type != "Temporary":
frappe.throw(
_("Row #{0}: {1} account is not of type {2}").format(
row.idx, row.temporary_opening_account, "Temporary"
)
)
def get_invoices(self):
invoices = []
for row in self.invoices:
@@ -203,6 +214,7 @@ class OpeningInvoiceCreationTool(Document):
"description": row.item_name or "Opening Invoice Item",
income_expense_account_field: row.temporary_opening_account,
"cost_center": cost_center,
"project": row.get("project") or self.get("project"),
}
)
@@ -269,12 +281,20 @@ class OpeningInvoiceCreationTool(Document):
def start_import(invoices):
errors = 0
names = []
total = len(invoices)
for idx, d in enumerate(invoices):
# Scope each invoice to a savepoint so a failure only undoes that invoice.
# A plain rollback() would discard the whole transaction — including invoices
# imported earlier in this batch and the error logs of earlier failures (the
# latter only survive on mariadb because the Error Log table is MyISAM; on
# postgres they would be lost). Rolling back to a savepoint keeps both.
savepoint = f"opening_invoice_{frappe.generate_hash(length=8)}"
frappe.db.savepoint(savepoint)
is_last = idx == total - 1
try:
invoice_number = None
if d.invoice_number:
invoice_number = d.invoice_number
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
doc.insert(set_name=invoice_number)
@@ -282,10 +302,12 @@ def start_import(invoices):
if not frappe.in_test:
frappe.db.commit()
names.append(doc.name)
publish(idx, total, d.doctype, errors=errors if is_last else None)
except Exception:
errors += 1
frappe.db.rollback()
doc.log_error("Opening invoice creation failed")
publish(idx, total, d.doctype, errors=errors if is_last else None)
if errors:
frappe.msgprint(
_("You had {} errors while creating opening invoices. Check {} for more details").format(
@@ -297,7 +319,7 @@ def start_import(invoices):
return names
def publish(index, total, doctype):
def publish(index, total, doctype, errors=None):
frappe.publish_realtime(
"opening_invoice_creation_progress",
dict(
@@ -305,6 +327,7 @@ def publish(index, total, doctype):
message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
count=index + 1,
total=total,
errors=errors,
),
user=frappe.session.user,
)

View File

@@ -2,10 +2,12 @@
# See license.txt
import frappe
from frappe.utils import add_days, today
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.tests.utils import ERPNextTestSuite
@@ -14,21 +16,26 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
self,
invoice_type="Sales",
company=None,
party_1=None,
party_2=None,
invoice_number=None,
invoices=None,
project=None,
cost_center=None,
department=None,
return_doc=False,
):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(
invoice_type=invoice_type,
company=company,
party_1=party_1,
party_2=party_2,
invoice_number=invoice_number,
invoices=invoices,
project=project,
cost_center=cost_center,
department=department,
)
doc.update(args)
if return_doc:
return doc
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
@@ -37,8 +44,8 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
self.assertEqual(len(invoices), 2)
expected_value = {
"keys": ["customer", "outstanding_amount", "status"],
0: ["_Test Customer", 300, "Overdue"],
1: ["_Test Customer 1", 250, "Overdue"],
0: ["_Test Customer", 200, "Overdue"],
1: ["_Test Customer 1", 200, "Overdue"],
}
self.check_expected_values(invoices, expected_value)
@@ -55,48 +62,34 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
for field_idx, field in enumerate(expected_value["keys"]):
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
def test_opening_invoice_requires_temporary_account_type(self):
doc = self.make_invoices(company="_Test Opening Invoice Company", return_doc=True)
doc.invoices[0].temporary_opening_account = "Sales - _TOIC"
self.assertRaises(frappe.ValidationError, doc.make_invoices)
def test_opening_purchase_invoice_creation(self):
invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
"keys": ["supplier", "outstanding_amount", "status"],
0: ["_Test Supplier", 300, "Overdue"],
1: ["_Test Supplier 1", 250, "Overdue"],
0: ["_Test Supplier", 200, "Overdue"],
1: ["_Test Supplier 1", 200, "Overdue"],
}
self.check_expected_values(invoices, expected_value, "Purchase")
def test_opening_sales_invoice_creation_with_missing_debit_account(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
frappe.db.set_value("Company", company, "default_receivable_account", "")
old_default_receivable_account = frappe.db.get_value(
"Company", "_Test Opening Invoice Company", "default_receivable_account"
)
frappe.db.set_value("Company", "_Test Opening Invoice Company", "default_receivable_account", "")
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
cc = frappe.get_doc(
{
"doctype": "Cost Center",
"cost_center_name": "_Test Opening Invoice Company",
"is_group": 1,
"company": "_Test Opening Invoice Company",
}
)
cc.insert(ignore_mandatory=True)
cc2 = frappe.get_doc(
{
"doctype": "Cost Center",
"cost_center_name": "Main",
"is_group": 0,
"company": "_Test Opening Invoice Company",
"parent_cost_center": cc.name,
}
)
cc2.insert()
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
self.make_invoices(
company="_Test Opening Invoice Company",
invoices=[{"party": party_1}, {"party": party_2}],
)
# Check if missing debit account error raised
error_log = frappe.db.exists(
@@ -106,71 +99,107 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
self.assertTrue(error_log)
# teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def test_renaming_of_invoice_using_invoice_number_field(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
self.make_invoices(
company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11"
frappe.db.set_value(
"Company",
"_Test Opening Invoice Company",
"default_receivable_account",
old_default_receivable_account,
)
sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name")
sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name")
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
def test_renaming_of_invoice_using_invoice_number_field(self):
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
invoices = self.make_invoices(
company="_Test Opening Invoice Company",
invoices=[
{"party": party_1, "invoice_number": "TEST-NEW-INV-11"},
{"party": party_2},
],
)
# teardown
for inv in [sales_inv1, sales_inv2]:
doc = frappe.get_doc("Sales Invoice", inv)
doc.cancel()
self.assertEqual(invoices[0], "TEST-NEW-INV-11")
def test_opening_invoice_with_accounting_dimension(self):
invoices = self.make_invoices(
invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC"
)
expected_value = {
"keys": ["customer", "outstanding_amount", "status", "department"],
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
}
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
for invoice in invoices:
self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC")
def test_opening_entry_project_linking(self):
doc = self.make_invoices(
company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True
)
project_1 = make_project(
{"project_name": "Test Opening Invoice projecty 01", "company": "_Test Opening Invoice Company"}
)
project_2 = make_project(
{"project_name": "Test Opening Invoice projecty 02", "company": "_Test Opening Invoice Company"}
)
doc.invoices[0].project = project_1.name
doc.invoices[1].project = project_2.name
invoices = doc.make_invoices()
sales_invoice_1 = frappe.get_doc("Sales Invoice", invoices[0])
sales_invoice_2 = frappe.get_doc("Sales Invoice", invoices[1])
self.assertEqual(sales_invoice_1.items[0].project, project_1.name)
self.assertEqual(sales_invoice_2.items[0].project, project_2.name)
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
default_invoices = []
default_invoice_rows = [
{
"qty": 1.0,
"outstanding_amount": 200,
"party": f"_Test {party}",
"item_name": "Opening Item",
"due_date": add_days(today(), -10),
"posting_date": add_days(today(), -15),
"temporary_opening_account": get_temporary_opening_account(company),
},
{
"qty": 1.0,
"outstanding_amount": 200,
"party": f"_Test {party} 1",
"item_name": "Opening Item",
"due_date": add_days(today(), -10),
"posting_date": add_days(today(), -15),
"temporary_opening_account": get_temporary_opening_account(company),
},
]
for row in args.get("invoices") or default_invoice_rows:
default_invoices.append(
{
"qty": row.get("qty") or 1.0,
"outstanding_amount": row.get("outstanding_amount") or 200,
"party": row.get("party") or f"_Test {party}",
"item_name": row.get("item_name") or "Opening Item",
"due_date": row.get("due_date") or add_days(today(), -10),
"posting_date": row.get("posting_date") or add_days(today(), -15),
"temporary_opening_account": row.get("temporary_opening_account")
or get_temporary_opening_account(company),
"invoice_number": row.get("invoice_number"),
"project": row.get("project"),
"cost_center": row.get("cost_center"),
}
)
invoice_dict = frappe._dict(
{
"company": company,
"invoice_type": args.get("invoice_type", "Sales"),
"invoices": [
{
"qty": 1.0,
"outstanding_amount": 300,
"party": args.get("party_1") or f"_Test {party}",
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": args.get("invoice_number"),
},
{
"qty": 2.0,
"outstanding_amount": 250,
"party": args.get("party_2") or f"_Test {party} 1",
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": None,
},
],
"project": args.get("project"),
"cost_center": args.get("cost_center"),
"invoices": default_invoices,
}
)
invoice_dict.update(args)
invoice_dict.invoices = default_invoices
return invoice_dict

View File

@@ -21,7 +21,8 @@
"qty",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break"
"dimension_col_break",
"project"
],
"fields": [
{
@@ -81,6 +82,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Outstanding Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
@@ -125,11 +127,17 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Party Name"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"istable": 1,
"links": [],
"modified": "2026-03-20 02:11:42.023575",
"modified": "2026-07-02 15:17:11.938499",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",

View File

@@ -26,6 +26,7 @@ class OpeningInvoiceCreationToolItem(Document):
party_name: DF.Data | None
party_type: DF.Link | None
posting_date: DF.Date | None
project: DF.Link | None
qty: DF.Data | None
supplier_invoice_date: DF.Date | None
temporary_opening_account: DF.Link | None

View File

@@ -29,6 +29,7 @@
{
"fieldname": "advance_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Advance Account",
"options": "Account"
}
@@ -36,14 +37,15 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:08.489183",
"modified": "2026-05-27 14:19:00.888437",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Party Account",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -754,17 +754,21 @@ frappe.ui.form.on("Payment Entry", {
frm.set_paid_amount_based_on_received_amount = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.base_received_amount && frm.doc.source_exchange_rate) {
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
if (frm.doc.paid_amount && frm.doc.source_exchange_rate) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
// target exchange rate should always be same as source if both account currencies is same
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
frm.set_value("received_amount", frm.doc.paid_amount);
} else {
frm.set_value(
"paid_amount",
flt(frm.doc.base_paid_amount) / flt(frm.doc.source_exchange_rate)
);
const target_rate =
flt(frm.doc.target_exchange_rate) ||
(company_currency == frm.doc.paid_to_account_currency ? 1 : 0);
if (target_rate) {
frm.set_value("received_amount", flt(frm.doc.base_received_amount) / target_rate);
}
}
// set_unallocated_amount is called by below method,
@@ -780,18 +784,23 @@ frappe.ui.form.on("Payment Entry", {
target_exchange_rate: function (frm) {
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.base_paid_amount && frm.doc.target_exchange_rate) {
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
if (
!frm.doc.source_exchange_rate &&
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
) {
if (frm.doc.received_amount && frm.doc.target_exchange_rate) {
frm.set_value(
"base_received_amount",
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
);
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
frm.set_value("paid_amount", frm.doc.received_amount);
} else {
frm.set_value(
"received_amount",
flt(frm.doc.base_received_amount) / flt(frm.doc.target_exchange_rate)
);
const source_rate =
flt(frm.doc.source_exchange_rate) ||
(company_currency == frm.doc.paid_from_account_currency ? 1 : 0);
if (source_rate) {
frm.set_value("paid_amount", flt(frm.doc.base_paid_amount) / source_rate);
}
}
// set_unallocated_amount is called by below method,

View File

@@ -1206,9 +1206,9 @@ class PaymentEntry(AccountsController):
continue
if tax.add_deduct_tax == "Add":
included_taxes += tax.base_tax_amount
included_taxes += flt(tax.base_tax_amount)
else:
included_taxes -= tax.base_tax_amount
included_taxes -= flt(tax.base_tax_amount)
return included_taxes
@@ -2712,6 +2712,9 @@ def get_party_details(company, party_type, party, date, cost_center=None):
if not frappe.db.exists(party_type, party):
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
party_account = get_party_account(party_type, party, company)
account_currency = get_account_currency(party_account)
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
@@ -2719,7 +2722,7 @@ def get_party_details(company, party_type, party, date, cost_center=None):
if party_type in ["Customer", "Supplier"]:
party_bank_account = get_party_bank_account(party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party, ignore_permissions=False)
return {
"party_account": party_account,
@@ -2811,9 +2814,7 @@ def get_reference_details(
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
else:
exchange_rate = 1
outstanding_amount, total_amount = get_outstanding_on_journal_entry(
reference_name, party_type, party
)
outstanding_amount, total_amount = get_outstanding_on_journal_entry(reference_name, party_type, party)
elif reference_doctype == "Payment Entry":
if reverse_payment_details := frappe.db.get_all(

View File

@@ -1113,6 +1113,27 @@ class TestPaymentEntry(ERPNextTestSuite):
self.assertEqual(gl_entries, expected_gl_entries)
def test_payment_entry_with_inclusive_tax(self):
# inclusive tax built server-side: base_tax_amount is None until apply_taxes()
payment_entry = create_payment_entry(paid_amount=1180)
payment_entry.append(
"taxes",
{
"account_head": "_Test Account Service Tax - _TC",
"charge_type": "On Paid Amount",
"rate": 18,
"included_in_paid_amount": 1,
"add_deduct_tax": "Add",
"description": "Service Tax",
},
)
payment_entry.save()
payment_entry.submit()
# 1180 incl 18% => 1000 base + 180 tax
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), 180.0)
self.assertEqual(flt(payment_entry.unallocated_amount, 2), 1000.0)
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()

View File

@@ -10,76 +10,22 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.tests.utils import ERPNextTestSuite
class TestPaymentLedgerEntry(ERPNextTestSuite):
def setUp(self):
self.ple = qb.DocType("Payment Ledger Entry")
self.create_company()
self.create_item()
self.create_customer()
self.clear_old_entries()
def create_company(self):
company_name = "_Test Payment Ledger"
company = None
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses - _PL"
self.income_account = "Sales - _PL"
self.expense_account = "Cost of Goods Sold - _PL"
self.debit_to = "Debtors - _PL"
self.creditors = "Creditors - _PL"
# create bank account
if frappe.db.exists("Account", "HDFC - _PL"):
self.bank = "HDFC - _PL"
else:
bank_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": "HDFC",
"parent_account": "Bank Accounts - _PL",
"company": self.company,
}
)
bank_acc.save()
self.bank = bank_acc.name
def create_item(self):
item_name = "_Test PL Item"
item = create_item(
item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
name = "_Test PL Customer"
if frappe.db.exists("Customer", name):
self.customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.save()
self.customer = customer.name
self.company = "_Test Company"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
self.creditors = "Creditors - _TC"
self.bank = "Cash - _TC"
self.item = "_Test Item"
self.customer = "_Test Customer"
def create_sales_invoice(
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
@@ -152,18 +98,6 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
)
return so
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None):
je = frappe.new_doc("Journal Entry")
je.posting_date = posting_date or nowdate()

View File

@@ -6,8 +6,10 @@ import frappe
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.permissions import get_allowed_docs_for_doctype, get_user_permissions
from frappe.query_builder import Case, Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
import erpnext
@@ -74,6 +76,10 @@ class PaymentReconciliation(Document):
self.ple_posting_date_filter = []
self.dimensions = get_dimensions(with_cost_center_and_project=True)[0]
@property
def user_permissions(self):
return get_user_permissions(frappe.session.user)
def load_from_db(self):
# 'modified' attribute is required for `run_doc_method` to work properly.
doc_dict = frappe._dict(
@@ -153,6 +159,22 @@ class PaymentReconciliation(Document):
self.add_payment_entries(non_reconciled_payments)
def get_permitted_dimension_values(self, document_type, reference_doctype):
return get_allowed_docs_for_doctype(self.user_permissions.get(document_type, []), reference_doctype)
def validate_permitted_dimension_value(self, document_type, value, allowed):
if value and allowed and value not in allowed:
frappe.throw(
_("You do not have enough permission to access {0}: {1}").format(_(document_type), value),
frappe.PermissionError,
)
def get_user_permission_dimension_condition(self, field, allowed):
value_condition = field.isin(allowed)
if frappe.get_system_settings("apply_strict_user_permissions"):
return value_condition
return (IfNull(field, "") == "") | value_condition
def get_payment_entries(self):
party_account = [self.receivable_payable_account]
@@ -176,8 +198,13 @@ class PaymentReconciliation(Document):
dimensions = {}
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
dimensions.update({dimension: self.get(dimension)})
allowed = self.get_permitted_dimension_values(x.document_type, "Payment Entry")
if value := self.get(dimension):
self.validate_permitted_dimension_value(x.document_type, value, allowed)
dimensions[dimension] = value
elif allowed:
dimensions[dimension] = allowed
condition.update({"accounting_dimensions": dimensions})
payment_entries = get_advance_payment_entries_for_regional(
@@ -201,8 +228,12 @@ class PaymentReconciliation(Document):
# Dimension filters
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
conditions.append(jea[dimension] == self.get(dimension))
allowed = self.get_permitted_dimension_values(x.document_type, "Journal Entry Account")
if value := self.get(dimension):
self.validate_permitted_dimension_value(x.document_type, value, allowed)
conditions.append(jea[dimension] == value)
elif allowed:
conditions.append(self.get_user_permission_dimension_condition(jea[dimension], allowed))
if self.payment_name:
conditions.append(je.name.like(f"%%{self.payment_name}%%"))
@@ -746,8 +777,15 @@ class PaymentReconciliation(Document):
ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
if frappe.db.has_column("Payment Ledger Entry", dimension):
allowed = self.get_permitted_dimension_values(x.document_type, "Payment Ledger Entry")
if value := self.get(dimension):
self.validate_permitted_dimension_value(x.document_type, value, allowed)
self.accounting_dimension_filter_conditions.append(ple[dimension] == value)
elif allowed:
self.accounting_dimension_filter_conditions.append(
self.get_user_permission_dimension_condition(ple[dimension], allowed)
)
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear()
@@ -796,10 +834,17 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
allocated_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
)
for inv in dr_cr_notes:
if (
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
< inv.allocated_amount
flt(
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
- inv.allocated_amount,
allocated_amount_precision,
)
< 0
):
frappe.throw(
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import qb
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
from frappe.utils import add_days, add_years, cint, flt, getdate, nowdate, today
from frappe.utils.data import getdate as convert_to_date
from erpnext import get_default_cost_center
@@ -51,6 +51,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
sinv = create_sales_invoice(
qty=qty,
rate=rate,
posting_date=posting_date,
company=self.company,
customer=self.customer,
item_code=self.item,
@@ -1105,6 +1106,101 @@ class TestPaymentReconciliation(ERPNextTestSuite):
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
def test_user_permission_on_accounting_dimension_filters_vouchers(self):
test_user = "test@example.com"
permitted_ccs = ["_Test Cost Center - _TC", "_Test Cost Center 2 - _TC"]
restricted_cc = "_Test Write Off Cost Center - _TC"
existing_apply_strict_user_permissions = cint(
frappe.db.get_single_value("System Settings", "apply_strict_user_permissions")
)
self.addCleanup(
frappe.db.set_single_value,
"System Settings",
"apply_strict_user_permissions",
existing_apply_strict_user_permissions,
)
transaction_date = nowdate()
rate = 100
def make_invoice(cost_center):
si = self.create_sales_invoice(
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
)
si.cost_center = cost_center
for row in si.items:
row.cost_center = cost_center
return si.submit()
def make_payment(cost_center):
pe = self.create_payment_entry(posting_date=transaction_date, amount=rate)
pe.cost_center = cost_center
return pe.save().submit()
def make_journal(cost_center):
je = self.create_journal_entry(
self.bank, self.debit_to, 100, transaction_date, cost_center=cost_center
)
je.accounts[1].party_type = "Customer"
je.accounts[1].party = self.customer
return je.save().submit()
# Vouchers tagged with the two permitted cost centers
si_allowed = make_invoice(permitted_ccs[0])
pe_allowed = make_payment(permitted_ccs[1])
je_allowed = make_journal(permitted_ccs[0])
# Vouchers tagged with the restricted cost center
si_restricted = make_invoice(restricted_cc)
pe_restricted = make_payment(restricted_cc)
je_restricted = make_journal(restricted_cc)
# Payment entry with a BLANK cost center
pe_blank = make_payment(None)
for cc in permitted_ccs:
frappe.permissions.add_user_permission("Cost Center", cc, test_user)
# Without strict user permissions
frappe.db.set_single_value("System Settings", "apply_strict_user_permissions", 0)
with self.set_user(test_user):
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoice_numbers = [x.get("invoice_number") for x in pr.get("invoices")]
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertIn(si_allowed.name, invoice_numbers)
self.assertIn(pe_allowed.name, payment_vouchers)
self.assertIn(je_allowed.name, payment_vouchers)
self.assertIn(pe_blank.name, payment_vouchers)
self.assertNotIn(si_restricted.name, invoice_numbers)
self.assertNotIn(pe_restricted.name, payment_vouchers)
self.assertNotIn(je_restricted.name, payment_vouchers)
# With strict user permissions
frappe.db.set_single_value("System Settings", "apply_strict_user_permissions", 1)
with self.set_user(test_user):
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoice_numbers = [x.get("invoice_number") for x in pr.get("invoices")]
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertIn(si_allowed.name, invoice_numbers)
self.assertIn(pe_allowed.name, payment_vouchers)
self.assertIn(je_allowed.name, payment_vouchers)
self.assertNotIn(pe_blank.name, payment_vouchers)
self.assertNotIn(si_restricted.name, invoice_numbers)
self.assertNotIn(pe_restricted.name, payment_vouchers)
self.assertNotIn(je_restricted.name, payment_vouchers)
# with restricted dimension as a filter
with self.set_user(test_user):
pr = self.create_payment_reconciliation()
pr.cost_center = restricted_cc
self.assertRaises(frappe.PermissionError, pr.get_unreconciled_entries)
for cc in permitted_ccs:
frappe.permissions.remove_user_permission("Cost Center", cc, test_user)
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{
@@ -2018,7 +2114,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pr.reconcile()
si.reload()
self.assertEqual(si.status, "Partly Paid")
self.assertEqual(si.status, "Overdue")
# check PR tool output post reconciliation
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 120)
@@ -2414,6 +2510,76 @@ class TestPaymentReconciliation(ERPNextTestSuite):
self.assertEqual(flt(pr.allocation[0].difference_amount), 5000.0)
pr.reconcile()
def test_cr_note_split_across_invoices_floating_point_precision(self):
"""Regression: when a credit note is split across multiple invoices, floating-point
arithmetic (150 - 8.45 - 90.72 = 50.83000000000001) must not cause reconcile() to fail.
The test environment rounds INR totals to whole rupees (smallest_currency_fraction_value=0),
so the invoices are created with round-number totals (100, 200, 100) and then partially paid
down to the decimal outstanding amounts (8.45, 90.72, 72.57) via payment entries.
"""
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
# Create invoices on different posting dates to control sort-order in Payment Reconciliation
# (invoices are sorted by posting_date ascending, so si_a is processed first).
# Processing order 8.45 → 90.72 → 72.57 produces the float chain:
# 150 - 8.45 = 141.55 → 141.55 - 90.72 = 50.83000000000001
# The last allocation row will therefore carry allocated_amount = 50.83000000000001.
si_a = self.create_sales_invoice(qty=1, rate=100, posting_date=add_days(nowdate(), -2))
si_b = self.create_sales_invoice(qty=1, rate=200, posting_date=add_days(nowdate(), -1))
si_c = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate())
# Partially pay each invoice so the remaining outstanding is a clean decimal value.
# INR rounds the invoice total to a whole rupee, so we achieve decimal outstandings
# by subtracting a decimal-valued payment from the integer total:
# 100 - 91.55 = 8.45
# 200 - 109.28 = 90.72
# 100 - 27.43 = 72.57
for si, partial_paid in ((si_a, 91.55), (si_b, 109.28), (si_c, 27.43)):
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = partial_paid
pe.received_amount = partial_paid
pe.references[0].allocated_amount = partial_paid
pe.save().submit()
cr_note = self.create_sales_invoice(
qty=-1, rate=150, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
cr_note.is_return = 1
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
# Widen date range so all three invoices (oldest is -2 days) are fetched
pr.from_invoice_date = add_days(nowdate(), -2)
pr.to_invoice_date = nowdate()
pr.from_payment_date = nowdate()
pr.to_payment_date = nowdate()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 3)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Credit note (150) covers all of si_a (8.45) and si_b (90.72), then partially si_c
self.assertEqual(len(pr.allocation), 3)
last_row = pr.allocation[-1]
# Last allocated amount should be ~50.83 (possibly 50.83000000000001 due to float arithmetic)
self.assertAlmostEqual(flt(last_row.allocated_amount), 50.83, places=2)
# reconcile() must not raise "has been modified after you pulled it" due to float imprecision
pr.reconcile()
si_a.reload()
si_b.reload()
si_c.reload()
self.assertEqual(si_a.outstanding_amount, 0)
self.assertEqual(si_b.outstanding_amount, 0)
# si_c is only partially settled: 72.57 - 50.83 = 21.74
self.assertAlmostEqual(si_c.outstanding_amount, 21.74, places=2)
def create_fiscal_year(company, year_start_date, year_end_date):
fy_docname = frappe.db.exists(

View File

@@ -14,7 +14,8 @@
"section_break_mjlv",
"due_date",
"column_break_qghl",
"amount"
"amount",
"currency"
],
"fields": [
{
@@ -55,8 +56,18 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "currency",
"precision": "2"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_lnjp",
"fieldtype": "Column Break"
@@ -74,7 +85,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-01-19 02:21:36.455830",
"modified": "2026-07-11 00:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reference",

View File

@@ -379,6 +379,7 @@ class PaymentRequest(Document):
bank_amount=bank_amount,
created_from_payment_request=True,
)
payment_entry.set_missing_ref_details(force=True)
payment_entry.update(
{
@@ -783,6 +784,7 @@ def set_payment_references(payment_schedules):
"description": row.get("description"),
"due_date": row.get("due_date"),
"amount": row.get("payment_amount"),
"currency": row.get("currency"),
}
)
@@ -957,6 +959,7 @@ def resend_payment_email(docname):
@frappe.whitelist()
def make_payment_entry(docname):
doc = frappe.get_doc("Payment Request", docname)
doc.check_permission("read")
return doc.create_payment_entry(submit=False).as_dict()

View File

@@ -775,6 +775,22 @@ class TestPaymentRequest(ERPNextTestSuite):
pi.load_from_db()
self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
def test_payment_entry_reference_details_fetched_from_invoice(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=94500)
pi.submit()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1)
pr.grand_total = 94000
pr.submit()
pe = pr.create_payment_entry(submit=False)
self.assertEqual(pe.references[0].reference_name, pi.name)
self.assertEqual(pe.references[0].total_amount, pi.grand_total)
self.assertEqual(pe.references[0].outstanding_amount, pi.outstanding_amount)
self.assertEqual(pe.references[0].allocated_amount, 94000)
self.assertEqual(pe.paid_amount, 94000)
def test_consider_journal_entry_and_return_invoice(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry

View File

@@ -19,7 +19,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
def test_closing_entry(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv1 = make_journal_entry(
@@ -28,10 +27,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company=company,
company="Test PCV Company",
save=False,
)
jv1.company = company
jv1.company = "Test PCV Company"
jv1.save()
jv1.submit()
@@ -41,10 +40,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cost of Goods Sold - TPC",
account2="Cash - TPC",
cost_center=cost_center,
company=company,
company="Test PCV Company",
save=False,
)
jv2.company = company
jv2.company = "Test PCV Company"
jv2.save()
jv2.submit()
@@ -68,14 +67,13 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(pcv_gle, expected_gle)
def test_cost_center_wise_posting(self):
company = create_company()
surplus_account = create_account()
cost_center1 = create_cost_center("Main")
cost_center2 = create_cost_center("Western Branch")
create_sales_invoice(
company=company,
company="Test PCV Company",
cost_center=cost_center1,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
@@ -86,7 +84,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
posting_date="2021-03-15",
)
create_sales_invoice(
company=company,
company="Test PCV Company",
cost_center=cost_center2,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
@@ -131,12 +129,11 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
)
def test_period_closing_with_finance_book_entries(self):
company = create_company()
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
create_sales_invoice(
company=company,
company="Test PCV Company",
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
cost_center=cost_center,
@@ -153,9 +150,9 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
amount=400,
cost_center=cost_center,
posting_date="2021-03-15",
company=company,
company="Test PCV Company",
)
jv.company = company
jv.company = "Test PCV Company"
jv.finance_book = create_finance_book().name
jv.save()
jv.submit()
@@ -182,7 +179,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertSequenceEqual(pcv_gle, expected_gle)
def test_gl_entries_restrictions(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
self.make_period_closing_voucher(posting_date="2021-03-31")
@@ -193,16 +189,15 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company=company,
company="Test PCV Company",
save=False,
)
jv1.company = company
jv1.company = "Test PCV Company"
jv1.save()
self.assertRaises(frappe.ValidationError, jv1.submit)
def test_closing_balance_with_dimensions_and_test_reposting_entry(self):
company = create_company()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")
@@ -212,10 +207,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center1,
company=company,
company="Test PCV Company",
save=False,
)
jv1.company = company
jv1.company = "Test PCV Company"
jv1.save()
jv1.submit()
@@ -225,10 +220,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
company=company,
company="Test PCV Company",
save=False,
)
jv2.company = company
jv2.company = "Test PCV Company"
jv2.save()
jv2.submit()
@@ -255,11 +250,11 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
company=company,
company="Test PCV Company",
save=False,
)
jv3.company = company
jv3.company = "Test PCV Company"
jv3.save()
jv3.submit()
@@ -294,12 +289,12 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(cc2_closing_balance.credit, 500)
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name")
warehouse = frappe.db.get_value("Warehouse", {"company": "Test PCV Company"}, "name")
repost_doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"company": company,
"company": "Test PCV Company",
"posting_date": "2020-03-15",
"based_on": "Item and Warehouse",
"item_code": "Test Item 1",
@@ -340,7 +335,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
{"enable_immutable_ledger": 1},
)
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv = make_journal_entry(
@@ -349,21 +343,24 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company=company,
company="Test PCV Company",
save=False,
)
jv.company = company
jv.company = "Test PCV Company"
jv.save()
jv.submit()
self.make_period_closing_voucher(posting_date="2021-03-31")
# Passed posting_date is after PCV end date, so cancellation should not fail.
make_reverse_gl_entries(
voucher_type="Journal Entry",
voucher_no=jv.name,
posting_date="2022-01-01",
)
frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", "2021-12-31")
try:
make_reverse_gl_entries(
voucher_type="Journal Entry",
voucher_no=jv.name,
)
finally:
frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", None)
totals_after_cancel = frappe.db.sql(
"""
@@ -378,19 +375,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
def create_company():
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": "Test PCV Company",
"country": "United States",
"default_currency": "USD",
}
)
company.insert(ignore_if_duplicate=True)
return company.name
def create_account():
account = frappe.get_doc(
{

View File

@@ -156,6 +156,24 @@ class PricingRule(Document):
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
if self.apply_on == "Item Code":
self.validate_template_with_variant(values)
def validate_template_with_variant(self, item_codes):
# throws if a template and its variant both exist in one rule
variants = frappe.get_all(
"Item",
filters={"name": ("in", item_codes), "variant_of": ("in", item_codes)},
fields=["name", "variant_of"],
)
if variants:
variant = variants[0]
frappe.throw(
_("Variant {0} and its template {1} cannot both be added to the same Pricing Rule").format(
frappe.bold(variant.name), frappe.bold(variant.variant_of)
)
)
def validate_mandatory(self):
if self.has_priority and not self.priority:
throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))

View File

@@ -333,6 +333,31 @@ class TestPricingRule(ERPNextTestSuite):
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 17.5)
def test_pricing_rule_with_template_and_its_variant(self):
if not frappe.db.exists("Item", "Test Variant PRT"):
variant = frappe.new_doc("Item")
variant.item_code = "Test Variant PRT"
variant.item_name = "Test Variant PRT"
variant.item_group = "_Test Item Group"
variant.is_stock_item = 1
variant.variant_of = "_Test Variant Item"
variant.stock_uom = "_Test UOM"
variant.append("attributes", {"attribute": "Test Size", "attribute_value": "Medium"})
variant.insert()
rule = frappe.new_doc("Pricing Rule")
rule.title = "_Test Pricing Rule Template Variant"
rule.apply_on = "Item Code"
rule.currency = "USD"
rule.selling = 1
rule.rate_or_discount = "Discount Percentage"
rule.discount_percentage = 10
rule.company = "_Test Company"
rule.append("items", {"item_code": "_Test Variant Item"})
rule.append("items", {"item_code": "Test Variant PRT"})
self.assertRaises(frappe.ValidationError, rule.insert)
def test_pricing_rule_for_stock_qty(self):
test_record = {
"doctype": "Pricing Rule",

View File

@@ -89,48 +89,55 @@ class ProcessPeriodClosingVoucher(Document):
cancel_pcv_processing(self.name)
def initialize_parallel_threads(docname: str):
threads = 4
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
if normal_balances := (
qb.from_(ppcvd)
.select(ppcvd.name, ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
.limit(threads)
.for_update(skip_locked=True)
.run(as_dict=True)
):
if not is_scheduler_inactive():
for x in normal_balances:
frappe.db.set_value(
"Process Period Closing Voucher Detail",
x.name,
"status",
"Running",
)
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout=timeout,
is_async=True,
enqueue_after_commit=True,
docname=docname,
row_name=x.name,
date=x.processing_date,
report_type=x.report_type,
parentfield=x.parentfield,
)
# keep transaction on PPCV and PPCVD short
# prevents concurrency errors - REPEATABLE READ
if not frappe.in_test:
frappe.db.commit() # nosemgrep
else:
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
@frappe.whitelist()
def start_pcv_processing(docname: str):
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
if normal_balances := (
qb.from_(ppcvd)
.select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
.limit(4)
.for_update(skip_locked=True)
.run(as_dict=True)
):
if not is_scheduler_inactive():
for x in normal_balances:
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{
"processing_date": x.processing_date,
"parent": docname,
"report_type": x.report_type,
"parentfield": x.parentfield,
},
"status",
"Running",
)
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout="3600",
is_async=True,
enqueue_after_commit=True,
docname=docname,
date=x.processing_date,
report_type=x.report_type,
parentfield=x.parentfield,
)
else:
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
frappe.has_permission("Process Period Closing Voucher", "write", doc=docname, throw=True)
initialize_parallel_threads(docname)
@frappe.whitelist()
@@ -247,10 +254,12 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
@frappe.whitelist()
def schedule_next_date(docname: str):
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
if to_process := (
qb.from_(ppcvd)
.select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
.select(ppcvd.name, ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
.limit(1)
@@ -260,22 +269,23 @@ def schedule_next_date(docname: str):
if not is_scheduler_inactive():
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{
"processing_date": to_process[0].processing_date,
"parent": docname,
"report_type": to_process[0].report_type,
"parentfield": to_process[0].parentfield,
},
to_process[0].name,
"status",
"Running",
)
# keep transaction on PPCV and PPCVD short
# prevents concurrency errors - REPEATABLE READ
if not frappe.in_test:
frappe.db.commit() # nosemgrep
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout="3600",
timeout=timeout,
is_async=True,
enqueue_after_commit=True,
docname=docname,
row_name=to_process[0].name,
date=to_process[0].processing_date,
report_type=to_process[0].report_type,
parentfield=to_process[0].parentfield,
@@ -302,7 +312,7 @@ def schedule_next_date(docname: str):
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries",
queue="long",
timeout="3600",
timeout=timeout,
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
@@ -440,6 +450,11 @@ def summarize_and_post_ledger_entries(docname):
make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date)
# keep transaction on PPCV and PPCVD short
# prevents concurrency errors - REPEATABLE READ
if not frappe.in_test:
frappe.db.commit() # nosemgrep
frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed")
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
@@ -525,10 +540,10 @@ def build_dimension_wise_balance_dict(gl_entries):
return dimension_balances
def process_individual_date(docname: str, date, report_type, parentfield):
def process_individual_date(docname: str, row_name, date, report_type, parentfield):
current_date_status = frappe.db.get_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "report_type": report_type, "parentfield": parentfield},
row_name,
"status",
)
if current_date_status != "Running":
@@ -575,17 +590,20 @@ def process_individual_date(docname: str, date, report_type, parentfield):
# save results
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
row_name,
"closing_balance",
frappe.json.dumps(res),
)
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
row_name,
"status",
"Completed",
)
# commit heavy computation before touching PPCV or PPCVD
if not frappe.in_test:
frappe.db.commit() # nosemgrep
# chain call
schedule_next_date(docname)

View File

@@ -48,18 +48,27 @@ class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
ppcv.save()
return ppcv
def set_processing_date_status(self, date, ppcv, rpt_type, parentfield, status):
def set_processing_date_status(self, row_name, status):
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield},
row_name,
"status",
status,
)
def get_processing_date_closing_balance(self, date, ppcv, rpt_type, parentfield):
def get_row_name(self, ppcv_name, rpt_type, parentfield):
return frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": ppcv_name, "report_type": rpt_type, "parentfield": parentfield},
order_by="report_type, idx",
pluck="name",
limit=1,
)[0]
def get_processing_date_closing_balance(self, row_name):
return frappe.db.get_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield},
row_name,
"closing_balance",
)
@@ -97,11 +106,10 @@ class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
parentfield = "normal_balances"
rpt_type = "Profit and Loss"
# status has to be set to 'Running' for logic to run
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
bal = frappe.parse_json(
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
)
row_name = self.get_row_name(ppcv.name, rpt_type, parentfield)
self.set_processing_date_status(row_name, "Running")
process_individual_date(ppcv.name, row_name, today(), rpt_type, parentfield)
bal = frappe.parse_json(self.get_processing_date_closing_balance(row_name))
self.assertEqual(len(bal), 1)
expected_pl = {
"account": "Sales - _TC",
@@ -117,11 +125,10 @@ class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
# Balance sheet balance
rpt_type = "Balance Sheet"
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
bal = frappe.parse_json(
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
)
row_name = self.get_row_name(ppcv.name, rpt_type, parentfield)
self.set_processing_date_status(row_name, "Running")
process_individual_date(ppcv.name, row_name, today(), rpt_type, parentfield)
bal = frappe.parse_json(self.get_processing_date_closing_balance(row_name))
self.assertEqual(len(bal), 1)
expected_bs = {
"account": "Debtors - _TC",
@@ -138,11 +145,10 @@ class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
# Opening balance
parentfield = "z_opening_balances"
rpt_type = "Balance Sheet"
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
bal = frappe.parse_json(
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
)
row_name = self.get_row_name(ppcv.name, rpt_type, parentfield)
self.set_processing_date_status(row_name, "Running")
process_individual_date(ppcv.name, row_name, today(), rpt_type, parentfield)
bal = frappe.parse_json(self.get_processing_date_closing_balance(row_name))
self.assertEqual(len(bal), 2)
opening_cash = next(x for x in bal if x["account"] == "Cash - _TC")
expected_opening_cash = {

View File

@@ -1,7 +1,7 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
import frappe
from frappe.model.document import Document
@@ -24,3 +24,10 @@ class ProcessPeriodClosingVoucherDetail(Document):
# end: auto-generated types
pass
def on_doctype_update():
frappe.db.add_index(
"Process Period Closing Voucher Detail",
["parent", "status", "parentfield", "idx", "processing_date"],
)

View File

@@ -13,7 +13,7 @@
</div>
{% endif %}
</div>
<h2 class="text-center">{{ _("GENERAL LEDGER") }}</h2>
<h2 class="text-center">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
<div>
{% if filters.party[0] == filters.party_name[0] %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>

View File

@@ -99,9 +99,9 @@ class ProcessStatementOfAccounts(Document):
if not self.pdf_name:
self.pdf_name = "{{ customer.customer_name }}"
validate_template(self.subject)
validate_template(self.body)
validate_template(self.pdf_name)
validate_template(self.subject, restrict_globals=True)
validate_template(self.body, restrict_globals=True)
validate_template(self.pdf_name, restrict_globals=True)
if not self.customers:
frappe.throw(_("Customers not selected."))
@@ -416,7 +416,6 @@ def get_context(customer, doc):
return {
"doc": template_doc,
"customer": frappe.get_doc("Customer", customer),
"frappe": frappe.utils,
}
@@ -466,6 +465,8 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
when Is Billing Contact checked
and Primary email- email with Is Primary checked"""
frappe.has_permission("Customer", "read", customer_name, throw=True)
billing_email = frappe.db.sql(
"""
SELECT
@@ -509,6 +510,7 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
@frappe.whitelist()
def download_statements(document_name):
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
doc.check_permission("read")
report = get_report_pdf(doc)
if report:
frappe.local.response.filename = doc.name + ".pdf"
@@ -525,15 +527,15 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
if report:
for customer, report_pdf in report.items():
context = get_context(customer, doc)
filename = frappe.render_template(doc.pdf_name, context)
filename = frappe.render_template(doc.pdf_name, context, restrict_globals=True)
attachments = [{"fname": filename + ".pdf", "fcontent": report_pdf}]
recipients, cc = get_recipients_and_cc(customer, doc)
if not recipients:
continue
subject = frappe.render_template(doc.subject, context)
message = frappe.render_template(doc.body, context)
subject = frappe.render_template(doc.subject, context, restrict_globals=True)
message = frappe.render_template(doc.body, context, restrict_globals=True)
if doc.sender:
sender_email = frappe.db.get_value("Email Account", doc.sender, "email_id")

View File

@@ -21,10 +21,8 @@ class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin):
letterhead.is_default = 0
letterhead.save()
self.create_company()
self.create_customer()
self.company = "_Test Company"
self.create_customer(customer_name="Other Customer")
self.clear_old_entries()
self.si = create_sales_invoice()
create_sales_invoice(customer="Other Customer")

View File

@@ -1388,8 +1388,10 @@
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company"
"options": "Company",
"read_only": 1
},
{
"depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
@@ -1700,7 +1702,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-06-13 18:36:46.704623",
"modified": "2026-07-12 23:54:21.263951",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -1433,6 +1433,10 @@ class PurchaseInvoice(BuyingController):
# tax table gl entries
valuation_tax = {}
# Amount of each valuation charge actually capitalized into stock/asset valuation, keyed by
# tax row name - a non-stock item's share of a spread-across-all-items charge is excluded.
capitalized_valuation_tax = self.get_capitalized_valuation_tax()
for tax in self.get("taxes"):
amount, base_amount = self.get_tax_amounts(tax, None)
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
@@ -1469,8 +1473,7 @@ class PurchaseInvoice(BuyingController):
tax.idx, _(tax.category)
)
)
valuation_tax.setdefault(tax.name, 0)
valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
valuation_tax[tax.name] = capitalized_valuation_tax.get(tax.name, 0.0)
if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax:
# credit valuation tax amount in "Expenses Included In Valuation"

View File

@@ -2928,6 +2928,24 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
# Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail
self.assertRaises(frappe.ValidationError, pi.submit)
@ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_non_stock_item_over_billing_against_po_is_blocked(self):
service_item = create_item(
"_Test Service Item Non Stock PI",
is_stock_item=0,
is_purchase_item=1,
).name
po = create_purchase_order(item_code=service_item, qty=5, rate=100, do_not_save=False)
po.submit()
pi = make_pi_from_po(po.name)
pi.items[0].qty = 10 # overbill by 100 %
pi.save()
with self.assertRaises(frappe.ValidationError):
pi.submit()
def test_discount_percentage_not_set_when_amount_is_manually_set(self):
pi = make_purchase_invoice(do_not_save=True)
discount_amount = 7
@@ -3008,6 +3026,14 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
party_link.delete()
def test_purchase_invoice_cancellation_post_account_freezing_date(self):
pi = make_purchase_invoice()
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", add_days(getdate(), 1))
try:
self.assertRaises(frappe.ValidationError, pi.cancel)
finally:
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -11,13 +11,16 @@
"add_deduct_tax",
"charge_type",
"row_id",
"included_in_print_rate",
"included_in_paid_amount",
"col_break1",
"account_head",
"description",
"section_break_mvae",
"is_tax_withholding_account",
"set_by_item_tax_template",
"allocate_full_amount_to_stock_items",
"column_break_odzz",
"included_in_print_rate",
"included_in_paid_amount",
"section_break_10",
"rate",
"accounting_dimensions_section",
@@ -78,6 +81,15 @@
"oldfieldname": "row_id",
"oldfieldtype": "Data"
},
{
"default": "1",
"depends_on": "eval:doc.charge_type=='Actual' && ['Valuation', 'Valuation and Total'].includes(doc.category)",
"description": "If checked, the entire amount (e.g. Freight) is allocated to the valuation of stock & asset items only. If unchecked, the amount is distributed across all items and the portion belonging to non-stock items is not added to valuation.",
"fieldname": "allocate_full_amount_to_stock_items",
"fieldtype": "Check",
"label": "Allocate Full Amount to Stock Items",
"show_description_on_click": 1
},
{
"default": "0",
"description": "If checked, the tax amount will be considered as already included in the Print Rate / Print Amount",
@@ -272,13 +284,21 @@
"label": "Don't Recompute Tax",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_mvae",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_odzz",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-11-24 18:22:56.886010",
"modified": "2026-06-21 17:08:57.096729",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",

View File

@@ -17,6 +17,7 @@ class PurchaseTaxesandCharges(Document):
account_currency: DF.Link | None
account_head: DF.Link
add_deduct_tax: DF.Literal["Add", "Deduct"]
allocate_full_amount_to_stock_items: DF.Check
base_net_amount: DF.Currency
base_tax_amount: DF.Currency
base_tax_amount_after_discount_amount: DF.Currency

View File

@@ -460,8 +460,8 @@ class SalesInvoice(SellingController):
validate_account_head(item.idx, item.income_account, self.company, _("Income"))
def before_save(self):
self.set_account_for_mode_of_payment()
self.set_paid_amount()
self.set_account_for_mode_of_payment()
def before_submit(self):
self.add_remarks()
@@ -900,6 +900,13 @@ class SalesInvoice(SellingController):
def set_paid_amount(self):
paid_amount = 0.0
base_paid_amount = 0.0
if not cint(self.is_pos) and self.is_return:
self.set("payments", [])
self.paid_amount = paid_amount
self.base_paid_amount = base_paid_amount
return
for data in self.payments:
data.base_amount = flt(data.amount * self.conversion_rate, self.precision("base_paid_amount"))
paid_amount += data.amount
@@ -3149,8 +3156,6 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
from frappe.model.mapper import get_mapped_doc
def postprocess_dunning(source, target):
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text
dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1, "company": source.company})
if dunning_type:
dunning_type = frappe.get_doc("Dunning Type", dunning_type)
@@ -3159,14 +3164,8 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
target.dunning_fee = dunning_type.dunning_fee
target.income_account = dunning_type.income_account
target.cost_center = dunning_type.cost_center
letter_text = get_dunning_letter_text(
dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language
)
if letter_text:
target.body_text = letter_text.get("body_text")
target.closing_text = letter_text.get("closing_text")
target.language = letter_text.get("language")
target.language = source.language
target.get_dunning_letter_text()
# update outstanding from doc
if source.payment_schedule and len(source.payment_schedule) == 1:

View File

@@ -2150,11 +2150,14 @@ class TestSalesInvoice(ERPNextTestSuite):
def test_create_so_with_margin(self):
si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True)
price_list_rate = flt(100) * flt(si.plc_conversion_rate)
si.items[0].price_list_rate = price_list_rate
si.items[0].margin_type = "Percentage"
si.items[0].margin_rate_or_amount = 25
si.items[0].discount_amount = 0.0
si.items[0].discount_percentage = 0.0
# set rate to zero, so that it is recalculated on save
si.items[0].rate = 0
si.save()
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate))
@@ -3865,6 +3868,51 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertTrue("cannot overbill" in str(err.exception).lower())
dn.cancel()
@ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_non_stock_item_over_billing_against_so_is_blocked(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice as make_si_from_so
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
service_item = create_item(
"_Test Service Item Non Stock SI",
is_stock_item=0,
).name
so = make_sales_order(item_code=service_item, qty=5, rate=100)
so.submit()
si = make_si_from_so(so.name)
si.items[0].qty = 10 # overbill by 100 %
si.save()
with self.assertRaises(frappe.ValidationError):
si.submit()
@ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_non_stock_item_over_billing_against_so_from_quotation_is_blocked(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order as make_so_from_quotation
from erpnext.selling.doctype.quotation.test_quotation import make_quotation
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice as make_si_from_so
service_item = create_item(
"_Test Service Item Non Stock SI Quot",
is_stock_item=0,
).name
quotation = make_quotation(item_code=service_item, qty=5, rate=100)
so = make_so_from_quotation(quotation.name)
so.delivery_date = frappe.utils.add_days(frappe.utils.today(), 7)
so.insert()
so.submit()
si = make_si_from_so(so.name)
si.items[0].qty = 10 # overbill by 100 %
si.save()
with self.assertRaises(frappe.ValidationError):
si.submit()
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{

View File

@@ -16,12 +16,14 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_supplier()
self.create_usd_receivable_account()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.supplier = "_Test Supplier"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
self.cash = "Cash - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
def create_sales_invoice(self, do_not_submit=False):
si = create_sales_invoice(
@@ -372,7 +374,6 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(so.advance_paid, 0)
def test_06_unreconcile_advance_from_payment_entry(self):
self.enable_advance_as_liability()
so1 = self.create_sales_order()
so2 = self.create_sales_order()
@@ -423,7 +424,11 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
self.disable_advance_as_liability()
def test_07_adv_from_so_to_invoice(self):
self.enable_advance_as_liability()
frappe.db.set_value("Company", self.company, "book_advance_payments_in_separate_party_account", True)
frappe.db.set_value(
"Company", self.company, "default_advance_received_account", "Advance Received - _TC"
)
so = self.create_sales_order()
pe = self.create_payment_entry()
pe.paid_amount = 1000

View File

@@ -716,13 +716,15 @@ def make_reverse_gl_entries(
partial_cancel=partial_cancel,
)
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
# For reverse entries, use the posting_date parameter if provided and valid
# Otherwise fall back to original posting_date
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
if immutable_ledger_enabled:
validation_date = posting_date or frappe.form_dict.get("posting_date") or getdate()
else:
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
check_freezing_date(validation_date, gl_entries[0]["company"], adv_adj)
validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"])
if partial_cancel:
@@ -789,7 +791,7 @@ def make_reverse_gl_entries(
if immutable_ledger_enabled:
new_gle["is_cancelled"] = 0
new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
new_gle["posting_date"] = posting_date or frappe.form_dict.get("posting_date") or getdate()
elif posting_date:
new_gle["posting_date"] = posting_date
@@ -821,13 +823,24 @@ def check_freezing_date(posting_date, company, adv_adj=False):
)
def validate_against_pcv(is_opening, posting_date, company):
if is_opening and frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}):
def validate_opening_entry_against_pcv(company):
if frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}):
frappe.throw(
_("Opening Entry can not be created after Period Closing Voucher is created."),
_(
"A Period Closing Voucher is already submitted and an Opening Entry can no longer be created. {0} to learn more."
).format(
'<a href="https://docs.frappe.io/erpnext/period-closing-voucher#14-pcv-and-opening-entries" target="_blank" rel="noopener">'
+ _("Read the docs")
+ "</a>"
),
title=_("Invalid Opening Entry"),
)
def validate_against_pcv(is_opening, posting_date, company):
if is_opening:
validate_opening_entry_against_pcv(company)
last_pcv_date = frappe.db.get_value(
"Period Closing Voucher", {"docstatus": 1, "company": company}, [{"MAX": "period_end_date"}]
)

View File

@@ -0,0 +1,26 @@
{
"align": "Left",
"content": "<table class=\"invoice-header\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"logo-cell\" style=\"vertical-align:middle ! important\">\n\t\t\t\t<div class=\"logo-container\">\n\t\t\t\t\t{% set company_logo = frappe.db.get_value(\"Company\", doc.company, \"company_logo\") if doc.get(\"company\") else None %} {% if\n\t\t\t\t\tcompany_logo %}\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company_logo) }}\" alt=\"Company Logo\">\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td class=\"company-details\">\n\t\t\t\t{% if doc.company %}<div class=\"company-name\">{{ doc.company }}</div>{% endif %}\n\t\t\t\t{% if doc.company_address %} {% set company_address = frappe.db.get_value(\"Address\",\n\t\t\t\tdoc.company_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\",\n\t\t\t\t\"country\"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =\n\t\t\t\tfrappe.db.get_value(\"Address\", doc.billing_address, [\"address_line1\", \"address_line2\", \"city\",\n\t\t\t\t\"state\", \"pincode\", \"country\"], as_dict=True) %} {% endif %} {% if company_address %} {{\n\t\t\t\tcompany_address.address_line1 or \"\" }}<br>\n\t\t\t\t{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br>\n\t\t\t\t{% endif %} {{ company_address.city or \"\" }}, {{ company_address.state or \"\" }} {{\n\t\t\t\tcompany_address.pincode or \"\" }}, {{ company_address.country or \"\"}}<br>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t\t<td class=\"invoice-info-cell\">\n\t\t\t\t{% set website = frappe.db.get_value(\"Company\", doc.company, \"website\") if doc.get(\"company\") else None %} {% set email =\n\t\t\t\tfrappe.db.get_value(\"Company\", doc.company, \"email\") if doc.get(\"company\") else None %} {% set phone_no =\n\t\t\t\tfrappe.db.get_value(\"Company\", doc.company, \"phone_no\") if doc.get(\"company\") else None %}\n\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ doc.doctype }}</span>\n\t\t\t\t\t<span>{{ doc.name }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% if website %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Website:\") }}</span>\n\t\t\t\t\t<span>{{ website }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %} {% if email %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Email:\") }}</span>\n\t\t\t\t\t<span>{{ email }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %} {% if phone_no %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Contact:\") }}</span>\n\t\t\t\t\t<span>{{ phone_no }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>",
"creation": "2026-05-15 15:21:48.255627",
"custom_css": "\t.letter-head {\n\t\tborder-radius: 18px;\n\t\tpadding-right: 12px;\n\t\tmargin-left: 12px;\n\t\tmargin-right: 12px;\n\t}\n\n\t.letter-head td {\n\t\tpadding: 0px !important;\n\t}\n\t.invoice-header {\n\t\twidth: 100%;\n\t}\n\t.logo-cell {\n\t\twidth: 100px;\n\t\ttext-align: center;\n\t\tposition: relative;\n\t}\n\t.logo-container {\n\t\twidth: 90px;\n\t\tdisplay: block;\n\t}\n\t.logo-container img {\n\t\tmax-width: 90px;\n\t\tmax-height: 90px;\n\t\tdisplay: inline-block;\n\t\tborder-radius: 15px;\n\t}\n\t.company-details {\n\t\twidth: 40%;\n\t\talign-content: center;\n\t}\n\t.company-name {\n\t\tfont-size: 14px;\n\t\tfont-weight: bold;\n\t\tcolor: #171717;\n\t\tmargin-bottom: 4px;\n\t}\n\t.invoice-info-cell {\n\t\tfloat: right;\n\t\tvertical-align: top;\n\t}\n\t.invoice-info {\n\t\tmargin-bottom: 2px;\n\t}\n\t.invoice-label {\n\t\tcolor: #7c7c7c;\n\t\tdisplay: inline-block;\n\t\tmargin-right: 5px;\n\t}",
"disabled": 0,
"docstatus": 0,
"doctype": "Letter Head",
"footer_align": "Left",
"footer_image_height": 0.0,
"footer_image_width": 0.0,
"footer_source": "Image",
"idx": 0,
"image_height": 0.0,
"image_width": 0.0,
"is_default": 0,
"letter_head_for": "DocType",
"letter_head_name": "Company Letterhead",
"modified": "2026-07-12 21:11:44.765083",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Company Letterhead",
"owner": "Administrator",
"source": "HTML",
"standard": "Yes"
}

View File

@@ -0,0 +1,26 @@
{
"align": "Left",
"content": "<table class=\"letterhead-container\" style=\"width:100%\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"logo-address\">\n\t\t\t\t{% set company_logo = frappe.db.get_value(\"Company\", doc.company, \"company_logo\") if doc.get(\"company\") else None %} {% if\n\t\t\t\tcompany_logo %}\n\t\t\t\t<div class=\"logo\">\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company_logo) }}\" style=\"width:200px\">\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t\t{% if doc.company %}<div class=\"company-name\">{{ doc.company }}</div>{% endif %}\n\t\t\t\t<div class=\"company-address\">\n\t\t\t\t\t{% if doc.company_address %} {% set company_address = frappe.db.get_value(\"Address\",\n\t\t\t\t\tdoc.company_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\",\n\t\t\t\t\t\"country\"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =\n\t\t\t\t\tfrappe.db.get_value(\"Address\", doc.billing_address, [\"address_line1\", \"address_line2\",\n\t\t\t\t\t\"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %} {% endif %} {% if company_address\n\t\t\t\t\t%} {{ company_address.address_line1 or \"\" }}<br>\n\t\t\t\t\t{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br>\n\t\t\t\t\t{% endif %} {{ company_address.city or \"\" }}, {{ company_address.state or \"\" }} {{\n\t\t\t\t\tcompany_address.pincode or \"\" }}, {{ company_address.country or \"\"}}<br>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td style=\"vertical-align:top\">\n\t\t\t\t<div style=\"height:90px;margin-bottom:10px;text-align:right\">\n\t\t\t\t\t<div class=\"invoice-title\">{{ doc.doctype }}</div>\n\t\t\t\t\t<div class=\"invoice-number\">{{ doc.name }}</div>\n\t\t\t\t\t<br>\n\t\t\t\t</div>\n\t\t\t\t<div style=\"text-align:left;float:right\" class=\"other-details\">\n\t\t\t\t\t{% if doc.company %}{% set company_details = frappe.db.get_value(\"Company\", doc.company, [\"website\", \"email\",\n\t\t\t\t\t\"phone_no\"], as_dict=True) %}{% set website = company_details.website %}{% set email =\n\t\t\t\t\tcompany_details.email %}{% set phone_no = company_details.phone_no %}{% else %}{% set website = None %}{% set email = None %}{% set phone_no = None %}{% endif %} {% if website %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Website:\") }}</span><span class=\"contact-value\">{{ website }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %} {% if email %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Email:\") }}</span><span class=\"contact-value\">{{ email }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %} {% if phone_no %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Contact:\") }}</span><span class=\"contact-value\">{{ phone_no }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>\n",
"creation": "2026-05-15 15:21:48.373815",
"custom_css": "\t.print-format-preview {\n\t\tmargin-top: 12px;\n\t}\n\t.letter-head {\n\t\tborder-radius: 18px;\n\t\tbackground: #f8f8f8;\n\t\tpadding: 12px;\n\t\tmargin-left: 12px;\n\t\tmargin-right: 12px;\n\t}\n\t.letterhead-container {\n\t\twidth: 100%;\n\t}\n\t.letterhead-container .other-details {\n\t\tposition: absolute;\n\t\tright: 0;\n\t\tbottom: 0;\n\t}\n\t.logo-address {\n\t\twidth: 65%;\n\t\tvertical-align: top;\n\t}\n\n\t.letter-head .logo {\n\t\twidth: 90px;\n\t\tdisplay: block;\n\t\tmargin-bottom: 10px;\n\t}\n\n\t.letter-head .logo img {\n\t\tborder-radius: 15px;\n\t}\n\n\t.company-name {\n\t\tcolor: #171717;\n\t\tfont-weight: bold;\n\t\tline-height: 23px;\n\t\tmargin-bottom: 5px;\n\t}\n\n\t.company-address {\n\t\tcolor: #171717;\n\t\twidth: 300px;\n\t}\n\n\t.invoice-title {\n\t\tfont-weight: bold;\n\t}\n\n\t.invoice-number {\n\t\tcolor: #7c7c7c;\n\t}\n\n\t.contact-title {\n\t\tcolor: #7c7c7c;\n\t\twidth: 60px;\n\t\tdisplay: inline-block;\n\t\tvertical-align: top;\n\t\tmargin-right: 10px;\n\t}\n\n\t.contact-value {\n\t\tcolor: #171717;\n\t\tdisplay: inline-block;\n\t}\n\t.letterhead-container td {\n\t\tpadding: 0px !important;\n\t\tposition: relative;\n\t}",
"disabled": 0,
"docstatus": 0,
"doctype": "Letter Head",
"footer_align": "Left",
"footer_image_height": 0.0,
"footer_image_width": 0.0,
"footer_source": "Image",
"idx": 0,
"image_height": 0.0,
"image_width": 0.0,
"is_default": 0,
"letter_head_for": "DocType",
"letter_head_name": "Company Letterhead - Grey",
"modified": "2026-07-12 22:03:24.525672",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Company Letterhead - Grey",
"owner": "Administrator",
"source": "HTML",
"standard": "Yes"
}

View File

@@ -0,0 +1,26 @@
{
"align": "Left",
"content": "<table class=\"invoice-header\">\n\t<tbody>\n\t\t<tr>\n\n\t\t\t<td class=\"logo-cell\" style=\"vertical-align:top\">\n\t\t\t\t{% if doc.company %}{% set company = frappe.get_doc(\"Company\", doc.company) %}{% else %}{% set company = frappe._dict() %}{% endif %}\n\n\t\t\t\t<div class=\"logo-container\">\n\t\t\t\t\t{% if company.company_logo %}\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company.company_logo) }}\" alt=\"Company Logo\">\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td class=\"company-details\" style=\"vertical-align:top\">\n\t\t\t\t{% if company.name %}<div class=\"company-name\">{{ company.name }}</div>{% endif %}\n\n\t\t\t\t{% set company_address_name = frappe.db.get_value(\n\t\t\t\t\t\"Dynamic Link\",\n\t\t\t\t\t{\n\t\t\t\t\t\t\"link_doctype\": \"Company\",\n\t\t\t\t\t\t\"link_name\": company.name,\n\t\t\t\t\t\t\"parenttype\": \"Address\"\n\t\t\t\t\t},\n\t\t\t\t\t\"parent\"\n\t\t\t\t) %}\n\n\t\t\t\t{% if company_address_name %}\n\t\t\t\t\t{% set company_address = frappe.db.get_value(\n\t\t\t\t\t\t\"Address\",\n\t\t\t\t\t\tcompany_address_name,\n\t\t\t\t\t\t[\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"],\n\t\t\t\t\t\tas_dict=True\n\t\t\t\t\t) %}\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if company_address %}\n\t\t\t\t<div class=\"company-address\">\n\t\t\t\t\t{{ company_address.address_line1 or \"\" }}\n\n\t\t\t\t\t{% if company_address.address_line2 %}\n\t\t\t\t\t\t<br>{{ company_address.address_line2 }}\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t<br>\n\n\t\t\t\t\t{{ company_address.city or \"\" }}\n\t\t\t\t\t{% if company_address.state %}, {{ company_address.state }}{% endif %}\n\t\t\t\t\t{{ company_address.pincode or \"\" }}\n\n\t\t\t\t\t{% if company_address.country %}\n\t\t\t\t\t\t, {{ company_address.country }}\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t\t<td class=\"invoice-info-cell\" style=\"vertical-align:top;text-align:right\">\n\n\t\t\t\t{% set website = frappe.db.get_value(\"Company\", doc.company, \"website\") %}\n\t\t\t\t{% set email = frappe.db.get_value(\"Company\", doc.company, \"email\") %}\n\t\t\t\t{% set phone_no = frappe.db.get_value(\"Company\", doc.company, \"phone_no\") %}\n\n\t\t\t\t{% if website %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Website:\") }}</span>\n\t\t\t\t\t<span>{{ website }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if email %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Email:\") }}</span>\n\t\t\t\t\t<span>{{ email }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if phone_no %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Contact:\") }}</span>\n\t\t\t\t\t<span>{{ phone_no }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t</tr>\n\t</tbody>\n</table>",
"creation": "2026-05-15 19:49:47.582252",
"custom_css": ".letter-head {\n\tborder-radius: 18px;\n\tpadding: 8px 10px;\n\tmargin: 10px 0 14px;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tcolor: #171717;\n}\n\n.letter-head td {\n\tpadding: 0 !important;\n\tvertical-align: middle;\n}\n\n.invoice-header {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\ttable-layout: fixed;\n\tborder-bottom: 1px solid #ededed;\n\tpadding-bottom: 10px;\n}\n\n.logo-cell {\n\twidth: 100px;\n\ttext-align: center;\n\twhite-space: nowrap;\n}\n\n.logo-container {\n\tdisplay: inline-block;\n\tmargin: auto;\n}\n\n.logo-container img {\n\tmax-width: 95px;\n\tmax-height: 95px;\n\tdisplay: block;\n\tborder-radius: 12px;\n}\n\n.company-details {\n\twidth: 55%;\n\tpadding-left: 10px !important;\n\tline-height: 1.5;\n}\n\n.company-name {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: #171717;\n\tmargin-bottom: 4px;\n}\n\n.company-address {\n\tfont-size: 14px;\n\tline-height: 1.5;\n\tcolor: #171717;\n}\n\n.invoice-info-cell {\n\twidth: 240px;\n\ttext-align: right;\n\tvertical-align: top !important;\n\tline-height: 1.5;\n}\n\n.document-name {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: #171717;\n\tmargin-bottom: 6px;\n}\n\n.invoice-info {\n\tfont-size: 14px;\n\tcolor: #171717;\n\tmargin-bottom: 2px;\n\tfont-variant-numeric: tabular-nums;\n}\n\n.invoice-label {\n\tcolor: #7c7c7c;\n\tfont-weight: 500;\n\tmargin-right: 4px;\n\tdisplay: inline-block;\n}",
"disabled": 0,
"docstatus": 0,
"doctype": "Letter Head",
"footer_align": "Left",
"footer_image_height": 0.0,
"footer_image_width": 0.0,
"footer_source": "Image",
"idx": 0,
"image_height": 0.0,
"image_width": 0.0,
"is_default": 0,
"letter_head_for": "Report",
"letter_head_name": "Company Letterhead Report",
"modified": "2026-06-24 18:06:39.820968",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Company Letterhead Report",
"owner": "Administrator",
"source": "HTML",
"standard": "Yes"
}

View File

@@ -428,6 +428,17 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
Will first search in party (Customer / Supplier) record, if not found,
will search in group (Customer Group / Supplier Group),
finally will return default."""
def account_perm_check(account):
ptype = "select" if frappe.only_has_select_perm("Account") else "read"
if frappe.has_permission("Account", ptype, account):
return
# Using custom message to prevent data leak in case of `apply_strict_permission` is enabled.
frappe.throw(
_("User don't have permissions to select/read this account."), exc=frappe.PermissionError
)
if not party_type:
frappe.throw(_("Party Type is mandatory"))
if not company:
@@ -438,46 +449,51 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
"default_receivable_account" if party_type == "Customer" else "default_payable_account"
)
return frappe.get_cached_value("Company", company, default_account_name)
account = frappe.db.get_value(
"Party Account", {"parenttype": party_type, "parent": party, "company": company}, "account"
)
if not account and party_type in ["Customer", "Supplier"]:
party_group_doctype = "Customer Group" if party_type == "Customer" else "Supplier Group"
group = frappe.get_cached_value(party_type, party, scrub(party_group_doctype))
account = frappe.get_cached_value("Company", company, default_account_name)
else:
account = frappe.db.get_value(
"Party Account",
{"parenttype": party_group_doctype, "parent": group, "company": company},
"account",
"Party Account", {"parenttype": party_type, "parent": party, "company": company}, "account"
)
if not account and party_type in ["Customer", "Supplier"]:
default_account_name = (
"default_receivable_account" if party_type == "Customer" else "default_payable_account"
)
account = frappe.get_cached_value("Company", company, default_account_name)
if not account and party_type in ["Customer", "Supplier"]:
party_group_doctype = "Customer Group" if party_type == "Customer" else "Supplier Group"
group = frappe.get_cached_value(party_type, party, scrub(party_group_doctype))
account = frappe.db.get_value(
"Party Account",
{"parenttype": party_group_doctype, "parent": group, "company": company},
"account",
)
existing_gle_currency = get_party_gle_currency(party_type, party, company)
if existing_gle_currency:
if account:
account_currency = frappe.get_cached_value("Account", account, "account_currency")
if (account and account_currency != existing_gle_currency) or not account:
account = get_party_gle_account(party_type, party, company)
if not account and party_type in ["Customer", "Supplier"]:
default_account_name = (
"default_receivable_account" if party_type == "Customer" else "default_payable_account"
)
account = frappe.get_cached_value("Company", company, default_account_name)
# get default account on the basis of party type
if not account:
account_type = frappe.get_cached_value("Party Type", party_type, "account_type")
default_account_name = "default_" + account_type.lower() + "_account"
account = frappe.get_cached_value("Company", company, default_account_name)
existing_gle_currency = get_party_gle_currency(party_type, party, company)
if existing_gle_currency:
if account:
account_currency = frappe.get_cached_value("Account", account, "account_currency")
if (account and account_currency != existing_gle_currency) or not account:
account = get_party_gle_account(party_type, party, company)
if include_advance and party_type in ["Customer", "Supplier", "Student"]:
# get default account on the basis of party type
if not account:
account_type = frappe.get_cached_value("Party Type", party_type, "account_type")
default_account_name = "default_" + account_type.lower() + "_account"
account = frappe.get_cached_value("Company", company, default_account_name)
if account:
account_perm_check(account)
if include_advance and party and party_type in ["Customer", "Supplier", "Student"]:
advance_account = get_party_advance_account(party_type, party, company)
if advance_account:
account_perm_check(advance_account)
return [account, advance_account]
else:
return [account]
return [account]
return account
@@ -927,6 +943,15 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
if party_type == "Supplier":
info["total_unpaid"] = -1 * info["total_unpaid"]
if info["total_unpaid"] < 0:
info["balance_label"] = (
"Total Advance Paid" if party_type == "Supplier" else "Total Advance Received"
)
info["balance_amount"] = abs(info["total_unpaid"])
else:
info["balance_label"] = "Total Unpaid"
info["balance_amount"] = info["total_unpaid"]
company_wise_info.append(info)
return company_wise_info

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,13 +4,20 @@
"columns": [],
"creation": "2013-04-22 16:16:03",
"default_print_format": "Accounts Payable Standard",
"disable_prepared_report_automation": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"doctype_to_sync": [
{
"doc_type": "Payment Ledger Entry"
}
],
"filters": [],
"generate_csv": 0,
"idx": 3,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:14.716933",
"modified": "2026-07-01 13:37:41.185347",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Payable",
@@ -33,5 +40,6 @@
"role": "Auditor"
}
],
"snapshot_report": 0,
"timeout": 0
}

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