Compare commits

...

144 Commits

Author SHA1 Message Date
Frappe PR Bot
d6b2fb2f96 chore(release): Bumped to Version 15.110.0
# [15.110.0](https://github.com/frappe/erpnext/compare/v15.109.3...v15.110.0) (2026-06-02)

### Bug Fixes

* billing address does not belongs to the company error ([5c392d6](5c392d6123))
* **book_appointment:** when scheduling is disabled, block API endpoints (backport [#55455](https://github.com/frappe/erpnext/issues/55455)) ([#55456](https://github.com/frappe/erpnext/issues/55456)) ([2a12ae1](2a12ae1afe))
* check perm for account (backport [#55479](https://github.com/frappe/erpnext/issues/55479)) ([#55482](https://github.com/frappe/erpnext/issues/55482)) ([1238aeb](1238aeb30a))
* **issue:** check permission before issue status modification (backport [#55458](https://github.com/frappe/erpnext/issues/55458)) ([#55459](https://github.com/frappe/erpnext/issues/55459)) ([338feb3](338feb31e1))
* **je:** preserve account on duplicate row when party row exists (backport [#55180](https://github.com/frappe/erpnext/issues/55180)) ([#55513](https://github.com/frappe/erpnext/issues/55513)) ([741216d](741216d3eb))
* **manufacturing:** allow to edit batch size while creating a work order ([#55332](https://github.com/frappe/erpnext/issues/55332)) ([41bf2f3](41bf2f32fd))
* material transfer in transit issue (backport [#55320](https://github.com/frappe/erpnext/issues/55320)) ([#55324](https://github.com/frappe/erpnext/issues/55324)) ([067c23f](067c23f20e))
* new bom version should not recalculate operations through routing (backport [#55370](https://github.com/frappe/erpnext/issues/55370)) ([#55371](https://github.com/frappe/erpnext/issues/55371)) ([4669ff2](4669ff295f))
* pick correct name when creating user from RFQ (backport [#55468](https://github.com/frappe/erpnext/issues/55468)) ([#55471](https://github.com/frappe/erpnext/issues/55471)) ([e429e60](e429e608c2))
* **pos:** escape html output in pos page templates (backport [#55527](https://github.com/frappe/erpnext/issues/55527)) ([#55528](https://github.com/frappe/erpnext/issues/55528)) ([689a3f5](689a3f50ae))
* **pos:** escape item data on pos item selector (backport [#55503](https://github.com/frappe/erpnext/issues/55503)) ([#55523](https://github.com/frappe/erpnext/issues/55523)) ([96bd97d](96bd97dd6d))
* **pos:** preserve contacts and enforce permissions in set_customer_info (backport [#55463](https://github.com/frappe/erpnext/issues/55463)) ([#55465](https://github.com/frappe/erpnext/issues/55465)) ([0353262](03532624b8))
* **ppr:** make default_advance_account optional ([aa94c3f](aa94c3ff22))
* **quotation:** made customer contact column visible (backport [#55433](https://github.com/frappe/erpnext/issues/55433)) ([#55434](https://github.com/frappe/erpnext/issues/55434)) ([a2d924c](a2d924c48f))
* **regional:** Japanese CT Rate (backport [#54998](https://github.com/frappe/erpnext/issues/54998)) ([#55437](https://github.com/frappe/erpnext/issues/55437)) ([2a52ea6](2a52ea6850))
* replace get_query with get_list for permission-aware queries in v15 ([ad511b8](ad511b80c0))
* stock reco for legacy serial nos ([93dcba4](93dcba40ec))
* **stock:** add warning message to notify the user to configure the inspection ([42e2fd5](42e2fd5fc9))
* **stock:** allow to create quality inspection after purchase/delivery ([10664b7](10664b7b95))
* **stock:** change qb to qb get_query to fix filter issues (backport [#55443](https://github.com/frappe/erpnext/issues/55443)) ([#55444](https://github.com/frappe/erpnext/issues/55444)) ([75d00ef](75d00ef173))
* **stock:** change valuation rate column label in stock ledger entry/report (backport [#55323](https://github.com/frappe/erpnext/issues/55323)) ([#55393](https://github.com/frappe/erpnext/issues/55393)) ([94fd15e](94fd15e550))
* **stock:** get_actual_qty during cancellations (backport [#55388](https://github.com/frappe/erpnext/issues/55388)) ([#55391](https://github.com/frappe/erpnext/issues/55391)) ([ad6e3a4](ad6e3a45d2))
* update default_advance_account type ([7200c22](7200c22890))
* use get_query instead of get_all for data fetching ([264433b](264433b23d))

### Features

* **payment-entry:** warn user before cancelling reconciled payment entry ([87c6ad4](87c6ad4f85))
2026-06-02 16:55:43 +00:00
Mihir Kandoi
a6b7142c18 Merge pull request #55546 from frappe/version-15-hotfix 2026-06-02 22:24:03 +05:30
Khushi Rawat
6796617921 Merge pull request #55542 from frappe/mergify/bp/version-15-hotfix/pr-55539
feat(payment-entry): warn user before cancelling reconciled payment entry (backport #55539)
2026-06-02 17:09:59 +05:30
ruthra kumar
c65d768020 Merge pull request #55544 from frappe/mergify/bp/version-15-hotfix/pr-54979
fix(ppr): make default_advance_account optional (backport #54979)
2026-06-02 16:48:42 +05:30
Dany Robert
7200c22890 fix: update default_advance_account type
(cherry picked from commit 30b9e11303)
2026-06-02 10:06:40 +00:00
Dany Robert
aa94c3ff22 fix(ppr): make default_advance_account optional
(cherry picked from commit 4b1d369ac6)
2026-06-02 10:06:39 +00:00
khushi8112
87c6ad4f85 feat(payment-entry): warn user before cancelling reconciled payment entry
(cherry picked from commit f0ba54d957)
2026-06-02 10:00:11 +00:00
Khushi Rawat
35b4ada3e2 Merge pull request #55358 from frappe/mergify/bp/version-15-hotfix/pr-55137
fix: use get_query instead of get_all for data fetching (backport #55137)
2026-06-02 13:19:11 +05:30
khushi8112
ad511b80c0 fix: replace get_query with get_list for permission-aware queries in v15 2026-06-02 12:17:37 +05:30
rohitwaghchaure
ad55c7c372 Merge pull request #55518 from frappe/mergify/bp/version-15-hotfix/pr-55415
fix(stock): allow to create quality inspection after purchase/delivery (backport #55415)
2026-06-02 11:52:05 +05:30
Diptanil Saha
689a3f50ae fix(pos): escape html output in pos page templates (backport #55527) (#55528)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 00:42:59 +05:30
Diptanil Saha
96bd97dd6d fix(pos): escape item data on pos item selector (backport #55503) (#55523) 2026-06-01 22:17:44 +05:30
rohitwaghchaure
897722c35f chore: fixed conflicts 2026-06-01 22:14:46 +05:30
rohitwaghchaure
54cbc91166 chore: fixed conflicts 2026-06-01 22:11:58 +05:30
mergify[bot]
ecf9aa146c chore(serial_and_batch_bundle): remove update_serial_or_batch method (backport #55481) (#55515)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-06-01 16:40:41 +00:00
Sudharsanan11
42e2fd5fc9 fix(stock): add warning message to notify the user to configure the inspection
(cherry picked from commit e003fe4de0)

# Conflicts:
#	erpnext/public/js/controllers/transaction.js
2026-06-01 16:13:51 +00:00
Sudharsanan11
10664b7b95 fix(stock): allow to create quality inspection after purchase/delivery
(cherry picked from commit c6a88ab1d2)

# Conflicts:
#	erpnext/controllers/stock_controller.py
#	erpnext/public/js/controllers/transaction.js
2026-06-01 16:13:50 +00:00
ruthra kumar
1980307048 Merge pull request #55514 from ruthra-kumar/remove_flaky_process_pcv_test
refactor(test): remove flaky test in process pcv
2026-06-01 19:35:07 +05:30
Frappe PR Bot
13eeddd1f6 chore(release): Bumped to Version 15.109.3
## [15.109.3](https://github.com/frappe/erpnext/compare/v15.109.2...v15.109.3) (2026-06-01)

### Bug Fixes

* only consider non-opening balance for Balance sheet accounts ([4a6af25](4a6af25d11))
2026-06-01 14:01:43 +00:00
ruthra kumar
dc08b615f1 Merge pull request #55501 from frappe/mergify/bp/version-15/pr-55495
fix: opening bal double counting in Process Period Closing Voucher (backport #55495)
2026-06-01 19:30:03 +05:30
ruthra kumar
ce94f4fd11 refactor(test): remove flaky test in process pcv 2026-06-01 19:15:50 +05:30
ruthra kumar
e314d0cfc5 refactor: color coded status in list view
(cherry picked from commit cfeffbb354)
2026-06-01 19:13:36 +05:30
mergify[bot]
741216d3eb fix(je): preserve account on duplicate row when party row exists (backport #55180) (#55513)
fix(je): preserve account on duplicate row when party row exists (#55180)

(cherry picked from commit 57dbac712f)

Co-authored-by: Gajendra Nishad <75714258+gajjug004@users.noreply.github.com>
2026-06-01 18:46:21 +05:30
ruthra kumar
94e15ae9ef refactor: tabbed view for process period closing voucher
(cherry picked from commit 1960c81619)
2026-06-01 18:09:46 +05:30
ruthra kumar
4a6af25d11 fix: only consider non-opening balance for Balance sheet accounts
(cherry picked from commit a2b8334046)
2026-06-01 18:09:41 +05:30
ruthra kumar
c7fbc133e6 Merge pull request #55498 from frappe/mergify/bp/version-15-hotfix/pr-55495
fix: opening bal double counting in Process Period Closing Voucher (backport #55495)
2026-06-01 17:50:32 +05:30
ruthra kumar
e5aa45cf0d test: prevent double counting of opening balances
(cherry picked from commit 7f2af123ee)
2026-06-01 17:50:10 +05:30
ruthra kumar
3e3689d938 refactor: color coded status in list view
(cherry picked from commit cfeffbb354)
2026-06-01 15:10:00 +05:30
ruthra kumar
d0fc3f029f refactor: tabbed view for process period closing voucher
(cherry picked from commit 1960c81619)
2026-06-01 15:09:57 +05:30
ruthra kumar
a9cfa22199 refactor: only consider non-opening balance for Balance sheet accounts
(cherry picked from commit a2b8334046)
2026-06-01 09:32:45 +00:00
mergify[bot]
1238aeb30a fix: check perm for account (backport #55479) (#55482)
fix: check perm for account (#55479)

(cherry picked from commit dd1d2925d5)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2026-06-01 14:29:35 +05:30
Frappe PR Bot
1c5220b86f chore(release): Bumped to Version 15.109.2
## [15.109.2](https://github.com/frappe/erpnext/compare/v15.109.1...v15.109.2) (2026-06-01)

### Bug Fixes

* billing address does not belongs to the company error ([c2063c4](c2063c4707))
2026-06-01 06:08:30 +00:00
rohitwaghchaure
779f1b6104 Merge pull request #55474 from frappe/mergify/bp/version-15/pr-55424
fix: billing address does not belongs to the company error (backport #55417) (backport #55424)
2026-06-01 11:36:58 +05:30
Rohit Waghchaure
c2063c4707 fix: billing address does not belongs to the company error
(cherry picked from commit 9df07b367a)
(cherry picked from commit 5c392d6123)
2026-06-01 06:01:45 +00:00
mergify[bot]
e429e608c2 fix: pick correct name when creating user from RFQ (backport #55468) (#55471)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: pick correct name when creating user from RFQ (#55468)
2026-06-01 05:52:40 +00:00
mergify[bot]
75d00ef173 fix(stock): change qb to qb get_query to fix filter issues (backport #55443) (#55444)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): change qb to qb get_query to fix filter issues (#55443)
2026-06-01 05:33:25 +00:00
mergify[bot]
94fd15e550 fix(stock): change valuation rate column label in stock ledger entry/report (backport #55323) (#55393)
Co-authored-by: Sudharsanan11 <sudharsananashok1975@gmail.com>
2026-06-01 05:10:27 +00:00
mergify[bot]
03532624b8 fix(pos): preserve contacts and enforce permissions in set_customer_info (backport #55463) (#55465)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(pos): preserve contacts and enforce permissions in set_customer_info (#55463)
2026-06-01 05:18:34 +05:30
mergify[bot]
338feb31e1 fix(issue): check permission before issue status modification (backport #55458) (#55459)
* fix(issue): check permission before issue status modification (#55458)

(cherry picked from commit 876f403500)

# Conflicts:
#	erpnext/support/doctype/issue/issue.py

* chore: resolve conflicts

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-05-31 19:06:01 +00:00
mergify[bot]
2a805e090c refactor: task_info portal pages (backport #55448) (#55453)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-05-31 22:07:01 +05:30
mergify[bot]
2a12ae1afe fix(book_appointment): when scheduling is disabled, block API endpoints (backport #55455) (#55456)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(book_appointment): when scheduling is disabled, block API endpoints (#55455)
2026-05-31 16:06:25 +00:00
mergify[bot]
715ca39abc refactor(pos_profile): migrating raw sql to qb in set_defaults (backport #55447) (#55449)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-05-31 09:40:26 +00:00
Raffael Meyer
cad14ac3e6 chore: mark as out of beta (backport #55439) (#55440) 2026-05-30 19:09:45 +00:00
mergify[bot]
2a52ea6850 fix(regional): Japanese CT Rate (backport #54998) (#55437)
Co-authored-by: mh35 <mh35jp@gmail.com>
fix(regional): Japanese CT Rate (#54998)
2026-05-30 22:00:19 +05:30
mergify[bot]
a2d924c48f fix(quotation): made customer contact column visible (backport #55433) (#55434)
* fix(quotation): made customer contact column visible (#55433)

(cherry picked from commit 9758eb868d)

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

* chore: resolved conflicts

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-05-30 14:20:40 +00:00
rohitwaghchaure
3ad39c987b Merge pull request #55424 from frappe/mergify/bp/version-15-hotfix/pr-55417
fix: billing address does not belongs to the company error (backport #55417)
2026-05-30 12:43:31 +05:30
Rohit Waghchaure
5c392d6123 fix: billing address does not belongs to the company error
(cherry picked from commit 9df07b367a)
2026-05-29 17:23:55 +00:00
Frappe PR Bot
9e7b03173d chore(release): Bumped to Version 15.109.1
## [15.109.1](https://github.com/frappe/erpnext/compare/v15.109.0...v15.109.1) (2026-05-29)

### Bug Fixes

* material transfer in transit issue (backport [#55320](https://github.com/frappe/erpnext/issues/55320)) (backport [#55324](https://github.com/frappe/erpnext/issues/55324)) ([#55404](https://github.com/frappe/erpnext/issues/55404)) ([bfdf1e4](bfdf1e43f9))
2026-05-29 12:07:01 +00:00
mergify[bot]
bfdf1e43f9 fix: material transfer in transit issue (backport #55320) (backport #55324) (#55404)
Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
fix: material transfer in transit issue (backport #55320) (#55324)
2026-05-29 17:35:20 +05:30
mergify[bot]
ad6e3a45d2 fix(stock): get_actual_qty during cancellations (backport #55388) (#55391)
Co-authored-by: archielister <archie.lister@lush.co.uk>
fix(stock): get_actual_qty during cancellations (#55388)
2026-05-28 22:40:11 +05:30
mergify[bot]
4669ff295f fix: new bom version should not recalculate operations through routing (backport #55370) (#55371)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: new bom version should not recalculate operations through routing (#55370)
2026-05-28 16:01:36 +05:30
khushi8112
264433b23d fix: use get_query instead of get_all for data fetching
(cherry picked from commit 1fd99337b3)
2026-05-27 19:17:02 +00:00
Pandiyan P
41bf2f32fd fix(manufacturing): allow to edit batch size while creating a work order (#55332) 2026-05-27 18:39:59 +05:30
rohitwaghchaure
a6d4bc5c86 Merge pull request #55298 from frappe/mergify/bp/version-15-hotfix/pr-55242
fix: stock reco for legacy serial nos (backport #55242)
2026-05-27 12:15:19 +05:30
Rohit Waghchaure
93dcba40ec fix: stock reco for legacy serial nos
(cherry picked from commit 9d5fd11bcd)
2026-05-27 11:50:30 +05:30
mergify[bot]
067c23f20e fix: material transfer in transit issue (backport #55320) (#55324)
Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-27 11:45:23 +05:30
Frappe PR Bot
16fbf8299f chore(release): Bumped to Version 15.109.0
# [15.109.0](https://github.com/frappe/erpnext/compare/v15.108.3...v15.109.0) (2026-05-27)

### Bug Fixes

* consider batchwise valuation in stock ageing report (backport [#54919](https://github.com/frappe/erpnext/issues/54919)) ([#55229](https://github.com/frappe/erpnext/issues/55229)) ([418a7fb](418a7fb301))
* consumed operation cost calculation (backport [#54858](https://github.com/frappe/erpnext/issues/54858)) ([#55132](https://github.com/frappe/erpnext/issues/55132)) ([46d5395](46d5395148))
* default use_for_shopping_cart to 0 in set_taxes ([960be3e](960be3e081))
* edit stock uom qty for purchase documents (backport [#55135](https://github.com/frappe/erpnext/issues/55135)) ([#55178](https://github.com/frappe/erpnext/issues/55178)) ([425e6c5](425e6c52f4))
* ERPNextTestSuite to change_settings ([76078a7](76078a7fb9))
* faster range calculation on process period closing voucher ([bf27f2d](bf27f2d869))
* fg valuation rate in repack entry when multiple FGs ([238f168](238f1685f1))
* **general-ledger:** show raw GL entries when categorize_by is empty (backport [#54816](https://github.com/frappe/erpnext/issues/54816)) ([#54829](https://github.com/frappe/erpnext/issues/54829)) ([b972b7c](b972b7c307))
* import change_settings ([9d21199](9d211990c3))
* inclusive tax amount not considered while setting LCV from purchase invoice ([cba4c9f](cba4c9f0ee))
* incoming rate for legacy serial no ([6e6ef83](6e6ef83d60))
* incorrect error message string in sales order (backport [#55090](https://github.com/frappe/erpnext/issues/55090)) ([#55094](https://github.com/frappe/erpnext/issues/55094)) ([04e28f9](04e28f9556))
* invalid filter on item_group (backport [#55186](https://github.com/frappe/erpnext/issues/55186)) ([#55187](https://github.com/frappe/erpnext/issues/55187)) ([25739ae](25739ae217))
* merge conflicts ([59e9f51](59e9f5192c))
* **payment_entry:** sync paid/received amounts for cross-currency entries (backport [#55270](https://github.com/frappe/erpnext/issues/55270)) ([#55271](https://github.com/frappe/erpnext/issues/55271)) ([d31a051](d31a051c74))
* prevent AttributeError in batch query filters (backport [#55257](https://github.com/frappe/erpnext/issues/55257)) ([#55278](https://github.com/frappe/erpnext/issues/55278)) ([4f89f3a](4f89f3a856))
* **project:** update customer and sales order as no copy ([9145760](914576040e))
* removed redundant code ([259f499](259f499e25))
* set bin details when adding item using update items (backport [#55096](https://github.com/frappe/erpnext/issues/55096)) ([#55097](https://github.com/frappe/erpnext/issues/55097)) ([aa79247](aa79247c39))
* single variant creation error (backport [#55286](https://github.com/frappe/erpnext/issues/55286)) ([#55288](https://github.com/frappe/erpnext/issues/55288)) ([937eb87](937eb87932))
* **stock:** apply posting datetime filters while fetching available batches (backport [#54976](https://github.com/frappe/erpnext/issues/54976)) ([#55184](https://github.com/frappe/erpnext/issues/55184)) ([ff442cd](ff442cd8e7))
* **stock:** remove precision for valuation rate while creating sle (backport [#55249](https://github.com/frappe/erpnext/issues/55249)) ([#55259](https://github.com/frappe/erpnext/issues/55259)) ([8b241b4](8b241b45e2))
* **stock:** remove recalculate current qty function ([#55121](https://github.com/frappe/erpnext/issues/55121)) ([1c90c3b](1c90c3bbc2))
* update import ([31c251d](31c251d956))
* use passed posting date in make_reverse_gl_entries ([4436585](4436585aa0))

### Features

* add get_parent_supplier_groups using query builder ([6517ed7](6517ed72b4))

### Performance Improvements

* skip delink_original_entry during cancellation when Immutable Ledger is enabled ([#55130](https://github.com/frappe/erpnext/issues/55130)) ([034e159](034e159ee4))
2026-05-27 00:23:04 +00:00
Diptanil Saha
7ce7e3d5e5 Merge pull request #55316 from frappe/version-15-hotfix
chore: release v15
2026-05-27 05:51:33 +05:30
Diptanil Saha
60fdc6bc1a Merge branch 'version-15' into version-15-hotfix 2026-05-27 05:31:52 +05:30
mergify[bot]
b972b7c307 fix(general-ledger): show raw GL entries when categorize_by is empty (backport #54816) (#54829)
fix(general-ledger): show raw GL entries when categorize_by is empty (#54816)

(cherry picked from commit dfbe847307)

# Conflicts:
#	erpnext/accounts/report/general_ledger/general_ledger.py

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-05-26 23:34:47 +00:00
Nihantra C. Patel
4927d346c8 Merge pull request #55294 from frappe/mergify/bp/version-15-hotfix/pr-55268
fix: use passed posting date for period closing validation in reverse GL entries (backport #55268)
2026-05-26 23:13:14 +05:30
Nihantra Patel
8f164cff1d test: immutable ledger reverse entry 2026-05-26 22:53:14 +05:30
Nihantra C. Patel
31c251d956 fix: update import 2026-05-26 22:17:23 +05:30
rohitwaghchaure
bc81992a40 Merge pull request #55296 from frappe/mergify/bp/version-15-hotfix/pr-55290
fix: inclusive tax amount not considered while setting LCV from purchase invoice (backport #55290)
2026-05-26 16:23:13 +05:30
rohitwaghchaure
66267cf99a chore: fix conflicts 2026-05-26 15:57:24 +05:30
Nihantra C. Patel
9d211990c3 fix: import change_settings 2026-05-26 15:56:38 +05:30
Nihantra C. Patel
76078a7fb9 fix: ERPNextTestSuite to change_settings 2026-05-26 15:48:41 +05:30
Rohit Waghchaure
cba4c9f0ee fix: inclusive tax amount not considered while setting LCV from purchase invoice
(cherry picked from commit 048ddfc265)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
2026-05-26 10:15:01 +00:00
mergify[bot]
46d5395148 fix: consumed operation cost calculation (backport #54858) (#55132)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: consumed operation cost calculation (#54858)
2026-05-26 10:14:19 +00:00
Nihantra Patel
b8b2141e20 test: update testcase
(cherry picked from commit 9c39b01f1c)
2026-05-26 10:13:33 +00:00
Nihantra Patel
4436585aa0 fix: use passed posting date in make_reverse_gl_entries
(cherry picked from commit f040bdf165)
2026-05-26 10:13:33 +00:00
mergify[bot]
937eb87932 fix: single variant creation error (backport #55286) (#55288)
* fix: single variant creation error

(cherry picked from commit bda75135c3)

* feat: allow creation of any number of variants in multiple item variant creation dialog

(cherry picked from commit 090c25d848)

# Conflicts:
#	erpnext/controllers/item_variant.py

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-26 08:25:21 +00:00
ruthra kumar
6a21d28030 Merge pull request #55280 from frappe/mergify/bp/version-15-hotfix/pr-55256
refactor: handle processes stuck in running state in process pcv (backport #55256)
2026-05-26 10:52:25 +05:30
mergify[bot]
4f89f3a856 fix: prevent AttributeError in batch query filters (backport #55257) (#55278)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix: prevent AttributeError in batch query filters (#55257)
2026-05-26 05:11:11 +00:00
mergify[bot]
8b241b45e2 fix(stock): remove precision for valuation rate while creating sle (backport #55249) (#55259)
Co-authored-by: Sudharsanan11 <sudharsananashok1975@gmail.com>
2026-05-26 10:29:27 +05:30
ruthra kumar
b517f26085 refactor: atomic summarization step for process pcv
(cherry picked from commit 6cb7971342)
2026-05-26 04:57:58 +00:00
ruthra kumar
f28b948e1b refactor: handle processes stuck in running state in process pcv
(cherry picked from commit f414778486)
2026-05-26 04:57:57 +00:00
ruthra kumar
a797ab3482 refactor: summarize in background
(cherry picked from commit 1c3a9f7dd9)
2026-05-26 04:57:57 +00:00
mergify[bot]
d31a051c74 fix(payment_entry): sync paid/received amounts for cross-currency entries (backport #55270) (#55271)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): sync paid/received amounts for cross-currency entries (#55270)
2026-05-25 23:21:43 +05:30
rohitwaghchaure
aad270914a Merge pull request #55243 from frappe/mergify/bp/version-15-hotfix/pr-55216
fix: fg valuation rate in repack entry when multiple FGs (backport #55216)
2026-05-25 15:34:30 +05:30
mergify[bot]
af3e7f53ac refactor: stock ageing report (backport #55231) (#55236)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-25 12:44:55 +05:30
Rohit Waghchaure
238f1685f1 fix: fg valuation rate in repack entry when multiple FGs
(cherry picked from commit a47e4c04f7)
2026-05-25 06:15:07 +00:00
mergify[bot]
418a7fb301 fix: consider batchwise valuation in stock ageing report (backport #54919) (#55229)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-24 09:48:12 +00:00
Nishka Gosalia
304474d2f7 Merge pull request #55195 from frappe/mergify/bp/version-15-hotfix/pr-55189
fix(project): update customer and sales order as no copy (backport #55189)
2026-05-23 16:00:12 +05:30
Nishka Gosalia
59e9f5192c fix: merge conflicts 2026-05-23 15:35:19 +05:30
nareshkannasln
914576040e fix(project): update customer and sales order as no copy
(cherry picked from commit 9d8f3863f2)

# Conflicts:
#	erpnext/projects/doctype/project/project.json
2026-05-22 12:21:04 +00:00
mergify[bot]
ff442cd8e7 fix(stock): apply posting datetime filters while fetching available batches (backport #54976) (#55184)
fix(stock): apply posting datetime filters while fetching available batches

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-22 11:34:43 +00:00
mergify[bot]
25739ae217 fix: invalid filter on item_group (backport #55186) (#55187)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: invalid filter on item_group (#55186)
2026-05-22 16:49:10 +05:30
mergify[bot]
425e6c52f4 fix: edit stock uom qty for purchase documents (backport #55135) (#55178)
Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
fix: edit stock uom qty for purchase documents (#55135)
2026-05-22 09:12:19 +00:00
ruthra kumar
97d2152a36 Merge pull request #55165 from frappe/mergify/bp/version-15-hotfix/pr-55130
perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled (backport #55130)
2026-05-22 14:26:00 +05:30
Nihantra C. Patel
034e159ee4 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/doctype/payment_ledger_entry/test_payment_ledger_entry.py
#	erpnext/accounts/general_ledger.py
2026-05-22 12:52:49 +05:30
diptanilsaha
fff023bf7b Merge pull request #55143 from frappe/mergify/bp/version-15-hotfix/pr-55127
refactor: migrate get_tax_template to query builder with hierarchical group matching (backport #55127)
2026-05-22 02:32:22 +05:30
mergify[bot]
429e02e6f9 chore: migrate Address/Contact custom fields from JSON fixtures to install (backport #55084) (#55087)
fixtures to install (backport #55084)
2026-05-21 20:41:36 +00:00
diptanilsaha
eb96f0429f 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-22 02:08:38 +05:30
diptanilsaha
960be3e081 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-22 02:08:38 +05:30
diptanilsaha
2a91c7229a 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-22 02:08:26 +05:30
diptanilsaha
6517ed72b4 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:35:54 +00:00
diptanilsaha
c125d1489c 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:35:54 +00:00
rohitwaghchaure
be1f1e8781 Merge pull request #55138 from frappe/mergify/bp/version-15-hotfix/pr-55134
fix: removed redundant code (backport #55134)
2026-05-21 16:17:03 +05:30
Rohit Waghchaure
259f499e25 fix: removed redundant code
(cherry picked from commit 14b17cd8a6)
2026-05-21 09:55:19 +00:00
rohitwaghchaure
fc05c38b9b Merge pull request #54977 from frappe/mergify/bp/version-15-hotfix/pr-54962
fix: incoming rate for legacy serial no (backport #54962)
2026-05-21 15:00:09 +05:30
rohitwaghchaure
da8d25d80a chore: fix linters issue
Added a setting to control fetching incoming rates for serial numbers.
2026-05-21 14:38:05 +05:30
rohitwaghchaure
6981599103 chore: fix conflicts
Removed fields related to parallel reposting and column breaks, and updated the modified date.
2026-05-21 14:31:22 +05:30
rohitwaghchaure
5557e982bf chore: fix conflicts
Removed legacy fields related to incoming rate and parallel reposting.
2026-05-21 14:30:26 +05:30
rohitwaghchaure
08466218d8 chore: fix conflicts
Removed legacy code for fetching incoming rates from serial numbers.
2026-05-21 14:28:29 +05:30
Pandiyan P
1c90c3bbc2 fix(stock): remove recalculate current qty function (#55121) 2026-05-21 06:11:41 +00:00
mergify[bot]
aa79247c39 fix: set bin details when adding item using update items (backport #55096) (#55097)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: set bin details when adding item using update items (#55096)
2026-05-20 16:21:25 +05:30
mergify[bot]
04e28f9556 fix: incorrect error message string in sales order (backport #55090) (#55094)
Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com>
fix: incorrect error message string in sales order (#55090)
2026-05-20 09:31:21 +00:00
Frappe PR Bot
519e409c1d chore(release): Bumped to Version 15.108.3
## [15.108.3](https://github.com/frappe/erpnext/compare/v15.108.2...v15.108.3) (2026-05-20)

### Bug Fixes

* faster range calculation on process period closing voucher ([5bd8132](5bd8132630))
2026-05-20 07:21:47 +00:00
ruthra kumar
3b748abfe5 Merge pull request #55081 from frappe/mergify/bp/version-15/pr-55072
perf: faster opening balance range calculation in process period closing voucher (backport #55072)
2026-05-20 12:50:17 +05:30
ruthra kumar
57d1f27e84 refactor: ppcv select with for update and skip locked
(cherry picked from commit eba58b2837)
2026-05-20 06:50:32 +00:00
ruthra kumar
5bd8132630 fix: faster range calculation on process period closing voucher
(cherry picked from commit ee33574a6d)
2026-05-20 06:50:31 +00:00
ruthra kumar
d666871d86 Merge pull request #55077 from frappe/mergify/bp/version-15-hotfix/pr-55072
perf: faster opening balance range calculation in process period closing voucher (backport #55072)
2026-05-20 12:08:30 +05:30
ruthra kumar
d81b6ab5dc refactor: ppcv select with for update and skip locked
(cherry picked from commit eba58b2837)
2026-05-20 06:18:41 +00:00
ruthra kumar
bf27f2d869 fix: faster range calculation on process period closing voucher
(cherry picked from commit ee33574a6d)
2026-05-20 06:18:41 +00:00
Frappe PR Bot
cf337824e7 chore(release): Bumped to Version 15.108.2
## [15.108.2](https://github.com/frappe/erpnext/compare/v15.108.1...v15.108.2) (2026-05-20)

### Bug Fixes

* add warehouse vaildation for repack entry (backport [#54866](https://github.com/frappe/erpnext/issues/54866)) ([#54900](https://github.com/frappe/erpnext/issues/54900)) ([b969662](b969662b6c))
* item leaderboard uses Sales/Purchase Invoice instead of Orders ([#55038](https://github.com/frappe/erpnext/issues/55038)) ([0b41df5](0b41df5ac8))
* merge conflicts ([8512eb4](8512eb4493))
* normalize date comparison to avoid datatype mismatch ([49b4830](49b4830785))
* **patch:** drop dead procedures first before other changes ([67d6761](67d67616ca))
* **payment_entry:** fix paid/received amount calculation for multi-currency accounts (backport [#54963](https://github.com/frappe/erpnext/issues/54963)) ([#54969](https://github.com/frappe/erpnext/issues/54969)) ([651af67](651af67b26))
* remove sql procedure method from AR report ([c705a93](c705a93776))
* stock balance showing incorrect value because of incorrect SLE ([dbacfd1](dbacfd13b8))
* **stock:** add whole number quantity validation in Stock Reconciliation (backport [#54922](https://github.com/frappe/erpnext/issues/54922)) ([#54924](https://github.com/frappe/erpnext/issues/54924)) ([48ed078](48ed07816d))
* **stock:** update buying amount calculation in gross profit report (backport [#55020](https://github.com/frappe/erpnext/issues/55020)) ([#55023](https://github.com/frappe/erpnext/issues/55023)) ([5e1880f](5e1880f09e))
* toast message for item price insert ([#55009](https://github.com/frappe/erpnext/issues/55009)) ([9309aec](9309aec209))
* validate company region in uae vat 201 (backport [#54899](https://github.com/frappe/erpnext/issues/54899)) ([#55054](https://github.com/frappe/erpnext/issues/55054)) ([5ad80b8](5ad80b8fb9))

### Reverts

* Revert "fix: debit credit not equal in purchase transactions for mult… (backport [#54906](https://github.com/frappe/erpnext/issues/54906)) ([#54907](https://github.com/frappe/erpnext/issues/54907)) ([6d3cd7d](6d3cd7d38a))
2026-05-20 04:10:36 +00:00
diptanilsaha
a4bdbec8f8 Merge pull request #55052 from frappe/version-15-hotfix
chore: release v15
2026-05-20 09:38:58 +05:30
Nabin Hait
0b41df5ac8 fix: item leaderboard uses Sales/Purchase Invoice instead of Orders (#55038)
Fixes https://github.com/frappe/erpnext/issues/46657
2026-05-20 00:49:39 +05:30
ruthra kumar
08f4437902 Merge pull request #55056 from frappe/mergify/bp/version-15-hotfix/pr-55053
fix(patch): drop dead procedures first before other changes (backport #55053)
2026-05-19 17:01:32 +05:30
mergify[bot]
5ad80b8fb9 fix: validate company region in uae vat 201 (backport #54899) (#55054)
Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
fix: validate company region in uae vat 201 (#54899)
2026-05-19 16:51:59 +05:30
ruthra kumar
67d67616ca fix(patch): drop dead procedures first before other changes
(cherry picked from commit 61d24ba55f)
2026-05-19 11:08:24 +00:00
rohitwaghchaure
1983204112 Merge pull request #55047 from frappe/mergify/bp/version-15-hotfix/pr-55046
fix: stock balance showing incorrect value because of incorrect SLE (backport #55046)
2026-05-19 14:14:47 +05:30
Rohit Waghchaure
dbacfd13b8 fix: stock balance showing incorrect value because of incorrect SLE
(cherry picked from commit 94b95d6c2f)
2026-05-19 08:22:09 +00:00
Ravibharathi
3b3e33d354 Merge pull request #55041 from frappe/mergify/bp/version-15-hotfix/pr-54761
fix: normalize date comparison to avoid datatype mismatch (backport #54761)
2026-05-19 11:53:32 +05:30
ervishnucs
49b4830785 fix: normalize date comparison to avoid datatype mismatch
(cherry picked from commit 01e382b106)

# Conflicts:
#	erpnext/accounts/party.py
2026-05-19 11:38:21 +05:30
mergify[bot]
5e1880f09e fix(stock): update buying amount calculation in gross profit report (backport #55020) (#55023)
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:37 +05:30
ruthra kumar
470bf628c7 Merge pull request #55014 from frappe/mergify/bp/version-15-hotfix/pr-55001
fix: remove sql procedure method from AR report (backport #55001)
2026-05-18 14:05:49 +05:30
ruthra kumar
c705a93776 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/accounts/report/accounts_receivable/accounts_receivable.py
#	erpnext/patches.txt
2026-05-18 13:49:25 +05:30
Nishka Gosalia
9ef783cd48 Merge pull request #55010 from frappe/mergify/bp/version-15-hotfix/pr-55009
fix: toast message for item price insert (backport #55009)
2026-05-18 12:04:48 +05:30
Nishka Gosalia
8512eb4493 fix: merge conflicts 2026-05-18 11:44:57 +05:30
Nishka Gosalia
9309aec209 fix: toast message for item price insert (#55009)
(cherry picked from commit ae9c632e39)

# Conflicts:
#	erpnext/stock/get_item_details.py
2026-05-18 06:11:11 +00:00
ruthra kumar
d02314935d Merge pull request #55003 from frappe/mergify/bp/version-15-hotfix/pr-51197
refactor: remove custom sql function in AR SQL procedure approach (backport #51197)
2026-05-18 11:11:54 +05:30
ruthra kumar
f5d83599cc refactor: remove custom sql function in AR SQL procedure approach
(cherry picked from commit d1e0992253)

# Conflicts:
#	erpnext/accounts/report/accounts_receivable/accounts_receivable.py
2026-05-18 10:55:04 +05:30
Rohit Waghchaure
6e6ef83d60 fix: incoming rate for legacy serial no
(cherry picked from commit 2773b7c002)

# Conflicts:
#	erpnext/stock/deprecated_serial_batch.py
#	erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
#	erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
2026-05-15 16:40:08 +00:00
mergify[bot]
651af67b26 fix(payment_entry): fix paid/received amount calculation for multi-currency accounts (backport #54963) (#54969)
* fix(payment_entry): `paid_amount` and `received_amount` calculation depending upon `account_currency`

(cherry picked from commit 69642860ee)

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

* chore: resolve conflicts

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-15 10:33:07 +00:00
ruthra kumar
47c6bc4b91 Merge pull request #54959 from frappe/mergify/bp/version-15/pr-54941
fix: flag to disable opening balance calculation in general ledger (backport #54941)
2026-05-15 13:43:24 +05:30
ruthra kumar
f037ee6501 refactor: flag to disable opening balance calculation
(cherry picked from commit 28a2230d02)
2026-05-15 07:32:24 +00:00
ruthra kumar
2b2eb2fa27 Merge pull request #54956 from frappe/mergify/bp/version-15-hotfix/pr-54941
fix: flag to disable opening balance calculation in general ledger (backport #54941)
2026-05-15 13:01:01 +05:30
ruthra kumar
d1d4480187 refactor: flag to disable opening balance calculation
(cherry picked from commit 28a2230d02)
2026-05-15 06:50:58 +00:00
mergify[bot]
48ed07816d fix(stock): add whole number quantity validation in Stock Reconciliation (backport #54922) (#54924)
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:20:13 +00:00
Frappe PR Bot
d43862624a chore(release): Bumped to Version 15.108.1
## [15.108.1](https://github.com/frappe/erpnext/compare/v15.108.0...v15.108.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 [#54907](https://github.com/frappe/erpnext/issues/54907)) ([#54917](https://github.com/frappe/erpnext/issues/54917)) ([dc4b9cc](dc4b9cc4bc))
2026-05-13 11:16:09 +00:00
mergify[bot]
dc4b9cc4bc Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (backport #54907) (#54917)
Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (#54907)

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

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

* chore: resolve conflicts

---------


(cherry picked from commit 6d3cd7d38a)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 16:44:53 +05:30
mergify[bot]
6d3cd7d38a Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (#54907)
* 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)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 09:58:45 +00:00
mergify[bot]
b969662b6c fix: add warehouse vaildation for repack entry (backport #54866) (#54900)
* fix: add warehouse vaildation for repack entry (#54866)

(cherry picked from commit bc07b2d3e5)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py

* chore: resolve conflicts

---------

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 06:42:18 +00:00
108 changed files with 3125 additions and 1424 deletions

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.108.0"
__version__ = "15.110.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

@@ -517,6 +517,7 @@ def get_account_autoname(account_number, account_name, company):
def update_account_number(name, account_name, account_number=None, from_descendant=False):
_ensure_idle_system()
account = frappe.get_cached_doc("Account", name)
account.check_permission("write")
if not account:
return

View File

@@ -30,16 +30,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

@@ -95,7 +95,6 @@
"receivable_payable_fetch_method",
"default_ageing_range",
"column_break_ntmi",
"drop_ar_procedures",
"legacy_section",
"ignore_is_opening_check_for_reporting",
"payment_request_settings",
@@ -561,7 +560,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",
@@ -623,13 +622,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",
@@ -671,7 +663,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-06 14:49:11.467716",
"modified": "2026-05-18 12:16:33.679345",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -701,4 +693,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -60,7 +60,7 @@ class AccountsSettings(Document):
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
post_change_gl_entries: DF.Check
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
role_allowed_to_over_bill: DF.Link | None
@@ -170,11 +170,3 @@ class AccountsSettings(Document):
),
title=_("Auto Tax Settings Error"),
)
@frappe.whitelist()
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"autoname": "format:Bank Statement Import on {creation}",
"beta": 1,
"creation": "2019-08-04 14:16:08.318714",
"doctype": "DocType",
"editable_grid": 1,
@@ -211,10 +210,11 @@
],
"hide_toolbar": 1,
"links": [],
"modified": "2024-06-25 17:32:07.658250",
"modified": "2026-05-30 20:51:10.353723",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -230,7 +230,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -2,7 +2,6 @@
"actions": [],
"allow_events_in_timeline": 1,
"autoname": "naming_series:",
"beta": 1,
"creation": "2019-07-05 16:34:31.013238",
"doctype": "DocType",
"engine": "InnoDB",
@@ -400,7 +399,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2024-11-26 13:46:07.760867",
"modified": "2026-05-30 20:40:30.851842",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning",
@@ -449,9 +448,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"title_field": "customer_name",
"track_changes": 1
}
}

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"allow_rename": 1,
"beta": 1,
"creation": "2019-12-04 04:59:08.003664",
"doctype": "DocType",
"editable_grid": 1,
@@ -107,7 +106,7 @@
"link_fieldname": "dunning_type"
}
],
"modified": "2021-11-13 00:25:35.659283",
"modified": "2026-05-30 20:40:09.952533",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning Type",
@@ -151,7 +150,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -378,15 +378,17 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
accounts_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
row.exchange_rate = 1;
$.each(doc.accounts, function (i, d) {
if (d.account && d.party && d.party_type) {
row.account = d.account;
row.party = d.party;
row.party_type = d.party_type;
row.exchange_rate = d.exchange_rate;
}
});
if (!row.exchange_rate) row.exchange_rate = 1;
if (!row.account) {
$.each(doc.accounts, function (i, d) {
if (d.account && d.party && d.party_type) {
row.account = d.account;
row.party = d.party;
row.party_type = d.party_type;
row.exchange_rate = d.exchange_rate;
}
});
}
// set difference
if (doc.difference) {

View File

@@ -1,6 +1,6 @@
{
"actions": [],
"allow_copy": 1,
"beta": 1,
"creation": "2017-08-29 02:22:54.947711",
"doctype": "DocType",
"editable_grid": 1,
@@ -64,10 +64,10 @@
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"collapsible": 1,
@@ -82,7 +82,8 @@
],
"hide_toolbar": 1,
"issingle": 1,
"modified": "2022-01-04 15:25:06.053187",
"links": [],
"modified": "2026-05-30 20:43:36.282738",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",
@@ -99,7 +100,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -31,6 +31,7 @@ class OpeningInvoiceCreationTool(Document):
create_missing_party: DF.Check
invoice_type: DF.Literal["Sales", "Purchase"]
invoices: DF.Table[OpeningInvoiceCreationToolItem]
project: DF.Link | None
# end: auto-generated types
def onload(self):

View File

@@ -725,31 +725,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) {
@@ -781,49 +762,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,
@@ -832,6 +788,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);
},
@@ -840,11 +822,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");
@@ -861,15 +846,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)
);
}
}
@@ -1789,6 +1773,35 @@ frappe.ui.form.on("Payment Entry", {
},
});
},
before_cancel: function (frm) {
return new Promise((resolve, reject) => {
frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_linked_bank_transactions",
args: { payment_entry: frm.doc.name },
callback: function (r) {
const linked = r.message || [];
if (!linked.length) {
resolve();
return;
}
const bt_links = linked
.map((name) => frappe.utils.get_form_link("Bank Transaction", name, true))
.join(", ");
frappe.confirm(
__(
"This Payment Entry is reconciled with {0}. Cancelling will automatically unreconcile it. Do you want to proceed?",
[bt_links]
),
() => resolve(),
() => reject(),
__("Yes"),
__("No")
);
},
});
});
},
});
frappe.ui.form.on("Payment Entry Reference", {

View File

@@ -350,7 +350,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)",
@@ -800,7 +800,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-05-15 18:01:04.013025",
"modified": "2026-05-15 13:31:01.166010",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -3599,3 +3599,16 @@ def make_payment_order(source_name, target_doc=None):
@erpnext.allow_regional
def add_regional_gl_entries(gl_entries, doc):
return
@frappe.whitelist()
def get_linked_bank_transactions(payment_entry: str) -> list:
frappe.has_permission("Payment Entry", ptype="read", doc=payment_entry, throw=True)
return frappe.get_all(
"Bank Transaction Payments",
filters={
"payment_document": "Payment Entry",
"payment_entry": payment_entry,
},
pluck="parent",
)

View File

@@ -3,8 +3,9 @@
import frappe
from frappe import qb
from frappe.query_builder.functions import Count, Sum
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import nowdate
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
@@ -94,6 +95,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
posting_date = nowdate()
sinv = create_sales_invoice(
posting_date=posting_date,
qty=qty,
rate=rate,
company=self.company,
@@ -535,3 +537,82 @@ class TestPaymentLedgerEntry(FrappeTestCase):
# with references removed, deletion should be possible
so.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)
@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

@@ -5,11 +5,13 @@
import unittest
import frappe
from frappe.tests.utils import change_settings
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
@@ -351,6 +353,51 @@ class TestPeriodClosingVoucher(unittest.TestCase):
return pcv
@change_settings(
"Accounts Settings",
{"enable_immutable_ledger": 1},
)
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
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

@@ -202,15 +202,14 @@ class POSProfile(Document):
def set_defaults(self, include_current_pos=True):
frappe.defaults.clear_default("is_pos")
if not include_current_pos:
condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "'")
else:
condition = " where pfu.default = 1 "
pfu = frappe.qb.DocType("POS Profile User")
pos_view_users = frappe.db.sql_list(
f"""select pfu.user
from `tabPOS Profile User` as pfu {condition}"""
)
query = frappe.qb.from_(pfu).select(pfu.user).where(pfu.default == 1)
if not include_current_pos:
query = query.where(pfu.name != self.name)
pos_view_users = query.run(as_list=1, pluck=True)
for user in pos_view_users:
if user:

View File

@@ -151,13 +151,13 @@
"label": "Default Advance Account",
"mandatory_depends_on": "doc.party_type",
"options": "Account",
"reqd": 1
"reqd": 0
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-01-08 08:22:14.798085",
"modified": "2026-05-16 11:43:12.758685",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",

View File

@@ -23,7 +23,7 @@ class ProcessPaymentReconciliation(Document):
bank_cash_account: DF.Link | None
company: DF.Link
cost_center: DF.Link | None
default_advance_account: DF.Link
default_advance_account: DF.Link | None
error_log: DF.LongText | None
from_invoice_date: DF.Date | None
from_payment_date: DF.Date | None
@@ -215,10 +215,7 @@ def trigger_reconciliation_for_queued_docs():
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
def get_filters_as_tuple(fields, doc):
filters = ()
for x in fields:
filters += tuple(doc.get(x))
return filters
return tuple(doc.get(x) or "" for x in fields)
for x in all_queued:
doc = frappe.get_doc("Process Payment Reconciliation", x)

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"autoname": "format:Process-PCV-{###}",
"creation": "2025-09-25 15:44:03.534699",
"doctype": "DocType",
@@ -7,11 +8,13 @@
"field_order": [
"parent_pcv",
"status",
"amended_from",
"section_normal_balances",
"p_l_closing_balance",
"normal_balances",
"bs_closing_balance",
"z_opening_balances",
"amended_from"
"normal_balances",
"section_opening_balances",
"z_opening_balances"
],
"fields": [
{
@@ -64,17 +67,27 @@
"fieldname": "bs_closing_balance",
"fieldtype": "JSON",
"label": "Balance Sheet Closing Balance"
},
{
"fieldname": "section_normal_balances",
"fieldtype": "Tab Break",
"label": "Normal Balances"
},
{
"fieldname": "section_opening_balances",
"fieldtype": "Tab Break",
"label": "Opening Balances"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-05 11:40:24.996403",
"modified": "2026-06-01 12:16:37.374412",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Period Closing Voucher",
"naming_rule": "Expression",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{

View File

@@ -69,8 +69,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:
@@ -90,12 +90,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:
@@ -130,9 +134,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")
@@ -166,6 +171,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):
@@ -235,12 +243,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(
@@ -278,7 +289,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:
@@ -534,6 +559,9 @@ def process_individual_date(docname: str, date, report_type, parentfield):
if parentfield == "z_opening_balances":
query = query.where(gle.is_opening.eq("Yes"))
else:
# Keep balances aligned with legacy PCV logic (non-opening transactions only)
query = query.where(gle.is_opening.eq("No"))
query = query.groupby(gle.account)
for dim in dimensions:

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// render
frappe.listview_settings["Process Period Closing Voucher"] = {
add_fields: ["status"],
get_indicator: function (doc) {
const status_colors = {
Queued: "blue",
Running: "orange",
Paused: "gray",
Completed: "green",
Cancelled: "red",
};
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
},
};

View File

@@ -8,10 +8,13 @@ import frappe
from frappe import _
from frappe.contacts.doctype.address.address import get_default_address
from frappe.model.document import Document
from frappe.query_builder import DocType
from frappe.query_builder.functions import IfNull
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):
@@ -174,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
@@ -234,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

@@ -72,6 +72,117 @@ class TestTaxRule(unittest.TestCase):
"_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

@@ -700,7 +700,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

@@ -669,7 +669,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.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
@@ -743,7 +743,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)

View File

@@ -129,8 +129,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()
@@ -321,81 +319,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)
@@ -1390,136 +1313,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;
"""
# Function
genkey_function_name = "ar_genkey"
genkey_function_sql = f"""
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
begin
if allocate then
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
else
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
end if;
end
"""
# 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 = `{genkey_function_name}`(ple, false))
then
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), 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 (`{genkey_function_name}`(ple, true), 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.genkey_function_name not in existing_procedures:
frappe.db.sql(self.genkey_function_sql)
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

@@ -176,10 +176,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

@@ -279,7 +279,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"]
@@ -398,7 +406,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
# Opening for filtered account
data.append(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)":
for _acc, acc_dict in gle_map.items():
# acc
if acc_dict.entries:
@@ -528,7 +542,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
group_by_value = gle.get(group_by)
gle.voucher_type = gle.voucher_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

@@ -786,19 +786,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:
@@ -1049,25 +1041,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

@@ -1934,8 +1934,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()
@@ -2027,6 +2028,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)
@@ -2043,9 +2045,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

@@ -6,6 +6,7 @@ import json
import frappe
from frappe import _
from frappe.contacts.doctype.contact.contact import get_full_name
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments
from frappe.model.mapper import get_mapped_doc
@@ -272,12 +273,20 @@ class RequestforQuotation(BuyingController):
supplier_doc.save()
def create_user(self, rfq_supplier, link):
contact_name = None
if rfq_supplier.contact:
name_fields = frappe.get_value(
"Contact", rfq_supplier.contact, ["first_name", "middle_name", "last_name"]
)
if name_fields:
contact_name = get_full_name(*name_fields)
user = frappe.get_doc(
{
"doctype": "User",
"send_welcome_email": 0,
"email": rfq_supplier.email_id,
"first_name": rfq_supplier.supplier_name or rfq_supplier.supplier,
"first_name": contact_name or rfq_supplier.supplier_name or rfq_supplier.supplier,
"user_type": "Website User",
"redirect_url": link,
}

View File

@@ -69,6 +69,7 @@ from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import (
_get_item_tax_template,
_get_item_tax_template_from_item_group,
get_bin_details,
get_conversion_factor,
get_item_details,
get_item_tax_map,
@@ -3704,6 +3705,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
child_item.warehouse = get_item_warehouse(item, p_doc, 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

View File

@@ -364,17 +364,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 _get_item_tax_template
from erpnext.stock.utils import get_combine_datetime
# searches for active employees
@@ -369,10 +370,14 @@ def get_delivery_notes_to_be_billed(
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
)
query = frappe.qb.get_query(
"Delivery Note",
fields=fields,
filters=filters,
)
query = (
frappe.qb.from_(DeliveryNote)
.select(*[DeliveryNote[f] for f in fields])
.where(
query.where(
(DeliveryNote.docstatus == 1)
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
& (DeliveryNote[searchfield].like(f"%{txt}%"))
@@ -386,12 +391,11 @@ def get_delivery_notes_to_be_billed(
)
)
)
.orderby(DeliveryNote[searchfield], order=Order.asc)
.limit(page_len)
.offset(start)
)
if filters and isinstance(filters, dict):
for key, value in filters.items():
query = query.where(DeliveryNote[key] == value)
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
return query.run(as_dict=as_dict)
@@ -476,6 +480,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()))
@@ -529,6 +540,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

@@ -240,10 +240,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 not frappe.db.get_single_value("Selling Settings", "allow_negative_rates_for_items"):

View File

@@ -1680,7 +1680,7 @@ def repost_required_for_queue(doc: StockController) -> bool:
@frappe.whitelist()
def check_item_quality_inspection(doctype, items):
def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str | list[dict]):
if isinstance(items, str):
items = json.loads(items)
@@ -1692,13 +1692,30 @@ def check_item_quality_inspection(doctype, items):
"Delivery Note": "inspection_required_before_delivery",
}
items_to_remove = []
for item in items:
if not frappe.db.get_value("Item", item.get("item_code"), inspection_fieldname_map.get(doctype)):
items_to_remove.append(item)
items = [item for item in items if item not in items_to_remove]
inspection_fieldname = inspection_fieldname_map.get(doctype)
if inspection_fieldname is None:
return []
return items
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
)
if allow_after_transaction:
return items
item_codes = list({item.get("item_code") for item in items})
Item = frappe.qb.DocType("Item")
results = (
frappe.qb.from_(Item)
.select(Item.name)
.where((Item.name.isin(item_codes)) & (Item[inspection_fieldname] == 1))
.run(as_dict=True)
)
inspection_required_items = {row.name for row in results}
return [item for item in items if item.get("item_code") in inspection_required_items]
@frappe.whitelist()

View File

@@ -183,10 +183,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:
@@ -244,13 +240,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
@@ -382,16 +372,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,
@@ -17,6 +21,19 @@ class TestItemVariant(unittest.TestCase):
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

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

View File

@@ -438,7 +438,7 @@ frappe.ui.form.on("BOM", {
},
routing(frm) {
if (frm.doc.routing) {
if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations) {
frappe.call({
doc: frm.doc,
method: "get_routing",

View File

@@ -1367,18 +1367,71 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account):
def add_operations_cost(stock_entry, work_order=None, expense_account=None):
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit
from erpnext.stock.doctype.stock_entry.stock_entry import (
get_consumed_operating_cost,
get_operating_cost_per_unit,
)
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
if operating_cost_per_unit:
stock_entry.append(
"additional_costs",
{
def append_operating_cost(amount, operation=None, qty=None):
if amount:
row = {
"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(
amount,
frappe.get_precision("Landed Cost Taxes and Charges", "amount"),
),
"has_operating_cost": 1,
}
if operation:
row["operation_id"] = operation.name
if qty is not None:
row["qty"] = qty
stock_entry.append(
"additional_costs",
row,
)
if (
work_order
and stock_entry.bom_no
and frappe.db.get_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies")
and work_order.get("use_multi_level_bom")
):
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
append_operating_cost(
operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
qty=flt(stock_entry.fg_completed_qty),
)
elif work_order and work_order.get("operations"):
for operation in work_order.get("operations"):
qty = flt(stock_entry.fg_completed_qty)
amount = 0
if flt(operation.completed_qty):
consumed_cost = get_consumed_operating_cost(
work_order.name, stock_entry.bom_no, operation.name
)
remaining_cost = flt(
flt(operation.actual_operating_cost) - flt(consumed_cost.get("consumed_cost")),
operation.precision("actual_operating_cost"),
)
remaining_qty = flt(operation.completed_qty) - flt(consumed_cost.get("consumed_qty"))
if remaining_cost <= 0 or remaining_qty <= 0:
continue
qty = min(remaining_qty, flt(stock_entry.fg_completed_qty))
amount = remaining_cost / remaining_qty * qty
elif work_order.qty:
amount = flt(operation.planned_operating_cost) / flt(work_order.qty) * qty
append_operating_cost(amount, operation=operation, qty=qty)
else:
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
append_operating_cost(
operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
qty=flt(stock_entry.fg_completed_qty),
)
if work_order and work_order.additional_operating_cost and work_order.qty:

View File

@@ -126,11 +126,13 @@
"label": "Image"
},
{
"default": "1",
"fetch_from": "operation.batch_size",
"fetch_if_empty": 1,
"fieldname": "batch_size",
"fieldtype": "Int",
"label": "Batch Size"
"fieldtype": "Float",
"label": "Batch Size",
"non_negative": 1
},
{
"depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing",
@@ -196,13 +198,14 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-02-17 15:33:28.495850",
"modified": "2026-05-27 12:09:44.797434",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -17,7 +17,7 @@ class BOMOperation(Document):
base_cost_per_unit: DF.Float
base_hour_rate: DF.Currency
base_operating_cost: DF.Currency
batch_size: DF.Int
batch_size: DF.Float
cost_per_unit: DF.Float
description: DF.TextEditor | None
fixed_time: DF.Check

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"autoname": "naming_series:",
"creation": "2018-07-09 17:23:29.518745",
"doctype": "DocType",
@@ -135,6 +136,7 @@
"fieldname": "wip_warehouse",
"fieldtype": "Link",
"label": "WIP Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse",
"reqd": 1
},
@@ -511,7 +513,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2025-08-04 15:47:54.514290",
"modified": "2026-05-12 12:17:17.750857",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",

View File

@@ -7,7 +7,7 @@ from typing import Literal
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import random_string
from frappe.utils import flt, random_string
from frappe.utils.data import add_to_date, now, today
from erpnext.manufacturing.doctype.job_card.job_card import (
@@ -697,6 +697,403 @@ class TestJobCard(FrappeTestCase):
self.assertEqual(wo_doc.process_loss_qty, 2)
self.assertEqual(wo_doc.status, "Completed")
def test_op_cost_calculation(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
suffix = random_string(5)
workstation = make_workstation(
workstation_name=f"Test Workstation Z {suffix}", hour_rate_rent=240, hour_rate_labour=0
)
workstation.update(
{
"hour_rate_rent": 240,
"hour_rate_labour": 0,
"hour_rate_electricity": 0,
"hour_rate_consumable": 0,
}
)
workstation.save()
operations = [
{
"operation": f"Test Operation A1 {suffix}",
"workstation": workstation.name,
"time_in_mins": 30,
},
]
warehouse = create_warehouse(f"Test Warehouse 123 for Job Card {suffix}")
setup_operations(operations)
item_code = f"Test Job Card Process Qty Item {suffix}"
for item in [item_code, item_code + "RM 1", item_code + "RM 2"]:
if not frappe.db.exists("Item", item):
make_item(
item,
{
"item_name": item,
"stock_uom": "Nos",
"is_stock_item": 1,
},
)
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
bom_doc = setup_bom(
item_code=item_code,
routing=routing_doc.name,
raw_materials=[item_code + "RM 1", item_code + "RM 2"],
source_warehouse=warehouse,
)
for row in bom_doc.items:
make_stock_entry(
item_code=row.item_code,
target=row.source_warehouse,
qty=10,
basic_rate=100,
)
wo_doc = make_wo_order_test_record(
production_item=item_code,
bom_no=bom_doc.name,
qty=10,
skip_transfer=1,
wip_warehouse=warehouse,
source_warehouse=warehouse,
)
first_job_card = 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_job_card)
from_time = "2025-01-01 09:00:00"
for _ in jc.scheduled_time_logs:
jc.append(
"time_logs",
{
"from_time": from_time,
"to_time": add_to_date(from_time, minutes=1),
"completed_qty": 4,
},
)
jc.for_quantity = 4
jc.save()
jc.submit()
s1 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 4))
s1.submit()
wo_doc.reload()
precision = s1.additional_costs[0].precision("amount")
self.assertEqual(
flt(s1.additional_costs[0].amount, precision),
flt(wo_doc.operations[0].actual_operating_cost, precision),
)
make_job_card(
wo_doc.name,
[
{
"name": wo_doc.operations[0].name,
"operation": operations[0]["operation"],
"workstation": wo_doc.operations[0].workstation,
"qty": 6,
"pending_qty": 6,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
from_time = "2025-01-01 10:00:00"
job_card.append(
"time_logs",
{
"from_time": from_time,
"to_time": add_to_date(from_time, minutes=2),
"completed_qty": 6,
},
)
job_card.for_quantity = 6
job_card.save()
job_card.submit()
s2 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6))
wo_doc.reload()
precision = s2.additional_costs[0].precision("amount")
self.assertEqual(
flt(s2.additional_costs[0].amount, precision),
flt(wo_doc.operations[0].actual_operating_cost - s1.additional_costs[0].amount, precision),
)
@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
suffix = random_string(5)
workstation = make_workstation(
workstation_name=f"Test Workstation for Overproduction {suffix}",
hour_rate_rent=10,
hour_rate_labour=10,
)
workstation.update(
{
"hour_rate_rent": 10,
"hour_rate_labour": 10,
"hour_rate_electricity": 0,
"hour_rate_consumable": 0,
}
)
workstation.save()
operations = [
{"operation": f"Test Operation 1 {suffix}", "workstation": workstation.name, "time_in_mins": 30},
{"operation": f"Test Operation 2 {suffix}", "workstation": workstation.name, "time_in_mins": 30},
]
warehouse = create_warehouse(f"Test Warehouse for Overproduction {suffix}")
setup_operations(operations)
fg = make_item(f"Test FG for Overproduction {suffix}", {"stock_uom": "Nos", "is_stock_item": 1})
rm = make_item(f"Test RM for Overproduction {suffix}", {"stock_uom": "Nos", "is_stock_item": 1})
routing_doc = create_routing(routing_name=f"Testing Route {suffix}", 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 = "2025-01-02 09:00:00"
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 = "2025-01-05 09:00:00"
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()
def assert_operating_costs(stock_entry, qty, previous_entries):
wo_doc.reload()
for idx, operation in enumerate(wo_doc.operations):
consumed_cost = sum(
entry.additional_costs[idx].amount for entry in previous_entries if entry.docstatus == 1
)
consumed_qty = sum(
entry.additional_costs[idx].qty for entry in previous_entries if entry.docstatus == 1
)
remaining_cost = operation.actual_operating_cost - consumed_cost
remaining_qty = operation.completed_qty - consumed_qty
precision = stock_entry.additional_costs[idx].precision("amount")
expected_cost = flt(remaining_cost / remaining_qty * min(remaining_qty, qty), precision)
self.assertEqual(flt(stock_entry.additional_costs[idx].amount, precision), expected_cost)
assert_operating_costs(s, 6, [])
make_job_card(
wo_doc.name,
[
{
"name": wo_doc.operations[0].name,
"operation": operations[0]["operation"],
"workstation": wo_doc.operations[0].workstation,
"qty": 2,
"pending_qty": 2,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
from_time = "2025-01-09 09:00:00"
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": operations[1]["operation"],
"workstation": wo_doc.operations[1].workstation,
"qty": 2,
"pending_qty": 2,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
from_time = "2025-01-12 09:00:00"
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()
assert_operating_costs(s2, 1, [s])
make_job_card(
wo_doc.name,
[
{
"name": wo_doc.operations[0].name,
"operation": operations[0]["operation"],
"workstation": wo_doc.operations[0].workstation,
"qty": 2,
"pending_qty": 2,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
from_time = "2025-01-16 09:00:00"
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": operations[1]["operation"],
"workstation": wo_doc.operations[1].workstation,
"qty": 2,
"pending_qty": 2,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
from_time = "2025-01-19 09:00:00"
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()
s3 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 2))
s3.submit()
assert_operating_costs(s3, 2, [s, s2])
s2.cancel()
s4 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 3))
s4.submit()
assert_operating_costs(s4, 3, [s, s3])
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,
@@ -33,6 +34,7 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Source Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse"
},
{
@@ -105,7 +107,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-22 18:50:00.003444",
"modified": "2026-05-12 12:22:18.506904",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Item",
@@ -115,4 +117,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

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";
@@ -315,7 +296,7 @@ frappe.ui.form.on("Work Order", {
{
fieldtype: "Data",
fieldname: "name",
label: __("Operation Id"),
label: __("Operation ID"),
},
{
fieldtype: "Float",
@@ -385,6 +366,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",
@@ -249,6 +250,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",
"options": "Warehouse"
},
@@ -257,6 +259,7 @@
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse",
"reqd": 1
},
@@ -269,6 +272,7 @@
"fieldname": "scrap_warehouse",
"fieldtype": "Link",
"label": "Scrap Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse"
},
{
@@ -498,6 +502,7 @@
"fieldname": "source_warehouse",
"fieldtype": "Link",
"label": "Source Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse"
},
{
@@ -602,7 +607,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2025-06-21 00:55:45.916224",
"modified": "2026-05-19 12:20:38.102403",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",

View File

@@ -158,7 +158,7 @@ class WorkOrder(Document):
self.calculate_operating_cost()
self.validate_qty()
self.validate_transfer_against()
self.validate_operation_time()
self.validate_operations()
self.status = self.get_status()
self.validate_workstation_type()
self.reset_use_multi_level_bom()
@@ -1120,9 +1120,12 @@ class WorkOrder(Document):
title=_("Missing value"),
)
def validate_operation_time(self):
def validate_operations(self):
for d in self.operations:
if not d.time_in_mins > 0:
if not d.batch_size or d.batch_size <= 0:
d.batch_size = 1
if d.time_in_mins <= 0:
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
def update_required_items(self):

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"creation": "2016-04-18 07:38:26.314642",
"doctype": "DocType",
"editable_grid": 1,
@@ -46,6 +47,7 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Source Warehouse",
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
"options": "Warehouse"
},
{
@@ -151,7 +153,7 @@
],
"istable": 1,
"links": [],
"modified": "2025-12-02 11:16:05.081613",
"modified": "2026-05-12 12:05:16.687866",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Item",

View File

@@ -185,10 +185,11 @@
"read_only": 1
},
{
"default": "1",
"fieldname": "batch_size",
"fieldtype": "Float",
"label": "Batch Size",
"read_only": 1
"non_negative": 1
},
{
"fieldname": "sequence_id",
@@ -225,14 +226,15 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-05-15 15:10:06.885440",
"modified": "2026-05-27 12:56:37.240431",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -433,3 +433,5 @@ erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
erpnext.patches.v16_0.add_portal_redirects
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
erpnext.patches.v16_0.depends_on_inv_dimensions
erpnext.patches.v16_0.clear_procedures_from_receivable_report
erpnext.patches.v16_0.migrate_address_contact_custom_fields

View File

@@ -0,0 +1,12 @@
import frappe
def execute():
frappe.db.sql("drop function if exists ar_genkey")
frappe.db.sql("drop procedure if exists ar_init_tmp_table")
frappe.db.sql("drop procedure if exists ar_allocate_to_tmp_table")
if frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method") == "Raw SQL":
frappe.db.set_single_value(
"Accounts Settings", "receivable_payable_fetch_method", "UnBuffered Cursor"
)

View File

@@ -0,0 +1,16 @@
import frappe
from erpnext.setup.install import create_address_and_contact_custom_fields
def execute():
"""Replace fixture-based custom fields on Address and Contact with programmatic ones."""
for custom_field in (
"Address-tax_category",
"Address-is_your_company_address",
"Contact-is_billing_contact",
):
if frappe.db.exists("Custom Field", custom_field):
frappe.delete_doc("Custom Field", custom_field, ignore_missing=True, force=True)
create_address_and_contact_custom_fields()

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"beta": 1,
"creation": "2016-04-22 05:27:52.109319",
"doctype": "DocType",
"document_type": "Setup",
@@ -87,7 +86,7 @@
],
"issingle": 1,
"links": [],
"modified": "2022-12-19 21:10:29.127277",
"modified": "2026-05-30 20:51:04.415019",
"modified_by": "Administrator",
"module": "Portal",
"name": "Homepage",
@@ -114,6 +113,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],

View File

@@ -176,6 +176,7 @@
"fieldtype": "Link",
"in_global_search": 1,
"label": "Customer",
"no_copy": 1,
"oldfieldname": "customer",
"oldfieldtype": "Link",
"options": "Customer",
@@ -190,6 +191,7 @@
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"no_copy": 1,
"options": "Sales Order"
},
{
@@ -462,7 +464,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
"modified": "2025-08-21 17:57:58.314809",
"modified": "2026-05-22 16:45:50.762759",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",

View File

@@ -176,14 +176,9 @@ erpnext.buying = {
callback: (r) => {
if (!r.message) return;
if (!this.frm.doc.billing_address) {
this.frm.set_value("billing_address", r.message.primary_address || "");
}
this.frm.set_value("billing_address", r.message.primary_address || "");
if (
frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") &&
!this.frm.doc.shipping_address
) {
if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) {
this.frm.set_value("shipping_address", r.message.shipping_address || "");
}
},

View File

@@ -2506,11 +2506,29 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
method: "erpnext.controllers.stock_controller.check_item_quality_inspection",
args: {
doctype: this.frm.doc.doctype,
items: this.frm.doc.items
docstatus: this.frm.doc.docstatus,
items: this.frm.doc.items,
},
freeze: true,
callback: function (r) {
r.message.forEach(item => {
if (r.message.length == 0) {
let type = inspection_type === "Incoming" ? "Purchase" : "Delivery";
let fieldname =
inspection_type === "Incoming"
? "Inspection Required before Purchase"
: "Inspection Required before Delivery";
frappe.msgprint({
title: __("Quality Inspection Not Configured"),
message: __(`Enable <b>{0}</b> on the Item master to proceed with {1} inspection.`, [
fieldname,
type,
]),
});
return;
}
r.message.forEach((item) => {
if (me.has_inspection_required(item)) {
let dialog_items = dialog.fields_dict.items;
dialog_items.df.data.push({

View File

@@ -472,6 +472,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
warehouse:
this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse,
is_inward: is_inward,
posting_date: this.frm.doc.posting_date,
posting_time: this.frm.doc.posting_time,
include_expired_batches: include_expired_batches,
},
};

View File

@@ -6,6 +6,7 @@ import erpnext
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.regional.report.uae_vat_201.uae_vat_201 import (
execute,
get_exempt_total,
get_standard_rated_expenses_tax,
get_standard_rated_expenses_total,
@@ -39,6 +40,13 @@ class TestUaeVat201(TestCase):
make_item("_Test UAE VAT Zero Rated Item", properties={"is_zero_rated": 1, "is_exempt": 0})
make_item("_Test UAE VAT Exempt Item", properties={"is_zero_rated": 0, "is_exempt": 1})
def test_validate_company_region(self):
self.assertRaises(
frappe.exceptions.ValidationError,
execute,
{"company": "_Test Company"},
)
def test_uae_vat_201_report(self):
make_sales_invoices()
create_purchase_invoices()

View File

@@ -10,6 +10,13 @@ frappe.query_reports["UAE VAT 201"] = {
options: "Company",
reqd: 1,
default: frappe.defaults.get_user_default("Company"),
get_query: function () {
return {
filters: {
country: "United Arab Emirates",
},
};
},
},
{
fieldname: "from_date",

View File

@@ -5,13 +5,25 @@
import frappe
from frappe import _
from erpnext import get_region
def execute(filters=None):
validate_company_region(filters)
columns = get_columns()
data, emirates, amounts_by_emirate = get_data(filters)
return columns, data
def validate_company_region(filters):
if filters.get("company") and get_region(filters.get("company")) != "United Arab Emirates":
frappe.throw(
_(
"The company {0} is not in United Arab Emirates. UAE VAT 201 report is only available for companies in United Arab Emirates."
).format(frappe.bold(filters.get("company")))
)
def get_columns():
"""Creates a list of dictionaries that are used to generate column headers of the data table."""
return [

View File

@@ -308,7 +308,6 @@
"read_only": 1
},
{
"depends_on": "eval:(doc.quotation_to=='Customer' && doc.party_name)",
"fieldname": "col_break98",
"fieldtype": "Column Break",
"width": "50%"
@@ -1108,7 +1107,7 @@
"idx": 82,
"is_submittable": 1,
"links": [],
"modified": "2025-07-31 17:23:48.875382",
"modified": "2026-05-30 17:40:02.667637",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",

View File

@@ -385,7 +385,7 @@ class SalesOrder(SellingController):
and not cint(d.delivered_by_supplier)
):
frappe.throw(
_("Delivery warehouse required for stock item {0}").format(d.item_code), WarehouseRequired
_("Source warehouse required for stock item {0}").format(d.item_code), WarehouseRequired
)
def validate_with_previous_doc(self):

View File

@@ -377,42 +377,80 @@ def get_past_order_list(search_term, status, limit=20):
@frappe.whitelist()
def set_customer_info(fieldname, customer, value=""):
customer_doc = frappe.get_doc("Customer", customer)
customer_doc.check_permission("write")
if fieldname == "loyalty_program":
frappe.db.set_value("Customer", customer, "loyalty_program", value)
customer_doc.loyalty_program = value
else:
contact = customer_doc.get("customer_primary_contact")
if not contact:
Contact = DocType("Contact")
DynamicLink = DocType("Dynamic Link")
contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact")
if not contact:
contact = frappe.db.sql(
"""
SELECT parent FROM `tabDynamic Link`
WHERE
parenttype = 'Contact' AND
parentfield = 'links' AND
link_doctype = 'Customer' AND
link_name = %s
""",
(customer),
as_dict=1,
)
contact = contact[0].get("parent") if contact else None
# Inner join with Contact DocType, to priorities records that have is_primary_contact set.
query = (
frappe.qb.from_(DynamicLink)
.join(Contact)
.on(DynamicLink.parent == Contact.name)
.select(DynamicLink.parent)
.where(
(DynamicLink.link_name == customer)
& (DynamicLink.parentfield == "links")
& (DynamicLink.parenttype == "Contact")
& (DynamicLink.link_doctype == "Customer")
)
.orderby(Contact.is_primary_contact, order=Order.desc)
)
if not contact:
new_contact = frappe.new_doc("Contact")
new_contact.is_primary_contact = 1
new_contact.first_name = customer
new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}])
new_contact.save()
contact = new_contact.name
frappe.db.set_value("Customer", customer, "customer_primary_contact", contact)
contacts = query.run(pluck=DynamicLink.parent)
contact_doc = frappe.get_doc("Contact", contact)
if fieldname == "email_id":
contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}])
frappe.db.set_value("Customer", customer, "email_id", value)
elif fieldname == "mobile_no":
contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}])
frappe.db.set_value("Customer", customer, "mobile_no", value)
contact_doc.save()
contact = contacts[0] if contacts else None
if not contact:
new_contact = frappe.new_doc("Contact")
new_contact.is_primary_contact = 1
new_contact.first_name = customer
new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}])
new_contact.save()
contact = new_contact.name
def set_primary_phone_no_email(field, value):
# Create new record instead deleting existing email or phone_no and setting the new row as primary.
field_mapper = {
"email_ids": {"field": "email_id", "primary": "is_primary"},
"phone_nos": {"field": "phone", "primary": "is_primary_mobile_no"},
}
value_already_exists = False
for d in contact_doc.get(field):
if d.get(field_mapper[field].get("field")) == value and not value_already_exists:
d.set(field_mapper[field]["primary"], 1)
value_already_exists = True
continue
d.set(field_mapper[field]["primary"], 0)
if not value_already_exists:
contact_doc.append(
field, {field_mapper[field]["field"]: value, field_mapper[field]["primary"]: 1}
)
contact_doc = frappe.get_doc("Contact", contact)
# setting is_primary_contact = 1 on Contact to refetch the same contact incase it's removed from Customer records.
contact_doc.set("is_primary_contact", 1)
if fieldname == "email_id":
set_primary_phone_no_email("email_ids", value)
elif fieldname == "mobile_no":
set_primary_phone_no_email("phone_nos", value)
# Saving contact_doc to set mobile_no and email.
contact_doc.save()
# Auto-fetches from Contact DocType, no need to set values separately.
customer_doc.customer_primary_contact = contact
# using save method instead db.set_value which bypasses the validation for loyalty program
# and auto sets the mobile_no and email field on customer records.
customer_doc.save()
@frappe.whitelist()

View File

@@ -174,8 +174,8 @@ erpnext.PointOfSale.Controller = class {
set_opening_entry_status() {
this.page.set_title_sub(
`<span class="indicator orange">
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}">
Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)}
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${encodeURIComponent(this.pos_opening)}">
Opened at ${frappe.utils.escape_html(frappe.datetime.str_to_user(this.pos_opening_time))}
</a>
</span>`
);

View File

@@ -178,7 +178,7 @@ erpnext.PointOfSale.ItemCart = class {
me.$totals_section.find(".edit-cart-btn").click();
}
const item_row_name = unescape($cart_item.attr("data-row-name"));
const item_row_name = $cart_item.attr("data-row-name");
me.events.cart_item_clicked({ name: item_row_name });
this.numpad_value = "";
});
@@ -453,10 +453,10 @@ erpnext.PointOfSale.ItemCart = class {
<div class="customer-display">
${this.get_customer_image()}
<div class="customer-name-desc">
<div class="customer-name">${customer_name}</div>
<div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
${get_customer_description()}
</div>
<div class="reset-customer-btn" data-customer="${escape(customer)}">
<div class="reset-customer-btn" data-customer="${frappe.utils.escape_html(customer)}">
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
</svg>
@@ -473,11 +473,13 @@ erpnext.PointOfSale.ItemCart = class {
if (!email_id && !mobile_no) {
return `<div class="customer-desc">${__("Click to add email / phone")}</div>`;
} else if (email_id && !mobile_no) {
return `<div class="customer-desc">${email_id}</div>`;
return `<div class="customer-desc">${frappe.utils.escape_html(email_id)}</div>`;
} else if (mobile_no && !email_id) {
return `<div class="customer-desc">${mobile_no}</div>`;
return `<div class="customer-desc">${frappe.utils.escape_html(mobile_no)}</div>`;
} else {
return `<div class="customer-desc">${email_id} - ${mobile_no}</div>`;
return `<div class="customer-desc">${frappe.utils.escape_html(
email_id
)} - ${frappe.utils.escape_html(mobile_no)}</div>`;
}
}
}
@@ -485,9 +487,13 @@ erpnext.PointOfSale.ItemCart = class {
get_customer_image() {
const { customer, image } = this.customer_info || {};
if (image) {
return `<div class="customer-image"><img src="${image}" alt="${image}""></div>`;
return `<div class="customer-image"><img src="${frappe.utils.escape_html(
image
)}" alt="${frappe.utils.escape_html(image)}"></div>`;
} else {
return `<div class="customer-image customer-abbr">${frappe.get_abbr(customer)}</div>`;
return `<div class="customer-image customer-abbr">${frappe.utils.escape_html(
frappe.get_abbr(customer)
)}</div>`;
}
}
@@ -549,10 +555,10 @@ erpnext.PointOfSale.ItemCart = class {
if (t.tax_amount_after_discount_amount == 0.0) return;
// if tax rate is 0, don't print it.
const description = /[0-9]+/.test(t.description)
? t.description
? frappe.utils.escape_html(t.description)
: t.rate != 0
? `${t.description} @ ${t.rate}%`
: t.description;
? `${frappe.utils.escape_html(t.description)} @ ${t.rate}%`
: frappe.utils.escape_html(t.description);
return `<div class="tax-row">
<div class="tax-label">${description}</div>
<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div>
@@ -566,8 +572,9 @@ erpnext.PointOfSale.ItemCart = class {
}
get_cart_item({ name }) {
const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`;
return this.$cart_items_wrapper.find(item_selector);
return this.$cart_items_wrapper.find(".cart-item-wrapper").filter(function () {
return $(this).attr("data-row-name") === name;
});
}
get_item_from_frm(item) {
@@ -597,7 +604,9 @@ erpnext.PointOfSale.ItemCart = class {
if (!$item_to_update.length) {
this.$cart_items_wrapper.append(
`<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div>
`<div class="cart-item-wrapper" data-row-name="${frappe.utils.escape_html(
item_data.name
)}"></div>
<div class="seperator"></div>`
);
$item_to_update = this.get_cart_item(item_data);
@@ -607,7 +616,7 @@ erpnext.PointOfSale.ItemCart = class {
`${get_item_image_html()}
<div class="item-name-desc">
<div class="item-name">
${item_data.item_name}
${frappe.utils.escape_html(item_data.item_name)}
</div>
${get_description_html()}
</div>
@@ -636,7 +645,7 @@ erpnext.PointOfSale.ItemCart = class {
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
return `
<div class="item-qty-rate">
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
<div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.amount, currency)}</div>
<div class="item-amount">${format_currency(item_data.rate, currency)}</div>
@@ -645,7 +654,7 @@ erpnext.PointOfSale.ItemCart = class {
} else {
return `
<div class="item-qty-rate">
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
<div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.rate, currency)}</div>
</div>
@@ -666,7 +675,7 @@ erpnext.PointOfSale.ItemCart = class {
}
}
item_data.description = frappe.ellipsis(item_data.description, 45);
return `<div class="item-desc">${item_data.description}</div>`;
return `<div class="item-desc">${frappe.utils.escape_html(item_data.description)}</div>`;
}
return ``;
}
@@ -678,22 +687,26 @@ erpnext.PointOfSale.ItemCart = class {
<div class="item-image">
<img
onerror="cur_pos.cart.handle_broken_image(this)"
src="${image}" alt="${frappe.get_abbr(item_name)}"">
src="${frappe.utils.escape_html(image)}" alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}">
</div>`;
} else {
return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`;
return `<div class="item-image item-abbr">${frappe.utils.escape_html(
frappe.get_abbr(item_name)
)}</div>`;
}
}
}
handle_broken_image($img) {
const item_abbr = $($img).attr("alt");
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`);
$($img)
.parent()
.replaceWith(`<div class="item-image item-abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
}
update_selector_value_in_cart_item(selector, value, item) {
const $item_to_update = this.get_cart_item(item);
$item_to_update.attr(`data-${selector}`, escape(value));
$item_to_update.attr(`data-${selector}`, value);
}
toggle_checkout_btn(show_checkout) {
@@ -892,8 +905,8 @@ erpnext.PointOfSale.ItemCart = class {
<div class="customer-display">
${this.get_customer_image()}
<div class="customer-name-desc">
<div class="customer-name">${customer_name}</div>
<div class="customer-desc">${customer}</div>
<div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
<div class="customer-desc">${frappe.utils.escape_html(customer)}</div>
</div>
</div>
<div class="customer-fields-container">
@@ -980,6 +993,7 @@ erpnext.PointOfSale.ItemCart = class {
customer: current_customer,
value: this.value,
},
freeze: true,
callback: (r) => {
if (!r.exc) {
me.customer_info[this.df.fieldname] = this.value;
@@ -1029,9 +1043,11 @@ erpnext.PointOfSale.ItemCart = class {
};
transaction_container.append(
`<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
`<div class="invoice-wrapper" data-invoice-name="${frappe.utils.escape_html(
invoice.name
)}">
<div class="invoice-name-date">
<div class="invoice-name">${invoice.name}</div>
<div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
<div class="invoice-date">${posting_datetime}</div>
</div>
<div class="invoice-total-status">
@@ -1039,7 +1055,7 @@ erpnext.PointOfSale.ItemCart = class {
${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
</div>
<div class="invoice-status">
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}">
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status] || ""}">
<span>${__(invoice.status)}</span>
</span>
</div>

View File

@@ -128,25 +128,27 @@ erpnext.PointOfSale.ItemDetails = class {
return ``;
}
this.$item_name.html(item_name);
this.$item_name.html(frappe.utils.escape_html(item_name));
this.$item_description.html(get_description_html());
this.$item_price.html(format_currency(price_list_rate, this.currency));
if (!this.hide_images && image) {
this.$item_image.html(
`<img
onerror="cur_pos.item_details.handle_broken_image(this)"
class="h-full" src="${image}"
alt="${frappe.get_abbr(item_name)}"
class="h-full" src="${frappe.utils.escape_html(image)}"
alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}"
style="object-fit: cover;">`
);
} else {
this.$item_image.html(`<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`);
this.$item_image.html(
`<div class="item-abbr">${frappe.utils.escape_html(frappe.get_abbr(item_name))}</div>`
);
}
}
handle_broken_image($img) {
const item_abbr = $($img).attr("alt");
$($img).replaceWith(`<div class="item-abbr">${item_abbr}</div>`);
$($img).replaceWith(`<div class="item-abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
}
render_discount_dom(item) {

View File

@@ -107,39 +107,45 @@ erpnext.PointOfSale.ItemSelector = class {
<div class="flex items-center justify-center border-b-grey text-6xl text-grey-100" style="height:8rem; min-height:8rem">
<img
onerror="cur_pos.item_selector.handle_broken_image(this)"
class="h-full item-img" src="${item_image}"
alt="${frappe.get_abbr(item.item_name)}"
class="h-full item-img" src="${frappe.utils.escape_html(item_image)}"
alt="${frappe.utils.escape_html(frappe.get_abbr(item.item_name))}"
>
</div>`;
} else {
return `<div class="item-qty-pill">
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
</div>
<div class="item-display abbr">${frappe.get_abbr(item.item_name)}</div>`;
<div class="item-display abbr">${frappe.utils.escape_html(frappe.get_abbr(item.item_name))}</div>`;
}
}
return `<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
data-rate="${escape(price_list_rate || 0)}"
data-stock-uom="${escape(item.stock_uom)}"
title="${item.item_name}">
data-item-code="${frappe.utils.escape_html(item.item_code)}" data-serial-no="${frappe.utils.escape_html(
serial_no
)}"
data-batch-no="${frappe.utils.escape_html(batch_no)}" data-uom="${frappe.utils.escape_html(uom)}"
data-rate="${frappe.utils.escape_html(price_list_rate || 0)}"
data-stock-uom="${frappe.utils.escape_html(item.stock_uom)}"
title="${frappe.utils.escape_html(item.item_name)}">
${get_item_image_html()}
<div class="item-detail">
<div class="item-name">
${frappe.ellipsis(item.item_name, 18)}
${frappe.utils.escape_html(frappe.ellipsis(item.item_name, 18))}
</div>
<div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}</div>
<div class="item-rate">${
format_currency(price_list_rate, item.currency, precision) || 0
} / ${frappe.utils.escape_html(uom)}</div>
</div>
</div>`;
}
handle_broken_image($img) {
const item_abbr = $($img).attr("alt");
$($img).parent().replaceWith(`<div class="item-display abbr">${item_abbr}</div>`);
$($img)
.parent()
.replaceWith(`<div class="item-display abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
}
make_search_bar() {
@@ -252,14 +258,13 @@ erpnext.PointOfSale.ItemSelector = class {
this.$component.on("click", ".item-wrapper", function () {
const $item = $(this);
const item_code = unescape($item.attr("data-item-code"));
let batch_no = unescape($item.attr("data-batch-no"));
let serial_no = unescape($item.attr("data-serial-no"));
let uom = unescape($item.attr("data-uom"));
let rate = unescape($item.attr("data-rate"));
let stock_uom = unescape($item.attr("data-stock-uom"));
const item_code = $item.attr("data-item-code");
let batch_no = $item.attr("data-batch-no");
let serial_no = $item.attr("data-serial-no");
let uom = $item.attr("data-uom");
let rate = $item.attr("data-rate");
let stock_uom = $item.attr("data-stock-uom");
// escape(undefined) returns "undefined" then unescape returns "undefined"
batch_no = batch_no === "undefined" ? undefined : batch_no;
serial_no = serial_no === "undefined" ? undefined : serial_no;
uom = uom === "undefined" ? undefined : uom;

View File

@@ -38,7 +38,7 @@ erpnext.PointOfSale.PastOrderList = class {
});
const me = this;
this.$invoices_container.on("click", ".invoice-wrapper", function () {
const invoice_name = unescape($(this).attr("data-invoice-name"));
const invoice_name = $(this).attr("data-invoice-name");
me.events.open_invoice_data(invoice_name);
});
@@ -99,14 +99,14 @@ erpnext.PointOfSale.PastOrderList = class {
const posting_datetime = frappe.datetime.str_to_user(
invoice.posting_date + " " + invoice.posting_time
);
return `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
return `<div class="invoice-wrapper" data-invoice-name="${frappe.utils.escape_html(invoice.name)}">
<div class="invoice-name-date">
<div class="invoice-name">${invoice.name}</div>
<div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
<div class="invoice-date">
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
${frappe.ellipsis(invoice.customer_name, 20)}
${frappe.utils.escape_html(frappe.ellipsis(invoice.customer_name, 20))}
</div>
</div>
<div class="invoice-total-status">

View File

@@ -81,23 +81,27 @@ erpnext.PointOfSale.PastOrderSummary = class {
return `<div class="left-section">
<div class="customer-section">
<div class="customer-name">${doc.customer_name}</div>
${is_customer_naming_by_customer_name ? `<div class="customer-code">${doc.customer}</div>` : ""}
<div class="customer-email">${this.customer_email}</div>
<div class="customer-name">${frappe.utils.escape_html(doc.customer_name)}</div>
${
is_customer_naming_by_customer_name
? `<div class="customer-code">${frappe.utils.escape_html(doc.customer)}</div>`
: ""
}
<div class="customer-email">${frappe.utils.escape_html(this.customer_email)}</div>
</div>
<div class="cashier">${__("Sold by")}: ${doc.owner}</div>
<div class="cashier">${__("Sold by")}: ${frappe.utils.escape_html(doc.owner)}</div>
</div>
<div class="right-section">
<div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div>
<div class="invoice-name">${doc.name}</div>
<div class="invoice-name">${frappe.utils.escape_html(doc.name)}</div>
<span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${__(doc.status)}</span></span>
</div>`;
}
get_item_html(doc, item_data) {
return `<div class="item-row-wrapper">
<div class="item-name">${item_data.item_name}</div>
<div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div>
<div class="item-name">${frappe.utils.escape_html(item_data.item_name)}</div>
<div class="item-qty">${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</div>
<div class="item-rate-disc">${get_rate_discount_html()}</div>
</div>`;
@@ -139,10 +143,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
.map((t) => {
// if tax rate is 0, don't print it.
const description = /[0-9]+/.test(t.description)
? t.description
? frappe.utils.escape_html(t.description)
: t.rate != 0
? `${t.description} @ ${t.rate}%`
: t.description;
? `${frappe.utils.escape_html(t.description)} @ ${t.rate}%`
: frappe.utils.escape_html(t.description);
return `
<div class="tax-row">
<div class="tax-label">${description}</div>

View File

@@ -408,8 +408,10 @@ erpnext.PointOfSale.Payment = class {
return `
<div class="payment-mode-wrapper">
<div class="mode-of-payment" data-mode="${mode}" data-payment-type="${payment_type}">
${p.mode_of_payment}
<div class="mode-of-payment" data-mode="${mode}" data-payment-type="${frappe.utils.escape_html(
payment_type
)}">
${frappe.utils.escape_html(p.mode_of_payment)}
<div class="${mode}-amount pay-amount">${amount}</div>
<div class="${mode} mode-of-payment-control"></div>
</div>
@@ -544,7 +546,7 @@ erpnext.PointOfSale.Payment = class {
<div class="mode-of-payment loyalty-card" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
Redeem Loyalty Points
<div class="loyalty-amount-amount pay-amount">${amount}</div>
<div class="loyalty-amount-name">${loyalty_program}</div>
<div class="loyalty-amount-name">${frappe.utils.escape_html(loyalty_program)}</div>
<div class="loyalty-amount mode-of-payment-control"></div>
</div>
</div>`

View File

@@ -138,12 +138,30 @@ class Analytics:
self.get_sales_transactions_based_on_project()
self.get_rows()
def _get_permitted_parent_names(self):
return frappe.get_list(
self.filters.doc_type,
fields=["name"],
filters={
"docstatus": 1,
"company": ["in", self.filters.company],
self.date_field: ("between", [self.filters.from_date, self.filters.to_date]),
},
pluck="name",
)
def get_sales_transactions_based_on_order_type(self):
if self.filters["value_quantity"] == "Value":
value_field = "base_net_total"
else:
value_field = "total_qty"
permitted_names = self._get_permitted_parent_names()
if not permitted_names:
self.entries = []
self.get_teams()
return
doctype = DocType(self.filters.doc_type)
self.entries = (
@@ -153,12 +171,7 @@ class Analytics:
doctype[self.date_field],
doctype[value_field].as_("value_field"),
)
.where(
(doctype.docstatus == 1)
& (doctype.company.isin(self.filters.company))
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
& (IfNull(doctype.order_type, "") != "")
)
.where((doctype.name.isin(permitted_names)) & (IfNull(doctype.order_type, "") != ""))
.orderby(doctype.order_type)
).run(as_dict=True)
@@ -186,8 +199,10 @@ class Analytics:
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
filters.update({"is_opening": "No"})
self.entries = frappe.get_all(
self.filters.doc_type, fields=[entity, entity_name, value_field, self.date_field], filters=filters
self.entries = frappe.get_list(
self.filters.doc_type,
fields=[entity, entity_name, value_field, self.date_field],
filters=filters,
)
self.entity_names = {}
@@ -200,6 +215,12 @@ class Analytics:
else:
value_field = "stock_qty"
permitted_names = self._get_permitted_parent_names()
if not permitted_names:
self.entries = []
self.entity_names = {}
return
doctype = DocType(self.filters.doc_type)
doctype_item = DocType(f"{self.filters.doc_type} Item")
@@ -214,11 +235,7 @@ class Analytics:
doctype_item[value_field].as_("value_field"),
doctype[self.date_field],
)
.where(
(doctype_item.docstatus == 1)
& (doctype.company.isin(self.filters.company))
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
)
.where((doctype_item.docstatus == 1) & (doctype.name.isin(permitted_names)))
).run(as_dict=True)
self.entity_names = {}
@@ -248,7 +265,7 @@ class Analytics:
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
filters.update({"is_opening": "No"})
self.entries = frappe.get_all(
self.entries = frappe.get_list(
self.filters.doc_type,
fields=[entity_field, value_field, self.date_field],
filters=filters,
@@ -261,6 +278,12 @@ class Analytics:
else:
value_field = "qty"
permitted_names = self._get_permitted_parent_names()
if not permitted_names:
self.entries = []
self.get_groups()
return
doctype = DocType(self.filters.doc_type)
doctype_item = DocType(f"{self.filters.doc_type} Item")
@@ -273,11 +296,7 @@ class Analytics:
doctype_item[value_field].as_("value_field"),
doctype[self.date_field],
)
.where(
(doctype_item.docstatus == 1)
& (doctype.company.isin(self.filters.company))
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
)
.where((doctype_item.docstatus == 1) & (doctype.name.isin(permitted_names)))
).run(as_dict=True)
self.get_groups()
@@ -300,8 +319,10 @@ class Analytics:
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
filters.update({"is_opening": "No"})
self.entries = frappe.get_all(
self.filters.doc_type, fields=[entity, value_field, self.date_field], filters=filters
self.entries = frappe.get_list(
self.filters.doc_type,
fields=[entity, value_field, self.date_field],
filters=filters,
)
def get_rows(self):

View File

@@ -77,13 +77,11 @@ class CustomerGroup(NestedSet):
def get_parent_customer_groups(customer_group):
lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"])
return frappe.db.sql(
"""select name from `tabCustomer Group`
where lft <= %s and rgt >= %s
order by lft asc""",
(lft, rgt),
as_dict=True,
return frappe.get_all(
"Customer Group",
filters=[["lft", "<=", lft], ["rgt", ">=", rgt]],
fields=["name"],
order_by="lft asc",
)

View File

@@ -70,3 +70,13 @@ class SupplierGroup(NestedSet):
def on_trash(self):
NestedSet.validate_if_child_exists(self)
frappe.utils.nestedset.update_nsm(self)
def get_parent_supplier_groups(supplier_group):
lft, rgt = frappe.db.get_value("Supplier Group", supplier_group, ["lft", "rgt"])
return frappe.get_all(
"Supplier Group",
filters=[["lft", "<=", lft], ["rgt", ">=", rgt]],
fields=["name"],
order_by="lft asc",
)

View File

@@ -24,6 +24,7 @@ def after_install():
set_single_defaults()
create_print_setting_custom_fields()
create_address_and_contact_custom_fields()
create_custom_company_links()
add_all_roles_to("Administrator")
create_default_success_action()
@@ -132,6 +133,37 @@ def create_print_setting_custom_fields():
)
def create_address_and_contact_custom_fields():
create_custom_fields(
{
"Address": [
{
"label": _("Tax Category"),
"fieldname": "tax_category",
"fieldtype": "Link",
"options": "Tax Category",
"insert_after": "fax",
},
{
"label": _("Is Your Company Address"),
"fieldname": "is_your_company_address",
"fieldtype": "Check",
"default": "0",
"insert_after": "linked_with",
},
],
"Contact": [
{
"label": _("Is Billing Contact"),
"fieldname": "is_billing_contact",
"fieldtype": "Check",
"insert_after": "is_primary_contact",
},
],
}
)
def create_custom_company_links():
"""Add link fields to Company in Email Account and Communication.

View File

@@ -4115,9 +4115,14 @@
},
"Japan": {
"Japan Tax": {
"account_name": "CT",
"tax_rate": 5.00
"Japan Tax 10%": {
"account_name": "CT 10%",
"tax_rate": 10.00,
"default": 1
},
"Japan Tax 8%": {
"account_name": "CT 8%",
"tax_rate": 8.00
}
},

View File

@@ -88,7 +88,7 @@ def get_all_customers(date_range, company, field, limit=None):
@frappe.whitelist()
def get_all_items(date_range, company, field, limit=None):
def get_all_items(date_range: str, company: str, field: str, limit: int | None = None):
if field in ("available_stock_qty", "available_stock_value"):
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
results = frappe.db.get_all(
@@ -103,21 +103,21 @@ def get_all_items(date_range, company, field, limit=None):
else:
if field == "total_sales_amount":
select_field = "base_net_amount"
select_doctype = "Sales Order"
select_doctype = "Sales Invoice"
elif field == "total_purchase_amount":
select_field = "base_net_amount"
select_doctype = "Purchase Order"
select_doctype = "Purchase Invoice"
elif field == "total_qty_sold":
select_field = "stock_qty"
select_doctype = "Sales Order"
select_doctype = "Sales Invoice"
elif field == "total_qty_purchased":
select_field = "stock_qty"
select_doctype = "Purchase Order"
select_doctype = "Purchase Invoice"
filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
filters.append(["posting_date", "between", [from_date, to_date]])
child_doctype = f"{select_doctype} Item"
return frappe.get_list(

View File

@@ -30,11 +30,20 @@ class DeprecatedSerialNoValuation:
def get_incoming_value_for_serial_nos(self, serial_nos):
from erpnext.stock.utils import get_combine_datetime
do_not_fetch_rate = frappe.db.get_single_value(
"Stock Reposting Settings", "do_not_fetch_incoming_rate_from_serial_no"
)
# get rate from serial nos within same company
incoming_values = 0.0
for serial_no in serial_nos:
sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1)
if sn_details and sn_details.purchase_rate and sn_details.company == self.sle.company:
if (
sn_details
and sn_details.purchase_rate
and sn_details.company == self.sle.company
and (not frappe.flags.through_repost_item_valuation or not do_not_fetch_rate)
):
self.serial_no_incoming_rate[serial_no] += flt(sn_details.purchase_rate)
incoming_values += self.serial_no_incoming_rate[serial_no]
continue

View File

@@ -263,8 +263,9 @@ def update_qty(bin_name, args):
# actual qty is already updated by processing current voucher
actual_qty = bin_details.actual_qty or 0.0
# actual qty is not up to date in case of backdated transaction
if future_sle_exists(args):
# actual qty is not up to date in case of backdated transactions
# or when cancellations are the most recent SLE
if future_sle_exists(args) or args.get("is_cancelled"):
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))

View File

@@ -226,13 +226,6 @@ frappe.ui.form.on("Item", {
});
frm.set_df_property("is_fixed_asset", "read_only", frm.doc.__onload?.asset_exists ? 1 : 0);
frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0);
frm.set_query("item_group", () => {
return {
filters: {
is_group: 0,
},
};
});
},
validate: function (frm) {
@@ -411,12 +404,6 @@ $.extend(erpnext.item, {
};
};
frm.fields_dict["item_group"].get_query = function (doc, cdt, cdn) {
return {
filters: [["Item Group", "docstatus", "!=", 2]],
};
};
frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function (
doc,
cdt,
@@ -594,11 +581,10 @@ $.extend(erpnext.item, {
default: 0,
onchange: function () {
let selected_attributes = get_selected_attributes();
let lengths = [];
Object.keys(selected_attributes).map((key) => {
lengths.push(selected_attributes[key].length);
let lengths = Object.keys(selected_attributes).map((key) => {
return selected_attributes[key].length;
});
if (lengths.includes(0)) {
if (!lengths.length) {
me.multiple_variant_dialog.get_primary_btn().html(__("Create Variants"));
me.multiple_variant_dialog.disable_primary_action();
} else {
@@ -635,7 +621,7 @@ $.extend(erpnext.item, {
fieldtype: "HTML",
fieldname: "help",
options: `<label class="control-label">
${__("Select at least one value from each of the attributes.")}
${__("Select at least one attribute value.")}
</label>`,
},
]
@@ -693,6 +679,9 @@ $.extend(erpnext.item, {
selected_attributes[attribute_name].push($(opt).attr("data-fieldname"));
}
});
if (!selected_attributes[attribute_name].length) {
delete selected_attributes[attribute_name];
}
});
return selected_attributes;

View File

@@ -855,8 +855,13 @@ class Item(Document):
if disabled:
frappe.throw(_("Attribute {0} is disabled.").format(frappe.bold(d.attribute)))
if not numeric_values and not frappe.db.exists(
"Item Attribute Value", {"parent": d.attribute, "attribute_value": d.attribute_value}
if (
not numeric_values
and d.attribute_value
and not frappe.db.exists(
"Item Attribute Value",
{"parent": d.attribute, "attribute_value": d.attribute_value},
)
):
frappe.throw(
_("Attribute Value {0} is not valid for the selected attribute {1}.").format(

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"creation": "2014-07-11 11:51:00.453717",
"doctype": "DocType",
"editable_grid": 1,
@@ -12,7 +13,10 @@
"col_break3",
"amount",
"base_amount",
"has_corrective_cost"
"has_corrective_cost",
"has_operating_cost",
"operation_id",
"qty"
],
"fields": [
{
@@ -70,12 +74,36 @@
"fieldtype": "Check",
"label": "Has Corrective Cost",
"read_only": 1
},
{
"default": "0",
"fieldname": "has_operating_cost",
"fieldtype": "Check",
"label": "Has Operating Cost",
"read_only": 1
},
{
"fieldname": "operation_id",
"fieldtype": "Data",
"hidden": 1,
"label": "Operation ID",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Qty",
"no_copy": 1,
"non_negative": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-01-20 12:22:03.455762",
"modified": "2026-05-19 12:21:07.953801",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Taxes and Charges",
@@ -83,4 +111,4 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

@@ -21,9 +21,12 @@ class LandedCostTaxesandCharges(Document):
exchange_rate: DF.Float
expense_account: DF.Link | None
has_corrective_cost: DF.Check
has_operating_cost: DF.Check
operation_id: DF.Data | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
qty: DF.Float
# end: auto-generated types
pass

View File

@@ -510,14 +510,7 @@ class PurchaseReceipt(BuyingController):
else flt(item.net_amount, item.precision("net_amount"))
)
outgoing_amount = (
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
)
outgoing_amount = item.base_net_amount
if self.is_internal_transfer() and item.valuation_rate:
outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
credit_amount = outgoing_amount
@@ -1257,7 +1250,7 @@ def get_billed_qty_amount_against_purchase_receipt(pr_doc):
.on(parent_table.name == table.parent)
.select(
table.pr_detail,
fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"),
fn.Sum(table.base_net_amount).as_("amount"),
fn.Sum(table.qty).as_("qty"),
)
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
@@ -1303,7 +1296,7 @@ def get_billed_qty_amount_against_purchase_order(pr_doc):
.select(
table.po_detail,
fn.Sum(table.qty).as_("qty"),
fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"),
fn.Sum(table.base_net_amount).as_("amount"),
)
.where((table.po_detail.isin(po_names)) & (table.docstatus == 1) & (table.pr_detail.isnull()))
.groupby(table.po_detail)

View File

@@ -4539,7 +4539,7 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(srbnb_cost, 1500)
def test_valuation_rate_for_rejected_materials_without_accepted_materials(self):
def test_valuation_rate_for_rejected_materials_withoout_accepted_materials(self):
item = make_item("Test Item with Rej Material Valuation WO Accepted", {"is_stock_item": 1})
company = "_Test Company with perpetual inventory"
@@ -5106,33 +5106,6 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(row.warehouse, "_Test Warehouse 1 - _TC")
self.assertEqual(row.incoming_rate, 100)
def test_bill_for_rejected_quantity_in_purchase_invoice(self):
item_code = make_item("Test Rejected Qty", {"is_stock_item": 1}).name
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
pr = make_purchase_receipt(
item_code=item_code,
qty=10,
rejected_qty=2,
rate=10,
warehouse="_Test Warehouse - _TC",
)
self.assertEqual(pr.total_qty, 10)
self.assertEqual(pr.total, 100)
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 1)
pr = make_purchase_receipt(
item_code=item_code,
qty=10,
rejected_qty=2,
rate=10,
warehouse="_Test Warehouse - _TC",
)
self.assertEqual(pr.total_qty, 12)
self.assertEqual(pr.total, 120)
def test_different_exchange_rate_in_pr_and_pi(self):
from erpnext.accounts.doctype.account.test_account import create_account
@@ -5197,6 +5170,157 @@ class TestPurchaseReceipt(FrappeTestCase):
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
)
@change_settings(
"Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0}
)
def test_srbnb_with_inclusive_tax_and_rate_change_in_pi(self):
"""
When 'Set Landed Cost Based on PI Rate' is enabled and PI has an inclusive tax:
- PR: qty=2, rate=1000 INR → base_net_amount=2000
- PI: rate changed to 2000, 5% tax included in basic rate
→ PI base_net_amount = 2 * 2000 / 1.05 ≈ 3809.52
The system must use PI's base_net_amount (not amount=4000) so that
SRBNB credit on PR = 3809.52, not 4000.
"""
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
cost_center = "Main - TCP1"
item_code = make_item(
"Test Item for SRBNB Inclusive Tax Rate Change",
{"is_stock_item": 1},
).name
pr = make_purchase_receipt(
item_code=item_code,
qty=2,
rate=1000,
company=company,
warehouse=warehouse,
cost_center=cost_center,
)
pi = make_purchase_invoice(pr.name)
pi.items[0].rate = 2000
pi.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - TCP1",
"category": "Total",
"add_deduct_tax": "Add",
"included_in_print_rate": 1,
"rate": 5,
"description": "Test Inclusive Tax",
"cost_center": cost_center,
},
)
pi.save()
pi.submit()
pr.reload()
# PI base_net_amount = qty * (rate / (1 + tax_rate/100)) = 2 * (2000 / 1.05)
pi_base_net_amount = flt(2 * 2000 / 1.05, 2)
pr_base_net_amount = flt(pr.items[0].amount, 2) # 2 * 1000 = 2000
expected_diff = flt(pi_base_net_amount - pr_base_net_amount, 2)
self.assertAlmostEqual(pr.items[0].amount_difference_with_purchase_invoice, expected_diff, places=2)
# Total SRBNB credit = PR base_net_amount + amount_difference = PI base_net_amount
srbnb_account = "Stock Received But Not Billed - TCP1"
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True)
srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account)
self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2)
@change_settings(
"Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0}
)
def test_srbnb_with_inclusive_tax_and_exchange_rate_change_in_pi(self):
"""
When 'Set Landed Cost Based on PI Rate' is enabled, PI has an inclusive tax, and only
the exchange rate changes on the PI (rate stays the same):
- PR: qty=2, rate=100 USD, conversion_rate=70 → base_net_amount=14000 INR
- PI: same rate=100 USD, conversion_rate changed to 90, 5% tax included in basic rate
→ PI base_net_amount = 2 * (100 / 1.05) * 90 ≈ 17142.86 INR
The system must use PI's base_net_amount (not amount = 2*100*90 = 18000) so that
SRBNB credit on PR = 17142.86, not 18000.
"""
from erpnext.accounts.doctype.account.test_account import create_account
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
cost_center = "Main - TCP1"
party_account = create_account(
account_name="USD Payable For SRBNB Exchange Rate Test",
parent_account="Accounts Payable - TCP1",
account_type="Payable",
company=company,
account_currency="USD",
)
supplier = create_supplier(
supplier_name="_Test USD Supplier for SRBNB Exchange Rate",
default_currency="USD",
party_account=party_account,
).name
item_code = make_item(
"Test Item for SRBNB Inclusive Tax Exchange Rate Change",
{"is_stock_item": 1},
).name
pr = make_purchase_receipt(
item_code=item_code,
qty=2,
rate=100,
currency="USD",
conversion_rate=70,
company=company,
warehouse=warehouse,
supplier=supplier,
)
pi = make_purchase_invoice(pr.name)
pi.conversion_rate = 90
pi.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - TCP1",
"category": "Total",
"add_deduct_tax": "Add",
"included_in_print_rate": 1,
"rate": 5,
"description": "Test Inclusive Tax",
"cost_center": cost_center,
},
)
pi.save()
pi.submit()
pr.reload()
# PI base_net_amount = qty * (rate / (1 + tax_rate/100)) * new_conversion_rate
# = 2 * (100 / 1.05) * 90 ≈ 17142.86 INR
# PR base_net_amount = qty * rate * pr_conversion_rate = 2 * 100 * 70 = 14000 INR
tax_amount_pr = (200 - flt(200 / 1.05, 2)) * 90
pi_base_net_amount = flt(2 * 100 * 90) - flt(tax_amount_pr)
pr_base_net_amount = flt(2 * 100 * 70)
expected_diff = flt(pi_base_net_amount - pr_base_net_amount)
self.assertAlmostEqual(pr.items[0].amount_difference_with_purchase_invoice, expected_diff, places=2)
# Total SRBNB credit = PR base_net_amount + amount_difference = PI base_net_amount
srbnb_account = "Stock Received But Not Billed - TCP1"
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True)
srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account)
self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -2194,25 +2194,6 @@ def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, wareh
return doc
@frappe.whitelist()
def update_serial_or_batch(bundle_id, serial_no=None, batch_no=None):
if batch_no and not serial_no:
if qty := frappe.db.get_value(
"Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty"
):
frappe.db.set_value(
"Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty", qty + 1
)
return
doc = frappe.get_cached_doc("Serial and Batch Bundle", bundle_id)
if not serial_no and not batch_no:
return
doc.append("entries", {"serial_no": serial_no, "batch_no": batch_no, "qty": 1})
doc.save(ignore_permissions=True)
def get_serial_and_batch_ledger(**kwargs):
kwargs = frappe._dict(kwargs)

View File

@@ -774,7 +774,7 @@ class StockEntry(StockController):
else:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Manufacture":
if self.purpose in ["Manufacture", "Repack"]:
if d.is_finished_item or d.is_scrap_item:
d.s_warehouse = None
if not d.t_warehouse:
@@ -1174,13 +1174,21 @@ class StockEntry(StockController):
)
def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost):
finished_items = [d.item_code for d in self.get("items") if d.is_finished_item]
finished_items = [
d.item_code for d in self.get("items") if d.is_finished_item and not d.set_basic_rate_manually
]
if len(finished_items) == 1:
return flt(outgoing_items_cost / finished_item_qty)
else:
unique_finished_items = set(finished_items)
if len(unique_finished_items) == 1:
total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
if unique_finished_items:
total_fg_qty = sum(
[
flt(d.transfer_qty)
for d in self.items
if d.is_finished_item and not d.set_basic_rate_manually
]
)
return flt(outgoing_items_cost / total_fg_qty)
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float:
@@ -3124,12 +3132,12 @@ class StockEntry(StockController):
args = {
"source_dt": "Stock Entry Detail",
"target_field": "transferred_qty",
"target_ref_field": "qty",
"target_ref_field": "transfer_qty",
"target_dt": "Stock Entry Detail",
"join_field": "ste_detail",
"target_parent_dt": "Stock Entry",
"target_parent_field": "per_transferred",
"source_field": "qty",
"source_field": "transfer_qty",
"percent_join_field": "against_stock_entry",
}
@@ -3362,6 +3370,33 @@ def get_work_order_details(work_order, company):
}
def get_consumed_operating_cost(work_order, bom_no, operation_id=None):
table = frappe.qb.DocType("Stock Entry")
child_table = frappe.qb.DocType("Landed Cost Taxes and Charges")
query = (
frappe.qb.from_(child_table)
.join(table)
.on(child_table.parent == table.name)
.select(
Sum(child_table.amount).as_("consumed_cost"),
Sum(child_table.qty).as_("consumed_qty"),
)
.where(
(table.docstatus == 1)
& (table.work_order == work_order)
& (table.purpose == "Manufacture")
& (table.bom_no == bom_no)
& (child_table.has_operating_cost == 1)
)
)
if operation_id:
query = query.where(child_table.operation_id == operation_id)
data = query.run(as_dict=True)
return data[0] if data else frappe._dict()
def get_operating_cost_per_unit(work_order=None, bom_no=None):
operating_cost_per_unit = 0
if work_order:

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"allow_copy": 1,
"autoname": "MAT-SLE-.YYYY.-.#####",
"creation": "2013-01-29 19:25:42",
@@ -204,7 +205,7 @@
{
"fieldname": "valuation_rate",
"fieldtype": "Currency",
"label": "Valuation Rate",
"label": "Average Rate",
"oldfieldname": "valuation_rate",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
@@ -362,11 +363,11 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-10-04 09:59:15.546556",
"modified": "2026-05-26 19:07:43.537450",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",
"naming_rule": "Expression (old style)",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{

View File

@@ -82,6 +82,7 @@ class StockReconciliation(StockController):
self.set_total_qty_and_amount()
self.validate_putaway_capacity()
self.validate_inventory_dimension()
self.validate_uom_is_integer("stock_uom", "qty")
if self._action == "submit":
self.validate_reserved_stock()
@@ -566,15 +567,18 @@ class StockReconciliation(StockController):
def calculate_difference_amount(self, item, item_dict):
qty_precision = item.precision("qty")
val_precision = item.precision("valuation_rate")
amount_precision = item.precision("amount")
new_qty = flt(item.qty, qty_precision)
new_valuation_rate = flt(item.valuation_rate or item_dict.get("rate"), val_precision)
new_valuation_rate = flt(item.valuation_rate or item_dict.get("rate"))
current_qty = flt(item_dict.get("qty"), qty_precision)
current_valuation_rate = flt(item_dict.get("rate"), val_precision)
current_valuation_rate = flt(item_dict.get("rate"))
self.difference_amount += (new_qty * new_valuation_rate) - (current_qty * current_valuation_rate)
new_amount = flt(new_qty * new_valuation_rate, amount_precision)
current_amount = flt(current_qty * current_valuation_rate, amount_precision)
self.difference_amount += new_amount - current_amount
def validate_data(self):
def _get_msg(row_num, msg):
@@ -874,7 +878,7 @@ class StockReconciliation(StockController):
"company": self.company,
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
"is_cancelled": 1 if self.docstatus == 2 else 0,
"valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")),
"valuation_rate": flt(row.valuation_rate),
}
)
@@ -1034,86 +1038,6 @@ class StockReconciliation(StockController):
else:
self._cancel()
def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
from erpnext.stock.stock_ledger import get_valuation_rate
for row in self.items:
if voucher_detail_no != row.name:
continue
if row.current_qty < 0:
return
val_rate = 0.0
current_qty = 0.0
if row.current_serial_and_batch_bundle:
current_qty = self.get_current_qty_for_serial_or_batch(row, sle_creation)
elif row.serial_no:
item_dict = get_stock_balance_for(
row.item_code,
row.warehouse,
self.posting_date,
self.posting_time,
row=row,
company=self.company,
)
current_qty = item_dict.get("qty")
row.current_serial_no = item_dict.get("serial_nos")
row.current_valuation_rate = item_dict.get("rate")
val_rate = item_dict.get("rate")
elif row.batch_no:
current_qty = get_batch_qty_for_stock_reco(
row.item_code,
row.warehouse,
row.batch_no,
self.posting_date,
self.posting_time,
self.name,
sle_creation,
)
precesion = row.precision("current_qty")
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
if not row.serial_no:
val_rate = get_incoming_rate(
frappe._dict(
{
"item_code": row.item_code,
"warehouse": row.warehouse,
"qty": current_qty * -1,
"serial_and_batch_bundle": row.current_serial_and_batch_bundle,
"batch_no": row.batch_no,
"voucher_type": self.doctype,
"voucher_no": self.name,
"company": self.company,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
}
)
)
row.current_valuation_rate = val_rate
row.current_qty = current_qty
row.db_set(
{
"current_qty": row.current_qty,
"current_valuation_rate": row.current_valuation_rate,
"current_amount": flt(row.current_qty * row.current_valuation_rate),
}
)
if add_new_sle and not frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
"name",
):
if not row.current_serial_and_batch_bundle:
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
row.reload()
self.add_missing_stock_ledger_entry(row, voucher_detail_no, sle_creation)
def add_missing_stock_ledger_entry(self, row, voucher_detail_no, sle_creation):
if row.current_qty == 0:
return

View File

@@ -1039,7 +1039,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
sr.reload()
self.assertTrue(sr.items[0].serial_and_batch_bundle)
self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
def test_not_reconcile_all_batch(self):
from erpnext.stock.doctype.batch.batch import get_batch_qty

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"allow_rename": 1,
"beta": 1,
"creation": "2021-10-01 10:56:30.814787",
@@ -13,6 +14,7 @@
"end_time",
"limits_dont_apply_on",
"item_based_reposting",
"do_not_fetch_incoming_rate_from_serial_no",
"errors_notification_section",
"notify_reposting_error_to_role"
],
@@ -65,12 +67,19 @@
"fieldname": "errors_notification_section",
"fieldtype": "Section Break",
"label": "Errors Notification"
},
{
"default": "0",
"description": "For legacy serial nos, do not fetch incoming rate from serial no and calculate it based on the inward transaction",
"fieldname": "do_not_fetch_incoming_rate_from_serial_no",
"fieldtype": "Check",
"label": "Do not fetch incoming rate from Serial No"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-07-08 11:27:46.659056",
"modified": "2026-05-15 12:59:34.392491",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",

View File

@@ -16,6 +16,7 @@ class StockRepostingSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
do_not_fetch_incoming_rate_from_serial_no: DF.Check
end_time: DF.Time | None
item_based_reposting: DF.Check
limit_reposting_timeslot: DF.Check

View File

@@ -294,9 +294,8 @@ def clean_all_descriptions():
@frappe.whitelist()
def get_enable_stock_uom_editing():
return frappe.get_cached_value(
return frappe.get_single_value(
"Stock Settings",
None,
["allow_to_edit_stock_uom_qty_for_sales", "allow_to_edit_stock_uom_qty_for_purchase"],
as_dict=1,
)

View File

@@ -1044,7 +1044,7 @@ def insert_item_price(args):
)
item_price.insert()
frappe.msgprint(
_("Item Price Added for {0} in Price List {1}").format(
_("Item Price added for {0} in Price List - {1}").format(
get_link_to_form("Item", args.item_code), args.price_list
),
alert=True,
@@ -1070,7 +1070,9 @@ def insert_item_price(args):
)
item_price.insert()
frappe.msgprint(
_("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list),
_("Item Price added for {0} in Price List - {1}").format(
get_link_to_form("Item", args.item_code), args.price_list
),
alert=True,
)

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,11 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data
from erpnext.stock.report.stock_ageing.stock_ageing import (
FIFOSlots,
format_report_data,
get_average_age,
)
class TestStockAgeing(FrappeTestCase):
@@ -868,6 +872,560 @@ class TestStockAgeing(FrappeTestCase):
range_valuations = range_values[1::2]
self.assertEqual(range_valuations, [15, 7.5, 20, 5])
def test_batch_item_report_formatting_preserves_mixed_fifo_slots(self):
item_details = {
"Batch Mixed Item": {
"details": frappe._dict(
name="Batch Mixed Item",
item_name="Batch Mixed Item",
description="Batch Mixed Item",
item_group=None,
brand=None,
has_batch_no=True,
stock_uom="Nos",
),
"fifo_queue": [
["SA-BATCH-MIXED-SLOT", 1, 5.0, "2021-12-01", 50.0],
[3.0, "2021-12-02", 30.0],
],
"has_serial_no": False,
"total_qty": 8.0,
}
}
report_data = format_report_data(self.filters, item_details, self.filters["to_date"])
self.assertEqual(report_data[0][7:15], [8.0, 80.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
def test_average_age_accepts_batchwise_valuation_slots(self):
fifo_queue = [["SA-BATCH-SLOT", 1, 5.0, "2021-12-01", 50.0]]
self.assertEqual(get_average_age(fifo_queue, self.filters["to_date"]), 9.0)
def test_serial_transfer_replay_preserves_serial_slots(self):
fifo_slots = FIFOSlots(self.filters, [])
transfer_key = ("001", "Serial Item", "WH 1")
fifo_slots.transferred_item_details[transfer_key] = [[2, "2021-12-01", 20]]
row = frappe._dict(
name="Serial Item",
actual_qty=2,
stock_value_difference=20,
posting_date="2021-12-05",
has_serial_no=True,
)
fifo_queue = []
fifo_slots._compute_incoming_stock(row, fifo_queue, transfer_key, ["SN-A", "SN-B"], [])
self.assertEqual(fifo_queue, [["SN-A", "2021-12-01", 10.0], ["SN-B", "2021-12-01", 10.0]])
self.assertFalse(fifo_slots.transferred_item_details[transfer_key])
def test_batch_transfer_replay_removes_zeroed_negative_slot(self):
fifo_slots = FIFOSlots(self.filters, [])
fifo_queue = [["SA-ZERO-BATCH", 1, -4, "2021-12-01", -40]]
fifo_slots._add_transfer_slot_to_fifo_queue(fifo_queue, ["SA-ZERO-BATCH", 1, 4, "2021-12-02", 40])
self.assertEqual(fifo_queue, [])
def test_batchwise_valuation(self):
from erpnext.stock.doctype.item.test_item import make_item
item_code = make_item(
"Test Stock Ageing Batchwise Valuation",
{
"is_stock_item": 1,
"has_batch_no": 1,
"valuation_method": "FIFO",
},
).name
def make_batch(batch_id, use_batchwise_valuation):
if not frappe.db.exists("Batch", batch_id):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_id,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", use_batchwise_valuation)
batchwise_above_90 = "SA-BATCHWISE-ABOVE-90"
non_batchwise_above_90 = "SA-NON-BATCHWISE-ABOVE-90"
batchwise_61_90 = "SA-BATCHWISE-61-90"
non_batchwise_61_90 = "SA-NON-BATCHWISE-61-90"
batchwise_31_60 = "SA-BATCHWISE-31-60"
non_batchwise_31_60 = "SA-NON-BATCHWISE-31-60"
batchwise_0_30 = "SA-BATCHWISE-0-30"
non_batchwise_0_30 = "SA-NON-BATCHWISE-0-30"
for batch_id, use_batchwise_valuation in {
batchwise_above_90: 1,
non_batchwise_above_90: 0,
batchwise_61_90: 1,
non_batchwise_61_90: 0,
batchwise_31_60: 1,
non_batchwise_31_60: 0,
batchwise_0_30: 1,
non_batchwise_0_30: 0,
}.items():
make_batch(batch_id, use_batchwise_valuation)
qty_after_transaction = 0
def make_sle(posting_date, voucher_no, batch_no, actual_qty, stock_value_difference):
nonlocal qty_after_transaction
qty_after_transaction += actual_qty
return frappe._dict(
name=item_code,
actual_qty=actual_qty,
qty_after_transaction=qty_after_transaction,
stock_value_difference=stock_value_difference,
warehouse="WH 1",
posting_date=posting_date,
voucher_type="Stock Entry",
voucher_no=voucher_no,
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=batch_no,
valuation_rate=10,
)
sle = [
make_sle("2021-08-01", "001", batchwise_above_90, 50, 500),
make_sle("2021-08-10", "002", non_batchwise_above_90, 60, 600),
make_sle("2021-08-20", "003", batchwise_above_90, -10, -100),
make_sle("2021-09-01", "004", non_batchwise_above_90, -15, -150),
make_sle("2021-09-20", "005", batchwise_61_90, 40, 400),
make_sle("2021-09-25", "006", non_batchwise_61_90, 50, 500),
make_sle("2021-09-30", "007", batchwise_61_90, -5, -50),
make_sle("2021-10-05", "008", non_batchwise_above_90, -20, -200),
make_sle("2021-10-20", "009", batchwise_31_60, 30, 300),
make_sle("2021-10-25", "010", non_batchwise_31_60, 40, 400),
make_sle("2021-10-30", "011", batchwise_31_60, -8, -80),
make_sle("2021-11-05", "012", non_batchwise_above_90, -25, -250),
make_sle("2021-11-20", "013", batchwise_0_30, 20, 200),
make_sle("2021-11-25", "014", non_batchwise_0_30, 30, 300),
make_sle("2021-11-30", "015", batchwise_0_30, -6, -60),
make_sle("2021-12-01", "016", non_batchwise_61_90, -10, -100),
]
slots = FIFOSlots(self.filters, sle).generate()
item_result = slots[item_code]
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
self.assertEqual(item_result["total_qty"], 221.0)
self.assertEqual(
item_result["fifo_queue"],
[
[batchwise_above_90, 1, 40.0, "2021-08-01", 400.0],
[batchwise_61_90, 1, 35.0, "2021-09-20", 350.0],
[non_batchwise_61_90, 0, 40.0, "2021-09-25", 400.0],
[batchwise_31_60, 1, 22.0, "2021-10-20", 220.0],
[non_batchwise_31_60, 0, 40, "2021-10-25", 400],
[batchwise_0_30, 1, 14.0, "2021-11-20", 140.0],
[non_batchwise_0_30, 0, 30, "2021-11-25", 300],
],
)
report_data = format_report_data(self.filters, slots, self.filters["to_date"])
range_values = report_data[0][7:15]
self.assertEqual(range_values, [44.0, 440.0, 62.0, 620.0, 75.0, 750.0, 40.0, 400.0])
def test_batchwise_valuation_same_voucher_transfer(self):
from erpnext.stock.doctype.item.test_item import make_item
item_code = make_item(
"Test Stock Ageing Batchwise Transfer",
{
"is_stock_item": 1,
"has_batch_no": 1,
"valuation_method": "FIFO",
},
).name
def make_batch(batch_id):
if not frappe.db.exists("Batch", batch_id):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_id,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", 1)
source_batch = "SA-BATCHWISE-TRANSFER-SOURCE"
target_batch = "SA-BATCHWISE-TRANSFER-TARGET"
make_batch(source_batch)
make_batch(target_batch)
sle = [
frappe._dict(
name=item_code,
actual_qty=20,
qty_after_transaction=20,
stock_value_difference=200,
warehouse="WH 1",
posting_date="2021-09-01",
voucher_type="Stock Entry",
voucher_no="001",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=source_batch,
valuation_rate=10,
),
frappe._dict(
name=item_code,
actual_qty=-15,
qty_after_transaction=5,
stock_value_difference=-150,
warehouse="WH 1",
posting_date="2021-10-01",
voucher_type="Stock Entry",
voucher_no="002",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=source_batch,
valuation_rate=10,
),
frappe._dict(
name=item_code,
actual_qty=10,
qty_after_transaction=15,
stock_value_difference=100,
warehouse="WH 1",
posting_date="2021-10-01",
voucher_type="Stock Entry",
voucher_no="002",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=target_batch,
valuation_rate=10,
),
]
fifo_slots = FIFOSlots(self.filters, sle)
slots = fifo_slots.generate()
item_result = slots[item_code]
self.assertEqual(item_result["total_qty"], 15.0)
self.assertEqual(
item_result["fifo_queue"],
[
[source_batch, 1, 5.0, "2021-09-01", 50.0],
[target_batch, 1, 10.0, "2021-09-01", 100.0],
],
)
self.assertEqual(
fifo_slots.transferred_item_details[("002", item_code, "WH 1")],
[[5.0, "2021-09-01", 50.0]],
)
def test_batchwise_valuation_negative_stock_same_voucher(self):
from erpnext.stock.doctype.item.test_item import make_item
item_code = make_item(
"Test Stock Ageing Batchwise Negative Stock",
{
"is_stock_item": 1,
"has_batch_no": 1,
"valuation_method": "FIFO",
},
).name
batch_no = "SA-BATCHWISE-NEGATIVE-STOCK"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
sle = [
frappe._dict(
name=item_code,
actual_qty=-10,
qty_after_transaction=-10,
stock_value_difference=-100,
warehouse="WH 1",
posting_date="2021-12-01",
voucher_type="Stock Entry",
voucher_no="001",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=batch_no,
valuation_rate=10,
)
]
fifo_slots = FIFOSlots(self.filters, sle)
slots = fifo_slots.generate()
item_result = slots[item_code]
self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -10, "2021-12-01", -100]])
self.assertEqual(
fifo_slots.transferred_item_details[("001", item_code, "WH 1")], [[10, "2021-12-01", 100]]
)
sle.append(
frappe._dict(
name=item_code,
actual_qty=6,
qty_after_transaction=-4,
stock_value_difference=60,
warehouse="WH 1",
posting_date="2021-12-01",
voucher_type="Stock Entry",
voucher_no="001",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=batch_no,
valuation_rate=10,
)
)
fifo_slots = FIFOSlots(self.filters, sle)
slots = fifo_slots.generate()
item_result = slots[item_code]
self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -4.0, "2021-12-01", -40.0]])
self.assertEqual(
fifo_slots.transferred_item_details[("001", item_code, "WH 1")],
[[4.0, "2021-12-01", 40.0]],
)
def test_batchwise_valuation_neutralizes_non_head_negative_batch(self):
from erpnext.stock.doctype.item.test_item import make_item
item_code = make_item(
"Test Stock Ageing Batchwise Negative Non Head",
{
"is_stock_item": 1,
"has_batch_no": 1,
"valuation_method": "FIFO",
},
).name
buffer_batch = "SA-BATCHWISE-NEGATIVE-BUFFER"
negative_batch = "SA-BATCHWISE-NEGATIVE-NON-HEAD"
for batch_no in [buffer_batch, negative_batch]:
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
sle = [
frappe._dict(
name=item_code,
actual_qty=5,
qty_after_transaction=5,
stock_value_difference=50,
warehouse="WH 1",
posting_date="2021-11-30",
voucher_type="Stock Entry",
voucher_no="001",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=buffer_batch,
valuation_rate=10,
),
frappe._dict(
name=item_code,
actual_qty=-10,
qty_after_transaction=-5,
stock_value_difference=-100,
warehouse="WH 1",
posting_date="2021-12-01",
voucher_type="Stock Entry",
voucher_no="002",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=negative_batch,
valuation_rate=10,
),
frappe._dict(
name=item_code,
actual_qty=6,
qty_after_transaction=1,
stock_value_difference=60,
warehouse="WH 1",
posting_date="2021-12-01",
voucher_type="Stock Entry",
voucher_no="002",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=negative_batch,
valuation_rate=10,
),
]
fifo_slots = FIFOSlots(self.filters, sle)
slots = fifo_slots.generate()
item_result = slots[item_code]
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
self.assertEqual(
item_result["fifo_queue"],
[
[buffer_batch, 1, 5, "2021-11-30", 50],
[negative_batch, 1, -4.0, "2021-12-01", -40.0],
],
)
self.assertEqual(
fifo_slots.transferred_item_details[("002", item_code, "WH 1")],
[[4.0, "2021-12-01", 40.0]],
)
def test_batchwise_valuation_negative_stock_later_voucher(self):
from erpnext.stock.doctype.item.test_item import make_item
item_code = make_item(
"Test Stock Ageing Batchwise Negative Later Voucher",
{
"is_stock_item": 1,
"has_batch_no": 1,
"valuation_method": "FIFO",
},
).name
batch_no = "SA-BATCHWISE-NEGATIVE-LATER-VOUCHER"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
sle = [
frappe._dict(
name=item_code,
actual_qty=-10,
qty_after_transaction=-10,
stock_value_difference=-100,
warehouse="WH 1",
posting_date="2021-11-01",
voucher_type="Stock Entry",
voucher_no="001",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=batch_no,
valuation_rate=10,
),
frappe._dict(
name=item_code,
actual_qty=6,
qty_after_transaction=-4,
stock_value_difference=60,
warehouse="WH 1",
posting_date="2021-11-10",
voucher_type="Stock Entry",
voucher_no="002",
has_serial_no=False,
has_batch_no=True,
serial_no=None,
batch_no=batch_no,
valuation_rate=10,
),
]
slots = FIFOSlots(self.filters, sle).generate()
item_result = slots[item_code]
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
self.assertEqual(item_result["total_qty"], -4.0)
self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -4.0, "2021-11-10", -40.0]])
def test_batchwise_valuation_stock_reconciliation_with_bundle(self):
from frappe.utils import add_days, getdate, nowdate
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
)
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
suffix = frappe.generate_hash(length=8).upper()
item_code = make_item(
f"Test Stock Ageing Batch Reco {suffix}",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": f"SA-RECO-{suffix}-.###",
"valuation_method": "FIFO",
},
).name
warehouse = "_Test Warehouse - _TC"
base_date = nowdate()
opening_reco = create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=12,
rate=10,
posting_date=add_days(base_date, -2),
posting_time="10:00:00",
)
batch_no = get_batch_from_bundle(opening_reco.items[0].serial_and_batch_bundle)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=5,
rate=10,
batch_no=batch_no,
posting_date=add_days(base_date, -1),
posting_time="10:00:00",
)
filters = frappe._dict(
company="_Test Company",
to_date=base_date,
ranges=["30", "60", "90"],
item_code=item_code,
)
slots = FIFOSlots(filters).generate()
item_result = slots[item_code]
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
self.assertEqual(item_result["total_qty"], 5.0)
self.assertEqual(
item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
)
def generate_item_and_item_wh_wise_slots(filters, sle):
"Return results with and without 'show_warehouse_wise_stock'"

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