Compare commits

...

211 Commits

Author SHA1 Message Date
Frappe PR Bot
ff46d20b25 chore(release): Bumped to Version 16.20.0
# [16.20.0](https://github.com/frappe/erpnext/compare/v16.19.1...v16.20.0) (2026-05-27)

### Bug Fixes

* consider batchwise valuation in stock ageing report (backport [#54919](https://github.com/frappe/erpnext/issues/54919)) ([#55230](https://github.com/frappe/erpnext/issues/55230)) ([a1457c7](a1457c759d))
* consumed operation cost calculation (backport [#54858](https://github.com/frappe/erpnext/issues/54858)) ([#55133](https://github.com/frappe/erpnext/issues/55133)) ([dfc9144](dfc91441b4))
* correct description for Is Rate Adjustment Entry (Debit Note) checkbox ([a39733d](a39733ddd0))
* correct remarks for foreign currency payment entries ([f3f8f32](f3f8f327df))
* corrected the pricing rule taking the wrong value (backport [#54894](https://github.com/frappe/erpnext/issues/54894)) ([#55124](https://github.com/frappe/erpnext/issues/55124)) ([1a75c14](1a75c14308))
* default use_for_shopping_cart to 0 in set_taxes ([54be4ee](54be4ee275))
* don't reset net_purchase_amount for Composite Asset if already set ([99642b9](99642b9636))
* edit stock uom qty for purchase documents (backport [#55135](https://github.com/frappe/erpnext/issues/55135)) ([#55179](https://github.com/frappe/erpnext/issues/55179)) ([123b4ad](123b4ad563))
* **employee:** js error if user does not have write permission for date field (backport [#55312](https://github.com/frappe/erpnext/issues/55312)) ([#55314](https://github.com/frappe/erpnext/issues/55314)) ([4dff5a7](4dff5a7820))
* faster range calculation on process period closing voucher ([e56ee38](e56ee383bc))
* fg valuation rate in repack entry when multiple FGs ([7b6adce](7b6adce89a))
* inclusive tax amount not considered while setting LCV from purchase invoice ([bd4c244](bd4c24493c))
* incorrect error message string in sales order (backport [#55090](https://github.com/frappe/erpnext/issues/55090)) ([#55095](https://github.com/frappe/erpnext/issues/55095)) ([17bc2b6](17bc2b691f))
* invalid filter on item_group (backport [#55186](https://github.com/frappe/erpnext/issues/55186)) ([#55188](https://github.com/frappe/erpnext/issues/55188)) ([ea86347](ea863477a4))
* item price with party condition (backport [#55100](https://github.com/frappe/erpnext/issues/55100)) ([#55107](https://github.com/frappe/erpnext/issues/55107)) ([cc438a4](cc438a4600))
* job card buttons color (backport [#55252](https://github.com/frappe/erpnext/issues/55252)) ([#55261](https://github.com/frappe/erpnext/issues/55261)) ([69c6ed3](69c6ed3cd9))
* **manufacturing:** fetch from_bom name in production plan (backport [#55085](https://github.com/frappe/erpnext/issues/55085)) ([#55092](https://github.com/frappe/erpnext/issues/55092)) ([36aca51](36aca51fbb))
* **manufacturing:** remove forecast_qty and adjust_qty fields from sa… (backport [#55129](https://github.com/frappe/erpnext/issues/55129)) ([#55136](https://github.com/frappe/erpnext/issues/55136)) ([bde7f16](bde7f1660e))
* **payment_entry:** sync paid/received amounts for cross-currency entries (backport [#55270](https://github.com/frappe/erpnext/issues/55270)) ([#55272](https://github.com/frappe/erpnext/issues/55272)) ([705814f](705814f066))
* pos profile form cleanup (backport [#52436](https://github.com/frappe/erpnext/issues/52436)) ([#55285](https://github.com/frappe/erpnext/issues/55285)) ([1f14ef2](1f14ef2344))
* prevent AttributeError in batch query filters (backport [#55257](https://github.com/frappe/erpnext/issues/55257)) ([#55279](https://github.com/frappe/erpnext/issues/55279)) ([bfd37dc](bfd37dcc21))
* **project:** update customer and sales order as no copy ([1e61ca1](1e61ca162f))
* removed redundant code ([a7eb3ac](a7eb3acd1a))
* **sales_invoice:** skip stock update for POS invoices linked to Delivery Note (backport [#55311](https://github.com/frappe/erpnext/issues/55311)) ([#55313](https://github.com/frappe/erpnext/issues/55313)) ([cd7e1bb](cd7e1bbff1))
* set bin details when adding item using update items (backport [#55096](https://github.com/frappe/erpnext/issues/55096)) ([#55098](https://github.com/frappe/erpnext/issues/55098)) ([bb87ffc](bb87ffc90a))
* single variant creation error ([82b0372](82b0372d5b))
* slow query ([66c9170](66c9170465))
* **stock:** apply posting datetime filters while fetching available batches (backport [#54976](https://github.com/frappe/erpnext/issues/54976)) ([#55185](https://github.com/frappe/erpnext/issues/55185)) ([edf6bea](edf6bea2ee))
* **stock:** remove precision for valuation rate while creating sle (backport [#55249](https://github.com/frappe/erpnext/issues/55249)) ([#55260](https://github.com/frappe/erpnext/issues/55260)) ([9600ecd](9600ecd61c))
* **stock:** remove recalculate current qty function (backport [#54774](https://github.com/frappe/erpnext/issues/54774)) ([#55075](https://github.com/frappe/erpnext/issues/55075)) ([56a9b37](56a9b37fac))
* use passed posting date in make_reverse_gl_entries ([3ce9cf2](3ce9cf2bd8))
* valuation rate missing for standalone credit notes for moving av… (backport [#55102](https://github.com/frappe/erpnext/issues/55102)) ([#55104](https://github.com/frappe/erpnext/issues/55104)) ([b11365b](b11365b8c2))

### Features

* add get_parent_supplier_groups using query builder ([82793cb](82793cbd4d))
* add party groups functionality to party specific item (backport [#54988](https://github.com/frappe/erpnext/issues/54988)) ([#55245](https://github.com/frappe/erpnext/issues/55245)) ([a618f4c](a618f4cca4))
* allow creation of any number of variants in multiple item variant creation dialog ([27db98d](27db98d222))
* pending qty in job card ([b372e6f](b372e6f118))

### Performance Improvements

* skip delink_original_entry during cancellation when Immutable Ledger is enabled ([#55130](https://github.com/frappe/erpnext/issues/55130)) ([8a4cb28](8a4cb28d90))
* skip delink_original_entry during cancellation when Immutable Ledger is enabled (backport [#55130](https://github.com/frappe/erpnext/issues/55130)) ([#55166](https://github.com/frappe/erpnext/issues/55166)) ([92689e0](92689e05da))
2026-05-27 01:22:47 +00:00
mergify[bot]
d215fa7623 chore: remove frappe-semgrep-rules submodule (backport #55083) (#55319)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-27 01:21:08 +00:00
Diptanil Saha
bf8f7ba883 Merge pull request #55307 from frappe/version-16-hotfix
chore: release v16
2026-05-27 05:33:15 +05:30
mergify[bot]
cd7e1bbff1 fix(sales_invoice): skip stock update for POS invoices linked to Delivery Note (backport #55311) (#55313)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(sales_invoice): skip stock update for POS invoices linked to Delivery Note (#55311)
2026-05-26 20:32:53 +00:00
mergify[bot]
4dff5a7820 fix(employee): js error if user does not have write permission for date field (backport #55312) (#55314)
Co-authored-by: Himanshu Jain <118419692+trufurs@users.noreply.github.com>
fix(employee): js error if user does not have write permission for date field (#55312)
2026-05-27 01:58:49 +05:30
rohitwaghchaure
1781893c3c Merge pull request #55297 from frappe/mergify/bp/version-16-hotfix/pr-55290
fix: inclusive tax amount not considered while setting LCV from purchase invoice (backport #55290)
2026-05-26 16:13:51 +05:30
Nihantra C. Patel
304de47b48 Merge pull request #55295 from frappe/mergify/bp/version-16-hotfix/pr-55268
fix: use passed posting date for period closing validation in reverse GL entries (backport #55268)
2026-05-26 16:06:56 +05:30
Rohit Waghchaure
bd4c24493c fix: inclusive tax amount not considered while setting LCV from purchase invoice
(cherry picked from commit 048ddfc265)
2026-05-26 10:15:26 +00:00
Nihantra Patel
6925b6b645 test: update testcase
(cherry picked from commit 9c39b01f1c)
2026-05-26 10:14:00 +00:00
Nihantra Patel
3ce9cf2bd8 fix: use passed posting date in make_reverse_gl_entries
(cherry picked from commit f040bdf165)
2026-05-26 10:14:00 +00:00
Mihir Kandoi
0ede7759df Merge pull request #55289 from frappe/mergify/bp/version-16-hotfix/pr-55286
fix: single variant creation error (backport #55286)
2026-05-26 13:57:01 +05:30
diptanilsaha
1f14ef2344 fix: pos profile form cleanup (backport #52436) (#55285)
fix: pos profile form cleanup (#52436)
2026-05-26 13:49:39 +05:30
Mihir Kandoi
27db98d222 feat: allow creation of any number of variants in multiple item variant creation dialog
(cherry picked from commit 090c25d848)
2026-05-26 08:05:44 +00:00
Mihir Kandoi
82b0372d5b fix: single variant creation error
(cherry picked from commit bda75135c3)
2026-05-26 08:05:43 +00:00
mergify[bot]
69c6ed3cd9 fix: job card buttons color (backport #55252) (#55261)
Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-26 13:16:41 +05:30
mergify[bot]
ca6bcb57d3 refactor: remove unused customer field in Item DocType (backport #55277) (#55283)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-26 05:47:16 +00:00
ruthra kumar
03d96c7b85 Merge pull request #55281 from frappe/mergify/bp/version-16-hotfix/pr-55256
refactor: handle processes stuck in running state in process pcv (backport #55256)
2026-05-26 10:56:24 +05:30
mergify[bot]
bfd37dcc21 fix: prevent AttributeError in batch query filters (backport #55257) (#55279)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix: prevent AttributeError in batch query filters (#55257)
2026-05-26 05:20:04 +00:00
mergify[bot]
9600ecd61c fix(stock): remove precision for valuation rate while creating sle (backport #55249) (#55260)
Co-authored-by: Sudharsanan11 <sudharsananashok1975@gmail.com>
2026-05-26 10:29:39 +05:30
ruthra kumar
5dc37b1130 refactor: atomic summarization step for process pcv
(cherry picked from commit 6cb7971342)
2026-05-26 04:58:22 +00:00
ruthra kumar
a19611a2e9 refactor: handle processes stuck in running state in process pcv
(cherry picked from commit f414778486)
2026-05-26 04:58:22 +00:00
ruthra kumar
6755101654 refactor: summarize in background
(cherry picked from commit 1c3a9f7dd9)
2026-05-26 04:58:21 +00:00
mergify[bot]
705814f066 fix(payment_entry): sync paid/received amounts for cross-currency entries (backport #55270) (#55272)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): sync paid/received amounts for cross-currency entries (#55270)
2026-05-25 23:22:02 +05:30
mergify[bot]
1a75c14308 fix: corrected the pricing rule taking the wrong value (backport #54894) (#55124)
fix: corrected the pricing rule taking the wrong value (#54894)

(cherry picked from commit 06477119d1)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-05-25 16:02:26 +05:30
rohitwaghchaure
56d99152f9 Merge pull request #55244 from frappe/mergify/bp/version-16-hotfix/pr-55216
fix: fg valuation rate in repack entry when multiple FGs (backport #55216)
2026-05-25 15:34:16 +05:30
mergify[bot]
a618f4cca4 feat: add party groups functionality to party specific item (backport #54988) (#55245)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-25 12:38:44 +05:30
Rohit Waghchaure
7b6adce89a fix: fg valuation rate in repack entry when multiple FGs
(cherry picked from commit a47e4c04f7)
2026-05-25 06:15:29 +00:00
mergify[bot]
7752f703d2 refactor: stock ageing report (backport #55231) (#55237)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-24 18:30:45 +05:30
MochaMind
7159b13ee2 chore: update POT file (#55234) 2026-05-24 14:48:11 +02:00
mergify[bot]
ea60efd91a refactor: use frappe.db.bulk_update instead of Case queries in subcon… (backport #55232) (#55233)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-24 09:56:53 +00:00
mergify[bot]
a1457c759d fix: consider batchwise valuation in stock ageing report (backport #54919) (#55230)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: consider batchwise valuation in stock ageing report (#54919)
2026-05-24 07:18:07 +00:00
rohitwaghchaure
ab9e30b487 Merge pull request #55183 from frappe/mergify/bp/version-16-hotfix/pr-55091
feat: pending qty in job card (backport #55091)
2026-05-24 09:30:01 +05:30
Nishka Gosalia
9596e6e1e9 Merge pull request #55196 from frappe/mergify/bp/version-16-hotfix/pr-55189
fix(project): update customer and sales order as no copy (backport #55189)
2026-05-23 15:32:06 +05:30
nareshkannasln
1e61ca162f fix(project): update customer and sales order as no copy
(cherry picked from commit 9d8f3863f2)
2026-05-22 12:21:34 +00:00
mergify[bot]
edf6bea2ee fix(stock): apply posting datetime filters while fetching available batches (backport #54976) (#55185)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): apply posting datetime filters while fetching available batches (#54976)
2026-05-22 11:23:24 +00:00
mergify[bot]
ea863477a4 fix: invalid filter on item_group (backport #55186) (#55188)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: invalid filter on item_group (#55186)
2026-05-22 16:49:23 +05:30
rohitwaghchaure
171cd41928 chore: fix conflicts 2026-05-22 15:25:34 +05:30
Rohit Waghchaure
d2a793b03b refactor: better timer and complete button
(cherry picked from commit 1be92f6d05)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.js
2026-05-22 09:52:50 +00:00
Rohit Waghchaure
3081368aad refactor: job_card.js code for better readability
(cherry picked from commit 0a215b0717)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.js
2026-05-22 09:52:49 +00:00
Rohit Waghchaure
b372e6f118 feat: pending qty in job card
(cherry picked from commit db64f451c1)
2026-05-22 09:52:49 +00:00
rohitwaghchaure
e2558b6e51 Merge pull request #55182 from frappe/mergify/bp/version-16-hotfix/pr-55159
fix: slow query (backport #55159)
2026-05-22 15:22:18 +05:30
Rohit Waghchaure
66c9170465 fix: slow query
(cherry picked from commit d44f574581)
2026-05-22 09:18:04 +00:00
mergify[bot]
123b4ad563 fix: edit stock uom qty for purchase documents (backport #55135) (#55179)
Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
fix: edit stock uom qty for purchase documents (#55135)
2026-05-22 09:14:32 +00:00
ruthra kumar
92689e05da perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled (backport #55130) (#55166)
perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled (#55130)

* perf: get payment ledger and remove update from delink when immutable ledger is enabled

* revert: changes of get_payment_ledger_entries

* perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled

* test: for immutable ledger

* test: add posting_date in create_sales_invoice

* fix: link validation err with immutable ledger on

* test: update testcase of the immutable ledger

* refactor(test): simpler test for immutable invariants

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit 9eeccecd30)

# Conflicts:
#	erpnext/accounts/general_ledger.py

Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com>
2026-05-22 14:25:32 +05:30
mergify[bot]
bde7f1660e fix(manufacturing): remove forecast_qty and adjust_qty fields from sa… (backport #55129) (#55136)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix(manufacturing): remove forecast_qty and adjust_qty fields from sa… (#55129)
2026-05-22 14:23:51 +05:30
Nihantra C. Patel
8a4cb28d90 perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled (#55130)
* perf: get payment ledger and remove update from delink when immutable ledger is enabled

* revert: changes of get_payment_ledger_entries

* perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled

* test: for immutable ledger

* test: add posting_date in create_sales_invoice

* fix: link validation err with immutable ledger on

* test: update testcase of the immutable ledger

* refactor(test): simpler test for immutable invariants

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit 9eeccecd30)

# Conflicts:
#	erpnext/accounts/general_ledger.py
2026-05-22 12:49:43 +05:30
diptanilsaha
498bd2fb4b Merge pull request #55144 from frappe/mergify/bp/version-16-hotfix/pr-55127
refactor: migrate get_tax_template to query builder with hierarchical group matching (backport #55127)
2026-05-22 01:48:45 +05:30
Khushi Rawat
bfb7a0e941 Merge pull request #55152 from frappe/mergify/bp/version-16-hotfix/pr-55146
fix: correct remarks for foreign currency payment entries (backport #55146)
2026-05-21 20:33:30 +05:30
khushi8112
f3f8f327df fix: correct remarks for foreign currency payment entries
(cherry picked from commit c6cde700b5)
2026-05-21 14:42:44 +00:00
Khushi Rawat
0d816010dd Merge pull request #55148 from frappe/mergify/bp/version-16-hotfix/pr-55147
fix: correct description for Is Rate Adjustment Entry (Debit Note) checkbox (backport #55147)
2026-05-21 20:08:20 +05:30
khushi8112
a39733ddd0 fix: correct description for Is Rate Adjustment Entry (Debit Note) checkbox
(cherry picked from commit 92c969478e)
2026-05-21 12:38:23 +00:00
Khushi Rawat
5c9149c5a5 Merge pull request #55145 from frappe/mergify/bp/version-16-hotfix/pr-55142
fix: don't reset net_purchase_amount for Composite Asset if already set (backport #55142)
2026-05-21 17:11:24 +05:30
khushi8112
99642b9636 fix: don't reset net_purchase_amount for Composite Asset if already set
(cherry picked from commit 98dae6e43a)
2026-05-21 11:38:28 +00:00
diptanilsaha
a63b344a0a test: add tests for supplier group hierarchy and use_for_shopping_cart filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 8c43118725)
2026-05-21 11:36:30 +00:00
diptanilsaha
54be4ee275 fix: default use_for_shopping_cart to 0 in set_taxes
Ensures regular transactions only match tax rules where
use_for_shopping_cart = 0, preventing webshop-specific rules
from applying to standard documents.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 4d43c74f5f)
2026-05-21 11:36:30 +00:00
diptanilsaha
c45d2a3487 refactor: rewrite get_tax_template using query builder
Migrates from raw frappe.db.sql with string interpolation to frappe.qb.
Adds hierarchical supplier_group matching (mirrors customer_group behaviour).
Removes unused get_customer_group_condition helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit f98975f51a)
2026-05-21 11:36:30 +00:00
diptanilsaha
82793cbd4d feat: add get_parent_supplier_groups using query builder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit cb610b79d2)
2026-05-21 11:36:29 +00:00
diptanilsaha
620161c526 refactor: migrate get_parent_customer_groups to query builder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 91a2a7b0a0)
2026-05-21 11:36:29 +00:00
mergify[bot]
dfc91441b4 fix: consumed operation cost calculation (backport #54858) (#55133)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: consumed operation cost calculation (#54858)
2026-05-21 10:33:43 +00:00
rohitwaghchaure
e630ab64eb Merge pull request #55139 from frappe/mergify/bp/version-16-hotfix/pr-55134
fix: removed redundant code (backport #55134)
2026-05-21 15:56:28 +05:30
Rohit Waghchaure
a7eb3acd1a fix: removed redundant code
(cherry picked from commit 14b17cd8a6)
2026-05-21 09:55:48 +00:00
Mihir Kandoi
02f4d9a4d6 chore: add whitelist (#55113) 2026-05-20 16:33:00 +00:00
mergify[bot]
6d8bbc5b6f chore: migrate Address/Contact custom fields from JSON fixtures to install (backport #55084) (#55088)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
fixtures to install (#55084)
2026-05-20 21:24:40 +05:30
mergify[bot]
cc438a4600 fix: item price with party condition (backport #55100) (#55107)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: item price with party condition (#55100)
2026-05-20 20:24:56 +05:30
mergify[bot]
b11365b8c2 fix: valuation rate missing for standalone credit notes for moving av… (backport #55102) (#55104)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: valuation rate missing for standalone credit notes for moving av… (#55102)
2026-05-20 11:49:54 +00:00
mergify[bot]
bb87ffc90a fix: set bin details when adding item using update items (backport #55096) (#55098)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: set bin details when adding item using update items (#55096)
2026-05-20 16:21:10 +05:30
mergify[bot]
17bc2b691f fix: incorrect error message string in sales order (backport #55090) (#55095)
Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com>
fix: incorrect error message string in sales order (#55090)
2026-05-20 09:33:40 +00:00
mergify[bot]
36aca51fbb fix(manufacturing): fetch from_bom name in production plan (backport #55085) (#55092)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix(manufacturing): fetch from_bom name in production plan (#55085)
2026-05-20 09:19:31 +00:00
mergify[bot]
56a9b37fac fix(stock): remove recalculate current qty function (backport #54774) (#55075)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): remove recalculate current qty function (#54774)
2026-05-20 14:41:55 +05:30
Frappe PR Bot
6ef4a2d82c chore(release): Bumped to Version 16.19.1
## [16.19.1](https://github.com/frappe/erpnext/compare/v16.19.0...v16.19.1) (2026-05-20)

### Bug Fixes

* faster range calculation on process period closing voucher ([fa4aa0c](fa4aa0c1b6))
2026-05-20 07:25:32 +00:00
ruthra kumar
e4e796146d Merge pull request #55082 from frappe/mergify/bp/version-16/pr-55072
perf: faster opening balance range calculation in process period closing voucher (backport #55072)
2026-05-20 12:53:58 +05:30
ruthra kumar
ea1d0cc277 refactor: ppcv select with for update and skip locked
(cherry picked from commit eba58b2837)
2026-05-20 06:51:01 +00:00
ruthra kumar
fa4aa0c1b6 fix: faster range calculation on process period closing voucher
(cherry picked from commit ee33574a6d)
2026-05-20 06:51:01 +00:00
ruthra kumar
05f641d3bc Merge pull request #55078 from frappe/mergify/bp/version-16-hotfix/pr-55072
perf: faster opening balance range calculation in process period closing voucher (backport #55072)
2026-05-20 12:06:32 +05:30
ruthra kumar
830d035459 refactor: ppcv select with for update and skip locked
(cherry picked from commit eba58b2837)
2026-05-20 06:19:03 +00:00
ruthra kumar
e56ee383bc fix: faster range calculation on process period closing voucher
(cherry picked from commit ee33574a6d)
2026-05-20 06:19:02 +00:00
Frappe PR Bot
fdfcbf72bd chore(release): Bumped to Version 16.19.0
# [16.19.0](https://github.com/frappe/erpnext/compare/v16.18.3...v16.19.0) (2026-05-20)

### Bug Fixes

* add filter subtitle in print formats ([c4037da](c4037daca8))
* add warehouse vaildation for repack entry (backport [#54866](https://github.com/frappe/erpnext/issues/54866)) ([#54901](https://github.com/frappe/erpnext/issues/54901)) ([596c257](596c2571f6))
* changes to gl print template ([caa524f](caa524f661))
* disallow editing on reversal journals ([6a53982](6a53982f4a))
* **general-ledger:** show raw GL entries when categorize_by is empty (backport [#54816](https://github.com/frappe/erpnext/issues/54816)) ([#54830](https://github.com/frappe/erpnext/issues/54830)) ([c041cd2](c041cd27b5))
* handle None delivery_date when sorting MPS data (backport [#55028](https://github.com/frappe/erpnext/issues/55028)) ([#55059](https://github.com/frappe/erpnext/issues/55059)) ([f272d32](f272d32f80))
* improve design and refactor ar print template ([059372a](059372add5))
* improve filter details render logic to avoid showing duplicate information ([040b31d](040b31d3a7))
* incoming rate for legacy serial no (backport [#54962](https://github.com/frappe/erpnext/issues/54962)) ([#54978](https://github.com/frappe/erpnext/issues/54978)) ([6bce78c](6bce78c66d))
* minor bug fixes for ar print template ([09b19f7](09b19f7a2a))
* minor bugs in print templates ([e1446fc](e1446fc6f4))
* minor changes in print template ([0ead229](0ead2296e6))
* minor changes in print template ([16bc28b](16bc28bd70))
* minor changes in print templates ([0d50e03](0d50e03595))
* minor text issues in print ([daaa4ca](daaa4ca0c8))
* normalize date comparison to avoid datatype mismatch ([42f6cb4](42f6cb40d1))
* **patch:** drop dead procedures first before other changes ([0df9591](0df9591910))
* **payment_entry:** fix paid/received amount calculation for multi-currency accounts (backport [#54963](https://github.com/frappe/erpnext/issues/54963)) ([#54970](https://github.com/frappe/erpnext/issues/54970)) ([48b09eb](48b09eb52e))
* posting date and time ([1c44c60](1c44c60dbd))
* prevent duplicate task execution and timestamp error in transaction deletion (backport [#55021](https://github.com/frappe/erpnext/issues/55021)) ([#55025](https://github.com/frappe/erpnext/issues/55025)) ([9857cc6](9857cc64d6))
* remove parent page ([10b4090](10b409005d))
* remove sql procedure method from AR report ([414319d](414319daeb))
* revamp print formats for accounts receivable summary and accounts payable summary reports ([928fab6](928fab6f7e))
* status not changing for dropshipped POs and SOs (backport [#54934](https://github.com/frappe/erpnext/issues/54934)) ([#54937](https://github.com/frappe/erpnext/issues/54937)) ([3c571a1](3c571a1691))
* stock balance showing incorrect value because of incorrect SLE ([0b3344b](0b3344bad9))
* **stock:** add whole number quantity validation in Stock Reconciliation (backport [#54922](https://github.com/frappe/erpnext/issues/54922)) ([#54925](https://github.com/frappe/erpnext/issues/54925)) ([c499454](c4994548c3))
* **stock:** ignore fetching warehouse account for asset items (backport [#54403](https://github.com/frappe/erpnext/issues/54403)) ([#54961](https://github.com/frappe/erpnext/issues/54961)) ([5e5b5cf](5e5b5cfa0c))
* **stock:** update buying amount calculation in gross profit report (backport [#55020](https://github.com/frappe/erpnext/issues/55020)) ([#55024](https://github.com/frappe/erpnext/issues/55024)) ([8870619](88706192d7))
* styling in trial_balance.html and print format ([9a18d31](9a18d318d9))
* toast message for item price insert ([#55009](https://github.com/frappe/erpnext/issues/55009)) ([c967792](c967792ccb))
* use route_options for Credit Note and Debit Note sidebar links (backport [#55026](https://github.com/frappe/erpnext/issues/55026)) ([#55063](https://github.com/frappe/erpnext/issues/55063)) ([1941c3b](1941c3b136))
* **UX:** Buying settings form cleanup ([#54731](https://github.com/frappe/erpnext/issues/54731)) ([e7ae296](e7ae296614))
* **UX:** Item master form cleanup ([#54538](https://github.com/frappe/erpnext/issues/54538)) ([0eb049c](0eb049cd85))
* validate company region in uae vat 201 (backport [#54899](https://github.com/frappe/erpnext/issues/54899)) ([#55055](https://github.com/frappe/erpnext/issues/55055)) ([4015c2b](4015c2b9a4))
* warn when accounting dimension fieldname conflicts with existing fields (backport [#55036](https://github.com/frappe/erpnext/issues/55036)) ([#55062](https://github.com/frappe/erpnext/issues/55062)) ([68a5eae](68a5eae3ff))

### Features

* add print format for accounts payable report ([1c6dc80](1c6dc80b70))
* introduce print format for Accounts Receivable report ([4e7f2ee](4e7f2eeaa0))
* introduce print formats for financial statements ([3283c46](3283c461f1))
* print format for report trial balance ([1d08448](1d08448d1a))

### Reverts

* Revert "fix: debit credit not equal in purchase transactions for mult… (backport [#54906](https://github.com/frappe/erpnext/issues/54906)) ([#54908](https://github.com/frappe/erpnext/issues/54908)) ([0d07083](0d07083299))
2026-05-20 04:10:54 +00:00
diptanilsaha
fb7f820885 Merge pull request #55051 from frappe/version-16-hotfix
chore: release v16
2026-05-20 09:39:15 +05:30
mergify[bot]
1941c3b136 fix: use route_options for Credit Note and Debit Note sidebar links (backport #55026) (#55063)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
fix: use route_options for Credit Note and Debit Note sidebar links (#55026)
2026-05-20 00:37:32 +05:30
mergify[bot]
f272d32f80 fix: handle None delivery_date when sorting MPS data (backport #55028) (#55059)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
fix: handle None delivery_date when sorting MPS data (#55028)
2026-05-20 00:37:14 +05:30
mergify[bot]
68a5eae3ff fix: warn when accounting dimension fieldname conflicts with existing fields (backport #55036) (#55062)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
fix: warn when accounting dimension fieldname conflicts with existing fields (#55036)
2026-05-19 23:34:44 +05:30
ruthra kumar
1b07844237 Merge pull request #55057 from frappe/mergify/bp/version-16-hotfix/pr-55053
fix(patch): drop dead procedures first before other changes (backport #55053)
2026-05-19 17:05:41 +05:30
mergify[bot]
4015c2b9a4 fix: validate company region in uae vat 201 (backport #54899) (#55055)
Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
fix: validate company region in uae vat 201 (#54899)
2026-05-19 11:29:17 +00:00
ruthra kumar
0df9591910 fix(patch): drop dead procedures first before other changes
(cherry picked from commit 61d24ba55f)
2026-05-19 11:08:51 +00:00
rohitwaghchaure
4403e1c0f4 Merge pull request #55048 from frappe/mergify/bp/version-16-hotfix/pr-55046
fix: stock balance showing incorrect value because of incorrect SLE (backport #55046)
2026-05-19 14:14:34 +05:30
Rohit Waghchaure
0b3344bad9 fix: stock balance showing incorrect value because of incorrect SLE
(cherry picked from commit 94b95d6c2f)
2026-05-19 08:22:34 +00:00
Ravibharathi
5cc335dd53 Merge pull request #55042 from frappe/mergify/bp/version-16-hotfix/pr-54761
fix: normalize date comparison to avoid datatype mismatch (backport #54761)
2026-05-19 11:47:25 +05:30
ervishnucs
42f6cb40d1 fix: normalize date comparison to avoid datatype mismatch
(cherry picked from commit 01e382b106)
2026-05-19 05:57:54 +00:00
mergify[bot]
88706192d7 fix(stock): update buying amount calculation in gross profit report (backport #55020) (#55024)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): update buying amount calculation in gross profit report (#55020)
2026-05-19 09:44:48 +05:30
mergify[bot]
9857cc64d6 fix: prevent duplicate task execution and timestamp error in transaction deletion (backport #55021) (#55025)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
fix: prevent duplicate task execution and timestamp error in transaction deletion (#55021)
2026-05-18 17:55:05 +00:00
ruthra kumar
ff0533d085 Merge pull request #54818 from frappe/mergify/bp/version-16-hotfix/pr-54783
fix: disallow editing on reversal journals (backport #54783)
2026-05-18 15:41:05 +05:30
ruthra kumar
b0f770780c Merge pull request #55015 from frappe/mergify/bp/version-16-hotfix/pr-55001
fix: remove sql procedure method from AR report (backport #55001)
2026-05-18 14:09:00 +05:30
ruthra kumar
414319daeb fix: remove sql procedure method from AR report
(cherry picked from commit 63a7142b9b)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/patches.txt
2026-05-18 13:51:16 +05:30
Nishka Gosalia
1945a2fe39 Merge pull request #55011 from frappe/mergify/bp/version-16-hotfix/pr-55009
fix: toast message for item price insert (backport #55009)
2026-05-18 11:59:48 +05:30
Nishka Gosalia
c967792ccb fix: toast message for item price insert (#55009)
(cherry picked from commit ae9c632e39)
2026-05-18 06:11:36 +00:00
Soham Kulkarni
253248c8e8 Merge pull request #55002 from frappe/mergify/bp/version-16-hotfix/pr-55000
fix: remove parent page (backport #55000)
2026-05-18 10:58:22 +05:30
sokumon
10b409005d fix: remove parent page
(cherry picked from commit e13bd9eaa6)
2026-05-18 05:01:35 +00:00
Ejaaz Khan
272ea30031 Merge pull request #54975 from frappe/mergify/bp/version-16-hotfix/pr-54655
refactor: remove dead print format (backport #54655)
2026-05-18 10:07:20 +05:30
MochaMind
098579ffbc chore: update POT file (#54990) 2026-05-17 21:43:32 +02:00
mergify[bot]
6bce78c66d fix: incoming rate for legacy serial no (backport #54962) (#54978)
fix: incoming rate for legacy serial no

(cherry picked from commit 2773b7c002)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-16 20:48:28 +05:30
mergify[bot]
5e5b5cfa0c fix(stock): ignore fetching warehouse account for asset items (backport #54403) (#54961)
* fix(stock): ignore fetching warehouse account for asset items

(cherry picked from commit 6fe08428c1)

* test(stock): add test to create pr for asset item without checking the stock account

(cherry picked from commit 8cf4402823)

---------

Co-authored-by: Sudharsanan11 <sudharsananashok1975@gmail.com>
2026-05-16 11:52:23 +00:00
Ejaaz Khan
2c78b6c36a refactor: remove dead print format
(cherry picked from commit c933c2bd53)
2026-05-15 11:57:31 +00:00
mergify[bot]
48b09eb52e fix(payment_entry): fix paid/received amount calculation for multi-currency accounts (backport #54963) (#54970)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-15 10:33:33 +00:00
ruthra kumar
799d6d159c Merge pull request #54960 from frappe/mergify/bp/version-16/pr-54941
fix: flag to disable opening balance calculation in general ledger (backport #54941)
2026-05-15 14:31:57 +05:30
ruthra kumar
48f59a033f refactor: flag to disable opening balance calculation
(cherry picked from commit 28a2230d02)
2026-05-15 07:32:52 +00:00
ruthra kumar
1716026e11 Merge pull request #54957 from frappe/mergify/bp/version-16-hotfix/pr-54941
fix: flag to disable opening balance calculation in general ledger (backport #54941)
2026-05-15 12:41:25 +05:30
ruthra kumar
b1e356fd97 refactor: flag to disable opening balance calculation
(cherry picked from commit 28a2230d02)
2026-05-15 06:51:20 +00:00
Frappe PR Bot
2807c9f08f chore(release): Bumped to Version 16.18.3
## [16.18.3](https://github.com/frappe/erpnext/compare/v16.18.2...v16.18.3) (2026-05-14)

### Bug Fixes

* status not changing for dropshipped POs and SOs (backport [#54934](https://github.com/frappe/erpnext/issues/54934)) (backport [#54937](https://github.com/frappe/erpnext/issues/54937)) ([#54938](https://github.com/frappe/erpnext/issues/54938)) ([5271773](5271773595))
2026-05-14 09:39:02 +00:00
mergify[bot]
5271773595 fix: status not changing for dropshipped POs and SOs (backport #54934) (backport #54937) (#54938)
fix: status not changing for dropshipped POs and SOs (backport #54934) (#54937)

fix: status not changing for dropshipped POs and SOs (#54934)

* fix: status not changing for dropshipped POs and SOs

* test: change test case to accomodate new flow

(cherry picked from commit 78a79120ea)


(cherry picked from commit 3c571a1691)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-14 09:37:26 +00:00
mergify[bot]
3c571a1691 fix: status not changing for dropshipped POs and SOs (backport #54934) (#54937)
fix: status not changing for dropshipped POs and SOs (#54934)

* fix: status not changing for dropshipped POs and SOs

* test: change test case to accomodate new flow

(cherry picked from commit 78a79120ea)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-14 14:47:35 +05:30
Nishka Gosalia
9a9b330af6 Merge pull request #54910 from frappe/mergify/bp/version-16-hotfix/pr-54731
fix(UX): Buying settings form cleanup (backport #54731)
2026-05-14 11:56:28 +05:30
Frappe PR Bot
dd35cd1f84 chore(release): Bumped to Version 16.18.2
## [16.18.2](https://github.com/frappe/erpnext/compare/v16.18.1...v16.18.2) (2026-05-14)

### Bug Fixes

* posting date and time ([ab09029](ab090295d9))
2026-05-14 05:35:39 +00:00
rohitwaghchaure
77a6299e8b Merge pull request #54931 from frappe/mergify/bp/version-16/pr-54928
fix: posting date and time (backport #54905) (backport #54928)
2026-05-14 11:04:07 +05:30
rohitwaghchaure
b79ec7cbdd chore: fix linter issue
(cherry picked from commit 3c993377aa)
(cherry picked from commit 21ada7799c)
2026-05-14 02:04:52 +00:00
rohitwaghchaure
927360dd1d chore: fixed test case
(cherry picked from commit c740f77a6f)
(cherry picked from commit f4e66914c6)
2026-05-14 02:04:52 +00:00
Rohit Waghchaure
ab090295d9 fix: posting date and time
(cherry picked from commit fb6c05f186)
(cherry picked from commit 1c44c60dbd)
2026-05-14 02:04:51 +00:00
rohitwaghchaure
76204b920d Merge pull request #54928 from frappe/mergify/bp/version-16-hotfix/pr-54905
fix: posting date and time (backport #54905)
2026-05-14 07:33:26 +05:30
rohitwaghchaure
21ada7799c chore: fix linter issue
(cherry picked from commit 3c993377aa)
2026-05-13 18:19:17 +00:00
rohitwaghchaure
f4e66914c6 chore: fixed test case
(cherry picked from commit c740f77a6f)
2026-05-13 18:19:17 +00:00
Rohit Waghchaure
1c44c60dbd fix: posting date and time
(cherry picked from commit fb6c05f186)
2026-05-13 18:19:17 +00:00
mergify[bot]
596c2571f6 fix: add warehouse vaildation for repack entry (backport #54866) (#54901)
fix: add warehouse vaildation for repack entry (#54866)

(cherry picked from commit bc07b2d3e5)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-13 15:24:51 +00:00
mergify[bot]
c4994548c3 fix(stock): add whole number quantity validation in Stock Reconciliation (backport #54922) (#54925)
fix(stock): add whole number quantity validation in Stock Reconciliation (#54922)

(cherry picked from commit f9dec73042)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-13 15:03:45 +00:00
Khushi Rawat
604f80f043 Merge pull request #54913 from frappe/mergify/bp/version-16-hotfix/pr-53843
feat: Accounts Payable print template revamp and print format introduction (backport #53843)
2026-05-13 16:07:00 +05:30
Khushi Rawat
4a4757dbc6 Merge pull request #54915 from frappe/mergify/bp/version-16-hotfix/pr-53762
feat: General ledger print template revamp and print format introduction (backport #53762)
2026-05-13 16:06:27 +05:30
Khushi Rawat
436e0269f8 Merge pull request #54914 from frappe/mergify/bp/version-16-hotfix/pr-53822
feat: Accounts Receivable print template revamp and print format introduction (backport #53822)
2026-05-13 16:05:47 +05:30
Khushi Rawat
5d73da5a85 Merge pull request #54912 from frappe/mergify/bp/version-16-hotfix/pr-53870
feat: AR and AP summary reports print template revamp and print format introduction (backport #53870)
2026-05-13 15:57:07 +05:30
Khushi Rawat
886c7cc5a3 Merge pull request #54911 from frappe/mergify/bp/version-16-hotfix/pr-53934
feat: Financial Statements print format introduction (backport #53934)
2026-05-13 15:56:45 +05:30
Frappe PR Bot
c4b7b15824 chore(release): Bumped to Version 16.18.1
## [16.18.1](https://github.com/frappe/erpnext/compare/v16.18.0...v16.18.1) (2026-05-13)

### Reverts

* Revert "fix: debit credit not equal in purchase transactions for mult… (backport [#54906](https://github.com/frappe/erpnext/issues/54906)) (backport [#54908](https://github.com/frappe/erpnext/issues/54908)) ([#54916](https://github.com/frappe/erpnext/issues/54916)) ([cfd3847](cfd3847255))
2026-05-13 10:16:21 +00:00
mergify[bot]
cfd3847255 Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (backport #54908) (#54916)
Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (#54908)

Revert "fix: debit credit not equal in purchase transactions for mult… (#54906)

* Revert "fix: debit credit not equal in purchase transactions for multi currency"

This reverts commit 75bcea57f4.

* Revert "test: add test case"

This reverts commit 1d30a202c3.

* Revert "fix: include rejected qty in tax (purchase receipt)"

This reverts commit 8c9a88abbe.

(cherry picked from commit cf5e8ce878)


(cherry picked from commit 0d07083299)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 10:14:46 +00:00
Khushi Rawat
d430736177 Merge pull request #54634 from frappe/mergify/bp/version-16-hotfix/pr-54538
fix(UX): Item master form cleanup (backport #54538)
2026-05-13 15:11:31 +05:30
Shllokkk
caa524f661 fix: changes to gl print template
(cherry picked from commit e8d08df044)
2026-05-13 09:41:22 +00:00
Shllokkk
cd69b66761 refactor: table body data rendering cleanup
(cherry picked from commit 0d4f56bf84)
2026-05-13 09:41:22 +00:00
Shllokkk
040b31d3a7 fix: improve filter details render logic to avoid showing duplicate information
(cherry picked from commit 9660debe28)
2026-05-13 09:41:21 +00:00
Shllokkk
04893ae0e3 refactor: clean and standardize print template for general ledger report
(cherry picked from commit 3ba36212b0)
2026-05-13 09:41:21 +00:00
Shllokkk
0ead2296e6 fix: minor changes in print template
(cherry picked from commit e3019c827c)
2026-05-13 09:40:53 +00:00
Shllokkk
09b19f7a2a fix: minor bug fixes for ar print template
(cherry picked from commit 4228885f1e)
2026-05-13 09:40:52 +00:00
Shllokkk
4e7f2eeaa0 feat: introduce print format for Accounts Receivable report
(cherry picked from commit e6a32a9d02)
2026-05-13 09:40:52 +00:00
Shllokkk
059372add5 fix: improve design and refactor ar print template
(cherry picked from commit ffc59ebc9c)
2026-05-13 09:40:52 +00:00
Shllokkk
16bc28bd70 fix: minor changes in print template
(cherry picked from commit 915fcc0166)
2026-05-13 09:40:24 +00:00
Shllokkk
1c6dc80b70 feat: add print format for accounts payable report
(cherry picked from commit 2bf9d41797)
2026-05-13 09:40:24 +00:00
Shllokkk
748a3d72a3 refactor: revamp print template for accounts payable report
(cherry picked from commit c051536182)
2026-05-13 09:40:23 +00:00
Shllokkk
0d50e03595 fix: minor changes in print templates
(cherry picked from commit 44e0b36093)
2026-05-13 09:39:56 +00:00
Shllokkk
e1446fc6f4 fix: minor bugs in print templates
(cherry picked from commit 86ee9959a2)
2026-05-13 09:39:56 +00:00
Shllokkk
928fab6f7e fix: revamp print formats for accounts receivable summary and accounts payable summary reports
(cherry picked from commit 5bbcb73808)
2026-05-13 09:39:55 +00:00
mergify[bot]
0d07083299 Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (#54908)
Revert "fix: debit credit not equal in purchase transactions for mult… (#54906)

* Revert "fix: debit credit not equal in purchase transactions for multi currency"

This reverts commit 75bcea57f4.

* Revert "test: add test case"

This reverts commit 1d30a202c3.

* Revert "fix: include rejected qty in tax (purchase receipt)"

This reverts commit 8c9a88abbe.

(cherry picked from commit cf5e8ce878)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 15:09:38 +05:30
Shllokkk
c4037daca8 fix: add filter subtitle in print formats
(cherry picked from commit e82b4d9ca7)
2026-05-13 09:39:05 +00:00
Shllokkk
9a18d318d9 fix: styling in trial_balance.html and print format
(cherry picked from commit 5858b14071)
2026-05-13 09:39:05 +00:00
Shllokkk
2ff9f00ce0 refactor: print templates for financial statements
(cherry picked from commit e8777a1e34)
2026-05-13 09:39:05 +00:00
Shllokkk
daaa4ca0c8 fix: minor text issues in print
(cherry picked from commit fa0a9085ca)
2026-05-13 09:39:04 +00:00
Shllokkk
1d08448d1a feat: print format for report trial balance
(cherry picked from commit ac7e5271b0)
2026-05-13 09:39:04 +00:00
Shllokkk
3283c461f1 feat: introduce print formats for financial statements
(cherry picked from commit 82cac9c40f)
2026-05-13 09:39:04 +00:00
Nishka Gosalia
e7ae296614 fix(UX): Buying settings form cleanup (#54731)
* fix(UX): Buying settings form cleanup

* fix: controller approach modification

* fix: dark mode support

(cherry picked from commit 45f05fbeaa)
2026-05-13 09:24:12 +00:00
mergify[bot]
c041cd27b5 fix(general-ledger): show raw GL entries when categorize_by is empty (backport #54816) (#54830)
fix(general-ledger): show raw GL entries when categorize_by is empty (#54816)

(cherry picked from commit dfbe847307)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-05-13 05:42:11 +05:30
Frappe PR Bot
dc914adb62 chore(release): Bumped to Version 16.18.0
# [16.18.0](https://github.com/frappe/erpnext/compare/v16.17.0...v16.18.0) (2026-05-12)

### Bug Fixes

* added permission validation for `deactivate_sales_person` (backport [#54884](https://github.com/frappe/erpnext/issues/54884)) ([#54886](https://github.com/frappe/erpnext/issues/54886)) ([98de025](98de025a09))
* check if item is dropshipped before updating quantity (backport [#54825](https://github.com/frappe/erpnext/issues/54825)) ([#54827](https://github.com/frappe/erpnext/issues/54827)) ([0db7e1e](0db7e1e56b))
* **crm:** handle empty _assign in appointment auto assignment (backport [#54782](https://github.com/frappe/erpnext/issues/54782)) ([#54795](https://github.com/frappe/erpnext/issues/54795)) ([f36bdaa](f36bdaadae))
* decimal issue ([8b9b83a](8b9b83a9df))
* do not rely on client side to update quantities during partial d… (backport [#54804](https://github.com/frappe/erpnext/issues/54804)) ([#54821](https://github.com/frappe/erpnext/issues/54821)) ([f24b556](f24b556336))
* fetch get_item_tax_template while update items (backport [#53708](https://github.com/frappe/erpnext/issues/53708)) ([#54767](https://github.com/frappe/erpnext/issues/54767)) ([4fbaea1](4fbaea17f8))
* incorrect serial nos picked during disassemble (backport [#54757](https://github.com/frappe/erpnext/issues/54757)) ([#54760](https://github.com/frappe/erpnext/issues/54760)) ([66ae590](66ae590adc))
* incorrect validation thrown for drop shipped PI (backport [#54751](https://github.com/frappe/erpnext/issues/54751)) ([#54753](https://github.com/frappe/erpnext/issues/54753)) ([379ebbe](379ebbe8c4))
* raw material should not have target warehouse in manufacture entry (backport [#54849](https://github.com/frappe/erpnext/issues/54849)) ([#54861](https://github.com/frappe/erpnext/issues/54861)) ([3dbadfa](3dbadfadd5))
* rename supplier wise stock analytics report ([7086db1](7086db1e1c))
* **stock:** apply filters for rejected warehouse in pick list (backport [#54733](https://github.com/frappe/erpnext/issues/54733)) ([#54776](https://github.com/frappe/erpnext/issues/54776)) ([cf0d9df](cf0d9dfbfd))
* **stock:** ignore reserved qty for stock levels in batch (backport [#54790](https://github.com/frappe/erpnext/issues/54790)) ([#54797](https://github.com/frappe/erpnext/issues/54797)) ([338d190](338d1904c1))
* **stock:** priorities pick list parent warehouse (backport [#54788](https://github.com/frappe/erpnext/issues/54788)) ([#54793](https://github.com/frappe/erpnext/issues/54793)) ([d3bc629](d3bc629f68))
* **task:** update depends_on for closing date and review date [#54850](https://github.com/frappe/erpnext/issues/54850) (backport [#54852](https://github.com/frappe/erpnext/issues/54852)) ([#54863](https://github.com/frappe/erpnext/issues/54863)) ([b962a1a](b962a1a0cd))
* validate variant values (backport [#54831](https://github.com/frappe/erpnext/issues/54831)) ([#54839](https://github.com/frappe/erpnext/issues/54839)) ([87b798b](87b798b936))

### Features

* partial delivery in dropshipping (backport [#54787](https://github.com/frappe/erpnext/issues/54787)) ([#54800](https://github.com/frappe/erpnext/issues/54800)) ([f64f871](f64f871d45))
* Philippines chart of account (backport [#53918](https://github.com/frappe/erpnext/issues/53918)) ([#54888](https://github.com/frappe/erpnext/issues/54888)) ([8f03108](8f0310859d))
2026-05-12 18:49:27 +00:00
diptanilsaha
41bff45d7a Merge pull request #54865 from frappe/version-16-hotfix
chore: release v16
2026-05-13 00:17:54 +05:30
mergify[bot]
8f0310859d feat: Philippines chart of account (backport #53918) (#54888)
feat: Added Philippines chart of account json file (#53918)

* feat: Added philipinnes chart of account json file



* feat: made changes as per review comments and corrected indentation

* feat: made changes as per review comments

* feat: made changes as per review comments to resolve the issues

* fix: fixed changes as per review comments



* fix: fixed changes as per review comments on bank group account



---------




(cherry picked from commit 5560f6c270)

Signed-off-by: Soham-ambibuzz <soham.pawar@ambibuzz.com>
Signed-off-by: soham7117 <sohampawar626@gmail.com>
Co-authored-by: Soham-ambibuzz <soham.pawar@ambibuzz.com>
Co-authored-by: soham7117 <sohampawar626@gmail.com>
2026-05-12 16:46:05 +00:00
mergify[bot]
98de025a09 fix: added permission validation for deactivate_sales_person (backport #54884) (#54886)
* fix: added permission validation for `deactivate_sales_person` (#54884)

(cherry picked from commit 9134db9cd3)

# Conflicts:
#	erpnext/setup/doctype/employee/employee.py

* chore: resolved conflict

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-12 16:31:21 +00:00
mergify[bot]
b962a1a0cd fix(task): update depends_on for closing date and review date #54850 (backport #54852) (#54863)
fix(task): update depends_on for closing date and review date #54850 (#54852)

(cherry picked from commit 3532c1cc69)

Co-authored-by: Jaypal Lakum <96212547+jp-the-dev@users.noreply.github.com>
2026-05-12 10:13:49 +00:00
mergify[bot]
3dbadfadd5 fix: raw material should not have target warehouse in manufacture entry (backport #54849) (#54861)
fix: raw material should not have target warehouse in manufacture entry (#54849)

(cherry picked from commit b5527cf328)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-12 09:46:45 +00:00
Nishka Gosalia
67ad437dd3 Merge pull request #54854 from frappe/mergify/bp/version-16-hotfix/pr-54835
fix: rename supplier wise stock analytics report (backport #54835)
2026-05-12 14:08:34 +05:30
nishkagosalia
7086db1e1c fix: rename supplier wise stock analytics report
(cherry picked from commit 85206e0278)
2026-05-12 07:08:04 +00:00
mergify[bot]
87b798b936 fix: validate variant values (backport #54831) (#54839)
fix: validate variant values (#54831)

(cherry picked from commit 95705f18aa)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-11 21:23:34 +05:30
ruthra kumar
b894b02ebc Merge pull request #54832 from frappe/mergify/bp/version-16-hotfix/pr-54828
refactor(test): speed up payment reconciliation tests (backport #54828)
2026-05-11 14:28:02 +05:30
ruthra kumar
1d20469c99 refactor(test): speed up payment reconciliation tests
(cherry picked from commit f58242dca7)
2026-05-11 08:40:07 +00:00
mergify[bot]
0db7e1e56b fix: check if item is dropshipped before updating quantity (backport #54825) (#54827)
fix: check if item is dropshipped before updating quantity (#54825)

(cherry picked from commit 23e9ad3fd9)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-11 08:08:09 +00:00
mergify[bot]
f24b556336 fix: do not rely on client side to update quantities during partial d… (backport #54804) (#54821)
* fix: do not rely on client side to update quantities during partial d… (#54804)

fix: do not rely on client side to update quantities during partial dropship
(cherry picked from commit 03acbc3dc9)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.py

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-11 07:40:14 +00:00
ruthra kumar
6a53982f4a fix: disallow editing on reversal journals
(cherry picked from commit 26ca7445eb)
2026-05-11 04:38:45 +00:00
MochaMind
1fcd2837e8 chore: update POT file (#54814) 2026-05-10 13:56:48 +02:00
mergify[bot]
f64f871d45 feat: partial delivery in dropshipping (backport #54787) (#54800)
* feat: partial delivery in dropshipping (#54787)

(cherry picked from commit db74360396)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.py
#	erpnext/buying/doctype/purchase_order_item/purchase_order_item.json

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-09 02:01:42 +00:00
mergify[bot]
f36bdaadae fix(crm): handle empty _assign in appointment auto assignment (backport #54782) (#54795)
fix(crm): handle empty _assign in appointment auto assignment (#54782)

(cherry picked from commit a4a389bd41)

Co-authored-by: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com>
2026-05-08 12:55:38 +00:00
mergify[bot]
d3bc629f68 fix(stock): priorities pick list parent warehouse (backport #54788) (#54793)
fix(stock): priorities pick list parent warehouse (#54788)

(cherry picked from commit 4e850f31d5)

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-05-08 12:42:02 +00:00
mergify[bot]
338d1904c1 fix(stock): ignore reserved qty for stock levels in batch (backport #54790) (#54797)
fix(stock): ignore reserved qty for stock levels in batch (#54790)

(cherry picked from commit 0b6a372a52)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-08 12:35:22 +00:00
mergify[bot]
4fbaea17f8 fix: fetch get_item_tax_template while update items (backport #53708) (#54767)
* fix: fetch get_item_tax_template while update items

(cherry picked from commit 03c9d16ca6)

* fix: resolve item tax template from item group in update items

(cherry picked from commit 97e7916b66)

* fix: resolve item tax template from item group in update items

(cherry picked from commit ad22256b2d)

---------

Co-authored-by: ervishnucs <ervishnucs369@gmail.com>
2026-05-07 16:04:20 +05:30
mergify[bot]
cf0d9dfbfd fix(stock): apply filters for rejected warehouse in pick list (backport #54733) (#54776)
fix(stock): apply filters for rejected warehouse in pick list (#54733)

(cherry picked from commit 0fc96e8f7d)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-07 10:31:19 +00:00
mergify[bot]
66ae590adc fix: incorrect serial nos picked during disassemble (backport #54757) (#54760)
fix: incorrect serial nos picked during disassemble

(cherry picked from commit 25f7fa548d)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-06 16:06:03 +05:30
mergify[bot]
379ebbe8c4 fix: incorrect validation thrown for drop shipped PI (backport #54751) (#54753)
* fix: incorrect validation thrown for drop shipped PI (#54751)

(cherry picked from commit 907a809f3f)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-06 05:55:26 +00:00
Frappe PR Bot
7b494dc9e8 chore(release): Bumped to Version 16.17.0
# [16.17.0](https://github.com/frappe/erpnext/compare/v16.16.0...v16.17.0) (2026-05-05)

### Bug Fixes

* accounts and account types in German CoA "SKR 03" (backport [#54711](https://github.com/frappe/erpnext/issues/54711)) ([#54713](https://github.com/frappe/erpnext/issues/54713)) ([982810a](982810a700))
* add missing fields in set_currency_labels (backport [#54689](https://github.com/frappe/erpnext/issues/54689)) ([#54690](https://github.com/frappe/erpnext/issues/54690)) ([bca893a](bca893a508))
* Backfill `not_applicable` on Item Tax Template Details for German companies (backport [#54682](https://github.com/frappe/erpnext/issues/54682)) ([#54686](https://github.com/frappe/erpnext/issues/54686)) ([a22d773](a22d773341))
* copy project from first row to new rows (backport [#53295](https://github.com/frappe/erpnext/issues/53295)) ([#54620](https://github.com/frappe/erpnext/issues/54620)) ([e24ab72](e24ab72c0d))
* correct project filter in buying doctypes (backport [#54644](https://github.com/frappe/erpnext/issues/54644)) ([#54652](https://github.com/frappe/erpnext/issues/54652)) ([86cf256](86cf256358))
* correct titles set to {customer_name} or {supplier_name} text strings (backport [#54656](https://github.com/frappe/erpnext/issues/54656)) ([#54669](https://github.com/frappe/erpnext/issues/54669)) ([38cfeb1](38cfeb1bb7))
* dont show serial/batch button when PR is submitted (backport [#54642](https://github.com/frappe/erpnext/issues/54642)) ([#54646](https://github.com/frappe/erpnext/issues/54646)) ([6dbc17d](6dbc17d71a))
* error when creating quotation from CRM (backport [#54722](https://github.com/frappe/erpnext/issues/54722)) ([#54725](https://github.com/frappe/erpnext/issues/54725)) ([2cd4c1a](2cd4c1a052))
* hide payment and payment request buttons based on permissions in invoices and orders (backport [#53920](https://github.com/frappe/erpnext/issues/53920)) ([#54736](https://github.com/frappe/erpnext/issues/54736)) ([e60490d](e60490dceb))
* incorrect expense account book in purchase return (backport [#54681](https://github.com/frappe/erpnext/issues/54681)) ([#54693](https://github.com/frappe/erpnext/issues/54693)) ([0dade2c](0dade2c38c))
* mark item tax templates as not applicable (backport [#54673](https://github.com/frappe/erpnext/issues/54673)) ([#54677](https://github.com/frappe/erpnext/issues/54677)) ([126e13b](126e13be25))
* **payment_entry:** convert the date args to string type before escaping in `get_outstanding_reference_documents` (backport [#54639](https://github.com/frappe/erpnext/issues/54639)) ([#54648](https://github.com/frappe/erpnext/issues/54648)) ([19a8ebe](19a8ebe8a5))
* **project:** use user.email for invitations and skip disabled users. (backport [#54561](https://github.com/frappe/erpnext/issues/54561)) ([#54667](https://github.com/frappe/erpnext/issues/54667)) ([288cdf3](288cdf3bf0))
* py error on sales forecast doctype (backport [#54641](https://github.com/frappe/erpnext/issues/54641)) ([#54643](https://github.com/frappe/erpnext/issues/54643)) ([7bd360a](7bd360aa29))
* Remove bom stock report link from manufacturing workspace ([0f27881](0f27881fed))
* **selling:** blanket order ordered qty recalculation on sales order status change (backport [#54593](https://github.com/frappe/erpnext/issues/54593)) ([#54623](https://github.com/frappe/erpnext/issues/54623)) ([9db03bc](9db03bc520))
* set valid_from in created Item Price (backport [#54696](https://github.com/frappe/erpnext/issues/54696)) ([#54700](https://github.com/frappe/erpnext/issues/54700)) ([bbb4e79](bbb4e79d0a))
* show correct status in Serial No Ledger (backport [#54567](https://github.com/frappe/erpnext/issues/54567)) ([#54626](https://github.com/frappe/erpnext/issues/54626)) ([d6f2ff6](d6f2ff6b87))
* show in and out qty in the stock ledger report for stock recos ([d27cf48](d27cf48b19))
* skip depreciation rescheduling when asset is fully depreciated on sale ([d3c893d](d3c893d08b))
* skip rescheduling only for asset being disposed ([07a957c](07a957c164))
* use RecoverableErrors isinstance check for repost timeout status (backport [#54543](https://github.com/frappe/erpnext/issues/54543)) ([#54649](https://github.com/frappe/erpnext/issues/54649)) ([b300159](b3001595ab))

### Features

* copy terms attachments to transactions (backport [#53403](https://github.com/frappe/erpnext/issues/53403)) ([#54661](https://github.com/frappe/erpnext/issues/54661)) ([bd932da](bd932da08b))
* **ux:** Naming series dialog ([#54554](https://github.com/frappe/erpnext/issues/54554)) ([48ebb4c](48ebb4ca61))

### Performance Improvements

* max recursion depth error in serial no (backport [#54629](https://github.com/frappe/erpnext/issues/54629)) ([#54631](https://github.com/frappe/erpnext/issues/54631)) ([808214f](808214fd95))
2026-05-05 16:32:20 +00:00
rohitwaghchaure
2bc07f18a7 Merge pull request #54745 from frappe/mergify/bp/version-16-hotfix/pr-54723
fix: decimal issue in stock ageing report (backport #54723)
2026-05-05 22:02:16 +05:30
diptanilsaha
ed69dafbe8 Merge pull request #54740 from frappe/version-16-hotfix 2026-05-05 22:00:39 +05:30
Nishka Gosalia
c985f94009 Merge pull request #54743 from frappe/mergify/bp/version-16-hotfix/pr-54732
fix: Remove bom stock report link from manufacturing workspace (backport #54732)
2026-05-05 16:44:55 +05:30
Rohit Waghchaure
8b9b83a9df fix: decimal issue
(cherry picked from commit 542eb6aca4)
2026-05-05 11:13:04 +00:00
nishkagosalia
0f27881fed fix: Remove bom stock report link from manufacturing workspace
(cherry picked from commit f86568b078)
2026-05-05 10:51:13 +00:00
mergify[bot]
e60490dceb fix: hide payment and payment request buttons based on permissions in invoices and orders (backport #53920) (#54736)
Co-authored-by: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com>
Co-authored-by: ravibharathi656 <ravibharathi656@gmail.com>
fix: hide payment and payment request buttons based on permissions in invoices and orders (#53920)
2026-05-05 12:17:57 +05:30
mergify[bot]
2cd4c1a052 fix: error when creating quotation from CRM (backport #54722) (#54725)
fix: error when creating quotation from CRM (#54722)

(cherry picked from commit 2d3190effb)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-04 16:04:03 +00:00
mergify[bot]
982810a700 fix: accounts and account types in German CoA "SKR 03" (backport #54711) (#54713)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: accounts and account types in German CoA "SKR 03" (#54711)
2026-05-03 17:49:02 +00:00
MochaMind
18006b978f chore: update POT file (#54710) 2026-05-03 14:24:28 +02:00
mergify[bot]
bbb4e79d0a fix: set valid_from in created Item Price (backport #54696) (#54700)
* fix: set valid_from in created Item Price (#54696)

Co-authored-by: Kaajal-Chhattani <kaajal.chhattani@aurigait.com>
(cherry picked from commit 6246a9aa6e)

# Conflicts:
#	erpnext/stock/get_item_details.py

* chore: resolve conflicts

---------

Co-authored-by: Kaajalchhattani <89331214+Kaajalchhattani@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-02 16:45:52 +00:00
mergify[bot]
bca893a508 fix: add missing fields in set_currency_labels (backport #54689) (#54690)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: add missing fields in set_currency_labels (#54689)
2026-05-01 14:39:39 +02:00
mergify[bot]
0dade2c38c fix: incorrect expense account book in purchase return (backport #54681) (#54693)
fix: incorrect expense account book in purchase return

(cherry picked from commit 2a720e7008)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-01 12:47:05 +05:30
mergify[bot]
a22d773341 fix: Backfill not_applicable on Item Tax Template Details for German companies (backport #54682) (#54686)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: Backfill `not_applicable` on Item Tax Template Details for German companies (#54682)
2026-05-01 04:29:06 +02:00
mergify[bot]
126e13be25 fix: mark item tax templates as not applicable (backport #54673) (#54677)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: mark item tax templates as not applicable (#54673)
2026-04-30 17:52:24 +02:00
mergify[bot]
288cdf3bf0 fix(project): use user.email for invitations and skip disabled users. (backport #54561) (#54667)
fix(project): use user.email for invitations and skip disabled users. (#54561)

* fix(project): use user.email for invitations and skip disabled users.

* Update erpnext/projects/doctype/project/project.py



* fix(project): remove duplicate loop causing indentation error

* fix(project): resolve pre-commit hook failure

---------


(cherry picked from commit 231dd1856f)

Co-authored-by: Hemil-Sangani <hemil@sanskartechnolab.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-30 14:35:12 +05:30
rohitwaghchaure
2422237c1a Merge pull request #54671 from frappe/mergify/bp/version-16-hotfix/pr-54664
fix: show in and out qty in the stock ledger report for stock recos (backport #54664)
2026-04-30 14:34:23 +05:30
mergify[bot]
38cfeb1bb7 fix: correct titles set to {customer_name} or {supplier_name} text strings (backport #54656) (#54669)
Co-authored-by: Trusted Computer <75872475+trustedcomputer@users.noreply.github.com>
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
fix: correct titles set to {customer_name} or {supplier_name} text strings (#54656)
2026-04-30 08:52:23 +00:00
Rohit Waghchaure
d27cf48b19 fix: show in and out qty in the stock ledger report for stock recos
(cherry picked from commit da081254a6)
2026-04-30 08:44:26 +00:00
Khushi Rawat
c232f1f450 Merge pull request #54659 from frappe/mergify/bp/version-16-hotfix/pr-54658
fix: skip depreciation rescheduling when asset is fully depreciated on sale (backport #54658)
2026-04-30 11:31:15 +05:30
mergify[bot]
bd932da08b feat: copy terms attachments to transactions (backport #53403) (#54661)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-04-29 23:43:28 +02:00
khushi8112
07a957c164 fix: skip rescheduling only for asset being disposed
(cherry picked from commit 88b82383f5)
2026-04-29 21:05:17 +00:00
khushi8112
d3c893d08b fix: skip depreciation rescheduling when asset is fully depreciated on sale
(cherry picked from commit c4155b6c81)
2026-04-29 21:05:17 +00:00
mergify[bot]
b3001595ab fix: use RecoverableErrors isinstance check for repost timeout status (backport #54543) (#54649)
fix: use RecoverableErrors isinstance check for repost timeout status

When a Repost Item Valuation job is killed by an RQ worker timeout
(JobTimeoutException raised via SIGALRM), the existing status detection
relied solely on traceback string matching for 'timeout' or 'Deadlock'.

This is unreliable because SIGALRM can interrupt a C-extension call
(e.g. inside pypika's copy.copy()) before Python records the exception
in the traceback. In that case the traceback shows only the interrupted
frame -- not JobTimeoutException -- so the job is permanently marked
'Failed' instead of 'In Progress', preventing the scheduler from
automatically retrying it.

RecoverableErrors = (JobTimeoutException, QueryDeadlockError,
QueryTimeoutError) is already defined at the top of this file and is
already used further down in the same except block to suppress email
notifications. Extend its use to also guard the status decision.

The traceback string fallback is kept as a secondary check for
forward compatibility with other timeout signals.

Fixes: jobs permanently stuck as 'Failed' after RQ worker timeout,
requiring manual re-queue to resume reposting.

(cherry picked from commit a49e2de866)

Co-authored-by: Assem Bahnasy <bahnasyassem@gmail.com>
2026-04-29 12:02:04 +00:00
mergify[bot]
86cf256358 fix: correct project filter in buying doctypes (backport #54644) (#54652)
fix: correct project filter in buying doctypes (#54644)

(cherry picked from commit a04c028522)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 17:28:12 +05:30
mergify[bot]
19a8ebe8a5 fix(payment_entry): convert the date args to string type before escaping in get_outstanding_reference_documents (backport #54639) (#54648)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): convert the date args to string type before escaping in `get_outstanding_reference_documents` (#54639)
2026-04-29 11:45:24 +00:00
mergify[bot]
6dbc17d71a fix: dont show serial/batch button when PR is submitted (backport #54642) (#54646)
fix: dont show serial/batch button when PR is submitted (#54642)

(cherry picked from commit 060defcc2b)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 11:32:37 +00:00
mergify[bot]
7bd360aa29 fix: py error on sales forecast doctype (backport #54641) (#54643)
fix: py error on sales forecast doctype (#54641)

fix: py error on sales forecase doctype
(cherry picked from commit d0d8cff48f)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 11:13:18 +00:00
Nishka Gosalia
2e438011da Merge pull request #54635 from frappe/mergify/bp/version-16-hotfix/pr-54554 2026-04-29 15:21:22 +05:30
Nishka Gosalia
48ebb4ca61 feat(ux): Naming series dialog (#54554)
(cherry picked from commit 844f3dbc0b)
2026-04-29 09:15:45 +00:00
Khushi Rawat
0eb049cd85 fix(UX): Item master form cleanup (#54538)
* fix: UI improvements for item form

* fix: add descriptions and tooltips to all checkboxes

* feat: show toast notification when item price is created

* fix: do not use selling rate for opening stock entry

* fix: add descriptions and tooltips to item default fields

* fix(test): give valuation rate for opening stock entry creation

* fix: moving naming series toggle before the return

* refactor: more changes in the form UI

(cherry picked from commit 43937acd8b)
2026-04-29 09:15:20 +00:00
mergify[bot]
808214fd95 perf: max recursion depth error in serial no (backport #54629) (#54631)
perf: max recursion depth error in serial no (#54629)

(cherry picked from commit 503b5bf140)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 08:53:07 +00:00
mergify[bot]
d6f2ff6b87 fix: show correct status in Serial No Ledger (backport #54567) (#54626)
* refactor: extract SN status logic

(cherry picked from commit cb2e6e1e2e)

* fix: show correct status in Serial No Ledger

(cherry picked from commit 2b3e047143)

---------

Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2026-04-29 13:55:18 +05:30
mergify[bot]
9db03bc520 fix(selling): blanket order ordered qty recalculation on sales order status change (backport #54593) (#54623)
fix(selling): blanket order ordered qty recalculation on sales order status change (#54593)

(cherry picked from commit d68801e73a)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-04-29 06:47:55 +00:00
mergify[bot]
e24ab72c0d fix: copy project from first row to new rows (backport #53295) (#54620)
fix: copy project to new item row from parent

(cherry picked from commit 68cc518497)

Co-authored-by: ravibharathi656 <ravibharathi656@gmail.com>
2026-04-29 11:55:46 +05:30
193 changed files with 10824 additions and 4764 deletions

View File

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

View File

@@ -1,126 +0,0 @@
{
"custom_fields": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2018-12-28 22:29:21.828090",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "tax_category",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 15,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "fax",
"label": "Tax Category",
"length": 0,
"mandatory_depends_on": null,
"modified": "2018-12-28 22:29:21.828090",
"modified_by": "Administrator",
"name": "Address-tax_category",
"no_copy": 0,
"options": "Tax Category",
"owner": "Administrator",
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2020-10-14 17:41:40.878179",
"default": "0",
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "is_your_company_address",
"fieldtype": "Check",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 20,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "linked_with",
"label": "Is Your Company Address",
"length": 0,
"mandatory_depends_on": null,
"modified": "2020-10-14 17:41:40.878179",
"modified_by": "Administrator",
"name": "Address-is_your_company_address",
"no_copy": 0,
"options": null,
"owner": "Administrator",
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"width": null
}
],
"custom_perms": [],
"doctype": "Address",
"property_setters": [],
"sync_on_migrate": 1
}

View File

@@ -34,6 +34,13 @@
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Anlagen im Bau": {
"is_group": 1,
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
"account_number": "0498",
"account_type": "Capital Work in Progress"
}
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
}
@@ -317,13 +324,21 @@
"account_number": "3800",
"account_type": "Expenses Included In Asset Valuation"
},
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
"account_number": "3960",
"account_type": "Stock Adjustment"
},
"Herstellungskosten": {
"account_number": "4996",
"account_type": "Cost of Goods Sold"
},
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
"account_number": "2310",
"account_type": "Expense Account"
},
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
"account_number": "2320",
"account_type": "Stock Adjustment"
"account_type": "Expense Account"
},
"Verwaltungskosten": {
"account_number": "4997",
@@ -340,7 +355,7 @@
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Accumulated Depreciation"
"account_type": "Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",

View File

@@ -0,0 +1,840 @@
{
"name": "Philippines",
"country": "Philippines",
"tree": {
"Asset": {
"account_number": "1000",
"is_group": 1,
"root_type": "Asset",
"Current Assets": {
"account_number": "1001",
"is_group": 1,
"root_type": "Asset",
"Cash": {
"account_number": "1100",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Cash on Hand": {
"account_number": "1101",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
},
"Petty Cash Fund": {
"account_number": "1200",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Petty Cash Fund": {
"account_number": "1201",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
}
}
},
"Bank Accounts": {
"account_number": "1102",
"is_group": 1,
"root_type": "Asset",
"account_type": "Bank"
},
"Advances to Officers & Employees": {
"account_number": "1290",
"is_group": 1,
"root_type": "Asset",
"Advances to Officers & Employees": {
"account_number": "1291",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable Trade": {
"account_number": "1300",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Trade": {
"account_number": "1301",
"is_group": 0,
"root_type": "Asset",
"account_type": "Receivable"
}
},
"Accounts Receivable - Affiliates": {
"account_number": "1310",
"is_group": 1,
"root_type": "Asset",
"Due from Company": {
"account_number": "1311",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable - Others": {
"account_number": "1400",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Others": {
"account_number": "1401",
"is_group": 0,
"root_type": "Asset"
}
},
"Parts, Materials and Supplies": {
"account_number": "1500",
"is_group": 1,
"root_type": "Asset",
"Parts, Materials and Supplies": {
"account_number": "1501",
"is_group": 0,
"root_type": "Asset"
},
"Raw Materials - Demo": {
"account_number": "1502",
"is_group": 0,
"root_type": "Asset"
}
},
"Project in Progress": {
"account_number": "1510",
"is_group": 1,
"root_type": "Asset",
"Project in Progress": {
"account_number": "1511",
"is_group": 0,
"root_type": "Asset"
},
"Factory Overhead Variance": {
"account_number": "1512",
"is_group": 0,
"root_type": "Asset"
}
},
"Finished Goods": {
"account_number": "1520",
"is_group": 1,
"root_type": "Asset",
"Finished Goods Inventory": {
"account_number": "1531",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock"
},
"Inventory in Transit": {
"account_number": "1532",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock Adjustment"
}
},
"Prepayments": {
"account_number": "1600",
"is_group": 1,
"root_type": "Asset",
"Prepaid Insurance & Bonds": {
"account_number": "1601",
"is_group": 0,
"root_type": "Asset"
},
"Prepaid Rent": {
"account_number": "1602",
"is_group": 0,
"root_type": "Asset"
}
},
"VAT Input Tax": {
"account_number": "1610",
"is_group": 1,
"root_type": "Asset",
"VAT Input Tax - Goods": {
"account_number": "1611",
"is_group": 0,
"root_type": "Asset",
"account_type": "Tax"
}
}
},
"Non - Current Assets": {
"account_number": "1002",
"is_group": 1,
"root_type": "Asset",
"Property, Plants And Equipments": {
"account_number": "1700",
"is_group": 1,
"root_type": "Asset",
"Land": {
"account_number": "1701",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Buildings & Improvements": {
"account_number": "1702",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Delivery & Trans Equipment": {
"account_number": "1703",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Furniture & Fixtures": {
"account_number": "1704",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Machinery & Equipment": {
"account_number": "1705",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
}
},
"Accum Depr. - Property, Plants and Equipment": {
"account_number": "1800",
"is_group": 1,
"root_type": "Asset",
"Accumulated Dep Bdgs & Improv": {
"account_number": "1801",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Delivery & Trans": {
"account_number": "1802",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Furniture & Fixture": {
"account_number": "1803",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Depreciation - Machinery & Equipment": {
"account_number": "1804",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
}
}
},
"Other Assets": {
"account_number": "1003",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1900",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1901",
"is_group": 0,
"root_type": "Asset"
}
},
"Miscellaneous Deposits": {
"account_number": "1910",
"is_group": 1,
"root_type": "Asset",
"Miscellaneous Deposits": {
"account_number": "1911",
"is_group": 0,
"root_type": "Asset"
}
},
"Retirement Fund": {
"account_number": "1920",
"is_group": 1,
"root_type": "Asset",
"Retirement Fund": {
"account_number": "1921",
"is_group": 0,
"root_type": "Asset"
}
},
"Investment": {
"account_number": "1930",
"is_group": 1,
"root_type": "Asset",
"Investment": {
"account_number": "1931",
"is_group": 0,
"root_type": "Asset"
}
},
"System Development": {
"account_number": "1940",
"is_group": 1,
"root_type": "Asset",
"System Development": {
"account_number": "1941",
"is_group": 0,
"root_type": "Asset"
}
}
}
},
"Liability": {
"account_number": "2000",
"is_group": 1,
"root_type": "Liability",
"Current Liabilities": {
"account_number": "2001",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable Trade": {
"account_number": "2100",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Trade": {
"account_number": "2101",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
}
},
"Accounts Payable Others": {
"account_number": "2110",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Payroll": {
"account_number": "2111",
"is_group": 0,
"root_type": "Liability"
}
},
"VAT Output Tax": {
"account_number": "2200",
"is_group": 1,
"root_type": "Liability",
"VAT Output Tax": {
"account_number": "2201",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Wages": {
"account_number": "2210",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Wages": {
"account_number": "2211",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Expanded": {
"account_number": "2220",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Expanded": {
"account_number": "2221",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Accruals And Other Current Payables": {
"account_number": "2300",
"is_group": 1,
"root_type": "Liability",
"Stock Received But Not Billed": {
"account_number": "2301",
"is_group": 0,
"root_type": "Liability",
"account_type": "Stock Received But Not Billed"
}
},
"Payable to Government and Other Institutions": {
"account_number": "2400",
"is_group": 1,
"root_type": "Liability",
"SSS Premium Payable": {
"account_number": "2401",
"is_group": 0,
"root_type": "Liability"
},
"SSS Salary Loan Payable": {
"account_number": "2402",
"is_group": 0,
"root_type": "Liability"
},
"PhilHealth Premium": {
"account_number": "2403",
"is_group": 0,
"root_type": "Liability"
},
"Pag-ibig Loan Payable": {
"account_number": "2404",
"is_group": 0,
"root_type": "Liability"
},
"Coop Loans": {
"account_number": "2405",
"is_group": 0,
"root_type": "Liability"
},
"Coop Contributions": {
"account_number": "2406",
"is_group": 0,
"root_type": "Liability"
},
"Canteen": {
"account_number": "2407",
"is_group": 0,
"root_type": "Liability"
},
"AUB Loan Payable": {
"account_number": "2408",
"is_group": 0,
"root_type": "Liability"
},
"HSBC Loan Payable": {
"account_number": "2409",
"is_group": 0,
"root_type": "Liability"
}
},
"Customer Deposits": {
"account_number": "2500",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
}
},
"Non Current Liabilities": {
"account_number": "2002",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2600",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2601",
"is_group": 0,
"root_type": "Liability"
}
},
"Deferred Income": {
"account_number": "2700",
"is_group": 1,
"root_type": "Liability",
"Deferred Income": {
"account_number": "2701",
"is_group": 0,
"root_type": "Liability"
}
},
"Notes Payable": {
"account_number": "2800",
"is_group": 1,
"root_type": "Liability",
"Notes Payable": {
"account_number": "2801",
"is_group": 0,
"root_type": "Liability"
}
},
"Dividends Payable": {
"account_number": "2900",
"is_group": 0,
"root_type": "Liability"
}
}
},
"Equity": {
"account_number": "3000",
"is_group": 1,
"root_type": "Equity",
"STOCKHOLDER'S EQUITY": {
"account_number": "3001",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3100",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3101",
"is_group": 0,
"root_type": "Equity"
}
},
"Subscription Receivable": {
"account_number": "3200",
"is_group": 1,
"root_type": "Equity",
"Subscription Receivable": {
"account_number": "3201",
"is_group": 0,
"root_type": "Equity"
}
},
"Retained Earnings": {
"account_number": "3300",
"is_group": 1,
"root_type": "Equity",
"Retained Earnings": {
"account_number": "3301",
"is_group": 0,
"root_type": "Equity"
}
},
"Current Year (Profit/Loss)": {
"account_number": "3400",
"is_group": 0,
"root_type": "Equity"
},
"Drawings": {
"account_number": "3500",
"is_group": 1,
"root_type": "Equity",
"Drawings": {
"account_number": "3501",
"is_group": 0,
"root_type": "Equity"
}
}
}
},
"Income": {
"account_number": "4000",
"is_group": 1,
"root_type": "Income",
"Gross Sales": {
"account_number": "4100",
"is_group": 1,
"root_type": "Income",
"Sales": {
"account_number": "4101",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Adjustment": {
"account_number": "4200",
"is_group": 1,
"root_type": "Income",
"Sales Return And Allowance": {
"account_number": "4201",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Discount": {
"account_number": "4300",
"is_group": 1,
"root_type": "Income",
"Sales Discount": {
"account_number": "4301",
"is_group": 0,
"root_type": "Income"
}
},
"Other Income": {
"account_number": "6000",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6010",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6011",
"is_group": 0,
"root_type": "Income"
}
},
"Dividend Income": {
"account_number": "6020",
"is_group": 1,
"root_type": "Income",
"Dividend Income": {
"account_number": "6021",
"is_group": 0,
"root_type": "Income"
}
}
}
},
"Expense": {
"account_number": "5000",
"is_group": 1,
"root_type": "Expense",
"Operating Expenses": {
"account_number": "5100",
"is_group": 1,
"root_type": "Expense",
"Salaries, Wages": {
"account_number": "5101",
"is_group": 0,
"root_type": "Expense"
},
"13th Month Pay & Bonus": {
"account_number": "5102",
"is_group": 0,
"root_type": "Expense"
},
"Overtime & Night Diff": {
"account_number": "5103",
"is_group": 0,
"root_type": "Expense"
},
"Incentive/Performance Bonus": {
"account_number": "5104",
"is_group": 0,
"root_type": "Expense"
},
"Employees Benefits": {
"account_number": "5105",
"is_group": 0,
"root_type": "Expense"
},
"Advertising & Promotions": {
"account_number": "5106",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Leasehold Improvement": {
"account_number": "5107",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Pre-Operating": {
"account_number": "5108",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of System Development": {
"account_number": "5109",
"is_group": 0,
"root_type": "Expense"
},
"Audit & Legal Fee": {
"account_number": "5110",
"is_group": 0,
"root_type": "Expense"
},
"Bad Debts Expenses": {
"account_number": "5111",
"is_group": 0,
"root_type": "Expense"
},
"Client Service & Maintenance": {
"account_number": "5112",
"is_group": 0,
"root_type": "Expense"
},
"Commission Expenses": {
"account_number": "5113",
"is_group": 0,
"root_type": "Expense"
},
"Communications": {
"account_number": "5114",
"is_group": 0,
"root_type": "Expense"
},
"Contractual Services": {
"account_number": "5115",
"is_group": 0,
"root_type": "Expense"
},
"Depreciation Expenses": {
"account_number": "5116",
"is_group": 0,
"root_type": "Expense",
"account_type": "Depreciation"
},
"Donation & Contribution": {
"account_number": "5117",
"is_group": 0,
"root_type": "Expense"
},
"Dues & Subscription": {
"account_number": "5118",
"is_group": 0,
"root_type": "Expense"
},
"Employee Med/Dental/Hosp Expenses": {
"account_number": "5119",
"is_group": 0,
"root_type": "Expense"
},
"Employee Uniforms": {
"account_number": "5120",
"is_group": 0,
"root_type": "Expense"
},
"Equipage": {
"account_number": "5121",
"is_group": 0,
"root_type": "Expense"
},
"Expenses for Reclassification": {
"account_number": "5122",
"is_group": 0,
"root_type": "Expense"
},
"Gas & Oil": {
"account_number": "5123",
"is_group": 0,
"root_type": "Expense"
},
"Insurance Expenses": {
"account_number": "5124",
"is_group": 0,
"root_type": "Expense"
},
"Light & Water": {
"account_number": "5125",
"is_group": 0,
"root_type": "Expense"
},
"Local/Overseas Travel": {
"account_number": "5126",
"is_group": 0,
"root_type": "Expense"
},
"Meals & Transportation Expenses": {
"account_number": "5127",
"is_group": 0,
"root_type": "Expense"
},
"Meeting & Conferences": {
"account_number": "5128",
"is_group": 0,
"root_type": "Expense"
},
"Miscellaneous Expenses": {
"account_number": "5129",
"is_group": 0,
"root_type": "Expense"
},
"Mockup Expenses": {
"account_number": "5130",
"is_group": 0,
"root_type": "Expense"
},
"Obsolescence Expenses": {
"account_number": "5131",
"is_group": 0,
"root_type": "Expense"
},
"Other Support Cost": {
"account_number": "5132",
"is_group": 0,
"root_type": "Expense"
},
"Pag-ibig Contribution": {
"account_number": "5133",
"is_group": 0,
"root_type": "Expense"
},
"Performance Bonds": {
"account_number": "5134",
"is_group": 0,
"root_type": "Expense"
},
"Pre Employment Expenses": {
"account_number": "5135",
"is_group": 0,
"root_type": "Expense"
},
"Professional Fees": {
"account_number": "5136",
"is_group": 0,
"root_type": "Expense"
},
"Recruitment & Employment": {
"account_number": "5137",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses": {
"account_number": "5138",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses Others": {
"account_number": "5139",
"is_group": 0,
"root_type": "Expense"
},
"Repairs & Maintenance": {
"account_number": "5140",
"is_group": 0,
"root_type": "Expense"
},
"Representation Expenses": {
"account_number": "5141",
"is_group": 0,
"root_type": "Expense"
},
"Research & Development": {
"account_number": "5142",
"is_group": 0,
"root_type": "Expense"
},
"Security Expenses": {
"account_number": "5143",
"is_group": 0,
"root_type": "Expense"
},
"Shared Services Fee": {
"account_number": "5144",
"is_group": 0,
"root_type": "Expense"
},
"SSS/Medicare/EC Contributions": {
"account_number": "5145",
"is_group": 0,
"root_type": "Expense"
},
"Stationery & Supplies": {
"account_number": "5146",
"is_group": 0,
"root_type": "Expense"
},
"Taxes & Licenses": {
"account_number": "5147",
"is_group": 0,
"root_type": "Expense",
"account_type": "Tax"
},
"Training & Seminar": {
"account_number": "5148",
"is_group": 0,
"root_type": "Expense"
}
},
"Stock Adjustment": {
"account_number": "5200",
"is_group": 0,
"root_type": "Expense",
"account_type": "Stock Adjustment"
},
"Round Off": {
"account_number": "5300",
"is_group": 0,
"root_type": "Expense",
"account_type": "Round Off"
},
"Expenses Included In Valuation": {
"account_number": "5400",
"is_group": 0,
"root_type": "Expense",
"account_type": "Expenses Included In Valuation"
}
}
}
}

View File

@@ -43,6 +43,7 @@ class AccountingDimension(Document):
def validate(self):
self.validate_doctype()
validate_column_name(self.fieldname)
self.validate_fieldname_conflict()
self.validate_dimension_defaults()
def validate_doctype(self):
@@ -74,6 +75,27 @@ class AccountingDimension(Document):
message += _("Please create a new Accounting Dimension if required.")
frappe.throw(message)
def validate_fieldname_conflict(self):
conflicting_doctypes = []
for doctype in get_doctypes_with_dimensions():
meta = frappe.get_meta(doctype, cached=False)
if any(f.fieldname == self.fieldname for f in meta.get("fields")):
conflicting_doctypes.append(doctype)
if conflicting_doctypes:
frappe.msgprint(
_(
"Fieldname {0} already exists in the following doctypes: {1}. "
"A separate dimension field will not be added to these doctypes. "
"GL Entries will use the value of the existing field as the dimension value."
).format(
frappe.bold(self.fieldname),
", ".join(frappe.bold(d) for d in conflicting_doctypes),
),
title=_("Fieldname Conflict"),
indicator="orange",
)
def validate_dimension_defaults(self):
companies = []
for default in self.get("dimension_defaults"):

View File

@@ -38,16 +38,6 @@ frappe.ui.form.on("Accounts Settings", {
add_taxes_from_item_tax_template(frm) {
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
},
drop_ar_procedures: function (frm) {
frm.call({
doc: frm.doc,
method: "drop_ar_sql_procedures",
callback: function (r) {
frappe.show_alert(__("Procedures dropped"), 5);
},
});
},
});
function toggle_tax_settings(frm, field_name) {

View File

@@ -96,7 +96,6 @@
"receivable_payable_fetch_method",
"default_ageing_range",
"column_break_ntmi",
"drop_ar_procedures",
"legacy_section",
"ignore_is_opening_check_for_reporting",
"tab_break_dpet",
@@ -523,7 +522,7 @@
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
"options": "Buffered Cursor\nUnBuffered Cursor"
},
{
"fieldname": "accounts_receivable_payable_tuning_section",
@@ -592,13 +591,6 @@
"fieldname": "column_break_ntmi",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
"fieldname": "drop_ar_procedures",
"fieldtype": "Button",
"label": "Drop Procedures"
},
{
"default": "0",
"fieldname": "fetch_valuation_rate_for_internal_transaction",
@@ -725,7 +717,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-04-13 15:30:28.729627",
"modified": "2026-05-18 12:16:33.679345",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -89,7 +89,7 @@ class AccountsSettings(Document):
make_payment_via_journal_entry: DF.Check
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
repost_allowed_types: DF.Table[RepostAllowedTypes]
@@ -209,13 +209,6 @@ class AccountsSettings(Document):
set_allow_on_submit_for_dimension_fields(doctypes)
@frappe.whitelist()
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
def toggle_accounting_dimension_sections(hide):
accounting_dimension_doctypes = frappe.get_hooks("accounting_dimension_doctypes")

View File

@@ -70,6 +70,10 @@ frappe.ui.form.on("Journal Entry", {
},
refresh: function (frm) {
if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) {
frm.set_read_only();
}
erpnext.toggle_naming_series();
if (frm.doc.docstatus > 0) {

View File

@@ -710,31 +710,12 @@ frappe.ui.form.on("Payment Entry", {
if (!frm.doc.paid_from_account_currency || !frm.doc.company) return;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from) {
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency: frm.doc.paid_from_account_currency,
to_currency: company_currency,
transaction_date: frm.doc.posting_date,
},
callback: function (r, rt) {
frm.set_value("source_exchange_rate", r.message);
},
});
} else {
frm.events.set_current_exchange_rate(
frm,
"source_exchange_rate",
frm.doc.paid_from_account_currency,
company_currency
);
}
}
frm.events.set_current_exchange_rate(
frm,
"source_exchange_rate",
frm.doc.paid_from_account_currency,
company_currency
);
},
paid_to_account_currency: function (frm) {
@@ -766,49 +747,24 @@ frappe.ui.form.on("Payment Entry", {
posting_date: function (frm) {
frm.events.paid_from_account_currency(frm);
frm.events.paid_to_account_currency(frm);
},
source_exchange_rate: function (frm) {
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_amount) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
// 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("base_received_amount", frm.doc.base_paid_amount);
} else if (company_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.base_paid_amount);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
}
// set_unallocated_amount is called by below method,
// no need trigger separately
frm.events.set_total_allocated_amount(frm);
}
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
target_exchange_rate: function (frm) {
frm.set_paid_amount_based_on_received_amount = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.received_amount) {
frm.set_value(
"base_received_amount",
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
);
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.source_exchange_rate &&
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("base_paid_amount", frm.doc.base_received_amount);
} else if (company_currency == frm.doc.paid_from_account_currency) {
frm.set_value("paid_amount", frm.doc.base_received_amount);
frm.set_value("base_paid_amount", frm.doc.base_received_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);
} else {
frm.set_value(
"paid_amount",
flt(frm.doc.base_paid_amount) / flt(frm.doc.source_exchange_rate)
);
}
// set_unallocated_amount is called by below method,
@@ -817,6 +773,32 @@ frappe.ui.form.on("Payment Entry", {
}
frm.set_paid_amount_based_on_received_amount = false;
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
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
) {
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
} else {
frm.set_value(
"received_amount",
flt(frm.doc.base_received_amount) / flt(frm.doc.target_exchange_rate)
);
}
// set_unallocated_amount is called by below method,
// no need trigger separately
frm.events.set_total_allocated_amount(frm);
}
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("target_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
@@ -825,11 +807,14 @@ frappe.ui.form.on("Payment Entry", {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (!frm.doc.received_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.paid_amount);
} else if (company_currency == frm.doc.paid_to_account_currency) {
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
if (company_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.base_paid_amount);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
} else if (frm.doc.target_exchange_rate) {
frm.set_value(
"received_amount",
flt(frm.doc.base_paid_amount) / flt(frm.doc.target_exchange_rate)
);
}
}
frm.trigger("reset_received_amount");
@@ -846,15 +831,14 @@ frappe.ui.form.on("Payment Entry", {
);
if (!frm.doc.paid_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("paid_amount", frm.doc.received_amount);
if (frm.doc.target_exchange_rate) {
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
}
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
} else if (company_currency == frm.doc.paid_from_account_currency) {
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
if (company_currency == frm.doc.paid_from_account_currency) {
frm.set_value("paid_amount", frm.doc.base_received_amount);
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
} else if (frm.doc.source_exchange_rate) {
frm.set_value(
"paid_amount",
flt(frm.doc.base_received_amount) / flt(frm.doc.source_exchange_rate)
);
}
}

View File

@@ -322,7 +322,7 @@
"reqd": 1
},
{
"depends_on": "doc.received_amount",
"depends_on": "eval:doc.received_amount;",
"fieldname": "base_received_amount",
"fieldtype": "Currency",
"label": "Received Amount (Company Currency)",
@@ -795,7 +795,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2026-03-09 17:15:30.453920",
"modified": "2026-05-15 13:31:01.166010",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -1238,9 +1238,9 @@ class PaymentEntry(AccountsController):
else:
remarks = [
_("Amount {0} {1} {2} {3}").format(
_(self.paid_to_account_currency)
_(self.paid_from_account_currency)
if self.payment_type == "Receive"
else _(self.paid_from_account_currency),
else _(self.paid_to_account_currency),
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
_("received from") if self.payment_type == "Receive" else _("paid to"),
self.party,
@@ -1256,7 +1256,7 @@ class PaymentEntry(AccountsController):
for d in self.get("references"):
if d.allocated_amount:
remarks.append(
_("Amount {0} {1} against {2} {3}").format(
_("Amount {0} {1} adjusted against {2} {3}").format(
_(self.party_account_currency),
d.allocated_amount,
d.reference_doctype,
@@ -1267,7 +1267,7 @@ class PaymentEntry(AccountsController):
for d in self.get("deductions"):
if d.amount:
remarks.append(
_("Amount {0} {1} deducted against {2}").format(
_("Amount {0} {1} as adjustment to {2}").format(
_(self.company_currency), d.amount, d.account
)
)
@@ -2328,16 +2328,19 @@ def get_outstanding_reference_documents(args, validate=False):
}
for fieldname, date_fields in date_fields_dict.items():
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
if args.get(date_fields[0]) and args.get(date_fields[1]):
condition += f" and {fieldname} between {frappe.db.escape(args.get(date_fields[0]))} and {frappe.db.escape(args.get(date_fields[1]))}"
condition += f" and {fieldname} between {from_date} and {to_date}"
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += f" and {fieldname} >= {frappe.db.escape(args.get(date_fields[0]))}"
condition += f" and {fieldname} >= {from_date}"
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += f" and {fieldname} <= {frappe.db.escape(args.get(date_fields[1]))}"
condition += f" and {fieldname} <= {to_date}"
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):

View File

@@ -3,7 +3,8 @@
import frappe
from frappe import qb
from frappe.utils import nowdate
from frappe.query_builder.functions import Count, Sum
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
@@ -90,6 +91,7 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
posting_date = nowdate()
sinv = create_sales_invoice(
posting_date=posting_date,
qty=qty,
rate=rate,
company=self.company,
@@ -531,3 +533,82 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
# with references removed, deletion should be possible
so.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"enable_immutable_ledger": 1},
)
def test_reverse_entries_on_cancel_for_immutable_ledger(self):
invoice_posting_date = add_days(nowdate(), -5)
gle = qb.DocType("GL Entry")
ple = qb.DocType("Payment Ledger Entry")
si = self.create_sales_invoice(qty=1, rate=100, posting_date=invoice_posting_date)
gles_before = (
qb.from_(gle)
.select(
Count(gle.name),
)
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
.run()[0][0]
)
ples_before = (
qb.from_(ple)
.select(
Count(ple.name),
)
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
.run()[0][0]
)
si.cancel()
gles_after = (
qb.from_(gle)
.select(Count(gle.account))
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
.run()[0][0]
)
self.assertEqual(gles_after, gles_before * 2)
ples_after = (
qb.from_(ple)
.select(
Count(ple.name),
)
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
.run()[0][0]
)
self.assertEqual(ples_after, ples_before * 2)
# assert debit/credit are reversed
gl_entries = (
qb.from_(gle)
.select(gle.account, Sum(gle.debit).as_("total_debit"), Sum(gle.credit).as_("total_credit"))
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
.groupby(gle.account)
.run(as_dict=True)
)
for gl in gl_entries:
with self.subTest(gl=gl):
self.assertEqual(gl.total_debit, gl.total_credit)
# assert amounts are reversed
pl_entries = (
qb.from_(ple)
.select(ple.account, Sum(ple.amount).as_("total_amount"))
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked == 0))
.groupby(ple.account)
.run(as_dict=True)
)
for pl in pl_entries:
with self.subTest(pl=pl):
self.assertEqual(pl.total_amount, 0)
self.assertFalse(
frappe.db.exists(
"Payment Ledger Entry",
{"voucher_type": si.doctype, "voucher_no": si.name, "delinked": 1},
)
)

View File

@@ -21,122 +21,23 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestPaymentReconciliation(ERPNextTestSuite):
def setUp(self):
self.create_company()
self.create_item()
self.create_customer()
self.create_account()
self.create_cost_center()
self.clear_old_entries()
def create_company(self):
company = None
if frappe.db.exists("Company", "_Test Payment Reconciliation"):
company = frappe.get_doc("Company", "_Test Payment Reconciliation")
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": "_Test Payment Reconciliation",
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses - _PR"
self.income_account = "Sales - _PR"
self.expense_account = "Cost of Goods Sold - _PR"
self.debit_to = "Debtors - _PR"
self.creditors = "Creditors - _PR"
self.cash = "Cash - _PR"
# create bank account
if frappe.db.exists("Account", "HDFC - _PR"):
self.bank = "HDFC - _PR"
else:
bank_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": "HDFC",
"parent_account": "Bank Accounts - _PR",
"company": self.company,
}
)
bank_acc.save()
self.bank = bank_acc.name
def create_item(self):
item = create_item(
item_code="_Test PR Item", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
self.customer = make_customer("_Test PR Customer")
self.customer2 = make_customer("_Test PR Customer 2")
self.customer3 = make_customer("_Test PR Customer 3", "EUR")
self.customer4 = make_customer("_Test PR Customer 4", "EUR")
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
def create_account(self):
accounts = [
{
"attribute": "debtors_eur",
"account_name": "Debtors EUR",
"parent_account": "Accounts Receivable - _PR",
"account_currency": "EUR",
"account_type": "Receivable",
},
{
"attribute": "creditors_usd",
"account_name": "Payable USD",
"parent_account": "Accounts Payable - _PR",
"account_currency": "USD",
"account_type": "Payable",
},
# 'Payable' account for capturing advance paid, under 'Assets' group
{
"attribute": "advance_payable_account",
"account_name": "Advance Paid",
"parent_account": "Current Assets - _PR",
"account_currency": "INR",
"account_type": "Payable",
},
# 'Receivable' account for capturing advance received, under 'Liabilities' group
{
"attribute": "advance_receivable_account",
"account_name": "Advance Received",
"parent_account": "Current Liabilities - _PR",
"account_currency": "INR",
"account_type": "Receivable",
},
]
for x in accounts:
x = frappe._dict(x)
if not frappe.db.get_value(
"Account", filters={"account_name": x.account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = x.account_name
acc.parent_account = x.parent_account
acc.company = self.company
acc.account_currency = x.account_currency
acc.account_type = x.account_type
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": x.account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
setattr(self, x.attribute, acc.name)
self.company = "_Test Company"
self.debit_to = "Debtors - _TC"
self.creditors = "Creditors - _TC"
self.bank = "HDFC - _TC"
self.cash = "Cash - _TC"
self.item = "_Test Item"
self.cost_center = self.main_cc = "Main - _TC"
self.sub_cc = "Sub - _TC"
self.customer = "_Test Customer"
self.advance_receivable_account = "Advance Received - _TC"
self.advance_payable_account = "Advance Paid - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.warehouse = "All Warehouses - _TC"
self.customer_usd = "_Test Customer USD"
self.debtors_usd = "_Test Receivable USD - _TC"
self.creditors_usd = "_Test Payable USD - _TC"
def create_sales_invoice(
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
@@ -253,18 +154,6 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
return pord
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_payment_reconciliation(self, party_is_customer=True):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
@@ -300,22 +189,6 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
return je
def create_cost_center(self):
# Setup cost center
cc_name = "Sub"
self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company))
cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name})
if cc_exists:
self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name)
else:
sub_cc = frappe.new_doc("Cost Center")
sub_cc.cost_center_name = "Sub"
sub_cc.parent_cost_center = self.main_cc.parent_cost_center
sub_cc.company = self.main_cc.company
self.sub_cc = sub_cc.save()
def test_filter_min_max(self):
# check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300)
@@ -478,7 +351,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_payment_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
sales = "Sales - _TC"
amount = 921
# debit debtors account to record an invoice
je = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
@@ -513,7 +386,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
transaction_date = nowdate()
self.supplier = "_Test Supplier USD"
self.supplier2 = make_supplier("_Test Supplier2 USD", "USD")
self.supplier2 = "_Test Another Supplier USD"
amount = 100
exc_rate1 = 80
exc_rate2 = 83
@@ -666,7 +539,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_journal_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
sales = "Sales - _TC"
amount = 100
# debit debtors account to simulate a invoice
@@ -841,47 +714,49 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_pr_output_foreign_currency_and_amount(self):
# test for currency and amount invoices and payments
transaction_date = nowdate()
# In EUR
# In USD
amount = 100
exchange_rate = 80
si = self.create_sales_invoice(
qty=1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
si.customer = self.customer3
si.currency = "EUR"
si.customer = self.customer_usd
si.currency = "USD"
si.conversion_rate = exchange_rate
si.debit_to = self.debtors_eur
si.debit_to = self.debtors_usd
si = si.save().submit()
cr_note = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note.customer = self.customer3
cr_note.customer = self.customer_usd
cr_note.is_return = 1
cr_note.currency = "EUR"
cr_note.currency = "USD"
cr_note.conversion_rate = exchange_rate
cr_note.debit_to = self.debtors_eur
cr_note.debit_to = self.debtors_usd
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer3
pr.receivable_payable_account = self.debtors_eur
pr.party = self.customer_usd
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.invoices[0].amount, amount)
self.assertEqual(pr.invoices[0].currency, "EUR")
self.assertEqual(pr.invoices[0].currency, "USD")
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")
self.assertEqual(pr.payments[0].currency, "USD")
cr_note.cancel()
pay = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=self.customer3)
pay.paid_from = self.debtors_eur
pay.paid_from_account_currency = "EUR"
pay = self.create_payment_entry(
amount=amount, posting_date=transaction_date, customer=self.customer_usd
)
pay.paid_from = self.debtors_usd
pay.paid_from_account_currency = "USD"
pay.source_exchange_rate = exchange_rate
pay.received_amount = exchange_rate * amount
pay = pay.save().submit()
@@ -890,21 +765,21 @@ class TestPaymentReconciliation(ERPNextTestSuite):
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")
self.assertEqual(pr.payments[0].currency, "USD")
def test_difference_amount_via_journal_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.customer = self.customer_usd
si.currency = "USD"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.debit_to = self.debtors_usd
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1 = self.create_journal_entry("HDFC - _TC", self.debtors_usd, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = 0
@@ -912,7 +787,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.accounts[0].debit_in_account_currency = 8000
je1.accounts[0].debit = 8000
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].party = self.customer_usd
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
@@ -921,7 +796,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2 = self.create_journal_entry("HDFC - _TC", self.debtors_usd, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = 0
@@ -929,7 +804,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je2.accounts[0].debit_in_account_currency = 16000
je2.accounts[0].debit = 16000
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].party = self.customer_usd
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
@@ -939,8 +814,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.party = self.customer_usd
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
@@ -960,7 +835,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
pr.allocation[0].difference_account = "Exchange Gain/Loss - _TC"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
@@ -969,7 +844,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pr.reconcile()
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
{"account": self.debtors_usd, "docstatus": 1, "reference_name": si.name},
[{"SUM": "credit", "as": "amount"}],
group_by="reference_name",
)[0].amount
@@ -979,7 +854,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
jea_parent = frappe.db.get_all(
"Journal Entry Account",
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
filters={"account": self.debtors_usd, "docstatus": 1, "reference_name": si.name, "credit": 500},
fields=["parent"],
)[0]
self.assertEqual(
@@ -991,14 +866,14 @@ class TestPaymentReconciliation(ERPNextTestSuite):
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.customer = self.customer_usd
si.currency = "USD"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.debit_to = self.debtors_usd
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1 = self.create_journal_entry("HDFC - _TC", self.debtors_usd, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = -8000
@@ -1006,7 +881,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.accounts[0].debit_in_account_currency = 0
je1.accounts[0].debit = 0
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].party = self.customer_usd
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
@@ -1015,7 +890,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2 = self.create_journal_entry("HDFC - _TC", self.debtors_usd, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = -16000
@@ -1023,7 +898,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je2.accounts[0].debit_in_account_currency = 0
je2.accounts[0].debit = 0
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].party = self.customer_usd
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
@@ -1033,8 +908,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.party = self.customer_usd
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
@@ -1054,7 +929,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
pr.allocation[0].difference_account = "Exchange Gain/Loss - _TC"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
@@ -1063,7 +938,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pr.reconcile()
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
{"account": self.debtors_usd, "docstatus": 1, "reference_name": si.name},
[{"SUM": "credit", "as": "amount"}],
group_by="reference_name",
)[0].amount
@@ -1073,7 +948,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
jea_parent = frappe.db.get_all(
"Journal Entry Account",
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
filters={"account": self.debtors_usd, "docstatus": 1, "reference_name": si.name, "credit": 500},
fields=["parent"],
)[0]
self.assertEqual(
@@ -1085,10 +960,10 @@ class TestPaymentReconciliation(ERPNextTestSuite):
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer5
si.currency = "EUR"
si.customer = self.customer_usd
si.currency = "USD"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.debit_to = self.debtors_usd
si.save().submit()
# Make payment using Payment Entry
@@ -1096,8 +971,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
party=self.customer_usd,
paid_from=self.debtors_usd,
paid_to=self.bank,
paid_amount=100,
)
@@ -1111,8 +986,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
party=self.customer_usd,
paid_from=self.debtors_usd,
paid_to=self.bank,
paid_amount=200,
)
@@ -1123,8 +998,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pe2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer5
pr.receivable_payable_account = self.debtors_eur
pr.party = self.customer_usd
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
@@ -1152,14 +1027,14 @@ class TestPaymentReconciliation(ERPNextTestSuite):
"""
si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True)
si.cost_center = self.main_cc.name
si.cost_center = self.main_cc
si.submit()
pr = get_payment_entry(si.doctype, si.name)
pr.cost_center = self.sub_cc.name
pr.cost_center = self.sub_cc
pr = pr.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.cost_center = self.main_cc
pr.get_unreconciled_entries()
@@ -1176,38 +1051,38 @@ class TestPaymentReconciliation(ERPNextTestSuite):
# 'Main - PR' Cost Center
si1 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True)
si1.cost_center = self.main_cc.name
si1.cost_center = self.main_cc
si1.submit()
pe1 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
pe1.cost_center = self.main_cc.name
pe1.cost_center = self.main_cc
pe1 = pe1.save().submit()
je1 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
je1.accounts[0].cost_center = self.main_cc.name
je1.accounts[1].cost_center = self.main_cc.name
je1.accounts[0].cost_center = self.main_cc
je1.accounts[1].cost_center = self.main_cc
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer
je1 = je1.save().submit()
# 'Sub - PR' Cost Center
si2 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True)
si2.cost_center = self.sub_cc.name
si2.cost_center = self.sub_cc
si2.submit()
pe2 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
pe2.cost_center = self.sub_cc.name
pe2.cost_center = self.sub_cc
pe2 = pe2.save().submit()
je2 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
je2.accounts[0].cost_center = self.sub_cc.name
je2.accounts[1].cost_center = self.sub_cc.name
je2.accounts[0].cost_center = self.sub_cc
je2.accounts[1].cost_center = self.sub_cc
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer
je2 = je2.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.cost_center = self.main_cc
pr.get_unreconciled_entries()
@@ -1219,7 +1094,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
self.assertCountEqual(payment_vouchers, [pe1.name, je1.name])
# Change cost center
pr.cost_center = self.sub_cc.name
pr.cost_center = self.sub_cc
pr.get_unreconciled_entries()
@@ -1242,7 +1117,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer
si.currency = "EUR"
si.currency = "USD"
si.conversion_rate = 85
si.debit_to = self.debit_to
si.save().submit()
@@ -1624,7 +1499,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
"credit": 0.0,
},
{
"account": "Cash - _PR",
"account": "Cash - _TC",
"voucher_no": pe.name,
"against_voucher": None,
"debit": 0.0,
@@ -1782,10 +1657,10 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
amount = 200.0
je = self.create_journal_entry(self.debit_to, self.bank, amount)
je.accounts[0].cost_center = self.main_cc.name
je.accounts[0].cost_center = self.main_cc
je.accounts[0].party_type = "Customer"
je.accounts[0].party = self.customer
je.accounts[1].cost_center = self.main_cc.name
je.accounts[1].cost_center = self.main_cc
je = je.save().submit()
pe = self.create_payment_entry(amount=amount).save().submit()
@@ -1879,7 +1754,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
self.assertEqual(pl_entries, expected_ple)
def test_advance_payment_reconciliation_against_journal_for_supplier(self):
self.supplier = make_supplier("_Test Supplier")
self.supplier = "_Test Supplier"
frappe.db.set_value(
"Company",
self.company,
@@ -1891,10 +1766,10 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
amount = 200.0
je = self.create_journal_entry(self.creditors, self.bank, -amount)
je.accounts[0].cost_center = self.main_cc.name
je.accounts[0].cost_center = self.main_cc
je.accounts[0].party_type = "Supplier"
je.accounts[0].party = self.supplier
je.accounts[1].cost_center = self.main_cc.name
je.accounts[1].cost_center = self.main_cc
je = je.save().submit()
pe = self.create_payment_entry(amount=amount)
@@ -2062,13 +1937,13 @@ class TestPaymentReconciliation(ERPNextTestSuite):
},
{
"account": self.bank,
"cost_center": self.sub_cc.name,
"cost_center": self.sub_cc,
"credit_in_account_currency": 0,
"debit_in_account_currency": 500,
},
{
"account": self.cash,
"cost_center": self.sub_cc.name,
"cost_center": self.sub_cc,
"credit_in_account_currency": 0,
"debit_in_account_currency": 500,
},
@@ -2337,7 +2212,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_customer(self):
transaction_date = nowdate()
customer = self.customer3
customer = self.customer_usd
amount = 1000
exchange_rate_at_payment = 100
exchange_rate_at_reverse_payment = 95
@@ -2345,8 +2220,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
# Receive amount from customer - 1,00,000
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=customer)
pe.payment_type = "Receive"
pe.paid_from = self.debtors_eur
pe.paid_from_account_currency = "EUR"
pe.paid_from = self.debtors_usd
pe.paid_from_account_currency = "USD"
pe.source_exchange_rate = exchange_rate_at_payment
pe.paid_amount = amount
pe.received_amount = exchange_rate_at_payment * amount
@@ -2364,14 +2239,14 @@ class TestPaymentReconciliation(ERPNextTestSuite):
reverse_pe.target_exchange_rate = exchange_rate_at_reverse_payment
reverse_pe.paid_amount = exchange_rate_at_reverse_payment * amount
reverse_pe.received_amount = amount
reverse_pe.paid_to = self.debtors_eur
reverse_pe.paid_to_account_currency = "EUR"
reverse_pe.paid_to = self.debtors_usd
reverse_pe.paid_to_account_currency = "USD"
reverse_pe.save().submit()
# Reconcile payments
pr = self.create_payment_reconciliation()
pr.party = customer
pr.receivable_payable_account = self.debtors_eur
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
invoices = [invoice.as_dict() for invoice in pr.invoices]
payments = [payment.as_dict() for payment in pr.payments]
@@ -2436,13 +2311,13 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_customer(self):
transaction_date = nowdate()
customer = self.customer3
customer = self.customer_usd
amount = 1000
exchange_rate_at_payment = 95
exchange_rate_at_reverse_payment = 100
# Receive amount from customer - 95,000
je1 = self.create_journal_entry(self.cash, self.debtors_eur, amount, transaction_date)
je1 = self.create_journal_entry(self.cash, self.debtors_usd, amount, transaction_date)
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].debit_in_account_currency = exchange_rate_at_payment * amount
@@ -2456,7 +2331,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.submit()
# Pay amount to customer - 1,00,000
je2 = self.create_journal_entry(self.debtors_eur, self.cash, amount, transaction_date)
je2 = self.create_journal_entry(self.debtors_usd, self.cash, amount, transaction_date)
je2.multi_currency = 1
je2.accounts[0].party_type = "Customer"
je2.accounts[0].party = customer
@@ -2472,7 +2347,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
# Reconcile payments
pr = self.create_payment_reconciliation()
pr.party = customer
pr.receivable_payable_account = self.debtors_eur
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
@@ -2540,34 +2415,6 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pr.reconcile()
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.type = "Individual"
if currency:
customer.default_currency = currency
customer.save()
return customer.name
else:
return customer_name
def make_supplier(supplier_name, currency=None):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
supplier.type = "Individual"
if currency:
supplier.default_currency = currency
supplier.save()
return supplier.name
else:
return supplier_name
def create_fiscal_year(company, year_start_date, year_end_date):
fy_docname = frappe.db.exists(
"Fiscal Year", {"year_start_date": year_start_date, "year_end_date": year_end_date}

View File

@@ -8,6 +8,7 @@ from frappe.utils import today
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import make_reverse_gl_entries
from erpnext.accounts.utils import get_fiscal_year
from erpnext.tests.utils import ERPNextTestSuite
@@ -334,6 +335,48 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
return pcv
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"enable_immutable_ledger": 1},
)
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company=company,
save=False,
)
jv.company = company
jv.save()
jv.submit()
self.make_period_closing_voucher(posting_date="2021-03-31")
# Passed posting_date is after PCV end date, so cancellation should not fail.
make_reverse_gl_entries(
voucher_type="Journal Entry",
voucher_no=jv.name,
posting_date="2022-01-01",
)
totals_after_cancel = frappe.db.sql(
"""
select sum(debit) as total_debit, sum(credit) as total_credit
from `tabGL Entry`
where voucher_type=%s and voucher_no=%s and is_cancelled=0
""",
("Journal Entry", jv.name),
as_dict=True,
)[0]
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
def create_company():
company = frappe.get_doc(

View File

@@ -664,6 +664,7 @@
"fieldname": "total_billing_amount",
"fieldtype": "Currency",
"label": "Total Billing Amount",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
@@ -1531,6 +1532,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -1639,7 +1641,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-04-28 06:06:14.283612",
"modified": "2026-05-01 02:37:30.580568",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -13,52 +13,69 @@
"column_break_9",
"warehouse",
"company_address",
"section_break_15",
"applicable_for_users",
"accounting_tab",
"section_break_11",
"payments",
"set_grand_total_to_default_mop",
"price_list_and_currency_section",
"currency",
"column_break_bptt",
"selling_price_list",
"write_off_section",
"write_off_account",
"column_break_ukpz",
"write_off_cost_center",
"column_break_pkca",
"write_off_limit",
"income_and_expense_account",
"income_account",
"column_break_byzk",
"expense_account",
"taxes_section",
"taxes_and_charges",
"column_break_cjpp",
"tax_category",
"section_break_19",
"account_for_change_amount",
"disable_rounded_total",
"column_break_23",
"apply_discount_on",
"allow_partial_payment",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"pos_configurations_tab",
"section_break_14",
"hide_images",
"hide_unavailable_items",
"auto_add_item_to_cart",
"validate_stock_on_save",
"print_receipt_on_order_complete",
"action_on_new_invoice",
"validate_stock_on_save",
"column_break_16",
"update_stock",
"ignore_pricing_rule",
"print_receipt_on_order_complete",
"pos_item_selector_section",
"hide_images",
"column_break_rpny",
"hide_unavailable_items",
"column_break_stcl",
"auto_add_item_to_cart",
"pos_item_details_section",
"allow_rate_change",
"column_break_hwfg",
"allow_discount_change",
"set_grand_total_to_default_mop",
"allow_partial_payment",
"section_break_15",
"applicable_for_users",
"section_break_23",
"item_groups",
"column_break_25",
"customer_groups",
"more_info_tab",
"section_break_16",
"print_format",
"letter_head",
"column_break0",
"tc_name",
"select_print_heading",
"section_break_19",
"selling_price_list",
"currency",
"write_off_account",
"write_off_cost_center",
"write_off_limit",
"account_for_change_amount",
"disable_rounded_total",
"column_break_23",
"income_account",
"expense_account",
"taxes_and_charges",
"tax_category",
"apply_discount_on",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"utm_analytics_section",
"utm_source",
"column_break_tvls",
@@ -133,8 +150,7 @@
},
{
"fieldname": "section_break_14",
"fieldtype": "Section Break",
"label": "Configuration"
"fieldtype": "Section Break"
},
{
"description": "Only show Items from these Item Groups",
@@ -155,6 +171,7 @@
"options": "POS Customer Group"
},
{
"collapsible": 1,
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Print Settings"
@@ -194,7 +211,7 @@
{
"fieldname": "section_break_19",
"fieldtype": "Section Break",
"label": "Accounting"
"label": "Miscellaneous"
},
{
"fieldname": "selling_price_list",
@@ -430,6 +447,7 @@
},
{
"default": "0",
"description": "Applicable on POS Invoice",
"fieldname": "allow_partial_payment",
"fieldtype": "Check",
"label": "Allow Partial Payment"
@@ -447,6 +465,83 @@
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "Campaign"
},
{
"fieldname": "accounting_tab",
"fieldtype": "Tab Break",
"label": "Accounting"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "pos_configurations_tab",
"fieldtype": "Tab Break",
"label": "POS Configurations"
},
{
"fieldname": "price_list_and_currency_section",
"fieldtype": "Section Break",
"label": "Price List & Currency"
},
{
"fieldname": "column_break_bptt",
"fieldtype": "Column Break"
},
{
"fieldname": "write_off_section",
"fieldtype": "Section Break",
"label": "Write Off"
},
{
"fieldname": "column_break_ukpz",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_pkca",
"fieldtype": "Column Break"
},
{
"fieldname": "income_and_expense_account",
"fieldtype": "Section Break",
"label": "Income and Expense"
},
{
"fieldname": "column_break_byzk",
"fieldtype": "Column Break"
},
{
"fieldname": "taxes_section",
"fieldtype": "Section Break",
"label": "Taxes"
},
{
"fieldname": "column_break_cjpp",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_item_selector_section",
"fieldtype": "Section Break",
"label": "POS Item Selector"
},
{
"fieldname": "column_break_rpny",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_stcl",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_item_details_section",
"fieldtype": "Section Break",
"label": "POS Item Details"
},
{
"fieldname": "column_break_hwfg",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
@@ -475,7 +570,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2026-02-10 14:24:48.597412",
"modified": "2026-05-26 12:07:48.597412",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@@ -72,8 +72,8 @@ class ProcessPeriodClosingVoucher(Document):
pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv)
if pcv.is_first_period_closing_voucher():
gl = qb.DocType("GL Entry")
min = qb.from_(gl).select(Min(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
max = qb.from_(gl).select(Max(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
min = qb.from_(gl).select(Min(gl.posting_date)).run()[0][0]
max = qb.from_(gl).select(Max(gl.posting_date)).run()[0][0]
dates = self.get_dates(get_datetime(min), get_datetime(max))
for x in dates:
@@ -93,12 +93,16 @@ class ProcessPeriodClosingVoucher(Document):
def start_pcv_processing(docname: str):
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
if normal_balances := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
fields=["processing_date", "report_type", "parentfield"],
order_by="parentfield, idx, processing_date",
limit=4,
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
if normal_balances := (
qb.from_(ppcvd)
.select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
.limit(4)
.for_update(skip_locked=True)
.run(as_dict=True)
):
if not is_scheduler_inactive():
for x in normal_balances:
@@ -133,9 +137,10 @@ def pause_pcv_processing(docname: str):
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
# If a date is stuck in 'Running' state, this will allow it to procced.
if queued_dates := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
filters={"parent": docname, "status": ["in", ["Queued", "Running"]]},
pluck="name",
):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
@@ -169,6 +174,9 @@ def resume_pcv_processing(docname: str):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run()
start_pcv_processing(docname)
else:
# If a parent doc is stuck in 'Running' state, will allow it to proceed.
schedule_next_date(docname)
def update_default_dimensions(dimension_fields, gl_entry, dimension_values):
@@ -238,12 +246,15 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
@frappe.whitelist()
def schedule_next_date(docname: str):
if to_process := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
fields=["processing_date", "report_type", "parentfield"],
order_by="parentfield, idx, processing_date",
limit=1,
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
if to_process := (
qb.from_(ppcvd)
.select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
.limit(1)
.for_update(skip_locked=True)
.run(as_dict=True)
):
if not is_scheduler_inactive():
frappe.db.set_value(
@@ -281,7 +292,21 @@ def schedule_next_date(docname: str):
)
# Ensure both normal and opening balances are processed for all dates
if total_no_of_dates == completed:
summarize_and_post_ledger_entries(docname)
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
is_job_running,
)
job_name = f"summarize_{docname}"
if not is_job_running(job_name):
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries",
queue="long",
timeout="3600",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
docname=docname,
)
def make_dict_json_compliant(dimension_wise_balance) -> dict:

View File

@@ -115,7 +115,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount != 0 &&
!doc.on_hold &&
frappe.model.can_create("Payment Entry")
) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -130,7 +135,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount > 0 &&
!cint(doc.is_return) &&
!doc.on_hold &&
frappe.boot.user.in_create.includes("Payment Request")
) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -443,13 +454,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"discount_account",
"cost_center",
"project",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["expense_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
on_submit() {
@@ -558,12 +570,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
frappe.ui.form.on("Purchase Invoice", {
setup: function (frm) {
frm.custom_make_buttons = {

View File

@@ -110,6 +110,7 @@
"sales_invoice_item",
"material_request",
"material_request_item",
"delivered_by_supplier",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -1001,13 +1002,22 @@
"label": "Tax Withholding Category",
"options": "Tax Withholding Category",
"print_hide": 1
},
{
"default": "0",
"fieldname": "delivered_by_supplier",
"fieldtype": "Check",
"hidden": 1,
"label": "Delivered by Supplier",
"print_hide": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-04-07 15:40:45.687554",
"modified": "2026-05-06 08:08:40.782395",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -31,6 +31,7 @@ class PurchaseInvoiceItem(Document):
conversion_factor: DF.Float
cost_center: DF.Link | None
deferred_expense_account: DF.Link | None
delivered_by_supplier: DF.Check
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent

View File

@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -135,13 +135,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
if (doc.outstanding_amount > 0) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request_with_schedule();
},
__("Create")
);
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request_with_schedule();
},
__("Create")
);
}
this.frm.add_custom_button(
__("Invoice Discounting"),
this.make_invoice_discounting.bind(this),
@@ -552,12 +554,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"income_account",
"discount_account",
"cost_center",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["income_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
set_dynamic_labels() {

View File

@@ -1152,6 +1152,7 @@
"hide_seconds": 1,
"label": "Rounding Adjustment",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1164,6 +1165,7 @@
"label": "Rounded Total",
"oldfieldname": "rounded_total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1919,7 +1921,7 @@
{
"default": "0",
"depends_on": "eval: !doc.is_return",
"description": "Issue a debit note with 0 qty against an existing Sales Invoice",
"description": "Issue a debit note against an existing Sales Invoice to adjust the rate. The quantity will be retained from the original invoice.",
"fieldname": "is_debit_note",
"fieldtype": "Check",
"label": "Is Rate Adjustment Entry (Debit Note)"
@@ -2365,7 +2367,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-04-28 13:08:19.849783",
"modified": "2026-05-21 17:31:11.190958",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -968,9 +968,6 @@ class SalesInvoice(SellingController):
if selling_price_list:
self.set("selling_price_list", selling_price_list)
if not for_validate:
self.update_stock = cint(pos.get("update_stock"))
# set pos values in items
for item in self.get("items"):
if item.get("item_code"):
@@ -981,6 +978,10 @@ class SalesInvoice(SellingController):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
if not for_validate:
dn_flag = any(d.get("dn_detail") for d in self.get("items"))
self.update_stock = 0 if dn_flag else cint(pos.get("update_stock"))
# fetch terms
if self.tc_name and not self.terms:
self.terms = frappe.db.get_value("Terms and Conditions", self.tc_name, "terms")

View File

@@ -14,6 +14,7 @@ from frappe.utils import cstr
from frappe.utils.nestedset import get_root_of
from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups
from erpnext.setup.doctype.supplier_group.supplier_group import get_parent_supplier_groups
class IncorrectCustomerGroup(frappe.ValidationError):
@@ -176,38 +177,44 @@ def get_party_details(party, party_type, args=None):
def get_tax_template(posting_date, args):
"""Get matching tax rule"""
args = frappe._dict(args)
conditions = []
TaxRule = DocType("Tax Rule")
query = frappe.qb.from_(TaxRule).select("*")
if posting_date:
conditions.append(
f"""(from_date is null or from_date <= '{posting_date}')
and (to_date is null or to_date >= '{posting_date}')"""
query = query.where(
(TaxRule.from_date.isnull() | (TaxRule.from_date <= posting_date))
& (TaxRule.to_date.isnull() | (TaxRule.to_date >= posting_date))
)
else:
conditions.append("(from_date is null) and (to_date is null)")
query = query.where(TaxRule.from_date.isnull() & TaxRule.to_date.isnull())
conditions.append(
"ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")), False))
)
if "tax_category" in args.keys():
del args["tax_category"]
def get_group_ancestors(doctype, get_parents, value):
if not value:
value = get_root_of(doctype)
return [""] + [d.name for d in get_parents(value)]
group_fields = {
"customer_group": ("Customer Group", get_parent_customer_groups),
"supplier_group": ("Supplier Group", get_parent_supplier_groups),
}
args.setdefault("tax_category", "")
for key, value in args.items():
if key == "use_for_shopping_cart":
conditions.append(f"use_for_shopping_cart = {1 if value else 0}")
elif key == "customer_group":
if not value:
value = get_root_of("Customer Group")
customer_group_condition = get_customer_group_condition(value)
conditions.append(f"ifnull({key}, '') in ('', {customer_group_condition})")
query = query.where(TaxRule.use_for_shopping_cart == value)
elif key == "tax_category":
query = query.where(IfNull(TaxRule.tax_category, "") == (value or ""))
elif key in group_fields:
doctype, get_parents = group_fields[key]
query = query.where(
IfNull(TaxRule[key], "").isin(get_group_ancestors(doctype, get_parents, value))
)
else:
conditions.append(f"ifnull({key}, '') in ('', {frappe.db.escape(cstr(value))})")
query = query.where(IfNull(TaxRule[key], "").isin(["", value or ""]))
tax_rule = frappe.db.sql(
"""select * from `tabTax Rule`
where {}""".format(" and ".join(conditions)),
as_dict=True,
)
tax_rule = query.run(as_dict=True)
if not tax_rule:
return None
@@ -236,11 +243,3 @@ def get_tax_template(posting_date, args):
return None
return tax_template
def get_customer_group_condition(customer_group):
condition = ""
customer_groups = ["%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)]
if customer_groups:
condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups))
return condition

View File

@@ -62,6 +62,117 @@ class TestTaxRule(ERPNextTestSuite):
"_Test Sales Taxes and Charges Template - _TC",
)
def test_for_parent_supplier_group(self):
purchase_template = "_Test Purchase Taxes and Charges Template - _TC"
if not frappe.db.exists("Purchase Taxes and Charges Template", purchase_template):
frappe.get_doc(
{
"doctype": "Purchase Taxes and Charges Template",
"title": "_Test Purchase Taxes and Charges Template",
"company": "_Test Company",
"taxes": [
{
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Purchase Taxes and Charges",
"cost_center": "Main - _TC",
"rate": 6,
}
],
}
).insert()
make_tax_rule(
supplier_group="All Supplier Groups",
tax_type="Purchase",
purchase_tax_template=purchase_template,
priority=1,
use_for_shopping_cart=0,
from_date="2015-01-01",
save=1,
)
# "_Test Supplier Group" has "All Supplier Groups" as its parent — should match hierarchically
self.assertEqual(
get_tax_template(
"2015-01-01",
{
"supplier_group": "_Test Supplier Group",
"tax_type": "Purchase",
"use_for_shopping_cart": 0,
},
),
purchase_template,
)
def test_use_for_shopping_cart_filter(self):
city = "Test Cart City"
# higher priority ensures this rule wins when use_for_shopping_cart is not filtered
make_tax_rule(
customer="_Test Customer",
billing_city=city,
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
use_for_shopping_cart=0,
priority=2,
save=1,
)
make_tax_rule(
customer="_Test Customer",
billing_city=city,
sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
use_for_shopping_cart=1,
priority=1,
save=1,
)
# Cart request (use_for_shopping_cart=1) filters to cart rules only
self.assertEqual(
get_tax_template(
"2015-01-01",
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
),
"_Test Sales Taxes and Charges Template 1 - _TC",
)
# Non-cart request omits use_for_shopping_cart — no filter is applied, both rules
# are candidates; non-cart rule wins by higher priority
self.assertEqual(
get_tax_template(
"2015-01-01",
{"customer": "_Test Customer", "billing_city": city},
),
"_Test Sales Taxes and Charges Template - _TC",
)
def test_use_for_shopping_cart_default(self):
city = "Test Default Cart City"
# use_for_shopping_cart not set — Check field defaults to 0
make_tax_rule(
customer="_Test Customer",
billing_city=city,
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
use_for_shopping_cart=0, # Default is set to 1.
save=1,
)
# Non-cart request (no use_for_shopping_cart in args) matches the rule
self.assertEqual(
get_tax_template(
"2015-01-01",
{"customer": "_Test Customer", "billing_city": city},
),
"_Test Sales Taxes and Charges Template - _TC",
)
# Cart request (use_for_shopping_cart=1) does not match — rule has default 0
self.assertIsNone(
get_tax_template(
"2015-01-01",
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
)
)
def test_conflict_with_overlapping_dates(self):
tax_rule1 = make_tax_rule(
customer="_Test Customer",

View File

@@ -431,6 +431,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.adv_adj = adv_adj
gle.flags.update_outstanding = update_outstanding or "Yes"
gle.flags.notify_update = False
if gle.is_cancelled or is_immutable_ledger_enabled():
gle.flags.ignore_links = True
gle.submit()
if (
@@ -717,7 +719,12 @@ def make_reverse_gl_entries(
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
# For reverse entries, use the posting_date parameter if provided and valid
# Otherwise fall back to original posting_date
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"])
if partial_cancel:
# Partial cancel is only used by `Advance` in separate account feature.
# Only cancel GL entries for unlinked reference using `voucher_detail_no`

View File

@@ -677,7 +677,7 @@ def validate_due_date_with_template(posting_date, due_date, bill_date, template_
if not default_due_date:
return
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
if getdate(default_due_date) != getdate(posting_date) and getdate(due_date) > getdate(default_due_date):
if frappe.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
@@ -750,7 +750,7 @@ def set_taxes(
args.update({"tax_type": "Purchase"})
if use_for_shopping_cart:
args.update({"use_for_shopping_cart": use_for_shopping_cart})
args.update({"use_for_shopping_cart": cint(use_for_shopping_cart)})
return get_tax_template(posting_date, args)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,161 +0,0 @@
{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}
{% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div>
{% endif %}
{% if print_heading_template %}
{{ frappe.render_template(print_heading_template, {"doc":doc}) }}
{% else %}
{% endif %}
{%- if doc.meta.is_submittable and doc.docstatus==2-%}
<div class="text-center" document-status="cancelled">
<h4 style="margin: 0px;">{{ _("CANCELLED") }}</h4>
</div>
{%- endif -%}
{%- endmacro -%}
{% for page in layout %}
<div class="page-break">
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}
</div>
<style>
.taxes-section .order-taxes.mt-5{
margin-top: 0px !important;
}
.taxes-section .order-taxes .border-btm.pb-5{
padding-bottom: 0px !important;
}
.print-format label{
color: #74808b;
font-size: 12px;
margin-bottom: 4px;
}
</style>
{% if print_settings.repeat_header_footer %}
<div id="footer-html" class="visible-pdf">
{% if not no_letterhead and footer %}
<div class="letter-head-footer">
{{ footer }}
</div>
{% endif %}
<p class="text-center small page-number visible-pdf">
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
</p>
</div>
{% endif %}
<div class="row section-break" style="margin-bottom: 10px;">
<div class="col-xs-6 p-0">
<div class="col-xs-12 value text-uppercase"><b>{{ doc.customer }}</b></div>
<div class="col-xs-12">
{{ doc.address_display }}
</div>
<div class="col-xs-12">
{{ _("Contact: ")+doc.contact_display if doc.contact_display else '' }}
</div>
<div class="col-xs-12">
{{ _("Mobile: ")+doc.contact_mobile if doc.contact_mobile else '' }}
</div>
</div>
<div class="col-xs-3"></div>
<div class="col-xs-3" style="padding-left: 5px;">
<div>
<div><label>{{ _("Invoice ID") }}</label></div>
<div>{{ doc.name }}</div>
</div>
<div style="margin-top: 20px;">
<div><label>{{ _("Invoice Date") }}</label></div>
<div>{{ frappe.utils.format_date(doc.posting_date) }}</div>
</div>
<div style="margin-top: 20px;">
<div><label>{{ _("Due Date") }}</label></div>
<div>{{ frappe.utils.format_date(doc.due_date) }}</div>
</div>
</div>
</div>
<div class="section-break">
<table class="table table-bordered table-condensed mb-0" style="width: 100%; border-collapse: collapse; font-size: 12px;">
<colgroup>
<col style="width: 5%">
<col style="width: 45%">
<col style="width: 10%">
<col style="width: 20%">
<col style="width: 20%">
</colgroup>
<thead>
<tr>
<th class="text-uppercase" style="text-align:center">{{ _("Sr") }}</th>
<th class="text-uppercase" style="text-align:center">{{ _("Details") }}</th>
<th class="text-uppercase" style="text-align:center">{{ _("Qty") }}</th>
<th class="text-uppercase" style="text-align:right">{{ _("Rate") }}</th>
<th class="text-uppercase" style="text-align:right">{{ _("Amount") }}</th>
</tr>
</thead>
{% for item in doc.items %}
<tr>
<td style="text-align:center">{{ loop.index }}</td>
<td>
<b>{{ item.item_code }}: {{ item.item_name }}</b>
{% if (item.description != item.item_name) %}
<br>{{ item.description }}
{% endif %}
</td>
<td style="text-align: center;">
{{ item.get_formatted("qty", 0) }}
{{ item.get_formatted("uom", 0) }}
</td>
<td style="text-align: right;">{{ item.get_formatted("net_rate", doc) }}</td>
<td style="text-align: right;">{{ item.get_formatted("net_amount", doc) }}</td>
</tr>
{% endfor %}
</table>
<!-- total -->
<div class="row">
<div class="col-xs-6">
<div>
<label>{{ _("Amount in Words") }}</label>
{{ doc.in_words }}
</div>
<div style="margin-top: 20px;">
<label>{{ _("Payment Status") }}</label>
{{ doc.status }}
</div>
</div>
<div class="col-xs-6">
<div class="row section-break">
<div class="col-xs-7"><div>{{ _("Sub Total") }}</div></div>
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("net_total", doc) }}</div>
</div>
<div>
{% for d in doc.taxes %}
{% if d.tax_amount %}
<div class="row">
<div class="col-xs-8"><div>{{ _(d.description) }}</div></div>
<div class="col-xs-4" style="text-align: right;">{{ d.get_formatted("tax_amount") }}</div>
</div>
{% endif %}
{% endfor %}
</div>
<div class="row">
<div class="col-xs-7"><div>{{ _("Total") }}</div></div>
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("grand_total", doc) }}</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="row important data-field">
<div class="col-xs-12"><label>{{ _("Terms and Conditions") }}: </label></div>
<div class="col-xs-12">{{ doc.terms if doc.terms else '' }}</div>
</div>
</div>
</div>
</div>
{% endfor %}

View File

@@ -1,32 +0,0 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2025-01-22 16:23:51.012200",
"css": "",
"custom_format": 0,
"default_print_language": "en",
"disabled": 0,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "",
"font_size": 14,
"idx": 0,
"line_breaks": 0,
"margin_bottom": 0.0,
"margin_left": 0.0,
"margin_right": 0.0,
"margin_top": 0.0,
"modified": "2025-01-22 16:23:51.012200",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Print",
"owner": "Administrator",
"page_number": "Hide",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1,227 @@
{% include "accounts/report/accounts_receivable/accounts_receivable.html" %}
<style type="text/css">
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table thead th {
background: #f8f8f8;
text-align: center;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.text-center { text-align: center; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<br>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Supplier") %}:</strong>
{%= (filters.party.length && filters.party.join(", ")) || __("All Parties") %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Report Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.report_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th style="width: 8em; text-align: left;">{%= __("Date") %}</th>
<th style="text-align: left;">{%= __("Reference") %}</th>
{% if(filters.show_remarks) { %}
<th style="text-align: left;">{%= __("Remarks") %}</th>
{% } %}
<th style="width: 10em; text-align: right;">{%= __("Age (Days)") %}</th>
<th style="width: 10em; text-align: right;">{%= __("Invoiced Amount") %}</th>
<th style="width: 11em; text-align: right;">{%= __("Outstanding Amount") %}</th>
</tr>
</thead>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr>
<td class="text-left">{%= frappe.datetime.str_to_user(data[i]["posting_date"]) %}</td>
<td class="{% if(i == data.length - 1) { %}text-left text-bold{% } %}">
{% if(i == data.length - 1) { %}
{%= __("Total") %}
{% } else { %}
{%= data[i]["voucher_no"] %}
{% } %}
</td>
{% if(filters.show_remarks) { %}
<td class="text-left">
{% if(data[i]["remarks"] && data[i]["remarks"] != "No Remarks") { %}
{%= data[i]["remarks"] %}
{% } %}
</td>
{% } %}
<td class="text-right">{%= data[i]["age"] %}</td>
<td class="text-right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
<td class="text-right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
</tr>
{% } %}
</tbody>
</table>
</div>
&nbsp;
{% if(filters.show_future_payments) { %}
{%
var balance_row = data.slice(-1).pop();
var start = report.columns.findIndex(e => e.fieldname == 'age');
var currency = data[data.length - 1]["currency"];
var ranges = [
report.columns[start].label,
report.columns[start+1].label,
report.columns[start+2].label,
report.columns[start+3].label,
report.columns[start+4].label,
report.columns[start+5].label
];
%}
{% if(balance_row) { %}
<div class="report-table">
<table>
<thead>
<tr>
<th style="text-align: right;"></th>
{% for(var i = 0; i < ranges.length; i++) { %}
<th style="text-align: right;">{%= __(ranges[i]) %}</th>
{% } %}
<th style="text-align: right;">{%= __("Total") %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{%= __("Total Outstanding") %}</td>
<td class="text-right">{%= format_number(balance_row["age"], null, 2) %}</td>
<td class="text-right">{%= format_currency(balance_row["range1"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range2"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range3"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range4"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range5"], currency) %}</td>
<td class="text-right">{%= format_currency(flt(balance_row["outstanding"]), currency) %}</td>
</tr>
</tbody>
</table>
</div>
{% } %}
{% } %}
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -1 +1,180 @@
{% include "accounts/report/accounts_receivable/accounts_receivable.html" %}
<style type="text/css">
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table thead th {
background: #f8f8f8;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<br>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Supplier") %}:</strong>
{%= (filters.party && filters.party.length && filters.party.join(", ")) || __("All Parties") %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Ageing Based On") %}:</strong>
{%= __(filters.ageing_based_on) %}
</div>
<div>
<strong>{%= __("As on Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.report_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th class="text-left">{%= __("Supplier") %}</th>
<th class="text-right">{%= __("Total Invoiced Amount") %}</th>
<th class="text-right">{%= __("Total Paid Amount") %}</th>
<th class="text-right">{%= __("Debit Note Amount") %}</th>
<th class="text-right">{%= __("Total Outstanding Amount") %}</th>
</tr>
</thead>
<tbody>
{% for (var i = 0, l = data.length; i < l; i++) {
var row = data[i];
if (!(row.party || row.is_total_row)) continue;
%}
<tr>
<td class="{% if (row.is_total_row) { %}text-bold{% } %}">
{% if (row.is_total_row) { %}
{%= __("Total") %}
{% } else { %}
{%= row.party %}
{% } %}
</td>
<td class="text-right">
{%= format_currency(row.invoiced, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.paid, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.debit_note, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.outstanding, row.currency) %}
</td>
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -1,291 +1,225 @@
<style>
.print-format {
padding: 4mm;
font-size: 8.0pt !important;
}
.print-format td {
vertical-align:middle !important;
}
</style>
<style type="text/css">
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
<h2 class="text-center" style="margin-top:0">{%= __(report.report_name) %}</h2>
<h4 class="text-center">
{% if (filters.party) { %}
{%= __(filters.party) %}
{% } %}
</h4>
<h6 class="text-center">
{% if (filters.tax_id) { %}
{%= __("Tax Id: ")%} {%= filters.tax_id %}
{% } %}
</h6>
<h5 class="text-center">
{%= __(filters.ageing_based_on) %}
{%= __("Until") %}
{%= frappe.datetime.str_to_user(filters.report_date) %}
</h5>
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
<div class="clearfix">
<div class="pull-left">
{% if(filters.payment_terms) { %}
<strong>{%= __("Payment Terms") %}:</strong> {%= filters.payment_terms %}
{% } %}
</div>
<div class="pull-right">
{% if(filters.credit_limit) { %}
<strong>{%= __("Credit Limit") %}:</strong> {%= format_currency(filters.credit_limit) %}
{% } %}
</div>
</div>
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
{% if(filters.show_future_payments) { %}
{% var balance_row = data.slice(-1).pop();
var start = report.columns.findIndex((elem) => (elem.fieldname == 'age'));
var range1 = report.columns[start].label;
var range2 = report.columns[start+1].label;
var range3 = report.columns[start+2].label;
var range4 = report.columns[start+3].label;
var range5 = report.columns[start+4].label;
var range6 = report.columns[start+5].label;
%}
{% if(balance_row) { %}
<table class="table table-bordered table-condensed">
<caption class="text-right">(Amount in {%= data[0]["currency"] || "" %})</caption>
<colgroup>
<col style="width: 30mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
</colgroup>
.report-table thead th {
background: #f8f8f8;
text-align: center;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.text-center { text-align: center; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-left { text-align: left; }
.text-bold { font-weight: 700;}
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<br>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Customer") %}:</strong>
{%= (filters.party.length && filters.party.join(", ")) || __("All Parties") %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Report Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.report_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th>{%= __(" ") %}</th>
<th>{%= __(range1) %}</th>
<th>{%= __(range2) %}</th>
<th>{%= __(range3) %}</th>
<th>{%= __(range4) %}</th>
<th>{%= __(range5) %}</th>
<th>{%= __(range6) %}</th>
<th>{%= __("Total") %}</th>
<th style="width: 8em; text-align: left;">{%= __("Date") %}</th>
<th style="text-align: left;">{%= __("Reference") %}</th>
{% if(filters.show_remarks) { %}
<th style="text-align: left;">{%= __("Remarks") %}</th>
{% } %}
<th style="width: 10em; text-align: right;">{%= __("Age (Days)") %}</th>
<th style="width: 10em; text-align: right;">{%= __("Invoiced Amount") %}</th>
<th style="width: 11em; text-align: right;">{%= __("Outstanding Amount") %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{%= __("Total Outstanding") %}</td>
<td class="text-right">
{%= format_number(balance_row["age"], null, 2) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range1"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range2"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range3"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range4"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range5"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %}
</td>
</tr>
<td>{%= __("Future Payments") %}</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td class="text-right">
{%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %}
</td>
<tr class="cvs-footer">
<th class="text-left">{%= __("Cheques Required") %}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th class="text-right">
{%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}</th>
</tr>
</tbody>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr>
<td class="text-left">{%= frappe.datetime.str_to_user(data[i]["posting_date"]) %}</td>
<td class="{% if(i == data.length - 1) { %}text-left text-bold{% } %}">
{% if(i == data.length - 1) { %}
{%= __("Total") %}
{% } else { %}
{%= data[i]["voucher_no"] %}
{% } %}
</td>
{% if(filters.show_remarks) { %}
<td class="text-left">
{% if(data[i]["remarks"] && data[i]["remarks"] != "No Remarks") { %}
{%= data[i]["remarks"] %}
{% } %}
</td>
{% } %}
<td class="text-right">{%= data[i]["age"] %}</td>
<td class="text-right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
<td class="text-right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
</tr>
{% } %}
</tbody>
</table>
</div>
&nbsp;
{% if(filters.show_future_payments) { %}
{%
var balance_row = data.slice(-1).pop();
var start = report.columns.findIndex(e => e.fieldname == 'age');
var currency = data[data.length - 1]["currency"];
var ranges = [
report.columns[start].label,
report.columns[start+1].label,
report.columns[start+2].label,
report.columns[start+3].label,
report.columns[start+4].label,
report.columns[start+5].label
];
%}
{% if(balance_row) { %}
<div class="report-table">
<table>
<thead>
<tr>
<th style="text-align: right;"></th>
{% for(var i = 0; i < ranges.length; i++) { %}
<th style="text-align: right;">{%= __(ranges[i]) %}</th>
{% } %}
<th style="text-align: right;">{%= __("Total") %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{%= __("Total Outstanding") %}</td>
<td class="text-right">{%= format_number(balance_row["age"], null, 2) %}</td>
<td class="text-right">{%= format_currency(balance_row["range1"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range2"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range3"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range4"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range5"], currency) %}</td>
<td class="text-right">{%= format_currency(flt(balance_row["outstanding"]), currency) %}</td>
</tr>
</tbody>
</table>
</div>
{% } %}
{% } %}
<table class="table table-bordered">
<thead>
<tr>
{% if(report.report_name === "Accounts Receivable" || report.report_name === "Accounts Payable") { %}
<th style="width: 10%">{%= __("Date") %}</th>
<th style="width: 4%">{%= __("Age (Days)") %}</th>
{% if(report.report_name === "Accounts Receivable" && filters.show_sales_person) { %}
<th style="width: 14%">{%= __("Reference") %}</th>
<th style="width: 10%">{%= __("Sales Person") %}</th>
{% } else { %}
<th style="width: 24%">{%= __("Reference") %}</th>
{% } %}
{% if(!filters.show_future_payments) { %}
<th style="width: 20%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
{% } %}
<th style="width: 10%; text-align: right">{%= __("Invoiced Amount") %}</th>
{% if(!filters.show_future_payments) { %}
<th style="width: 10%; text-align: right">{%= __("Paid Amount") %}</th>
<th style="width: 10%; text-align: right">{%= report.report_name === "Accounts Receivable" ? __('Credit Note') : __('Debit Note') %}</th>
{% } %}
<th style="width: 10%; text-align: right">{%= __("Outstanding Amount") %}</th>
{% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %}
<th style="width: 12%">{%= __("Customer LPO No.") %}</th>
{% } %}
<th style="width: 10%">{%= __("Future Payment Ref") %}</th>
<th style="width: 10%">{%= __("Future Payment Amount") %}</th>
<th style="width: 10%">{%= __("Remaining Balance") %}</th>
{% } %}
{% } else { %}
<th style="width: 40%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
<th style="width: 15%">{%= __("Total Invoiced Amount") %}</th>
<th style="width: 15%">{%= __("Total Paid Amount") %}</th>
<th style="width: 15%">{%= report.report_name === "Accounts Receivable Summary" ? __('Credit Note Amount') : __('Debit Note Amount') %}</th>
<th style="width: 15%">{%= __("Total Outstanding Amount") %}</th>
{% } %}
</tr>
</thead>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr>
{% if(report.report_name === "Accounts Receivable" || report.report_name === "Accounts Payable") { %}
{% if(data[i]["party"]) { %}
<td>{%= frappe.datetime.str_to_user(data[i]["posting_date"]) %}</td>
<td style="text-align: right">{%= data[i]["age"] %}</td>
<td>
{% if(!filters.show_future_payments) { %}
{%= data[i]["voucher_type"] %}
<br>
{% } %}
{%= data[i]["voucher_no"] %}
</td>
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
{% if(report.report_name === "Accounts Receivable" && filters.show_sales_person) { %}
<td>{%= data[i]["sales_person"] %}</td>
{% } %}
{% if(!filters.show_future_payments) { %}
<td>
{% if(!filters.party?.length) { %}
{%= data[i]["party"] %}
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
<br> {%= data[i]["customer_name"] %}
{% } else if(data[i]["supplier_name"] != data[i]["party"]) { %}
<br> {%= data[i]["supplier_name"] %}
{% } %}
{% } %}
<div>
{% if data[i]["remarks"] %}
{%= __("Remarks") %}:
{%= data[i]["remarks"] %}
{% } %}
</div>
</td>
{% } %}
<td style="text-align: right">
{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
{% if(!filters.show_future_payments) { %}
<td style="text-align: right">
{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">
{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}</td>
{% } %}
<td style="text-align: right">
{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %}
<td style="text-align: right">
{%= data[i]["po_no"] %}</td>
{% } %}
<td style="text-align: right">{%= data[i]["future_ref"] %}</td>
<td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}</td>
{% } %}
{% } else { %}
<td></td>
{% if(!filters.show_future_payments) { %}
<td></td>
{% } %}
{% if(report.report_name === "Accounts Receivable" && filters.show_sales_person) { %}
<td></td>
{% } %}
<td></td>
<td style="text-align: right"><b>{%= __("Total") %}</b></td>
<td style="text-align: right">
{%= format_currency(data[i]["invoiced"], data[i]["currency"] ) %}</td>
{% if(!filters.show_future_payments) { %}
<td style="text-align: right">
{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %} </td>
{% } %}
<td style="text-align: right">
{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %}
<td style="text-align: right">
{%= data[i]["po_no"] %}</td>
{% } %}
<td style="text-align: right">{%= data[i]["future_ref"] %}</td>
<td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}</td>
{% } %}
{% } %}
{% } else { %}
{% if(data[i]["party"]|| "&nbsp;") { %}
{% if(!data[i]["is_total_row"]) { %}
<td>
{% if(!filters.party?.length) { %}
{%= data[i]["party"] %}
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
<br> {%= data[i]["customer_name"] %}
{% } else if(data[i]["supplier_name"] != data[i]["party"]) { %}
<br> {%= data[i]["supplier_name"] %}
{% } %}
{% } %}
<br>{%= __("Remarks") %}:
{%= data[i]["remarks"] %}
</td>
{% } else { %}
<td><b>{%= __("Total") %}</b></td>
{% } %}
<td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% } %}
{% } %}
</tr>
{% } %}
</tbody>
</table>
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
</div>

View File

@@ -131,8 +131,6 @@ class ReceivablePayableReport:
self.fetch_ple_in_buffered_cursor()
elif self.ple_fetch_method == "UnBuffered Cursor":
self.fetch_ple_in_unbuffered_cursor()
elif self.ple_fetch_method == "Raw SQL":
self.fetch_ple_in_sql_procedures()
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
@@ -323,81 +321,6 @@ class ReceivablePayableReport:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
def fetch_ple_in_sql_procedures(self):
self.proc = InitSQLProceduresForAR()
build_balance = f"""
begin not atomic
declare done boolean default false;
declare rec1 row type of `{self.proc._row_def_table_name}`;
declare ple cursor for {self.ple_query.get_sql()};
declare continue handler for not found set done = true;
open ple;
fetch ple into rec1;
while not done do
call {self.proc.init_procedure_name}(rec1);
fetch ple into rec1;
end while;
close ple;
set done = false;
open ple;
fetch ple into rec1;
while not done do
call {self.proc.allocate_procedure_name}(rec1);
fetch ple into rec1;
end while;
close ple;
end;
"""
frappe.db.sql(build_balance)
balances = frappe.db.sql(
f"""select
name,
voucher_type,
voucher_no,
party,
party_account `account`,
posting_date,
account_currency,
cost_center,
project,
sum(invoiced) `invoiced`,
sum(paid) `paid`,
sum(credit_note) `credit_note`,
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
sum(paid_in_account_currency) `paid_in_account_currency`,
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
as_dict=True,
)
for x in balances:
if self.filters.get("ignore_accounts"):
key = (x.voucher_type, x.voucher_no, x.party)
else:
key = (x.account, x.voucher_type, x.voucher_no, x.party)
_d = self.build_voucher_dict(x)
for field in [
"invoiced",
"paid",
"credit_note",
"outstanding",
"invoiced_in_account_currency",
"paid_in_account_currency",
"credit_note_in_account_currency",
"outstanding_in_account_currency",
"cost_center",
"project",
]:
_d[field] = x.get(field)
self.voucher_balance[key] = _d
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
@@ -1400,120 +1323,3 @@ def get_party_group_with_children(party, party_groups):
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
return list(set(all_party_groups))
class InitSQLProceduresForAR:
"""
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
"""
_varchar_type = get_definition("Data")
_currency_type = get_definition("Currency")
# Temporary Tables
_voucher_balance_name = "_ar_voucher_balance"
_voucher_balance_definition = f"""
create temporary table `{_voucher_balance_name}`(
name {_varchar_type},
voucher_type {_varchar_type},
voucher_no {_varchar_type},
party {_varchar_type},
party_account {_varchar_type},
posting_date date,
account_currency {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
invoiced {_currency_type},
paid {_currency_type},
credit_note {_currency_type},
invoiced_in_account_currency {_currency_type},
paid_in_account_currency {_currency_type},
credit_note_in_account_currency {_currency_type}) engine=memory;
"""
_row_def_table_name = "_ar_ple_row"
_row_def_table_definition = f"""
create temporary table `{_row_def_table_name}`(
name {_varchar_type},
account {_varchar_type},
voucher_type {_varchar_type},
voucher_no {_varchar_type},
against_voucher_type {_varchar_type},
against_voucher_no {_varchar_type},
party_type {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
party {_varchar_type},
posting_date date,
due_date date,
account_currency {_varchar_type},
amount {_currency_type},
amount_in_account_currency {_currency_type}) engine=memory;
"""
# Procedures
init_procedure_name = "ar_init_tmp_table"
init_procedure_sql = f"""
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
begin
if not exists (select name from `{_voucher_balance_name}` where name = sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)))
then
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, ple.project, 0, 0, 0, 0, 0, 0);
end if;
end;
"""
allocate_procedure_name = "ar_allocate_to_tmp_table"
allocate_procedure_sql = f"""
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
begin
declare invoiced {_currency_type} default 0;
declare invoiced_in_account_currency {_currency_type} default 0;
declare paid {_currency_type} default 0;
declare paid_in_account_currency {_currency_type} default 0;
declare credit_note {_currency_type} default 0;
declare credit_note_in_account_currency {_currency_type} default 0;
if ple.amount > 0 then
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
else
set invoiced = ple.amount;
set invoiced_in_account_currency = ple.amount_in_account_currency;
end if;
else
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
if (ple.voucher_no = ple.against_voucher_no) then
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
else
set credit_note = -1 * ple.amount;
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
end if;
else
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
end if;
end if;
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', '', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
end;
"""
def __init__(self):
existing_procedures = frappe.db.get_routines()
if self.init_procedure_name not in existing_procedures:
frappe.db.sql(self.init_procedure_sql)
if self.allocate_procedure_name not in existing_procedures:
frappe.db.sql(self.allocate_procedure_sql)
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
frappe.db.sql(self._voucher_balance_definition)
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
frappe.db.sql(self._row_def_table_definition)

View File

@@ -1 +1,180 @@
{% include "accounts/report/accounts_receivable/accounts_receivable.html" %}
<style type="text/css">
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table thead th {
background: #f8f8f8;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<br>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Customer") %}:</strong>
{%= (filters.party && filters.party.length && filters.party.join(", ")) || __("All Parties") %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Ageing Based On") %}:</strong>
{%= __(filters.ageing_based_on) %}
</div>
<div>
<strong>{%= __("As on Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.report_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th class="text-left">{%= __("Customer") %}</th>
<th class="text-right">{%= __("Total Invoiced Amount") %}</th>
<th class="text-right">{%= __("Total Paid Amount") %}</th>
<th class="text-right">{%= __("Credit Note Amount") %}</th>
<th class="text-right">{%= __("Total Outstanding Amount") %}</th>
</tr>
</thead>
<tbody>
{% for (var i = 0, l = data.length; i < l; i++) {
var row = data[i];
if (!(row.party || row.is_total_row)) continue;
%}
<tr>
<td class="{% if (row.is_total_row) { %}text-bold{% } %}">
{% if (row.is_total_row) { %}
{%= __("Total") %}
{% } else { %}
{%= row.party %}
{% } %}
</td>
<td class="text-right">
{%= format_currency(row.invoiced, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.paid, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.credit_note, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.outstanding, row.currency) %}
</td>
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -1 +1,224 @@
{% include "accounts/report/financial_statements.html" %}
{%
const report_columns = report
.get_columns_for_print()
.filter(col => !col.hidden);
if (report_columns.length > 8) {
frappe.throw(
__("Too many columns. Export the report and print it using a spreadsheet application.")
);
}
%}
<style>
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.financial-statements-important td { font-weight: bold; }
.financial-statements-blank-row td { height: 20px; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right {
text-align: right;
font-variant-numeric: tabular-nums;
}
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table th,
.report-table td {
padding: 6px 8px;
font-size: 14px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
}
.report-table thead th {
background: #f8f8f8;
font-weight: 500;
color: #7c7c7c;
}
.report-table tbody td {
vertical-align: top;
word-wrap: break-word;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Company") %}:</strong> {%= filters.company %}
</div>
<div>
<strong>{%= __("Currency") %}:</strong>
{%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Period Based On") %}:</strong>
{%= filters.filter_based_on %}
</div>
{% if (filters.filter_based_on === "Fiscal Year") { %}
<div>
<strong>{%= __("Start Year") %}:</strong> {%= filters.from_fiscal_year %}
</div>
<div>
<strong>{%= __("End Year") %}:</strong> {%= filters.to_fiscal_year %}
</div>
{% } else if (filters.filter_based_on === "Date Range") { %}
<div>
<strong>{%= __("Start Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_start_date) %}
</div>
<div>
<strong>{%= __("End Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_end_date) %}
</div>
{% } %}
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const align = i === 0 ? "text-left" : "text-right";
%}
<th class="{%= align %}">
{%= col.label %}
</th>
{% } %}
</tr>
</thead>
<tbody>
{% for (let j = 0, k = data.length; j < k; j++) { %}
{%
const row = data[j];
let row_class = "";
if (!(row.parent_account || row.parent_section)) {
row_class = "financial-statements-important";
}
if (!(row.account_name || row.section)) {
row_class += " financial-statements-blank-row";
}
%}
<tr class="{%= row_class %}">
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const value = row[col.fieldname];
const align = i === 0 ? "text-left" : "text-right";
%}
<td class="{%= align %}">
{% if (i === 0) { %}
<span style="padding-left: {%= cint(row.indent) * 2 %}em">
{%= String(row.account_name || row.section || "").replace(/^['"]|['"]$/g, "") %}
</span>
{% } else if (!is_null(value)) { %}
{%= frappe.format(value, col, {}, row) %}
{% } %}
</td>
{% } %}
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right text-muted">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -1 +1,224 @@
{% include "accounts/report/financial_statements.html" %}
{%
const report_columns = report
.get_columns_for_print()
.filter(col => !col.hidden);
if (report_columns.length > 8) {
frappe.throw(
__("Too many columns. Export the report and print it using a spreadsheet application.")
);
}
%}
<style>
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.financial-statements-important td { font-weight: bold; }
.financial-statements-blank-row td { height: 20px; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right {
text-align: right;
font-variant-numeric: tabular-nums;
}
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table th,
.report-table td {
padding: 6px 8px;
font-size: 14px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
}
.report-table thead th {
background: #f8f8f8;
font-weight: 500;
color: #7c7c7c;
}
.report-table tbody td {
vertical-align: top;
word-wrap: break-word;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Company") %}:</strong> {%= filters.company %}
</div>
<div>
<strong>{%= __("Currency") %}:</strong>
{%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Period Based On") %}:</strong>
{%= filters.filter_based_on %}
</div>
{% if (filters.filter_based_on === "Fiscal Year") { %}
<div>
<strong>{%= __("Start Year") %}:</strong> {%= filters.from_fiscal_year %}
</div>
<div>
<strong>{%= __("End Year") %}:</strong> {%= filters.to_fiscal_year %}
</div>
{% } else if (filters.filter_based_on === "Date Range") { %}
<div>
<strong>{%= __("Start Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_start_date) %}
</div>
<div>
<strong>{%= __("End Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_end_date) %}
</div>
{% } %}
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const align = i === 0 ? "text-left" : "text-right";
%}
<th class="{%= align %}">
{%= col.label %}
</th>
{% } %}
</tr>
</thead>
<tbody>
{% for (let j = 0, k = data.length; j < k; j++) { %}
{%
const row = data[j];
let row_class = "";
if (!(row.parent_account || row.parent_section)) {
row_class = "financial-statements-important";
}
if (!(row.account_name || row.section)) {
row_class += " financial-statements-blank-row";
}
%}
<tr class="{%= row_class %}">
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const value = row[col.fieldname];
const align = i === 0 ? "text-left" : "text-right";
%}
<td class="{%= align %}">
{% if (i === 0) { %}
<span style="padding-left: {%= cint(row.indent) * 2 %}em">
{%= String(row.account_name || row.section || "").replace(/^['"]|['"]$/g, "") %}
</span>
{% } else if (!is_null(value)) { %}
{%= frappe.format(value, col, {}, row) %}
{% } %}
</td>
{% } %}
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right text-muted">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -1,51 +1,114 @@
<!-- Modified on 25-11-2024
-->
<style type="text/css">
/* General styles for both screen display and print */
body, html {
margin-top: 10;
margin-top: 10px;
padding: 0;
width: 100%;
height: auto; /* Allow content to expand */
font-family: Arial, sans-serif; /* Example font */
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
/* Ensure consistent letter spacing across all media */
.title-letter-spacing {
letter-spacing: .2rem;
font-size: 15px;
font-weight: 600;
color: #171717;
}
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table thead th {
background: #f8f8f8;
text-align: center;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.date-col {
white-space: nowrap;
}
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-bold { font-weight: 700; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
color: #171717;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta .filter-row {
margin-bottom: 4px;
line-height: 1.5;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
/* Styles specific to printing and PDF generation */
@media print {
/* Set page size and margins for printing */
@page {
size: A4; /* Use fixed A4 page size */
size: A4;
margin-top: 10mm;
}
/* Force a page break before elements with the class "page-break" */
.page-break {
page-break-before: always;
margin-top: 10mm; /* Add some space after the break */
}
/* Ensure table headers repeat on each printed page */
thead {
display: table-header-group;
}
/* Ensure table footers repeat on each printed page */
tfoot {
display: table-footer-group;
}
th, td {
padding: 1px;
border: 1px solid black; /* Example border for clarity */
tr {
page-break-inside: avoid;
}
/* Hide elements that should not appear in print (optional) */
.no-print {
display: none !important;
}
@@ -53,129 +116,154 @@
</style>
<br>
<div style="font-family:Arial">
<div>
<div class="title-letter-spacing" style="text-align:center; font-size:15px; text-decoration:underline;">
<b>
{%= __("STATEMENT OF ACCOUNTS") %}<br>
{% if (filters.party_name) { %}
<br>{%= filters.party_name %}
{% } else if (filters.party && filters.party.length) { %}
<br>{%= filters.party %}
{% } else if (filters.account) { %}
<br>{%= filters.account %}
{% } else { %}
<br>{%= __("All Parties ") %}
{% } %}
</b>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __("Statement Of Accounts") %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div class="filter-row">
<strong>{%= __("Customer") %}:</strong>
{%=
(filters.party.length && filters.party.join(", ")) || filters.party_name || "All Parties"
%}
</div>
</div>
<div class="right text-right">
<div class="filter-row">
<strong>{%= __("Statement Period") %}:</strong>
{%= __("{0} to {1}", [
frappe.datetime.str_to_user(filters.from_date),
frappe.datetime.str_to_user(filters.to_date)
]) %}
</div>
</div>
</div>
<div style="text-align:center; font-size:13px;">
<b>
{%= __("{0} to {1}", [frappe.datetime.str_to_user(filters.from_date), frappe.datetime.str_to_user(filters.to_date)]) %}<br><br>
</b>
</div>
</div>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
<table style="width:100%; font-size: 11px">
<thead>
<tr class="title-letter-spacing" style="text-align: center; font-weight:bold">
<td style="border: 1.5px solid black; width: 7em">{%= __("Date").toLocaleUpperCase() %}</td>
<td style="border: 1.5px solid black">{%= __("Particulars").toLocaleUpperCase() %}</td>
{% if(filters.show_remarks) { %}
<td style="border: 1.5px solid black">{%= __("Remarks").toLocaleUpperCase() %}</td>
{% } %}
<td style="border: 1.5px solid black; width: 9em">{%= __("Debit").toLocaleUpperCase() %}</td>
<td style="border: 1.5px solid black; width: 9em">{%= __("Credit").toLocaleUpperCase() %}</td>
<td style="border: 1.5px solid black; width: 10.2em">{%= __("Balance").toLocaleUpperCase() %}</td>
</tr>
</thead>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr style="border-bottom: 1px solid black">
{% if(data[i].posting_date) { %}
<td style="text-align: center; border: 1px dotted black">
{%= frappe.datetime.str_to_user(data[i].posting_date) %}
</td>
<td style="border-right: 1px dotted black">
{%= data[i].voucher_type %} {%= data[i].voucher_no %}
{% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %}
{% } %}<br>
{% if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
</td>
{% if(filters.show_remarks) { %}
<td style="border-right: 1px dotted black; font-size: 10px">
{% if(data[i].remarks != "No Remarks" && data[i].remarks != "") { %}
{%= __("Remarks") %}: {%= data[i].remarks %}<br>
{% } %}
</td>
{% } %}
<td style="text-align: right; border-right: 1px dotted black">
{% if data[i].debit != 0 %}
{%= format_currency(data[i].debit, filters.presentation_currency) %}
{% } %}
</td>
<td style="text-align: right; border-right: 1px dotted black">
{% if data[i].credit != 0 %}
{%= format_currency(data[i].credit, filters.presentation_currency) %}
{% } %}
</td>
{% } else { %}
<td style="text-align: center; border: 1px dotted black">
{% if(i == 0) { %}
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th style="width: 8em; text-align: left;">{%= __("Date") %}</th>
<th style="text-align: left;">{%= __("Voucher Details") %}</th>
{% if(filters.show_remarks) { %}
<th style="text-align: left;">{%= __("Remarks") %}</th>
{% } %}
<th style="width: 10em; text-align: right;">{%= __("Debit") %}</th>
<th style="width: 10em; text-align: right;">{%= __("Credit") %}</th>
<th style="width: 10em; text-align: right;">{%= __("Balance") %}</th>
</tr>
</thead>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
{% var row = data[i]; %}
{% var is_entry = row.posting_date; %}
{% var is_last = i == l-1; %}
{% var is_second_last = i == l-2; %}
<tr>
<td class="text-left date-col">
{% if(is_entry) { %}
{%= frappe.datetime.str_to_user(row.posting_date) %}
{% } else if(i == 0) { %}
{%= frappe.datetime.str_to_user(filters.from_date) %}
{% } %}
</td>
<td style="text-align: left; border-right: 1px dotted black"><b>
{% if(i == l-2) { %}
{%= __("Total") %}
<td class="{% if(!is_entry) { %}text-left text-bold{% } %}">
{% if(is_entry) { %}
{%= row.voucher_type %} {%= row.voucher_no %}
{% if(!(filters.party || filters.account)) { %}
<div style="margin-top: 2px;">
{%= row.party || row.account %}
</div>
{% } %}
{% if(row.bill_no) { %}
<div style="margin-top: 2px;">
{%= __("Supplier Invoice No") %}: {%= row.bill_no %}
</div>
{% } %}
{% } else { %}
{% if(i == l-1) { %}
{% if(is_second_last) { %}
{%= __("Total") %}
{% } else if(is_last) { %}
{%= __("Closing [Opening + Total] ") %}
{% } else { %}
{%= frappe.format(data[i].account, {fieldtype: "Link"}) || "&nbsp;" %}
{% } %}
{% } %}</b>
</td>
{% if(filters.show_remarks) { %} <td style="text-align: left; border-right: 1px dotted black"></td>{% } %}
<td style="text-align: right; border-right: 1px dotted black">
{% if(i != 0){ %}
{% if(i != l-1){ %}
{%= data[i].account && format_currency(data[i].debit, filters.presentation_currency) %}
{%= frappe.format(row.account, {fieldtype: "Link"}) || "&nbsp;" %}
{% } %}
{% } %}
</td>
<td style="text-align: right; border-right: 1px dotted black">
{% if(i != 0){ %}
{% if(i != l-1){ %}
{%= data[i].account && format_currency(data[i].credit, filters.presentation_currency) %}
{% } %}
{% if(filters.show_remarks) { %}
<td class="text-left">
{% if(is_entry && row.remarks && row.remarks != "No Remarks") { %}
{%= row.remarks %}
{% } %}
</td>
{% } %}
{% if(i == l-1) { %}
<td style="text-align: right; font-weight:bold; border-right: 1px dotted black">
{%= format_currency(data[i].balance, filters.presentation_currency) %}
{% if(data[i].balance < 0){ %}Cr{% } %}
{% if(data[i].balance > 0){ %}Dr{% } %}
</td>
{% } else { %}
<td style="text-align: right; border-right: 1px dotted black">
{% if(i != l-2) { %}
{%= format_currency(data[i].balance, filters.presentation_currency) %}
{% } %}
</td>
{% } %}
</tr>
{% endfor%}
</tbody>
</table>
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
</div>
<td class="text-right">
{% if(is_entry) { %}
{% if(row.debit != 0) { %}
{%= format_currency(row.debit, filters.presentation_currency) %}
{% } %}
{% } else if(i != 0 && !is_last) { %}
{%= row.account && format_currency(row.debit, filters.presentation_currency) %}
{% } %}
</td>
<td class="text-right">
{% if(is_entry) { %}
{% if(row.credit != 0) { %}
{%= format_currency(row.credit, filters.presentation_currency) %}
{% } %}
{% } else if(i != 0 && !is_last) { %}
{%= row.account && format_currency(row.credit, filters.presentation_currency) %}
{% } %}
</td>
<td class="text-right {% if(is_last) { %}text-bold{% } %}">
{% if(is_last) { %}
{%= format_currency(row.balance, filters.presentation_currency) %}
{% if(row.balance < 0) { %} Cr{% } %}
{% if(row.balance > 0) { %} Dr{% } %}
{% } else { %}
{%= format_currency(row.balance, filters.presentation_currency) %}
{% } %}
</td>
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -177,10 +177,16 @@ frappe.query_reports["General Ledger"] = {
fieldtype: "Check",
default: 1,
},
{
fieldname: "disable_opening_balance_calculation",
label: __("Disable Opening Balance Calculation"),
fieldtype: "Check",
},
{
fieldname: "show_opening_entries",
label: __("Show Opening Entries"),
fieldtype: "Check",
depends_on: "eval: !doc.disable_opening_balance_calculation",
},
{
fieldname: "include_default_book_entries",

View File

@@ -283,7 +283,15 @@ def get_conditions(filters):
if filters.get("party"):
conditions.append("party in %(party)s")
if not (
if filters.get("disable_opening_balance_calculation"):
if not ignore_is_opening:
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
else:
conditions.append("posting_date >=%(from_date)s")
# opening balance calculation is done only if filtered on account/party
# so from_date filter is not applied
elif not (
filters.get("account")
or filters.get("party")
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
@@ -417,7 +425,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
# Opening for filtered account
add_total_to_data(totals, "opening")
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
if not filters.get("categorize_by"):
all_entries = []
for acc_dict in gle_map.values():
all_entries.extend(acc_dict.entries)
data += all_entries
elif filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
set_opening_closing = (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
)
@@ -483,7 +497,6 @@ def initialize_gle_map(gl_entries, filters):
totals=get_totals_dict(),
entries=[],
)
return gle_map
@@ -548,7 +561,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
gle.remarks = _(gle.remarks)
gle.party_type = _(gle.party_type)
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
if gle.posting_date < from_date or (
cstr(gle.is_opening) == "Yes"
and not show_opening_entries
and not filters.disable_opening_balance_calculation
):
if not group_by_voucher_consolidated:
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True)

View File

@@ -812,19 +812,11 @@ class GrossProfitGenerator:
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, row.item_row, item_code
)
elif self.delivery_notes.get((row.parent, row.item_code), None):
# check if Invoice has delivery notes
dn = self.delivery_notes.get((row.parent, row.item_code))
parenttype, parent, item_row, dn_warehouse = (
"Delivery Note",
dn["delivery_note"],
dn["item_row"],
dn["warehouse"],
)
my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
elif row.item_row and self.delivery_notes.get(row.item_row):
dn = self.delivery_notes[row.item_row]
if flt(dn.total_qty):
return flt(row.qty) * flt(dn.total_incoming_value) / flt(dn.total_qty)
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
elif row.sales_order and row.so_detail:
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
if incoming_amount:
@@ -1076,25 +1068,29 @@ class GrossProfitGenerator:
def get_delivery_notes(self):
self.delivery_notes = frappe._dict({})
if self.si_list:
from frappe.query_builder.functions import Sum
invoices = [x.parent for x in self.si_list]
dni = qb.DocType("Delivery Note Item")
delivery_notes = (
qb.from_(dni)
.select(
dni.against_sales_invoice.as_("sales_invoice"),
dni.item_code,
dni.warehouse,
dni.parent.as_("delivery_note"),
dni.name.as_("item_row"),
dni.si_detail,
Sum(dni.stock_qty * dni.incoming_rate).as_("total_incoming_value"),
Sum(dni.stock_qty).as_("total_qty"),
)
.where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices)))
.groupby(dni.against_sales_invoice, dni.item_code)
.orderby(dni.creation, order=Order.desc)
.where(
(dni.docstatus == 1)
& (dni.against_sales_invoice.isin(invoices))
& (dni.si_detail.isnotnull())
& (dni.si_detail != "")
)
.groupby(dni.si_detail)
.run(as_dict=True)
)
for entry in delivery_notes:
self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry
self.delivery_notes[entry.si_detail] = entry
def group_items_by_invoice(self):
"""

View File

@@ -1 +1,224 @@
{% include "accounts/report/financial_statements.html" %}
{%
const report_columns = report
.get_columns_for_print()
.filter(col => !col.hidden);
if (report_columns.length > 8) {
frappe.throw(
__("Too many columns. Export the report and print it using a spreadsheet application.")
);
}
%}
<style>
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.financial-statements-important td { font-weight: bold; }
.financial-statements-blank-row td { height: 20px; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right {
text-align: right;
font-variant-numeric: tabular-nums;
}
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table th,
.report-table td {
padding: 6px 8px;
font-size: 14px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
}
.report-table thead th {
background: #f8f8f8;
font-weight: 500;
color: #7c7c7c;
}
.report-table tbody td {
vertical-align: top;
word-wrap: break-word;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Company") %}:</strong> {%= filters.company %}
</div>
<div>
<strong>{%= __("Currency") %}:</strong>
{%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Period Based On") %}:</strong>
{%= filters.filter_based_on %}
</div>
{% if (filters.filter_based_on === "Fiscal Year") { %}
<div>
<strong>{%= __("Start Year") %}:</strong> {%= filters.from_fiscal_year %}
</div>
<div>
<strong>{%= __("End Year") %}:</strong> {%= filters.to_fiscal_year %}
</div>
{% } else if (filters.filter_based_on === "Date Range") { %}
<div>
<strong>{%= __("Start Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_start_date) %}
</div>
<div>
<strong>{%= __("End Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_end_date) %}
</div>
{% } %}
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const align = i === 0 ? "text-left" : "text-right";
%}
<th class="{%= align %}">
{%= col.label %}
</th>
{% } %}
</tr>
</thead>
<tbody>
{% for (let j = 0, k = data.length; j < k; j++) { %}
{%
const row = data[j];
let row_class = "";
if (!(row.parent_account || row.parent_section)) {
row_class = "financial-statements-important";
}
if (!(row.account_name || row.section)) {
row_class += " financial-statements-blank-row";
}
%}
<tr class="{%= row_class %}">
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const value = row[col.fieldname];
const align = i === 0 ? "text-left" : "text-right";
%}
<td class="{%= align %}">
{% if (i === 0) { %}
<span style="padding-left: {%= cint(row.indent) * 2 %}em">
{%= String(row.account_name || row.section || "").replace(/^['"]|['"]$/g, "") %}
</span>
{% } else if (!is_null(value)) { %}
{%= frappe.format(value, col, {}, row) %}
{% } %}
</td>
{% } %}
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right text-muted">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -1 +1,215 @@
{% include "accounts/report/financial_statements.html" %}
{%
const report_columns = report
.get_columns_for_print()
.filter(col => !col.hidden);
if (report_columns.length > 8) {
frappe.throw(
__("Too many columns. Export the report and print it using a spreadsheet application.")
);
}
%}
<style>
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.financial-statements-important td { font-weight: bold; }
.financial-statements-blank-row td { height: 20px; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right {
text-align: right;
font-variant-numeric: tabular-nums;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table th,
.report-table td {
padding: 6px 8px;
font-size: 14px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
}
.report-table thead th {
background: #f8f8f8;
font-weight: 500;
color: #7c7c7c;
}
.report-table tbody td.text-left {
vertical-align: top;
word-wrap: break-word;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Company") %}:</strong> {%= filters.company %}
</div>
<div>
<strong>{%= __("Currency") %}:</strong>
{%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("From Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.from_date) %}
</div>
<div>
<strong>{%= __("To Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.to_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const align = i === 0 ? "text-left" : "text-right";
const styling = i === 0 ? "" : "width: 9em";
%}
<th class="{%= align %}" style= "{%= styling%}">
{%= col.label %}
</th>
{% } %}
</tr>
</thead>
<tbody>
{% for (let j = 0, k = data.length; j < k; j++) { %}
{%
const row = data[j];
let row_class = "";
if (!(row.parent_account || row.parent_section)) {
row_class = "financial-statements-important";
}
if (!(row.account_name || row.section)) {
row_class += " financial-statements-blank-row";
}
%}
<tr class="{%= row_class %}">
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const value = row[col.fieldname];
const align = i === 0 ? "text-left" : "text-right";
%}
<td class="{%= align %}">
{% if (i === 0) { %}
<span style="padding-left: {%= cint(row.indent) * 2 %}em">
{%= String(row.account_name || row.section || "").replace(/^['"]|['"]$/g, "") %}
</span>
{% } else if (!is_null(value)) { %}
{%= frappe.format(value, col, {}, row) %}
{% } %}
</td>
{% } %}
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right text-muted">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -40,7 +40,7 @@ import erpnext
from erpnext.accounts.doctype.account.account import get_account_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on
from erpnext.stock.utils import get_combine_datetime, get_stock_value_on
if TYPE_CHECKING:
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
@@ -1752,31 +1752,31 @@ def sort_stock_vouchers_by_posting_date(
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
values = []
condition = ""
posting_datetime = get_combine_datetime(posting_date, posting_time)
SLE = DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(SLE)
.select(SLE.voucher_type, SLE.voucher_no)
.distinct()
.where(SLE.posting_datetime >= posting_datetime)
.where(SLE.is_cancelled == 0)
.orderby(SLE.posting_datetime)
.orderby(SLE.creation)
.for_update()
)
if for_items:
condition += " and item_code in ({})".format(", ".join(["%s"] * len(for_items)))
values += for_items
query = query.where(SLE.item_code.isin(for_items))
if for_warehouses:
condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
values += for_warehouses
query = query.where(SLE.warehouse.isin(for_warehouses))
if company:
condition += " and company = %s"
values.append(company)
query = query.where(SLE.company == company)
future_stock_vouchers = frappe.db.sql(
f"""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
where
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
and is_cancelled = 0
{condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""",
tuple([posting_date, posting_time, *values]),
as_dict=True,
)
future_stock_vouchers = query.run(as_dict=True)
return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers]
@@ -2130,8 +2130,9 @@ def create_payment_ledger_entry(
ple = frappe.get_doc(entry)
if cancel:
delink_original_entry(ple, partial_cancel=partial_cancel)
if is_immutable_ledger_enabled():
if not is_immutable_ledger_enabled():
delink_original_entry(ple, partial_cancel=partial_cancel)
else:
ple.delinked = 0
ple.posting_date = frappe.form_dict.get("posting_date") or getdate()
@@ -2220,6 +2221,7 @@ def delink_original_entry(pl_entry, partial_cancel=False):
qb.update(ple)
.set(ple.modified, now())
.set(ple.modified_by, frappe.session.user)
.set(ple.delinked, True)
.where(
(ple.company == pl_entry.company)
& (ple.account_type == pl_entry.account_type)
@@ -2236,9 +2238,6 @@ def delink_original_entry(pl_entry, partial_cancel=False):
if partial_cancel:
query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no)
if not is_immutable_ledger_enabled():
query = query.set(ple.delinked, True)
query.run()

View File

@@ -14,7 +14,7 @@
"for_user": "",
"hide_custom": 0,
"icon": "table",
"idx": 0,
"idx": 1,
"indicator_color": "",
"is_hidden": 0,
"label": "Financial Reports",
@@ -266,13 +266,13 @@
"type": "Link"
}
],
"modified": "2025-12-24 12:49:25.266357",
"modified": "2026-05-18 09:49:45.138296",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Financial Reports",
"number_cards": [],
"owner": "Administrator",
"parent_page": "Accounting",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",

View File

@@ -551,7 +551,9 @@ frappe.ui.form.on("Asset", {
asset_type: function (frm) {
if (frm.doc.docstatus == 0) {
if (frm.doc.asset_type == "Composite Asset") {
frm.set_value("net_purchase_amount", 0);
if (!frm.doc.net_purchase_amount) {
frm.set_value("net_purchase_amount", 0);
}
} else {
frm.set_df_property("net_purchase_amount", "read_only", 0);
}

View File

@@ -1977,7 +1977,7 @@ def create_asset_category(enable_cwip=1):
asset_category.insert()
def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_asset=0):
def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_asset=0, asset_category=None):
meta = frappe.get_meta("Asset")
naming_series = meta.get_field("naming_series").options.splitlines()[0] or "ACC-ASS-.YYYY.-"
try:
@@ -1987,7 +1987,7 @@ def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_ass
"item_code": item_code or "Macbook Pro",
"item_name": "Macbook Pro",
"description": "Macbook Pro Retina Display",
"asset_category": "Computers",
"asset_category": asset_category or "Computers",
"item_group": "All Item Groups",
"stock_uom": "Nos",
"is_stock_item": 0,

View File

@@ -195,6 +195,9 @@ def reschedule_depreciation(asset_doc, notes, disposal_date=None):
for row in asset_doc.get("finance_books"):
current_schedule = get_asset_depr_schedule_doc(asset_doc.name, None, row.finance_book)
if disposal_date and flt(row.value_after_depreciation) <= flt(row.expected_value_after_useful_life):
continue
if current_schedule:
if current_schedule.docstatus == 1:
new_schedule = frappe.copy_doc(current_schedule)

View File

@@ -2,10 +2,59 @@
// For license information, please see license.txt
frappe.ui.form.on("Buying Settings", {
// refresh: function(frm) {
// }
refresh(frm) {
if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm);
const display = frm.doc.supp_master_name === "Naming Series";
frm.set_df_property("naming_series_details", "hidden", !display);
frm.set_df_property("configure", "hidden", !display);
if (display) {
frm.naming_controller.load_master_series("Supplier", "naming_series_details");
}
frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm));
},
supp_master_name(frm) {
const display = frm.doc.supp_master_name === "Naming Series";
frm.set_df_property("naming_series_details", "hidden", !display);
frm.set_df_property("configure", "hidden", !display);
if (display) {
frm.naming_controller.load_master_series("Supplier", "naming_series_details");
} else {
frm.doc.naming_series_details = "";
frm.refresh_field("naming_series_details");
}
frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm));
},
configure(frm) {
frm.naming_controller.show_naming_series_dialog("Supplier", ({ naming_series_options }) => {
frm.doc.naming_series_details = naming_series_options;
frm.refresh_field("naming_series_details");
});
},
});
function get_transactions(frm) {
const transactions = [
{ label: __("Supplier"), doctype: "Supplier" },
{ label: __("Material Request"), doctype: "Material Request" },
{ label: __("Request for Quotation"), doctype: "Request for Quotation" },
{ label: __("Purchase Order"), doctype: "Purchase Order" },
{ label: __("Purchase Invoice"), doctype: "Purchase Invoice" },
{ label: __("Purchase Receipt"), doctype: "Purchase Receipt" },
];
if (frm.doc.supp_master_name !== "Naming Series") {
return transactions.filter((t) => t.doctype !== "Supplier");
}
return transactions;
}
frappe.tour["Buying Settings"] = [
{
fieldname: "supp_master_name",

View File

@@ -6,44 +6,51 @@
"engine": "InnoDB",
"field_order": [
"supplier_and_price_defaults_section",
"supplier_defaults_section",
"supp_master_name",
"supplier_group",
"buying_price_list",
"naming_series_details",
"configure",
"column_break_4",
"supplier_group",
"pricing_tab",
"buying_price_list",
"section_break_vwgg",
"maintain_same_rate",
"column_break_lwxs",
"maintain_same_rate_action",
"role_to_override_stop_action",
"section_break_xmlt",
"po_required",
"blanket_order_allowance",
"column_break_sbwq",
"pr_required",
"project_update_frequency",
"transaction_settings_section",
"column_break_fcyl",
"set_landed_cost_based_on_purchase_invoice_rate",
"allow_zero_qty_in_supplier_quotation",
"use_transaction_date_exchange_rate",
"allow_zero_qty_in_request_for_quotation",
"allow_negative_rates_for_items",
"po_required",
"pr_required",
"project_update_frequency",
"column_break_12",
"maintain_same_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"allow_negative_rates_for_items",
"set_valuation_rate_for_rejected_materials",
"disable_last_purchase_rate",
"show_pay_button",
"purchase_invoice_settings_section",
"bill_for_rejected_quantity_in_purchase_invoice",
"use_transaction_date_exchange_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
"zero_quantity_line_items_section",
"allow_zero_qty_in_supplier_quotation",
"allow_zero_qty_in_request_for_quotation",
"allow_zero_qty_in_purchase_order",
"blanket_order_section",
"blanket_order_allowance",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
"over_transfer_allowance",
"validate_consumed_qty",
"section_break_xcug",
"auto_create_subcontracting_order",
"column_break_izrr",
"auto_create_purchase_receipt",
"request_for_quotation_tab",
"fixed_email"
"fixed_email",
"document_naming_tab",
"transaction_naming_html"
],
"fields": [
{
@@ -54,6 +61,7 @@
"options": "Supplier Name\nNaming Series\nAuto Name"
},
{
"documentation_url": "https://docs.frappe.io/erpnext/buying-settings#2-default-supplier-group",
"fieldname": "supplier_group",
"fieldtype": "Link",
"label": "Default Supplier Group",
@@ -68,26 +76,27 @@
{
"fieldname": "po_required",
"fieldtype": "Select",
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
"label": "Is Purchase Order required for Purchase Invoice & Receipt creation?",
"options": "No\nYes"
},
{
"fieldname": "pr_required",
"fieldtype": "Select",
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
"label": "Is Purchase Receipt required for Purchase Invoice creation?",
"options": "No\nYes"
},
{
"default": "0",
"description": "Warn or stop if Item rate is changed in Purchase Invoice or Purchase Receipt generated from a Purchase Order.",
"fieldname": "maintain_same_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout the Purchase Cycle"
"label": "Maintain same rate throughout the purchase cycle"
},
{
"default": "0",
"fieldname": "allow_multiple_items",
"fieldtype": "Check",
"label": "Allow Item To Be Added Multiple Times in a Transaction"
"label": "Allow Item to be added multiple times in a transaction"
},
{
"fieldname": "subcontract",
@@ -96,9 +105,10 @@
},
{
"default": "BOM",
"documentation_url": "https://docs.frappe.io/erpnext/buying-settings#1-backflush-raw-materials-of-subcontract-based-on",
"fieldname": "backflush_raw_materials_of_subcontract_based_on",
"fieldtype": "Select",
"label": "Backflush Raw Materials of Subcontract Based On",
"label": "Backflush raw materials of subcontract based on",
"options": "BOM\nMaterial Transferred for Subcontract"
},
{
@@ -108,25 +118,21 @@
"fieldtype": "Float",
"label": "Over Transfer Allowance (%)"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "Stop",
"depends_on": "maintain_same_rate",
"description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action If Same Rate is Not Maintained",
"label": "Action if same rate is not maintained",
"mandatory_depends_on": "maintain_same_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval:doc.maintain_same_rate_action == 'Stop'",
"depends_on": "maintain_same_rate",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"label": "Role allowed to override stop action",
"options": "Role"
},
{
@@ -134,12 +140,12 @@
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
"fieldtype": "Check",
"label": "Bill for Rejected Quantity in Purchase Invoice"
"label": "Bill for rejected quantity in Purchase Invoice"
},
{
"fieldname": "supplier_and_price_defaults_section",
"fieldtype": "Tab Break",
"label": "Naming Series and Price Defaults"
"label": "Defaults"
},
{
"fieldname": "column_break_4",
@@ -156,16 +162,17 @@
},
{
"default": "0",
"description": "Prevents the system from automatically using the rate from the last purchase transaction when creating new purchase orders or transactions.",
"fieldname": "disable_last_purchase_rate",
"fieldtype": "Check",
"label": "Disable Last Purchase Rate"
"label": "Disable last purchase rate"
},
{
"default": "1",
"depends_on": "eval: frappe.boot.versions && frappe.boot.versions.payments",
"fieldname": "show_pay_button",
"fieldtype": "Check",
"label": "Show Pay Button in Purchase Order Portal"
"label": "Show pay button in Purchase Order portal"
},
{
"default": "0",
@@ -193,30 +200,25 @@
"fieldname": "section_break_xcug",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_izrr",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Subcontracting Order (Draft) will be auto-created on submission of Purchase Order.",
"fieldname": "auto_create_subcontracting_order",
"fieldtype": "Check",
"label": "Auto Create Subcontracting Order"
"label": "Auto create Subcontracting Order"
},
{
"default": "0",
"description": "Purchase Receipt (Draft) will be auto-created on submission of Subcontracting Receipt.",
"fieldname": "auto_create_purchase_receipt",
"fieldtype": "Check",
"label": "Auto Create Purchase Receipt"
"label": "Auto create Purchase Receipt"
},
{
"default": "Each Transaction",
"description": "How often should Project be updated of Total Purchase Cost ?",
"fieldname": "project_update_frequency",
"fieldtype": "Select",
"label": "Update frequency of Project",
"label": "How often should project be updated of Total Purchase Cost ?",
"options": "Each Transaction\nManual"
},
{
@@ -240,14 +242,6 @@
"fieldtype": "Check",
"label": "Allow Supplier Quotation with Zero Quantity"
},
{
"fieldname": "section_break_xmlt",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_sbwq",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_fcyl",
"fieldtype": "Column Break"
@@ -258,7 +252,7 @@
"description": "If enabled, the system will generate an accounting entry for materials rejected in the Purchase Receipt.",
"fieldname": "set_valuation_rate_for_rejected_materials",
"fieldtype": "Check",
"label": "Set Valuation Rate for Rejected Materials"
"label": "Set valuation rate for rejected Materials"
},
{
"fieldname": "request_for_quotation_tab",
@@ -279,23 +273,77 @@
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
"fieldname": "validate_consumed_qty",
"fieldtype": "Check",
"label": "Validate Consumed Qty (as per BOM)"
"label": "Validate consumed quantity (as per BOM)"
},
{
"default": "0",
"fieldname": "allow_negative_rates_for_items",
"fieldtype": "Check",
"label": "Allow Negative rates for Items"
"label": "Allow negative rates for Items"
},
{
"fieldname": "supplier_defaults_section",
"fieldtype": "Section Break",
"label": "Supplier Defaults"
},
{
"fieldname": "section_break_vwgg",
"fieldtype": "Section Break"
},
{
"fieldname": "blanket_order_section",
"fieldtype": "Section Break",
"label": "Blanket Orders"
},
{
"fieldname": "zero_quantity_line_items_section",
"fieldtype": "Section Break",
"label": "Zero-Quantity Line Items"
},
{
"fieldname": "purchase_invoice_settings_section",
"fieldtype": "Section Break",
"label": "Purchase Invoice Settings"
},
{
"fieldname": "column_break_lwxs",
"fieldtype": "Column Break"
},
{
"fieldname": "pricing_tab",
"fieldtype": "Tab Break",
"label": "Pricing"
},
{
"fieldname": "document_naming_tab",
"fieldtype": "Tab Break",
"label": "Document Naming"
},
{
"fieldname": "configure",
"fieldtype": "Button",
"hidden": 1,
"label": "Configure Series"
},
{
"fieldname": "transaction_naming_html",
"fieldtype": "HTML"
},
{
"fieldname": "naming_series_details",
"fieldtype": "Small Text",
"hidden": 1,
"is_virtual": 1,
"label": "Naming Series options"
}
],
"grid_page_length": 50,
"hide_toolbar": 0,
"icon": "fa fa-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-04-15 16:07:35.484787",
"modified": "2026-05-05 16:30:37.184607",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@@ -313,7 +361,6 @@
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"role": "Purchase Manager",

View File

@@ -354,9 +354,9 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
}
if (is_drop_ship && doc.status != "Delivered") {
if (is_drop_ship && !["Completed", "Delivered"].includes(doc.status)) {
this.frm.add_custom_button(
__("Delivered"),
__("Deliver (Dropship)"),
this.delivered_by_supplier.bind(this),
__("Status")
);
@@ -374,7 +374,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
if (doc.status != "Closed") {
if (doc.status != "On Hold") {
if (flt(doc.per_received) < 100 && allow_receipt) {
if (
doc.items
.filter((item) => !item.delivered_by_supplier)
.some((item) => item.received_qty < item.qty) &&
allow_receipt
) {
this.frm.add_custom_button(
__("Purchase Receipt"),
() => {
@@ -416,7 +421,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
__("Create")
);
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
if (
frappe.model.can_create("Payment Entry") &&
flt(doc.per_billed) < 100 &&
doc.status != "Delivered"
) {
this.frm.add_custom_button(
__("Payment"),
() => this.make_payment_entry(),
@@ -424,7 +433,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
if (flt(doc.per_billed) < 100) {
if (flt(doc.per_billed) < 100 && frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -660,12 +669,20 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (doc.schedule_date) {
row.schedule_date = doc.schedule_date;
refresh_field("schedule_date", cdn, "items");
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
this.frm.script_manager.copy_from_first_row("items", row, ["schedule_date"]);
field_copy.push("project");
}
if (doc.schedule_date) {
frappe.model.set_value(cdt, cdn, "schedule_date", doc.schedule_date);
} else {
field_copy.push("schedule_date");
}
if (field_copy.length) {
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
}
@@ -718,7 +735,108 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
delivered_by_supplier() {
this.frm.cscript.update_status("Deliver", "Delivered");
const data = this.frm.doc.items
.filter((item) => item.delivered_by_supplier == 1)
.map((item) => {
return {
__checked: item.qty > item.received_qty,
name: item.name,
item_code: item.item_code,
item_name: item.item_name,
qty: item.qty,
uom: item.uom,
delivered_qty: item.received_qty || 0,
qty_change: item.qty - item.received_qty,
};
});
const dialog = new frappe.ui.Dialog({
title: __("Set Dropship Items Delivered Quantity"),
size: "extra-large",
fields: [
{
fieldname: "items",
fieldtype: "Table",
data: data,
cannot_add_rows: true,
cannot_delete_rows: true,
fields: [
{
fieldname: "name",
fieldtype: "Data",
read_only: true,
hidden: 1,
},
{
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
label: __("Item Code"),
in_list_view: 1,
read_only: true,
},
{
fieldname: "item_name",
fieldtype: "Data",
label: __("Item Name"),
in_list_view: 1,
read_only: true,
},
{
fieldname: "qty",
fieldtype: "Float",
label: __("Quantity"),
in_list_view: 1,
read_only: true,
},
{
fieldname: "uom",
fieldtype: "Data",
label: __("UOM"),
in_list_view: 1,
read_only: true,
},
{
fieldname: "delivered_qty",
fieldtype: "Float",
label: __("Delivered Qty"),
read_only: true,
in_list_view: 1,
},
{
fieldname: "qty_change",
fieldtype: "Float",
label: __("Qty Change"),
in_list_view: 1,
reqd: 1,
},
],
},
],
primary_action: (values) => {
const frm = this.frm;
frappe.call({
doc: frm.doc,
method: "update_dropship_received_qty",
args: {
data: values.items
.filter((item) => item.__checked)
.map((item) => ({
name: item.name,
current_qty: item.delivered_qty,
qty_change: item.qty_change,
})),
},
callback: function (r) {
if (!r.exc) {
frm.reload_doc();
frappe.toast(__("Quantities updated successfully."));
dialog.hide();
}
},
});
},
});
dialog.show();
}
items_on_form_rendered() {
@@ -740,12 +858,6 @@ cur_frm.cscript.update_status = function (label, status) {
});
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
if (cur_frm.doc.is_old_subcontracting_flow) {
cur_frm.fields_dict["items"].grid.get_field("bom").get_query = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];

View File

@@ -218,7 +218,6 @@ class PurchaseOrder(BuyingController):
self.create_raw_materials_supplied()
self.validate_fg_item_for_subcontracting()
self.set_received_qty_for_drop_ship_items()
if not self.advance_payment_status:
self.advance_payment_status = "Not Initiated"
@@ -492,7 +491,8 @@ class PurchaseOrder(BuyingController):
self.update_status_updater_if_from_pp()
if self.has_drop_ship_item():
self.update_delivered_qty_in_sales_order()
self.set_received_qty_to_zero_for_drop_ship_items()
self.update_receiving_percentage()
self.update_reserved_qty_for_subcontract()
self.check_on_hold_or_closed_status()
@@ -566,20 +566,80 @@ class PurchaseOrder(BuyingController):
so.set_status(update=True)
so.notify_update()
def set_received_qty_to_zero_for_drop_ship_items(self):
for item in self.items:
if item.delivered_by_supplier:
item.db_set("received_qty", 0)
def has_drop_ship_item(self):
return any(d.delivered_by_supplier for d in self.items)
@frappe.whitelist()
def update_dropship_received_qty(self, data: list[dict]):
if not data:
frappe.throw(_("Please select at least one item to update delivered quantity."))
for d in data:
item = next((item for item in self.items if item.name == d.get("name")), None)
if not item:
frappe.throw(
_("Item with name {0} not found in the Purchase Order").format(frappe.bold(d.get("name")))
)
if not item.delivered_by_supplier:
frappe.throw(
_(
"Item {0} is not a drop ship item. Only drop ship items can have Delivered Qty updated."
).format(frappe.bold(item.item_code))
)
if not item.has_permlevel_access_to("received_qty", permission_type="write"):
frappe.throw(
_("You don't have permission to update Received Qty DocField for item {0}").format(
frappe.bold(item.item_code)
)
)
if not d.get("qty_change"):
frappe.throw(
_(
"Item {0} has no changes in delivered quantity. Please unselect the row if you do not wish to update its quantity."
).format(frappe.bold(item.item_code))
)
if d.get("qty_change") < 0 and abs(d.get("qty_change")) > item.received_qty:
frappe.throw(
_("Delivered Qty cannot be reduced by more than {0} for item {1}").format(
item.received_qty, frappe.bold(item.item_code)
)
)
if d.get("qty_change") > 0 and item.received_qty + d.get("qty_change") > item.qty:
frappe.throw(
_("Delivered Qty cannot be increased by more than {0} for item {1}").format(
item.qty - item.received_qty, frappe.bold(item.item_code)
)
)
qty_change = item.received_qty + d.get("qty_change")
item.db_set("received_qty", qty_change, update_modified=True)
self.add_comment(
"Label",
_("updated delivered quantity for item {0} to {1}").format(
frappe.bold(item.item_code), frappe.bold(qty_change)
),
)
self.update_receiving_percentage()
self.set_status(update=True)
self.update_delivered_qty_in_sales_order()
def is_against_so(self):
return any(d.sales_order for d in self.items if d.sales_order)
def is_against_pp(self):
return any(d.production_plan for d in self.items if d.production_plan)
def set_received_qty_for_drop_ship_items(self):
for item in self.items:
if item.delivered_by_supplier == 1:
item.received_qty = item.qty
def update_reserved_qty_for_subcontract(self):
if self.is_old_subcontracting_flow:
for d in self.supplied_items:
@@ -592,7 +652,7 @@ class PurchaseOrder(BuyingController):
for item in self.items:
received_qty += min(item.received_qty, item.qty)
total_qty += item.qty
if total_qty:
if total_qty and received_qty:
self.db_set("per_received", flt(received_qty / total_qty) * 100, update_modified=False)
else:
self.db_set("per_received", 0, update_modified=False)

View File

@@ -627,6 +627,7 @@
"fieldtype": "Float",
"label": "Received Qty",
"no_copy": 1,
"non_negative": 1,
"oldfieldname": "received_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
@@ -950,7 +951,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-11-30 16:51:57.761673",
"modified": "2026-05-14 12:16:16.192936",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -126,9 +126,3 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
// for backward compatibility: combine new and previous states
extend_cscript(cur_frm.cscript, new erpnext.buying.SupplierQuotationController({ frm: cur_frm }));
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};

View File

@@ -345,9 +345,9 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Supplier-Wise Sales Analytics",
"label": "Item Wise Consumption",
"link_count": 0,
"link_to": "Supplier-Wise Sales Analytics",
"link_to": "Item Wise Consumption",
"link_type": "Report",
"onboard": 1,
"type": "Link"

View File

@@ -71,6 +71,8 @@ from erpnext.stock.get_item_details import (
NOT_APPLICABLE_TAX,
ItemDetailsCtx,
_get_item_tax_template,
_get_item_tax_template_from_item_group,
get_bin_details,
get_conversion_factor,
get_item_details,
get_item_tax_map,
@@ -327,6 +329,7 @@ class AccountsController(TransactionBase):
# Determine if drop ship applies
is_drop_ship = self.doctype in {
"Purchase Order",
"Purchase Invoice",
"Sales Order",
"Sales Invoice",
} and self.is_drop_ship(self.items)
@@ -3672,7 +3675,12 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
}
)
child_item.item_tax_template = _get_item_tax_template(ctx, item.taxes)
item_tax_template = _get_item_tax_template(ctx, item.taxes)
if not item_tax_template:
item_tax_template = _get_item_tax_template_from_item_group(ctx, item.item_group)
child_item.item_tax_template = item_tax_template
child_item.item_tax_rate = get_item_tax_map(
doc=parent_doc,
tax_template=child_item.item_tax_template,
@@ -3730,6 +3738,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True)
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
child_item.update(get_bin_details(child_item.item_code, child_item.warehouse, p_doc.get("company")))
if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
# Initialized value will update in parent validation
@@ -3901,8 +3910,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
)
qty_limits = {
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")),
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")),
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity.")),
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity.")),
}
if parent_doctype in qty_limits:

View File

@@ -461,17 +461,7 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
)
net_rate = (
flt(
(item.base_net_amount / item.received_qty) * item.qty,
item.precision("base_net_amount"),
)
if item.received_qty
and frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
else item.base_net_amount
)
net_rate = item.base_net_amount
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate

View File

@@ -209,7 +209,9 @@ def create_variant(item, args, use_template_image=False):
variant_attributes = []
for d in template.attributes:
variant_attributes.append({"attribute": d.attribute, "attribute_value": args.get(_(d.attribute))})
attribute_value = args.get(_(d.attribute)) or args.get(d.attribute)
if attribute_value:
variant_attributes.append({"attribute": d.attribute, "attribute_value": attribute_value})
variant.set("attributes", variant_attributes)
copy_attributes_to_variant(template, variant)
@@ -228,6 +230,12 @@ def enqueue_multiple_variant_creation(item, args, use_template_image=False):
# There can be innumerable attribute combinations, enqueue
if isinstance(args, str):
variants = json.loads(args)
else:
variants = args
variants = {key: values for key, values in variants.items() if values}
if not variants:
frappe.throw(_("Please select at least one attribute value"))
total_variants = 1
for key in variants:
total_variants *= len(variants[key])
@@ -251,6 +259,7 @@ def create_multiple_variants(item, args, use_template_image=False):
count = 0
if isinstance(args, str):
args = json.loads(args)
args = {key: values for key, values in args.items() if values}
template_item = frappe.get_doc("Item", item)
args_set = generate_keyed_value_combinations(args)
@@ -285,6 +294,9 @@ def generate_keyed_value_combinations(args):
"""
# Return empty list if empty
if not args:
return []
args = {key: values for key, values in args.items() if values}
if not args:
return []

View File

@@ -17,6 +17,7 @@ from pypika import Order
import erpnext
from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template
from erpnext.stock.utils import get_combine_datetime
# searches for active employees
@@ -210,16 +211,28 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if filters and isinstance(filters, dict):
if filters.get("customer") or filters.get("supplier"):
party_type = "Customer" if filters.get("customer") else "Supplier"
party = filters.get("customer") or filters.get("supplier")
group = "Customer Group" if filters.get("customer") else "Supplier Group"
item_rules_list = frappe.get_all(
"Party Specific Item",
filters={
"party": ["!=", party],
"party_type": "Customer" if filters.get("customer") else "Supplier",
"party_type": party_type,
},
fields=["restrict_based_on", "based_on_value"],
)
party_group_rules_list = frappe.get_all(
"Party Specific Item",
filters={"party_type": group},
fields=["party as party_group", "restrict_based_on", "based_on_value"],
)
current_party_group = frappe.get_value(party_type, party, frappe.scrub(group))
for rule in party_group_rules_list:
if current_party_group != rule.party_group:
item_rules_list.append(rule)
filters_dict = {}
for rule in item_rules_list:
if rule["restrict_based_on"] == "Item":
@@ -484,6 +497,13 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
.limit(page_len)
)
if not filters.get("is_inward"):
if filters.get("posting_date") and filters.get("posting_time"):
query = query.where(
stock_ledger_entry.posting_datetime
<= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time"))
)
if not filters.get("include_expired_batches"):
query = query.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
@@ -537,6 +557,13 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
.limit(page_len)
)
if not filters.get("is_inward"):
if filters.get("posting_date") and filters.get("posting_time"):
bundle_query = bundle_query.where(
stock_ledger_entry.posting_datetime
<= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time"))
)
if not filters.get("include_expired_batches"):
bundle_query = bundle_query.where(
(batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())

View File

@@ -579,6 +579,7 @@ class SellingController(StockController):
or (
get_valuation_method(d.item_code, self.company) == "Moving Average"
and self.get("is_return")
and not is_standalone
)
):
d.incoming_rate = get_incoming_rate(

View File

@@ -281,10 +281,10 @@ class StatusUpdater(Document):
# get unique transactions to update
for d in self.get_all_children():
if hasattr(d, "qty") and d.qty < 0 and not self.get("is_return"):
if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code))
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
if (

View File

@@ -269,14 +269,15 @@ class StockController(AccountsController):
)
is_asset_pr = any(d.get("is_fixed_asset") for d in self.get("items"))
if (
need_inventory_map = (self.get_stock_items() or self.get("packed_items")) and (
cint(erpnext.is_perpetual_inventory_enabled(self.company))
or provisional_accounting_for_non_stock_items
or is_asset_pr
):
)
inventory_account_map = frappe._dict()
if need_inventory_map:
inventory_account_map = self.get_inventory_account_map()
if need_inventory_map or provisional_accounting_for_non_stock_items or is_asset_pr:
if self.docstatus == 1:
if not gl_entries:
gl_entries = (

View File

@@ -651,20 +651,25 @@ class SubcontractingInwardController:
).update_manufacturing_qty_fields()
elif self.purpose in ["Subcontracting Delivery", "Subcontracting Return"]:
fieldname = "delivered_qty" if self.purpose == "Subcontracting Delivery" else "returned_qty"
qty_map = defaultdict(lambda: defaultdict(float))
for item in self.items:
doctype = (
"Subcontracting Inward Order Item"
if not item.type and not item.is_legacy_scrap_item
else "Subcontracting Inward Order Secondary Item"
)
frappe.db.set_value(
doctype,
item.scio_detail,
fieldname,
frappe.get_value(doctype, item.scio_detail, fieldname)
+ (item.transfer_qty if self._action == "submit" else -item.transfer_qty),
qty_map[doctype][item.scio_detail] += (
item.transfer_qty if self._action == "submit" else -item.transfer_qty
)
for doctype, item_qty_map in qty_map.items():
table = frappe.qb.DocType(doctype)
field = table[fieldname]
doc_updates = {
scio_detail: {fieldname: field + qty} for scio_detail, qty in item_qty_map.items()
}
frappe.db.bulk_update(doctype, doc_updates, chunk_size=len(doc_updates))
def update_inward_order_received_items(self):
if self.subcontracting_inward_order:
match self.purpose:
@@ -679,14 +684,18 @@ class SubcontractingInwardController:
else -item.transfer_qty
for item in self.items
}
case_expr = Case()
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
for scio_rm_name, qty in scio_rm_names.items():
case_expr = case_expr.when(table.name == scio_rm_name, table.returned_qty + qty)
frappe.qb.update(table).set(table.returned_qty, case_expr).where(
(table.name.isin(list(scio_rm_names.keys()))) & (table.docstatus == 1)
).run()
doc_updates = {
scio_rm_name: {"returned_qty": table.returned_qty + qty}
for scio_rm_name, qty in scio_rm_names.items()
}
if doc_updates:
frappe.db.bulk_update(
"Subcontracting Inward Order Received Item",
doc_updates,
chunk_size=len(doc_updates),
update_modified=False,
)
def update_inward_order_received_items_for_raw_materials_receipt(self):
data = frappe._dict()
@@ -737,9 +746,7 @@ class SubcontractingInwardController:
fields=["rate", "name", "required_qty", "received_qty"],
)
deleted_docs = []
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
case_expr_qty, case_expr_rate = Case(), Case()
doc_updates = {}
for d in result:
current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1)
current_rate = flt(data[d.name].rate)
@@ -754,16 +761,17 @@ class SubcontractingInwardController:
)
if not d.required_qty and not d.received_qty:
deleted_docs.append(d.name)
frappe.delete_doc("Subcontracting Inward Order Received Item", d.name)
else:
case_expr_qty = case_expr_qty.when(table.name == d.name, d.received_qty)
case_expr_rate = case_expr_rate.when(table.name == d.name, d.rate)
doc_updates[d.name] = {"received_qty": d.received_qty, "rate": d.rate}
if final_list := list(set(data.keys()) - set(deleted_docs)):
frappe.qb.update(table).set(table.received_qty, case_expr_qty).set(
table.rate, case_expr_rate
).where((table.name.isin(final_list)) & (table.docstatus == 1)).run()
if doc_updates:
frappe.db.bulk_update(
"Subcontracting Inward Order Received Item",
doc_updates,
chunk_size=len(doc_updates),
update_modified=False,
)
def update_inward_order_received_items_for_manufacture(self):
customer_warehouse = frappe.get_cached_value(
@@ -815,8 +823,8 @@ class SubcontractingInwardController:
)
if data := data.run(as_dict=True):
deleted_docs, used_item_wh = [], []
case_expr = Case()
used_item_wh = []
doc_updates = {}
for d in data:
if not d.warehouse:
d.warehouse = next(
@@ -828,15 +836,17 @@ class SubcontractingInwardController:
qty = d.consumed_qty + item_code_wh[(d.rm_item_code, d.warehouse)]
if qty or d.is_customer_provided_item or not d.is_additional_item:
case_expr = case_expr.when((table.name == d.name), qty)
doc_updates[d.name] = {"consumed_qty": qty}
else:
deleted_docs.append(d.name)
frappe.delete_doc("Subcontracting Inward Order Received Item", d.name)
if final_list := list(set([d.name for d in data]) - set(deleted_docs)):
frappe.qb.update(table).set(table.consumed_qty, case_expr).where(
(table.name.isin(final_list)) & (table.docstatus == 1)
).run()
if doc_updates:
frappe.db.bulk_update(
"Subcontracting Inward Order Received Item",
doc_updates,
chunk_size=len(doc_updates),
update_modified=False,
)
main_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
for extra_item in [
@@ -908,27 +918,25 @@ class SubcontractingInwardController:
for d in result
}
)
deleted_docs = []
case_expr = Case()
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
doc_updates = {}
for key, value in secondary_items_dict.items():
if (
self._action == "cancel"
and value.produced_qty - abs(secondary_items.get(key)) == 0
):
deleted_docs.append(value.name)
frappe.delete_doc("Subcontracting Inward Order Secondary Item", value.name)
else:
case_expr = case_expr.when(
table.name == value.name, value.produced_qty + secondary_items.get(key)
)
doc_updates[value.name] = {
"produced_qty": value.produced_qty + secondary_items.get(key)
}
if final_list := list(
set([v.name for v in secondary_items_dict.values()]) - set(deleted_docs)
):
frappe.qb.update(table).set(table.produced_qty, case_expr).where(
(table.name.isin(final_list)) & (table.docstatus == 1)
).run()
if doc_updates:
frappe.db.bulk_update(
"Subcontracting Inward Order Secondary Item",
doc_updates,
chunk_size=len(doc_updates),
update_modified=False,
)
fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
for secondary_item in [

View File

@@ -169,10 +169,6 @@ class calculate_taxes_and_totals:
return
if not self.discount_amount_applied:
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
do_not_round_fields = ["valuation_rate", "incoming_rate"]
for item in self.doc.items:
@@ -236,13 +232,7 @@ class calculate_taxes_and_totals:
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
else:
qty = (
(item.qty + item.rejected_qty)
if bill_for_rejected_quantity_in_purchase_invoice
and self.doc.doctype == "Purchase Receipt"
else item.qty
)
item.amount = flt(item.rate * qty, item.precision("amount"))
item.amount = flt(item.rate * item.qty, item.precision("amount"))
item.net_amount = item.amount
@@ -402,16 +392,9 @@ class calculate_taxes_and_totals:
self.doc.total
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
for item in self._items:
self.doc.total += item.amount
self.doc.total_qty += (
(item.qty + item.rejected_qty)
if bill_for_rejected_quantity_in_purchase_invoice and self.doc.doctype == "Purchase Receipt"
else item.qty
)
self.doc.total_qty += item.qty
self.doc.base_total += item.base_amount
self.doc.net_total += item.net_amount
self.doc.base_net_total += item.base_net_amount

View File

@@ -3,7 +3,11 @@ import unittest
import frappe
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
from erpnext.controllers.item_variant import (
copy_attributes_to_variant,
generate_keyed_value_combinations,
make_variant_item_code,
)
from erpnext.stock.doctype.item.test_item import set_item_variant_settings
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
create_quality_inspection_parameter,
@@ -18,6 +22,19 @@ class TestItemVariant(ERPNextTestSuite):
variant = make_item_variant()
self.assertEqual(variant.get("quality_inspection_template"), "_Test QC Template")
def test_generate_keyed_value_combinations_ignores_empty_attributes(self):
combinations = generate_keyed_value_combinations(
{"Test Colour": ["Red", "Blue"], "Test Size": ["Small", "Large"], "Test Fit": []}
)
self.assertEqual(len(combinations), 4)
self.assertNotIn("Test Fit", combinations[0])
single_attribute_combinations = generate_keyed_value_combinations(
{"Test Colour": ["Red", "Blue"], "Test Size": []}
)
self.assertEqual(single_attribute_combinations, [{"Test Colour": "Red"}, {"Test Colour": "Blue"}])
def create_variant_with_tables(item, args):
if isinstance(args, str):

View File

@@ -235,10 +235,13 @@ def _get_agents_sorted_by_asc_workload(date):
return agent_list
appointment_counter = Counter(agent_list)
for appointment in appointments:
assigned_to = frappe.parse_json(appointment._assign)
if not assigned_to:
assign_data = appointment._assign
if isinstance(assign_data, str):
assign_data = assign_data.strip()
if not assign_data:
continue
if (assigned_to[0] in agent_list) and getdate(appointment.scheduled_time) == date:
assigned_to = frappe.parse_json(assign_data)
if assigned_to and (assigned_to[0] in agent_list) and getdate(appointment.scheduled_time) == date:
appointment_counter[assigned_to[0]] += 1
sorted_agent_list = appointment_counter.most_common()
sorted_agent_list.reverse()

View File

@@ -1,60 +0,0 @@
{
"custom_fields": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2019-12-02 11:00:03.432994",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Contact",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "is_billing_contact",
"fieldtype": "Check",
"hidden": 0,
"idx": 27,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"insert_after": "is_primary_contact",
"label": "Is Billing Contact",
"length": 0,
"modified": "2019-12-02 11:00:03.432994",
"modified_by": "Administrator",
"name": "Contact-is_billing_contact",
"no_copy": 0,
"options": null,
"owner": "Administrator",
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"width": null
}
],
"custom_perms": [],
"doctype": "Contact",
"property_setters": [],
"sync_on_migrate": 1
}

File diff suppressed because it is too large Load Diff

View File

@@ -1638,12 +1638,12 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=
)
def add_operating_cost_component_wise(
stock_entry, work_order=None, consumed_operating_cost=None, op_expense_account=None, job_card=None
):
def add_operating_cost_component_wise(stock_entry, work_order=None, op_expense_account=None, job_card=None):
if not work_order:
return False
from erpnext.stock.doctype.stock_entry.stock_entry import get_consumed_operating_cost
cost_added = False
for row in work_order.operations:
if job_card and job_card.operation_id != row.name:
@@ -1661,18 +1661,32 @@ def add_operating_cost_component_wise(
},
)
consumed_operating_cost = (
get_consumed_operating_cost(work_order.name, stock_entry.bom_no, row.name) or []
)
for wc in workstation_cost:
expense_account = (
get_component_account(wc.operating_component, stock_entry.company) or op_expense_account
)
consumed_op_cost = next(
(
cost
for cost in consumed_operating_cost
if cost.get("operating_component") == wc.operating_component
),
{},
)
actual_cp_operating_cost = flt(
flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0) - consumed_operating_cost,
flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0)
- flt(consumed_op_cost.get("consumed_cost")),
row.precision("actual_operating_cost"),
)
per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty - work_order.produced_qty)
remaining_qty = row.completed_qty - consumed_op_cost.get("consumed_qty", 0)
per_unit_cost = actual_cp_operating_cost / (remaining_qty or 1)
operating_cost = per_unit_cost * stock_entry.fg_completed_qty
if per_unit_cost:
if actual_cp_operating_cost:
stock_entry.append(
"additional_costs",
{
@@ -1680,8 +1694,14 @@ def add_operating_cost_component_wise(
"description": _("{0} Operating Cost for operation {1}").format(
wc.operating_component, row.operation
),
"amount": per_unit_cost * flt(stock_entry.fg_completed_qty),
"amount": flt(
min(operating_cost, actual_cp_operating_cost),
frappe.get_precision("Landed Cost Taxes and Charges", "amount"),
),
"has_operating_cost": 1,
"operation_id": row.name,
"operating_component": wc.operating_component,
"qty": min(remaining_qty, stock_entry.fg_completed_qty),
},
)
@@ -1699,17 +1719,15 @@ def get_component_account(parent, company):
def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None):
from erpnext.stock.doctype.stock_entry.stock_entry import (
get_consumed_operating_cost,
get_operating_cost_per_unit,
get_remaining_operating_cost,
)
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
remaining_operating_cost = get_remaining_operating_cost(work_order, stock_entry.bom_no)
if operating_cost_per_unit:
if remaining_operating_cost:
cost_added = add_operating_cost_component_wise(
stock_entry,
work_order,
get_consumed_operating_cost(work_order.name, stock_entry.bom_no),
expense_account,
job_card=job_card,
)
@@ -1720,7 +1738,10 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
{
"expense_account": expense_account,
"description": _("Operating Cost as per Work Order / BOM"),
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
"amount": flt(
remaining_operating_cost * stock_entry.fg_completed_qty,
frappe.get_precision("Landed Cost Taxes and Charges", "amount"),
),
"has_operating_cost": 1,
},
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,40 @@
{
"actions": [],
"allow_bulk_edit": 1,
"autoname": "naming_series:",
"creation": "2026-03-31 21:06:16.282931",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_smqo",
"job_card_dashboard",
"section_break_fsba",
"work_order",
"column_break_uqjq",
"production_item",
"column_break_qrpg",
"for_quantity",
"column_break_yecz",
"bom_no",
"section_break_oisd",
"company",
"naming_series",
"work_order",
"employee",
"column_break_4",
"posting_date",
"project",
"bom_no",
"is_subcontracted",
"semi_finished_good__finished_good_section",
"finished_good",
"production_item",
"semi_fg_bom",
"total_completed_qty",
"column_break_mcnb",
"for_quantity",
"transferred_qty",
"manufactured_qty",
"semi_fg_bom",
"section_break_folk",
"pending_qty",
"column_break_cyjw",
"process_loss_qty",
"total_completed_qty",
"section_break_wpjf",
"transferred_qty",
"column_break_lgte",
"manufactured_qty",
"production_section",
"operation",
"source_warehouse",
@@ -35,6 +45,7 @@
"workstation_type",
"workstation",
"target_warehouse",
"employee",
"section_break_8",
"items",
"quality_inspection_section",
@@ -71,8 +82,10 @@
"item_name",
"requested_qty",
"is_paused",
"is_subcontracted",
"track_semi_finished_goods",
"column_break_20",
"project",
"remarks",
"section_break_dfoc",
"status",
@@ -155,6 +168,7 @@
"fieldname": "wip_warehouse",
"fieldtype": "Link",
"label": "WIP Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"mandatory_depends_on": "eval:!doc.finished_good || doc.skip_material_transfer === 0 || (doc.skip_material_transfer && doc.backflush_from_wip_warehouse)",
"options": "Warehouse"
},
@@ -506,6 +520,7 @@
"fieldname": "target_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"mandatory_depends_on": "eval:doc.track_semi_finished_goods",
"options": "Warehouse"
},
@@ -518,6 +533,7 @@
"fieldname": "source_warehouse",
"fieldtype": "Link",
"label": "Source Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse"
},
{
@@ -622,12 +638,64 @@
"fieldname": "secondary_items_section",
"fieldtype": "Tab Break",
"label": "Secondary Items"
},
{
"fieldname": "section_break_folk",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "column_break_cyjw",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "pending_qty",
"fieldtype": "Float",
"label": "Pending Qty"
},
{
"fieldname": "section_break_wpjf",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_lgte",
"fieldtype": "Column Break"
},
{
"fieldname": "job_card_dashboard",
"fieldtype": "HTML"
},
{
"fieldname": "section_break_oisd",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_uqjq",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_qrpg",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_yecz",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_smqo",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "section_break_fsba",
"fieldtype": "Section Break"
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2026-03-31 21:06:48.987740",
"modified": "2026-05-21 18:37:05.688342",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",

View File

@@ -103,6 +103,7 @@ class JobCard(Document):
operation_id: DF.Data | None
operation_row_id: DF.Int
operation_row_number: DF.Literal[None]
pending_qty: DF.Float
posting_date: DF.Date | None
process_loss_qty: DF.Float
production_item: DF.Link | None
@@ -881,7 +882,9 @@ class JobCard(Document):
precision = self.precision("total_completed_qty")
total_completed_qty = flt(
flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision)
flt(self.total_completed_qty, precision)
+ flt(self.process_loss_qty, precision)
+ flt(self.pending_qty, precision)
)
if self.for_quantity and flt(total_completed_qty, precision) != flt(self.for_quantity, precision):
@@ -928,8 +931,10 @@ class JobCard(Document):
self.process_loss_qty = 0.0
if self.total_completed_qty and self.for_quantity > self.total_completed_qty:
self.process_loss_qty = flt(self.for_quantity, precision) - flt(
self.total_completed_qty, precision
self.process_loss_qty = (
flt(self.for_quantity, precision)
- flt(self.total_completed_qty, precision)
- flt(self.pending_qty, precision)
)
def update_work_order(self):
@@ -943,13 +948,14 @@ class JobCard(Document):
):
return
for_quantity, time_in_mins, process_loss_qty = 0, 0, 0
for_quantity, time_in_mins, process_loss_qty, pending_qty = 0, 0, 0, 0
data = self.get_current_operation_data()
if data and len(data) > 0:
for_quantity = flt(data[0].completed_qty)
time_in_mins = flt(data[0].time_in_mins)
process_loss_qty = flt(data[0].process_loss_qty)
pending_qty = flt(data[0].pending_qty)
wo = frappe.get_doc("Work Order", self.work_order)
@@ -957,8 +963,8 @@ class JobCard(Document):
self.update_corrective_in_work_order(wo)
elif self.operation_id:
self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
self.validate_produced_quantity(for_quantity, process_loss_qty, pending_qty, wo)
self.update_work_order_data(for_quantity, process_loss_qty, pending_qty, time_in_mins, wo)
def update_semi_finished_good_details(self):
if self.operation_id:
@@ -987,11 +993,11 @@ class JobCard(Document):
wo.flags.ignore_validate_update_after_submit = True
wo.save()
def validate_produced_quantity(self, for_quantity, process_loss_qty, wo):
def validate_produced_quantity(self, for_quantity, process_loss_qty, pending_qty, wo):
if self.docstatus < 2:
return
if wo.produced_qty > for_quantity + process_loss_qty:
if wo.produced_qty > for_quantity + process_loss_qty + pending_qty:
first_part_msg = _(
"The {0} {1} is used to calculate the valuation cost for the finished good {2}."
).format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item))
@@ -1004,7 +1010,7 @@ class JobCard(Document):
_("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error")
)
def update_work_order_data(self, for_quantity, process_loss_qty, time_in_mins, wo):
def update_work_order_data(self, for_quantity, process_loss_qty, pending_qty, time_in_mins, wo):
workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log")
@@ -1026,6 +1032,7 @@ class JobCard(Document):
if data.get("name") == self.operation_id:
data.completed_qty = for_quantity
data.process_loss_qty = process_loss_qty
data.pending_qty = pending_qty
data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None
data.actual_end_time = time_data[0].end_time if time_data else None
@@ -1051,6 +1058,7 @@ class JobCard(Document):
{"SUM": "total_time_in_mins", "as": "time_in_mins"},
{"SUM": "total_completed_qty", "as": "completed_qty"},
{"SUM": "process_loss_qty", "as": "process_loss_qty"},
{"SUM": "pending_qty", "as": "pending_qty"},
],
filters={
"docstatus": 1,
@@ -1445,10 +1453,19 @@ class JobCard(Document):
if isinstance(kwargs, dict):
kwargs = frappe._dict(kwargs)
if kwargs.end_time:
if kwargs.for_quantity:
self.for_quantity = kwargs.for_quantity
if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) < 0:
frappe.throw(_("Pending quantity cannot be negative."))
if flt(kwargs.process_loss_qty) and flt(kwargs.process_loss_qty) < 0:
frappe.throw(_("Process loss quantity cannot be negative."))
if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) > self.for_quantity:
frappe.throw(_("Pending quantity cannot be greater than the for quantity."))
self.pending_qty = flt(kwargs.pending_qty)
self.process_loss_qty = flt(kwargs.process_loss_qty)
if kwargs.end_time:
self.add_time_logs(
to_time=kwargs.end_time,
completed_qty=kwargs.qty,

View File

@@ -720,6 +720,7 @@ class TestJobCard(ERPNextTestSuite):
)
jc.time_logs[0].completed_qty = 8
jc.pending_qty = 0.0
jc.save()
jc.submit()
@@ -1080,6 +1081,243 @@ class TestJobCard(ERPNextTestSuite):
self.assertEqual(s.items[3].item_code, "_Test Item")
self.assertEqual(s.items[3].transfer_qty, 2)
@ERPNextTestSuite.change_settings(
"Manufacturing Settings", {"overproduction_percentage_for_work_order": 100}
)
def test_operating_cost_with_overproduction(self):
from erpnext.manufacturing.doctype.routing.test_routing import (
create_routing,
setup_bom,
setup_operations,
)
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_stock_entry_for_wo,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
workstation = make_workstation(
workstation_name="Test Workstation for Overproduction", hour_rate_rent=10, hour_rate_labour=10
)
operations = [
{"operation": "Test Operation 1", "workstation": workstation.name, "time_in_mins": 30},
{"operation": "Test Operation 2", "workstation": workstation.name, "time_in_mins": 30},
]
warehouse = create_warehouse("Test Warehouse for Overproduction")
setup_operations(operations)
fg = make_item("Test FG for Overproduction", {"stock_uom": "Nos", "is_stock_item": 1})
rm = make_item("Test RM for Overproduction", {"stock_uom": "Nos", "is_stock_item": 1})
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
bom_doc = setup_bom(
item_code=fg.name,
routing=routing_doc.name,
raw_materials=[rm.name],
source_warehouse=warehouse,
)
for row in bom_doc.items:
make_stock_entry(
item_code=row.item_code,
target=row.source_warehouse,
qty=100,
basic_rate=100,
)
wo_doc = make_wo_order_test_record(
production_item=fg.name,
bom_no=bom_doc.name,
qty=10,
skip_transfer=1,
source_warehouse=warehouse,
)
first_operation = frappe.get_all(
"Job Card",
filters={"work_order": wo_doc.name, "sequence_id": 1},
fields=["name"],
order_by="sequence_id",
limit=1,
)[0].name
jc = frappe.get_doc("Job Card", first_operation)
from_time = add_to_date(now(), days=1)
for _ in jc.scheduled_time_logs:
jc.append(
"time_logs",
{
"from_time": from_time,
"to_time": add_to_date(from_time, days=1),
"completed_qty": 4,
},
)
jc.for_quantity = 4
jc.save()
jc.submit()
second_operation = frappe.get_all(
"Job Card",
filters={"work_order": wo_doc.name, "sequence_id": 2},
fields=["name"],
order_by="sequence_id",
limit=1,
)[0].name
jc = frappe.get_doc("Job Card", second_operation)
from_time = add_to_date(now(), days=2)
for _ in jc.scheduled_time_logs:
jc.append(
"time_logs",
{
"from_time": from_time,
"to_time": add_to_date(from_time, days=2),
"completed_qty": 4,
},
)
jc.for_quantity = 4
jc.save()
jc.submit()
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6)) # overproduction
s.submit()
self.assertEqual(s.additional_costs[0].amount, 240)
self.assertEqual(s.additional_costs[1].amount, 240)
self.assertEqual(s.additional_costs[2].amount, 480)
self.assertEqual(s.additional_costs[3].amount, 480)
make_job_card(
wo_doc.name,
[
{
"name": wo_doc.operations[0].name,
"operation": "Test Operation 1",
"qty": 2,
"pending_qty": 2,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
from_time = add_to_date(now(), days=4)
job_card.append(
"time_logs",
{
"from_time": from_time,
"to_time": add_to_date(from_time, days=1),
"completed_qty": 2,
},
)
job_card.for_quantity = 2
job_card.save()
job_card.submit()
make_job_card(
wo_doc.name,
[
{
"name": wo_doc.operations[1].name,
"operation": "Test Operation 2",
"qty": 2,
"pending_qty": 2,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
from_time = add_to_date(now(), days=5)
job_card.append(
"time_logs",
{
"from_time": from_time,
"to_time": add_to_date(from_time, days=2),
"completed_qty": 2,
},
)
job_card.for_quantity = 2
job_card.save()
job_card.submit()
s2 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 1))
s2.submit()
self.assertEqual(s2.additional_costs[0].amount, 120)
self.assertEqual(s2.additional_costs[1].amount, 120)
self.assertEqual(s2.additional_costs[2].amount, 240)
self.assertEqual(s2.additional_costs[3].amount, 240)
make_job_card(
wo_doc.name,
[
{
"name": wo_doc.operations[0].name,
"operation": "Test Operation 1",
"qty": 2,
"pending_qty": 2,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
from_time = add_to_date(now(), days=7)
job_card.append(
"time_logs",
{
"from_time": from_time,
"to_time": add_to_date(from_time, days=1),
"completed_qty": 2,
},
)
job_card.for_quantity = 2
job_card.save()
job_card.submit()
make_job_card(
wo_doc.name,
[
{
"name": wo_doc.operations[1].name,
"operation": "Test Operation 2",
"qty": 2,
"pending_qty": 2,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
from_time = add_to_date(now(), days=8)
job_card.append(
"time_logs",
{
"from_time": from_time,
"to_time": add_to_date(from_time, days=2),
"completed_qty": 2,
},
)
job_card.for_quantity = 2
job_card.save()
job_card.submit()
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 2))
s.submit()
self.assertEqual(s.additional_costs[0].amount, 240)
self.assertEqual(s.additional_costs[1].amount, 240)
self.assertEqual(s.additional_costs[2].amount, 480)
self.assertEqual(s.additional_costs[3].amount, 480)
s2.cancel()
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 3))
s.submit()
self.assertEqual(s.additional_costs[0].amount, 240)
self.assertEqual(s.additional_costs[1].amount, 240)
self.assertEqual(s.additional_costs[2].amount, 480)
self.assertEqual(s.additional_costs[3].amount, 480)
def create_bom_with_multiple_operations():
"Create a BOM with multiple operations and Material Transfer against Job Card"

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"creation": "2018-07-09 17:20:44.737289",
"doctype": "DocType",
"editable_grid": 1,
@@ -34,6 +35,7 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Source Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse"
},
{
@@ -113,7 +115,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-12-04 14:30:19.472294",
"modified": "2026-05-12 12:22:18.506904",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Item",

View File

@@ -292,7 +292,7 @@ class MasterProductionSchedule(Document):
return item_wise_data
def add_mps_data(self, data):
data = frappe._dict(sorted(data.items(), key=lambda x: x[0][1]))
data = frappe._dict(sorted(data.items(), key=lambda x: x[0][1] or ""))
for key in data:
row = data[key]

View File

@@ -1315,6 +1315,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
bom.name.as_("main_bom"),
)
.where(
(bei.docstatus < 2)
@@ -1384,6 +1385,7 @@ def get_subitems(
item.purchase_uom,
item_uom.conversion_factor,
bom.item.as_("main_bom_item"),
bom.name.as_("main_bom"),
bom_item.is_phantom_item,
)
.where(

View File

@@ -47,11 +47,3 @@ frappe.ui.form.on("Sales Forecast", {
}
},
});
frappe.ui.form.on("Sales Forecast Item", {
adjust_qty(frm, cdt, cdn) {
let row = locals[cdt][cdn];
row.demand_qty = row.forecast_qty + row.adjust_qty;
frappe.model.set_value(cdt, cdn, "demand_qty", row.demand_qty);
},
});

View File

@@ -3,7 +3,7 @@ from frappe import _
def get_data():
return {
"fieldname": "demand_planning",
"fieldname": "sales_forecast",
"transactions": [
{
"label": _("MPS"),

View File

@@ -10,8 +10,6 @@
"item_name",
"uom",
"delivery_date",
"forecast_qty",
"adjust_qty",
"demand_qty",
"warehouse"
],
@@ -55,22 +53,6 @@
"label": "Delivery Date",
"read_only": 1
},
{
"columns": 2,
"fieldname": "forecast_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Forecast Qty",
"non_negative": 1,
"read_only": 1
},
{
"columns": 2,
"fieldname": "adjust_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Adjust Qty"
},
{
"columns": 3,
"fieldname": "demand_qty",
@@ -94,7 +76,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-18 21:59:38.859082",
"modified": "2026-05-21 12:38:47.636301",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Sales Forecast Item",

View File

@@ -14,10 +14,8 @@ class SalesForecastItem(Document):
if TYPE_CHECKING:
from frappe.types import DF
adjust_qty: DF.Float
delivery_date: DF.Date | None
demand_qty: DF.Float
forecast_qty: DF.Float
item_code: DF.Link
item_name: DF.Data | None
parent: DF.Data

View File

@@ -12,21 +12,10 @@ frappe.ui.form.on("Work Order", {
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
// Set query for warehouses
frm.set_query("wip_warehouse", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("source_warehouse", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.events.set_company_filters(frm, "wip_warehouse");
frm.events.set_company_filters(frm, "source_warehouse");
frm.events.set_company_filters(frm, "fg_warehouse");
frm.events.set_company_filters(frm, "scrap_warehouse");
frm.set_query("source_warehouse", "required_items", function () {
return {
@@ -44,24 +33,6 @@ frappe.ui.form.on("Work Order", {
};
});
frm.set_query("fg_warehouse", function () {
return {
filters: {
company: frm.doc.company,
is_group: 0,
},
};
});
frm.set_query("scrap_warehouse", function () {
return {
filters: {
company: frm.doc.company,
is_group: 0,
},
};
});
// Set query for BOM
frm.set_query("bom_no", function () {
if (frm.doc.production_item) {
@@ -118,6 +89,16 @@ frappe.ui.form.on("Work Order", {
});
},
set_company_filters(frm, fieldname) {
frm.set_query(fieldname, () => {
return {
filters: {
company: frm.doc.company,
},
};
});
},
onload: function (frm) {
if (!frm.doc.status) frm.doc.status = "Draft";
@@ -348,7 +329,7 @@ frappe.ui.form.on("Work Order", {
{
fieldtype: "Data",
fieldname: "name",
label: __("Operation Id"),
label: __("Operation ID"),
},
{
fieldtype: "Float",
@@ -425,6 +406,7 @@ frappe.ui.form.on("Work Order", {
if (pending_qty) {
dialog.fields_dict.operations.df.data.push({
__checked: 1,
name: data.name,
operation: data.operation,
workstation: data.workstation,

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2025-04-09 12:09:40.634472",
@@ -266,6 +267,7 @@
"fieldname": "wip_warehouse",
"fieldtype": "Link",
"label": "Work-in-Progress Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"mandatory_depends_on": "eval:(!doc.skip_transfer || doc.from_wip_warehouse) && !doc.track_semi_finished_goods",
"options": "Warehouse"
},
@@ -274,6 +276,7 @@
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse",
"read_only_depends_on": "subcontracting_inward_order"
},
@@ -286,6 +289,7 @@
"fieldname": "scrap_warehouse",
"fieldtype": "Link",
"label": "Scrap Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse"
},
{
@@ -513,6 +517,7 @@
"fieldname": "source_warehouse",
"fieldtype": "Link",
"label": "Source Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse",
"read_only_depends_on": "eval:doc.subcontracting_inward_order"
},
@@ -706,7 +711,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2026-04-17 13:42:12.374055",
"modified": "2026-05-19 12:20:38.102403",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",

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