Compare commits

...

104 Commits

Author SHA1 Message Date
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
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
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
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
Frappe PR Bot
d47aa4917a chore(release): Bumped to Version 16.23.1
## [16.23.1](https://github.com/frappe/erpnext/compare/v16.23.0...v16.23.1) (2026-06-19)

### Bug Fixes

* type def in get_linked_payments (backport [#56100](https://github.com/frappe/erpnext/issues/56100)) ([#56133](https://github.com/frappe/erpnext/issues/56133)) ([6569fa2](6569fa2b9f))
2026-06-19 08:39:21 +00:00
mergify[bot]
6569fa2b9f fix: type def in get_linked_payments (backport #56100) (#56133)
fix: type def in get_linked_payments (#56100)

(cherry picked from commit 8e21af0a63)

Co-authored-by: Nikhil Kothari <nik.kothari22@live.com>
2026-06-19 08:37:40 +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
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
166 changed files with 108926 additions and 29223 deletions

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

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

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

@@ -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

@@ -1078,7 +1078,7 @@ def reconcile_vouchers(bank_transaction_name: str | int, vouchers: str, is_new_v
@frappe.whitelist()
def get_linked_payments(
bank_transaction_name: str | int,
document_types: list[str] | None = None,
document_types: str | list[str] | None = None,
from_date: str | date | None = None,
to_date: str | date | None = None,
filter_by_reference_date: bool | None = None,

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

@@ -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

@@ -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()

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
@@ -451,6 +454,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 +466,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

@@ -616,6 +616,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):

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()

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

@@ -74,29 +74,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 +122,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 +216,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"),
}
)

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": [
{
@@ -125,11 +126,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-04-29 17:08:15.617047",
"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

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

@@ -20,7 +20,6 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
self.create_company()
self.create_item()
self.create_customer()
self.clear_old_entries()
def create_company(self):
company_name = "_Test Payment Ledger"

View File

@@ -95,6 +95,8 @@ def start_pcv_processing(docname: str):
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
if normal_balances := (
qb.from_(ppcvd)
@@ -121,7 +123,7 @@ def start_pcv_processing(docname: str):
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,
@@ -247,6 +249,8 @@ 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)
@@ -272,7 +276,7 @@ def schedule_next_date(docname: str):
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,
@@ -302,7 +306,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,

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

@@ -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

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

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,7 +716,7 @@ def make_reverse_gl_entries(
partial_cancel=partial_cancel,
)
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
check_freezing_date(gl_entries[0]["posting_date"], gl_entries[0]["company"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
@@ -821,13 +821,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

@@ -9,11 +9,10 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.create_supplier(currency="USD", supplier_name="Test Supplier2")
self.create_usd_payable_account()
self.company = "_Test Company"
self.item = "_Test Item"
self.supplier = "_Test Supplier 2"
self.creditors_usd = "_Test Payable USD - _TC"
def test_accounts_payable_for_foreign_currency_supplier(self):
pi = self.create_purchase_invoice(do_not_submit=True)

View File

@@ -12,11 +12,17 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.create_usd_receivable_account()
self.clear_old_entries()
self.company = "_Test Company"
self.company_abbr = "_TC"
self.customer = "_Test Customer"
self.item = "_Test Item"
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.cash = "Cash - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
frappe.set_user("Administrator")

View File

@@ -11,10 +11,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.maxDiff = None
self.create_company()
self.create_customer()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
def test_01_receivable_summary_output(self):
"""

View File

@@ -84,7 +84,13 @@ def build_budget_map(budget_records, filters):
budget_distributions = get_budget_distributions(budget)
for row in budget_distributions:
if not row.start_date or not row.end_date:
continue
months = get_months_in_range(row.start_date, row.end_date)
if not months:
continue
monthly_budget = flt(row.amount) / len(months)
for month_date in months:

View File

@@ -12,10 +12,12 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
self.cash = "Cash - _TC"
def create_sales_invoice(self, do_not_submit=False, **args):
si = create_sales_invoice(

View File

@@ -61,11 +61,16 @@ class TestDeferredRevenueAndExpense(ERPNextTestSuite, AccountsTestMixin):
)
def setUp(self):
self.create_company()
self.create_customer("_Test Customer")
self.create_supplier("_Test Furniture Supplier")
self.company = "_Test Company"
self.company_abbr = "_TC"
self.customer = "_Test Customer"
self.supplier = "_Test Supplier"
self.warehouse = "Stores - _TC"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.setup_deferred_accounts_and_items()
self.clear_old_entries()
@ERPNextTestSuite.change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
def test_deferred_revenue(self):

View File

@@ -12,7 +12,13 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestGeneralAndPaymentLedger(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.company = "_Test Company"
self.debit_to = "Debtors - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.cost_center = "Main - _TC"
self.income_account = "Sales - _TC"
self.warehouse = "Stores - _TC"
self.creditors = "Creditors - _TC"
self.cleanup()
def cleanup(self):

View File

@@ -14,7 +14,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestGeneralLedger(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company"
self.clear_old_entries()
def clear_old_entries(self):
doctype_list = [

View File

@@ -18,8 +18,6 @@ class TestGrossProfit(ERPNextTestSuite):
self.create_item()
self.create_bundle()
self.create_customer()
self.create_sales_invoice()
self.clear_old_entries()
def create_company(self):
company_name = "_Test Gross Profit"

View File

@@ -9,9 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_supplier()
self.create_item()
self.company = "_Test Company"
self.supplier = "_Test Supplier"
self.item = "_Test Item"
def create_purchase_invoice(self, do_not_submit=False):
pi = make_purchase_invoice(

View File

@@ -9,9 +9,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
si = create_sales_invoice(

View File

@@ -14,9 +14,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestProfitAndLossStatement(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")

View File

@@ -10,9 +10,13 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
self.income_account = "Sales - _TC"
self.cash = "Cash - _TC"
self.create_child_cost_center()
def create_child_cost_center(self):

View File

@@ -9,10 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestSupplierLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_supplier()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.supplier = "_Test Supplier"
self.item = "_Test Item"
def create_purchase_invoice(self, do_not_submit=False):
frappe.set_user("Administrator")

View File

@@ -20,8 +20,7 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.clear_old_entries()
self.company = "_Test Company"
create_records()
def test_tax_withholding_for_customers(self):

View File

@@ -146,7 +146,6 @@ def get_appropriate_company(filters):
return company
@frappe.whitelist()
def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=None, with_item_data=False):
from erpnext.accounts.report.gross_profit.gross_profit import GrossProfitGenerator

View File

@@ -3,7 +3,7 @@
frappe.ui.form.on("Buying Settings", {
refresh(frm) {
if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm);
if (!frm.naming_controller) frm.naming_controller = new frappe.ui.NamingSeriesController(frm);
const display = frm.doc.supp_master_name === "Naming Series";
frm.set_df_property("naming_series_details", "hidden", !display);

View File

@@ -68,6 +68,31 @@ frappe.ui.form.on("Supplier", {
});
frm.make_methods = {
"Purchase Order": () =>
frappe.model.with_doctype("Purchase Order", function () {
const po = frappe.model.get_new_doc("Purchase Order");
po.supplier = frm.doc.name;
frappe.set_route("Form", "Purchase Order", po.name);
}),
"Purchase Invoice": () =>
frappe.model.with_doctype("Purchase Invoice", function () {
const pi = frappe.model.get_new_doc("Purchase Invoice");
pi.supplier = frm.doc.name;
frappe.set_route("Form", "Purchase Invoice", pi.name);
}),
"Request for Quotation": () =>
frappe.model.with_doctype("Request for Quotation", function () {
const rfq = frappe.model.get_new_doc("Request for Quotation");
const row = frappe.model.add_child(rfq, "suppliers");
row.supplier = frm.doc.name;
frappe.set_route("Form", "Request for Quotation", rfq.name);
}),
"Supplier Quotation": () =>
frappe.model.with_doctype("Supplier Quotation", function () {
const sq = frappe.model.get_new_doc("Supplier Quotation");
sq.supplier = frm.doc.name;
frappe.set_route("Form", "Supplier Quotation", sq.name);
}),
"Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name),
"Pricing Rule": () => frm.trigger("make_pricing_rule"),
};
@@ -117,6 +142,20 @@ frappe.ui.form.on("Supplier", {
__("View")
);
for (const doctype in frm.make_methods) {
frm.add_custom_button(__(doctype), frm.make_methods[doctype], __("Create"));
}
if (frm.doc.supplier_group) {
frm.add_custom_button(
__("Get Supplier Group Details"),
function () {
frm.trigger("get_supplier_group_details");
},
__("Actions")
);
}
if (
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
frappe.model.can_create("Party Link")
@@ -173,6 +212,8 @@ frappe.ui.form.on("Supplier", {
frm.toggle_reqd("represents_company", true);
} else {
frm.toggle_reqd("represents_company", false);
frm.set_value("represents_company", "");
frm.set_value("companies", []);
}
},
show_party_link_dialog: function (frm) {

View File

@@ -11,72 +11,76 @@
"engine": "InnoDB",
"field_order": [
"naming_series",
"supplier_type",
"supplier_name",
"supplier_type",
"alias",
"gender",
"column_break0",
"supplier_group",
"country",
"is_transporter",
"image",
"defaults_section",
"default_currency",
"default_bank_account",
"column_break_10",
"default_price_list",
"column_break2",
"supplier_details",
"column_break_30",
"website",
"language",
"customer_numbers",
"payment_terms",
"contact_and_address_tab",
"address_contacts",
"address_html",
"column_break1",
"contact_html",
"primary_address_and_contact_detail_section",
"column_break_44",
"supplier_primary_address",
"primary_address",
"column_break_mglr",
"supplier_primary_contact",
"mobile_no",
"email_id",
"tax_tab",
"tax_id",
"tax_category",
"column_break_27",
"tax_withholding_category",
"tax_withholding_group",
"accounting_tab",
"payment_terms",
"default_accounts_section",
"accounts",
"internal_supplier_section",
"is_internal_supplier",
"represents_company",
"column_break_16",
"section_break_pgad",
"companies",
"tax_tab",
"taxation_section",
"tax_id",
"tax_category",
"column_break_27",
"tax_withholding_category",
"tax_withholding_group",
"settings_tab",
"invoice_settings_section",
"is_transporter",
"allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt",
"column_break_54",
"disabled",
"rfq_and_purchase_order_settings_section",
"is_frozen",
"block_supplier_section",
"on_hold",
"hold_type",
"release_date",
"rfq_and_purchase_order_settings_section",
"warn_rfqs",
"prevent_rfqs",
"column_break_oxjw",
"warn_pos",
"prevent_pos",
"block_supplier_section",
"on_hold",
"hold_type",
"column_break_59",
"release_date",
"portal_users_tab",
"portal_users",
"more_info_tab",
"column_break2",
"website",
"language",
"column_break_30",
"supplier_details",
"section_break_jqla",
"customer_numbers",
"dashboard_tab"
],
"fields": [
@@ -101,6 +105,13 @@
"oldfieldtype": "Data",
"reqd": 1
},
{
"fieldname": "alias",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Alias",
"unique": 1
},
{
"fieldname": "country",
"fieldtype": "Link",
@@ -110,21 +121,24 @@
{
"fieldname": "default_bank_account",
"fieldtype": "Link",
"label": "Default Company Bank Account",
"label": "Company Bank Account",
"options": "Bank Account"
},
{
"description": "Supplier's tax identification number (e.g. PAN, VAT, GST)",
"fieldname": "tax_id",
"fieldtype": "Data",
"label": "Tax ID"
},
{
"description": "Determines which tax rules apply to this supplier",
"fieldname": "tax_category",
"fieldtype": "Link",
"label": "Tax Category",
"options": "Tax Category"
},
{
"description": "TDS / withholding tax category applied when paying this supplier",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"label": "Tax Withholding Category",
@@ -132,15 +146,18 @@
},
{
"default": "0",
"description": "Enable to make this supplier selectable as a transporter on Delivery Notes and Stock Entries",
"fieldname": "is_transporter",
"fieldtype": "Check",
"label": "Is Transporter"
},
{
"default": "0",
"description": "Used for inter-company transactions",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
"label": "Is Internal Supplier"
"label": "Is Internal Supplier",
"show_description_on_click": 1
},
{
"depends_on": "is_internal_supplier",
@@ -192,6 +209,7 @@
{
"bold": 1,
"default": "0",
"description": "Disabled suppliers are hidden from selection in new transactions but remain in historical records",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
@@ -232,7 +250,7 @@
"depends_on": "represents_company",
"fieldname": "companies",
"fieldtype": "Table",
"label": "Allowed To Transact With",
"label": "Allowed to transact with",
"options": "Allowed To Transact With"
},
{
@@ -258,21 +276,24 @@
{
"fieldname": "payment_terms",
"fieldtype": "Link",
"label": "Default Payment Terms Template",
"label": "Payment Terms Template",
"options": "Payment Terms Template"
},
{
"default": "0",
"description": "When enabled, transactions with this supplier will be blocked based on the Hold Type below",
"fieldname": "on_hold",
"fieldtype": "Check",
"label": "Block Supplier"
"label": "Block Supplier",
"show_description_on_click": 1
},
{
"default": "All",
"depends_on": "eval:doc.on_hold",
"fieldname": "hold_type",
"fieldtype": "Select",
"label": "Hold Type",
"options": "\nAll\nInvoices\nPayments"
"options": "All\nInvoices\nPayments"
},
{
"depends_on": "eval:doc.on_hold",
@@ -307,14 +328,13 @@
"read_only": 1
},
{
"description": "Mention if non-standard payable account",
"description": "Override the default payable / advance accounts on a per-company basis. Leave blank to use each company's defaults from Company settings.",
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Accounts",
"label": "Per-Company Accounts",
"options": "Party Account"
},
{
"collapsible": 1,
"collapsible_depends_on": "supplier_details",
"fieldname": "column_break2",
"fieldtype": "Section Break",
@@ -329,7 +349,7 @@
"oldfieldtype": "Data"
},
{
"description": "Statutory info and other general information about your Supplier",
"description": "General information about your Supplier",
"fieldname": "supplier_details",
"fieldtype": "Text",
"label": "Supplier Details",
@@ -342,6 +362,7 @@
},
{
"default": "0",
"description": "Frozen suppliers block ledger entries until unfrozen. Use this to temporarily lock accounting activity without disabling the supplier.",
"fieldname": "is_frozen",
"fieldtype": "Check",
"label": "Is Frozen"
@@ -350,13 +371,13 @@
"default": "0",
"fieldname": "allow_purchase_invoice_creation_without_purchase_order",
"fieldtype": "Check",
"label": "Allow Purchase Invoice Creation Without Purchase Order"
"label": "Allow purchase invoice creation without purchase order"
},
{
"default": "0",
"fieldname": "allow_purchase_invoice_creation_without_purchase_receipt",
"fieldtype": "Check",
"label": "Allow Purchase Invoice Creation Without Purchase Receipt"
"label": "Allow purchase invoice creation without purchase receipt"
},
{
"fieldname": "primary_address_and_contact_detail_section",
@@ -367,7 +388,7 @@
"description": "Reselect, if the chosen contact is edited after save",
"fieldname": "supplier_primary_contact",
"fieldtype": "Link",
"label": "Supplier Primary Contact",
"label": "Primary Contact",
"no_copy": 1,
"options": "Contact"
},
@@ -382,17 +403,13 @@
"fetch_from": "supplier_primary_contact.email_id",
"fieldname": "email_id",
"fieldtype": "Read Only",
"label": "Email Id",
"label": "Email ID",
"no_copy": 1
},
{
"fieldname": "column_break_44",
"fieldtype": "Column Break"
},
{
"fieldname": "primary_address",
"fieldtype": "Text Editor",
"label": "Primary Address",
"label": "Primary Address Preview",
"no_copy": 1,
"read_only": 1
},
@@ -400,7 +417,7 @@
"description": "Reselect, if the chosen address is edited after save",
"fieldname": "supplier_primary_address",
"fieldtype": "Link",
"label": "Supplier Primary Address",
"label": "Primary Address",
"no_copy": 1,
"options": "Address"
},
@@ -436,10 +453,11 @@
"label": "Tax"
},
{
"collapsible": 1,
"collapsible_depends_on": "is_internal_supplier",
"fieldname": "internal_supplier_section",
"fieldtype": "Section Break",
"label": "Internal Supplier Accounting"
"hide_border": 1,
"label": "Internal Supplier Details"
},
{
"fieldname": "column_break_16",
@@ -458,10 +476,6 @@
"fieldtype": "Section Break",
"label": "Block Supplier"
},
{
"fieldname": "column_break_59",
"fieldtype": "Column Break"
},
{
"fieldname": "default_accounts_section",
"fieldtype": "Section Break",
@@ -483,12 +497,14 @@
"fieldtype": "Column Break"
},
{
"description": "Account / customer numbers assigned to your companies by this supplier (for reconciliation on their statements)",
"fieldname": "customer_numbers",
"fieldtype": "Table",
"label": "Customer Numbers",
"options": "Customer Number At Supplier"
},
{
"description": "Used to pick the correct rate row inside the Tax Withholding Category for this supplier (e.g. Company vs Individual rates)",
"fieldname": "tax_withholding_group",
"fieldtype": "Link",
"label": "Tax Withholding Group",
@@ -504,11 +520,34 @@
{
"fieldname": "rfq_and_purchase_order_settings_section",
"fieldtype": "Section Break",
"hidden": 1,
"label": "RFQ and Purchase Order Settings"
},
{
"fieldname": "column_break_oxjw",
"fieldtype": "Column Break"
},
{
"fieldname": "taxation_section",
"fieldtype": "Section Break",
"label": "Tax Identification"
},
{
"fieldname": "invoice_settings_section",
"fieldtype": "Section Break"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "section_break_jqla",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_pgad",
"fieldtype": "Section Break"
}
],
"grid_page_length": 50,
@@ -522,7 +561,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-05-29 16:52:59.441272",
"modified": "2026-06-22 12:23:09.241125",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
@@ -582,7 +621,7 @@
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "supplier_group",
"search_fields": "supplier_group, alias",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",

View File

@@ -39,6 +39,7 @@ class Supplier(TransactionBase):
from erpnext.utilities.doctype.portal_user.portal_user import PortalUser
accounts: DF.Table[PartyAccount]
alias: DF.Data | None
allow_purchase_invoice_creation_without_purchase_order: DF.Check
allow_purchase_invoice_creation_without_purchase_receipt: DF.Check
companies: DF.Table[AllowedToTransactWith]
@@ -50,7 +51,7 @@ class Supplier(TransactionBase):
disabled: DF.Check
email_id: DF.ReadOnly | None
gender: DF.Link | None
hold_type: DF.Literal["", "All", "Invoices", "Payments"]
hold_type: DF.Literal["All", "Invoices", "Payments"]
image: DF.AttachImage | None
is_frozen: DF.Check
is_internal_supplier: DF.Check
@@ -88,7 +89,6 @@ class Supplier(TransactionBase):
def before_save(self):
if not self.on_hold:
self.hold_type = ""
self.release_date = ""
elif self.on_hold and not self.hold_type:
self.hold_type = "All"

View File

@@ -1,8 +1,14 @@
frappe.listview_settings["Supplier"] = {
add_fields: ["supplier_name", "supplier_group", "image", "on_hold"],
add_fields: ["supplier_name", "supplier_group", "image", "on_hold", "disabled", "is_frozen"],
get_indicator: function (doc) {
if (cint(doc.on_hold)) {
return [__("On Hold"), "red"];
if (cint(doc.disabled)) {
return [__("Disabled"), "gray", "disabled,=,1"];
} else if (cint(doc.on_hold)) {
return [__("On Hold"), "red", "on_hold,=,1"];
} else if (cint(doc.is_frozen)) {
return [__("Frozen"), "orange", "is_frozen,=,1"];
} else {
return [__("Active"), "green", "disabled,=,0|on_hold,=,0|is_frozen,=,0"];
}
},
};

View File

@@ -414,39 +414,29 @@ class BuyingController(SubcontractingController):
stock_and_asset_items = []
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
for d in self.get("items"):
if d.item_code:
stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount)
(
tax_accounts,
total_valuation_amount,
all_item_charges,
stock_item_charges,
) = self.get_tax_details()
last_item_idx = d.idx
# Pre-compute each item's share of the "Actual" valuation charges (keyed by row idx).
actual_charge_per_item = self.distribute_actual_tax_amount(
stock_and_asset_items, all_item_charges, stock_item_charges
)
tax_accounts, total_valuation_amount, total_actual_tax_amount = self.get_tax_details()
remaining_amount = total_actual_tax_amount
last_item_idx = max((d.idx for d in self.get("items")), default=1)
for i, item in enumerate(self.get("items")):
if item.item_code and (item.qty or item.get("rejected_qty")):
item_tax_amount, actual_tax_amount = 0.0, 0.0
if i == (last_item_idx - 1):
# dump any rounding remainder of the On Net Total valuation on the last item
item_tax_amount = total_valuation_amount
actual_tax_amount = remaining_amount
else:
# calculate item tax amount
item_tax_amount = self.get_item_tax_amount(item, tax_accounts)
total_valuation_amount -= item_tax_amount
if total_actual_tax_amount:
actual_tax_amount = self.get_item_actual_tax_amount(
item,
total_actual_tax_amount,
stock_and_asset_items_amount,
stock_and_asset_items_qty,
)
remaining_amount -= actual_tax_amount
# This code is required here to calculate the correct valuation for stock items
if item.item_code not in stock_and_asset_items:
item.valuation_rate = 0.0
@@ -454,7 +444,8 @@ class BuyingController(SubcontractingController):
# Item tax amount is the total tax amount applied on that item and actual tax type amount
item.item_tax_amount = flt(
item_tax_amount + actual_tax_amount, self.precision("item_tax_amount", item)
item_tax_amount + actual_charge_per_item.get(item.idx, 0.0),
self.precision("item_tax_amount", item),
)
self.round_floats_in(item)
@@ -503,7 +494,11 @@ class BuyingController(SubcontractingController):
def get_tax_details(self):
tax_accounts = []
total_valuation_amount = 0.0
total_actual_tax_amount = 0.0
# Per-row "Actual" valuation charge amounts, kept separate (not pooled) so each can be
# distributed individually - this keeps the per-item item_tax_amount in lockstep with the
# per-tax-row amount capitalized in the GL (see get_capitalized_valuation_tax).
all_item_charges = []
stock_item_charges = []
for d in self.get("taxes"):
if d.category not in ["Valuation", "Valuation and Total"]:
@@ -516,10 +511,13 @@ class BuyingController(SubcontractingController):
if d.charge_type == "On Net Total":
total_valuation_amount += amount
tax_accounts.append(d.account_head)
elif d.charge_type == "Actual" and d.get("allocate_full_amount_to_stock_items"):
# Capitalize the full amount onto stock/asset items only (e.g. Freight)
stock_item_charges.append(amount)
else:
total_actual_tax_amount += amount
all_item_charges.append(amount)
return tax_accounts, total_valuation_amount, total_actual_tax_amount
return tax_accounts, total_valuation_amount, all_item_charges, stock_item_charges
def get_item_tax_amount(self, item, tax_accounts):
item_tax_amount = 0.0
@@ -540,16 +538,81 @@ class BuyingController(SubcontractingController):
return item_tax_amount
def get_item_actual_tax_amount(
self, item, actual_tax_amount, stock_and_asset_items_amount, stock_and_asset_items_qty
):
item_proportion = (
flt(item.base_net_amount) / stock_and_asset_items_amount
if stock_and_asset_items_amount
else flt(item.qty) / stock_and_asset_items_qty
def distribute_actual_tax_amount(self, stock_and_asset_items, all_item_charges, stock_item_charges):
"""Distribute "Actual" valuation charges to each item, keyed by row idx.
Each charge is spread individually (not pooled together) so the resulting per-item
item_tax_amount decomposes exactly into the per-tax-row amount capitalized in the GL
(see get_capitalized_valuation_tax) - pooling first and spreading the aggregate can drift
by rounding for multiple charges over unevenly valued items. A charge in `all_item_charges`
is spread across every item by net amount; a non-stock item's share is computed but never
capitalized (e.g. a genuine tax). A charge in `stock_item_charges` (flagged
`allocate_full_amount_to_stock_items`) is spread across stock/asset items only, so the whole
charge is capitalized (e.g. Freight).
"""
all_items = [d for d in self.get("items") if d.item_code]
stock_items = [d for d in all_items if d.item_code in stock_and_asset_items]
charge_per_item = {}
for charge in all_item_charges:
self._spread_charge_over_items(charge_per_item, charge, all_items)
for charge in stock_item_charges:
self._spread_charge_over_items(charge_per_item, charge, stock_items)
return charge_per_item
def _spread_charge_over_items(self, charge_per_item, total_charge, items):
"""Add each item's proportional share of `total_charge` into `charge_per_item`.
Proportion is by net amount (falling back to qty); any rounding remainder is assigned
to the last item in the group."""
if not total_charge or not items:
return
total_amount = sum(flt(d.base_net_amount) for d in items)
total_qty = sum(flt(d.qty) for d in items)
# Nothing to proportion against (all rows have zero amount and zero qty)
if not total_amount and not total_qty:
return
remaining = total_charge
for d in items[:-1]:
proportion = flt(d.base_net_amount) / total_amount if total_amount else flt(d.qty) / total_qty
charge = flt(proportion * total_charge, self.precision("item_tax_amount", d))
charge_per_item[d.idx] = charge_per_item.get(d.idx, 0.0) + charge
remaining -= charge
last = items[-1]
charge_per_item[last.idx] = charge_per_item.get(last.idx, 0.0) + flt(
remaining, self.precision("item_tax_amount", last)
)
return flt(item_proportion * actual_tax_amount, self.precision("item_tax_amount", item))
def get_capitalized_valuation_tax(self):
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
all_items = [d for d in self.get("items") if d.item_code]
stock_item_idx = {d.idx for d in all_items if d.item_code in stock_and_asset_items}
capitalized = {}
for tax in self.get("taxes"):
if tax.category not in ("Valuation", "Valuation and Total"):
continue
amount = flt(tax.base_tax_amount_after_discount_amount) * (
-1 if tax.get("add_deduct_tax") == "Deduct" else 1
)
if not amount:
continue
if tax.charge_type == "Actual" and not tax.get("allocate_full_amount_to_stock_items"):
# Spread across all items; only the stock/asset items' share is capitalized.
charge_per_item = {}
self._spread_charge_over_items(charge_per_item, amount, all_items)
amount = sum(
charge for item_idx, charge in charge_per_item.items() if item_idx in stock_item_idx
)
capitalized[tax.name] = amount
return capitalized
def set_incoming_rate(self):
"""

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _
from frappe.query_builder import Case
from frappe.utils import cstr, flt
from erpnext.utilities.product import get_item_codes_by_attributes
@@ -129,6 +130,53 @@ def validate_is_incremental(numeric_attribute, attribute, value, item):
)
def get_attribute_value_renames(item_attribute):
"""Return old to new attribute value mappings for renamed Item Attribute Value rows."""
if item_attribute.numeric_values:
return {}
db_value = item_attribute.get_doc_before_save()
if not db_value:
return {}
old_values = {d.name: d.attribute_value for d in db_value.item_attribute_values}
renames = {}
for row in item_attribute.item_attribute_values:
if row.name in old_values and old_values[row.name] != row.attribute_value:
renames[old_values[row.name]] = row.attribute_value
return renames
def update_variant_attribute_values(item_attribute):
"""Propagate renamed Item Attribute Values to Item Variant Attribute on variant items."""
value_map = get_attribute_value_renames(item_attribute)
if not value_map:
return
item_variant_table = frappe.qb.DocType("Item Variant Attribute")
item_table = frappe.qb.DocType("Item")
attribute_value = item_variant_table.attribute_value
attribute_value_case = Case()
for old_value, new_value in value_map.items():
attribute_value_case = attribute_value_case.when(attribute_value == old_value, new_value)
(
frappe.qb.update(item_variant_table)
.join(item_table)
.on(item_table.name == item_variant_table.parent)
.set(attribute_value, attribute_value_case.else_(attribute_value))
.where(item_table.variant_of.isnotnull())
.where(item_table.variant_of != "")
.where(item_variant_table.attribute == item_attribute.name)
.where(attribute_value.isin(list(value_map)))
).run()
frappe.flags.attribute_values = None
def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True):
allow_rename_attribute_value = frappe.db.get_single_value(
"Item Variant Settings", "allow_rename_attribute_value"

View File

@@ -216,11 +216,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
group = "Customer Group" if filters.get("customer") else "Supplier Group"
item_rules_list = frappe.get_all(
"Party Specific Item",
filters={
"party": ["!=", party],
"party_type": party_type,
},
fields=["restrict_based_on", "based_on_value"],
filters={"party_type": party_type},
fields=["party", "restrict_based_on", "based_on_value"],
)
party_group_rules_list = frappe.get_all(
@@ -229,21 +226,30 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
fields=["party as party_group", "restrict_based_on", "based_on_value"],
)
current_party_group = frappe.get_value(party_type, party, frappe.scrub(group))
restricted_items = defaultdict(set)
allowed_items = defaultdict(set)
for rule in item_rules_list:
restrict_based_on = "name" if rule.restrict_based_on == "Item" else rule.restrict_based_on
if rule.party == party:
allowed_items[restrict_based_on].add(rule.based_on_value)
else:
restricted_items[restrict_based_on].add(rule.based_on_value)
for rule in party_group_rules_list:
if current_party_group != rule.party_group:
item_rules_list.append(rule)
restrict_based_on = "name" if rule.restrict_based_on == "Item" else rule.restrict_based_on
filters_dict = {}
for rule in item_rules_list:
if rule["restrict_based_on"] == "Item":
rule["restrict_based_on"] = "name"
filters_dict[rule.restrict_based_on] = []
if current_party_group == rule.party_group:
allowed_items[restrict_based_on].add(rule.based_on_value)
else:
restricted_items[restrict_based_on].add(rule.based_on_value)
for rule in item_rules_list:
filters_dict[rule.restrict_based_on].append(rule.based_on_value)
for filter in filters_dict:
filters[scrub(filter)] = ["not in", filters_dict[filter]]
for field, restricted_values in restricted_items.items():
values_to_exclude = restricted_values - allowed_items[field]
if values_to_exclude:
filters[scrub(field)] = ["not in", list(values_to_exclude)]
if filters.get("customer"):
del filters["customer"]

View File

@@ -445,6 +445,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
doc.pricing_rules = []
doc.return_against = source.name
doc.set_warehouse = ""
if doctype == "Sales Invoice":
doc.is_debit_note = 0
if doctype == "Sales Invoice" or doctype == "POS Invoice":
doc.is_pos = source.is_pos

View File

@@ -186,7 +186,8 @@ class StatusUpdater(Document):
"""
def on_discard(self):
self.db_set("status", "Cancelled")
if self.meta.has_field("status"):
self.db_set("status", "Cancelled")
def update_prevdoc_status(self):
self.update_qty()

View File

@@ -22,6 +22,14 @@ from erpnext.controllers.sales_and_purchase_return import (
filter_serial_batches,
make_serial_batch_bundle_for_return,
)
# Re-exported for backward compatibility; canonical home is erpnext.exceptions.
from erpnext.exceptions import (
BatchExpiredError,
QualityInspectionNotSubmittedError,
QualityInspectionRejectedError,
QualityInspectionRequiredError,
)
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock import get_warehouse_account_map
@@ -37,22 +45,6 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
from erpnext.stock.stock_ledger import get_items_to_be_repost
class QualityInspectionRequiredError(frappe.ValidationError):
pass
class QualityInspectionRejectedError(frappe.ValidationError):
pass
class QualityInspectionNotSubmittedError(frappe.ValidationError):
pass
class BatchExpiredError(frappe.ValidationError):
pass
class StockController(AccountsController):
def validate(self):
super().validate()
@@ -2163,7 +2155,7 @@ def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str
inspection_fieldname = inspection_fieldname_map.get(doctype)
if inspection_fieldname is None:
return []
return items if doctype == "Stock Entry" else []
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"

View File

@@ -743,7 +743,14 @@ class SubcontractingInwardController:
"name": ["in", list(data.keys())],
"docstatus": 1,
},
fields=["rate", "name", "required_qty", "received_qty"],
fields=[
"rate",
"name",
"required_qty",
"received_qty",
"returned_qty",
"consumed_qty",
],
)
doc_updates = {}
@@ -751,13 +758,17 @@ class SubcontractingInwardController:
current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1)
current_rate = flt(data[d.name].rate)
# Calculate weighted average rate
old_total = d.rate * d.received_qty
# Weighted average rate must be computed on the on-hand balance
balance_qty = d.received_qty - d.returned_qty - d.consumed_qty
old_total = d.rate * balance_qty
current_total = current_rate * current_qty
new_balance_qty = balance_qty + current_qty
d.received_qty = d.received_qty + current_qty
d.rate = (
flt((old_total + current_total) / d.received_qty, precision) if d.received_qty else 0.0
flt((old_total + current_total) / new_balance_qty, precision)
if new_balance_qty > 0
else 0.0
)
if not d.required_qty and not d.received_qty:

View File

@@ -32,11 +32,12 @@ from erpnext.utilities.regional import temporary_flag
class calculate_taxes_and_totals:
def __init__(self, doc: Document):
self.doc = doc
frappe.flags.round_off_applicable_accounts = []
frappe.flags.round_off_applicable_accounts = (
get_round_off_applicable_accounts(self.doc.company, [], self.doc) or []
)
frappe.flags.round_row_wise_tax = frappe.get_single_value("Accounts Settings", "round_row_wise_tax")
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
def filter_rows(self):
@@ -1240,14 +1241,16 @@ def get_itemised_tax_breakup_html(doc):
@frappe.whitelist()
def get_round_off_applicable_accounts(company, account_list):
def get_round_off_applicable_accounts(
company: str, account_list: list | str, doc: str | dict | Document | None = None
):
# required to set correct region
with temporary_flag("company", company):
return get_regional_round_off_accounts(company, account_list)
return get_regional_round_off_accounts(company, account_list, doc)
@erpnext.allow_regional
def get_regional_round_off_accounts(company, account_list):
def get_regional_round_off_accounts(company, account_list, doc=None):
pass

View File

@@ -1,3 +1,5 @@
from unittest.mock import patch
import frappe
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
@@ -6,6 +8,28 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestTaxesAndTotals(ERPNextTestSuite):
def test_regional_round_off_accounts(self):
"""
Regional overrides cannot extend the list in-place — the return
value must be assigned back to frappe.flags.round_off_applicable_accounts.
"""
test_account = "_Test Round Off Account"
def mock_regional(company, account_list: list, doc=None) -> list:
# Simulates a regional override
account_list.extend([test_account])
return account_list
so = make_sales_order(do_not_save=True)
with patch(
"erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts",
mock_regional,
):
calculate_taxes_and_totals(so)
self.assertIn(test_account, frappe.flags.round_off_applicable_accounts)
def test_disabling_rounded_total_resets_base_fields(self):
"""Disabling rounded total should also clear base rounded values."""
so = make_sales_order(do_not_save=True)

View File

@@ -20,7 +20,11 @@
"section_break_13",
"carry_forward_communication_and_comments",
"column_break_junk",
"update_timestamp_on_new_communication"
"update_timestamp_on_new_communication",
"frappe_crm_section",
"enable_frappe_crm_data_synchronization",
"column_break_jbzj",
"allowed_users"
],
"fields": [
{
@@ -105,6 +109,30 @@
"fieldname": "enable_opportunity_creation_from_contact_us",
"fieldtype": "Check",
"label": "Enable Opportunity Creation from Contact Us"
},
{
"fieldname": "frappe_crm_section",
"fieldtype": "Section Break",
"label": "Frappe CRM"
},
{
"fieldname": "column_break_jbzj",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.enable_frappe_crm_data_synchronization === 1;",
"fieldname": "allowed_users",
"fieldtype": "Table MultiSelect",
"label": "Allowed Users",
"options": "Frappe CRM Allowed User",
"permlevel": 1
},
{
"default": "0",
"fieldname": "enable_frappe_crm_data_synchronization",
"fieldtype": "Check",
"label": "Enable Frappe CRM Data Synchronization",
"permlevel": 1
}
],
"grid_page_length": 50,
@@ -112,7 +140,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-06-11 23:09:49.750381",
"modified": "2026-06-22 01:26:13.474915",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM Settings",
@@ -146,6 +174,16 @@
"role": "Sales Master Manager",
"share": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",

View File

@@ -3,6 +3,7 @@
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields, delete_custom_fields
from frappe.model.document import Document
@@ -15,12 +16,16 @@ class CRMSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.crm.doctype.frappe_crm_allowed_user.frappe_crm_allowed_user import FrappeCRMAllowedUser
allow_lead_duplication_based_on_emails: DF.Check
allowed_users: DF.TableMultiSelect[FrappeCRMAllowedUser]
auto_creation_of_contact: DF.Check
campaign_naming_by: DF.Literal["Campaign Name", "Naming Series"]
carry_forward_communication_and_comments: DF.Check
close_opportunity_after_days: DF.Int
default_valid_till: DF.Data | None
enable_frappe_crm_data_synchronization: DF.Check
enable_opportunity_creation_from_contact_us: DF.Check
update_timestamp_on_new_communication: DF.Check
# end: auto-generated types
@@ -28,6 +33,7 @@ class CRMSettings(Document):
def validate(self):
frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))
self.validate_enable_opportunity_creation_from_contact_us()
self.validate_allowed_users()
def validate_enable_opportunity_creation_from_contact_us(self):
contact_disabled = frappe.get_single_value("Contact Us Settings", "is_disabled")
@@ -38,3 +44,43 @@ class CRMSettings(Document):
"Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
)
)
def validate_allowed_users(self):
if self.enable_frappe_crm_data_synchronization and not self.allowed_users:
frappe.throw(
_(
"Please add atleast one user on Allowed Users to allow Data Synchronization from Frappe CRM site."
)
)
def before_save(self):
self.clear_allowed_users()
def on_update(self):
self.custom_fields_for_frappe_crm_data_sync()
def clear_allowed_users(self):
if not self.enable_frappe_crm_data_synchronization:
self.allowed_users = []
def custom_fields_for_frappe_crm_data_sync(self):
custom_fields = {
"Quotation": [
{
"fieldname": "crm_deal",
"fieldtype": "Data",
"label": "Frappe CRM Deal",
"insert_after": "party_name",
}
],
"Customer": [
{
"fieldname": "crm_deal",
"fieldtype": "Data",
"label": "Frappe CRM Deal",
"insert_after": "prospect_name",
}
],
}
create_custom_fields(custom_fields, ignore_validate=True)

View File

@@ -0,0 +1,36 @@
{
"actions": [],
"allow_bulk_edit": 1,
"allow_rename": 1,
"creation": "2026-06-22 00:47:12.265968",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"user"
],
"fields": [
{
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-06-22 01:49:54.586410",
"modified_by": "Administrator",
"module": "CRM",
"name": "Frappe CRM Allowed User",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,23 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class FrappeCRMAllowedUser(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
user: DF.Link
# end: auto-generated types
_DOCTYPE_NAME = "Frappe CRM Allowed User"

View File

@@ -2,35 +2,12 @@ import json
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@frappe.whitelist()
def create_custom_fields_for_frappe_crm():
frappe.only_for("System Manager")
custom_fields = {
"Quotation": [
{
"fieldname": "crm_deal",
"fieldtype": "Data",
"label": "Frappe CRM Deal",
"insert_after": "party_name",
}
],
"Customer": [
{
"fieldname": "crm_deal",
"fieldtype": "Data",
"label": "Frappe CRM Deal",
"insert_after": "prospect_name",
}
],
}
create_custom_fields(custom_fields, ignore_validate=True)
@frappe.whitelist()
def create_prospect_against_crm_deal():
validate_frappe_crm_sync()
doc = frappe.form_dict
prospect = frappe.new_doc("Prospect")
prospect.company_name = doc.organization or doc.lead_name
@@ -161,6 +138,8 @@ CUSTOMER_ALLOWED_FIELDS = {
@frappe.whitelist()
def create_customer(customer_data=None):
validate_frappe_crm_sync()
if not customer_data:
customer_data = frappe.form_dict
@@ -181,3 +160,21 @@ def create_customer(customer_data=None):
except Exception:
frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal")
pass
def validate_frappe_crm_sync():
CRMSettings = frappe.get_single("CRM Settings")
if not CRMSettings.enable_frappe_crm_data_synchronization:
frappe.throw(
_("Frappe CRM data synchronization is not enabled on ERPNext. Contact System Manager of ERPNext.")
)
allowed_users = [d.user for d in CRMSettings.allowed_users]
if frappe.session.user not in allowed_users:
frappe.throw(
_(
"User not allowed to synchronize data from Frappe CRM on ERPNext. Contact System Manager of ERPNext."
),
exc=frappe.PermissionError,
)

View File

@@ -28,3 +28,20 @@ class MandatoryAccountDimensionError(frappe.ValidationError):
class ReportingCurrencyExchangeNotFoundError(frappe.ValidationError):
pass
# stock
class QualityInspectionRequiredError(frappe.ValidationError):
pass
class QualityInspectionRejectedError(frappe.ValidationError):
pass
class QualityInspectionNotSubmittedError(frappe.ValidationError):
pass
class BatchExpiredError(frappe.ValidationError):
pass

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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