Compare commits

..

153 Commits

Author SHA1 Message Date
Frappe PR Bot
99ead94ffe chore(release): Bumped to Version 15.38.3
## [15.38.3](https://github.com/frappe/erpnext/compare/v15.38.2...v15.38.3) (2024-10-16)

### Bug Fixes

* added parentheses for correct query formation for logical OR condition ([21a7dd4](21a7dd43a9))
* added string for translation in bank reconciliation statement ([e10a580](e10a58074f))
* conversion factor issue (backport [#43645](https://github.com/frappe/erpnext/issues/43645)) ([#43674](https://github.com/frappe/erpnext/issues/43674)) ([b2deb89](b2deb89826))
* delete invalid pricing rule on change of applicable_for ([5d6fc71](5d6fc71556))
* don't update reference to SI / PI on advances ([b72906a](b72906a7a1))
* ignore free item when qty is zero ([e5aaa5b](e5aaa5b6e5))
* incorrect warehouse in the serial no selector for rejection (backport [#43671](https://github.com/frappe/erpnext/issues/43671)) ([#43673](https://github.com/frappe/erpnext/issues/43673)) ([c490a66](c490a66540))
* Link opportunity from RFQ to supplier quotation ([eb1f125](eb1f1255eb))
* missing child company accounts in consolidated balance sheet ([4db12fe](4db12fe2da))
* quotation to so frappe crm (backport [#43644](https://github.com/frappe/erpnext/issues/43644)) ([#43646](https://github.com/frappe/erpnext/issues/43646)) ([f3ceb4a](f3ceb4ac7d))
* refetch items from BOM if 'Use Multi-Level BOM' has changed usin… (backport [#43672](https://github.com/frappe/erpnext/issues/43672)) ([#43676](https://github.com/frappe/erpnext/issues/43676)) ([492ba53](492ba539e8))
* removed unused query ([8668ae9](8668ae92d8))
* run gl_entries and closing voucher processes in same function ([ea12897](ea12897ce9))
* show incorrect entries filter in Stock Ledger Invariant Check report (backport [#43619](https://github.com/frappe/erpnext/issues/43619)) ([#43622](https://github.com/frappe/erpnext/issues/43622)) ([d604b12](d604b12d51))
* **stock:** Grab posting date/time from SABB (backport [#43493](https://github.com/frappe/erpnext/issues/43493)) ([#43502](https://github.com/frappe/erpnext/issues/43502)) ([cd9f949](cd9f949b12))
* update formatings ([c2c6d27](c2c6d27625))
* update formatings ([a70181e](a70181e025))
* update item details with actual quantity. ([930e79c](930e79c351))
* Use `ref_doc.get()` for `party_account_currency` ([928b6b1](928b6b1510))
* zero incoming rate for delivery note return ([#43642](https://github.com/frappe/erpnext/issues/43642)) ([85088e4](85088e4aff))
2024-10-16 05:01:44 +00:00
ruthra kumar
e05ae14d49 Merge pull request #43667 from frappe/version-15-hotfix
chore: release v15
2024-10-16 10:30:25 +05:30
mergify[bot]
cd9f949b12 fix(stock): Grab posting date/time from SABB (backport #43493) (#43502)
fix(stock): Grab posting date/time from SABB (#43493)

(cherry picked from commit ade121dac6)

Co-authored-by: Corentin Forler <10946971+cogk@users.noreply.github.com>
2024-10-15 17:45:35 +05:30
ruthra kumar
aef544cd53 Merge pull request #43684 from frappe/mergify/bp/version-15-hotfix/pr-43661
fix: missing child company accounts in consolidated balance sheet (backport #43661)
2024-10-15 17:37:52 +05:30
ruthra kumar
d34025dc11 Merge pull request #43682 from frappe/mergify/bp/version-15-hotfix/pr-43663
fix: run gl_entries and closing voucher processes in same function (backport #43663)
2024-10-15 17:22:39 +05:30
ljain112
4db12fe2da fix: missing child company accounts in consolidated balance sheet
(cherry picked from commit 7fae9d57d2)
2024-10-15 11:38:10 +00:00
ljain112
ea12897ce9 fix: run gl_entries and closing voucher processes in same function
(cherry picked from commit af4daa5b0f)
2024-10-15 11:35:55 +00:00
ruthra kumar
802d9b2d4a Merge pull request #43680 from frappe/mergify/bp/version-15-hotfix/pr-43602
fix: added parentheses for correct query formation for logical OR condition (backport #43602)
2024-10-15 17:03:22 +05:30
mergify[bot]
492ba539e8 fix: refetch items from BOM if 'Use Multi-Level BOM' has changed usin… (backport #43672) (#43676)
fix: refetch items from BOM if 'Use Multi-Level BOM' has changed usin… (#43672)

fix: refetch items from BOM if 'Use Multi-Level BOM' has changed using api
(cherry picked from commit 05915415de)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-15 16:47:40 +05:30
ljain112
21a7dd43a9 fix: added parentheses for correct query formation for logical OR condition
(cherry picked from commit c0da8f11f7)
2024-10-15 11:15:59 +00:00
ruthra kumar
50d1fa4665 Merge pull request #43678 from frappe/mergify/bp/version-15-hotfix/pr-43600
fix: added string for translation in bank reconciliation statement (backport #43600)
2024-10-15 16:41:06 +05:30
ljain112
e10a58074f fix: added string for translation in bank reconciliation statement
(cherry picked from commit c99d9f7037)
2024-10-15 11:02:42 +00:00
mergify[bot]
b2deb89826 fix: conversion factor issue (backport #43645) (#43674)
fix: conversion factor issue (#43645)

(cherry picked from commit a52756f1d4)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-15 16:04:48 +05:30
mergify[bot]
c490a66540 fix: incorrect warehouse in the serial no selector for rejection (backport #43671) (#43673)
fix: incorrect warehouse in the serial no selector for rejection (#43671)

(cherry picked from commit 29ff682eca)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-15 16:04:36 +05:30
ruthra kumar
afa0c13587 Merge pull request #43670 from frappe/mergify/bp/version-15-hotfix/pr-43557
fix: delete invalid pricing rule on change of applicable_for values (backport #43557)
2024-10-15 15:56:43 +05:30
ljain112
4dbee00b82 test: added test for change in applicable_for_value in promotional scheme
(cherry picked from commit 2613bdd868)
2024-10-15 10:00:47 +00:00
ljain112
5d6fc71556 fix: delete invalid pricing rule on change of applicable_for
(cherry picked from commit 42746fc630)
2024-10-15 10:00:47 +00:00
ruthra kumar
06dd5e0071 Merge pull request #43666 from frappe/mergify/bp/version-15-hotfix/pr-43662
fix: removed unused query (backport #43662)
2024-10-15 13:52:32 +05:30
ruthra kumar
105f9ec2e1 chore: resolve conflict 2024-10-15 13:32:29 +05:30
ruthra kumar
dc6fdbb836 Merge pull request #43660 from frappe/mergify/bp/version-15-hotfix/pr-43642
fix: zero incoming rate for delivery note return (backport #43642)
2024-10-15 13:26:32 +05:30
ljain112
8668ae92d8 fix: removed unused query
(cherry picked from commit 5f590ddfa2)

# Conflicts:
#	erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
2024-10-15 07:53:44 +00:00
ruthra kumar
ec7e5c48de Merge pull request #43658 from frappe/mergify/bp/version-15-hotfix/pr-43570
fix: update item details with actual quantity (backport #43570)
2024-10-15 13:15:39 +05:30
rohitwaghchaure
85088e4aff fix: zero incoming rate for delivery note return (#43642)
(cherry picked from commit 6087a57b0c)
2024-10-15 06:28:59 +00:00
Bhavan23
c2c6d27625 fix: update formatings
(cherry picked from commit 5044297321)
2024-10-15 06:27:52 +00:00
Bhavan23
a70181e025 fix: update formatings
(cherry picked from commit 5f4a523340)
2024-10-15 06:27:52 +00:00
Bhavan23
86017b223a test: Validate the actual quantity when creating a material request from the sales order
(cherry picked from commit 17fdd42645)
2024-10-15 06:27:51 +00:00
Bhavan23
930e79c351 fix: update item details with actual quantity.
(cherry picked from commit 9dbdfec9b7)
2024-10-15 06:27:51 +00:00
ruthra kumar
2c7f5ec324 Merge pull request #43521 from mujeerhashmi/version-15-hotfix
fix: Link opportunity from RFQ to supplier quotation
2024-10-15 10:08:25 +05:30
ruthra kumar
a9f5e86600 Merge pull request #43654 from frappe/mergify/bp/version-15-hotfix/pr-43601
refactor: remove 'format:' based naming (backport #43601)
2024-10-15 07:34:35 +05:30
ruthra kumar
d6decf9172 Merge pull request #43650 from frappe/mergify/bp/version-15-hotfix/pr-43614
fix: ignore free item when qty is zero (backport #43614)
2024-10-15 06:01:30 +05:30
ruthra kumar
1fac17b36f chore: resolve conflict 2024-10-15 06:00:12 +05:30
venkat102
9d05a6ebc0 refactor: remove 'format:' based naming
(cherry picked from commit e8e1ec0e85)

# Conflicts:
#	erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json
2024-10-14 15:46:06 +00:00
Ninad1306
389ee909a5 test: test case to validate free item is ignored when qty is zero
(cherry picked from commit a2b41a0c16)
2024-10-14 10:50:58 +00:00
Ninad1306
e5aaa5b6e5 fix: ignore free item when qty is zero
(cherry picked from commit 7ae98f77ee)
2024-10-14 10:50:58 +00:00
mergify[bot]
f3ceb4ac7d fix: quotation to so frappe crm (backport #43644) (#43646)
fix: quotation to so frappe crm (#43644)

(cherry picked from commit d57624b182)

Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com>
2024-10-14 15:03:35 +05:30
Smit Vora
050ca4b726 Merge pull request #43641 from frappe/mergify/bp/version-15-hotfix/pr-43638
fix: Use `ref_doc.get()` for `party_account_currency` (backport #43638)
2024-10-14 13:59:13 +05:30
Abdeali Chharchhoda
928b6b1510 fix: Use ref_doc.get() for party_account_currency
(cherry picked from commit b79549422a)
2024-10-14 06:09:09 +00:00
Frappe PR Bot
ef1e121bd4 chore(release): Bumped to Version 15.38.2
## [15.38.2](https://github.com/frappe/erpnext/compare/v15.38.1...v15.38.2) (2024-10-13)

### Bug Fixes

* don't update reference to SI / PI on advances ([8bf8bcf](8bf8bcf739))
2024-10-13 05:23:41 +00:00
ruthra kumar
68f1b41969 Merge pull request #43633 from frappe/mergify/bp/version-15/pr-43627
fix: reconciled advance from reported in reconciliation tool (backport #43627)
2024-10-13 10:52:21 +05:30
ruthra kumar
a329003f7f chore: use correct hook for advance payment doctypes 2024-10-13 10:35:48 +05:30
ruthra kumar
cf1eabe049 chore: resolve conflict 2024-10-13 10:35:14 +05:30
ruthra kumar
4c78a682ad chore: better comments for context
(cherry picked from commit e7505e92c9)
2024-10-13 04:59:58 +00:00
ruthra kumar
4752ed2483 test: reconciled Invoice should not showup in tool
Scenario should be tested on 'Advance in separate party account'

(cherry picked from commit f1ec61c19e)
2024-10-13 04:59:58 +00:00
ruthra kumar
e56dd8268b test: unreconciliation of individual SO from Advance Payment
(cherry picked from commit 8a6978e550)
2024-10-13 04:59:58 +00:00
ruthra kumar
e0477cf59f refactor(test): utility methods for enabling advance in separate acc
(cherry picked from commit a21a406d04)

# Conflicts:
#	erpnext/accounts/test/accounts_mixin.py
2024-10-13 04:59:58 +00:00
ruthra kumar
8c115e146b refactor: use hooks to identify advance doctypes
(cherry picked from commit e7bb960bb5)
2024-10-13 04:59:58 +00:00
ruthra kumar
6267ab994c refactor: reference update logic in advance
(cherry picked from commit a112581acd)
2024-10-13 04:59:58 +00:00
ruthra kumar
8bf8bcf739 fix: don't update reference to SI / PI on advances
(cherry picked from commit b409f74620)
2024-10-13 04:59:57 +00:00
ruthra kumar
eed02d3f44 Merge pull request #43632 from frappe/mergify/bp/version-15-hotfix/pr-43627
fix: reconciled advance from reported in reconciliation tool (backport #43627)
2024-10-13 10:24:41 +05:30
ruthra kumar
6265582e53 refactor: use correct hook for identifying advance doctypes 2024-10-13 09:50:41 +05:30
ruthra kumar
361836e735 chore: resolve conflict 2024-10-13 08:45:15 +05:30
ruthra kumar
ae73d9c621 chore: better comments for context
(cherry picked from commit e7505e92c9)
2024-10-13 03:08:25 +00:00
ruthra kumar
2c2ca22d12 test: reconciled Invoice should not showup in tool
Scenario should be tested on 'Advance in separate party account'

(cherry picked from commit f1ec61c19e)
2024-10-13 03:08:25 +00:00
ruthra kumar
e37a88fdb6 test: unreconciliation of individual SO from Advance Payment
(cherry picked from commit 8a6978e550)
2024-10-13 03:08:25 +00:00
ruthra kumar
9c26093a51 refactor(test): utility methods for enabling advance in separate acc
(cherry picked from commit a21a406d04)

# Conflicts:
#	erpnext/accounts/test/accounts_mixin.py
2024-10-13 03:08:24 +00:00
ruthra kumar
5ce2d73692 refactor: use hooks to identify advance doctypes
(cherry picked from commit e7bb960bb5)
2024-10-13 03:08:24 +00:00
ruthra kumar
ca0a962870 refactor: reference update logic in advance
(cherry picked from commit a112581acd)
2024-10-13 03:08:24 +00:00
ruthra kumar
b72906a7a1 fix: don't update reference to SI / PI on advances
(cherry picked from commit b409f74620)
2024-10-13 03:08:23 +00:00
mergify[bot]
d604b12d51 fix: show incorrect entries filter in Stock Ledger Invariant Check report (backport #43619) (#43622)
fix: show incorrect entries filter in Stock Ledger Invariant Check report (#43619)

fix: show incorrect entry filter in Stock Ledger Invariant Check report
(cherry picked from commit 8beee1982f)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-11 16:22:22 +05:30
Frappe PR Bot
d46cf46375 chore(release): Bumped to Version 15.38.1
## [15.38.1](https://github.com/frappe/erpnext/compare/v15.38.0...v15.38.1) (2024-10-09)

### Bug Fixes

* 'NoneType' object has no attribute 'has_serial_no' (backport [#43514](https://github.com/frappe/erpnext/issues/43514)) ([#43574](https://github.com/frappe/erpnext/issues/43574)) ([60508a9](60508a9706))
* [#42014](https://github.com/frappe/erpnext/issues/42014) --resolve conflicts ([85d7405](85d74050e1))
* Accepted and Rejected warehouse cannot be same (backport [#43568](https://github.com/frappe/erpnext/issues/43568)) ([#43573](https://github.com/frappe/erpnext/issues/43573)) ([83ce3dd](83ce3dd915))
* add include closed orders option in so/po trends report v15 ([5660e8b](5660e8b26d))
* add parenttype condition for item table in Purchase Register Report ([8ce81a0](8ce81a058a))
* Add removed test code `b41f10c1b9` ([30fd11f](30fd11f138))
* allow to change the batch in the subcontracting receipt (backport [#43584](https://github.com/frappe/erpnext/issues/43584)) ([#43588](https://github.com/frappe/erpnext/issues/43588)) ([9e109ac](9e109acec7))
* create Account Closing Balance even though there are no transaction in period ([d6f70f5](d6f70f533a))
* creation of contact, customer, opportunity, quotation and prospect from lead ([ef10c4e](ef10c4ea4f))
* creation of contact, customer, opportunity, quotation and prospect from lead --prettier ([5a2a404](5a2a404a50))
* deduct advances adjusted for threshold check for tcs ([6decb7c](6decb7cc34))
* do not include advances for tds vouchers ([ee8485a](ee8485a54a))
* frappe dependency update ([0a70b3f](0a70b3ffcc))
* include parent item group in query ([55464c7](55464c79c4))
* make LCV button not working for PI and PR (backport [#43592](https://github.com/frappe/erpnext/issues/43592)) ([#43593](https://github.com/frappe/erpnext/issues/43593)) ([120b481](120b481c4a))
* multiple issues in Payment Request ([#42427](https://github.com/frappe/erpnext/issues/42427)) ([ea69ba7](ea69ba7cd8))
* production plan bom error (backport [#43591](https://github.com/frappe/erpnext/issues/43591)) ([#43594](https://github.com/frappe/erpnext/issues/43594)) ([029021f](029021f035))
* read only filters in multidialog fields (backport [#43503](https://github.com/frappe/erpnext/issues/43503)) ([#43513](https://github.com/frappe/erpnext/issues/43513)) ([d69a974](d69a974a4d))
* Remove `advance_payment_status` uses ([907e3af](907e3af1b0))
* Remove unreference method ([770bc1c](770bc1c293))
* Remove unused  field ([e785928](e785928c0f))
* Remove unused function `get_paid_amount_against_order` ([7591662](75916629c8))
* Separate `on_submit` and `before_submit` of PR ([dbd7b83](dbd7b83204))
* the purchase receipt trends and delivery note trends report (backport [#43585](https://github.com/frappe/erpnext/issues/43585)) ([#43587](https://github.com/frappe/erpnext/issues/43587)) ([355ba2f](355ba2f632))
* Unknown column 'serial_no' in 'field list' (backport [#43515](https://github.com/frappe/erpnext/issues/43515)) ([#43569](https://github.com/frappe/erpnext/issues/43569)) ([fc9a3c0](fc9a3c0c92))
* Update Values before `after_mapping` hook is called ([#42682](https://github.com/frappe/erpnext/issues/42682)) ([6770610](6770610c6d))
* validation for corrective job card (backport [#43555](https://github.com/frappe/erpnext/issues/43555)) ([#43558](https://github.com/frappe/erpnext/issues/43558)) ([cf0fa0d](cf0fa0db7b))
2024-10-09 12:02:42 +00:00
rohitwaghchaure
c3f6edcd01 Merge pull request #43563 from frappe/version-15-hotfix
chore: release v15
2024-10-09 17:31:15 +05:30
mergify[bot]
120b481c4a fix: make LCV button not working for PI and PR (backport #43592) (#43593)
fix: make LCV button not working for PI and PR (#43592)

(cherry picked from commit 48a12e7213)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-09 15:41:38 +05:30
mergify[bot]
029021f035 fix: production plan bom error (backport #43591) (#43594)
fix: production plan bom error (#43591)

(cherry picked from commit ab171326f3)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-09 15:41:21 +05:30
mergify[bot]
9e109acec7 fix: allow to change the batch in the subcontracting receipt (backport #43584) (#43588)
fix: allow to change the batch in the subcontracting receipt (#43584)

(cherry picked from commit fc67867a60)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-09 13:43:02 +05:30
mergify[bot]
355ba2f632 fix: the purchase receipt trends and delivery note trends report (backport #43585) (#43587)
* fix: fix the purchase receipt trends and delivery note trends report

(cherry picked from commit 2e9dda1588)

* fix: trends date filter issue --formatter

(cherry picked from commit b3e4463a4f)

---------

Co-authored-by: Vishv-silveroak <108357657+Vishv-024@users.noreply.github.com>
Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com>
2024-10-09 13:38:43 +05:30
mergify[bot]
83ce3dd915 fix: Accepted and Rejected warehouse cannot be same (backport #43568) (#43573)
fix: Accepted and Rejected warehouse cannot be same (#43568)

(cherry picked from commit 5130f7d411)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-09 09:06:34 +05:30
mergify[bot]
60508a9706 fix: 'NoneType' object has no attribute 'has_serial_no' (backport #43514) (#43574)
fix: 'NoneType' object has no attribute 'has_serial_no' (#43514)

(cherry picked from commit 6ddda6c949)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-09 09:06:20 +05:30
Smit Vora
660d20f7fa Merge pull request #43572 from frappe/mergify/bp/version-15-hotfix/pr-43271
fix: deduct advances adjusted for threshold check for tcs (backport #43271)
2024-10-08 22:31:21 +05:30
Nihantra C. Patel
0b2603bbf1 Merge pull request #43577 from Nihantra-Patel/feat_trends_report_v15
fix: add include closed orders option in so/po trends report v15
2024-10-08 19:07:39 +05:30
Nihantra Patel
5660e8b26d fix: add include closed orders option in so/po trends report v15 2024-10-08 18:48:11 +05:30
mergify[bot]
cf0fa0db7b fix: validation for corrective job card (backport #43555) (#43558)
* fix: validation for corrective job card (#43555)

(cherry picked from commit 7a0a893d08)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.py

* chore: fix conflicts

* chore: fix linters issue

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-08 17:40:52 +05:30
mergify[bot]
fc9a3c0c92 fix: Unknown column 'serial_no' in 'field list' (backport #43515) (#43569)
fix: Unknown column 'serial_no' in 'field list' (#43515)

(cherry picked from commit 69127e8609)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-08 17:40:22 +05:30
ljain112
2b4cd0a9bb test: added test cases for the tcs deduction for advances adjusted.
(cherry picked from commit efe238cefd)
2024-10-08 12:05:52 +00:00
ljain112
6decb7cc34 fix: deduct advances adjusted for threshold check for tcs
(cherry picked from commit 767c8f92be)
2024-10-08 12:05:52 +00:00
Smit Vora
9039b86e8a Merge pull request #43553 from frappe/mergify/bp/version-15-hotfix/pr-43397
fix: do not include advances for tds vouchers (backport #43397)
2024-10-08 11:30:02 +05:30
ljain112
ee8485a54a fix: do not include advances for tds vouchers
(cherry picked from commit 7ef918421e)
2024-10-08 05:09:10 +00:00
mergify[bot]
05db28c64f chore: Allow apps to extend voucher subtypes (backport #43528) (backport #43550) (#43551)
chore: Allow apps to extend voucher subtypes (#43528)

* chore: Allow apps to extend voucher subtypes

(cherry picked from commit a1525d9b8e)

* chore: Allow apps to extend voucher subtypes

(cherry picked from commit 8a1e38a43b)

* chore: Allow apps to extend voucher subtypes

(cherry picked from commit ca8820b566)

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit bcd0105915)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2024-10-08 09:20:34 +05:30
mergify[bot]
bcd0105915 chore: Allow apps to extend voucher subtypes (#43528)
* chore: Allow apps to extend voucher subtypes

(cherry picked from commit a1525d9b8e)

* chore: Allow apps to extend voucher subtypes

(cherry picked from commit 8a1e38a43b)

* chore: Allow apps to extend voucher subtypes

(cherry picked from commit ca8820b566)

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2024-10-08 09:01:25 +05:30
Smit Vora
225adf5cbc Merge pull request #43527 from frappe/mergify/bp/version-15-hotfix/pr-43391
fix: create Account Closing Balance even though there are no transaction in period (backport #43391)
2024-10-07 22:34:46 +05:30
Smit Vora
af5947edd0 Merge pull request #43407 from Abdeali099/pr-backport
fix: multiple issues in Payment Request (#42427)
2024-10-07 21:45:01 +05:30
ljain112
d6f70f533a fix: create Account Closing Balance even though there are no transaction in period
(cherry picked from commit 43deaea96b)
2024-10-07 16:12:28 +00:00
Smit Vora
1dd4168c0e Merge pull request #43525 from frappe/mergify/bp/version-15-hotfix/pr-43384
fix: add parenttype condition for item table in Purchase Register Report (backport #43384)
2024-10-07 21:42:06 +05:30
Smit Vora
db4360d76c Merge pull request #43523 from frappe/mergify/bp/version-15-hotfix/pr-43385
fix: include parent item group in query (backport #43385)
2024-10-07 21:41:05 +05:30
ljain112
8ce81a058a fix: add parenttype condition for item table in Purchase Register Report
(cherry picked from commit 28abf191fc)
2024-10-07 15:55:50 +00:00
ljain112
55464c79c4 fix: include parent item group in query
(cherry picked from commit ad0090068d)
2024-10-07 15:47:46 +00:00
Smit Vora
0c599c2b6d chore: remove unused filed 2024-10-07 20:36:53 +05:30
Syed Mujeer Hashmi
eb1f1255eb fix: Link opportunity from RFQ to supplier quotation 2024-10-07 14:12:43 +00:00
Abdeali Chharchhoda
62cc86114b test: Change Accounts Settings for multi currency (https://github.com/frappe/erpnext/pull/42427#discussion_r1789859737) 2024-10-07 16:00:06 +05:30
Smit Vora
5268da2e55 Merge pull request #43355 from Ninad1306/mapping_docs_fix
fix: update child table from the last source doc (backport #42925)
2024-10-07 15:18:41 +05:30
Smit Vora
d695fea251 Merge pull request #43512 from Ninad1306/sales_purchase_mapping_fix
fix: Update Values before `after_mapping` hook is called (backport #42682)
2024-10-07 15:15:44 +05:30
Nihantra C. Patel
6b2983d8c1 Merge pull request #43296 from frappe/mergify/bp/version-15-hotfix/pr-42014
fix: creation of contact, customer, opportunity, quotation and prospect from lead (backport #42014)
2024-10-07 14:59:57 +05:30
Nihantra C. Patel
85d74050e1 fix: #42014 --resolve conflicts 2024-10-07 14:51:04 +05:30
mergify[bot]
d69a974a4d fix: read only filters in multidialog fields (backport #43503) (#43513)
fix: read only filters in multidialog fields (#43503)

(cherry picked from commit 13eb3c5c14)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-07 13:57:56 +05:30
Abdeali Chharchhoda
3d9d56ab50 test: Remove Payment Gateway settings from test 2024-10-07 12:42:59 +05:30
Abdeali Chharchhoda
dbd7b83204 fix: Separate on_submit and before_submit of PR 2024-10-07 12:33:42 +05:30
Ninad Parikh
6770610c6d fix: Update Values before after_mapping hook is called (#42682)
* fix: update values before after_mapping hook is called

* fix: appropriate function name
2024-10-07 12:19:49 +05:30
Abdeali Chharchhoda
75916629c8 fix: Remove unused function get_paid_amount_against_order 2024-10-07 12:10:51 +05:30
Abdeali Chharchhoda
e785928c0f fix: Remove unused field 2024-10-07 12:08:02 +05:30
Frappe PR Bot
edfa6e41e1 chore(release): Bumped to Version 15.38.0
# [15.38.0](https://github.com/frappe/erpnext/compare/v15.37.0...v15.38.0) (2024-10-04)

### Bug Fixes

* 'NoneType' object has no attribute 'has_serial_no' ([21a0157](21a01575b6))
* add company filter in Warehouse wise Item Balance Age and Value ([4fc6d3e](4fc6d3ef64))
* adjustmen entry for stock reco ([c551c27](c551c2714c))
* Cannot read properties of undefined (reading 'price_list_rate') (backport [#43376](https://github.com/frappe/erpnext/issues/43376)) ([#43377](https://github.com/frappe/erpnext/issues/43377)) ([47f06dc](47f06dc180))
* Data missing in table: None, MandatoryError (backport [#43422](https://github.com/frappe/erpnext/issues/43422)) ([#43429](https://github.com/frappe/erpnext/issues/43429)) ([4b3f143](4b3f143f83))
* **Dunning:** logic for fetching text (backport [#43160](https://github.com/frappe/erpnext/issues/43160)) ([#43490](https://github.com/frappe/erpnext/issues/43490)) ([1b28a4e](1b28a4e928))
* Fix API endpoint for Frankfurter ([d96cee8](d96cee8779))
* Ignore transaction deletion check on ledger entry insertion ([1d6f97a](1d6f97ad94))
* **Item:** error message on tax rate (backport [#42955](https://github.com/frappe/erpnext/issues/42955)) ([#42956](https://github.com/frappe/erpnext/issues/42956)) ([5fc5934](5fc5934942))
* last purchase rate for purchase invoice (backport [#43448](https://github.com/frappe/erpnext/issues/43448)) ([#43452](https://github.com/frappe/erpnext/issues/43452)) ([ee2c8c8](ee2c8c869a))
* negative stock error for batch (backport [#43450](https://github.com/frappe/erpnext/issues/43450)) ([#43454](https://github.com/frappe/erpnext/issues/43454)) ([7bf6251](7bf6251c21))
* patch to update Currency Exchange Settings for `frankfurter.app` (backport [#43481](https://github.com/frappe/erpnext/issues/43481)) ([#43483](https://github.com/frappe/erpnext/issues/43483)) ([35a08f8](35a08f8830))
* quality inspection creation (backport [#43416](https://github.com/frappe/erpnext/issues/43416)) ([#43417](https://github.com/frappe/erpnext/issues/43417)) ([a1b6628](a1b6628c41))
* **Quotation:** calculate row values for alternative items (backport [#43054](https://github.com/frappe/erpnext/issues/43054)) ([#43495](https://github.com/frappe/erpnext/issues/43495)) ([4fa5131](4fa5131590))
* removed validation for materials return (backport [#43461](https://github.com/frappe/erpnext/issues/43461)) ([#43463](https://github.com/frappe/erpnext/issues/43463)) ([9c0a17e](9c0a17e4d5))
* serial and batch no selector (backport [#43387](https://github.com/frappe/erpnext/issues/43387)) ([#43390](https://github.com/frappe/erpnext/issues/43390)) ([74c880c](74c880c232))
* set margin fields for purchase documents when updating items ([6516e68](6516e68fa0))
* Stock Ledger Invariant Check report ([2984bad](2984bad2c0))
* Stock UOM not fetched when Stock Entry create from Item Dashboard (backport [#43457](https://github.com/frappe/erpnext/issues/43457)) ([#43465](https://github.com/frappe/erpnext/issues/43465)) ([f2a72e5](f2a72e5f82))
* tests for work order consumption (backport [#41814](https://github.com/frappe/erpnext/issues/41814)) ([#43430](https://github.com/frappe/erpnext/issues/43430)) ([86b10ce](86b10ce9bb))
* use serial and batch fields (backport [#43421](https://github.com/frappe/erpnext/issues/43421)) ([#43423](https://github.com/frappe/erpnext/issues/43423)) ([d495d93](d495d93840))

### Features

* added 'cost of new capitalized asset' column ([27cd51e](27cd51e267))
* provide hook point for bulk transaction tasks ([50e47e7](50e47e796d))
2024-10-04 03:07:05 +00:00
ruthra kumar
5a9522e70f Merge pull request #43467 from frappe/version-15-hotfix
chore: release v15
2024-10-04 08:35:53 +05:30
mergify[bot]
4fa5131590 fix(Quotation): calculate row values for alternative items (backport #43054) (#43495)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(Quotation): calculate row values for alternative items (#43054)
2024-10-04 01:23:59 +01:00
mergify[bot]
1b28a4e928 fix(Dunning): logic for fetching text (backport #43160) (#43490)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(Dunning): logic for fetching text (#43160)
2024-10-03 20:28:53 +01:00
mergify[bot]
f2a72e5f82 fix: Stock UOM not fetched when Stock Entry create from Item Dashboard (backport #43457) (#43465)
fix: Stock UOM not fetched when Stock Entry create from Item Dashboard (#43457)

(cherry picked from commit 895b072bad)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-02 18:46:01 +05:30
Sagar Vora
661eb058b9 Merge pull request #43485 from frappe/mergify/bp/version-15-hotfix/pr-43475
fix: set margin fields for purchase documents when updating items (backport #43475)
2024-10-02 15:24:08 +05:30
Sagar Vora
6516e68fa0 fix: set margin fields for purchase documents when updating items
(cherry picked from commit 7be4d56be2)
2024-10-02 09:52:29 +00:00
mergify[bot]
35a08f8830 fix: patch to update Currency Exchange Settings for frankfurter.app (backport #43481) (#43483)
Co-authored-by: Sagar Vora <sagar@resilient.tech>
2024-10-02 15:15:48 +05:30
Sagar Vora
562327f041 Merge pull request #43478 from frappe/mergify/bp/version-15-hotfix/pr-43476
fix: Fix API endpoint for Frankfurter (backport #43476)
2024-10-02 14:43:14 +05:30
Sagar Vora
8e7d893669 test: update test for API change
(cherry picked from commit c444de017a)
2024-10-02 09:11:47 +00:00
Corentin Forler
d96cee8779 fix: Fix API endpoint for Frankfurter
(cherry picked from commit 33e72111c7)
2024-10-02 09:11:47 +00:00
mergify[bot]
96c4d1af63 Serial no report (backport #43444) (#43464)
Serial no report (#43444)

* chore: remove the field that which is not exiting in serial no

* chore: remove the field that which is not exiting in serial no

* chore: remove the field that which is not exiting in serial no

(cherry picked from commit 661efadf41)

Co-authored-by: Vishv-silveroak <108357657+Vishv-024@users.noreply.github.com>
2024-10-01 14:27:27 +05:30
mergify[bot]
9c0a17e4d5 fix: removed validation for materials return (backport #43461) (#43463)
fix: removed validation for materials return (#43461)

(cherry picked from commit 1c7154c7ca)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-01 14:26:38 +05:30
mergify[bot]
ee2c8c869a fix: last purchase rate for purchase invoice (backport #43448) (#43452)
* fix: last purchase rate for purchase invoice

(cherry picked from commit fb9d106633)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py

* chore: fix conflicts

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2024-10-01 12:54:26 +05:30
Nihantra C. Patel
15b34a607f Merge pull request #43459 from frappe/mergify/bp/version-15-hotfix/pr-43455
fix: add company filter in Warehouse wise Item Balance Age and Value (backport #43455)
2024-10-01 11:46:46 +05:30
Nihantra Patel
4fc6d3ef64 fix: add company filter in Warehouse wise Item Balance Age and Value
(cherry picked from commit 75950f86cf)
2024-10-01 05:47:36 +00:00
mergify[bot]
7bf6251c21 fix: negative stock error for batch (backport #43450) (#43454)
fix: negative stock error for batch (#43450)

(cherry picked from commit 912ba7789c)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-01 10:34:48 +05:30
mergify[bot]
5fc5934942 fix(Item): error message on tax rate (backport #42955) (#42956)
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2024-09-30 22:18:49 +02:00
Frappe PR Bot
01f9139ebd chore(release): Bumped to Version 15.37.0
# [15.37.0](https://github.com/frappe/erpnext/compare/v15.36.4...v15.37.0) (2024-09-30)

### Features

* added 'cost of new capitalized asset' column ([96d8b52](96d8b5242d))
2024-09-30 17:21:09 +00:00
rohitwaghchaure
4be557bdce Merge pull request #43453 from frappe/mergify/bp/version-15/pr-43412
feat: added 'cost of new capitalized asset' column (backport #43399) (backport #43412)
2024-09-30 22:49:53 +05:30
Khushi Rawat
96d8b5242d feat: added 'cost of new capitalized asset' column
(cherry picked from commit 1eb9cc33fc)
(cherry picked from commit 27cd51e267)
2024-09-30 15:45:59 +00:00
Abdeali Chharchhoda
67bd540135 test: Removed initial PR status assertion 2024-09-30 18:21:59 +05:30
Abdeali Chharchhoda
30fd11f138 fix: Add removed test code b41f10c1b9 2024-09-30 17:20:42 +05:30
rohitwaghchaure
0986d3ebe4 Merge pull request #43440 from frappe/mergify/bp/version-15-hotfix/pr-43437
fix: adjustment entry for stock reco (backport #43437)
2024-09-30 15:38:00 +05:30
Rohit Waghchaure
c551c2714c fix: adjustmen entry for stock reco
(cherry picked from commit 4e463b7d6d)
2024-09-30 08:57:54 +00:00
rohitwaghchaure
efc97cc59f Merge pull request #43438 from frappe/mergify/bp/version-15-hotfix/pr-43436
fix: 'NoneType' object has no attribute 'has_serial_no' (backport #43436)
2024-09-30 14:26:17 +05:30
Rohit Waghchaure
21a01575b6 fix: 'NoneType' object has no attribute 'has_serial_no'
(cherry picked from commit 28f9fd2507)
2024-09-30 08:11:20 +00:00
ruthra kumar
6f3b5604b9 Merge pull request #43434 from frappe/mergify/bp/version-15-hotfix/pr-43058
refactor: use hooks to extend bulk_transaction (backport #43058)
2024-09-30 09:58:44 +05:30
Kitti U
50e47e796d feat: provide hook point for bulk transaction tasks
(cherry picked from commit d4dd01d8d1)
2024-09-30 04:07:53 +00:00
Khushi Rawat
4d3e43bdbe Merge pull request #43412 from frappe/mergify/bp/version-15-hotfix/pr-43399
feat: added 'cost of new capitalized asset' column (backport #43399)
2024-09-29 23:28:43 +05:30
rohitwaghchaure
928c887de5 Merge pull request #43433 from frappe/mergify/bp/version-15-hotfix/pr-43420
fix: Stock Ledger Invariant Check report (backport #43420)
2024-09-29 22:55:58 +05:30
Rohit Waghchaure
2984bad2c0 fix: Stock Ledger Invariant Check report
(cherry picked from commit d7daedc5b2)
2024-09-29 17:07:33 +00:00
Frappe PR Bot
2d09ef2509 chore(release): Bumped to Version 15.36.4
## [15.36.4](https://github.com/frappe/erpnext/compare/v15.36.3...v15.36.4) (2024-09-29)

### Bug Fixes

* Data missing in table: None, MandatoryError (backport [#43422](https://github.com/frappe/erpnext/issues/43422)) ([#43429](https://github.com/frappe/erpnext/issues/43429)) ([2c4610c](2c4610c021))
2024-09-29 16:38:13 +00:00
rohitwaghchaure
8b5997e38f Merge pull request #43431 from frappe/mergify/bp/version-15/pr-43429
fix: Data missing in table: None, MandatoryError (backport #43422) (backport #43429)
2024-09-29 22:07:02 +05:30
mergify[bot]
86b10ce9bb fix: tests for work order consumption (backport #41814) (#43430)
fix: tests for work order consumption (#41814)

* fix: tests for work order automatic SABB creation

* fix: qty

* chore: show created sabb

* chore: fix syntax

* fix: check SABB qty

* fix: add batched consumable to manufacture

* fix: missing fg qty field

* fix: improve test debug

* chore: linting

* chore: removed extra hash icons

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
(cherry picked from commit ca3c680909)

Co-authored-by: Richard Case <110036763+casesolved-co-uk@users.noreply.github.com>
2024-09-29 22:06:50 +05:30
mergify[bot]
2c4610c021 fix: Data missing in table: None, MandatoryError (backport #43422) (#43429)
fix: Data missing in table: None, MandatoryError (#43422)

(cherry picked from commit 8e33e0e1d2)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit 4b3f143f83)
2024-09-29 15:57:24 +00:00
mergify[bot]
4b3f143f83 fix: Data missing in table: None, MandatoryError (backport #43422) (#43429)
fix: Data missing in table: None, MandatoryError (#43422)

(cherry picked from commit 8e33e0e1d2)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-09-29 21:18:25 +05:30
mergify[bot]
d495d93840 fix: use serial and batch fields (backport #43421) (#43423)
fix: use serial and batch fields (#43421)

(cherry picked from commit ca16089d9d)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-09-29 11:09:32 +05:30
mergify[bot]
a1b6628c41 fix: quality inspection creation (backport #43416) (#43417)
fix: quality inspection creation (#43416)

(cherry picked from commit a594c05296)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-09-29 09:46:14 +05:30
Abdeali Chharchhoda
770bc1c293 fix: Remove unreference method 2024-09-28 12:29:34 +05:30
Abdeali Chharchhoda
907e3af1b0 fix: Remove advance_payment_status uses 2024-09-28 12:09:33 +05:30
Frappe PR Bot
e706aa692a chore(release): Bumped to Version 15.36.3
## [15.36.3](https://github.com/frappe/erpnext/compare/v15.36.2...v15.36.3) (2024-09-27)

### Bug Fixes

* Ignore transaction deletion check on ledger entry insertion ([c1f14f2](c1f14f2991))
2024-09-27 18:30:37 +00:00
Deepesh Garg
a5fa287dad Merge pull request #43413 from frappe/mergify/bp/version-15/pr-43411
fix: Ignore transaction deletion check on ledger entry insertion (#43410)
2024-09-27 23:58:16 +05:30
Deepesh Garg
c1f14f2991 fix: Ignore transaction deletion check on ledger entry insertion
(cherry picked from commit 998f6a92a4)
(cherry picked from commit 1d6f97ad94)
2024-09-27 18:25:49 +00:00
Deepesh Garg
6d66002374 Merge pull request #43411 from frappe/mergify/bp/version-15-hotfix/pr-43410
fix: Ignore transaction deletion check on ledger entry insertion (#43410)
2024-09-27 23:54:54 +05:30
Khushi Rawat
27cd51e267 feat: added 'cost of new capitalized asset' column
(cherry picked from commit 1eb9cc33fc)
2024-09-27 18:23:34 +00:00
Deepesh Garg
1d6f97ad94 fix: Ignore transaction deletion check on ledger entry insertion
(cherry picked from commit 998f6a92a4)
2024-09-27 18:22:41 +00:00
Abdeali Chharchhodawala
ea69ba7cd8 fix: multiple issues in Payment Request (#42427)
* fix: multiple issues in Payment Request

* chore: minor changes

* fix: remove  bug

* fix: replace `round` with `flt`

* fix: update `set_advance_payment_status()` logic

* fix: removed bug of `set_advance_payment_status`

* fix: changes as per review

* refactor: replace sql query of `matched_payment_requests` to query builder

* fix: replace `locals` with `get_doc` in set_query

* fix: changes during review

* fix: minor review changes

* fix: remove unnecessary code for setting payment entry received amount

* fix: logic for ser payment_request if PE made from transaction

* fix: Use rounded total to make Payment Request from `Sales Invoice` or `Purchase Invoice`

* refactor: enhance logic of `set_open_payment_requests_to_references`

* fix: added one optional arg `created_from_payment_request`

* fix: handle multiple allocation of PR at PE's reference

* fix: logic for PR if outstanding docs fetch

* fix: formatted Link field for `Payment Request` for PE's references

* fix: replace `get_all()` with `get_list()` for getting Payment Request for Link field

* fix: replace `get_all()` with `get_list()` for getting Payment Request for Link field

* chore: format `payment_entry.js` file

* style: Show preview popup of `Payment Request`

* fix: remove minor bug

* fix: add virtual field for Payment Term and Request `outstanding_amount` in PE's reference

* fix: get outstanding amount in PE's reference on realtime

* fix: move allocation of allocated_amount to server side (no change)

* fix: some minor changes to allocation

* fix: Split `Payment Request` if PE is created from PR and there are `Payment Terms`

* fix: minor logic changes

* fix: Allocation of allocated_amount if `paid_amount` is changes

* fix: improve logic of allocation

* fix: set matched payment request if unset

* fix: minor changes

* fix: Allocate single Payment Request if PE created from PR

* fix: improve code logic

* fix: Removed duplication code

* fix: proper message title

* refactor: Rename method of Allocation Amount to References

* refactor: Changing `grand_total` description based on `party_type`

* refactor: update Payment Request

* fix: Remove virtual property of payment_term_oustanding from references

* fix: fetch party account currency for creating payment request

* fix: use transaction currency as base in payment request

* fix: party amount for creating payment entry

* fix: allow for proportional amount paid by bank

* fix: Changed field order in Payment Request

* fix: Minor refactor in Payment Entry Reference table data

* test: Added test cases for allow Payment at `Partially Paid` status for PR

* test: Update partial paid status test case

* test: Update test case for same currency PR

* refactor: Wider the `msgprint` dialog for after save PE

* test: Update PR test cases

* chore: Remove dirty lines

* test: Checking `Advance Payment Status`

* fix: formatting update

* fix: Use `flt` where doing subtraction

* test: PR test case with Payment Term for same currency

* fix: remove redundant `flt`

* test: Add test cases for PR

---------

Co-authored-by: Sagar Vora <sagar@resilient.tech>
2024-09-27 20:29:22 +05:30
mergify[bot]
74c880c232 fix: serial and batch no selector (backport #43387) (#43390)
fix: serial and batch no selector (#43387)

(cherry picked from commit e4e96d2a44)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-09-26 12:06:27 +05:30
mergify[bot]
47f06dc180 fix: Cannot read properties of undefined (reading 'price_list_rate') (backport #43376) (#43377)
fix: Cannot read properties of undefined (reading 'price_list_rate') (#43376)

(cherry picked from commit a63dca0984)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-09-25 10:09:17 +05:30
Ninad1306
0a70b3ffcc fix: frappe dependency update 2024-09-24 11:24:31 +05:30
ruthra kumar
4278b08147 Merge pull request #43065 from Ninad1306/merge_taxes_fix
fix: Reset Value Conditionally Based on Merge Taxes
2024-09-24 11:10:09 +05:30
Smit Vora
f36a68b42b Merge pull request #42925 from Ninad1306/mapping_docs_fix
fix: Replace `add_if_empty` with `reset_value` flag
2024-09-24 11:10:09 +05:30
Nihantra Patel
5a2a404a50 fix: creation of contact, customer, opportunity, quotation and prospect from lead --prettier
(cherry picked from commit 5844897c34)

# Conflicts:
#	erpnext/crm/doctype/lead/lead.js
2024-09-19 05:58:01 +00:00
Nihantra Patel
ef10c4ea4f fix: creation of contact, customer, opportunity, quotation and prospect from lead
(cherry picked from commit 8304d19e8b)

# Conflicts:
#	erpnext/crm/doctype/lead/lead.js
2024-09-19 05:58:01 +00:00
90 changed files with 2912 additions and 780 deletions

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.36.2"
__version__ = "15.38.3"
def get_default_company(user=None):

View File

@@ -109,7 +109,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "frankfurter.app/{transaction_date}"
api = "api.frankfurter.app/{transaction_date}"
protocol = "https://"
if use_http:

View File

@@ -210,19 +210,31 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
@frappe.whitelist()
def get_dunning_letter_text(dunning_type, doc, language=None):
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if isinstance(doc, str):
doc = json.loads(doc)
if not language:
language = doc.get("language")
if language:
filters = {"parent": dunning_type, "language": language}
else:
filters = {"parent": dunning_type, "is_default_language": 1}
letter_text = frappe.db.get_value(
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
)
if letter_text:
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
)
if not letter_text:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
)
if not letter_text:
return {}
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}

View File

@@ -174,6 +174,17 @@ frappe.ui.form.on("Payment Entry", {
};
});
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
return {
query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query",
filters: {
reference_doctype: row.reference_doctype,
reference_name: row.reference_name,
},
};
});
frm.set_query("sales_taxes_and_charges_template", function () {
return {
filters: {
@@ -191,7 +202,15 @@ frappe.ui.form.on("Payment Entry", {
},
};
});
frm.add_fetch(
"payment_request",
"outstanding_amount",
"payment_request_outstanding",
"Payment Entry Reference"
);
},
refresh: function (frm) {
erpnext.hide_company(frm);
frm.events.hide_unhide_fields(frm);
@@ -216,6 +235,7 @@ frappe.ui.form.on("Payment Entry", {
);
}
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
frappe.flags.allocate_payment_amount = true;
},
validate_company: (frm) => {
@@ -797,7 +817,7 @@ frappe.ui.form.on("Payment Entry", {
);
if (frm.doc.payment_type == "Pay")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
else frm.events.set_unallocated_amount(frm);
frm.set_paid_amount_based_on_received_amount = false;
@@ -818,7 +838,7 @@ frappe.ui.form.on("Payment Entry", {
}
if (frm.doc.payment_type == "Receive")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true);
else frm.events.set_unallocated_amount(frm);
},
@@ -989,6 +1009,7 @@ frappe.ui.form.on("Payment Entry", {
c.outstanding_amount = d.outstanding_amount;
c.bill_no = d.bill_no;
c.payment_term = d.payment_term;
c.payment_term_outstanding = d.payment_term_outstanding;
c.allocated_amount = d.allocated_amount;
c.account = d.account;
@@ -1038,7 +1059,8 @@ frappe.ui.form.on("Payment Entry", {
frm.events.allocate_party_amount_against_ref_docs(
frm,
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount,
false
);
},
});
@@ -1052,93 +1074,13 @@ frappe.ui.form.on("Payment Entry", {
return ["Sales Invoice", "Purchase Invoice"];
},
allocate_party_amount_against_ref_docs: function (frm, paid_amount, paid_amount_change) {
var total_positive_outstanding_including_order = 0;
var total_negative_outstanding = 0;
var total_deductions = frappe.utils.sum(
$.map(frm.doc.deductions || [], function (d) {
return flt(d.amount);
})
);
paid_amount -= total_deductions;
$.each(frm.doc.references || [], function (i, row) {
if (flt(row.outstanding_amount) > 0)
total_positive_outstanding_including_order += flt(row.outstanding_amount);
else total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
await frm.call("allocate_amount_to_references", {
paid_amount: paid_amount,
paid_amount_change: paid_amount_change,
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
});
var allocated_negative_outstanding = 0;
if (
(frm.doc.payment_type == "Receive" && frm.doc.party_type == "Customer") ||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Supplier") ||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Employee")
) {
if (total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
allocated_negative_outstanding =
total_negative_outstanding < remaining_outstanding
? total_negative_outstanding
: remaining_outstanding;
}
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"));
if (paid_amount > total_negative_outstanding) {
if (total_negative_outstanding == 0) {
frappe.msgprint(
__("Cannot {0} {1} {2} without any negative outstanding invoice", [
frm.doc.payment_type,
frm.doc.party_type == "Customer" ? "to" : "from",
frm.doc.party_type,
])
);
return false;
} else {
frappe.msgprint(
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [
total_negative_outstanding,
])
);
return false;
}
} else {
allocated_positive_outstanding = total_negative_outstanding - paid_amount;
allocated_negative_outstanding =
paid_amount +
(total_positive_outstanding_including_order < allocated_positive_outstanding
? total_positive_outstanding_including_order
: allocated_positive_outstanding);
}
}
$.each(frm.doc.references || [], function (i, row) {
if (frappe.flags.allocate_payment_amount == 0) {
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
row.allocated_amount = 0;
} else if (
frappe.flags.allocate_payment_amount != 0 &&
(!row.allocated_amount || paid_amount_change)
) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
row.allocated_amount =
row.outstanding_amount >= allocated_positive_outstanding
? allocated_positive_outstanding
: row.outstanding_amount;
allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
row.allocated_amount =
Math.abs(row.outstanding_amount) >= allocated_negative_outstanding
? -1 * allocated_negative_outstanding
: row.outstanding_amount;
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
}
}
});
frm.refresh_fields();
frm.events.set_total_allocated_amount(frm);
},
@@ -1686,6 +1628,62 @@ frappe.ui.form.on("Payment Entry", {
return current_tax_amount;
},
cost_center: function (frm) {
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
args: {
company: frm.doc.company,
date: frm.doc.posting_date,
paid_from: frm.doc.paid_from,
paid_to: frm.doc.paid_to,
ptype: frm.doc.party_type,
pty: frm.doc.party,
cost_center: frm.doc.cost_center,
},
callback: function (r, rt) {
if (r.message) {
frappe.run_serially([
() => {
frm.set_value(
"paid_from_account_balance",
r.message.paid_from_account_balance
);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance);
},
]);
}
},
});
}
},
after_save: function (frm) {
const { matched_payment_requests } = frappe.last_response;
if (!matched_payment_requests) return;
const COLUMN_LABEL = [
[__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")],
];
frappe.msgprint({
title: __("Unset Matched Payment Request"),
message: COLUMN_LABEL.concat(matched_payment_requests),
as_table: true,
wide: true,
primary_action: {
label: __("Allocate Payment Request"),
action() {
frappe.hide_msgprint();
frm.call("set_matched_payment_requests", { matched_payment_requests }, () => {
frm.dirty();
});
},
},
});
},
});
frappe.ui.form.on("Payment Entry Reference", {
@@ -1778,35 +1776,3 @@ frappe.ui.form.on("Payment Entry Deduction", {
frm.events.set_unallocated_amount(frm);
},
});
frappe.ui.form.on("Payment Entry", {
cost_center: function (frm) {
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
args: {
company: frm.doc.company,
date: frm.doc.posting_date,
paid_from: frm.doc.paid_from,
paid_to: frm.doc.paid_to,
ptype: frm.doc.party_type,
pty: frm.doc.party,
cost_center: frm.doc.cost_center,
},
callback: function (r, rt) {
if (r.message) {
frappe.run_serially([
() => {
frm.set_value(
"paid_from_account_balance",
r.message.paid_from_account_balance
);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance);
},
]);
}
},
});
}
},
});

View File

@@ -7,8 +7,10 @@ from functools import reduce
import frappe
from frappe import ValidationError, _, qb, scrub, throw
from frappe.query_builder import Tuple
from frappe.query_builder.functions import Count
from frappe.utils import cint, comma_or, flt, getdate, nowdate
from frappe.utils.data import comma_and, fmt_money
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
from pypika import Case
from pypika.functions import Coalesce, Sum
@@ -98,13 +100,17 @@ class PaymentEntry(AccountsController):
self.set_status()
self.set_total_in_words()
def before_save(self):
self.set_matched_unset_payment_requests_to_response()
def on_submit(self):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries()
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_payment_schedule()
self.update_payment_requests()
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def set_liability_account(self):
@@ -188,30 +194,34 @@ class PaymentEntry(AccountsController):
super().on_cancel()
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_payment_req_status()
self.update_payment_requests(cancel=True)
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def set_payment_req_status(self):
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
def update_payment_requests(self, cancel=False):
from erpnext.accounts.doctype.payment_request.payment_request import (
update_payment_requests_as_per_pe_references,
)
update_payment_req_status(self, None)
update_payment_requests_as_per_pe_references(self.references, cancel=cancel)
def update_outstanding_amounts(self):
self.set_missing_ref_details(force=True)
def validate_duplicate_entry(self):
reference_names = []
reference_names = set()
for d in self.get("references"):
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request)
if key in reference_names:
frappe.throw(
_("Row #{0}: Duplicate entry in References {1} {2}").format(
d.idx, d.reference_doctype, d.reference_name
)
)
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
reference_names.add(key)
def set_bank_account_data(self):
if self.bank_account:
@@ -237,6 +247,8 @@ class PaymentEntry(AccountsController):
if self.payment_type == "Internal Transfer":
return
self.validate_allocated_amount_as_per_payment_request()
if self.party_type in ("Customer", "Supplier"):
self.validate_allocated_amount_with_latest_data()
else:
@@ -249,6 +261,27 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
def validate_allocated_amount_as_per_payment_request(self):
"""
Allocated amount should not be greater than the outstanding amount of the Payment Request.
"""
if not self.references:
return
pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references)
if not pr_outstanding_amounts:
return
for ref in self.references:
if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]:
frappe.throw(
msg=_(
"Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}"
).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)),
title=_("Invalid Allocated Amount"),
)
def term_based_allocation_enabled_for_reference(
self, reference_doctype: str, reference_name: str
) -> bool:
@@ -1133,6 +1166,8 @@ class PaymentEntry(AccountsController):
if not self.party_account:
return
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
if self.payment_type == "Receive":
against_account = self.paid_to
else:
@@ -1178,11 +1213,30 @@ class PaymentEntry(AccountsController):
{
dr_or_cr: allocated_amount_in_company_currency,
dr_or_cr + "_in_account_currency": d.allocated_amount,
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
"cost_center": cost_center,
}
)
if self.book_advance_payments_in_separate_party_account:
if d.reference_doctype in advance_payment_doctypes:
# Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine
gle.update(
{
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
}
)
else:
# Do not reference Invoices while Advance is in separate party account
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
else:
gle.update(
{
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
}
)
gl_entries.append(gle)
if self.unallocated_amount:
@@ -1606,6 +1660,380 @@ class PaymentEntry(AccountsController):
return current_tax_fraction
def set_matched_unset_payment_requests_to_response(self):
"""
Find matched Payment Requests for those references which have no Payment Request set.\n
And set to `frappe.response` to show in the frontend for allocation.
"""
if not self.references:
return
matched_payment_requests = get_matched_payment_request_of_references(
[row for row in self.references if not row.payment_request]
)
if not matched_payment_requests:
return
frappe.response["matched_payment_requests"] = matched_payment_requests
@frappe.whitelist()
def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount):
"""
Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n
:param paid_amount: Paid Amount / Received Amount.
:param paid_amount_change: Flag to check if `Paid Amount` is changed or not.
:param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag)
"""
if not self.references:
return
if not allocate_payment_amount:
for ref in self.references:
ref.allocated_amount = 0
return
# calculating outstanding amounts
precision = self.precision("paid_amount")
total_positive_outstanding_including_order = 0
total_negative_outstanding = 0
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
for ref in self.references:
reference_outstanding_amount = ref.outstanding_amount
abs_outstanding_amount = abs(reference_outstanding_amount)
if reference_outstanding_amount > 0:
total_positive_outstanding_including_order += abs_outstanding_amount
else:
total_negative_outstanding += abs_outstanding_amount
# calculating allocated outstanding amounts
allocated_negative_outstanding = 0
allocated_positive_outstanding = 0
# checking party type and payment type
if (self.payment_type == "Receive" and self.party_type == "Customer") or (
self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee")
):
if total_positive_outstanding_including_order > paid_amount:
remaining_outstanding = flt(
total_positive_outstanding_including_order - paid_amount, precision
)
allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding)
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
elif self.party_type in ("Supplier", "Employee"):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(
_("Cannot {0} from {2} without any negative outstanding invoice").format(
self.payment_type,
self.party_type,
)
)
else:
frappe.msgprint(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
)
)
return
else:
allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision)
allocated_negative_outstanding = paid_amount + min(
total_positive_outstanding_including_order, allocated_positive_outstanding
)
# inner function to set `allocated_amount` to those row which have no PR
def _allocation_to_unset_pr_row(
row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding
):
if outstanding_amount > 0 and allocated_positive_outstanding >= 0:
row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount)
allocated_positive_outstanding = flt(
allocated_positive_outstanding - row.allocated_amount, precision
)
elif outstanding_amount < 0 and allocated_negative_outstanding:
row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1
allocated_negative_outstanding = flt(
allocated_negative_outstanding - abs(row.allocated_amount), precision
)
return allocated_positive_outstanding, allocated_negative_outstanding
# allocate amount based on `paid_amount` is changed or not
if not paid_amount_change:
for ref in self.references:
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
ref,
ref.outstanding_amount,
allocated_positive_outstanding,
allocated_negative_outstanding,
)
allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount"))
else:
payment_request_outstanding_amounts = (
get_payment_request_outstanding_set_in_references(self.references) or {}
)
references_outstanding_amounts = get_references_outstanding_amount(self.references) or {}
remaining_references_allocated_amounts = references_outstanding_amounts.copy()
# Re allocate amount to those references which have PR set (Higher priority)
for ref in self.references:
if not ref.payment_request:
continue
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
reference_outstanding_amount = references_outstanding_amounts[key]
pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request]
if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0:
# allocate amount according to outstanding amounts
outstanding_amounts = (
allocated_positive_outstanding,
reference_outstanding_amount,
pr_outstanding_amount,
)
ref.allocated_amount = min(outstanding_amounts)
# update amounts to track allocation
allocated_amount = ref.allocated_amount
allocated_positive_outstanding = flt(
allocated_positive_outstanding - allocated_amount, precision
)
remaining_references_allocated_amounts[key] = flt(
remaining_references_allocated_amounts[key] - allocated_amount, precision
)
payment_request_outstanding_amounts[ref.payment_request] = flt(
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
)
elif reference_outstanding_amount < 0 and allocated_negative_outstanding:
# allocate amount according to outstanding amounts
outstanding_amounts = (
allocated_negative_outstanding,
abs(reference_outstanding_amount),
pr_outstanding_amount,
)
ref.allocated_amount = min(outstanding_amounts) * -1
# update amounts to track allocation
allocated_amount = abs(ref.allocated_amount)
allocated_negative_outstanding = flt(
allocated_negative_outstanding - allocated_amount, precision
)
remaining_references_allocated_amounts[key] += allocated_amount # negative amount
payment_request_outstanding_amounts[ref.payment_request] = flt(
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
)
# Re allocate amount to those references which have no PR (Lower priority)
for ref in self.references:
if ref.payment_request:
continue
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
reference_outstanding_amount = remaining_references_allocated_amounts[key]
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
ref,
reference_outstanding_amount,
allocated_positive_outstanding,
allocated_negative_outstanding,
)
@frappe.whitelist()
def set_matched_payment_requests(self, matched_payment_requests):
"""
Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n
:param matched_payment_requests: List of tuple of matched Payment Requests.
---
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
"""
if not self.references or not matched_payment_requests:
return
if isinstance(matched_payment_requests, str):
matched_payment_requests = json.loads(matched_payment_requests)
# modify matched_payment_requests
# like (reference_doctype, reference_name, allocated_amount): payment_request
payment_requests = {}
for row in matched_payment_requests:
key = tuple(row[:3])
payment_requests[key] = row[3]
for ref in self.references:
if ref.payment_request:
continue
key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount)
if key in payment_requests:
ref.payment_request = payment_requests[key]
del payment_requests[key] # to avoid duplicate allocation
def get_matched_payment_request_of_references(references=None):
"""
Get those `Payment Requests` which are matched with `References`.\n
- Amount must be same.
- Only single `Payment Request` available for this amount.
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
"""
if not references:
return
# to fetch matched rows
refs = {
(row.reference_doctype, row.reference_name, row.allocated_amount)
for row in references
if row.reference_doctype and row.reference_name and row.allocated_amount
}
if not refs:
return
PR = frappe.qb.DocType("Payment Request")
# query to group by reference_doctype, reference_name, outstanding_amount
subquery = (
frappe.qb.from_(PR)
.select(
PR.reference_doctype,
PR.reference_name,
PR.outstanding_amount.as_("allocated_amount"),
PR.name.as_("payment_request"),
Count("*").as_("count"),
)
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
.where(PR.status != "Paid")
.where(PR.docstatus == 1)
.groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
)
# query to fetch matched rows which are single
matched_prs = (
frappe.qb.from_(subquery)
.select(
subquery.reference_doctype,
subquery.reference_name,
subquery.allocated_amount,
subquery.payment_request,
)
.where(subquery.count == 1)
.run()
)
return matched_prs if matched_prs else None
def get_references_outstanding_amount(references=None):
"""
Fetch accurate outstanding amount of `References`.\n
- If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`.
- If `Payment Term` is not set, then fetch outstanding amount from `References` it self.
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
"""
if not references:
return
refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {}
refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {}
return {**refs_with_payment_term, **refs_without_payment_term}
def get_outstanding_of_references_with_payment_term(references=None):
"""
Fetch outstanding amount of `References` which have `Payment Term` set.\n
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
"""
if not references:
return
refs = {
(row.reference_doctype, row.reference_name, row.payment_term)
for row in references
if row.reference_doctype and row.reference_name and row.payment_term
}
if not refs:
return
PS = frappe.qb.DocType("Payment Schedule")
response = (
frappe.qb.from_(PS)
.select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding)
.where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs))
).run(as_dict=True)
if not response:
return
return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response}
def get_outstanding_of_references_with_no_payment_term(references):
"""
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
- Fetch outstanding amount from `References` it self.
Note: `None` is used for allocation of `Payment Request`
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
"""
if not references:
return
outstanding_amounts = {}
for ref in references:
if ref.payment_term:
continue
key = (ref.reference_doctype, ref.reference_name, None)
if key not in outstanding_amounts:
outstanding_amounts[key] = ref.outstanding_amount
return outstanding_amounts
def get_payment_request_outstanding_set_in_references(references=None):
"""
Fetch outstanding amount of `Payment Request` which are set in `References`.\n
Example: {payment_request: outstanding_amount, ...}
"""
if not references:
return
referenced_payment_requests = {row.payment_request for row in references if row.payment_request}
if not referenced_payment_requests:
return
PR = frappe.qb.DocType("Payment Request")
response = (
frappe.qb.from_(PR)
.select(PR.name, PR.outstanding_amount)
.where(PR.name.isin(referenced_payment_requests))
).run()
return dict(response) if response else None
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
@@ -2236,6 +2664,8 @@ def get_payment_entry(
party_type=None,
payment_type=None,
reference_date=None,
ignore_permissions=False,
created_from_payment_request=False,
):
doc = frappe.get_doc(dt, dn)
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
@@ -2385,9 +2815,179 @@ def get_payment_entry(
pe.set_difference_amount()
# If PE is created from PR directly, then no need to find open PRs for the references
if not created_from_payment_request:
allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount"))
return pe
def get_open_payment_requests_for_references(references=None):
"""
Fetch all unpaid Payment Requests for the references. \n
- Each reference can have multiple Payment Requests. \n
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
"""
if not references:
return
refs = {
(row.reference_doctype, row.reference_name)
for row in references
if row.reference_doctype and row.reference_name and row.allocated_amount
}
if not refs:
return
PR = frappe.qb.DocType("Payment Request")
response = (
frappe.qb.from_(PR)
.select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
.where(PR.status != "Paid")
.where(PR.docstatus == 1)
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
).run(as_dict=True)
if not response:
return
reference_payment_requests = {}
for row in response:
key = (row.reference_doctype, row.reference_name)
if key not in reference_payment_requests:
reference_payment_requests[key] = {row.name: row.outstanding_amount}
else:
reference_payment_requests[key][row.name] = row.outstanding_amount
return reference_payment_requests
def allocate_open_payment_requests_to_references(references=None, precision=None):
"""
Allocate unpaid Payment Requests to the references. \n
---
- Allocation based on below factors
- Reference Allocated Amount
- Reference Outstanding Amount (With Payment Terms or without Payment Terms)
- Reference Payment Request's outstanding amount
---
- Allocation based on below scenarios
- Reference's Allocated Amount == Payment Request's Outstanding Amount
- Allocate the Payment Request to the reference
- This PR will not be allocated further
- Reference's Allocated Amount < Payment Request's Outstanding Amount
- Allocate the Payment Request to the reference
- Reduce the PR's outstanding amount by the allocated amount
- This PR can be allocated further
- Reference's Allocated Amount > Payment Request's Outstanding Amount
- Allocate the Payment Request to the reference
- Reduce Allocated Amount of the reference by the PR's outstanding amount
- Create a new row for the remaining amount until the Allocated Amount is 0
- Allocate PR if available
---
- Note:
- Priority is given to the first Payment Request of respective references.
- Single Reference can have multiple rows.
- With Payment Terms or without Payment Terms
- With Payment Request or without Payment Request
"""
if not references:
return
# get all unpaid payment requests for the references
references_open_payment_requests = get_open_payment_requests_for_references(references)
if not references_open_payment_requests:
return
if not precision:
precision = references[0].precision("allocated_amount")
# to manage new rows
row_number = 1
MOVE_TO_NEXT_ROW = 1
TO_SKIP_NEW_ROW = 2
while row_number <= len(references):
row = references[row_number - 1]
reference_key = (row.reference_doctype, row.reference_name)
# update the idx to maintain the order
row.idx = row_number
# unpaid payment requests for the reference
reference_payment_requests = references_open_payment_requests.get(reference_key)
if not reference_payment_requests:
row_number += MOVE_TO_NEXT_ROW # to move to next reference row
continue
# get the first payment request and its outstanding amount
payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items()))
allocated_amount = row.allocated_amount
# allocate the payment request to the reference and PR's outstanding amount
row.payment_request = payment_request
if pr_outstanding_amount == allocated_amount:
del reference_payment_requests[payment_request]
row_number += MOVE_TO_NEXT_ROW
elif pr_outstanding_amount > allocated_amount:
# reduce the outstanding amount of the payment request
reference_payment_requests[payment_request] -= allocated_amount
row_number += MOVE_TO_NEXT_ROW
else:
# split the reference row to allocate the remaining amount
del reference_payment_requests[payment_request]
row.allocated_amount = pr_outstanding_amount
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
# set the remaining amount to the next row
while allocated_amount:
# create a new row for the remaining amount
new_row = frappe.copy_doc(row)
references.insert(row_number, new_row)
# get the first payment request and its outstanding amount
payment_request, pr_outstanding_amount = next(
iter(reference_payment_requests.items()), (None, None)
)
# update new row
new_row.idx = row_number + 1
new_row.payment_request = payment_request
new_row.allocated_amount = min(
pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount
)
if not payment_request or not pr_outstanding_amount:
row_number += TO_SKIP_NEW_ROW
break
elif pr_outstanding_amount == allocated_amount:
del reference_payment_requests[payment_request]
row_number += TO_SKIP_NEW_ROW
break
elif pr_outstanding_amount > allocated_amount:
reference_payment_requests[payment_request] -= allocated_amount
row_number += TO_SKIP_NEW_ROW
break
else:
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
del reference_payment_requests[payment_request]
row_number += MOVE_TO_NEXT_ROW
def update_accounting_dimensions(pe, doc):
"""
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document

View File

@@ -10,6 +10,7 @@
"due_date",
"bill_no",
"payment_term",
"payment_term_outstanding",
"account_type",
"payment_type",
"column_break_4",
@@ -18,7 +19,9 @@
"allocated_amount",
"exchange_rate",
"exchange_gain_loss",
"account"
"account",
"payment_request",
"payment_request_outstanding"
],
"fields": [
{
@@ -120,12 +123,33 @@
"fieldname": "payment_type",
"fieldtype": "Data",
"label": "Payment Type"
},
{
"fieldname": "payment_request",
"fieldtype": "Link",
"label": "Payment Request",
"options": "Payment Request"
},
{
"depends_on": "eval: doc.payment_term",
"fieldname": "payment_term_outstanding",
"fieldtype": "Float",
"label": "Payment Term Outstanding",
"read_only": 1
},
{
"depends_on": "eval: doc.payment_request && doc.payment_request_outstanding",
"fieldname": "payment_request_outstanding",
"fieldtype": "Float",
"is_virtual": 1,
"label": "Payment Request Outstanding",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-04-05 09:44:08.310593",
"modified": "2024-09-16 18:11:50.019343",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -1,7 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
@@ -25,11 +25,19 @@ class PaymentEntryReference(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
payment_request: DF.Link | None
payment_request_outstanding: DF.Float
payment_term: DF.Link | None
payment_term_outstanding: DF.Float
payment_type: DF.Data | None
reference_doctype: DF.Link
reference_name: DF.DynamicLink
total_amount: DF.Float
# end: auto-generated types
pass
@property
def payment_request_outstanding(self):
if not self.payment_request:
return
return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount")

View File

@@ -48,8 +48,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
}
if (
(!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") &&
frm.doc.status == "Initiated"
frm.doc.payment_request_type == "Outward" &&
["Initiated", "Partially Paid"].includes(frm.doc.status)
) {
frm.add_custom_button(__("Create Payment Entry"), function () {
frappe.call({

View File

@@ -19,9 +19,11 @@
"reference_name",
"transaction_details",
"grand_total",
"currency",
"is_a_subscription",
"column_break_18",
"currency",
"outstanding_amount",
"party_account_currency",
"subscription_section",
"subscription_plans",
"bank_account_details",
@@ -69,6 +71,7 @@
{
"fieldname": "transaction_date",
"fieldtype": "Date",
"in_preview": 1,
"label": "Transaction Date"
},
{
@@ -133,7 +136,8 @@
"no_copy": 1,
"options": "reference_doctype",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "transaction_details",
@@ -141,12 +145,14 @@
"label": "Transaction Details"
},
{
"description": "Amount in customer's currency",
"description": "Amount in transaction currency",
"fieldname": "grand_total",
"fieldtype": "Currency",
"in_preview": 1,
"label": "Amount",
"non_negative": 1,
"options": "currency"
"options": "currency",
"reqd": 1
},
{
"default": "0",
@@ -392,19 +398,37 @@
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval: doc.docstatus === 1",
"description": "Amount in party's bank account currency",
"fieldname": "outstanding_amount",
"fieldtype": "Currency",
"in_preview": 1,
"label": "Outstanding Amount",
"non_negative": 1,
"options": "party_account_currency",
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"fieldname": "party_account_currency",
"fieldtype": "Link",
"label": "Party Account Currency",
"options": "Currency",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-08-07 16:39:54.288002",
"modified": "2024-09-16 17:50:54.440090",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
@@ -439,6 +463,7 @@
"write": 1
}
],
"show_preview_popup": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []

View File

@@ -3,9 +3,11 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue
from erpnext import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
@@ -45,6 +47,7 @@ class PaymentRequest(Document):
bank_account: DF.Link | None
bank_account_no: DF.ReadOnly | None
branch_code: DF.ReadOnly | None
company: DF.Link | None
cost_center: DF.Link | None
currency: DF.Link | None
email_to: DF.Data | None
@@ -56,16 +59,18 @@ class PaymentRequest(Document):
mode_of_payment: DF.Link | None
mute_email: DF.Check
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
outstanding_amount: DF.Currency
party: DF.DynamicLink | None
party_account_currency: DF.Link | None
party_type: DF.Link | None
payment_account: DF.ReadOnly | None
payment_channel: DF.Literal["", "Email", "Phone"]
payment_channel: DF.Literal["", "Email", "Phone", "Other"]
payment_gateway: DF.ReadOnly | None
payment_gateway_account: DF.Link | None
payment_order: DF.Link | None
payment_request_type: DF.Literal["Outward", "Inward"]
payment_url: DF.Data | None
print_format: DF.Literal
print_format: DF.Literal[None]
project: DF.Link | None
reference_doctype: DF.Link | None
reference_name: DF.DynamicLink | None
@@ -84,7 +89,6 @@ class PaymentRequest(Document):
subscription_plans: DF.Table[SubscriptionPlanDetail]
swift_number: DF.ReadOnly | None
transaction_date: DF.Date | None
company: DF.Link | None
# end: auto-generated types
def validate(self):
@@ -100,6 +104,12 @@ class PaymentRequest(Document):
frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self):
if self.grand_total == 0:
frappe.throw(
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
title=_("Invalid Amount"),
)
existing_payment_request_amount = flt(
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
)
@@ -147,6 +157,28 @@ class PaymentRequest(Document):
).format(self.grand_total, amount)
)
def before_submit(self):
if (
self.currency != self.party_account_currency
and self.party_account_currency == get_company_currency(self.company)
):
# set outstanding amount in party account currency
invoice = frappe.get_value(
self.reference_doctype,
self.reference_name,
["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"],
as_dict=1,
)
grand_total = invoice.get("rounded_total") or invoice.get("grand_total")
base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total")
self.outstanding_amount = flt(
self.grand_total / grand_total * base_grand_total,
self.precision("outstanding_amount"),
)
else:
self.outstanding_amount = self.grand_total
def on_submit(self):
if self.payment_request_type == "Outward":
self.db_set("status", "Initiated")
@@ -275,7 +307,7 @@ class PaymentRequest(Document):
def set_as_paid(self):
if self.payment_channel == "Phone":
self.db_set("status", "Paid")
self.db_set({"status": "Paid", "outstanding_amount": 0})
else:
payment_entry = self.create_payment_entry()
@@ -296,26 +328,32 @@ class PaymentRequest(Document):
else:
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
party_account_currency = (
self.get("party_account_currency")
or ref_doc.get("party_account_currency")
or get_account_currency(party_account)
)
party_amount = bank_amount = self.outstanding_amount
bank_amount = self.grand_total
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
else:
party_amount = self.grand_total
exchange_rate = ref_doc.get("conversion_rate")
bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))
# outstanding amount is already in Part's account currency
payment_entry = get_payment_entry(
self.reference_doctype,
self.reference_name,
party_amount=party_amount,
bank_account=self.payment_account,
bank_amount=bank_amount,
created_from_payment_request=True,
)
payment_entry.update(
{
"mode_of_payment": self.mode_of_payment,
"reference_no": self.name,
"reference_no": self.name, # to prevent validation error
"reference_date": nowdate(),
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
self.reference_doctype, self.reference_name, self.name
@@ -323,6 +361,9 @@ class PaymentRequest(Document):
}
)
# Allocate payment_request for each reference in payment_entry (Payment Term can splits the row)
self._allocate_payment_request_to_pe_references(references=payment_entry.references)
# Update dimensions
payment_entry.update(
{
@@ -331,14 +372,6 @@ class PaymentRequest(Document):
}
)
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
amount = payment_entry.base_paid_amount
else:
amount = self.grand_total
payment_entry.received_amount = amount
payment_entry.get("references")[0].allocated_amount = amount
# Update 'Paid Amount' on Forex transactions
if self.currency != ref_doc.company_currency:
if (
@@ -429,6 +462,62 @@ class PaymentRequest(Document):
return create_stripe_subscription(gateway_controller, data)
def _allocate_payment_request_to_pe_references(self, references):
"""
Allocate the Payment Request to the Payment Entry references based on\n
- Allocated Amount.
- Outstanding Amount of Payment Request.\n
Payment Request is doc itself and references are the rows of Payment Entry.
"""
if len(references) == 1:
references[0].payment_request = self.name
return
precision = references[0].precision("allocated_amount")
outstanding_amount = self.outstanding_amount
# to manage rows
row_number = 1
MOVE_TO_NEXT_ROW = 1
TO_SKIP_NEW_ROW = 2
NEW_ROW_ADDED = False
while row_number <= len(references):
row = references[row_number - 1]
# update the idx to maintain the order
row.idx = row_number
if outstanding_amount == 0:
if not NEW_ROW_ADDED:
break
row_number += MOVE_TO_NEXT_ROW
continue
# allocate the payment request to the row
row.payment_request = self.name
if row.allocated_amount <= outstanding_amount:
outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision)
row_number += MOVE_TO_NEXT_ROW
else:
remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision)
row.allocated_amount = outstanding_amount
outstanding_amount = 0
# create a new row without PR for remaining unallocated amount
new_row = frappe.copy_doc(row)
references.insert(row_number, new_row)
# update new row
new_row.idx = row_number + 1
new_row.payment_request = None
new_row.allocated_amount = remaining_allocated_amount
NEW_ROW_ADDED = True
row_number += TO_SKIP_NEW_ROW
@frappe.whitelist(allow_guest=True)
def make_payment_request(**args):
@@ -459,11 +548,15 @@ def make_payment_request(**args):
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
)
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
if not grand_total:
frappe.throw(_("Payment Request is already created"))
if draft_payment_request:
frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
@@ -477,6 +570,13 @@ def make_payment_request(**args):
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
)
party_type = args.get("party_type") or "Customer"
party_account_currency = ref_doc.get("party_account_currency")
if not party_account_currency:
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
party_account_currency = get_account_currency(party_account)
pr.update(
{
"payment_gateway_account": gateway_account.get("name"),
@@ -485,6 +585,7 @@ def make_payment_request(**args):
"payment_channel": gateway_account.get("payment_channel"),
"payment_request_type": args.get("payment_request_type"),
"currency": ref_doc.currency,
"party_account_currency": party_account_currency,
"grand_total": grand_total,
"mode_of_payment": args.mode_of_payment,
"email_to": args.recipient_id or ref_doc.owner,
@@ -493,7 +594,7 @@ def make_payment_request(**args):
"reference_doctype": args.dt,
"reference_name": args.dn,
"company": ref_doc.get("company"),
"party_type": args.get("party_type") or "Customer",
"party_type": party_type,
"party": args.get("party") or ref_doc.get("customer"),
"bank_account": bank_account,
}
@@ -539,9 +640,11 @@ def get_amount(ref_doc, payment_account=None):
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.grand_total)
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
else:
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
grand_total = flt(
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
)
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
@@ -563,24 +666,20 @@ def get_amount(ref_doc, payment_account=None):
def get_existing_payment_request_amount(ref_dt, ref_dn):
"""
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
and get the summation of existing paid payment request for Phone payment channel.
Return the total amount of Payment Requests against a reference document.
"""
existing_payment_request_amount = frappe.db.sql(
"""
select sum(grand_total)
from `tabPayment Request`
where
reference_doctype = %s
and reference_name = %s
and docstatus = 1
and (status != 'Paid'
or (payment_channel = 'Phone'
and status = 'Paid'))
""",
(ref_dt, ref_dn),
PR = frappe.qb.DocType("Payment Request")
response = (
frappe.qb.from_(PR)
.select(Sum(PR.grand_total))
.where(PR.reference_doctype == ref_dt)
.where(PR.reference_name == ref_dn)
.where(PR.docstatus == 1)
.run()
)
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
return response[0][0] if response[0] else 0
def get_gateway_details(args): # nosemgrep
@@ -627,41 +726,66 @@ def make_payment_entry(docname):
return doc.create_payment_entry(submit=False).as_dict()
def update_payment_req_status(doc, method):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
def update_payment_requests_as_per_pe_references(references=None, cancel=False):
"""
Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`.
"""
if not references:
return
for ref in doc.references:
payment_request_name = frappe.db.get_value(
"Payment Request",
{
"reference_doctype": ref.reference_doctype,
"reference_name": ref.reference_name,
"docstatus": 1,
},
precision = references[0].precision("allocated_amount")
referenced_payment_requests = frappe.get_all(
"Payment Request",
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
fields=[
"name",
"grand_total",
"outstanding_amount",
"payment_request_type",
],
)
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
for ref in references:
if not ref.payment_request:
continue
payment_request = referenced_payment_requests[ref.payment_request]
pr_outstanding = payment_request["outstanding_amount"]
# update outstanding amount
new_outstanding_amount = flt(
pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount,
precision,
)
if payment_request_name:
ref_details = get_reference_details(
ref.reference_doctype,
ref.reference_name,
doc.party_account_currency,
doc.party_type,
doc.party,
# to handle same payment request for the multiple allocations
payment_request["outstanding_amount"] = new_outstanding_amount
if not cancel and new_outstanding_amount < 0:
frappe.throw(
msg=_(
"The allocated amount is greater than the outstanding amount of Payment Request {0}"
).format(ref.payment_request),
title=_("Invalid Allocated Amount"),
)
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
status = pay_req_doc.status
if status != "Paid" and not ref_details.outstanding_amount:
status = "Paid"
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
status = "Partially Paid"
elif ref_details.outstanding_amount == ref_details.total_amount:
if pay_req_doc.payment_request_type == "Outward":
status = "Initiated"
elif pay_req_doc.payment_request_type == "Inward":
status = "Requested"
# update status
if new_outstanding_amount == payment_request["grand_total"]:
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
elif new_outstanding_amount == 0:
status = "Paid"
elif new_outstanding_amount > 0:
status = "Partially Paid"
pay_req_doc.db_set("status", status)
# update database
frappe.db.set_value(
"Payment Request",
ref.payment_request,
{"outstanding_amount": new_outstanding_amount, "status": status},
)
def get_dummy_message(doc):
@@ -745,3 +869,35 @@ def validate_payment(doc, method=None):
doc.reference_docname
)
)
@frappe.whitelist()
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
# permission checks in `get_list()`
reference_doctype = filters.get("reference_doctype")
reference_name = filters.get("reference_doctype")
if not reference_doctype or not reference_name:
return []
open_payment_requests = frappe.get_list(
"Payment Request",
filters={
"reference_doctype": filters["reference_doctype"],
"reference_name": filters["reference_name"],
"status": ["!=", "Paid"],
"outstanding_amount": ["!=", 0], # for compatibility with old data
"docstatus": 1,
},
fields=["name", "grand_total", "outstanding_amount"],
order_by="transaction_date ASC,creation ASC",
)
return [
(
pr.name,
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
)
for pr in open_payment_requests
]

View File

@@ -1,11 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import re
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
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
@@ -15,6 +17,7 @@ from erpnext.setup.utils import get_exchange_rate
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
payment_method = [
@@ -278,3 +281,246 @@ class TestPaymentRequest(FrappeTestCase):
self.assertEqual(pe.paid_amount, 800)
self.assertEqual(pe.base_received_amount, 800)
self.assertEqual(pe.received_amount, 10)
def test_multiple_payment_if_partially_paid_for_same_currency(self):
so = make_sales_order(currency="INR", qty=1, rate=1000)
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
self.assertEqual(pr.grand_total, 1000)
self.assertEqual(pr.outstanding_amount, pr.grand_total)
self.assertEqual(pr.party_account_currency, pr.currency) # INR
so.load_from_db()
# to make partial payment
pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 200
pe.references[0].allocated_amount = 200
pe.submit()
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
pr.load_from_db()
self.assertEqual(pr.status, "Partially Paid")
self.assertEqual(pr.outstanding_amount, 800)
self.assertEqual(pr.grand_total, 1000)
# complete payment
pe = pr.create_payment_entry()
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
self.assertEqual(pe.references[0].allocated_amount, 800)
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 1000)
# creating a more payment Request must not allowed
self.assertRaisesRegex(
frappe.exceptions.ValidationError,
re.compile(r"Payment Request is already created"),
make_payment_request,
dt="Sales Order",
dn=so.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
def test_multiple_payment_if_partially_paid_for_multi_currency(self):
pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1)
pi.credit_to = "Creditors - _TC"
pi.submit()
pr = make_payment_request(
dt="Purchase Invoice",
dn=pi.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
# 100 USD -> 5000 INR
self.assertEqual(pr.grand_total, 100)
self.assertEqual(pr.outstanding_amount, 5000)
self.assertEqual(pr.currency, "USD")
self.assertEqual(pr.party_account_currency, "INR")
self.assertEqual(pr.status, "Initiated")
# to make partial payment
pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 2000
pe.references[0].allocated_amount = 2000
pe.submit()
self.assertEqual(pe.references[0].payment_request, pr.name)
pr.load_from_db()
self.assertEqual(pr.status, "Partially Paid")
self.assertEqual(pr.outstanding_amount, 3000)
self.assertEqual(pr.grand_total, 100)
# complete payment
pe = pr.create_payment_entry()
self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount
self.assertEqual(pe.references[0].allocated_amount, 3000)
self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero
self.assertEqual(pe.references[0].payment_request, pr.name)
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 100)
# creating a more payment Request must not allowed
self.assertRaisesRegex(
frappe.exceptions.ValidationError,
re.compile(r"Payment Request is already created"),
make_payment_request,
dt="Purchase Invoice",
dn=pi.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
def test_single_payment_with_payment_term_for_same_currency(self):
create_payment_terms_template()
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000)
po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
po.save()
po.submit()
pr = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
self.assertEqual(pr.grand_total, 20000)
self.assertEqual(pr.outstanding_amount, pr.grand_total)
self.assertEqual(pr.party_account_currency, pr.currency) # INR
self.assertEqual(pr.status, "Initiated")
po.load_from_db()
pe = pr.create_payment_entry()
self.assertEqual(len(pe.references), 2)
self.assertEqual(pe.paid_amount, 20000)
# check 1st payment term
self.assertEqual(pe.references[0].allocated_amount, 16949.2)
self.assertEqual(pe.references[0].payment_request, pr.name)
# check 2nd payment term
self.assertEqual(pe.references[1].allocated_amount, 3050.8)
self.assertEqual(pe.references[1].payment_request, pr.name)
po.load_from_db()
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 20000)
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
def test_single_payment_with_payment_term_for_multi_currency(self):
create_payment_terms_template()
si = create_sales_invoice(
do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50
)
si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
si.save()
si.submit()
pr = make_payment_request(
dt="Sales Invoice",
dn=si.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
# 200 USD -> 10000 INR
self.assertEqual(pr.grand_total, 200)
self.assertEqual(pr.outstanding_amount, 10000)
self.assertEqual(pr.currency, "USD")
self.assertEqual(pr.party_account_currency, "INR")
pe = pr.create_payment_entry()
self.assertEqual(len(pe.references), 2)
self.assertEqual(pe.paid_amount, 10000)
# check 1st payment term
# convert it via dollar and conversion_rate
self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion
self.assertEqual(pe.references[0].payment_request, pr.name)
# check 2nd payment term
self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion
self.assertEqual(pe.references[1].payment_request, pr.name)
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 200)
def test_payment_cancel_process(self):
so = make_sales_order(currency="INR", qty=1, rate=1000)
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
self.assertEqual(pr.grand_total, 1000)
self.assertEqual(pr.outstanding_amount, pr.grand_total)
so.load_from_db()
pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 800
pe.references[0].allocated_amount = 800
pe.submit()
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
pr.load_from_db()
self.assertEqual(pr.status, "Partially Paid")
self.assertEqual(pr.outstanding_amount, 200)
self.assertEqual(pr.grand_total, 1000)
# cancelling PE
pe.cancel()
pr.load_from_db()
self.assertEqual(pr.status, "Requested")
self.assertEqual(pr.outstanding_amount, 1000)
self.assertEqual(pr.grand_total, 1000)
so.load_from_db()

View File

@@ -60,7 +60,7 @@ class PeriodClosingVoucher(AccountsController):
)
if gle_count > 5000:
frappe.enqueue(
make_reverse_gl_entries,
process_cancellation,
voucher_type="Period Closing Voucher",
voucher_no=self.name,
queue="long",
@@ -71,9 +71,7 @@ class PeriodClosingVoucher(AccountsController):
alert=True,
)
else:
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
self.delete_closing_entries()
process_cancellation(voucher_type="Period Closing Voucher", voucher_no=self.name)
def validate_future_closing_vouchers(self):
if frappe.db.exists(
@@ -86,12 +84,6 @@ class PeriodClosingVoucher(AccountsController):
)
)
def delete_closing_entries(self):
closing_balance = frappe.qb.DocType("Account Closing Balance")
frappe.qb.from_(closing_balance).delete().where(
closing_balance.period_closing_voucher == self.name
).run()
def validate_account_head(self):
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
@@ -166,14 +158,7 @@ class PeriodClosingVoucher(AccountsController):
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
if len(gl_entries + closing_entries) > 3000:
frappe.enqueue(
process_gl_entries,
gl_entries=gl_entries,
voucher_name=self.name,
timeout=3000,
)
frappe.enqueue(
process_closing_entries,
process_gl_and_closing_entries,
gl_entries=gl_entries,
closing_entries=closing_entries,
voucher_name=self.name,
@@ -187,8 +172,9 @@ class PeriodClosingVoucher(AccountsController):
alert=True,
)
else:
process_gl_entries(gl_entries, self.name)
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
process_gl_and_closing_entries(
gl_entries, closing_entries, self.name, self.company, self.posting_date
)
def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = []
@@ -353,9 +339,10 @@ class PeriodClosingVoucher(AccountsController):
if get_opening_entries:
query = query.where(
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
| gl_entry.is_opening
== "Yes"
( # noqa: UP034
(gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date))
| (gl_entry.is_opening == "Yes")
)
)
else:
query = query.where(
@@ -373,12 +360,16 @@ class PeriodClosingVoucher(AccountsController):
return query.run(as_dict=1)
def process_gl_entries(gl_entries, voucher_name):
def process_gl_and_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
from erpnext.accounts.general_ledger import make_gl_entries
try:
if gl_entries:
make_gl_entries(gl_entries, merge_entries=False)
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
@@ -386,26 +377,21 @@ def process_gl_entries(gl_entries, voucher_name):
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
try:
if gl_entries + closing_entries:
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
def make_reverse_gl_entries(voucher_type, voucher_no):
def process_cancellation(voucher_type, voucher_no):
from erpnext.accounts.general_ledger import make_reverse_gl_entries
try:
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
delete_closing_entries(voucher_no)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
def delete_closing_entries(voucher_no):
closing_balance = frappe.qb.DocType("Account Closing Balance")
frappe.qb.from_(closing_balance).delete().where(
closing_balance.period_closing_voucher == voucher_no
).run()

View File

@@ -862,6 +862,7 @@ def get_item_group(pos_profile):
if pos_profile.get("item_groups"):
# Get items based on the item groups defined in the POS profile
for row in pos_profile.get("item_groups"):
item_groups.append(row.item_group)
item_groups.extend(get_descendants_of("Item Group", row.item_group))
return list(set(item_groups))

View File

@@ -1131,6 +1131,12 @@ class TestPricingRule(FrappeTestCase):
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 3)
so = make_sales_order(item_code="_Test Item", qty=5, do_not_submit=1)
so.items[0].qty = 1
del so.items[-1]
so.save()
self.assertEqual(len(so.items), 1)
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")

View File

@@ -657,6 +657,9 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if pricing_rule.round_free_qty:
qty = math.floor(qty)
if not qty:
return
free_item_data_args = {
"item_code": free_item,
"qty": qty,

View File

@@ -5,6 +5,8 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import IfNull
pricing_rule_fields = [
"apply_on",
@@ -162,22 +164,50 @@ class PromotionalScheme(Document):
if self.is_new():
return
transaction_exists = False
docnames = []
invalid_pricing_rule = self.get_invalid_pricing_rules()
# If user has changed applicable for
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
if not invalid_pricing_rule:
return
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
if frappe.db.exists(
"Pricing Rule Detail",
{
"pricing_rule": ["in", invalid_pricing_rule],
"docstatus": ["<", 2],
},
):
raise_for_transaction_exists(self.name)
for docname in docnames:
if frappe.db.exists("Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)}):
raise_for_transaction_exists(self.name)
for doc in invalid_pricing_rule:
frappe.delete_doc("Pricing Rule", doc)
if docnames and not transaction_exists:
for docname in docnames:
frappe.delete_doc("Pricing Rule", docname.name)
frappe.msgprint(
_("The following invalid Pricing Rules are deleted:")
+ "<br><br><ul><li>"
+ "</li><li>".join(invalid_pricing_rule)
+ "</li></ul>"
)
def get_invalid_pricing_rules(self):
pr = frappe.qb.DocType("Pricing Rule")
conditions = []
conditions.append(pr.promotional_scheme == self.name)
if self.applicable_for:
applicable_for = frappe.scrub(self.applicable_for)
applicable_for_list = [d.get(applicable_for) for d in self.get(applicable_for)]
conditions.append(
(IfNull(pr.applicable_for, "") != self.applicable_for)
| (
(IfNull(pr.applicable_for, "") == self.applicable_for)
& IfNull(pr[applicable_for], "").notin(applicable_for_list)
)
)
else:
conditions.append(IfNull(pr.applicable_for, "") != "")
return frappe.qb.from_(pr).select(pr.name).where(Criterion.all(conditions)).run(pluck=True)
def on_update(self):
self.validate()

View File

@@ -90,6 +90,31 @@ class TestPromotionalScheme(unittest.TestCase):
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
self.assertEqual(price_rules, [])
def test_change_applicable_for_values_in_promotional_scheme(self):
ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer")
ps.append("customer", {"customer": "_Test Customer 2"})
ps.save()
price_rules = frappe.get_all(
"Pricing Rule", filters={"promotional_scheme": ps.name, "applicable_for": "Customer"}
)
self.assertTrue(len(price_rules), 2)
ps.set("customer", [])
ps.append("customer", {"customer": "_Test Customer 2"})
ps.save()
price_rules = frappe.get_all(
"Pricing Rule",
filters={
"promotional_scheme": ps.name,
"applicable_for": "Customer",
"customer": "_Test Customer",
},
)
self.assertEqual(price_rules, [])
frappe.delete_doc("Promotional Scheme", ps.name)
def test_min_max_amount_configuration(self):
ps = make_promotional_scheme()
ps.price_discount_slabs[0].min_amount = 10

View File

@@ -561,11 +561,12 @@ frappe.ui.form.on("Purchase Invoice", {
frm.custom_make_buttons = {
"Purchase Invoice": "Return / Debit Note",
"Payment Entry": "Payment",
"Landed Cost Voucher": function () {
frm.trigger("create_landed_cost_voucher");
},
};
if (frm.doc.update_stock) {
frm.custom_make_buttons["Landed Cost Voucher"] = "Landed Cost Voucher";
}
frm.set_query("additional_discount_account", function () {
return {
filters: {
@@ -607,20 +608,6 @@ frappe.ui.form.on("Purchase Invoice", {
});
},
create_landed_cost_voucher: function (frm) {
let lcv = frappe.model.get_new_doc("Landed Cost Voucher");
lcv.company = frm.doc.company;
let lcv_receipt = frappe.model.get_new_doc("Landed Cost Purchase Invoice");
lcv_receipt.receipt_document_type = "Purchase Invoice";
lcv_receipt.receipt_document = frm.doc.name;
lcv_receipt.supplier = frm.doc.supplier;
lcv_receipt.grand_total = frm.doc.grand_total;
lcv.purchase_receipts = [lcv_receipt];
frappe.set_route("Form", lcv.doctype, lcv.name);
},
add_custom_buttons: function (frm) {
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
frm.add_custom_button(
@@ -645,6 +632,32 @@ frappe.ui.form.on("Purchase Invoice", {
__("View")
);
}
if (frm.doc.docstatus === 1 && frm.doc.update_stock) {
frm.add_custom_button(
__("Landed Cost Voucher"),
() => {
frm.events.make_lcv(frm);
},
__("Create")
);
}
},
make_lcv(frm) {
frappe.call({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_lcv",
args: {
doctype: frm.doc.doctype,
docname: frm.doc.name,
},
callback: (r) => {
if (r.message) {
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
},
});
},
onload: function (frm) {

View File

@@ -2292,6 +2292,24 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
def test_last_purchase_rate(self):
item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1)
pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100)
item.reload()
self.assertEqual(item.last_purchase_rate, 100)
pi2 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=200)
item.reload()
self.assertEqual(item.last_purchase_rate, 200)
pi2.cancel()
item.reload()
self.assertEqual(item.last_purchase_rate, 100)
pi1.cancel()
item.reload()
self.assertEqual(item.last_purchase_rate, 0)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -2123,7 +2123,7 @@ def make_delivery_note(source_name, target_doc=None):
"postprocess": update_item,
"condition": lambda doc: doc.delivered_by_supplier != 1,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {
"doctype": "Sales Team",
"field_map": {"incentives": "incentives"},

View File

@@ -327,7 +327,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances" value which cross the threshold
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
if cint(tax_details.round_off_tax_amount):
@@ -414,6 +414,9 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa
Use Payment Ledger to fetch unallocated Advance Payments
"""
if party_type == "Supplier":
return []
ple = qb.DocType("Payment Ledger Entry")
conditions = []
@@ -607,8 +610,6 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
conditions.append(ple.voucher_no == ple.against_voucher_no)
conditions.append(ple.company == inv.company)
(qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1))
advance_amt = (
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
)
@@ -631,9 +632,12 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
advance_adjusted = get_advance_adjusted_in_invoice(inv)
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
total_invoiced_amt = (
current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - advance_adjusted
)
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
chargeable_amt = total_invoiced_amt - cumulative_threshold
@@ -642,6 +646,14 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
return tcs_amount
def get_advance_adjusted_in_invoice(inv):
advances_adjusted = 0
for row in inv.get("advances", []):
advances_adjusted += row.allocated_amount
return advances_adjusted
def get_invoice_total_without_tcs(inv, tax_details):
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0

View File

@@ -210,6 +210,46 @@ class TestTaxWithholdingCategory(FrappeTestCase):
d.reload()
d.cancel()
def test_tcs_on_allocated_advance_payments(self):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
vouchers = []
# create advance payment
pe = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=30000
)
pe.paid_from = "Debtors - _TC"
pe.paid_to = "Cash - _TC"
pe.submit()
vouchers.append(pe)
si = create_sales_invoice(customer="Test TCS Customer", rate=50000)
advances = si.get_advance_entries()
si.append(
"advances",
{
"reference_type": advances[0].reference_type,
"reference_name": advances[0].reference_name,
"advance_amount": advances[0].amount,
"allocated_amount": 30000,
},
)
si.submit()
vouchers.append(si)
# assert tax collection on total invoice ,advance payment adjusted should be excluded.
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"])
# tcs = (inv amt)50000+(adv amt)30000-(adv adj) 30000 - threshold(30000) * rate 10%
self.assertEqual(tcs_charged, 2000)
# cancel invoice and payments to avoid clashing
for d in reversed(vouchers):
d.reload()
d.cancel()
def test_tds_calculation_on_net_total(self):
frappe.db.set_value(
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"

View File

@@ -7,7 +7,9 @@ from frappe.utils import today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@@ -363,3 +365,100 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
self.assertEqual(so.advance_paid, 0)
self.assertEqual(len(pe.references), 0)
self.assertEqual(pe.unallocated_amount, 100)
def test_06_unreconcile_advance_from_payment_entry(self):
self.enable_advance_as_liability()
so1 = self.create_sales_order()
so2 = self.create_sales_order()
pe = self.create_payment_entry()
# Allocation payment against Sales Order
pe.paid_amount = 260
pe.append(
"references",
{"reference_doctype": so1.doctype, "reference_name": so1.name, "allocated_amount": 150},
)
pe.append(
"references",
{"reference_doctype": so2.doctype, "reference_name": so2.name, "allocated_amount": 110},
)
pe.save().submit()
# Assert 'Advance Paid'
so1.reload()
self.assertEqual(so1.advance_paid, 150)
so2.reload()
self.assertEqual(so2.advance_paid, 110)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 2)
allocations = [(x.reference_name, x.allocated_amount) for x in unreconcile.allocations]
self.assertListEqual(allocations, [(so1.name, 150), (so2.name, 110)])
# unreconcile so2
unreconcile.remove(unreconcile.allocations[0])
unreconcile.save().submit()
# Assert 'Advance Paid'
so1.reload()
so2.reload()
pe.reload()
self.assertEqual(so1.advance_paid, 150)
self.assertEqual(so2.advance_paid, 0)
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.unallocated_amount, 110)
self.disable_advance_as_liability()
def test_07_adv_from_so_to_invoice(self):
self.enable_advance_as_liability()
so = self.create_sales_order()
pe = self.create_payment_entry()
pe.paid_amount = 1000
pe.append(
"references",
{"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 1000},
)
pe.save().submit()
# Assert 'Advance Paid'
so.reload()
self.assertEqual(so.advance_paid, 1000)
si = make_sales_invoice(so.name)
si.insert().submit()
pr = frappe.get_doc(
{
"doctype": "Payment Reconciliation",
"company": self.company,
"party_type": "Customer",
"party": so.customer,
}
)
accounts = get_party_account("Customer", so.customer, so.company, True)
pr.receivable_payable_account = accounts[0]
pr.default_advance_account = accounts[1]
pr.get_unreconciled_entries()
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 1)
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
# Assert 'Advance Paid'
so.reload()
self.assertEqual(so.advance_paid, 0)
self.disable_advance_as_liability()

View File

@@ -1,7 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:UNREC-{#####}",
"creation": "2023-08-22 10:26:34.421423",
"default_view": "List",
"doctype": "DocType",
@@ -58,11 +56,10 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-08-28 17:42:50.261377",
"modified": "2024-10-10 12:03:50.022444",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Unreconcile Payment",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{

View File

@@ -36,8 +36,9 @@ def get_group_by_asset_category_data(filters):
+ flt(row.cost_of_new_purchase)
- flt(row.cost_of_sold_asset)
- flt(row.cost_of_scrapped_asset)
- flt(row.cost_of_capitalized_asset)
)
# Update row with corresponding asset data
row.update(
next(
asset
@@ -111,13 +112,24 @@ def get_asset_categories_for_grouped_by_category(filters):
end
else
0
end), 0) as cost_of_scrapped_asset
end), 0) as cost_of_scrapped_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Capitalized" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_capitalized_asset
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
and not exists(
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
where acai.asset = a.name
and ac.posting_date <= %(to_date)s
and ac.posting_date < %(from_date)s
and ac.docstatus=1
)
group by a.asset_category
@@ -179,13 +191,24 @@ def get_asset_details_for_grouped_by_category(filters):
end
else
0
end), 0) as cost_of_scrapped_asset
end), 0) as cost_of_scrapped_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Capitalized" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_capitalized_asset
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
and not exists(
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
where acai.asset = a.name
and ac.posting_date <= %(to_date)s
and ac.posting_date < %(from_date)s
and ac.docstatus=1
)
group by a.name
@@ -217,6 +240,7 @@ def get_group_by_asset_data(filters):
+ flt(row.cost_of_new_purchase)
- flt(row.cost_of_sold_asset)
- flt(row.cost_of_scrapped_asset)
- flt(row.cost_of_capitalized_asset)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
@@ -445,6 +469,12 @@ def get_columns(filters):
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Cost of New Capitalized Asset"),
"fieldname": "cost_of_capitalized_asset",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Cost as on") + " " + formatdate(filters.to_date),
"fieldname": "cost_as_on_to_date",

View File

@@ -47,7 +47,7 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
},
],
formatter: function (value, row, column, data, default_formatter, filter) {
if (column.fieldname == "payment_entry" && value == "Cheques and Deposits incorrectly cleared") {
if (column.fieldname == "payment_entry" && value == __("Cheques and Deposits incorrectly cleared")) {
column.link_onclick =
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
}

View File

@@ -469,10 +469,13 @@ def update_parent_account_names(accounts):
for d in accounts:
if d.account_number:
account_name = d.account_number + " - " + d.account_name
account_key = d.account_number + " - " + d.account_name
else:
account_name = d.account_name
name_to_account_map[d.name] = account_name
account_key = d.account_name
d.account_key = account_key
name_to_account_map[d.name] = account_key
for account in accounts:
if account.parent_account:
@@ -505,33 +508,26 @@ def get_subsidiary_companies(company):
def get_accounts(root_type, companies):
accounts = []
added_accounts = []
for company in companies:
for account in frappe.get_all(
"Account",
fields=[
"name",
"is_group",
"company",
"parent_account",
"lft",
"rgt",
"root_type",
"report_type",
"account_name",
"account_number",
],
filters={"company": company, "root_type": root_type},
):
if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
if account_key not in added_accounts:
accounts.append(account)
added_accounts.append(account_key)
accounts.extend(
frappe.get_all(
"Account",
fields=[
"name",
"is_group",
"company",
"parent_account",
"lft",
"rgt",
"root_type",
"report_type",
"account_name",
"account_number",
],
filters={"company": company, "root_type": root_type},
)
)
return accounts
@@ -770,15 +766,17 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
def filter_accounts(accounts, depth=10):
parent_children_map = {}
accounts_by_name = {}
for d in accounts:
if d.account_number:
account_name = d.account_number + " - " + d.account_name
else:
account_name = d.account_name
d["company_wise_opening_bal"] = defaultdict(float)
accounts_by_name[account_name] = d
added_accounts = []
parent_children_map.setdefault(d.parent_account or None, []).append(d)
for d in accounts:
if d.account_key in added_accounts:
continue
added_accounts.append(d.account_key)
d["company_wise_opening_bal"] = defaultdict(float)
accounts_by_name[d.account_key] = d
parent_children_map.setdefault(d.parent_account_name or None, []).append(d)
filtered_accounts = []
@@ -790,7 +788,7 @@ def filter_accounts(accounts, depth=10):
for child in children:
child.indent = level
filtered_accounts.append(child)
add_to_list(child.name, level + 1)
add_to_list(child.account_key, level + 1)
add_to_list(None, 0)

View File

@@ -311,6 +311,7 @@ def get_account_columns(invoice_list, include_payments):
"""select distinct expense_account
from `tabPurchase Invoice Item` where docstatus = 1
and (expense_account is not null and expense_account != '')
and parenttype='Purchase Invoice'
and parent in (%s) order by expense_account"""
% ", ".join(["%s"] * len(invoice_list)),
tuple([inv.name for inv in invoice_list]),
@@ -451,7 +452,7 @@ def get_invoice_expense_map(invoice_list):
"""
select parent, expense_account, sum(base_net_amount) as amount
from `tabPurchase Invoice Item`
where parent in (%s)
where parent in (%s) and parenttype='Purchase Invoice'
group by parent, expense_account
"""
% ", ".join(["%s"] * len(invoice_list)),
@@ -522,7 +523,7 @@ def get_invoice_po_pr_map(invoice_list):
"""
select parent, purchase_order, purchase_receipt, po_detail, project
from `tabPurchase Invoice Item`
where parent in (%s)
where parent in (%s) and parenttype='Purchase Invoice'
"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),

View File

@@ -326,6 +326,7 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment
if join_required:
query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent)
query = query.where(child_doc.parenttype == doctype)
query = query.distinct()
if parent_doc.get_table_name() != "tabJournal Entry":

View File

@@ -87,6 +87,22 @@ class AccountsTestMixin:
"parent_account": "Bank Accounts - " + abbr,
}
),
frappe._dict(
{
"attribute_name": "advance_received",
"account_name": "Advance Received",
"parent_account": "Current Liabilities - " + abbr,
"account_type": "Receivable",
}
),
frappe._dict(
{
"attribute_name": "advance_paid",
"account_name": "Advance Paid",
"parent_account": "Current Assets - " + abbr,
"account_type": "Payable",
}
),
]
for acc in other_accounts:
acc_name = acc.account_name + " - " + abbr
@@ -101,9 +117,31 @@ class AccountsTestMixin:
"company": self.company,
}
)
new_acc.account_type = acc.get("account_type", None)
new_acc.save()
setattr(self, acc.attribute_name, new_acc.name)
self.identify_default_warehouses()
def enable_advance_as_liability(self):
company = frappe.get_doc("Company", self.company)
company.book_advance_payments_in_separate_party_account = True
company.default_advance_received_account = self.advance_received
company.default_advance_paid_account = self.advance_paid
company.save()
def disable_advance_as_liability(self):
company = frappe.get_doc("Company", self.company)
company.book_advance_payments_in_separate_party_account = False
company.default_advance_paid_account = company.default_advance_received_account = None
company.save()
def identify_default_warehouses(self):
for w in frappe.db.get_all(
"Warehouse", filters={"company": self.company}, fields=["name", "warehouse_name"]
):
setattr(self, "warehouse_" + w.warehouse_name.lower().strip().replace(" ", "_"), w.name)
def create_usd_receivable_account(self):
account_name = "Debtors USD"
if not frappe.db.get_value(

View File

@@ -474,10 +474,14 @@ def reconcile_against_document(
doc = frappe.get_doc(voucher_type, voucher_no)
frappe.flags.ignore_party_validation = True
# For payments with `Advance` in separate account feature enabled, only new ledger entries are posted for each reference.
# No need to cancel/delete payment ledger entries
# When Advance is allocated from an Order to an Invoice
# whole ledger must be reposted
repost_whole_ledger = any([x.voucher_detail_no for x in entries])
if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account:
doc.make_advance_gl_entries(cancel=1)
if repost_whole_ledger:
doc.make_gl_entries(cancel=1)
else:
doc.make_advance_gl_entries(cancel=1)
else:
_delete_pl_entries(voucher_type, voucher_no)
@@ -511,9 +515,14 @@ def reconcile_against_document(
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account:
# both ledgers must be posted to for `Advance` in separate account feature
# TODO: find a more efficient way post only for the new linked vouchers
doc.make_advance_gl_entries()
# When Advance is allocated from an Order to an Invoice
# whole ledger must be reposted
if repost_whole_ledger:
doc.make_gl_entries()
else:
# both ledgers must be posted to for `Advance` in separate account feature
# TODO: find a more efficient way post only for the new linked vouchers
doc.make_advance_gl_entries()
else:
gl_map = doc.build_gl_map()
# Make sure there is no overallocation

View File

@@ -738,7 +738,7 @@ def make_purchase_receipt(source_name, target_doc=None):
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
and doc.delivered_by_supplier != 1,
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
},
target_doc,
set_missing_values,
@@ -819,7 +819,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
"postprocess": update_item,
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
}
doc = get_mapped_doc(
@@ -889,6 +889,20 @@ def make_subcontracting_order(source_name, target_doc=None, save=False, submit=F
def get_mapped_subcontracting_order(source_name, target_doc=None):
def post_process(source_doc, target_doc):
target_doc.populate_items_table()
if target_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = target_doc.set_warehouse
else:
if source_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = source_doc.set_warehouse
else:
for idx, item in enumerate(target_doc.items):
item.warehouse = source_doc.items[idx].warehouse
if target_doc and isinstance(target_doc, str):
target_doc = json.loads(target_doc)
for key in ["service_items", "items", "supplied_items"]:
@@ -919,22 +933,9 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
},
},
target_doc,
post_process,
)
target_doc.populate_items_table()
source_doc = frappe.get_doc("Purchase Order", source_name)
if target_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = target_doc.set_warehouse
else:
if source_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = source_doc.set_warehouse
else:
for idx, item in enumerate(target_doc.items):
item.warehouse = source_doc.items[idx].warehouse
return target_doc

View File

@@ -390,6 +390,7 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=
"Request for Quotation": {
"doctype": "Supplier Quotation",
"validation": {"docstatus": ["=", 1]},
"field_map": {"opportunity": "opportunity"},
},
"Request for Quotation Item": {
"doctype": "Supplier Quotation Item",
@@ -455,6 +456,7 @@ def create_rfq_items(sq_doc, supplier, data):
"material_request",
"material_request_item",
"stock_qty",
"uom",
]:
args[field] = data.get(field)

View File

@@ -2,3 +2,10 @@
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
frappe.query_reports["Purchase Order Trends"]["filters"].push({
fieldname: "include_closed_orders",
label: __("Include Closed Orders"),
fieldtype: "Check",
default: 0,
});

View File

@@ -1063,6 +1063,13 @@ class AccountsController(TransactionBase):
"Stock Entry": "stock_entry_type",
"Asset Capitalization": "entry_type",
}
for method_name in frappe.get_hooks("voucher_subtypes"):
voucher_subtype = frappe.get_attr(method_name)(self)
if voucher_subtype:
return voucher_subtype
if self.doctype in voucher_subtypes:
return self.get(voucher_subtypes[self.doctype])
elif self.doctype == "Purchase Receipt" and self.is_return:
@@ -1073,6 +1080,7 @@ class AccountsController(TransactionBase):
return "Credit Note"
elif (self.doctype == "Purchase Invoice" and self.is_return) or self.doctype == "Sales Invoice":
return "Debit Note"
return self.doctype
def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
@@ -1966,7 +1974,7 @@ class AccountsController(TransactionBase):
).format(formatted_advance_paid, self.name, formatted_order_total)
)
frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid)
self.db_set("advance_paid", advance_paid)
@property
def company_abbr(self):
@@ -3309,7 +3317,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
items_added_or_removed = False # updated to true if any new item is added or removed
any_conversion_factor_changed = False
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_doc_permissions(parent, "write")
@@ -3425,25 +3432,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
# if rate is greater than price_list_rate, set margin
# or set discount
child_item.discount_percentage = 0
if parent_doctype in sales_doctypes:
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
else:
child_item.discount_percentage = flt(
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage"),
)
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
if parent_doctype in sales_doctypes:
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag:

View File

@@ -702,9 +702,11 @@ class BuyingController(SubcontractingController):
if self.get("is_return"):
return
if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
"Buying Settings", "disable_last_purchase_rate"
):
if self.doctype in [
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
] and not frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
update_last_purchase_rate(self, is_submit=0)
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:

View File

@@ -473,6 +473,16 @@ class SellingController(StockController):
raise_error_if_no_rate=False,
)
if (
not d.incoming_rate
and self.get("return_against")
and self.get("is_return")
and get_valuation_method(d.item_code) == "Moving Average"
):
d.incoming_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
if self.doctype == "Delivery Note" or self.get("update_stock"):

View File

@@ -63,6 +63,33 @@ class StockController(AccountsController):
self.set_rate_of_stock_uom()
self.validate_internal_transfer()
self.validate_putaway_capacity()
self.reset_conversion_factor()
def reset_conversion_factor(self):
for row in self.get("items"):
if row.uom != row.stock_uom:
continue
if row.conversion_factor != 1.0:
row.conversion_factor = 1.0
frappe.msgprint(
_(
"Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}."
).format(bold(row.item_code), bold(row.uom), bold(row.stock_uom)),
alert=True,
)
def validate_items_exist(self):
if not self.get("items"):
return
items = [d.item_code for d in self.get("items")]
exists_items = frappe.get_all("Item", filters={"name": ("in", items)}, pluck="name")
non_exists_items = set(items) - set(exists_items)
if non_exists_items:
frappe.throw(_("Items {0} do not exist in the Item master.").format(", ".join(non_exists_items)))
def validate_duplicate_serial_and_batch_bundle(self, table_name):
if not self.get(table_name):
@@ -345,6 +372,9 @@ class StockController(AccountsController):
@frappe.request_cache
def is_serial_batch_item(self, item_code) -> bool:
if not frappe.db.exists("Item", item_code):
frappe.throw(_("Item {0} does not exist.").format(bold(item_code)))
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
if item_details.has_serial_no or item_details.has_batch_no:

View File

@@ -576,30 +576,56 @@ class SubcontractingController(StockController):
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
args = frappe._dict(
{
"item_code": rm_obj.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * flt(rm_obj.consumed_qty),
"actual_qty": -1 * flt(rm_obj.consumed_qty),
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": item_row.name,
"company": self.company,
"allow_zero_valuation": 1,
}
)
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
item_row, rm_obj, rm_obj.consumed_qty
)
if rm_obj.serial_and_batch_bundle:
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
self.set_rate_for_supplied_items(rm_obj, item_row)
rm_obj.rate = get_incoming_rate(args)
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":
return
for row in self.supplied_items:
item_row = None
if row.reference_name:
item_row = self.get_item_row(row.reference_name)
if not item_row:
continue
self.set_rate_for_supplied_items(row, item_row)
def get_item_row(self, reference_name):
for item in self.items:
if item.name == reference_name:
return item
def set_rate_for_supplied_items(self, rm_obj, item_row):
args = frappe._dict(
{
"item_code": rm_obj.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * flt(rm_obj.consumed_qty),
"actual_qty": -1 * flt(rm_obj.consumed_qty),
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": item_row.name,
"company": self.company,
"allow_zero_valuation": 1,
}
)
if rm_obj.serial_and_batch_bundle:
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
if rm_obj.use_serial_batch_fields:
args["batch_no"] = rm_obj.batch_no
args["serial_no"] = rm_obj.serial_no
rm_obj.rate = get_incoming_rate(args)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))

View File

@@ -40,7 +40,7 @@ class calculate_taxes_and_totals:
return items
def calculate(self):
if not len(self._items):
if not len(self.doc.items):
return
self.discount_amount_applied = False
@@ -95,7 +95,7 @@ class calculate_taxes_and_totals:
if self.doc.get("is_return") and self.doc.get("return_against"):
return
for item in self._items:
for item in self.doc.items:
if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
@@ -154,7 +154,7 @@ class calculate_taxes_and_totals:
return
if not self.discount_amount_applied:
for item in self._items:
for item in self.doc.items:
self.doc.round_floats_in(item)
if item.discount_percentage == 100:
@@ -258,7 +258,7 @@ class calculate_taxes_and_totals:
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return
for item in self._items:
for item in self.doc.items:
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
cumulated_tax_fraction = 0
total_inclusive_tax_amount_per_qty = 0

View File

@@ -69,13 +69,15 @@ def get_data(filters, conditions):
"Delivery Note",
]:
posting_date = "t1.posting_date"
if filters.period_based_on:
if filters.period_based_on and conditions.get("trans") in ["Sales Invoice", "Purchase Invoice"]:
posting_date = "t1." + filters.period_based_on
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL"
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
cond += " and t1.status != 'Closed'"
if not filters.get("include_closed_orders"):
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
cond += " and t1.status != 'Closed'"
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
cond += " and t1.quotation_to = 'Customer'"
@@ -222,7 +224,7 @@ def period_wise_columns_query(filters, trans):
if trans in ["Purchase Receipt", "Delivery Note", "Purchase Invoice", "Sales Invoice"]:
trans_date = "posting_date"
if filters.period_based_on:
if filters.period_based_on and trans in ["Purchase Invoice", "Sales Invoice"]:
trans_date = filters.period_based_on
else:
trans_date = "transaction_date"

View File

@@ -28,18 +28,18 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
erpnext.toggle_naming_series();
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
this.frm.add_custom_button(
__("Opportunity"),
function () {
me.frm.trigger("make_opportunity");
},
__("Create")
);
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
this.frm.add_custom_button(__("Customer"), this.make_customer.bind(this), __("Create"));
this.frm.add_custom_button(__("Opportunity"), this.make_opportunity.bind(this), __("Create"));
this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create"));
if (!doc.__onload.linked_prospects.length) {
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
this.frm.add_custom_button(__("Add to Prospect"), this.add_lead_to_prospect, __("Action"));
this.frm.add_custom_button(__("Prospect"), this.make_prospect.bind(this), __("Create"));
this.frm.add_custom_button(
__("Add to Prospect"),
() => {
this.add_lead_to_prospect(this.frm);
},
__("Action")
);
}
}
@@ -53,8 +53,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
this.show_activities();
}
add_lead_to_prospect() {
let me = this;
add_lead_to_prospect(frm) {
frappe.prompt(
[
{
@@ -69,12 +68,12 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
frappe.call({
method: "erpnext.crm.doctype.lead.lead.add_lead_to_prospect",
args: {
lead: me.frm.doc.name,
lead: frm.doc.name,
prospect: data.prospect,
},
callback: function (r) {
if (!r.exc) {
me.frm.reload_doc();
frm.reload_doc();
}
},
freeze: true,
@@ -89,32 +88,123 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
make_customer() {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_customer",
frm: cur_frm,
frm: this.frm,
});
}
make_quotation() {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_quotation",
frm: cur_frm,
frm: this.frm,
});
}
async make_opportunity() {
const frm = this.frm;
let existing_prospect = (
await frappe.db.get_value(
"Prospect Lead",
{
lead: frm.doc.name,
},
"name",
null,
"Prospect"
)
).message?.name;
let fields = [];
if (!existing_prospect) {
fields.push(
{
label: "Create Prospect",
fieldname: "create_prospect",
fieldtype: "Check",
default: 1,
},
{
label: "Prospect Name",
fieldname: "prospect_name",
fieldtype: "Data",
default: frm.doc.company_name,
reqd: 1,
depends_on: "create_prospect",
}
);
}
await frm.reload_doc();
let existing_contact = (
await frappe.db.get_value(
"Contact",
{
first_name: frm.doc.first_name || frm.doc.lead_name,
last_name: frm.doc.last_name,
},
"name"
)
).message?.name;
if (!existing_contact) {
fields.push({
label: "Create Contact",
fieldname: "create_contact",
fieldtype: "Check",
default: "1",
});
}
if (fields.length) {
const d = new frappe.ui.Dialog({
title: __("Create Opportunity"),
fields: fields,
primary_action: function (data) {
frappe.call({
method: "create_prospect_and_contact",
doc: frm.doc,
args: {
data: data,
},
freeze: true,
callback: function (r) {
if (!r.exc) {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
frm: frm,
});
}
d.hide();
},
});
},
primary_action_label: __("Create"),
});
d.show();
} else {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
frm: frm,
});
}
}
make_prospect() {
const me = this;
frappe.model.with_doctype("Prospect", function () {
let prospect = frappe.model.get_new_doc("Prospect");
prospect.company_name = cur_frm.doc.company_name;
prospect.no_of_employees = cur_frm.doc.no_of_employees;
prospect.industry = cur_frm.doc.industry;
prospect.market_segment = cur_frm.doc.market_segment;
prospect.territory = cur_frm.doc.territory;
prospect.fax = cur_frm.doc.fax;
prospect.website = cur_frm.doc.website;
prospect.prospect_owner = cur_frm.doc.lead_owner;
prospect.notes = cur_frm.doc.notes;
prospect.company_name = me.frm.doc.company_name;
prospect.no_of_employees = me.frm.doc.no_of_employees;
prospect.industry = me.frm.doc.industry;
prospect.market_segment = me.frm.doc.market_segment;
prospect.territory = me.frm.doc.territory;
prospect.fax = me.frm.doc.fax;
prospect.website = me.frm.doc.website;
prospect.prospect_owner = me.frm.doc.lead_owner;
prospect.notes = me.frm.doc.notes;
let leads_row = frappe.model.add_child(prospect, "leads");
leads_row.lead = cur_frm.doc.name;
leads_row.lead = me.frm.doc.name;
frappe.set_route("Form", "Prospect", prospect.name);
});
@@ -150,90 +240,3 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
};
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
frappe.ui.form.on("Lead", {
make_opportunity: async function (frm) {
let existing_prospect = (
await frappe.db.get_value(
"Prospect Lead",
{
lead: frm.doc.name,
},
"name",
null,
"Prospect"
)
).message.name;
if (!existing_prospect) {
var fields = [
{
label: "Create Prospect",
fieldname: "create_prospect",
fieldtype: "Check",
default: 1,
},
{
label: "Prospect Name",
fieldname: "prospect_name",
fieldtype: "Data",
default: frm.doc.company_name,
depends_on: "create_prospect",
},
];
}
let existing_contact = (
await frappe.db.get_value(
"Contact",
{
first_name: frm.doc.first_name || frm.doc.lead_name,
last_name: frm.doc.last_name,
},
"name"
)
).message.name;
if (!existing_contact) {
fields.push({
label: "Create Contact",
fieldname: "create_contact",
fieldtype: "Check",
default: "1",
});
}
if (fields) {
var d = new frappe.ui.Dialog({
title: __("Create Opportunity"),
fields: fields,
primary_action: function () {
var data = d.get_values();
frappe.call({
method: "create_prospect_and_contact",
doc: frm.doc,
args: {
data: data,
},
freeze: true,
callback: function (r) {
if (!r.exc) {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
frm: frm,
});
}
d.hide();
},
});
},
primary_action_label: __("Create"),
});
d.show();
} else {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
frm: frm,
});
}
},
});

View File

@@ -365,7 +365,6 @@ doc_events = {
"Payment Entry": {
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
],
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],

View File

@@ -669,7 +669,7 @@ class JobCard(Document):
self.set_transferred_qty()
def validate_transfer_qty(self):
if self.items and self.transferred_qty < self.for_quantity:
if not self.is_corrective_job_card and self.items and self.transferred_qty < self.for_quantity:
frappe.throw(
_(
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"

View File

@@ -277,7 +277,7 @@ frappe.ui.form.on("Production Plan", {
frm.clear_table("prod_plan_references");
frappe.call({
method: "get_items",
method: "combine_so_items",
freeze: true,
doc: frm.doc,
callback: function () {

View File

@@ -269,6 +269,31 @@ class ProductionPlan(Document):
{"material_request": data.name, "material_request_date": data.transaction_date},
)
@frappe.whitelist()
def combine_so_items(self):
if self.combine_items and self.po_items and len(self.po_items) > 0:
items = []
for row in self.po_items:
items.append(
frappe._dict(
{
"parent": row.sales_order,
"item_code": row.item_code,
"warehouse": row.warehouse,
"qty": row.pending_qty,
"pending_qty": row.pending_qty,
"conversion_factor": 1.0,
"description": row.description,
"bom_no": row.bom_no,
}
)
)
self.set("po_items", [])
self.add_items(items)
else:
self.get_items()
@frappe.whitelist()
def get_items(self):
self.set("po_items", [])
@@ -435,24 +460,28 @@ class ProductionPlan(Document):
item_details = get_item_details(data.item_code, throw=False)
if self.combine_items:
if item_details.bom_no in refs:
refs[item_details.bom_no]["so_details"].append(
bom_no = item_details.bom_no
if data.get("bom_no"):
bom_no = data.get("bom_no")
if bom_no in refs:
refs[bom_no]["so_details"].append(
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
)
refs[item_details.bom_no]["qty"] += data.pending_qty
refs[bom_no]["qty"] += data.pending_qty
continue
else:
refs[item_details.bom_no] = {
refs[bom_no] = {
"qty": data.pending_qty,
"po_item_ref": data.name,
"so_details": [],
}
refs[item_details.bom_no]["so_details"].append(
refs[bom_no]["so_details"].append(
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
)
bom_no = data.bom_no or item_details and item_details.bom_no or ""
bom_no = data.bom_no or item_details and item_details.get("bom_no") or ""
if not bom_no:
continue

View File

@@ -1531,6 +1531,197 @@ class TestWorkOrder(FrappeTestCase):
self.assertFalse(serial_nos)
def test_backflushed_batch_raw_materials_based_on_transferred_autosabb(self):
frappe.db.set_single_value(
"Manufacturing Settings",
"backflush_raw_materials_based_on",
"Material Transferred for Manufacture",
)
batch_item = "Test Batch MCC Keyboard"
fg_item = "Test FG Item with Batch Raw Materials"
ste_doc = test_stock_entry.make_stock_entry(
item_code=batch_item, target="Stores - _TC", qty=8, basic_rate=100, do_not_save=True
)
# Inward raw materials in Stores warehouse
ste_doc.submit()
ste_doc.reload()
batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle)
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
# action taken upon Start button:
transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
)
transferred_ste_doc.submit()
transferred_ste_doc.reload()
self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
get_batch_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle), batch_no
)
self.assertEqual(transferred_ste_doc.items[0].qty, 4.0)
# Make additional consumption and link to WO
test_stock_entry.make_stock_entry(
item_code="Test Batch Battery Consumable",
target="Stores - _TC",
qty=8,
basic_rate=2.33,
)
consume_use_doc = test_stock_entry.make_stock_entry(
item_code="Test Batch Battery Consumable", # consumable not linked to BOM
source="Stores - _TC",
qty=4,
purpose="Material Consumption for Manufacture",
do_not_save=True,
)
consume_use_doc.work_order = wo_doc.name
consume_use_doc.fg_completed_qty = 4
consume_use_doc.submit()
consume_use_doc.reload()
manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4))
mfr_items = [i.as_dict() for i in manufacture_ste_doc.items]
manufacture_ste_doc.submit()
manufacture_ste_doc.reload()
self.assertTrue(len(mfr_items), 2)
self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
get_batch_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle), batch_no
)
self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0)
def test_backflushed_serial_no_raw_materials_based_on_transferred_autosabb(self):
frappe.db.set_single_value(
"Manufacturing Settings",
"backflush_raw_materials_based_on",
"Material Transferred for Manufacture",
)
sn_item = "Test Serial No BTT Headphone"
fg_item = "Test FG Item with Serial No Raw Materials"
ste_doc = test_stock_entry.make_stock_entry(
item_code=sn_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
)
# Inward raw materials in Stores warehouse
ste_doc.submit()
ste_doc.reload()
serial_nos_list = sorted(get_serial_nos_from_bundle(ste_doc.items[0].serial_and_batch_bundle))
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
)
transferred_ste_doc.submit()
transferred_ste_doc.reload()
self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
sorted(get_serial_nos_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle)),
serial_nos_list,
)
self.assertEqual(transferred_ste_doc.items[0].qty, 4.0)
# Make additional consumption and link to WO
test_stock_entry.make_stock_entry(
item_code="Test Serial Battery Consumable",
target="Stores - _TC",
qty=8,
basic_rate=3.33,
)
consume_use_doc = test_stock_entry.make_stock_entry(
item_code="Test Serial Battery Consumable", # consumable not linked to BOM
source="Stores - _TC",
qty=4,
purpose="Material Consumption for Manufacture",
do_not_save=True,
)
consume_use_doc.work_order = wo_doc.name
consume_use_doc.fg_completed_qty = 4
consume_use_doc.submit()
consume_use_doc.reload()
manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4))
mfr_items = [i.as_dict() for i in manufacture_ste_doc.items]
manufacture_ste_doc.submit()
manufacture_ste_doc.reload()
self.assertTrue(len(mfr_items), 2)
self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
sorted(get_serial_nos_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle)),
serial_nos_list,
)
self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0)
def test_backflushed_serial_no_batch_raw_materials_based_on_transferred_autosabb(self):
frappe.db.set_single_value(
"Manufacturing Settings",
"backflush_raw_materials_based_on",
"Material Transferred for Manufacture",
)
sn_batch_item = "Test Batch Serial No WebCam"
fg_item = "Test FG Item with Serial & Batch No Raw Materials"
ste_doc = test_stock_entry.make_stock_entry(
item_code=sn_batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
)
ste_doc.submit()
ste_doc.reload()
serial_nos_list = sorted(get_serial_nos_from_bundle(ste_doc.items[0].serial_and_batch_bundle))
batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle)
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
)
transferred_ste_doc.submit()
transferred_ste_doc.reload()
self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
sorted(get_serial_nos_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle)),
serial_nos_list,
)
self.assertEqual(
get_batch_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle), batch_no
)
self.assertEqual(transferred_ste_doc.items[0].qty, 4.0)
manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4))
manufacture_ste_doc.submit()
manufacture_ste_doc.reload()
self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
sorted(get_serial_nos_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle)),
serial_nos_list,
)
self.assertEqual(
get_batch_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle), batch_no
)
self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0)
bundle = manufacture_ste_doc.items[0].serial_and_batch_bundle
bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle)
qty = sum(e.qty for e in bundle_doc.entries)
self.assertEqual(qty, -4.0)
###
def test_non_consumed_material_return_against_work_order(self):
frappe.db.set_single_value(
"Manufacturing Settings",
@@ -2335,6 +2526,29 @@ def prepare_data_for_backflush_based_on_materials_transferred():
make_bom(item=item.name, source_warehouse="Stores - _TC", raw_materials=[batch_item_doc.name])
# Make additional items not attached to a BOM
make_item(
"Test Batch Battery Consumable",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBMK.#####",
"valuation_rate": 2.33,
"stock_uom": "Nos",
},
)
make_item(
"Test Serial Battery Consumable",
{
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "TSBH.#####",
"valuation_rate": 3.33,
"stock_uom": "Nos",
},
)
sn_item_doc = make_item(
"Test Serial No BTT Headphone",
{

View File

@@ -158,11 +158,20 @@ class WorkOrder(Document):
self.validate_operation_time()
self.status = self.get_status()
self.validate_workstation_type()
self.reset_use_multi_level_bom()
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
self.set_required_items(reset_only_qty=len(self.get("required_items")))
def reset_use_multi_level_bom(self):
if self.is_new():
return
before_save_obj = self.get_doc_before_save()
if before_save_obj.use_multi_level_bom != self.use_multi_level_bom:
self.get_items_and_operations_from_bom()
def validate_workstation_type(self):
for row in self.operations:
if not row.workstation and not row.workstation_type:
@@ -520,7 +529,6 @@ class WorkOrder(Document):
def delete_auto_created_batch_and_serial_no(self):
for row in frappe.get_all("Serial No", filters={"work_order": self.name}):
frappe.delete_doc("Serial No", row.name)
self.db_set("serial_no", "")
for row in frappe.get_all("Batch", filters={"reference_name": self.name}):
frappe.delete_doc("Batch", row.name)

View File

@@ -377,3 +377,4 @@ erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
erpnext.patches.v15_0.set_standard_stock_entry_type
erpnext.patches.v15_0.link_purchase_item_to_asset_doc
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter

View File

@@ -0,0 +1,11 @@
import frappe
def execute():
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
return
settings.set_parameters_and_result()
settings.flags.ignore_validate = True
settings.save()

View File

@@ -342,12 +342,15 @@ erpnext.buying = {
add_serial_batch_bundle(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let fields = ["has_batch_no", "has_serial_no"];
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
frappe.db.get_value("Item", item.item_code, fields)
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
fields.forEach((field) => {
item[field] = r.message[field];
});
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = false;
@@ -380,13 +383,16 @@ erpnext.buying = {
add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let fields = ["has_batch_no", "has_serial_no"];
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
frappe.db.get_value("Item", item.item_code, fields)
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward";
fields.forEach((field) => {
item[field] = r.message[field];
});
item.type_of_transaction = !doc.is_return > 0 ? "Inward" : "Outward";
item.is_rejected = true;
new erpnext.SerialBatchPackageSelector(
@@ -398,7 +404,7 @@ erpnext.buying = {
}
let update_values = {
"serial_and_batch_bundle": r.name,
"rejected_serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}

View File

@@ -128,7 +128,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_item_values() {
var me = this;
if (!this.discount_amount_applied) {
for (const item of this.frm._items || []) {
for (const item of this.frm.doc.items || []) {
frappe.model.round_floats_in(item);
item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
@@ -227,7 +227,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
});
if(has_inclusive_tax==false) return;
$.each(me.frm._items || [], function(n, item) {
$.each(this.frm.doc.items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
var cumulated_tax_fraction = 0.0;
var total_inclusive_tax_amount_per_qty = 0;
@@ -630,7 +630,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
_cleanup() {
this.frm.doc.base_in_words = this.frm.doc.in_words = "";
let items = this.frm._items;
let items = this.frm.doc.items;
if(items && items.length) {
if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {

View File

@@ -573,6 +573,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
callback: function(r) {
if(!r.exc) {
frappe.run_serially([
() => {
if (item.docstatus === 0
&& frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
&& !item.use_serial_batch_fields
&& cint(frappe.user_defaults?.use_serial_batch_fields) === 1
) {
item["use_serial_batch_fields"] = 1;
}
},
() => {
var d = locals[cdt][cdn];
me.add_taxes_from_item_tax_template(d.item_tax_rate);
@@ -1102,7 +1111,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
apply_discount_on_item(doc, cdt, cdn, field) {
var item = frappe.get_doc(cdt, cdn);
if(!item.price_list_rate) {
if(!item?.price_list_rate) {
item[field] = 0.0;
} else {
this.price_list_rate(doc, cdt, cdn);
@@ -1268,6 +1277,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
.filter(Boolean).length > 0;
} else if (this.frm.doc?.items) {
let first_row = this.frm.doc.items[0];
if (!first_row) {
return false
};
let mapped_rows = mappped_fields.filter(d => first_row[d])
return mapped_rows?.length > 0;

View File

@@ -920,6 +920,7 @@ erpnext.utils.map_current_doc = function (opts) {
target: opts.target,
date_field: opts.date_field || undefined,
setters: opts.setters,
read_only_setters: opts.read_only_setters,
data_fields: data_fields,
get_query: opts.get_query,
add_filters_group: 1,

View File

@@ -16,7 +16,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
let label = this.item?.has_serial_no ? __("Serial Nos") : __("Batch Nos");
let primary_label = this.bundle ? __("Update") : __("Add");
if (this.item?.has_serial_no && this.item?.batch_no) {
if (this.item?.has_serial_no && this.item?.has_batch_no) {
label = __("Serial Nos / Batch Nos");
}
@@ -24,6 +24,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
this.dialog = new frappe.ui.Dialog({
title: this.item?.title || primary_label,
size: "large",
fields: this.get_dialog_fields(),
primary_action_label: primary_label,
primary_action: () => this.update_bundle_entries(),
@@ -95,7 +96,12 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
options: "Warehouse",
default: this.get_warehouse(),
onchange: () => {
this.item.warehouse = this.dialog.get_value("warehouse");
if (this.item?.is_rejected) {
this.item.rejected_warehouse = this.dialog.get_value("warehouse");
} else {
this.item.warehouse = this.dialog.get_value("warehouse");
}
this.get_auto_data();
},
get_query: () => {
@@ -164,12 +170,14 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
fields.push({
fieldtype: "Section Break",
depends_on: "eval:doc.enter_manually !== 1 || doc.entries?.length > 0",
});
fields.push({
fieldname: "entries",
fieldtype: "Table",
allow_bulk_edit: true,
depends_on: "eval:doc.enter_manually !== 1 || doc.entries?.length > 0",
data: [],
fields: this.get_dialog_table_fields(),
});
@@ -178,6 +186,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
}
get_attach_field() {
let me = this;
let label = this.item?.has_serial_no ? __("Serial Nos") : __("Batch Nos");
let primary_label = this.bundle ? __("Update") : __("Add");
@@ -185,66 +194,41 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
label = __("Serial Nos / Batch Nos");
}
let fields = [
{
fieldtype: "Section Break",
label: __("{0} {1} via CSV File", [primary_label, label]),
},
];
if (this.item?.has_serial_no) {
fields = [
...fields,
{
fieldtype: "Check",
label: __("Import Using CSV file"),
fieldname: "import_using_csv_file",
default: 0,
let fields = [];
if (this.item.has_serial_no) {
fields.push({
fieldtype: "Check",
label: __("Enter Manually"),
fieldname: "enter_manually",
default: 1,
depends_on: "eval:doc.import_using_csv_file !== 1",
change() {
if (me.dialog.get_value("enter_manually")) {
me.dialog.set_value("import_using_csv_file", 0);
}
},
{
fieldtype: "Section Break",
label: __("{0} {1} Manually", [primary_label, label]),
depends_on: "eval:doc.import_using_csv_file === 0",
},
{
fieldtype: "Data",
label: __("Enter Serial No Range"),
fieldname: "serial_no_range",
depends_on: "eval:doc.import_using_csv_file === 0",
description: __('Enter "ABC-001::100" for serial nos "ABC-001" to "ABC-100".'),
onchange: () => {
this.set_serial_nos_from_range();
},
},
{
fieldtype: "Small Text",
label: __("Enter Serial Nos"),
fieldname: "upload_serial_nos",
depends_on: "eval:doc.import_using_csv_file === 0",
description: __("Enter each serial no in a new line"),
},
{
fieldtype: "Column Break",
depends_on: "eval:doc.import_using_csv_file === 0",
},
{
fieldtype: "Button",
fieldname: "make_serial_nos",
label: __("Create Serial Nos"),
depends_on: "eval:doc.import_using_csv_file === 0",
click: () => {
this.create_serial_nos();
},
},
{
fieldtype: "Section Break",
depends_on: "eval:doc.import_using_csv_file === 1",
},
];
});
}
fields = [
...fields,
{
fieldtype: "Check",
label: __("Import Using CSV file"),
fieldname: "import_using_csv_file",
depends_on: "eval:doc.enter_manually !== 1",
default: !this.item.has_serial_no ? 1 : 0,
change() {
if (me.dialog.get_value("import_using_csv_file")) {
me.dialog.set_value("enter_manually", 0);
}
},
},
{
fieldtype: "Section Break",
depends_on: "eval:doc.import_using_csv_file === 1",
label: __("{0} {1} via CSV File", [primary_label, label]),
},
{
fieldtype: "Button",
fieldname: "download_csv",
@@ -262,6 +246,44 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
},
];
if (this.item?.has_serial_no) {
fields = [
...fields,
{
fieldtype: "Section Break",
label: __("{0} {1} Manually", [primary_label, label]),
depends_on: "eval:doc.enter_manually === 1",
},
{
fieldtype: "Data",
label: __("Serial No Range"),
fieldname: "serial_no_range",
depends_on: "eval:doc.enter_manually === 1 && !doc.serial_no_series",
description: __('"SN-01::10" for "SN-01" to "SN-10"'),
onchange: () => {
this.set_serial_nos_from_range();
},
},
];
}
if (this.item?.has_serial_no) {
fields = [
...fields,
{
fieldtype: "Column Break",
depends_on: "eval:doc.enter_manually === 1",
},
{
fieldtype: "Small Text",
label: __("Enter Serial Nos"),
fieldname: "upload_serial_nos",
depends_on: "eval:doc.enter_manually === 1",
description: __("Enter each serial no in a new line"),
},
];
}
return fields;
}
@@ -487,12 +509,17 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
based_on = "FIFO";
}
let warehouse = this.item.warehouse || this.item.s_warehouse;
if (this.item?.is_rejected) {
warehouse = this.item.rejected_warehouse;
}
if (qty) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_auto_data",
args: {
item_code: this.item.item_code,
warehouse: this.item.warehouse || this.item.s_warehouse,
warehouse: warehouse,
has_serial_no: this.item.has_serial_no,
has_batch_no: this.item.has_batch_no,
qty: qty,
@@ -511,6 +538,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
scan_barcode_data() {
const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
this.dialog.set_value("enter_manually", 0);
if (scan_serial_no || scan_batch_no) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_serial_batch_no_exists",
@@ -554,14 +583,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
serial_no: scan_serial_no,
},
callback: (r) => {
if (r.message) {
this.dialog.fields_dict.entries.df.data.push({
serial_no: scan_serial_no,
batch_no: r.message,
});
this.dialog.fields_dict.entries.df.data.push({
serial_no: scan_serial_no,
batch_no: r.message,
});
this.dialog.fields_dict.scan_serial_no.set_value("");
}
this.dialog.fields_dict.scan_serial_no.set_value("");
this.dialog.fields_dict.entries.grid.refresh();
},
});
}
@@ -590,6 +618,12 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
update_bundle_entries() {
let entries = this.dialog.get_values().entries;
let warehouse = this.dialog.get_value("warehouse");
let upload_serial_nos = this.dialog.get_value("upload_serial_nos");
if (!entries?.length && upload_serial_nos) {
this.create_serial_nos();
return;
}
if ((entries && !entries.length) || !entries) {
frappe.throw(__("Please add atleast one Serial No / Batch No"));
@@ -599,6 +633,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
frappe.throw(__("Please select a Warehouse"));
}
if (this.item?.is_rejected && this.item.rejected_warehouse === this.item.warehouse) {
frappe.throw(__("Rejected Warehouse and Accepted Warehouse cannot be same."));
}
frappe
.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
@@ -610,9 +648,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
},
})
.then((r) => {
this.callback && this.callback(r.message);
this.frm.save();
this.dialog.hide();
frappe.run_serially([
() => {
this.callback && this.callback(r.message);
},
() => this.frm.save(),
() => this.dialog.hide(),
]);
});
}
@@ -636,6 +678,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
}
get_warehouse() {
if (this.item?.is_rejected) {
return this.item.rejected_warehouse;
}
return this.item?.type_of_transaction === "Outward"
? this.item.warehouse || this.item.s_warehouse
: this.item.warehouse || this.item.t_warehouse;
@@ -669,5 +715,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
});
this.dialog.fields_dict.entries.grid.refresh();
if (this.dialog.fields_dict.entries.df.data?.length) {
this.dialog.set_value("enter_manually", 0);
}
}
};

View File

@@ -369,24 +369,25 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
if customer:
target.customer = customer.name
target.customer_name = customer.customer_name
# sales team
if not target.get("sales_team"):
for d in customer.get("sales_team") or []:
target.append(
"sales_team",
{
"sales_person": d.sales_person,
"allocated_percentage": d.allocated_percentage or None,
"commission_rate": d.commission_rate,
},
)
if source.referral_sales_partner:
target.sales_partner = source.referral_sales_partner
target.commission_rate = frappe.get_value(
"Sales Partner", source.referral_sales_partner, "commission_rate"
)
# sales team
if not target.get("sales_team"):
for d in customer.get("sales_team") or []:
target.append(
"sales_team",
{
"sales_person": d.sales_person,
"allocated_percentage": d.allocated_percentage or None,
"commission_rate": d.commission_rate,
},
)
target.flags.ignore_permissions = ignore_permissions
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")
@@ -430,7 +431,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
"postprocess": update_item,
"condition": can_map_row,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
"Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True},
},
@@ -495,7 +496,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
"postprocess": update_item,
"condition": lambda row: not row.is_alternative,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
},
target_doc,

View File

@@ -561,12 +561,50 @@ class TestQuotation(FrappeTestCase):
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 10,
"included_in_print_rate": 1,
},
)
quotation.submit()
self.assertEqual(quotation.net_total, 290)
self.assertEqual(quotation.grand_total, 319)
self.assertEqual(round(quotation.items[1].net_rate, 2), 136.36)
self.assertEqual(round(quotation.items[1].amount, 2), 150)
self.assertEqual(round(quotation.items[2].net_rate, 2), 163.64)
self.assertEqual(round(quotation.items[2].amount, 2), 180)
self.assertEqual(round(quotation.net_total, 2), 263.64)
self.assertEqual(round(quotation.total_taxes_and_charges, 2), 26.36)
self.assertEqual(quotation.grand_total, 290)
def test_amount_calculation_for_alternative_items(self):
"""Make sure that the amount is calculated correctly for alternative items when the qty is changed."""
from erpnext.stock.doctype.item.test_item import make_item
item_list = []
stock_items = {
"_Test Simple Item 1": 100,
"_Test Alt 1": 120,
}
for item, rate in stock_items.items():
make_item(item, {"is_stock_item": 0})
item_list.append(
{
"item_code": item,
"qty": 1,
"rate": rate,
"is_alternative": "Alt" in item,
}
)
quotation = make_quotation(item_list=item_list, do_not_submit=1)
self.assertEqual(quotation.items[1].amount, 120)
quotation.items[1].qty = 2
quotation.save()
self.assertEqual(quotation.items[1].amount, 240)
def test_alternative_items_sales_order_mapping_with_stock_items(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order

View File

@@ -35,7 +35,7 @@ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry impor
get_sre_reserved_qty_details_for_voucher,
has_reserved_stock,
)
from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate
from erpnext.stock.get_item_details import get_bin_details, get_default_bom, get_price_list_rate
from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -838,6 +838,9 @@ def make_material_request(source_name, target_doc=None):
target.project = source_parent.project
target.qty = get_remaining_qty(source)
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
target.actual_qty = get_bin_details(
target.item_code, target.warehouse, source_parent.company, True
).get("actual_qty", 0)
args = target.as_dict().copy()
args.update(
@@ -933,7 +936,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
mapper = {
"Sales Order": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
}
@@ -1125,7 +1128,10 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
"condition": lambda doc: doc.qty
and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges",
"reset_value": True,
},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
},
target_doc,

View File

@@ -30,6 +30,7 @@ from erpnext.selling.doctype.sales_order.sales_order import (
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_bin_details
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
@@ -96,6 +97,12 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
self.assertEqual(mr.material_request_type, "Purchase")
self.assertEqual(len(mr.get("items")), len(so.get("items")))
for item in mr.get("items"):
actual_qty = get_bin_details(item.item_code, item.warehouse, mr.company, True).get(
"actual_qty", 0
)
self.assertEqual(flt(item.actual_qty), actual_qty)
def test_make_delivery_note(self):
so = make_sales_order(do_not_submit=True)

View File

@@ -2,3 +2,10 @@
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Sales Order Trends"] = $.extend({}, erpnext.sales_trends_filters);
frappe.query_reports["Sales Order Trends"]["filters"].push({
fieldname: "include_closed_orders",
label: __("Include Closed Orders"),
fieldtype: "Check",
default: 0,
});

View File

@@ -68,9 +68,9 @@ def patched_requests_get(*args, **kwargs):
if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"):
if test_exchange_values.get(kwargs["params"]["date"]):
return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200)
elif args[0].startswith("https://frankfurter.app") and kwargs.get("params"):
elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"):
if kwargs["params"].get("base") and kwargs["params"].get("symbols"):
date = args[0].replace("https://frankfurter.app/", "")
date = args[0].replace("https://api.frankfurter.app/", "")
if test_exchange_values.get(date):
return PatchResponse(
{"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200

View File

@@ -485,8 +485,14 @@ def is_deletion_doc_running(company: str | None = None, err_msg: str | None = No
def check_for_running_deletion_job(doc, method=None):
# Check if DocType has 'company' field
df = qb.DocType("DocField")
if qb.from_(df).select(df.parent).where((df.fieldname == "company") & (df.parent == doc.doctype)).run():
is_deletion_doc_running(
doc.company, _("Cannot make any transactions until the deletion job is completed")
)
if doc.doctype not in ("GL Entry", "Payment Ledger Entry", "Stock Ledger Entry"):
df = qb.DocType("DocField")
if (
qb.from_(df)
.select(df.parent)
.where((df.fieldname == "company") & (df.parent == doc.doctype))
.run()
):
is_deletion_doc_running(
doc.company, _("Cannot make any transactions until the deletion job is completed")
)

View File

@@ -98,7 +98,7 @@ def setup_currency_exchange():
ces.set("result_key", [])
ces.set("req_params", [])
ces.api_endpoint = "https://frankfurter.app/{transaction_date}"
ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}"
ces.append("result_key", {"key": "rates"})
ces.append("result_key", {"key": "{to_currency}"})
ces.append("req_params", {"key": "base", "value": "{from_currency}"})

View File

@@ -48,17 +48,18 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
let actual_qty = unescape(element.attr("data-actual_qty"));
let disable_quick_entry = Number(unescape(element.attr("data-disable_quick_entry")));
let entry_type = action === "Move" ? "Material Transfer" : "Material Receipt";
let stock_uom = unescape(element.attr("data-stock-uom"));
if (disable_quick_entry) {
open_stock_entry(item, warehouse, entry_type);
} else {
if (action === "Add") {
let rate = unescape($(this).attr("data-rate"));
erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function () {
erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, stock_uom, function () {
me.refresh();
});
} else {
erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function () {
erpnext.stock.move_item(item, warehouse, null, actual_qty, null, stock_uom, function () {
me.refresh();
});
}
@@ -207,7 +208,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
}
};
erpnext.stock.move_item = function (item, source, target, actual_qty, rate, callback) {
erpnext.stock.move_item = function (item, source, target, actual_qty, rate, stock_uom, callback) {
var dialog = new frappe.ui.Dialog({
title: target ? __("Add Item") : __("Move Item"),
fields: [
@@ -295,6 +296,8 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call
let row = frappe.model.add_child(doc, "items");
row.item_code = dialog.get_value("item_code");
row.s_warehouse = dialog.get_value("source");
row.stock_uom = stock_uom;
row.uom = stock_uom;
row.t_warehouse = dialog.get_value("target");
row.qty = dialog.get_value("qty");
row.conversion_factor = 1;

View File

@@ -71,6 +71,7 @@ def get_data(
item.update(
{
"item_name": frappe.get_cached_value("Item", item.item_code, "item_name"),
"stock_uom": frappe.get_cached_value("Item", item.item_code, "stock_uom"),
"disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no")
or frappe.get_cached_value("Item", item.item_code, "has_serial_no"),
"projected_qty": flt(item.projected_qty, precision),

View File

@@ -49,12 +49,14 @@
data-disable_quick_entry="{{ d.disable_quick_entry }}"
data-warehouse="{{ d.warehouse }}"
data-actual_qty="{{ d.actual_qty }}"
data-stock-uom="{{ d.stock_uom }}"
data-item="{{ escape(d.item_code) }}">{{ __("Move") }}</a>
{% endif %}
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add"
data-disable_quick_entry="{{ d.disable_quick_entry }}"
data-warehouse="{{ d.warehouse }}"
data-actual_qty="{{ d.actual_qty }}"
data-stock-uom="{{ d.stock_uom }}"
data-item="{{ escape(d.item_code) }}"
data-rate="{{ d.valuation_rate }}">{{ __("Add") }}</a>
</div>

View File

@@ -1030,7 +1030,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
},
"Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges",
"add_if_empty": True,
"reset_value": not (args and args.get("merge_taxes")),
"ignore": args.get("merge_taxes") if args else 0,
},
"Sales Team": {

View File

@@ -2039,6 +2039,47 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(sn.status, "Delivered")
self.assertEqual(sn.warranty_period, 100)
def test_batch_return_dn(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
item_code = make_item(
"Test Batch Return DN Item 1",
properties={
"has_batch_no": 1,
"valuation_method": "Moving Average",
"create_new_batch": 1,
"batch_number_series": "TBRDN1-.#####",
"is_stock_item": 1,
},
).name
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100)
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
dn = create_delivery_note(
item_code=item_code,
qty=5,
rate=500,
use_serial_batch_fields=1,
batch_no=batch_no,
)
dn_return = make_sales_return(dn.name)
dn_return.save().submit()
self.assertEqual(dn_return.items[0].qty, 5 * -1)
returned_batch_no = get_batch_from_bundle(dn_return.items[0].serial_and_batch_bundle)
self.assertEqual(batch_no, returned_batch_no)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": dn_return.name, "voucher_type": "Delivery Note"},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 100.0 * 5)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -22,6 +22,7 @@ from frappe.utils import (
strip_html,
)
from frappe.utils.html_utils import clean_html
from pypika import Order
import erpnext
from erpnext.controllers.item_variant import (
@@ -345,7 +346,13 @@ class Item(Document):
def validate_item_tax_net_rate_range(self):
for tax in self.get("taxes"):
if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate"))
frappe.throw(
_("Taxes row #{0}: {1} cannot be smaller than {2}").format(
tax.idx,
bold(_(tax.meta.get_label("maximum_net_rate"))),
bold(_(tax.meta.get_label("minimum_net_rate"))),
)
)
def update_template_tables(self):
template = frappe.get_cached_doc("Item", self.variant_of)
@@ -1133,34 +1140,10 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
"""returns last purchase details in stock uom"""
# get last purchase order item details
last_purchase_order = frappe.db.sql(
"""\
select po.name, po.transaction_date, po.conversion_rate,
po_item.conversion_factor, po_item.base_price_list_rate,
po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate
from `tabPurchase Order` po, `tabPurchase Order Item` po_item
where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and
po.name = po_item.parent
order by po.transaction_date desc, po.name desc
limit 1""",
(item_code, cstr(doc_name)),
as_dict=1,
)
last_purchase_order = get_purchase_voucher_details("Purchase Order", item_code, doc_name)
# get last purchase receipt item details
last_purchase_receipt = frappe.db.sql(
"""\
select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate,
pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage,
pr_item.base_rate, pr_item.base_net_rate
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item
where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and
pr.name = pr_item.parent
order by pr.posting_date desc, pr.posting_time desc, pr.name desc
limit 1""",
(item_code, cstr(doc_name)),
as_dict=1,
)
last_purchase_receipt = get_purchase_voucher_details("Purchase Receipt", item_code, doc_name)
purchase_order_date = getdate(
last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01"
@@ -1181,7 +1164,13 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
purchase_date = purchase_receipt_date
else:
return frappe._dict()
last_purchase_invoice = get_purchase_voucher_details("Purchase Invoice", item_code, doc_name)
if last_purchase_invoice:
last_purchase = last_purchase_invoice[0]
purchase_date = getdate(last_purchase.posting_date)
else:
return frappe._dict()
conversion_factor = flt(last_purchase.conversion_factor)
out = frappe._dict(
@@ -1207,6 +1196,40 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
return out
def get_purchase_voucher_details(doctype, item_code, document_name):
parent_doc = frappe.qb.DocType(doctype)
child_doc = frappe.qb.DocType(doctype + " Item")
query = (
frappe.qb.from_(parent_doc)
.inner_join(child_doc)
.on(parent_doc.name == child_doc.parent)
.select(
parent_doc.name,
parent_doc.conversion_rate,
child_doc.conversion_factor,
child_doc.base_price_list_rate,
child_doc.discount_percentage,
child_doc.base_rate,
child_doc.base_net_rate,
)
.where(parent_doc.docstatus == 1)
.where(child_doc.item_code == item_code)
.where(parent_doc.name != document_name)
)
if doctype in ("Purchase Receipt", "Purchase Invoice"):
query = query.select(parent_doc.posting_date, parent_doc.posting_time)
query = query.orderby(
parent_doc.posting_date, parent_doc.posting_time, parent_doc.name, order=Order.desc
)
else:
query = query.select(parent_doc.transaction_date)
query = query.orderby(parent_doc.transaction_date, parent_doc.name, order=Order.desc)
return query.run(as_dict=1)
def check_stock_uom_with_bin(item, stock_uom):
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
return

View File

@@ -11,25 +11,10 @@ erpnext.buying.setup_buying_controller();
frappe.ui.form.on("Purchase Receipt", {
setup: (frm) => {
frm.make_methods = {
"Landed Cost Voucher": () => {
let lcv = frappe.model.get_new_doc("Landed Cost Voucher");
lcv.company = frm.doc.company;
let lcv_receipt = frappe.model.get_new_doc("Landed Cost Purchase Receipt");
lcv_receipt.receipt_document_type = "Purchase Receipt";
lcv_receipt.receipt_document = frm.doc.name;
lcv_receipt.supplier = frm.doc.supplier;
lcv_receipt.grand_total = frm.doc.grand_total;
lcv.purchase_receipts = [lcv_receipt];
frappe.set_route("Form", lcv.doctype, lcv.name);
},
};
frm.custom_make_buttons = {
"Stock Entry": "Return",
"Purchase Invoice": "Purchase Invoice",
"Landed Cost Voucher": "Landed Cost Voucher",
};
frm.set_query("expense_account", "items", function () {
@@ -114,9 +99,35 @@ frappe.ui.form.on("Purchase Receipt", {
}
}
if (frm.doc.docstatus === 1) {
frm.add_custom_button(
__("Landed Cost Voucher"),
() => {
frm.events.make_lcv(frm);
},
__("Create")
);
}
frm.events.add_custom_buttons(frm);
},
make_lcv(frm) {
frappe.call({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_lcv",
args: {
doctype: frm.doc.doctype,
docname: frm.doc.name,
},
callback: (r) => {
if (r.message) {
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
},
});
},
add_custom_buttons: function (frm) {
if (frm.doc.docstatus == 0) {
frm.add_custom_button(

View File

@@ -1214,7 +1214,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
"add_if_empty": True,
"reset_value": not (args and args.get("merge_taxes")),
"ignore": args.get("merge_taxes") if args else 0,
},
},
@@ -1370,3 +1370,26 @@ def get_item_account_wise_additional_cost(purchase_document):
@erpnext.allow_regional
def update_regional_gl_entries(gl_list, doc):
return
@frappe.whitelist()
def make_lcv(doctype, docname):
landed_cost_voucher = frappe.new_doc("Landed Cost Voucher")
details = frappe.db.get_value(doctype, docname, ["supplier", "company", "base_grand_total"], as_dict=1)
landed_cost_voucher.company = details.company
landed_cost_voucher.append(
"purchase_receipts",
{
"receipt_document_type": doctype,
"receipt_document": docname,
"grand_total": details.base_grand_total,
"supplier": details.supplier,
},
)
landed_cost_voucher.get_items_from_purchase_receipts()
return landed_cost_voucher.as_dict()

View File

@@ -3657,6 +3657,21 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(data[0].get("bal_qty"), 50.0)
def test_same_stock_and_transaction_uom_conversion_factor(self):
item_code = "Test Item for Same Stock and Transaction UOM Conversion Factor"
create_item(item_code)
pr = make_purchase_receipt(
item_code=item_code,
qty=10,
rate=100,
stock_uom="Nos",
transaction_uom="Nos",
conversion_factor=10,
)
self.assertEqual(pr.items[0].conversion_factor, 1.0)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -245,6 +245,7 @@ class QualityInspection(Document):
for i in range(1, 11):
field = "reading_" + str(i)
if reading.get(field) is None:
data[field] = 0.0
continue
data[field] = parse_float(reading.get(field))

View File

@@ -280,7 +280,7 @@ class SerialandBatchBundle(Document):
)
def validate_negative_batch(self, batch_no, available_qty):
if available_qty < 0:
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty):
msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)}
has negative stock
of quantity {bold(available_qty)} in the
@@ -288,6 +288,18 @@ class SerialandBatchBundle(Document):
frappe.throw(_(msg), BatchNegativeStockError)
def is_stock_reco_for_valuation_adjustment(self, available_qty):
if (
self.voucher_type == "Stock Reconciliation"
and self.type_of_transaction == "Outward"
and self.voucher_detail_no
and abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty"))
== abs(available_qty)
):
return True
return False
def get_sle_for_outward_transaction(self):
sle = frappe._dict(
{
@@ -1296,8 +1308,12 @@ def add_serial_batch_ledgers(entries, child_row, doc, warehouse, do_not_save=Fal
if parent_doc and isinstance(parent_doc, str):
parent_doc = parse_json(parent_doc)
if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
sb_doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse)
bundle = child_row.serial_and_batch_bundle
if child_row.get("is_rejected"):
bundle = child_row.rejected_serial_and_batch_bundle
if frappe.db.exists("Serial and Batch Bundle", bundle):
sb_doc = update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, warehouse)
else:
sb_doc = create_serial_batch_no_ledgers(
entries, child_row, parent_doc, warehouse, do_not_save=do_not_save
@@ -1329,6 +1345,15 @@ def create_serial_batch_no_ledgers(
}
)
batch_no = None
if (
not entries[0].get("batch_no")
and entries[0].get("serial_no")
and frappe.get_cached_value("Item", child_row.item_code, "has_batch_no")
):
batch_no = get_batch(child_row.item_code)
for row in entries:
row = frappe._dict(row)
doc.append(
@@ -1336,7 +1361,7 @@ def create_serial_batch_no_ledgers(
{
"qty": (flt(row.qty) or 1.0) * (1 if type_of_transaction == "Inward" else -1),
"warehouse": warehouse,
"batch_no": row.batch_no,
"batch_no": row.batch_no or batch_no,
"serial_no": row.serial_no,
},
)
@@ -1351,6 +1376,18 @@ def create_serial_batch_no_ledgers(
return doc
def get_batch(item_code):
from erpnext.stock.doctype.batch.batch import make_batch
return make_batch(
frappe._dict(
{
"item": item_code,
}
)
)
def get_type_of_transaction(parent_doc, child_row):
type_of_transaction = child_row.get("type_of_transaction")
if parent_doc.get("doctype") == "Stock Entry":
@@ -1379,8 +1416,8 @@ def get_type_of_transaction(parent_doc, child_row):
return type_of_transaction
def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object:
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, warehouse=None) -> object:
doc = frappe.get_doc("Serial and Batch Bundle", bundle)
doc.voucher_detail_no = child_row.name
doc.posting_date = parent_doc.posting_date
doc.posting_time = parent_doc.posting_time

View File

@@ -447,9 +447,11 @@ frappe.ui.form.on("Stock Entry", {
source_doctype: "Stock Entry",
target: frm,
date_field: "posting_date",
read_only_setters: ["stock_entry_type", "purpose", "add_to_transit"],
setters: {
stock_entry_type: "Material Transfer",
purpose: "Material Transfer",
add_to_transit: 1,
},
get_query_filters: {
docstatus: 1,

View File

@@ -1594,10 +1594,6 @@ class StockEntry(StockController):
if pro_doc.status == "Stopped":
msg = f"Transaction not allowed against stopped Work Order {self.work_order}"
if self.is_return and pro_doc.status not in ["Completed", "Closed"]:
title = _("Stock Return")
msg = f"Work Order {self.work_order} must be completed or closed"
if msg:
frappe.throw(_(msg), title=title)
@@ -3148,11 +3144,13 @@ def get_available_materials(work_order) -> dict:
if row.serial_no:
for serial_no in get_serial_nos(row.serial_no):
item_data.serial_nos.remove(serial_no)
if serial_no in item_data.serial_nos:
item_data.serial_nos.remove(serial_no)
elif row.serial_nos:
for serial_no in get_serial_nos(row.serial_nos):
item_data.serial_nos.remove(serial_no)
if serial_no in item_data.serial_nos:
item_data.serial_nos.remove(serial_no)
return available_materials
@@ -3270,6 +3268,9 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N
for batch_no, qty in row.batches_to_be_consume.items():
doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1})
if not doc.entries:
return None
return doc.insert(ignore_permissions=True).name

View File

@@ -61,6 +61,7 @@ class StockReconciliation(StockController):
self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]
def validate(self):
self.validate_items_exist()
if not self.expense_account:
self.expense_account = frappe.get_cached_value(
"Company", self.company, "stock_adjustment_account"
@@ -162,6 +163,9 @@ class StockReconciliation(StockController):
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
"""Set Serial and Batch Bundle for each item"""
for item in self.items:
if not frappe.db.exists("Item", item.item_code):
frappe.throw(_("Item {0} does not exist").format(item.item_code))
if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
bundle = self.get_bundle_for_specific_serial_batch(item)
item.current_serial_and_batch_bundle = bundle.name
@@ -357,6 +361,9 @@ class StockReconciliation(StockController):
def set_new_serial_and_batch_bundle(self):
for item in self.items:
if not item.item_code:
continue
if item.use_serial_batch_fields:
continue
@@ -684,7 +691,7 @@ class StockReconciliation(StockController):
from erpnext.stock.stock_ledger import get_stock_value_difference
difference_amount = get_stock_value_difference(
row.item_code, row.warehouse, self.posting_date, self.posting_time
row.item_code, row.warehouse, self.posting_date, self.posting_time, self.name
)
if not difference_amount:

View File

@@ -1,30 +1,33 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"delivery_document_type\", \"in\", [\"Delivery Note\", \"Sales Invoice\"]], [\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
"modified": "2017-02-24 20:02:00.706889",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Service Contract Expiry",
"owner": "Administrator",
"ref_doctype": "Serial No",
"report_name": "Serial No Service Contract Expiry",
"report_type": "Report Builder",
"add_total_row": 0,
"columns": [],
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
"letterhead": null,
"modified": "2024-09-26 13:07:23.451182",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Service Contract Expiry",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Serial No",
"report_name": "Serial No Service Contract Expiry",
"report_type": "Report Builder",
"roles": [
{
"role": "Item Manager"
},
},
{
"role": "Stock Manager"
},
},
{
"role": "Stock User"
}
]
}
}

View File

@@ -1,30 +1,33 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 4,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.name\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warehouse\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"], [\"purchase_document_type\", \"Serial No\"], [\"purchase_document_no\", \"Serial No\"], [\"purchase_date\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"purchase_rate\", \"Serial No\"], [\"delivery_document_type\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"delivery_date\", \"Serial No\"], [\"supplier\", \"Serial No\"], [\"supplier_name\", \"Serial No\"]]}",
"modified": "2017-02-24 19:54:21.392265",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Status",
"owner": "Administrator",
"ref_doctype": "Serial No",
"report_name": "Serial No Status",
"report_type": "Report Builder",
"add_total_row": 0,
"columns": [],
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 4,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.name\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warehouse\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"],[\"purchase_document_no\", \"Serial No\"]]}",
"letterhead": null,
"modified": "2024-09-26 13:10:52.693648",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Status",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Serial No",
"report_name": "Serial No Status",
"report_type": "Report Builder",
"roles": [
{
"role": "Item Manager"
},
},
{
"role": "Stock Manager"
},
},
{
"role": "Stock User"
}
]
}
}

View File

@@ -1,30 +1,33 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"delivery_document_type\", \"in\", [\"Delivery Note\", \"Sales Invoice\"]], [\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warranty_expiry_date\", \"Serial No\"], [\"warranty_period\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"], [\"purchase_document_no\", \"Serial No\"], [\"purchase_date\", \"Serial No\"], [\"supplier\", \"Serial No\"], [\"supplier_name\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"delivery_date\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
"modified": "2017-02-24 20:01:53.097456",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Warranty Expiry",
"owner": "Administrator",
"ref_doctype": "Serial No",
"report_name": "Serial No Warranty Expiry",
"report_type": "Report Builder",
"add_total_row": 0,
"columns": [],
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
"letterhead": null,
"modified": "2024-09-26 13:07:23.451182",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Service Contract Expiry",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Serial No",
"report_name": "Serial No Service Contract Expiry",
"report_type": "Report Builder",
"roles": [
{
"role": "Item Manager"
},
},
{
"role": "Stock Manager"
},
},
{
"role": "Stock User"
}
]
}
}

View File

@@ -32,6 +32,12 @@ frappe.query_reports["Stock Ledger Invariant Check"] = {
mandatory: 1,
options: "Warehouse",
},
{
fieldname: "show_incorrect_entries",
fieldtype: "Check",
label: "Show Incorrect Entries",
default: 0,
},
],
formatter(value, row, column, data, default_formatter) {

View File

@@ -5,7 +5,7 @@ import json
import frappe
from frappe import _
from frappe.utils import get_link_to_form, parse_json
from frappe.utils import cint, flt, get_link_to_form, parse_json
SLE_FIELDS = (
"name",
@@ -36,7 +36,7 @@ def execute(filters=None):
def get_data(filters):
sles = get_stock_ledger_entries(filters)
return add_invariant_check_fields(sles)
return add_invariant_check_fields(sles, filters)
def get_stock_ledger_entries(filters):
@@ -48,11 +48,14 @@ def get_stock_ledger_entries(filters):
)
def add_invariant_check_fields(sles):
def add_invariant_check_fields(sles, filters):
balance_qty = 0.0
balance_stock_value = 0.0
incorrect_idx = 0
precision = frappe.get_precision("Stock Ledger Entry", "actual_qty")
for idx, sle in enumerate(sles):
queue = json.loads(sle.stock_queue)
queue = json.loads(sle.stock_queue) if sle.stock_queue else []
fifo_qty = 0.0
fifo_value = 0.0
@@ -95,6 +98,12 @@ def add_invariant_check_fields(sles):
)
sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value
if not incorrect_idx and filters.get("show_incorrect_entries"):
if is_sle_has_correct_data(sle, precision):
continue
else:
incorrect_idx = idx
if idx > 0:
sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
@@ -104,9 +113,23 @@ def add_invariant_check_fields(sles):
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
)
if filters.get("show_incorrect_entries"):
if incorrect_idx > 0:
sles = sles[cint(incorrect_idx) - 1 :]
return []
return sles
def is_sle_has_correct_data(sle, precision):
if flt(sle.difference_in_qty, precision) != 0.0 or flt(sle.diff_value_diff, precision) != 0:
print(flt(sle.difference_in_qty, precision), flt(sle.diff_value_diff, precision))
return False
return True
def get_columns():
return [
{

View File

@@ -3,6 +3,15 @@
frappe.query_reports["Warehouse wise Item Balance Age and Value"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
width: "80",
options: "Company",
reqd: 1,
default: frappe.defaults.get_user_default("Company"),
},
{
fieldname: "from_date",
label: __("From Date"),
@@ -39,6 +48,12 @@ frappe.query_reports["Warehouse wise Item Balance Age and Value"] = {
fieldtype: "Link",
width: "80",
options: "Warehouse",
get_query: function () {
const company = frappe.query_report.get_filter_value("company");
return {
filters: { company: company },
};
},
},
{
fieldname: "filter_total_zero_qty",

View File

@@ -109,8 +109,6 @@ def validate_filters(filters):
sle_count = flt(frappe.qb.from_("Stock Ledger Entry").select(Count("name")).run()[0][0])
if sle_count > 500000:
frappe.throw(_("Please set filter based on Item or Warehouse"))
if not filters.get("company"):
filters["company"] = frappe.defaults.get_user_default("Company")
def get_warehouse_list(filters):

View File

@@ -865,7 +865,7 @@ class update_entries_after:
sle.stock_value = self.wh_data.stock_value
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
if not sle.is_adjustment_entry or not self.args.get("sle_id"):
if not sle.is_adjustment_entry:
sle.stock_value_difference = stock_value_difference
sle.doctype = "Stock Ledger Entry"
@@ -1074,6 +1074,15 @@ class update_entries_after:
}
)
if not rate and sle.voucher_type in ["Delivery Note", "Sales Invoice"]:
rate = get_rate_for_return(
sle.voucher_type,
sle.voucher_no,
sle.item_code,
voucher_detail_no=sle.voucher_detail_no,
sle=sle,
)
else:
rate = get_rate_for_return(
sle.voucher_type,
@@ -1731,6 +1740,9 @@ def get_valuation_rate(
# Get moving average rate of a specific batch number
if warehouse and serial_and_batch_bundle:
sabb = frappe.db.get_value(
"Serial and Batch Bundle", serial_and_batch_bundle, ["posting_date", "posting_time"], as_dict=True
)
batch_obj = BatchNoValuation(
sle=frappe._dict(
{
@@ -1738,6 +1750,8 @@ def get_valuation_rate(
"warehouse": warehouse,
"actual_qty": -1,
"serial_and_batch_bundle": serial_and_batch_bundle,
"posting_date": sabb.posting_date,
"posting_time": sabb.posting_time,
}
)
)

View File

@@ -249,11 +249,15 @@ frappe.ui.form.on("Subcontracting Receipt", {
});
frm.set_query("batch_no", "supplied_items", (doc, cdt, cdn) => {
var row = locals[cdt][cdn];
let row = locals[cdt][cdn];
let filters = {
item_code: row.rm_item_code,
warehouse: doc.supplier_warehouse,
};
return {
filters: {
item: row.rm_item_code,
},
query: "erpnext.controllers.queries.get_batch_no",
filters: filters,
};
});

View File

@@ -237,9 +237,14 @@ class SubcontractingReceipt(SubcontractingController):
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
== "BOM"
and self.supplied_items
and not any(item.serial_and_batch_bundle for item in self.supplied_items)
):
self.supplied_items = []
if not any(
item.serial_and_batch_bundle or item.batch_no or item.serial_no
for item in self.supplied_items
):
self.supplied_items = []
else:
self.update_rate_for_supplied_items()
@frappe.whitelist()
def get_scrap_items(self, recalculate_rate=False):

View File

@@ -1361,6 +1361,66 @@ class TestSubcontractingReceipt(FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def test_change_batch_for_raw_materials(self):
set_backflush_based_on("BOM")
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
rm_item1 = make_item(
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BNGS-.####",
}
).name
bom = make_bom(item=fg_item, raw_materials=[rm_item1])
second_batch_no = None
for row in bom.items:
se = make_stock_entry(
item_code=row.item_code,
qty=1,
target="_Test Warehouse 1 - _TC",
rate=300,
)
se.reload()
se1 = make_stock_entry(
item_code=row.item_code,
qty=1,
target="_Test Warehouse 1 - _TC",
rate=300,
)
se1.reload()
second_batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 1,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 1,
},
]
sco = get_subcontracting_order(service_items=service_items)
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.reload()
scr.supplied_items[0].batch_no = second_batch_no
scr.supplied_items[0].use_serial_batch_fields = 1
scr.submit()
scr.reload()
batch_no = get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle)
self.assertEqual(batch_no, second_batch_no)
self.assertEqual(scr.items[0].rm_cost_per_qty, 300)
self.assertEqual(scr.items[0].service_cost_per_qty, 100)
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)

View File

@@ -137,6 +137,11 @@ def task(doc_name, from_doctype, to_doctype):
},
"Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice},
}
hooks = frappe.get_hooks("bulk_transaction_task_mapper")
for hook in hooks:
mapper.update(frappe.get_attr(hook)())
frappe.flags.bulk_transaction = True
if to_doctype in ["Payment Entry"]:
obj = mapper[from_doctype][to_doctype](from_doctype, doc_name)

View File

@@ -29,7 +29,7 @@ requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
[tool.bench.frappe-dependencies]
frappe = ">=15.10.0,<16.0.0"
frappe = ">=15.40.4,<16.0.0"
[tool.ruff]
line-length = 110