Compare commits

...

128 Commits

Author SHA1 Message Date
Frappe PR Bot
488e693ed8 chore(release): Bumped to Version 15.12.1
## [15.12.1](https://github.com/frappe/erpnext/compare/v15.12.0...v15.12.1) (2024-02-07)

### Bug Fixes

* set rate for PO created against BO (backport [#39765](https://github.com/frappe/erpnext/issues/39765)) (backport [#39767](https://github.com/frappe/erpnext/issues/39767)) ([#39775](https://github.com/frappe/erpnext/issues/39775)) ([ae2a8db](ae2a8db0d7))
2024-02-07 06:07:15 +00:00
mergify[bot]
ae2a8db0d7 fix: set rate for PO created against BO (backport #39765) (backport #39767) (#39775)
fix: set rate for PO created against BO (backport #39765) (#39767)

* fix: set rate for PO created against BO

(cherry picked from commit 0e5b4e5f07)

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

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
(cherry picked from commit 22138867f5)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2024-02-07 11:35:55 +05:30
Frappe PR Bot
2ba283d138 chore(release): Bumped to Version 15.12.0
# [15.12.0](https://github.com/frappe/erpnext/compare/v15.11.1...v15.12.0) (2024-02-06)

### Bug Fixes

* Blanket Order Ordered Quantity (backport [#39725](https://github.com/frappe/erpnext/issues/39725)) ([#39739](https://github.com/frappe/erpnext/issues/39739)) ([b126720](b126720826))
* conditionally display show btn setting ([e931432](e9314325cc))
* correctly calculate diff amount for included taxes ([#39655](https://github.com/frappe/erpnext/issues/39655)) ([7f4cd3c](7f4cd3cd15))
* do not throw validation for cancelled sle ([02f56ee](02f56ee20e))
* don't overwrite existing terms in transaction ([dcdd3e8](dcdd3e8cc4))
* Exchange rate on MR to PO creation for muticurrency POs ([#39646](https://github.com/frappe/erpnext/issues/39646)) ([29eb090](29eb090528))
* fetch/change tax template on basis of base_net_rate instead of net_rate ([#39448](https://github.com/frappe/erpnext/issues/39448)) ([d7c7748](d7c7748c03))
* incorrect landed cost voucher amount ([a40864d](a40864dede))
* out of range for valuation_rate column in SE (backport [#39687](https://github.com/frappe/erpnext/issues/39687)) ([#39691](https://github.com/frappe/erpnext/issues/39691)) ([41e4632](41e46326c0))
* Percentage handling in queries ([#39692](https://github.com/frappe/erpnext/issues/39692)) ([e76f598](e76f59803c))
* **portal:** show PO pay button if payments installed ([cea4ed6](cea4ed6f88))
* Pricing rule application/removal on qty change ([#39084](https://github.com/frappe/erpnext/issues/39084)) ([f310707](f31070741a))
* production plan date filters for orders (backport [#39702](https://github.com/frappe/erpnext/issues/39702)) ([#39720](https://github.com/frappe/erpnext/issues/39720)) ([3eac436](3eac436e7b))
* remove applied pricing rule on qty change (backport [#39688](https://github.com/frappe/erpnext/issues/39688)) ([#39737](https://github.com/frappe/erpnext/issues/39737)) ([f246684](f246684694))
* remove file from the disk after the completion of reposting ([b582e9c](b582e9c42c))
* show warehouse title field in sales docs (backport [#39746](https://github.com/frappe/erpnext/issues/39746)) ([#39755](https://github.com/frappe/erpnext/issues/39755)) ([53992de](53992deb10))
* update company in serial no doc (backport [#39733](https://github.com/frappe/erpnext/issues/39733)) ([#39747](https://github.com/frappe/erpnext/issues/39747)) ([00e86bf](00e86bf318))
* use old serial / batch fields to make serial batch bundle ([282c19e](282c19e7e1))
* **work order:** resolve type error during job card creation (backport [#39713](https://github.com/frappe/erpnext/issues/39713)) ([#39714](https://github.com/frappe/erpnext/issues/39714)) ([5fe40ac](5fe40ac085))

### Features

* copy emails from lead to customer ([#38647](https://github.com/frappe/erpnext/issues/38647)) ([986273b](986273b6d3))
* make material request for job card from workstation dashboard ([3bef12c](3bef12cb55))
* Period-wise closing entries for TB ([#39712](https://github.com/frappe/erpnext/issues/39712)) ([1822325](1822325f34))
* reference for POS SI payments ([#39523](https://github.com/frappe/erpnext/issues/39523)) ([4ee8cf3](4ee8cf3907))
* visual plant floor ([75bd1e6](75bd1e6b65))

### Performance Improvements

* memory consumption for the stock balance report ([#39626](https://github.com/frappe/erpnext/issues/39626)) ([5e29aab](5e29aab83b))
* Move dimension validation out of GL Entry doctype ([#39730](https://github.com/frappe/erpnext/issues/39730)) ([451c288](451c288011))
* timeout for auto material request through reorder level ([1b2831b](1b2831bdfe))
* timeout while submitting the purchase receipt entry ([55a8326](55a8326d06))
2024-02-06 18:44:29 +00:00
rohitwaghchaure
af8af2b36e Merge pull request #39758 from frappe/version-15-hotfix
chore: release v15
2024-02-07 00:13:18 +05:30
rohitwaghchaure
789aee4544 Merge pull request #39770 from frappe/mergify/bp/version-15-hotfix/pr-39769
fix: do not throw validation for canceled SLE (backport #39769)
2024-02-06 23:46:00 +05:30
Rohit Waghchaure
02f56ee20e fix: do not throw validation for cancelled sle
(cherry picked from commit 32ccf3524a)
2024-02-06 17:43:17 +00:00
rohitwaghchaure
c78d4c15d3 Merge pull request #39768 from frappe/mergify/bp/version-15-hotfix/pr-39764
fix: remove file from the disk after the completion of reposting (backport #39764)
2024-02-06 22:22:46 +05:30
Rohit Waghchaure
7833138c57 test: test case to check removed attached file
(cherry picked from commit 76b57a4338)
2024-02-06 15:58:17 +00:00
Rohit Waghchaure
b582e9c42c fix: remove file from the disk after the completion of reposting
(cherry picked from commit fb330d1b5a)
2024-02-06 15:58:16 +00:00
rohitwaghchaure
faf79f4d2a Merge pull request #39761 from frappe/mergify/bp/version-15-hotfix/pr-39718
fix: use old serial / batch fields to make serial batch bundle (backport #39718)
2024-02-06 16:26:33 +05:30
ruthra kumar
47c2bd7335 Merge pull request #39751 from frappe/mergify/bp/version-15-hotfix/pr-39694
refactor: enforce unique GL Account for each 'Bank Account' (backport #39694)
2024-02-06 15:48:31 +05:30
Rohit Waghchaure
73618f0605 test: test case to check use serial / batch fields feature
(cherry picked from commit 01650120d4)
2024-02-06 10:18:06 +00:00
Rohit Waghchaure
a08b97e886 test: fixed test cases
(cherry picked from commit c1e869f040)
2024-02-06 10:18:05 +00:00
Rohit Waghchaure
282c19e7e1 fix: use old serial / batch fields to make serial batch bundle
(cherry picked from commit 9fafc83632)
2024-02-06 10:18:05 +00:00
mergify[bot]
53992deb10 fix: show warehouse title field in sales docs (backport #39746) (#39755)
fix: show warehouse title field in sales docs

(cherry picked from commit ee14faaa39)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2024-02-06 12:45:37 +05:30
ruthra kumar
4235e08668 refactor(test): make use of test fixtures in Payment Order
(cherry picked from commit 322cdbaccf)
2024-02-06 04:00:23 +00:00
ruthra kumar
949f4c3790 refactor(test): generate uniq GL acc and Bank acc for each test case
(cherry picked from commit a9a2ec81de)
2024-02-06 04:00:23 +00:00
ruthra kumar
11d4382e19 refactor: ensure unique accounts for each Bank Account's
(cherry picked from commit 2caa2d677c)
2024-02-06 04:00:23 +00:00
mergify[bot]
00e86bf318 fix: update company in serial no doc (backport #39733) (#39747)
fix: update company in serial no doc

(cherry picked from commit 7a04f0f7ba)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2024-02-05 21:55:56 +05:30
rohitwaghchaure
d3c4bd7f5f Merge pull request #39745 from frappe/mergify/bp/version-15-hotfix/pr-39742
perf: timeout while submitting the purchase receipt entry (backport #39742)
2024-02-05 21:17:59 +05:30
rohitwaghchaure
cb15ff1d74 Merge pull request #39743 from frappe/mergify/bp/version-15-hotfix/pr-39730
perf: Move dimension validation out of GL Entry doctype (backport #39730)
2024-02-05 21:17:43 +05:30
Rohit Waghchaure
55a8326d06 perf: timeout while submitting the purchase receipt entry
(cherry picked from commit 1fa6233377)
2024-02-05 15:33:04 +00:00
Deepesh Garg
451c288011 perf: Move dimension validation out of GL Entry doctype (#39730)
(cherry picked from commit b834ed10d6)
2024-02-05 15:31:36 +00:00
rohitwaghchaure
7923bd7964 Merge pull request #39741 from frappe/mergify/bp/version-15-hotfix/pr-38362
feat: visual plant floor (backport #38362)
2024-02-05 19:42:20 +05:30
Rohit Waghchaure
3bef12cb55 feat: make material request for job card from workstation dashboard
(cherry picked from commit 6fea9d6dfe)
2024-02-05 13:50:45 +00:00
Rohit Waghchaure
75bd1e6b65 feat: visual plant floor
(cherry picked from commit 68c997aa06)
2024-02-05 13:50:44 +00:00
mergify[bot]
b126720826 fix: Blanket Order Ordered Quantity (backport #39725) (#39739)
* fix: disable no-copy for blanket order in PO

(cherry picked from commit 5ce5c352e4)

* fix: update BO Ordered Quantity on PO Close/Open

(cherry picked from commit 61ded697a7)

* test: BO on PO Close/Open

(cherry picked from commit 27d6c8b6d5)

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2024-02-05 16:31:20 +05:30
mergify[bot]
f246684694 fix: remove applied pricing rule on qty change (backport #39688) (#39737)
fix: remove pricing rule

(cherry picked from commit 7c6a5a0f23)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2024-02-05 16:09:00 +05:30
mergify[bot]
1822325f34 feat: Period-wise closing entries for TB (#39712)
feat: Period-wise closing entries for TB (#39712)

(cherry picked from commit 6e6c818084)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2024-02-05 14:34:33 +05:30
mergify[bot]
986273b6d3 feat: copy emails from lead to customer (#38647)
feat: copy emails from lead to customer

(cherry picked from commit 906ac093e3)

Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2024-02-05 14:05:44 +05:30
rohitwaghchaure
f4ac00595d Merge pull request #39729 from frappe/mergify/bp/version-15-hotfix/pr-39659
perf: timeout for auto material request through reorder level (backport #39659)
2024-02-05 13:20:22 +05:30
rohitwaghchaure
66661d0649 Merge pull request #39732 from frappe/mergify/bp/version-15-hotfix/pr-39626
perf: memory consumption for the stock balance report (backport #39626)
2024-02-05 13:17:41 +05:30
rohitwaghchaure
326751c8bd Merge pull request #39727 from frappe/mergify/bp/version-15-hotfix/pr-39684
fix: incorrect landed cost voucher amount (backport #39684)
2024-02-05 13:17:11 +05:30
rohitwaghchaure
5e29aab83b perf: memory consumption for the stock balance report (#39626)
(cherry picked from commit b70f3de16b)
2024-02-05 06:18:52 +00:00
Rohit Waghchaure
1b2831bdfe perf: timeout for auto material request through reorder level
(cherry picked from commit 951023f434)
2024-02-05 06:10:56 +00:00
Rohit Waghchaure
a40864dede fix: incorrect landed cost voucher amount
(cherry picked from commit d78a1e7814)
2024-02-05 06:09:51 +00:00
mergify[bot]
3eac436e7b fix: production plan date filters for orders (backport #39702) (#39720)
fix: production plan date filters for orders (#39702)

(cherry picked from commit 407045a1de)

Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
2024-02-05 10:14:42 +05:30
mergify[bot]
5fe40ac085 fix(work order): resolve type error during job card creation (backport #39713) (#39714)
fix(work order): resolve type error during job card creation (#39713)

fix: type error
(cherry picked from commit c81d597ca5)

Co-authored-by: Vishnu  VS <Vishnuviswambaran2002@gmail.com>
2024-02-04 22:59:17 +05:30
mergify[bot]
4ee8cf3907 feat: reference for POS SI payments (#39523)
feat: reference for POS SI payments (#39523)

* feat: reference field in SI payment

* fix: document link for pos si

* refactor: pos invoice queries

(cherry picked from commit d9a72c1e61)

Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
2024-02-04 17:54:56 +05:30
mergify[bot]
d7c7748c03 fix: fetch/change tax template on basis of base_net_rate instead of net_rate (#39448)
fix: fetch/change tax template on basis of base_net_rate instead of net_rate (#39448)

fix: change tax template on basis of base_net_rate instead of net_rate

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

Co-authored-by: Divyam Mistry <73271406+divyam-mistry@users.noreply.github.com>
2024-02-03 09:09:59 +05:30
mergify[bot]
f31070741a fix: Pricing rule application/removal on qty change (#39084)
fix: Pricing rule application/removal on qty change

(cherry picked from commit f52d7c7665)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2024-02-03 08:49:50 +05:30
Frappe PR Bot
317a74b58c chore(release): Bumped to Version 15.11.1
## [15.11.1](https://github.com/frappe/erpnext/compare/v15.11.0...v15.11.1) (2024-02-02)

### Bug Fixes

* Percentage handling in queries ([#39692](https://github.com/frappe/erpnext/issues/39692)) ([0405a2b](0405a2b817))
2024-02-02 15:24:10 +00:00
mergify[bot]
0405a2b817 fix: Percentage handling in queries (#39692)
fix: Percentage handling in queries (#39692)

fix: Percentage handling in queries (#39692)

* fix: Percentage handling in queries

* test: Account with percent sign

* chore: add test records

(cherry picked from commit 6d87cfeb8d)

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2024-02-02 20:53:03 +05:30
mergify[bot]
e76f59803c fix: Percentage handling in queries (#39692)
fix: Percentage handling in queries (#39692)

* fix: Percentage handling in queries

* test: Account with percent sign

* chore: add test records

(cherry picked from commit 6d87cfeb8d)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2024-02-02 20:27:36 +05:30
mergify[bot]
41e46326c0 fix: out of range for valuation_rate column in SE (backport #39687) (#39691)
fix: out of range for valuation_rate column in SE

(cherry picked from commit 1e15a3cc15)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2024-02-02 15:24:45 +05:30
Gursheen Kaur Anand
8574941cb6 Merge pull request #39648 from frappe/mergify/bp/version-15-hotfix/pr-39519
fix: don't overwrite existing terms in transaction (backport #39519)
2024-02-01 23:48:18 +05:30
ruthra kumar
4f994cf26c Merge pull request #39666 from frappe/mergify/bp/version-15-hotfix/pr-39655
fix: correctly calculate diff amount for included taxes (backport #39655)
2024-02-01 19:44:57 +05:30
ruthra kumar
bb3591deec Merge pull request #39680 from frappe/mergify/bp/version-15-hotfix/pr-39674
refactor: use pop up to inform of possible data issue (backport #39674)
2024-02-01 17:31:18 +05:30
ruthra kumar
e2d041f51c refactor: use pop up to inform of possible data issue
and leave a comment in communcation trail as well

(cherry picked from commit 78483e2ee6)
2024-02-01 11:43:19 +00:00
ruthra kumar
63f306f1b4 Merge pull request #39676 from frappe/mergify/bp/version-15-hotfix/pr-39671
refactor: move ignore ERR filter from SOA to General Ledger (backport #39671)
2024-02-01 16:38:33 +05:30
ruthra kumar
72c95d31d1 refactor(test): use party with USD billing currency
(cherry picked from commit beff566c82)
2024-02-01 10:41:39 +00:00
ruthra kumar
1ee15f65a8 test: ignore_err filter out in General Ledger
(cherry picked from commit affca3a519)
2024-02-01 10:41:39 +00:00
ruthra kumar
0a235584ad refactor: move ignore ERR filters from SOA to General Ledger
(cherry picked from commit c077eda64e)
2024-02-01 10:41:39 +00:00
Gursheen Kaur Anand
7f4cd3cd15 fix: correctly calculate diff amount for included taxes (#39655)
(cherry picked from commit 772f540bef)
2024-02-01 04:44:40 +00:00
barredterra
ba55d0ede7 chore: resolve conflicts 2024-01-31 13:19:19 +01:00
rohitwaghchaure
c74482a27c Merge pull request #39658 from frappe/mergify/bp/version-15-hotfix/pr-39643
fix(portal): show PO pay button if payments installed (backport #39643)
2024-01-31 17:30:48 +05:30
rohitwaghchaure
510ab769f1 chore: fix conflicts 2024-01-31 15:23:14 +05:30
Gursheen Anand
e9314325cc fix: conditionally display show btn setting
(cherry picked from commit 0c9572bb48)

# Conflicts:
#	erpnext/buying/doctype/buying_settings/buying_settings.json
2024-01-31 09:16:12 +00:00
Gursheen Anand
cea4ed6f88 fix(portal): show PO pay button if payments installed
(cherry picked from commit ae7be84d87)
2024-01-31 09:16:11 +00:00
mergify[bot]
29eb090528 fix: Exchange rate on MR to PO creation for muticurrency POs (#39646)
fix: Exchange rate on MR to PO creation for muticurrency POs (#39646)

(cherry picked from commit cfd1666181)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2024-01-31 12:49:48 +05:30
mergify[bot]
aebb0c7979 chore: cleanup doctype descriptions (backport #39637) (#39652)
* chore: cleanup doctype descriptions (#39637)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 6b8f046fb4)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/fiscal_year/fiscal_year.json
#	erpnext/stock/doctype/item_price/item_price.json
#	erpnext/stock/doctype/stock_settings/stock_settings.json

* chore: fix conflicts

---------

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2024-01-31 12:38:37 +05:30
barredterra
dcdd3e8cc4 fix: don't overwrite existing terms in transaction
(cherry picked from commit 77b044f1a6)

# Conflicts:
#	erpnext/public/js/controllers/transaction.js
2024-01-31 00:14:44 +00:00
Frappe PR Bot
f84f82c7d7 chore(release): Bumped to Version 15.11.0
# [15.11.0](https://github.com/frappe/erpnext/compare/v15.10.8...v15.11.0) (2024-01-30)

### Bug Fixes

* AttributeError in company transaction deletion ([222005c](222005ca44))
* **Batch:** reload doc after splitting ([a97b8fd](a97b8fd876))
* check page obj before adding menu ([0aa73e3](0aa73e31ce))
* default enable closing stock balance (backport [#39551](https://github.com/frappe/erpnext/issues/39551)) ([#39554](https://github.com/frappe/erpnext/issues/39554)) ([4853ab1](4853ab1fec))
* do not auto-populate item delivery date ([5cdc22a](5cdc22adf3))
* email list for auto reorder material request ([98c5653](98c56535d8))
* enqueue JV submission when more than 100 accounts ([07bcc24](07bcc24e35))
* fetch correct quantity and amount for grouped asset ([3bdd1e9](3bdd1e9514))
* honour currency precision while fetching balance ([e0a3820](e0a38207f6))
* incorrect active serial nos (backport [#39389](https://github.com/frappe/erpnext/issues/39389)) ([#39589](https://github.com/frappe/erpnext/issues/39589)) ([7def475](7def475eb1))
* incorrect amount in the material request item (backport [#39567](https://github.com/frappe/erpnext/issues/39567)) ([#39569](https://github.com/frappe/erpnext/issues/39569)) ([e0c35d6](e0c35d60b4))
* Item Tax template is not working for e-commerce ([2905db1](2905db19e5))
* make SO item code reqd ([171586d](171586db9b))
* not able to edit address through portal ([1f5dbeb](1f5dbebe68))
* not able to save BOM (duplicate key error) (backport [#39620](https://github.com/frappe/erpnext/issues/39620)) ([#39623](https://github.com/frappe/erpnext/issues/39623)) ([53c7055](53c705506b))
* not able to save subcontracting purchase receipt (old flow) (backport [#39590](https://github.com/frappe/erpnext/issues/39590)) ([#39592](https://github.com/frappe/erpnext/issues/39592)) ([63eef68](63eef680d0))
* not able to submit subcontracting pr (old flow) (backport [#39622](https://github.com/frappe/erpnext/issues/39622)) ([#39625](https://github.com/frappe/erpnext/issues/39625)) ([960443d](960443d268))
* Payment Terms Status for Sales Order report should show all payment terms from order not only this comming from template ([b2e0d24](b2e0d24954))
* perf issue while submitting stock entry (backport [#39634](https://github.com/frappe/erpnext/issues/39634)) ([#39642](https://github.com/frappe/erpnext/issues/39642)) ([d785c6c](d785c6ce3e))
* prevent extra transfer against inter transfer transaction (backport [#39213](https://github.com/frappe/erpnext/issues/39213)) ([#39596](https://github.com/frappe/erpnext/issues/39596)) ([6d4ca6a](6d4ca6a1ee))
* qtn tests using delivery date ([6b20a7e](6b20a7eed1))
* return doc obj after submit ([da33079](da33079f12))
* Serial No Ledger permission issue ([f9d1995](f9d1995954))
* skip liability account for internal transfer ([e2d85c5](e2d85c513c))
* type error on company doc ([a7d1368](a7d13686c7))

### Features

* In words in payment entry ([5df2f5d](5df2f5d523))
* New financial views - Growth and margin views for P&L and balance sheet ([#39588](https://github.com/frappe/erpnext/issues/39588)) ([a83f310](a83f3106f3))
* Partly billed status in Purchase Receipt ([#39543](https://github.com/frappe/erpnext/issues/39543)) ([e4230cf](e4230cf6d9))
2024-01-30 14:14:58 +00:00
rohitwaghchaure
32f77eae5d Merge pull request #39639 from frappe/version-15-hotfix
chore: release v15
2024-01-30 19:42:27 +05:30
mergify[bot]
d785c6ce3e fix: perf issue while submitting stock entry (backport #39634) (#39642)
fix: perf issue while submitting stock entry (#39634)

(cherry picked from commit b14886b227)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-01-30 15:43:50 +05:30
ruthra kumar
bc8ab51f9d Merge pull request #39633 from frappe/mergify/bp/version-15-hotfix/pr-39559
fix: prevent Return Invoices(Credit/Debit Note) from using a different account  (backport #39559)
2024-01-30 11:20:25 +05:30
ruthra kumar
5c9c3bee70 test: debit note account mismatch
(cherry picked from commit bdca718103)
2024-01-30 05:31:53 +00:00
ruthra kumar
c9ea83591f test: account mismatch validation
(cherry picked from commit 8bdc760733)
2024-01-30 05:31:53 +00:00
ruthra kumar
10fcf5af5f refactor: prevent '{debit/credit}_to' account mismatch
(cherry picked from commit 6f2fae1b61)
2024-01-30 05:31:52 +00:00
mergify[bot]
e4230cf6d9 feat: Partly billed status in Purchase Receipt (#39543)
feat: Partly billed status in Purchase Receipt (#39543)

(cherry picked from commit a673220feb)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2024-01-29 22:41:09 +05:30
mergify[bot]
53c705506b fix: not able to save BOM (duplicate key error) (backport #39620) (#39623)
fix: not able to save BOM (duplicate key error) (#39620)

(cherry picked from commit 4e182b89ce)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-01-29 21:22:34 +05:30
mergify[bot]
960443d268 fix: not able to submit subcontracting pr (old flow) (backport #39622) (#39625)
fix: not able to submit subcontracting pr (old flow) (#39622)

(cherry picked from commit 5cf47ae5f9)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-01-29 21:22:21 +05:30
Gursheen Kaur Anand
f71af209c0 Merge pull request #39618 from frappe/mergify/bp/version-15-hotfix/pr-39616
fix(minor): do not auto-populate item delivery date from qtn (backport #39616)
2024-01-29 16:53:39 +05:30
Gursheen Anand
6b20a7eed1 fix: qtn tests using delivery date
(cherry picked from commit 079cd30b9c)
2024-01-29 10:22:01 +00:00
Gursheen Anand
5cdc22adf3 fix: do not auto-populate item delivery date
(cherry picked from commit 49cb11c1f3)
2024-01-29 10:22:00 +00:00
mergify[bot]
9e36ab7de7 fix amount not updated when change rate in material request (backport #39606) (#39615)
fix amount not updated when change rate in material request (#39606)

* fix amount not updated when change rate in material request

* make code consistent

(cherry picked from commit efade9b9ae)

Co-authored-by: Jeffry Suryadharma <41689493+jeffrysurya@users.noreply.github.com>
2024-01-29 13:36:27 +05:30
Gursheen Kaur Anand
daafe45dd4 Merge pull request #39612 from frappe/mergify/bp/version-15-hotfix/pr-39562
fix: enqueue JV submission when > 100 accounts (backport #39562)
2024-01-29 11:55:58 +05:30
Gursheen Kaur Anand
706d1111ad Merge pull request #39610 from frappe/mergify/bp/version-15-hotfix/pr-39598
fix(minor): type error in financial statements for dashboard (backport #39598)
2024-01-29 11:42:17 +05:30
Gursheen Anand
da33079f12 fix: return doc obj after submit
(cherry picked from commit fc677811b7)
2024-01-29 06:10:03 +00:00
Gursheen Anand
07bcc24e35 fix: enqueue JV submission when more than 100 accounts
(cherry picked from commit 53b44ccf29)
2024-01-29 06:10:02 +00:00
Nabin Hait
0f0a5b73f6 Merge pull request #39566 from frappe/mergify/bp/version-15-hotfix/pr-39489
fix: fetch correct quantity and amount for grouped asset (backport #39489)
2024-01-29 11:38:33 +05:30
Gursheen Anand
0aa73e31ce fix: check page obj before adding menu
(cherry picked from commit 2486b646a1)
2024-01-29 06:07:13 +00:00
Frappe PR Bot
a3d4f63ba1 chore(release): Bumped to Version 15.10.8
## [15.10.8](https://github.com/frappe/erpnext/compare/v15.10.7...v15.10.8) (2024-01-29)

### Bug Fixes

* not able to save subcontracting purchase receipt (old flow) (backport [#39590](https://github.com/frappe/erpnext/issues/39590)) (backport [#39592](https://github.com/frappe/erpnext/issues/39592)) ([#39608](https://github.com/frappe/erpnext/issues/39608)) ([ff166e8](ff166e8104))
2024-01-29 04:39:51 +00:00
mergify[bot]
ff166e8104 fix: not able to save subcontracting purchase receipt (old flow) (backport #39590) (backport #39592) (#39608)
fix: not able to save subcontracting purchase receipt (old flow) (backport #39590) (#39592)

fix: not able to save subcontracting purchase receipt (old flow) (#39590)

(cherry picked from commit 67d828dab3)

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2024-01-29 10:08:40 +05:30
mergify[bot]
a118417645 refactor: provision to filter on dimensions in reconciliation tool (#39054)
* refactor: dimensions section in allocation table in reconciliation

(cherry picked from commit 1cde804c77)

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

* refactor: update dimension doctypes in hooks

(cherry picked from commit cfb3d87267)

* refactor: dimensions filter section in payment reconciliation

(cherry picked from commit 20e0acc20a)

* refactor: column break in dimension section

(cherry picked from commit 20576e0f47)

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

* refactor: handle dimension filters

(cherry picked from commit c1fe4bcc64)

* refactor: pass dimension filters to query

(cherry picked from commit ff60ec85b8)

* refactor: set query filters for dimensions

(cherry picked from commit ad8475cb8b)

* refactor: pass dimension details to query

(cherry picked from commit 5dc22e1811)

* refactor: replace sql with query builder for Jourals query

(cherry picked from commit 9c5a79209e)

* refactor: partial change on outstanding invoice popup

(cherry picked from commit 2154502955)

* fix: typo's and parameter changes

(cherry picked from commit 0ec17590ae)

* refactor: Credit Note and its Exc gain/loss JE inherits dimensions

(cherry picked from commit ab939cc6e8)

* refactor: apply dimension filters on cr/dr notes

(cherry picked from commit 188ff8cde7)

* chore: test dimension filter output

(cherry picked from commit e3c44231ab)

* test: dimension inheritance for cr note reconciliation

(cherry picked from commit ba5a7c8cd8)

* refactor: pass dimension values to Gain/Loss journal

(cherry picked from commit c44eb432a5)

# Conflicts:
#	erpnext/accounts/utils.py

* test: dimension inheritance in PE reconciliation

(cherry picked from commit 6148fb024b)

* refactor: pass dimensions on advance allocation

(cherry picked from commit cbd443a78a)

* test: dimension inheritance on adv allocation

(cherry picked from commit fcf4687c52)

* refactor: dynamic dimension filters in pop up

(cherry picked from commit f8bbb0619c)

* refactor: update dimensions, only if provided

(cherry picked from commit ec0f17ca8b)

* refactor: handle dynamic dimension in order query

(cherry picked from commit 7c2cb70387)

* chore: resolve conflicts

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2024-01-28 13:50:08 +05:30
mergify[bot]
a83f3106f3 feat: New financial views - Growth and margin views for P&L and balance sheet (#39588)
feat: New financial views - Growth and margin views for P&L and balance sheet (#39588)
2024-01-28 13:48:55 +05:30
mergify[bot]
6d4ca6a1ee fix: prevent extra transfer against inter transfer transaction (backport #39213) (#39596)
fix: prevent extra transfer against inter transfer transaction (#39213)

* fix: prevent extra transfer against inter transfer transaction

* fix: internal transfer dashboard

(cherry picked from commit 8fdc244e16)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-01-27 23:25:13 +05:30
ruthra kumar
ebec463166 Merge pull request #39594 from frappe/mergify/bp/version-15-hotfix/pr-39591
refactor: Do proper currency conversion on Future Payments column in AR/AP report (backport #39591)
2024-01-27 13:19:27 +05:30
ruthra kumar
10488520e7 test: future payment with foreign currency
(cherry picked from commit 7b37389115)
2024-01-27 07:23:17 +00:00
ruthra kumar
9e15a3c981 refactor: do currency conversion on future amount columns
(cherry picked from commit 0de4197c88)
2024-01-27 07:23:16 +00:00
mergify[bot]
63eef680d0 fix: not able to save subcontracting purchase receipt (old flow) (backport #39590) (#39592)
fix: not able to save subcontracting purchase receipt (old flow) (#39590)

(cherry picked from commit 67d828dab3)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-01-27 12:15:11 +05:30
mergify[bot]
7def475eb1 fix: incorrect active serial nos (backport #39389) (#39589)
fix: incorrect active serial nos

(cherry picked from commit 64cb1153de)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2024-01-27 11:57:58 +05:30
Frappe PR Bot
0e2f57ba89 chore(release): Bumped to Version 15.10.7
## [15.10.7](https://github.com/frappe/erpnext/compare/v15.10.6...v15.10.7) (2024-01-27)

### Bug Fixes

* incorrect amount in the material request item (backport [#39567](https://github.com/frappe/erpnext/issues/39567)) (backport [#39569](https://github.com/frappe/erpnext/issues/39569)) ([#39587](https://github.com/frappe/erpnext/issues/39587)) ([30bdba4](30bdba4801))
2024-01-27 04:43:59 +00:00
mergify[bot]
30bdba4801 fix: incorrect amount in the material request item (backport #39567) (backport #39569) (#39587)
fix: incorrect amount in the material request item (backport #39567) (#39569)

fix: incorrect amount in the material request item (#39567)

fix: incoorect amount in the material request
(cherry picked from commit 2bdfdeeb9a)

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2024-01-27 10:12:45 +05:30
mergify[bot]
ed06f0cd1b refactor: BOM creator item selector with short description (backport #39433) (#39435)
refactor: BOM creator item selector with short description

(cherry picked from commit 0f881bc90a)

Co-authored-by: Florian HENRY <florian.henry@open-concept.pro>
2024-01-27 09:51:14 +05:30
Gursheen Kaur Anand
9abc07e34b Merge pull request #39585 from frappe/mergify/bp/version-15-hotfix/pr-39532
fix: type error in transaction.js (backport #39532)
2024-01-26 23:46:57 +05:30
Gursheen Anand
a7d13686c7 fix: type error on company doc
(cherry picked from commit 030d35628d)
2024-01-26 17:31:07 +00:00
Deepesh Garg
ab99414242 Merge pull request #39577 from frappe/mergify/bp/version-15-hotfix/pr-39511
refactor(Sales Invoice): set account and sum for payments (#39511)
2024-01-26 10:04:23 +05:30
Deepesh Garg
fa8fb7a49e Merge pull request #39575 from frappe/mergify/bp/version-15-hotfix/pr-39108
feat: In words in payment entry (#39108)
2024-01-26 09:46:01 +05:30
barredterra
4fc3a264f9 refactor(Sales Invoice): set account for mode of payment
(cherry picked from commit 3815f07c33)
2024-01-26 04:15:14 +00:00
Deepesh Garg
5df2f5d523 feat: In words in payment entry
(cherry picked from commit b21da472f6)
2024-01-26 04:01:45 +00:00
mergify[bot]
e0c35d60b4 fix: incorrect amount in the material request item (backport #39567) (#39569)
fix: incorrect amount in the material request item (#39567)

fix: incoorect amount in the material request
(cherry picked from commit 2bdfdeeb9a)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-01-25 17:22:31 +05:30
Gursheen Kaur Anand
14aa9d3d0f Merge pull request #39564 from frappe/mergify/bp/version-15-hotfix/pr-39557
fix: make SO item code mandatory (backport #39557)
2024-01-25 16:39:39 +05:30
Nabin Hait
3bdd1e9514 fix: fetch correct quantity and amount for grouped asset
(cherry picked from commit 06f48c678b)
2024-01-25 10:51:53 +00:00
Gursheen Anand
171586db9b fix: make SO item code reqd
(cherry picked from commit 7f8303a493)
2024-01-25 10:50:38 +00:00
ruthra kumar
c3557a87ef Merge pull request #39552 from frappe/mergify/bp/version-15-hotfix/pr-38218
fix: honour currency precision while fetching balance (backport #38218)
2024-01-25 13:25:15 +05:30
mergify[bot]
4853ab1fec fix: default enable closing stock balance (backport #39551) (#39554)
fix: default enable closing stock balance (#39551)

(cherry picked from commit d1fb90edff)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-01-25 12:51:55 +05:30
Dany Robert
ed032b3224 chore: avoid explicit escaping for precision
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
(cherry picked from commit a8949174c8)
2024-01-25 07:00:45 +00:00
Dany Robert
8b7900e6a7 chore: change f-string to sql params
(cherry picked from commit 383a4b132e)
2024-01-25 07:00:45 +00:00
Dany Robert
e0a38207f6 fix: honour currency precision while fetching balance
(cherry picked from commit bfaa93b0ca)
2024-01-25 07:00:45 +00:00
rohitwaghchaure
e08c7bad63 Merge pull request #39549 from frappe/mergify/bp/version-15-hotfix/pr-39547
fix: Item Tax template is not working for e-commerce (backport #39547)
2024-01-25 11:53:30 +05:30
Rohit Waghchaure
2905db19e5 fix: Item Tax template is not working for e-commerce
(cherry picked from commit 7d3240ae3a)
2024-01-25 06:07:23 +00:00
ruthra kumar
bd7e8ed230 Merge pull request #39538 from frappe/mergify/bp/version-15-hotfix/pr-39331
fix: Payment Terms Status for Sales Order report show all payment terms from orders (not only when there is a payment terms template) (backport #39331)
2024-01-24 16:27:29 +05:30
Florian HENRY
b2e0d24954 fix: Payment Terms Status for Sales Order report should show all payment terms from order not only this comming from template
(cherry picked from commit 6c8f52b26f)
2024-01-24 10:43:08 +00:00
rohitwaghchaure
84a94cec6b Merge pull request #39531 from frappe/mergify/bp/version-15-hotfix/pr-39525
fix: email list for auto reorder material request (backport #39525)
2024-01-24 14:49:28 +05:30
ruthra kumar
871dc6b0da Merge pull request #39533 from frappe/mergify/bp/version-15-hotfix/pr-39529
fix: AttributeError in company transaction deletion (backport #39529)
2024-01-24 13:28:22 +05:30
ruthra kumar
222005ca44 fix: AttributeError in company transaction deletion
(cherry picked from commit b127aa308e)
2024-01-24 07:40:59 +00:00
Rohit Waghchaure
98c56535d8 fix: email list for auto reorder material request
(cherry picked from commit 764f3422a0)
2024-01-24 07:07:16 +00:00
rohitwaghchaure
f6a779e1aa Merge pull request #39527 from frappe/mergify/bp/version-15-hotfix/pr-39521
fix: not able to edit address through portal (backport #39521)
2024-01-24 12:17:05 +05:30
Rohit Waghchaure
1f5dbebe68 fix: not able to edit address through portal
(cherry picked from commit b046d980ad)
2024-01-24 06:20:31 +00:00
Gursheen Kaur Anand
4073dfb90b Merge pull request #39522 from frappe/mergify/bp/version-15-hotfix/pr-39493
fix: skip setting liability account for internal transfer (backport #39493)
2024-01-24 11:08:53 +05:30
Gursheen Anand
e2d85c513c fix: skip liability account for internal transfer
(cherry picked from commit 236b73565e)
2024-01-24 05:04:45 +00:00
rohitwaghchaure
880f648846 Merge pull request #39513 from frappe/mergify/bp/version-15-hotfix/pr-39503
fix: Serial No Ledger permission issue (backport #39503)
2024-01-24 10:27:23 +05:30
Raffael Meyer
84d2a80f30 Merge pull request #39516 from frappe/mergify/bp/version-15-hotfix/pr-39512 2024-01-23 19:00:53 +01:00
barredterra
fe41e2a341 refactor(Batch): use const instead of var
(cherry picked from commit 7a7a213285)
2024-01-23 16:55:27 +00:00
barredterra
a97b8fd876 fix(Batch): reload doc after splitting
to show updated qty

(cherry picked from commit 34ec2f8a2b)
2024-01-23 16:55:27 +00:00
barredterra
6ed42e939b refactor: split batch
(cherry picked from commit 3c7e7a76f0)
2024-01-23 16:55:26 +00:00
Rohit Waghchaure
f9d1995954 fix: Serial No Ledger permission issue
(cherry picked from commit 1a670ff266)
2024-01-23 13:37:35 +00:00
167 changed files with 4898 additions and 1758 deletions

View File

@@ -3,7 +3,7 @@ import inspect
import frappe
__version__ = "15.10.6"
__version__ = "15.12.1"
def get_default_company(user=None):

View File

@@ -118,6 +118,7 @@ class Account(NestedSet):
self.validate_balance_must_be_debit_or_credit()
self.validate_account_currency()
self.validate_root_company_and_sync_account_to_children()
self.validate_receivable_payable_account_type()
def validate_parent_child_account_type(self):
if self.parent_account:
@@ -188,6 +189,24 @@ class Account(NestedSet):
"Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
)
def validate_receivable_payable_account_type(self):
doc_before_save = self.get_doc_before_save()
receivable_payable_types = ["Receivable", "Payable"]
if (
doc_before_save
and doc_before_save.account_type in receivable_payable_types
and doc_before_save.account_type != self.account_type
):
# check for ledger entries
if frappe.db.get_all("GL Entry", filters={"account": self.name, "is_cancelled": 0}, limit=1):
msg = _(
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
).format(
frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
)
frappe.msgprint(msg)
self.add_comment("Comment", msg)
def validate_root_details(self):
doc_before_save = self.get_doc_before_save()

View File

@@ -6,6 +6,7 @@ import unittest
import frappe
from frappe.test_runner import make_test_records
from frappe.utils import nowdate
from erpnext.accounts.doctype.account.account import (
InvalidAccountMergeError,
@@ -324,6 +325,19 @@ class TestAccount(unittest.TestCase):
acc.account_currency = "USD"
self.assertRaises(frappe.ValidationError, acc.save)
def test_account_balance(self):
from erpnext.accounts.utils import get_balance_on
if not frappe.db.exists("Account", "Test Percent Account %5 - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Test Percent Account %5"
acc.parent_account = "Tax Assets - _TC"
acc.company = "_Test Company"
acc.insert()
balance = get_balance_on(account="Test Percent Account %5 - _TC", date=nowdate())
self.assertEqual(balance, 0)
def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"creation": "2013-06-24 15:49:57",
"description": "Settings for Accounts",
"doctype": "DocType",
"document_type": "Other",
"editable_grid": 1,
@@ -462,7 +461,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-11-20 09:37:47.650347",
"modified": "2024-01-30 14:04:26.553554",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -9,6 +9,7 @@ from frappe.contacts.address_and_contact import (
load_address_and_contact,
)
from frappe.model.document import Document
from frappe.utils import comma_and, get_link_to_form
class BankAccount(Document):
@@ -52,6 +53,17 @@ class BankAccount(Document):
def validate(self):
self.validate_company()
self.validate_iban()
self.validate_account()
def validate_account(self):
if self.account:
if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
)
def validate_company(self):
if self.is_company_account and not self.company:

View File

@@ -5,7 +5,9 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate
from pypika import Order
import erpnext
@@ -179,39 +181,62 @@ def get_payment_entries_for_bank_clearance(
pos_sales_invoices, pos_purchase_invoices = [], []
if include_pos_transactions:
pos_sales_invoices = frappe.db.sql(
"""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.customer as against_account, sip.clearance_date,
account.account_currency, 0 as credit
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
where
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by
si.posting_date ASC, si.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
si_payment = frappe.qb.DocType("Sales Invoice Payment")
si = frappe.qb.DocType("Sales Invoice")
acc = frappe.qb.DocType("Account")
pos_purchase_invoices = frappe.db.sql(
"""
select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
account.account_currency, 0 as debit
from `tabPurchase Invoice` pi, `tabAccount` account
where
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by
pi.posting_date ASC, pi.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
pos_sales_invoices = (
frappe.qb.from_(si_payment)
.inner_join(si)
.on(si_payment.parent == si.name)
.inner_join(acc)
.on(si_payment.account == acc.name)
.select(
ConstantColumn("Sales Invoice").as_("payment_document"),
si.name.as_("payment_entry"),
si_payment.reference_no.as_("cheque_number"),
si_payment.amount.as_("debit"),
si.posting_date,
si.customer.as_("against_account"),
si_payment.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("credit"),
)
.where(
(si.docstatus == 1)
& (si_payment.account == account)
& (si.posting_date >= from_date)
& (si.posting_date <= to_date)
)
.orderby(si.posting_date)
.orderby(si.name, order=Order.desc)
).run(as_dict=True)
pi = frappe.qb.DocType("Purchase Invoice")
pos_purchase_invoices = (
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.paid_amount.as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("debit"),
)
.where(
(pi.docstatus == 1)
& (pi.cash_bank_account == account)
& (pi.posting_date >= from_date)
& (pi.posting_date <= to_date)
)
.orderby(pi.posting_date)
.orderby(pi.name, order=Order.desc)
).run(as_dict=True)
entries = (
list(payment_entries)

View File

@@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase):
frappe.db.delete(dt)
clear_loan_transactions()
make_pos_profile()
add_transactions()
add_vouchers()
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
uniq_identifier = frappe.generate_hash(length=10)
gl_account = create_gl_account("_Test Bank " + uniq_identifier)
bank_account = create_bank_account(
gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
)
add_transactions(bank_account=bank_account)
add_vouchers(gl_account=gl_account)
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
@@ -219,7 +227,9 @@ def clear_loan_transactions():
frappe.db.delete("Loan Repayment")
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
def create_bank_account(
bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account"
):
try:
frappe.get_doc(
{
@@ -231,21 +241,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
pass
try:
frappe.get_doc(
bank_account = frappe.get_doc(
{
"doctype": "Bank Account",
"account_name": "Checking Account",
"account_name": bank_account_name,
"bank": bank_name,
"account": account_name,
"account": gl_account,
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
return bank_account.name
def add_transactions():
create_bank_account()
def create_gl_account(gl_account_name="_Test Bank - _TC"):
gl_account = frappe.get_doc(
{
"doctype": "Account",
"company": "_Test Company",
"parent_account": "Current Assets - _TC",
"account_type": "Bank",
"is_group": 0,
"account_name": gl_account_name,
}
).insert()
return gl_account.name
def add_transactions(bank_account="_Test Bank - _TC"):
doc = frappe.get_doc(
{
"doctype": "Bank Transaction",
@@ -253,7 +277,7 @@ def add_transactions():
"date": "2018-10-23",
"deposit": 1200,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank",
"bank_account": bank_account,
}
).insert()
doc.submit()
@@ -265,7 +289,7 @@ def add_transactions():
"date": "2018-10-23",
"deposit": 1700,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank",
"bank_account": bank_account,
}
).insert()
doc.submit()
@@ -277,7 +301,7 @@ def add_transactions():
"date": "2018-10-26",
"withdrawal": 690,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank",
"bank_account": bank_account,
}
).insert()
doc.submit()
@@ -289,7 +313,7 @@ def add_transactions():
"date": "2018-10-27",
"deposit": 3900,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank",
"bank_account": bank_account,
}
).insert()
doc.submit()
@@ -301,13 +325,13 @@ def add_transactions():
"date": "2018-10-27",
"withdrawal": 109080,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank",
"bank_account": bank_account,
}
).insert()
doc.submit()
def add_vouchers():
def add_vouchers(gl_account="_Test Bank - _TC"):
try:
frappe.get_doc(
{
@@ -323,7 +347,7 @@ def add_vouchers():
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
@@ -342,14 +366,14 @@ def add_vouchers():
pass
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Herr G Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Herr G Nov 18"
pe.reference_date = "2018-11-01"
pe.insert()
@@ -380,10 +404,10 @@ def add_vouchers():
pass
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
pi.cash_bank_account = "_Test Bank - _TC"
pi.cash_bank_account = gl_account
pi.insert()
pi.submit()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.paid_amount = 690
@@ -392,7 +416,7 @@ def add_vouchers():
pe.submit()
si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account)
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.insert()
@@ -415,16 +439,12 @@ def add_vouchers():
if not frappe.db.get_value(
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
):
mode_of_payment.append(
"accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
)
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1
si.append(
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
)
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
si.insert()
si.submit()

View File

@@ -3,7 +3,7 @@
"allow_import": 1,
"autoname": "field:year",
"creation": "2013-01-22 16:50:25",
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
"description": "Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -82,10 +82,11 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2020-11-05 12:16:53.081573",
"modified": "2024-01-30 12:35:38.645968",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -122,5 +123,6 @@
],
"show_name_in_global_search": 1,
"sort_field": "name",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@@ -13,16 +13,9 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.exceptions import (
InvalidAccountCurrency,
InvalidAccountDimensionError,
MandatoryAccountDimensionError,
)
from erpnext.exceptions import InvalidAccountCurrency
exclude_from_linked_with = True
@@ -98,7 +91,6 @@ class GLEntry(Document):
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
@@ -208,42 +200,6 @@ class GLEntry(Document):
)
)
def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map()
for key, value in dimension_filter_map.items():
dimension = key[0]
account = key[1]
if self.account == account:
if value["is_mandatory"] and not self.get(dimension):
frappe.throw(
_("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
),
MandatoryAccountDimensionError,
)
if value["allow_or_restrict"] == "Allow":
if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
else:
if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
def check_pl_account(self):
if (
self.is_opening == "Yes"

View File

@@ -150,6 +150,20 @@ class JournalEntry(AccountsController):
if not self.title:
self.title = self.get_title()
def submit(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
self.queue_action("submit", timeout=4600)
else:
return self._submit()
def cancel(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
self.queue_action("cancel", timeout=4600)
else:
return self._cancel()
def on_submit(self):
self.validate_cheque_info()
self.check_credit_limit()

View File

@@ -1,173 +1,77 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:distribution_id",
"beta": 0,
"creation": "2013-01-10 16:34:05",
"custom": 0,
"description": "**Monthly Distribution** helps you distribute the Budget/Target across months if you have seasonality in your business.",
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB",
"actions": [],
"autoname": "field:distribution_id",
"creation": "2013-01-10 16:34:05",
"description": "Helps you distribute the Budget/Target across months if you have seasonality in your business.",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"distribution_id",
"fiscal_year",
"percentages"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Name of the Monthly Distribution",
"fieldname": "distribution_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Distribution Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "distribution_id",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Name of the Monthly Distribution",
"fieldname": "distribution_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Distribution Name",
"oldfieldname": "distribution_id",
"oldfieldtype": "Data",
"reqd": 1,
"unique": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "fiscal_year",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Fiscal Year",
"length": 0,
"no_copy": 0,
"oldfieldname": "fiscal_year",
"oldfieldtype": "Select",
"options": "Fiscal Year",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0
},
"fieldname": "fiscal_year",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Fiscal Year",
"oldfieldname": "fiscal_year",
"oldfieldtype": "Select",
"options": "Fiscal Year",
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "percentages",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Monthly Distribution Percentages",
"length": 0,
"no_copy": 0,
"oldfieldname": "budget_distribution_details",
"oldfieldtype": "Table",
"options": "Monthly Distribution Percentage",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "percentages",
"fieldtype": "Table",
"label": "Monthly Distribution Percentages",
"oldfieldname": "budget_distribution_details",
"oldfieldtype": "Table",
"options": "Monthly Distribution Percentage"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-bar-chart",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-11-21 14:54:35.998761",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Monthly Distribution",
"name_case": "Title Case",
"owner": "Administrator",
],
"icon": "fa fa-bar-chart",
"idx": 1,
"links": [],
"modified": "2024-01-30 13:57:55.802744",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Monthly Distribution",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 2,
"print": 0,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
"permlevel": 2,
"read": 1,
"report": 1,
"role": "Accounts Manager"
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -640,7 +640,7 @@ frappe.ui.form.on('Payment Entry', {
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
const today = frappe.datetime.get_today();
const fields = [
let fields = [
{fieldtype:"Section Break", label: __("Posting Date")},
{fieldtype:"Date", label: __("From Date"),
fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)},
@@ -655,18 +655,29 @@ frappe.ui.form.on('Payment Entry', {
fieldname:"outstanding_amt_greater_than", default: 0},
{fieldtype:"Column Break"},
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"},
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
}
];
if (frm.dimension_filters) {
let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2);
fields.push({fieldtype:"Section Break"});
frm.dimension_filters.map((elem, idx)=>{
fields.push({
fieldtype: "Link",
label: elem.document_type == "Cost Center" ? "Cost Center" : elem.label,
options: elem.document_type,
fieldname: elem.fieldname || elem.document_type
});
if(idx+1 == column_break_insertion_point) {
fields.push({fieldtype:"Column Break"});
}
},
{fieldtype:"Column Break"},
});
}
fields = fields.concat([
{fieldtype:"Section Break"},
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
];
]);
let btn_text = "";

View File

@@ -87,12 +87,14 @@
"status",
"custom_remarks",
"remarks",
"base_in_words",
"column_break_16",
"letter_head",
"print_heading",
"bank",
"bank_account_no",
"payment_order",
"in_words",
"subscription_section",
"auto_repeat",
"amended_from",
@@ -746,6 +748,20 @@
"hidden": 1,
"label": "Book Advance Payments in Separate Party Account",
"read_only": 1
},
{
"fieldname": "base_in_words",
"fieldtype": "Small Text",
"label": "In Words (Company Currency)",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "in_words",
"fieldtype": "Small Text",
"label": "In Words",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
@@ -759,7 +775,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2023-11-23 12:07:20.887885",
"modified": "2024-01-03 12:46:41.759121",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -13,6 +13,7 @@ from pypika import Case
from pypika.functions import Coalesce, Sum
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.bank_account.bank_account import (
get_bank_account_details,
get_party_bank_account,
@@ -95,6 +96,7 @@ class PaymentEntry(AccountsController):
self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked()
self.set_status()
self.set_total_in_words()
def on_submit(self):
if self.difference_amount:
@@ -107,7 +109,7 @@ class PaymentEntry(AccountsController):
def set_liability_account(self):
# Auto setting liability account should only be done during 'draft' status
if self.docstatus > 0:
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
return
if not frappe.db.get_value(
@@ -702,6 +704,21 @@ class PaymentEntry(AccountsController):
self.db_set("status", self.status, update_modified=True)
def set_total_in_words(self):
from frappe.utils import money_in_words
if self.payment_type in ("Pay", "Internal Transfer"):
base_amount = abs(self.base_paid_amount)
amount = abs(self.paid_amount)
currency = self.paid_from_account_currency
elif self.payment_type == "Receive":
base_amount = abs(self.base_received_amount)
amount = abs(self.received_amount)
currency = self.paid_to_account_currency
self.base_in_words = money_in_words(base_amount, self.company_currency)
self.in_words = money_in_words(amount, currency)
def set_tax_withholding(self):
if not self.party_type == "Supplier":
return
@@ -929,19 +946,19 @@ class PaymentEntry(AccountsController):
)
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
if self.payment_type == "Receive":
self.difference_amount = base_party_amount - self.base_received_amount
elif self.payment_type == "Pay":
self.difference_amount = self.base_paid_amount - base_party_amount
else:
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
if self.payment_type == "Receive":
self.difference_amount = base_party_amount - self.base_received_amount + included_taxes
elif self.payment_type == "Pay":
self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes
else:
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
self.difference_amount = flt(
self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")
self.difference_amount - total_deductions, self.precision("difference_amount")
)
def get_included_taxes(self):
@@ -1588,6 +1605,13 @@ def get_outstanding_reference_documents(args, validate=False):
condition += " and cost_center='%s'" % args.get("cost_center")
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if args.get(dim.fieldname):
condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname))
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"],
"due_date": ["from_due_date", "to_due_date"],
@@ -1821,6 +1845,12 @@ def get_orders_to_be_billed(
if doc and hasattr(doc, "cost_center") and doc.cost_center:
condition = " and cost_center='%s'" % cost_center
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname))
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"
rounded_total_field = "base_rounded_total"

View File

@@ -4,9 +4,13 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import getdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
create_bank_account,
create_gl_account,
)
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_payment_entry,
make_payment_order,
@@ -14,28 +18,32 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
class TestPaymentOrder(unittest.TestCase):
class TestPaymentOrder(FrappeTestCase):
def setUp(self):
create_bank_account()
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
uniq_identifier = frappe.generate_hash(length=10)
self.gl_account = create_gl_account("_Test Bank " + uniq_identifier)
self.bank_account = create_bank_account(
gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier
)
def tearDown(self):
for bt in frappe.get_all("Payment Order"):
doc = frappe.get_doc("Payment Order", bt.name)
doc.cancel()
doc.delete()
frappe.db.rollback()
def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice()
payment_entry = get_payment_entry(
"Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC"
"Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account
)
payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate()
payment_entry.party_bank_account = "Checking Account - Citi Bank"
payment_entry.party_bank_account = self.bank_account
payment_entry.insert()
payment_entry.submit()
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
doc = create_payment_order_against_payment_entry(
payment_entry, "Payment Entry", self.bank_account
)
reference_doc = doc.get("references")[0]
self.assertEqual(reference_doc.reference_name, payment_entry.name)
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
@@ -43,13 +51,13 @@ class TestPaymentOrder(unittest.TestCase):
self.assertEqual(reference_doc.amount, 250)
def create_payment_order_against_payment_entry(ref_doc, order_type):
def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account):
payment_order = frappe.get_doc(
dict(
doctype="Payment Order",
company="_Test Company",
payment_order_type=order_type,
company_bank_account="Checking Account - Citi Bank",
company_bank_account=bank_account,
)
)
doc = make_payment_order(ref_doc.name, payment_order)

View File

@@ -95,6 +95,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.change_custom_button_type(__('Allocate'), null, 'default');
}
this.frm.trigger("set_query_for_dimension_filters");
// check for any running reconciliation jobs
if (this.frm.doc.receivable_payable_account) {
this.frm.call({
@@ -125,6 +127,25 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
}
}
set_query_for_dimension_filters() {
frappe.call({
method: "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.get_queries_for_dimension_filters",
args: {
company: this.frm.doc.company,
},
callback: (r) => {
if (!r.exc && r.message) {
r.message.forEach(x => {
this.frm.set_query(x.fieldname, () => {
return {
'filters': x.filters
};
});
});
}
}
});
}
company() {
this.frm.set_value('party', '');

View File

@@ -25,7 +25,9 @@
"invoice_limit",
"payment_limit",
"bank_cash_account",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"sec_break1",
"invoice_name",
"invoices",
@@ -208,6 +210,18 @@
"fieldname": "payment_name",
"fieldtype": "Data",
"label": "Filter on Payment"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.invoices.length == 0",
"depends_on": "eval:doc.receivable_payable_account",
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions Filter"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
@@ -215,7 +229,7 @@
"is_virtual": 1,
"issingle": 1,
"links": [],
"modified": "2023-11-17 17:33:55.701726",
"modified": "2023-12-14 13:38:16.264013",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",

View File

@@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
is_any_doc_running,
)
@@ -70,6 +71,7 @@ class PaymentReconciliation(Document):
self.common_filter_conditions = []
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
self.dimensions = get_dimensions()[0]
def load_from_db(self):
# 'modified' attribute is required for `run_doc_method` to work properly.
@@ -172,6 +174,14 @@ class PaymentReconciliation(Document):
if self.payment_name:
condition.update({"name": self.payment_name})
# pass dynamic dimension filter values to query builder
dimensions = {}
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
dimensions.update({dimension: self.get(dimension)})
condition.update({"accounting_dimensions": dimensions})
payment_entries = get_advance_payment_entries_for_regional(
self.party_type,
self.party,
@@ -185,66 +195,67 @@ class PaymentReconciliation(Document):
return payment_entries
def get_jv_entries(self):
condition = self.get_conditions()
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
conditions = self.get_journal_filter_conditions()
# Dimension filters
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
conditions.append(jea[dimension] == self.get(dimension))
if self.payment_name:
condition += f" and t1.name like '%%{self.payment_name}%%'"
conditions.append(je.name.like(f"%%{self.payment_name}%%"))
if self.get("cost_center"):
condition += f" and t2.cost_center = '{self.cost_center}' "
conditions.append(jea.cost_center == self.cost_center)
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
conditions.append(jea[dr_or_cr].gt(0))
bank_account_condition = (
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
if self.bank_cash_account:
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
journal_query = (
qb.from_(je)
.inner_join(jea)
.on(jea.parent == je.name)
.select(
ConstantColumn("Journal Entry").as_("reference_type"),
je.name.as_("reference_name"),
je.posting_date,
je.remark.as_("remarks"),
jea.name.as_("reference_row"),
jea[dr_or_cr].as_("amount"),
jea.is_advance,
jea.exchange_rate,
jea.account_currency.as_("currency"),
jea.cost_center.as_("cost_center"),
)
.where(
(je.docstatus == 1)
& (jea.party_type == self.party_type)
& (jea.party == self.party)
& (jea.account == self.receivable_payable_account)
& (
(jea.reference_type == "")
| (jea.reference_type.isnull())
| (jea.reference_type.isin(("Sales Order", "Purchase Order")))
)
)
.where(Criterion.all(conditions))
.orderby(je.posting_date)
)
limit = f"limit {self.payment_limit}" if self.payment_limit else " "
if self.payment_limit:
journal_query = journal_query.limit(self.payment_limit)
# nosemgrep
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as reference_type, t1.name as reference_name,
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
t2.account_currency as currency, t2.cost_center as cost_center
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1
and t2.party_type = %(party_type)s and t2.party = %(party)s
and t2.account = %(account)s and {dr_or_cr} > 0 {condition}
and (t2.reference_type is null or t2.reference_type = '' or
(t2.reference_type in ('Sales Order', 'Purchase Order')
and t2.reference_name is not null and t2.reference_name != ''))
and (CASE
WHEN t1.voucher_type in ('Debit Note', 'Credit Note')
THEN 1=1
ELSE {bank_account_condition}
END)
order by t1.posting_date
{limit}
""".format(
**{
"dr_or_cr": dr_or_cr,
"bank_account_condition": bank_account_condition,
"condition": condition,
"limit": limit,
}
),
{
"party_type": self.party_type,
"party": self.party,
"account": self.receivable_payable_account,
"bank_cash_account": "%%%s%%" % self.bank_cash_account,
},
as_dict=1,
)
journal_entries = journal_query.run(as_dict=True)
return list(journal_entries)
@@ -298,6 +309,7 @@ class PaymentReconciliation(Document):
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
get_payments=True,
accounting_dimensions=self.accounting_dimension_filter_conditions,
)
for inv in return_outstanding:
@@ -446,8 +458,15 @@ class PaymentReconciliation(Document):
row = self.append("allocation", {})
row.update(entry)
def update_dimension_values_in_allocated_entries(self, res):
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
res[dimension] = self.get(dimension)
return res
def get_allocated_entry(self, pay, inv, allocated_amount):
return frappe._dict(
res = frappe._dict(
{
"reference_type": pay.get("reference_type"),
"reference_name": pay.get("reference_name"),
@@ -463,6 +482,9 @@ class PaymentReconciliation(Document):
}
)
res = self.update_dimension_values_in_allocated_entries(res)
return res
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
adjust_allocations_for_taxes(self)
dr_or_cr = (
@@ -485,10 +507,10 @@ class PaymentReconciliation(Document):
reconciled_entry.append(payment_details)
if entry_list:
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
reconcile_against_document(entry_list, skip_ref_details_update_for_pe, self.dimensions)
if dr_or_cr_notes:
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions)
@frappe.whitelist()
def reconcile(self):
@@ -517,7 +539,7 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries()
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
payment_details = frappe._dict(
{
"voucher_type": row.get("reference_type"),
"voucher_no": row.get("reference_name"),
@@ -539,6 +561,12 @@ class PaymentReconciliation(Document):
}
)
for x in self.dimensions:
if row.get(x.fieldname):
payment_details[x.fieldname] = row.get(x.fieldname)
return payment_details
def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
if not self.get(fieldname):
@@ -646,6 +674,13 @@ class PaymentReconciliation(Document):
if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table"))
def build_dimensions_filter_conditions(self):
ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear()
self.accounting_dimension_filter_conditions.clear()
@@ -669,40 +704,30 @@ class PaymentReconciliation(Document):
if self.to_payment_date:
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
def get_conditions(self, get_payments=False):
condition = " and company = '{0}' ".format(self.company)
self.build_dimensions_filter_conditions()
if self.get("cost_center") and get_payments:
condition = " and cost_center = '{0}' ".format(self.cost_center)
def get_journal_filter_conditions(self):
conditions = []
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
conditions.append(je.company == self.company)
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
else ""
)
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
if self.from_payment_date:
conditions.append(je.posting_date.gte(self.from_payment_date))
if self.to_payment_date:
conditions.append(je.posting_date.lte(self.to_payment_date))
if self.minimum_payment_amount:
condition += (
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
)
conditions.append(je.total_debit.gte(self.minimum_payment_amount))
if self.maximum_payment_amount:
condition += (
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
)
conditions.append(je.total_debit.lte(self.maximum_payment_amount))
return condition
return conditions
def reconcile_dr_cr_note(dr_cr_notes, company):
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
@@ -752,6 +777,15 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
}
)
# Credit Note(JE) will inherit the same dimension values as payment
dimensions_dict = frappe._dict()
if active_dimensions:
for dim in active_dimensions:
dimensions_dict[dim.fieldname] = inv.get(dim.fieldname)
jv.accounts[0].update(dimensions_dict)
jv.accounts[1].update(dimensions_dict)
jv.flags.ignore_mandatory = True
jv.flags.ignore_exchange_rate = True
jv.remark = None
@@ -785,9 +819,27 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
inv.against_voucher,
None,
inv.cost_center,
dimensions_dict,
)
@erpnext.allow_regional
def adjust_allocations_for_taxes(doc):
pass
@frappe.whitelist()
def get_queries_for_dimension_filters(company: str = None):
dimensions_with_filters = []
for d in get_dimensions()[0]:
filters = {}
meta = frappe.get_meta(d.document_type)
if meta.has_field("company") and company:
filters.update({"company": company})
if meta.is_tree:
filters.update({"is_group": 0})
dimensions_with_filters.append({"fieldname": d.fieldname, "filters": filters})
return dimensions_with_filters

View File

@@ -23,7 +23,9 @@
"difference_account",
"exchange_rate",
"currency",
"cost_center"
"accounting_dimensions_section",
"cost_center",
"dimension_col_break"
],
"fields": [
{
@@ -151,12 +153,26 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "gain_loss_posting_date",
"fieldtype": "Date",
"label": "Difference Posting Date"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
}
],
"is_virtual": 1,
"istable": 1,
"links": [],
"modified": "2023-11-17 17:33:38.612615",
"modified": "2023-12-14 13:38:26.104150",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",

View File

@@ -11,7 +11,6 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_lo
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
SalesInvoice,
get_bank_cash_account,
get_mode_of_payment_info,
update_multi_mode_option,
)
@@ -208,7 +207,6 @@ class POSInvoice(SalesInvoice):
self.validate_stock_availablility()
self.validate_return_items_qty()
self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos()
self.validate_payment_amount()
self.validate_loyalty_transaction()
@@ -643,11 +641,6 @@ class POSInvoice(SalesInvoice):
update_multi_mode_option(self, pos_profile)
self.paid_amount = 0
def set_account_for_mode_of_payment(self):
for pay in self.payments:
if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
@frappe.whitelist()
def create_payment_request(self):
for pay in self.payments:

View File

@@ -80,13 +80,16 @@
"target_warehouse",
"quality_inspection",
"serial_and_batch_bundle",
"batch_no",
"use_serial_batch_fields",
"col_break5",
"allow_zero_valuation_rate",
"serial_no",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
"section_break_tlhi",
"serial_no",
"column_break_ciit",
"batch_no",
"edit_references",
"sales_order",
"so_detail",
@@ -628,13 +631,13 @@
"options": "Quality Inspection"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch",
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"fieldname": "col_break5",
@@ -649,14 +652,14 @@
"print_hide": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 1,
"in_list_view": 1,
"label": "Serial No",
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text",
"read_only": 1
"oldfieldtype": "Small Text"
},
{
"fieldname": "item_tax_rate",
@@ -824,17 +827,33 @@
"read_only": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"default": "0",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "section_break_tlhi",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ciit",
"fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
"modified": "2023-11-14 18:33:22.585715",
"modified": "2024-02-04 16:36:25.665743",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -82,6 +82,7 @@ class POSInvoiceItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None

View File

@@ -579,12 +579,17 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
item_details[field] += pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0)
@frappe.whitelist()
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
get_pricing_rule_items,
)
if isinstance(item_details, str):
item_details = json.loads(item_details)
item_details = frappe._dict(item_details)
for d in get_applied_pricing_rules(pricing_rules):
if not d or not frappe.db.exists("Pricing Rule", d):
continue

View File

@@ -120,18 +120,6 @@ def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {}
ageing = ""
err_journals = None
if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
err_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": doc.company,
"docstatus": 1,
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
},
as_list=True,
)
for entry in doc.customers:
if doc.include_ageing:
ageing = set_ageing(doc, entry)
@@ -144,8 +132,8 @@ def get_statement_dict(doc, get_statement_dict=False):
)
filters = get_common_filters(doc)
if err_journals:
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if doc.ignore_exchange_rate_revaluation_journals:
filters.update({"ignore_err": True})
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))

View File

@@ -696,6 +696,7 @@ class PurchaseInvoice(BuyingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
if self.is_old_subcontracting_flow:

View File

@@ -1985,6 +1985,21 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
def test_debit_note_with_account_mismatch(self):
new_creditors = create_account(
parent_account="Accounts Payable - _TC",
account_name="Creditors 2",
company="_Test Company",
account_type="Payable",
)
pi = make_purchase_invoice(qty=1, rate=1000)
dr_note = make_purchase_invoice(
qty=-1, rate=1000, is_return=1, return_against=pi.name, do_not_save=True
)
dr_note.credit_to = new_creditors
self.assertRaises(frappe.ValidationError, dr_note.save)
def test_debit_note_without_item(self):
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
pi.items[0].item_code = ""

View File

@@ -62,16 +62,19 @@
"rm_supp_cost",
"warehouse_section",
"warehouse",
"from_warehouse",
"quality_inspection",
"add_serial_batch_bundle",
"serial_and_batch_bundle",
"serial_no",
"use_serial_batch_fields",
"col_br_wh",
"from_warehouse",
"quality_inspection",
"rejected_warehouse",
"rejected_serial_and_batch_bundle",
"batch_no",
"section_break_rqbe",
"serial_no",
"rejected_serial_no",
"column_break_vbbb",
"batch_no",
"manufacture_details",
"manufacturer",
"column_break_13",
@@ -440,13 +443,11 @@
"print_hide": 1
},
{
"depends_on": "eval:!doc.is_fixed_asset",
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch",
"read_only": 1,
"search_index": 1
},
{
@@ -454,21 +455,18 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.is_fixed_asset",
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
"hidden": 1,
"label": "Serial No",
"read_only": 1
"label": "Serial No"
},
{
"depends_on": "eval:!doc.is_fixed_asset",
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "rejected_serial_no",
"fieldtype": "Text",
"label": "Rejected Serial No",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"fieldname": "accounting",
@@ -891,7 +889,7 @@
"label": "Apply TDS"
},
{
"depends_on": "eval:parent.update_stock == 1",
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -901,7 +899,7 @@
"search_index": 1
},
{
"depends_on": "eval:parent.update_stock == 1",
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -916,16 +914,31 @@
"options": "Asset"
},
{
"depends_on": "eval:parent.update_stock === 1",
"depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
},
{
"default": "0",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "section_break_rqbe",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_vbbb",
"fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-01-21 19:46:25.537861",
"modified": "2024-02-04 14:11:52.742228",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -88,6 +88,7 @@ class PurchaseInvoiceItem(Document):
stock_uom_rate: DF.Currency
total_weight: DF.Float
uom: DF.Link
use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link | None
weight_per_unit: DF.Float

View File

@@ -3,7 +3,7 @@
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:08",
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.",
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain a list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\", etc.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -77,7 +77,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2022-05-16 16:15:29.059370",
"modified": "2024-01-30 13:08:09.537242",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges Template",

View File

@@ -420,7 +420,8 @@ class SalesInvoice(SellingController):
self.calculate_taxes_and_totals()
def before_save(self):
set_account_for_mode_of_payment(self)
self.set_account_for_mode_of_payment()
self.set_paid_amount()
def on_submit(self):
self.validate_pos_paid_amount()
@@ -445,6 +446,7 @@ class SalesInvoice(SellingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
# this sequence because outstanding may get -ve
@@ -706,9 +708,6 @@ class SalesInvoice(SellingController):
):
data.sales_invoice = sales_invoice
def on_update(self):
self.set_paid_amount()
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
fields_to_check = [
@@ -739,6 +738,11 @@ class SalesInvoice(SellingController):
self.paid_amount = paid_amount
self.base_paid_amount = base_paid_amount
def set_account_for_mode_of_payment(self):
for payment in self.payments:
if not payment.account:
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
def validate_time_sheets_are_submitted(self):
for data in self.timesheets:
if data.time_sheet:
@@ -2107,12 +2111,6 @@ def make_sales_return(source_name, target_doc=None):
return make_return_doc("Sales Invoice", source_name, target_doc)
def set_account_for_mode_of_payment(self):
for data in self.payments:
if not data.account:
data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")
def get_inter_company_details(doc, doctype):
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
parties = frappe.db.get_all(

View File

@@ -1533,6 +1533,19 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
def test_return_invoice_with_account_mismatch(self):
debtors2 = create_account(
parent_account="Accounts Receivable - _TC",
account_name="Debtors 2",
company="_Test Company",
account_type="Receivable",
)
si = create_sales_invoice(qty=1, rate=1000)
cr_note = create_sales_invoice(
qty=-1, rate=1000, is_return=1, return_against=si.name, debit_to=debtors2, do_not_save=True
)
self.assertRaises(frappe.ValidationError, cr_note.save)
def test_gle_made_when_asset_is_returned(self):
create_asset_data()
asset = create_asset(item_code="Macbook Pro")

View File

@@ -83,14 +83,17 @@
"quality_inspection",
"pick_serial_and_batch",
"serial_and_batch_bundle",
"batch_no",
"incoming_rate",
"use_serial_batch_fields",
"col_break5",
"allow_zero_valuation_rate",
"serial_no",
"incoming_rate",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
"section_break_eoec",
"serial_no",
"column_break_ytgd",
"batch_no",
"edit_references",
"sales_order",
"so_detail",
@@ -600,12 +603,11 @@
"options": "Quality Inspection"
},
{
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch",
"read_only": 1,
"search_index": 1
},
{
@@ -621,13 +623,12 @@
"print_hide": 1
},
{
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Serial No",
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text",
"read_only": 1
"oldfieldtype": "Small Text"
},
{
"fieldname": "item_group",
@@ -891,6 +892,7 @@
"read_only": 1
},
{
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -904,12 +906,27 @@
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
},
{
"default": "0",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "section_break_eoec",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ytgd",
"fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-12-29 13:03:14.121298",
"modified": "2024-02-04 11:52:16.106541",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -86,6 +86,7 @@ class SalesInvoiceItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None

View File

@@ -8,6 +8,7 @@
"default",
"mode_of_payment",
"amount",
"reference_no",
"column_break_3",
"account",
"type",
@@ -75,11 +76,16 @@
"hidden": 1,
"label": "Default",
"read_only": 1
},
{
"fieldname": "reference_no",
"fieldtype": "Data",
"label": "Reference No"
}
],
"istable": 1,
"links": [],
"modified": "2020-08-03 12:45:39.986598",
"modified": "2024-01-23 16:20:06.436979",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Payment",
@@ -87,5 +93,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@@ -23,6 +23,7 @@ class SalesInvoicePayment(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
reference_no: DF.Data | None
type: DF.ReadOnly | None
# end: auto-generated types

View File

@@ -3,7 +3,7 @@
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:09",
"description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.",
"description": "Standard tax template that can be applied to all Sales Transactions. This template can contain a list of tax heads and also other expense/income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -79,7 +79,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2022-05-16 16:14:52.061672",
"modified": "2024-01-30 13:07:28.801104",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",

View File

@@ -13,9 +13,13 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
def make_gl_entries(
@@ -355,6 +359,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
process_debit_credit_difference(gl_map)
dimension_filter_map = get_dimension_filter_map()
if gl_map:
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
@@ -362,6 +367,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
for entry in gl_map:
validate_allowed_dimensions(entry, dimension_filter_map)
make_entry(entry, adv_adj, update_outstanding, from_repost)
@@ -700,3 +706,39 @@ def set_as_cancel(voucher_type, voucher_no):
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
(now(), frappe.session.user, voucher_type, voucher_no),
)
def validate_allowed_dimensions(gl_entry, dimension_filter_map):
for key, value in dimension_filter_map.items():
dimension = key[0]
account = key[1]
if gl_entry.account == account:
if value["is_mandatory"] and not gl_entry.get(dimension):
frappe.throw(
_("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account)
),
MandatoryAccountDimensionError,
)
if value["allow_or_restrict"] == "Allow":
if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(gl_entry.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(gl_entry.account),
),
InvalidAccountDimensionError,
)
else:
if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(gl_entry.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(gl_entry.account),
),
InvalidAccountDimensionError,
)

View File

@@ -5,7 +5,7 @@
from collections import OrderedDict
import frappe
from frappe import _, qb, scrub
from frappe import _, qb, query_builder, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
@@ -576,6 +576,8 @@ class ReceivablePayableReport(object):
def get_future_payments_from_payment_entry(self):
pe = frappe.qb.DocType("Payment Entry")
pe_ref = frappe.qb.DocType("Payment Entry Reference")
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
return (
frappe.qb.from_(pe)
.inner_join(pe_ref)
@@ -587,6 +589,11 @@ class ReceivablePayableReport(object):
(pe.posting_date).as_("future_date"),
(pe_ref.allocated_amount).as_("future_amount"),
(pe.reference_no).as_("future_ref"),
ifelse(
pe.payment_type == "Receive",
pe.source_exchange_rate * pe_ref.allocated_amount,
pe.target_exchange_rate * pe_ref.allocated_amount,
).as_("future_amount_in_base_currency"),
)
.where(
(pe.docstatus < 2)
@@ -623,13 +630,24 @@ class ReceivablePayableReport(object):
query = query.select(
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
)
query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency"))
else:
query = query.select(
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
)
query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency"))
else:
query = query.select(
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_(
"future_amount_in_base_currency"
)
)
query = query.select(
Sum(
jea.debit_in_account_currency
if self.account_type == "Payable"
else jea.credit_in_account_currency
).as_("future_amount")
)
query = query.having(qb.Field("future_amount") > 0)
@@ -645,14 +663,19 @@ class ReceivablePayableReport(object):
row.remaining_balance = row.outstanding
row.future_amount = 0.0
for future in self.future_payments.get((row.voucher_no, row.party), []):
if row.remaining_balance > 0 and future.future_amount:
if future.future_amount > row.outstanding:
if self.filters.in_party_currency:
future_amount_field = "future_amount"
else:
future_amount_field = "future_amount_in_base_currency"
if row.remaining_balance > 0 and future.get(future_amount_field):
if future.get(future_amount_field) > row.outstanding:
row.future_amount = row.outstanding
future.future_amount = future.future_amount - row.outstanding
future[future_amount_field] = future.get(future_amount_field) - row.outstanding
row.remaining_balance = 0
else:
row.future_amount += future.future_amount
future.future_amount = 0
row.future_amount += future.get(future_amount_field)
future[future_amount_field] = 0
row.remaining_balance = row.outstanding - row.future_amount
row.setdefault("future_ref", []).append(

View File

@@ -772,3 +772,92 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
report_output = sorted(report_output, key=lambda x: x[0])
self.assertEqual(expected_data, report_output)
def test_future_payments_on_foreign_currency(self):
self.customer2 = (
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
)
.insert()
.submit()
)
si = self.create_sales_invoice(do_not_submit=True)
si.posting_date = add_days(today(), -1)
si.customer = self.customer2
si.currency = "USD"
si.conversion_rate = 80
si.debit_to = self.debtors_usd
si.save().submit()
# full payment in USD
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.base_received_amount = 7500
pe.received_amount = 7500
pe.source_exchange_rate = 75
pe.save().submit()
filters = frappe._dict(
{
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_future_payments": True,
"in_party_currency": False,
}
)
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [8000.0, 8000.0, 500.0, 7500.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
filters.in_party_currency = True
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, 0.0, 100.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
pe.cancel()
# partial payment in USD on a future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.base_received_amount = 6750
pe.received_amount = 6750
pe.source_exchange_rate = 75
pe.paid_amount = 90 # in USD
pe.references[0].allocated_amount = 90
pe.save().submit()
filters.in_party_currency = False
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [8000.0, 8000.0, 1250.0, 6750.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
filters.in_party_currency = True
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, 10.0, 90.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)

View File

@@ -8,6 +8,20 @@ frappe.query_reports["Balance Sheet"] = $.extend(
erpnext.utils.add_dimensions("Balance Sheet", 10);
frappe.query_reports["Balance Sheet"]["filters"].push(
{
"fieldname": "selected_view",
"label": __("Select View"),
"fieldtype": "Select",
"options": [
{ "value": "Report", "label": __("Report View") },
{ "value": "Growth", "label": __("Growth View") }
],
"default": "Report",
"reqd": 1
},
);
frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "accumulated_values",
label: __("Accumulated Values"),

View File

@@ -203,8 +203,14 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check"
},
{
"fieldname": "ignore_err",
"label": __("Ignore Exchange Rate Revaluation Journals"),
"fieldtype": "Check"
}
]
}

View File

@@ -241,6 +241,19 @@ def get_conditions(filters):
if filters.get("against_voucher_no"):
conditions.append("against_voucher=%(against_voucher_no)s")
if filters.get("ignore_err"):
err_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": filters.get("company"),
"docstatus": 1,
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
},
as_list=True,
)
if err_journals:
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
from frappe.utils import flt, today
from erpnext.accounts.report.general_ledger.general_ledger import execute
@@ -148,3 +148,105 @@ class TestGeneralLedger(FrappeTestCase):
self.assertEqual(data[2]["credit"], 900)
self.assertEqual(data[3]["debit"], 100)
self.assertEqual(data[3]["credit"], 100)
def test_ignore_exchange_rate_journals_filter(self):
# create a new account with USD currency
account_name = "Test Debtors USD"
company = "_Test Company"
account = frappe.get_doc(
{
"account_name": account_name,
"is_group": 0,
"company": company,
"root_type": "Asset",
"report_type": "Balance Sheet",
"account_currency": "USD",
"parent_account": "Accounts Receivable - _TC",
"account_type": "Receivable",
"doctype": "Account",
}
)
account.insert(ignore_if_duplicate=True)
# create a JV to debit 1000 USD at 75 exchange rate
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = company
jv.multi_currency = 1
jv.cost_center = "_Test Cost Center - _TC"
jv.set(
"accounts",
[
{
"account": account.name,
"party_type": "Customer",
"party": "_Test Customer USD",
"debit_in_account_currency": 1000,
"credit_in_account_currency": 0,
"exchange_rate": 75,
"cost_center": "_Test Cost Center - _TC",
},
{
"account": "Cash - _TC",
"debit_in_account_currency": 0,
"credit_in_account_currency": 75000,
"cost_center": "_Test Cost Center - _TC",
},
],
)
jv.save()
jv.submit()
revaluation = frappe.new_doc("Exchange Rate Revaluation")
revaluation.posting_date = today()
revaluation.company = company
accounts = revaluation.get_accounts_data()
revaluation.extend("accounts", accounts)
row = revaluation.accounts[0]
row.new_exchange_rate = 83
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
)
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
revaluation.set_total_gain_loss()
revaluation = revaluation.save().submit()
# post journal entry for Revaluation doc
frappe.db.set_value(
"Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
)
revaluation_jv = revaluation.make_jv_for_revaluation()
revaluation_jv.cost_center = "_Test Cost Center - _TC"
for acc in revaluation_jv.get("accounts"):
acc.cost_center = "_Test Cost Center - _TC"
revaluation_jv.save()
revaluation_jv.submit()
# With ignore_err enabled
columns, data = execute(
frappe._dict(
{
"company": company,
"from_date": today(),
"to_date": today(),
"account": [account.name],
"group_by": "Group by Voucher (Consolidated)",
"ignore_err": True,
}
)
)
self.assertNotIn(revaluation_jv.name, set([x.voucher_no for x in data]))
# Without ignore_err enabled
columns, data = execute(
frappe._dict(
{
"company": company,
"from_date": today(),
"to_date": today(),
"account": [account.name],
"group_by": "Group by Voucher (Consolidated)",
"ignore_err": False,
}
)
)
self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))

View File

@@ -8,6 +8,21 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend(
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
{
"fieldname": "selected_view",
"label": __("Select View"),
"fieldtype": "Select",
"options": [
{ "value": "Report", "label": __("Report View") },
{ "value": "Growth", "label": __("Growth View") },
{ "value": "Margin", "label": __("Margin View") },
],
"default": "Report",
"reqd": 1
},
);
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
fieldname: "accumulated_values",
label: __("Accumulated Values"),

View File

@@ -78,8 +78,14 @@ frappe.query_reports["Trial Balance"] = {
"options": erpnext.get_presentation_currency_list()
},
{
"fieldname": "with_period_closing_entry",
"label": __("Period Closing Entry"),
"fieldname": "with_period_closing_entry_for_opening",
"label": __("With Period Closing Entry For Opening Balances"),
"fieldtype": "Check",
"default": 1
},
{
"fieldname": "with_period_closing_entry_for_current_period",
"label": __("Period Closing Entry For Current Period"),
"fieldtype": "Check",
"default": 1
},

View File

@@ -116,7 +116,7 @@ def get_data(filters):
max_rgt,
filters,
gl_entries_by_account,
ignore_closing_entries=not flt(filters.with_period_closing_entry),
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
ignore_opening_entries=True,
)
@@ -249,7 +249,7 @@ def get_opening_balance(
):
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
if not flt(filters.with_period_closing_entry):
if not flt(filters.with_period_closing_entry_for_opening):
if doctype == "Account Closing Balance":
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
else:

View File

@@ -237,10 +237,9 @@ def get_balance_on(
)
else:
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),))
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center),))
if account:
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
acc.check_permission("read")
@@ -259,7 +258,7 @@ def get_balance_on(
if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"):
in_account_currency = False
else:
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
if account_type:
accounts = frappe.db.get_all(
@@ -279,25 +278,29 @@ def get_balance_on(
if party_type and party:
cond.append(
"""gle.party_type = %s and gle.party = %s """
% (frappe.db.escape(party_type), frappe.db.escape(party, percent=False))
% (frappe.db.escape(party_type), frappe.db.escape(party))
)
if company:
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
cond.append("""gle.company = %s """ % (frappe.db.escape(company)))
if account or (party_type and party) or account_type:
precision = get_currency_precision()
if in_account_currency:
select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
select_field = (
"sum(round(debit_in_account_currency, %s)) - sum(round(credit_in_account_currency, %s))"
)
else:
select_field = "sum(debit) - sum(credit)"
select_field = "sum(round(debit, %s)) - sum(round(credit, %s))"
bal = frappe.db.sql(
"""
SELECT {0}
FROM `tabGL Entry` gle
WHERE {1}""".format(
select_field, " and ".join(cond)
)
),
(precision, precision),
)[0][0]
# if bal is None, return 0
return flt(bal)
@@ -345,7 +348,7 @@ def get_count_on(account, fieldname, date):
% (acc.lft, acc.rgt)
)
else:
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
entries = frappe.db.sql(
"""
@@ -453,7 +456,19 @@ def add_cc(args=None):
return cc.name
def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep
def _build_dimensions_dict_for_exc_gain_loss(
entry: dict | object = None, active_dimensions: list = None
):
dimensions_dict = frappe._dict()
if entry and active_dimensions:
for dim in active_dimensions:
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname)
return dimensions_dict
def reconcile_against_document(
args, skip_ref_details_update_for_pe=False, active_dimensions=None
): # nosemgrep
"""
Cancel PE or JV, Update against document, split if required and resubmit
"""
@@ -482,6 +497,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
check_if_advance_entry_modified(entry)
validate_allocated_amount(entry)
dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions)
# update ref in advance entry
if voucher_type == "Journal Entry":
referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False)
@@ -489,10 +506,14 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
# amount and account in args
# referenced_row is used to deduplicate gain/loss journal
entry.update({"referenced_row": referenced_row})
doc.make_exchange_gain_loss_journal([entry])
doc.make_exchange_gain_loss_journal([entry], dimensions_dict)
else:
referenced_row = update_reference_in_payment_entry(
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
entry,
doc,
do_not_save=True,
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict,
)
doc.save(ignore_permissions=True)
@@ -649,7 +670,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
def update_reference_in_payment_entry(
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False, dimensions_dict=None
):
reference_details = {
"reference_doctype": d.against_voucher_type,
@@ -662,6 +683,7 @@ def update_reference_in_payment_entry(
else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.difference_amount,
"account": d.account,
"dimensions": d.dimensions,
}
if d.voucher_detail_no:
@@ -694,7 +716,9 @@ def update_reference_in_payment_entry(
if not skip_ref_details_update_for_pe:
payment_entry.set_missing_ref_details()
payment_entry.set_amounts()
payment_entry.make_exchange_gain_loss_journal()
payment_entry.make_exchange_gain_loss_journal(
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
)
if not do_not_save:
payment_entry.save(ignore_permissions=True)
@@ -2031,6 +2055,7 @@ def create_gain_loss_journal(
ref2_dn,
ref2_detail_no,
cost_center,
dimensions,
) -> str:
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
@@ -2064,7 +2089,8 @@ def create_gain_loss_journal(
dr_or_cr + "_in_account_currency": 0,
}
)
if dimensions:
journal_account.update(dimensions)
journal_entry.append("accounts", journal_account)
journal_account = frappe._dict(
@@ -2080,7 +2106,8 @@ def create_gain_loss_journal(
reverse_dr_or_cr: abs(exc_gain_loss),
}
)
if dimensions:
journal_account.update(dimensions)
journal_entry.append("accounts", journal_account)
journal_entry.save()

View File

@@ -571,16 +571,16 @@ frappe.ui.form.on('Asset', {
indicator: 'red'
});
}
var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
var asset_quantity = is_grouped_asset ? item.qty : 1;
var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
frm.set_value('gross_purchase_amount', purchase_amount);
frm.set_value('purchase_receipt_amount', purchase_amount);
frm.set_value('asset_quantity', asset_quantity);
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
if(item.asset_location) { frm.set_value('location', item.asset_location); }
frappe.db.get_value('Item', item.item_code, 'is_grouped_asset', (r) => {
var asset_quantity = r.is_grouped_asset ? item.qty : 1;
var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
frm.set_value('gross_purchase_amount', purchase_amount);
frm.set_value('purchase_receipt_amount', purchase_amount);
frm.set_value('asset_quantity', asset_quantity);
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
if(item.asset_location) { frm.set_value('location', item.asset_location); }
});
},
set_depreciation_rate: function(frm, row) {

View File

@@ -126,6 +126,7 @@ class AssetCapitalization(StockController):
self.create_target_asset()
def on_submit(self):
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
self.update_target_asset()

View File

@@ -18,9 +18,12 @@
"amount",
"batch_and_serial_no_section",
"serial_and_batch_bundle",
"use_serial_batch_fields",
"column_break_13",
"batch_no",
"section_break_bfqc",
"serial_no",
"column_break_mbuv",
"batch_no",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break"
@@ -39,13 +42,13 @@
"reqd": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
"options": "Batch",
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"fieldname": "section_break_6",
@@ -102,12 +105,12 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Serial No",
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"fieldname": "item_code",
@@ -148,18 +151,34 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"default": "0",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "section_break_bfqc",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_mbuv",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-04-06 01:10:17.947952",
"modified": "2024-02-04 16:41:09.239762",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Stock Item",

View File

@@ -27,6 +27,7 @@ class AssetCapitalizationStockItem(Document):
serial_no: DF.SmallText | None
stock_qty: DF.Float
stock_uom: DF.Link
use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link
# end: auto-generated types

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"creation": "2013-06-25 11:04:03",
"description": "Settings for Buying Module",
"doctype": "DocType",
"document_type": "Other",
"engine": "InnoDB",
@@ -152,6 +151,7 @@
},
{
"default": "1",
"depends_on": "eval: frappe.boot.versions && frappe.boot.versions.payments",
"fieldname": "show_pay_button",
"fieldtype": "Check",
"label": "Show Pay Button in Purchase Order Portal"
@@ -214,7 +214,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-01-12 16:42:01.894346",
"modified": "2024-01-31 13:34:18.101256",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@@ -264,4 +264,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -453,6 +453,7 @@ class PurchaseOrder(BuyingController):
self.update_ordered_qty()
self.update_reserved_qty_for_subcontract()
self.update_subcontracting_order_status()
self.update_blanket_order()
self.notify_update()
clear_doctype_notifications(self)
@@ -626,6 +627,7 @@ class PurchaseOrder(BuyingController):
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
@frappe.request_cache
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item"""

View File

@@ -814,6 +814,30 @@ class TestPurchaseOrder(FrappeTestCase):
# To test if the PO does NOT have a Blanket Order
self.assertEqual(po_doc.items[0].blanket_order, None)
def test_blanket_order_on_po_close_and_open(self):
# Step - 1: Create Blanket Order
bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10)
# Step - 2: Create Purchase Order
po = create_purchase_order(
item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name
)
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 5)
# Step - 3: Close Purchase Order
po.update_status("Closed")
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 0)
# Step - 4: Re-Open Purchase Order
po.update_status("Re-open")
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 5)
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
@@ -1113,6 +1137,7 @@ def create_purchase_order(**args):
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1),
"against_blanket_order": args.against_blanket_order,
"against_blanket": args.against_blanket,
"material_request": args.material_request,
"material_request_item": args.material_request_item,
},

View File

@@ -545,7 +545,6 @@
"fieldname": "blanket_order",
"fieldtype": "Link",
"label": "Blanket Order",
"no_copy": 1,
"options": "Blanket Order"
},
{
@@ -553,7 +552,6 @@
"fieldname": "blanket_order_rate",
"fieldtype": "Currency",
"label": "Blanket Order Rate",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -917,7 +915,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-24 13:24:41.298416",
"modified": "2024-02-05 11:23:24.859435",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _, bold, qb, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import (
@@ -28,6 +29,7 @@ from frappe.utils import (
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimensions,
)
from erpnext.accounts.doctype.pricing_rule.utils import (
apply_pricing_rule_for_free_items,
@@ -200,6 +202,7 @@ class AccountsController(TransactionBase):
self.validate_party()
self.validate_currency()
self.validate_party_account_currency()
self.validate_return_against_account()
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
if invalid_advances := [
@@ -348,6 +351,20 @@ class AccountsController(TransactionBase):
for bundle in bundles:
frappe.delete_doc("Serial and Batch Bundle", bundle.name)
def validate_return_against_account(self):
if (
self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against
):
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To"
cr_dr_account = self.get(cr_dr_account_field)
if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account:
frappe.throw(
_("'{0}' account: '{1}' should match the Return Against Invoice").format(
frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account)
)
)
def validate_deferred_income_expense_account(self):
field_map = {
"Sales Invoice": "deferred_revenue_account",
@@ -676,7 +693,7 @@ class AccountsController(TransactionBase):
if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted
ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
@@ -1246,7 +1263,9 @@ class AccountsController(TransactionBase):
return True
return False
def make_exchange_gain_loss_journal(self, args: dict = None) -> None:
def make_exchange_gain_loss_journal(
self, args: dict = None, dimensions_dict: dict = None
) -> None:
"""
Make Exchange Gain/Loss journal for Invoices and Payments
"""
@@ -1299,6 +1318,7 @@ class AccountsController(TransactionBase):
self.name,
arg.get("referenced_row"),
arg.get("cost_center"),
dimensions_dict,
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -1379,6 +1399,7 @@ class AccountsController(TransactionBase):
self.name,
d.idx,
self.cost_center,
dimensions_dict,
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -1443,7 +1464,13 @@ class AccountsController(TransactionBase):
if lst:
from erpnext.accounts.utils import reconcile_against_document
reconcile_against_document(lst)
# pass dimension values to utility method
active_dimensions = get_dimensions()[0]
for x in lst:
for dim in active_dimensions:
if self.get(dim.fieldname):
x.update({dim.fieldname: self.get(dim.fieldname)})
reconcile_against_document(lst, active_dimensions=active_dimensions)
def on_cancel(self):
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
@@ -2712,47 +2739,37 @@ def get_common_query(
q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate"))
if condition:
if condition.get("name", None):
q = q.where(payment_entry.name.like(f"%{condition.get('name')}%"))
# conditions should be built as an array and passed as Criterion
common_filter_conditions = []
common_filter_conditions.append(payment_entry.company == condition["company"])
if condition.get("name", None):
common_filter_conditions.append(payment_entry.name.like(f"%{condition.get('name')}%"))
if condition.get("from_payment_date"):
common_filter_conditions.append(payment_entry.posting_date.gte(condition["from_payment_date"]))
if condition.get("to_payment_date"):
common_filter_conditions.append(payment_entry.posting_date.lte(condition["to_payment_date"]))
q = q.where(payment_entry.company == condition["company"])
q = (
q.where(payment_entry.posting_date >= condition["from_payment_date"])
if condition.get("from_payment_date")
else q
)
q = (
q.where(payment_entry.posting_date <= condition["to_payment_date"])
if condition.get("to_payment_date")
else q
)
if condition.get("get_payments") == True:
q = (
q.where(payment_entry.cost_center == condition["cost_center"])
if condition.get("cost_center")
else q
)
q = (
q.where(payment_entry.unallocated_amount >= condition["minimum_payment_amount"])
if condition.get("minimum_payment_amount")
else q
)
q = (
q.where(payment_entry.unallocated_amount <= condition["maximum_payment_amount"])
if condition.get("maximum_payment_amount")
else q
)
else:
q = (
q.where(payment_entry.total_debit >= condition["minimum_payment_amount"])
if condition.get("minimum_payment_amount")
else q
)
q = (
q.where(payment_entry.total_debit <= condition["maximum_payment_amount"])
if condition.get("maximum_payment_amount")
else q
)
if condition.get("cost_center"):
common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"])
if condition.get("accounting_dimensions"):
for field, val in condition.get("accounting_dimensions").items():
common_filter_conditions.append(payment_entry[field] == val)
if condition.get("minimum_payment_amount"):
common_filter_conditions.append(
payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"])
)
if condition.get("maximum_payment_amount"):
common_filter_conditions.append(
payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"])
)
q = q.where(Criterion.all(common_filter_conditions))
q = q.orderby(payment_entry.posting_date)
q = q.limit(limit) if limit else q

View File

@@ -724,17 +724,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
conditions, bin_conditions = [], []
filter_dict = get_doctype_wise_filters(filters)
query = """select `tabWarehouse`.name,
warehouse_field = "name"
meta = frappe.get_meta("Warehouse")
if meta.get("show_title_field_in_link") and meta.get("title_field"):
searchfield = meta.get("title_field")
warehouse_field = meta.get("title_field")
query = """select `tabWarehouse`.`{warehouse_field}`,
CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
from `tabWarehouse` left join `tabBin`
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
where
`tabWarehouse`.`{key}` like {txt}
{fcond} {mcond}
order by ifnull(`tabBin`.actual_qty, 0) desc
order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc
limit
{page_len} offset {start}
""".format(
warehouse_field=warehouse_field,
bin_conditions=get_filters_cond(
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
),

View File

@@ -602,7 +602,7 @@ class SellingController(StockController):
if self.doctype in ["Sales Order", "Quotation"]:
for item in self.items:
item.gross_profit = flt(
((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item)
((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item)
)
def set_customer_address(self):

View File

@@ -91,7 +91,8 @@ status_map = {
],
"Purchase Receipt": [
["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],

View File

@@ -6,7 +6,7 @@ from collections import defaultdict
from typing import List, Tuple
import frappe
from frappe import _
from frappe import _, bold
from frappe.utils import cint, flt, get_link_to_form, getdate
import erpnext
@@ -21,6 +21,9 @@ from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
)
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_type_of_transaction,
)
from erpnext.stock.stock_ledger import get_items_to_be_repost
@@ -126,6 +129,81 @@ class StockController(AccountsController):
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
def make_bundle_using_old_serial_batch_fields(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
# To handle test cases
if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields:
return
table_name = "items"
if self.doctype == "Asset Capitalization":
table_name = "stock_items"
for row in self.get(table_name):
if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"):
continue
if not row.use_serial_batch_fields and (
row.serial_no or row.batch_no or row.get("rejected_serial_no")
):
frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle"))
if row.use_serial_batch_fields and (
not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
):
if self.doctype == "Stock Reconciliation":
qty = row.qty
type_of_transaction = "Inward"
else:
qty = row.stock_qty
type_of_transaction = get_type_of_transaction(self, row)
sn_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
"qty": qty,
"type_of_transaction": type_of_transaction,
"company": self.company,
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
"serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
"batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None,
"batch_no": row.batch_no,
"use_serial_batch_fields": row.use_serial_batch_fields,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
if sn_doc.is_rejected:
row.rejected_serial_and_batch_bundle = sn_doc.name
row.db_set(
{
"rejected_serial_and_batch_bundle": sn_doc.name,
"rejected_serial_no": "",
}
)
else:
row.serial_and_batch_bundle = sn_doc.name
row.db_set(
{
"serial_and_batch_bundle": sn_doc.name,
"serial_no": "",
"batch_no": "",
}
)
def set_use_serial_batch_fields(self):
if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
for row in self.items:
row.use_serial_batch_fields = 1
def get_gl_entries(
self, warehouse_account=None, default_expense_account=None, default_cost_center=None
):
@@ -697,6 +775,9 @@ class StockController(AccountsController):
self.validate_in_transit_warehouses()
self.validate_multi_currency()
self.validate_packed_items()
if self.get("is_internal_supplier"):
self.validate_internal_transfer_qty()
else:
self.validate_internal_transfer_warehouse()
@@ -735,6 +816,116 @@ class StockController(AccountsController):
if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"):
frappe.throw(_("Packed Items cannot be transferred internally"))
def validate_internal_transfer_qty(self):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
return
item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty()
if not item_wise_transfer_qty:
return
item_wise_received_qty = self.get_item_wise_inter_received_qty()
precision = frappe.get_precision(self.doctype + " Item", "qty")
over_receipt_allowance = frappe.db.get_single_value(
"Stock Settings", "over_delivery_receipt_allowance"
)
parent_doctype = {
"Purchase Receipt": "Delivery Note",
"Purchase Invoice": "Sales Invoice",
}.get(self.doctype)
for key, transferred_qty in item_wise_transfer_qty.items():
recevied_qty = flt(item_wise_received_qty.get(key), precision)
if over_receipt_allowance:
transferred_qty = transferred_qty + flt(
transferred_qty * over_receipt_allowance / 100, precision
)
if recevied_qty > flt(transferred_qty, precision):
frappe.throw(
_("For Item {0} cannot be received more than {1} qty against the {2} {3}").format(
bold(key[1]),
bold(flt(transferred_qty, precision)),
bold(parent_doctype),
get_link_to_form(parent_doctype, self.get("inter_company_reference")),
)
)
def get_item_wise_inter_transfer_qty(self):
reference_field = "inter_company_reference"
if self.doctype == "Purchase Invoice":
reference_field = "inter_company_invoice_reference"
parent_doctype = {
"Purchase Receipt": "Delivery Note",
"Purchase Invoice": "Sales Invoice",
}.get(self.doctype)
child_doctype = parent_doctype + " Item"
parent_tab = frappe.qb.DocType(parent_doctype)
child_tab = frappe.qb.DocType(child_doctype)
query = (
frappe.qb.from_(parent_doctype)
.inner_join(child_tab)
.on(child_tab.parent == parent_tab.name)
.select(
child_tab.name,
child_tab.item_code,
child_tab.qty,
)
.where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1))
)
data = query.run(as_dict=True)
item_wise_transfer_qty = defaultdict(float)
for row in data:
item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty)
return item_wise_transfer_qty
def get_item_wise_inter_received_qty(self):
child_doctype = self.doctype + " Item"
parent_tab = frappe.qb.DocType(self.doctype)
child_tab = frappe.qb.DocType(child_doctype)
query = (
frappe.qb.from_(self.doctype)
.inner_join(child_tab)
.on(child_tab.parent == parent_tab.name)
.select(
child_tab.item_code,
child_tab.qty,
)
.where(parent_tab.docstatus < 2)
)
if self.doctype == "Purchase Invoice":
query = query.select(
child_tab.sales_invoice_item.as_("name"),
)
query = query.where(
parent_tab.inter_company_invoice_reference == self.inter_company_invoice_reference
)
else:
query = query.select(
child_tab.delivery_note_item.as_("name"),
)
query = query.where(parent_tab.inter_company_reference == self.inter_company_reference)
data = query.run(as_dict=True)
item_wise_transfer_qty = defaultdict(float)
for row in data:
item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty)
return item_wise_transfer_qty
def validate_putaway_capacity(self):
# if over receipt is attempted while 'apply putaway rule' is disabled
# and if rule was applied on the transaction, validate it.

View File

@@ -260,18 +260,22 @@ class SubcontractingController(StockController):
return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
def __get_consumed_items(self, doctype, receipt_items):
fields = [
"serial_no",
"rm_item_code",
"reference_name",
"batch_no",
"consumed_qty",
"main_item_code",
"parent as voucher_no",
]
if self.subcontract_data.receipt_supplied_items_field != "Purchase Receipt Item Supplied":
fields.append("serial_and_batch_bundle")
return frappe.get_all(
self.subcontract_data.receipt_supplied_items_field,
fields=[
"serial_no",
"rm_item_code",
"reference_name",
"serial_and_batch_bundle",
"batch_no",
"consumed_qty",
"main_item_code",
"parent as voucher_no",
],
fields=fields,
filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
)
@@ -881,7 +885,9 @@ class SubcontractingController(StockController):
"posting_time": self.posting_time,
"qty": -1 * item.consumed_qty,
"voucher_detail_no": item.name,
"serial_and_batch_bundle": item.serial_and_batch_bundle,
"serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
"serial_no": item.get("serial_no"),
"batch_no": item.get("batch_no"),
}
)

View File

@@ -97,6 +97,7 @@ class calculate_taxes_and_totals(object):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
"net_rate": item.net_rate or item.rate,
"base_net_rate": item.base_net_rate or item.base_rate,
"tax_category": self.doc.get("tax_category"),
"posting_date": self.doc.get("posting_date"),
"bill_date": self.doc.get("bill_date"),

View File

@@ -56,6 +56,7 @@ class TestAccountsController(FrappeTestCase):
20 series - Sales Invoice against Journals
30 series - Sales Invoice against Credit Notes
40 series - Company default Cost center is unset
50 series - Dimension inheritence
"""
def setUp(self):
@@ -1188,3 +1189,214 @@ class TestAccountsController(FrappeTestCase):
)
frappe.db.set_value("Company", self.company, "cost_center", cc)
def setup_dimensions(self):
# create dimension
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
)
create_dimension()
# make it non-mandatory
loc = frappe.get_doc("Accounting Dimension", "Location")
for x in loc.dimension_defaults:
x.mandatory_for_bs = False
x.mandatory_for_pl = False
loc.save()
def test_50_dimensions_filter(self):
"""
Test workings of dimension filters
"""
self.setup_dimensions()
rate_in_account_currency = 1
# Invoices
si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si1.department = "Management"
si1.save().submit()
si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si2.department = "Operations"
si2.save().submit()
# Payments
cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note1.department = "Management"
cr_note1.is_return = 1
cr_note1.save().submit()
cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note2.department = "Legal"
cr_note2.is_return = 1
cr_note2.save().submit()
pe1 = get_payment_entry(si1.doctype, si1.name)
pe1.references = []
pe1.department = "Research & Development"
pe1.save().submit()
pe2 = get_payment_entry(si1.doctype, si1.name)
pe2.references = []
pe2.department = "Management"
pe2.save().submit()
je1 = self.create_journal_entry(
acc1=self.debit_usd,
acc1_exc_rate=75,
acc2=self.cash,
acc1_amount=-1,
acc2_amount=-75,
acc2_exc_rate=1,
)
je1.accounts[0].party_type = "Customer"
je1.accounts[0].party = self.customer
je1.accounts[0].department = "Management"
je1.save().submit()
# assert dimension filter's result
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 2)
self.assertEqual(len(pr.payments), 5)
pr.department = "Legal"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
pr.department = "Management"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 3)
pr.department = "Research & Development"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
def test_51_cr_note_should_inherit_dimension(self):
self.setup_dimensions()
rate_in_account_currency = 1
# Invoice
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si.department = "Management"
si.save().submit()
# Payment
cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note.department = "Management"
cr_note.is_return = 1
cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.department = "Management"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
# There should be 2 journals, JE(Cr Note) and JE(Exchange Gain/Loss)
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_cr_note = self.get_journals_for(cr_note.doctype, cr_note.name)
self.assertNotEqual(exc_je_for_si, [])
self.assertEqual(len(exc_je_for_si), 2)
self.assertEqual(len(exc_je_for_cr_note), 2)
self.assertEqual(exc_je_for_si, exc_je_for_cr_note)
for x in exc_je_for_si + exc_je_for_cr_note:
with self.subTest(x=x):
self.assertEqual(
[cr_note.department, cr_note.department],
frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"),
)
def test_52_dimension_inhertiance_exc_gain_loss(self):
# Sales Invoice in Foreign Currency
self.setup_dimensions()
rate = 80
rate_in_account_currency = 1
dpt = "Research & Development"
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True)
si.department = dpt
si.save().submit()
pe = self.create_payment_entry(amount=1, source_exc_rate=82).save()
pe.department = dpt
pe = pe.save().submit()
pr = self.create_payment_reconciliation()
pr.department = dpt
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
# Exc Gain/Loss journals should inherit dimension from parent
journals = self.get_journals_for(si.doctype, si.name)
self.assertEqual(
[dpt, dpt],
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", [x.parent for x in journals])},
pluck="department",
),
)
def test_53_dimension_inheritance_on_advance(self):
self.setup_dimensions()
dpt = "Research & Development"
adv = self.create_payment_entry(amount=1, source_exc_rate=85)
adv.department = dpt
adv.save().submit()
adv.reload()
# Sales Invoices in different exchange rates
si = self.create_sales_invoice(qty=1, conversion_rate=82, rate=1, do_not_submit=True)
si.department = dpt
advances = si.get_advance_entries()
self.assertEqual(len(advances), 1)
self.assertEqual(advances[0].reference_name, adv.name)
si.append(
"advances",
{
"doctype": "Sales Invoice Advance",
"reference_type": advances[0].reference_type,
"reference_name": advances[0].reference_name,
"reference_row": advances[0].reference_row,
"advance_amount": 1,
"allocated_amount": 1,
"ref_exchange_rate": advances[0].exchange_rate,
"remarks": advances[0].remarks,
},
)
si = si.save().submit()
# Outstanding in both currencies should be '0'
adv.reload()
self.assertEqual(si.outstanding_amount, 0)
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
# Exc Gain/Loss journals should inherit dimension from parent
journals = self.get_journals_for(si.doctype, si.name)
self.assertEqual(
[dpt, dpt],
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", [x.parent for x in journals])},
pluck="department",
),
)

View File

@@ -543,6 +543,8 @@ accounting_dimension_doctypes = [
"Account Closing Balance",
"Supplier Quotation",
"Supplier Quotation Item",
"Payment Reconciliation",
"Payment Reconciliation Allocation",
]
get_matching_queries = (

View File

@@ -90,6 +90,7 @@ def make_order(source_name):
def update_item(source, target, source_parent):
target_qty = source.get("qty") - source.get("ordered_qty")
target.qty = target_qty if not flt(target_qty) < 0 else 0
target.rate = source.get("rate")
item = get_item_defaults(target.item_code, source_parent.company)
if item:
target.item_name = item.get("item_name")
@@ -111,6 +112,10 @@ def make_order(source_name):
},
},
)
if target_doc.doctype == "Purchase Order":
target_doc.set_missing_values()
return target_doc

View File

@@ -176,8 +176,10 @@ class BOM(WebsiteGenerator):
def autoname(self):
# ignore amended documents while calculating current index
search_key = f"{self.doctype}-{self.item}%"
existing_boms = frappe.get_all(
"BOM", filters={"item": self.item, "amended_from": ["is", "not set"]}, pluck="name"
"BOM", filters={"name": ("like", search_key), "amended_from": ["is", "not set"]}, pluck="name"
)
if existing_boms:

View File

@@ -101,6 +101,7 @@ frappe.ui.form.on("BOM Creator", {
}
})
dialog.fields_dict.item_code.get_query = "erpnext.controllers.queries.item_query";
dialog.show();
},
@@ -113,6 +114,16 @@ frappe.ui.form.on("BOM Creator", {
}
}
});
frm.set_query("item_code", "items", function() {
return {
query: "erpnext.controllers.queries.item_query",
}
});
frm.set_query("fg_item", "items", function() {
return {
query: "erpnext.controllers.queries.item_query",
}
});
},
refresh(frm) {
@@ -211,4 +222,4 @@ erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionC
}
};
extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm}));
extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm}));

View File

@@ -955,6 +955,14 @@ class JobCard(Document):
if update_status:
self.db_set("status", self.status)
if self.status in ["Completed", "Work In Progress"]:
status = {
"Completed": "Off",
"Work In Progress": "Production",
}.get(self.status)
self.update_status_in_workstation(status)
def set_wip_warehouse(self):
if not self.wip_warehouse:
self.wip_warehouse = frappe.db.get_single_value(
@@ -1035,6 +1043,12 @@ class JobCard(Document):
return False
def update_status_in_workstation(self, status):
if not self.workstation:
return
frappe.db.set_value("Workstation", self.workstation, "status", status)
@frappe.whitelist()
def make_time_log(args):

View File

@@ -0,0 +1,256 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Plant Floor", {
setup(frm) {
frm.trigger("setup_queries");
},
setup_queries(frm) {
frm.set_query("warehouse", (doc) => {
if (!doc.company) {
frappe.throw(__("Please select Company first"));
}
return {
filters: {
"is_group": 0,
"company": doc.company
}
}
});
},
refresh(frm) {
frm.trigger('prepare_stock_dashboard')
frm.trigger('prepare_workstation_dashboard')
},
prepare_workstation_dashboard(frm) {
let wrapper = $(frm.fields_dict["plant_dashboard"].wrapper);
wrapper.empty();
frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor({
wrapper: wrapper,
skip_filters: true,
plant_floor: frm.doc.name,
});
},
prepare_stock_dashboard(frm) {
if (!frm.doc.warehouse) {
return;
}
let wrapper = $(frm.fields_dict["stock_summary"].wrapper);
wrapper.empty();
frappe.visual_stock = new VisualStock({
wrapper: wrapper,
frm: frm,
});
},
});
class VisualStock {
constructor(opts) {
Object.assign(this, opts);
this.make();
}
make() {
this.prepare_filters();
this.prepare_stock_summary({
start:0
});
}
prepare_filters() {
this.wrapper.append(`
<div class="row">
<div class="col-sm-12 filter-section section-body">
</div>
</div>
`);
this.item_filter = frappe.ui.form.make_control({
df: {
fieldtype: "Link",
fieldname: "item_code",
placeholder: __("Item"),
options: "Item",
onchange: () => this.prepare_stock_summary({
start:0,
item_code: this.item_filter.value
})
},
parent: this.wrapper.find('.filter-section'),
render_input: true,
});
this.item_filter.$wrapper.addClass('form-column col-sm-3');
this.item_filter.$wrapper.find('.clearfix').hide();
this.item_group_filter = frappe.ui.form.make_control({
df: {
fieldtype: "Link",
fieldname: "item_group",
placeholder: __("Item Group"),
options: "Item Group",
change: () => this.prepare_stock_summary({
start:0,
item_group: this.item_group_filter.value
})
},
parent: this.wrapper.find('.filter-section'),
render_input: true,
});
this.item_group_filter.$wrapper.addClass('form-column col-sm-3');
this.item_group_filter.$wrapper.find('.clearfix').hide();
}
prepare_stock_summary(args) {
let {start, item_code, item_group} = args;
this.get_stock_summary(start, item_code, item_group).then(stock_summary => {
this.wrapper.find('.stock-summary-container').remove();
this.wrapper.append(`<div class="col-sm-12 stock-summary-container" style="margin-bottom:20px"></div>`);
this.stock_summary = stock_summary.message;
this.render_stock_summary();
this.bind_events();
});
}
async get_stock_summary(start, item_code, item_group) {
let stock_summary = await frappe.call({
method: "erpnext.manufacturing.doctype.plant_floor.plant_floor.get_stock_summary",
args: {
warehouse: this.frm.doc.warehouse,
start: start,
item_code: item_code,
item_group: item_group
}
});
return stock_summary;
}
render_stock_summary() {
let template = frappe.render_template("stock_summary_template", {
stock_summary: this.stock_summary
});
this.wrapper.find('.stock-summary-container').append(template);
}
bind_events() {
this.wrapper.find('.btn-add').click((e) => {
this.item_code = decodeURI($(e.currentTarget).attr('data-item-code'));
this.make_stock_entry([
{
label: __("For Item"),
fieldname: "item_code",
fieldtype: "Data",
read_only: 1,
default: this.item_code
},
{
label: __("Quantity"),
fieldname: "qty",
fieldtype: "Float",
reqd: 1
}
], __("Add Stock"), "Material Receipt")
});
this.wrapper.find('.btn-move').click((e) => {
this.item_code = decodeURI($(e.currentTarget).attr('data-item-code'));
this.make_stock_entry([
{
label: __("For Item"),
fieldname: "item_code",
fieldtype: "Data",
read_only: 1,
default: this.item_code
},
{
label: __("Quantity"),
fieldname: "qty",
fieldtype: "Float",
reqd: 1
},
{
label: __("To Warehouse"),
fieldname: "to_warehouse",
fieldtype: "Link",
options: "Warehouse",
reqd: 1,
get_query: () => {
return {
filters: {
"is_group": 0,
"company": this.frm.doc.company
}
}
}
}
], __("Move Stock"), "Material Transfer")
});
}
make_stock_entry(fields, title, stock_entry_type) {
frappe.prompt(fields,
(values) => {
this.values = values;
this.stock_entry_type = stock_entry_type;
this.update_values();
this.frm.call({
method: "make_stock_entry",
doc: this.frm.doc,
args: {
kwargs: this.values,
},
callback: (r) => {
if (!r.exc) {
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
})
}, __(title), __("Create")
);
}
update_values() {
if (!this.values.qty) {
frappe.throw(__("Quantity is required"));
}
let from_warehouse = "";
let to_warehouse = "";
if (this.stock_entry_type == "Material Receipt") {
to_warehouse = this.frm.doc.warehouse;
} else {
from_warehouse = this.frm.doc.warehouse;
to_warehouse = this.values.to_warehouse;
}
this.values = {
...this.values,
...{
"company": this.frm.doc.company,
"item_code": this.item_code,
"from_warehouse": from_warehouse,
"to_warehouse": to_warehouse,
"purpose": this.stock_entry_type,
}
}
}
}

View File

@@ -0,0 +1,97 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:floor_name",
"creation": "2023-10-06 15:06:07.976066",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"workstations_tab",
"plant_dashboard",
"stock_summary_tab",
"stock_summary",
"details_tab",
"column_break_mvbx",
"floor_name",
"company",
"warehouse"
],
"fields": [
{
"fieldname": "floor_name",
"fieldtype": "Data",
"label": "Floor Name",
"unique": 1
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "workstations_tab",
"fieldtype": "Tab Break",
"label": "Workstations"
},
{
"fieldname": "plant_dashboard",
"fieldtype": "HTML",
"label": "Plant Dashboard"
},
{
"fieldname": "details_tab",
"fieldtype": "Tab Break",
"label": "Floor"
},
{
"fieldname": "column_break_mvbx",
"fieldtype": "Column Break"
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
},
{
"depends_on": "eval:!doc.__islocal && doc.warehouse",
"fieldname": "stock_summary_tab",
"fieldtype": "Tab Break",
"label": "Stock Summary"
},
{
"fieldname": "stock_summary",
"fieldtype": "HTML",
"label": "Stock Summary"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-01-30 11:59:07.508535",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Plant Floor",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,129 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.query_builder import Order
from frappe.utils import get_link_to_form, nowdate, nowtime
class PlantFloor(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
company: DF.Link | None
floor_name: DF.Data | None
warehouse: DF.Link | None
# end: auto-generated types
@frappe.whitelist()
def make_stock_entry(self, kwargs):
if isinstance(kwargs, str):
kwargs = frappe.parse_json(kwargs)
if isinstance(kwargs, dict):
kwargs = frappe._dict(kwargs)
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.update(
{
"company": kwargs.company,
"from_warehouse": kwargs.from_warehouse,
"to_warehouse": kwargs.to_warehouse,
"purpose": kwargs.purpose,
"stock_entry_type": kwargs.purpose,
"posting_date": nowdate(),
"posting_time": nowtime(),
"items": self.get_item_details(kwargs),
}
)
stock_entry.set_missing_values()
return stock_entry
def get_item_details(self, kwargs) -> list[dict]:
item_details = frappe.db.get_value(
"Item", kwargs.item_code, ["item_name", "stock_uom", "item_group", "description"], as_dict=True
)
item_details.update(
{
"qty": kwargs.qty,
"uom": item_details.stock_uom,
"item_code": kwargs.item_code,
"conversion_factor": 1,
"s_warehouse": kwargs.from_warehouse,
"t_warehouse": kwargs.to_warehouse,
}
)
return [item_details]
@frappe.whitelist()
def get_stock_summary(warehouse, start=0, item_code=None, item_group=None):
stock_details = get_stock_details(
warehouse, start=start, item_code=item_code, item_group=item_group
)
max_count = 0.0
for d in stock_details:
d.actual_or_pending = (
d.projected_qty
+ d.reserved_qty
+ d.reserved_qty_for_production
+ d.reserved_qty_for_sub_contract
)
d.pending_qty = 0
d.total_reserved = (
d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract
)
if d.actual_or_pending > d.actual_qty:
d.pending_qty = d.actual_or_pending - d.actual_qty
d.max_count = max(d.actual_or_pending, d.actual_qty, d.total_reserved, max_count)
max_count = d.max_count
d.item_link = get_link_to_form("Item", d.item_code)
return stock_details
def get_stock_details(warehouse, start=0, item_code=None, item_group=None):
item_table = frappe.qb.DocType("Item")
bin_table = frappe.qb.DocType("Bin")
query = (
frappe.qb.from_(bin_table)
.inner_join(item_table)
.on(bin_table.item_code == item_table.name)
.select(
bin_table.item_code,
bin_table.actual_qty,
bin_table.projected_qty,
bin_table.reserved_qty,
bin_table.reserved_qty_for_production,
bin_table.reserved_qty_for_sub_contract,
bin_table.reserved_qty_for_production_plan,
bin_table.reserved_stock,
item_table.item_name,
item_table.item_group,
item_table.image,
)
.where(bin_table.warehouse == warehouse)
.limit(20)
.offset(start)
.orderby(bin_table.actual_qty, order=Order.desc)
)
if item_code:
query = query.where(bin_table.item_code == item_code)
if item_group:
query = query.where(item_table.item_group == item_group)
return query.run(as_dict=True)

View File

@@ -0,0 +1,61 @@
{% $.each(stock_summary, (idx, row) => { %}
<div class="row" style="border-bottom:1px solid var(--border-color); padding:4px 5px; margin-top: 3px;margin-bottom: 3px;">
<div class="col-sm-1">
{% if(row.image) { %}
<img style="width:50px;height:50px;" src="{{row.image}}">
{% } else { %}
<div style="width:50px;height:50px;background-color:var(--control-bg);text-align:center;padding-top:15px">{{frappe.get_abbr(row.item_code, 2)}}</div>
{% } %}
</div>
<div class="col-sm-3">
{% if (row.item_code === row.item_name) { %}
{{row.item_link}}
{% } else { %}
{{row.item_link}}
<p>
{{row.item_name}}
</p>
{% } %}
</div>
<div class="col-sm-1" title="{{ __('Actual Qty') }}">
{{ frappe.format(row.actual_qty, { fieldtype: "Float"})}}
</div>
<div class="col-sm-1" title="{{ __('Reserved Stock') }}">
{{ frappe.format(row.reserved_stock, { fieldtype: "Float"})}}
</div>
<div class="col-sm-4 small">
<span class="inline-graph">
<span class="inline-graph-half" title="{{ __("Reserved Qty") }}">
<span class="inline-graph-count">{{ row.total_reserved }}</span>
<span class="inline-graph-bar">
<span class="inline-graph-bar-inner"
style="width: {{ cint(Math.abs(row.total_reserved)/row.max_count * 100) || 5 }}%">
</span>
</span>
</span>
<span class="inline-graph-half" title="{{ __("Actual Qty {0} / Waiting Qty {1}", [row.actual_qty, row.pending_qty]) }}">
<span class="inline-graph-count">
{{ row.actual_qty }} {{ (row.pending_qty > 0) ? ("(" + row.pending_qty+ ")") : "" }}
</span>
<span class="inline-graph-bar">
<span class="inline-graph-bar-inner dark"
style="width: {{ cint(row.actual_qty/row.max_count * 100) }}%">
</span>
{% if row.pending_qty > 0 %}
<span class="inline-graph-bar-inner" title="{{ __("Projected Qty") }}"
style="width: {{ cint(row.pending_qty/row.max_count * 100) }}%">
</span>
{% endif %}
</span>
</span>
</span>
</div>
<div class="col-sm-1">
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add" data-item-code="{{ escape(row.item_code) }}">Add</button>
</div>
<div class="col-sm-1">
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-move" data-item-code="{{ escape(row.item_code) }}">Move</button>
</div>
</div>
{% }); %}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestPlantFloor(FrappeTestCase):
pass

View File

@@ -1334,10 +1334,10 @@ def get_sales_orders(self):
)
date_field_mapper = {
"from_date": self.from_date >= so.transaction_date,
"to_date": self.to_date <= so.transaction_date,
"from_delivery_date": self.from_delivery_date >= so_item.delivery_date,
"to_delivery_date": self.to_delivery_date <= so_item.delivery_date,
"from_date": so.transaction_date >= self.from_date,
"to_date": so.transaction_date <= self.to_date,
"from_delivery_date": so_item.delivery_date >= self.from_delivery_date,
"to_delivery_date": so_item.delivery_date <= self.to_delivery_date,
}
for field, value in date_field_mapper.items():

View File

@@ -1511,14 +1511,14 @@ def get_serial_nos_for_work_order(work_order, production_item):
def validate_operation_data(row):
if row.get("qty") <= 0:
if flt(row.get("qty")) <= 0:
frappe.throw(
_("Quantity to Manufacture can not be zero for the operation {0}").format(
frappe.bold(row.get("operation"))
)
)
if row.get("qty") > row.get("pending_qty"):
if flt(row.get("qty")) > flt(row.get("pending_qty")):
frappe.throw(
_("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})").format(
frappe.bold(row.get("operation")),

View File

@@ -2,6 +2,28 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Workstation", {
set_illustration_image(frm) {
let status_image_field = frm.doc.status == "Production" ? frm.doc.on_status_image : frm.doc.off_status_image;
if (status_image_field) {
frm.sidebar.image_wrapper.find(".sidebar-image").attr("src", status_image_field);
}
},
refresh(frm) {
frm.trigger("set_illustration_image");
frm.trigger("prepapre_dashboard");
},
prepapre_dashboard(frm) {
let $parent = $(frm.fields_dict["workstation_dashboard"].wrapper);
$parent.empty();
let workstation_dashboard = new WorkstationDashboard({
wrapper: $parent,
frm: frm
});
},
onload(frm) {
if(frm.is_new())
{
@@ -54,3 +76,243 @@ frappe.tour['Workstation'] = [
];
class WorkstationDashboard {
constructor({ wrapper, frm }) {
this.$wrapper = $(wrapper);
this.frm = frm;
this.prepapre_dashboard();
}
prepapre_dashboard() {
frappe.call({
method: "erpnext.manufacturing.doctype.workstation.workstation.get_job_cards",
args: {
workstation: this.frm.doc.name
},
callback: (r) => {
if (r.message) {
this.job_cards = r.message;
this.render_job_cards();
}
}
});
}
render_job_cards() {
let template = frappe.render_template("workstation_job_card", {
data: this.job_cards
});
this.$wrapper.html(template);
this.prepare_timer();
this.toggle_job_card();
this.bind_events();
}
toggle_job_card() {
this.$wrapper.find(".collapse-indicator-job").on("click", (e) => {
$(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").toggleClass("hide")
if ($(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").hasClass("hide"))
$(e.currentTarget).html(frappe.utils.icon("es-line-down", "sm", "mb-1"))
else
$(e.currentTarget).html(frappe.utils.icon("es-line-up", "sm", "mb-1"))
});
}
bind_events() {
this.$wrapper.find(".make-material-request").on("click", (e) => {
let job_card = $(e.currentTarget).attr("job-card");
this.make_material_request(job_card);
});
this.$wrapper.find(".btn-start").on("click", (e) => {
let job_card = $(e.currentTarget).attr("job-card");
this.start_job(job_card);
});
this.$wrapper.find(".btn-complete").on("click", (e) => {
let job_card = $(e.currentTarget).attr("job-card");
let pending_qty = flt($(e.currentTarget).attr("pending-qty"));
this.complete_job(job_card, pending_qty);
});
}
start_job(job_card) {
let me = this;
frappe.prompt([
{
fieldtype: 'Datetime',
label: __('Start Time'),
fieldname: 'start_time',
reqd: 1,
default: frappe.datetime.now_datetime()
},
{
label: __('Operator'),
fieldname: 'employee',
fieldtype: 'Link',
options: 'Employee',
}
], data => {
this.frm.call({
method: "start_job",
doc: this.frm.doc,
args: {
job_card: job_card,
from_time: data.start_time,
employee: data.employee,
},
callback(r) {
if (r.message) {
me.job_cards = [r.message];
me.prepare_timer()
me.update_job_card_details();
}
}
});
}, __("Enter Value"), __("Start Job"));
}
complete_job(job_card, qty_to_manufacture) {
let me = this;
let fields = [
{
fieldtype: 'Float',
label: __('Completed Quantity'),
fieldname: 'qty',
reqd: 1,
default: flt(qty_to_manufacture || 0)
},
{
fieldtype: 'Datetime',
label: __('End Time'),
fieldname: 'end_time',
default: frappe.datetime.now_datetime()
},
];
frappe.prompt(fields, data => {
if (data.qty <= 0) {
frappe.throw(__("Quantity should be greater than 0"));
}
this.frm.call({
method: "complete_job",
doc: this.frm.doc,
args: {
job_card: job_card,
qty: data.qty,
to_time: data.end_time,
},
callback: function(r) {
if (r.message) {
me.job_cards = [r.message];
me.prepare_timer()
me.update_job_card_details();
}
}
});
}, __("Enter Value"), __("Submit"));
}
make_material_request(job_card) {
frappe.call({
method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request",
args: {
source_name: job_card,
},
callback: (r) => {
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
});
}
prepare_timer() {
this.job_cards.forEach((data) => {
if (data.time_logs?.length) {
data._current_time = this.get_current_time(data);
if (data.time_logs[cint(data.time_logs.length) - 1].to_time) {
this.updateStopwatch(data);
} else {
this.initialiseTimer(data);
}
}
});
}
update_job_card_details() {
let color_map = {
"Pending": "var(--bg-blue)",
"In Process": "var(--bg-yellow)",
"Submitted": "var(--bg-blue)",
"Open": "var(--bg-gray)",
"Closed": "var(--bg-green)",
"Work In Progress": "var(--bg-orange)",
}
this.job_cards.forEach((data) => {
let job_card_selector = this.$wrapper.find(`
[data-name='${data.name}']`
);
$(job_card_selector).find(".job-card-status").text(data.status);
$(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]);
if (data.status === "Work In Progress") {
$(job_card_selector).find(".btn-start").addClass("hide");
$(job_card_selector).find(".btn-complete").removeClass("hide");
} else if (data.status === "Completed") {
$(job_card_selector).find(".btn-start").addClass("hide");
$(job_card_selector).find(".btn-complete").addClass("hide");
}
});
}
initialiseTimer(data) {
setInterval(() => {
data._current_time += 1;
this.updateStopwatch(data);
}, 1000);
}
updateStopwatch(data) {
let increment = data._current_time;
let hours = Math.floor(increment / 3600);
let minutes = Math.floor((increment - (hours * 3600)) / 60);
let seconds = cint(increment - (hours * 3600) - (minutes * 60));
let job_card_selector = `[data-job-card='${data.name}']`
let timer_selector = this.$wrapper.find(job_card_selector)
$(timer_selector).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString());
$(timer_selector).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString());
$(timer_selector).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString());
}
get_current_time(data) {
let current_time = 0.0;
data.time_logs.forEach(d => {
if (d.to_time) {
if (d.time_in_mins) {
current_time += flt(d.time_in_mins, 2) * 60;
} else {
current_time += this.get_seconds_diff(d.to_time, d.from_time);
}
} else {
current_time += this.get_seconds_diff(frappe.datetime.now_datetime(), d.from_time);
}
});
return current_time;
}
get_seconds_diff(d1, d2) {
return moment(d1).diff(d2, "seconds");
}
}

View File

@@ -8,10 +8,24 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"dashboard_tab",
"workstation_dashboard",
"details_tab",
"workstation_name",
"production_capacity",
"column_break_3",
"workstation_type",
"plant_floor",
"column_break_3",
"production_capacity",
"warehouse",
"production_capacity_section",
"parts_per_hour",
"workstation_status_tab",
"status",
"column_break_glcv",
"illustration_section",
"on_status_image",
"column_break_etmc",
"off_status_image",
"over_heads",
"hour_rate_electricity",
"hour_rate_consumable",
@@ -24,7 +38,9 @@
"description",
"working_hours_section",
"holiday_list",
"working_hours"
"working_hours",
"total_working_hours",
"connections_tab"
],
"fields": [
{
@@ -120,9 +136,10 @@
},
{
"default": "1",
"description": "Run parallel job cards in a workstation",
"fieldname": "production_capacity",
"fieldtype": "Int",
"label": "Production Capacity",
"label": "Job Capacity",
"reqd": 1
},
{
@@ -145,12 +162,97 @@
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"fieldname": "plant_floor",
"fieldtype": "Link",
"label": "Plant Floor",
"options": "Plant Floor"
},
{
"fieldname": "workstation_status_tab",
"fieldtype": "Tab Break",
"label": "Workstation Status"
},
{
"fieldname": "illustration_section",
"fieldtype": "Section Break",
"label": "Status Illustration"
},
{
"fieldname": "column_break_etmc",
"fieldtype": "Column Break"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Production\nOff\nIdle\nProblem\nMaintenance\nSetup"
},
{
"fieldname": "column_break_glcv",
"fieldtype": "Column Break"
},
{
"fieldname": "on_status_image",
"fieldtype": "Attach Image",
"label": "Active Status"
},
{
"fieldname": "off_status_image",
"fieldtype": "Attach Image",
"label": "Inactive Status"
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
},
{
"fieldname": "production_capacity_section",
"fieldtype": "Section Break",
"label": "Production Capacity"
},
{
"fieldname": "parts_per_hour",
"fieldtype": "Float",
"label": "Parts Per Hour"
},
{
"fieldname": "total_working_hours",
"fieldtype": "Float",
"label": "Total Working Hours"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "dashboard_tab",
"fieldtype": "Tab Break",
"label": "Job Cards"
},
{
"fieldname": "details_tab",
"fieldtype": "Tab Break",
"label": "Details"
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "workstation_dashboard",
"fieldtype": "HTML",
"label": "Workstation Dashboard"
}
],
"icon": "icon-wrench",
"idx": 1,
"image_field": "on_status_image",
"links": [],
"modified": "2022-11-04 17:39:01.549346",
"modified": "2023-11-30 12:43:35.808845",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation",

View File

@@ -11,7 +11,11 @@ from frappe.utils import (
comma_and,
flt,
formatdate,
get_link_to_form,
get_time,
get_url_to_form,
getdate,
time_diff_in_hours,
time_diff_in_seconds,
to_timedelta,
)
@@ -60,6 +64,23 @@ class Workstation(Document):
def before_save(self):
self.set_data_based_on_workstation_type()
self.set_hour_rate()
self.set_total_working_hours()
def set_total_working_hours(self):
self.total_working_hours = 0.0
for row in self.working_hours:
self.validate_working_hours(row)
if row.start_time and row.end_time:
row.hours = flt(time_diff_in_hours(row.end_time, row.start_time), row.precision("hours"))
self.total_working_hours += row.hours
def validate_working_hours(self, row):
if not (row.start_time and row.end_time):
frappe.throw(_("Row #{0}: Start Time and End Time are required").format(row.idx))
if get_time(row.start_time) >= get_time(row.end_time):
frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))
def set_hour_rate(self):
self.hour_rate = (
@@ -143,6 +164,141 @@ class Workstation(Document):
return schedule_date
@frappe.whitelist()
def start_job(self, job_card, from_time, employee):
doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", {"from_time": from_time, "employee": employee})
doc.save(ignore_permissions=True)
return doc
@frappe.whitelist()
def complete_job(self, job_card, qty, to_time):
doc = frappe.get_doc("Job Card", job_card)
for row in doc.time_logs:
if not row.to_time:
row.to_time = to_time
row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60
row.completed_qty = qty
doc.save(ignore_permissions=True)
doc.submit()
return doc
@frappe.whitelist()
def get_job_cards(workstation):
if frappe.has_permission("Job Card", "read"):
jc_data = frappe.get_all(
"Job Card",
fields=[
"name",
"production_item",
"work_order",
"operation",
"total_completed_qty",
"for_quantity",
"transferred_qty",
"status",
"expected_start_date",
"expected_end_date",
"time_required",
"wip_warehouse",
],
filters={
"workstation": workstation,
"docstatus": ("<", 2),
"status": ["not in", ["Completed", "Stopped"]],
},
order_by="expected_start_date, expected_end_date",
)
job_cards = [row.name for row in jc_data]
raw_materials = get_raw_materials(job_cards)
time_logs = get_time_logs(job_cards)
allow_excess_transfer = frappe.db.get_single_value(
"Manufacturing Settings", "job_card_excess_transfer"
)
for row in jc_data:
row.progress_percent = (
flt(row.total_completed_qty / row.for_quantity * 100, 2) if row.for_quantity else 0
)
row.progress_title = _("Total completed quantity: {0}").format(row.total_completed_qty)
row.status_color = get_status_color(row.status)
row.job_card_link = get_link_to_form("Job Card", row.name)
row.work_order_link = get_link_to_form("Work Order", row.work_order)
row.raw_materials = raw_materials.get(row.name, [])
row.time_logs = time_logs.get(row.name, [])
row.make_material_request = False
if row.for_quantity > row.transferred_qty or allow_excess_transfer:
row.make_material_request = True
return jc_data
def get_status_color(status):
color_map = {
"Pending": "var(--bg-blue)",
"In Process": "var(--bg-yellow)",
"Submitted": "var(--bg-blue)",
"Open": "var(--bg-gray)",
"Closed": "var(--bg-green)",
"Work In Progress": "var(--bg-orange)",
}
return color_map.get(status, "var(--bg-blue)")
def get_raw_materials(job_cards):
raw_materials = {}
data = frappe.get_all(
"Job Card Item",
fields=[
"parent",
"item_code",
"item_group",
"uom",
"item_name",
"source_warehouse",
"required_qty",
"transferred_qty",
],
filters={"parent": ["in", job_cards]},
)
for row in data:
raw_materials.setdefault(row.parent, []).append(row)
return raw_materials
def get_time_logs(job_cards):
time_logs = {}
data = frappe.get_all(
"Job Card Time Log",
fields=[
"parent",
"name",
"employee",
"from_time",
"to_time",
"time_in_mins",
],
filters={"parent": ["in", job_cards], "parentfield": "time_logs"},
order_by="parent, idx",
)
for row in data:
time_logs.setdefault(row.parent, []).append(row)
return time_logs
@frappe.whitelist()
def get_default_holiday_list():
@@ -201,3 +357,52 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime):
+ "\n".join(applicable_holidays),
WorkstationHolidayError,
)
@frappe.whitelist()
def get_workstations(**kwargs):
kwargs = frappe._dict(kwargs)
_workstation = frappe.qb.DocType("Workstation")
query = (
frappe.qb.from_(_workstation)
.select(
_workstation.name,
_workstation.description,
_workstation.status,
_workstation.on_status_image,
_workstation.off_status_image,
)
.orderby(_workstation.workstation_type, _workstation.name)
.where(_workstation.plant_floor == kwargs.plant_floor)
)
if kwargs.workstation:
query = query.where(_workstation.name == kwargs.workstation)
if kwargs.workstation_type:
query = query.where(_workstation.workstation_type == kwargs.workstation_type)
if kwargs.workstation_status:
query = query.where(_workstation.status == kwargs.workstation_status)
data = query.run(as_dict=True)
color_map = {
"Production": "var(--green-600)",
"Off": "var(--gray-600)",
"Idle": "var(--gray-600)",
"Problem": "var(--red-600)",
"Maintenance": "var(--yellow-600)",
"Setup": "var(--blue-600)",
}
for d in data:
d.workstation_name = get_link_to_form("Workstation", d.name)
d.status_image = d.on_status_image
d.background_color = color_map.get(d.status, "var(--red-600)")
d.workstation_link = get_url_to_form("Workstation", d.name)
if d.status != "Production":
d.status_image = d.off_status_image
return data

View File

@@ -0,0 +1,125 @@
<style>
.job-card-link {
min-height: 100px;
}
.section-head-job-card {
margin-bottom: 0px;
padding-bottom: 0px;
}
</style>
<div style = "max-height: 400px; overflow-y: auto;">
{% $.each(data, (idx, d) => { %}
<div class="row form-dashboard-section job-card-link form-links border-gray-200" data-name="{{d.name}}">
<div class="section-head section-head-job-card">
{{ d.operation }} - {{ d.production_item }}
<span class="ml-2 collapse-indicator-job mb-1" style="">
{{frappe.utils.icon("es-line-down", "sm", "mb-1")}}
</span>
</div>
<div class="row form-section" style="width:100%;margin-bottom:10px">
<div class="form-column col-sm-3">
<div class="frappe-control" title="{{__('Job Card')}}" style="text-decoration:underline">
{{ d.job_card_link }}
</div>
<div class="frappe-control" title="{{__('Work Order')}}" style="text-decoration:underline">
{{ d.work_order_link }}
</div>
</div>
<div class="form-column col-sm-2">
<div class="frappe-control timer" title="{{__('Timer')}}" style="text-align:center;font-size:14px;" data-job-card = {{escape(d.name)}}>
<span class="hours">00</span>
<span class="colon">:</span>
<span class="minutes">00</span>
<span class="colon">:</span>
<span class="seconds">00</span>
</div>
{% if(d.status === "Open") { %}
<div class="frappe-control" title="{{__('Expected Start Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
{{ frappe.format(d.expected_start_date, { fieldtype: 'Datetime' }) }}
</div>
{% } else { %}
<div class="frappe-control" title="{{__('Expected End Date')}}" style="text-align:center;font-size:11px;padding-top: 4px;">
{{ frappe.format(d.expected_end_date, { fieldtype: 'Datetime' }) }}
</div>
{% } %}
</div>
<div class="form-column col-sm-2">
<div class="frappe-control job-card-status" title="{{__('Status')}}" style="background:{{d.status_color}};text-align:center;border-radius:var(--border-radius-full)">
{{ d.status }}
</div>
</div>
<div class="form-column col-sm-2">
<div class="frappe-control" title="{{__('Qty to Manufacture')}}">
<div class="progress" title = "{{d.progress_title}}">
<div class="progress-bar progress-bar-success" style="width: {{d.progress_percent}}%">
</div>
</div>
</div>
<div class="frappe-control" style="text-align: center; font-size: 10px;">
{{ d.for_quantity }} / {{ d.total_completed_qty }}
</div>
</div>
<div class="form-column col-sm-2 text-center">
<button style="width: 85px;" class="btn btn-default btn-start {% if(d.status !== "Open") { %} hide {% } %}" job-card="{{d.name}}"> {{__("Start")}} </button>
<button style="width: 85px;" class="btn btn-default btn-complete {% if(d.status === "Open") { %} hide {% } %}" job-card="{{d.name}}" pending-qty="{{d.for_quantity - d.transferred_qty}}"> {{__("Complete")}} </button>
</div>
</div>
<div class="section-body section-body-job-card form-section hide">
<hr>
<div class="row">
<div class="form-column col-sm-2">
{{ __("Raw Materials") }}
</div>
{% if(d.make_material_request) { %}
<div class="form-column col-sm-10 text-right">
<button class="btn btn-default btn-xs make-material-request" job-card="{{d.name}}">{{ __("Material Request") }}</button>
</div>
{% } %}
</div>
{% if(d.raw_materials) { %}
<table class="table table-bordered table-condensed">
<thead>
<tr>
<th style="width: 5%" class="table-sr">Sr</th>
<th style="width: 15%">{{ __("Item") }}</th>
<th style="width: 15%">{{ __("Warehouse") }}</th>
<th style="width: 10%">{{__("UOM")}}</th>
<th style="width: 15%">{{__("Item Group")}}</th>
<th style="width: 20%" >{{__("Required Qty")}}</th>
<th style="width: 20%" >{{__("Transferred Qty")}}</th>
</tr>
</thead>
<tbody>
{% $.each(d.raw_materials, (row_index, child_row) => { %}
<tr>
<td class="table-sr">{{ row_index+1 }}</td>
{% if(child_row.item_code === child_row.item_name) { %}
<td>{{ child_row.item_code }}</td>
{% } else { %}
<td>{{ child_row.item_code }}: {{child_row.item_name}}</td>
{% } %}
<td>{{ child_row.source_warehouse }}</td>
<td>{{ child_row.uom }}</td>
<td>{{ child_row.item_group }}</td>
<td>{{ child_row.required_qty }}</td>
<td>{{ child_row.transferred_qty }}</td>
</tr>
{% }); %}
</tbody>
{% } %}
</table>
</div>
</div>
{% }); %}
</div>

View File

@@ -1,5 +1,16 @@
frappe.listview_settings['Workstation'] = {
// add_fields: ["status"],
// filters:[["status","=", "Open"]]
add_fields: ["status"],
get_indicator: function(doc) {
let color_map = {
"Production": "green",
"Off": "gray",
"Idle": "gray",
"Problem": "red",
"Maintenance": "yellow",
"Setup": "blue",
}
return [__(doc.status), color_map[doc.status], true];
}
};

View File

@@ -1,150 +1,58 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2014-12-24 14:46:40.678236",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2014-12-24 14:46:40.678236",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"start_time",
"hours",
"column_break_2",
"end_time",
"enabled"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "start_time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Start Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "end_time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "End Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Enabled"
},
{
"fieldname": "hours",
"fieldtype": "Float",
"label": "Hours",
"read_only": 1
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-12-13 05:02:36.754145",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation Working Hour",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
],
"istable": 1,
"links": [],
"modified": "2023-10-25 14:48:29.697498",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation Working Hour",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,13 @@
frappe.pages['visual-plant-floor'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Visual Plant Floor',
single_column: true
});
frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor(
{wrapper: $(wrapper).find('.layout-main-section')}, wrapper.page
);
}

View File

@@ -0,0 +1,29 @@
{
"content": null,
"creation": "2023-10-06 15:17:39.215300",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2023-10-06 15:18:00.622073",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "visual-plant-floor",
"owner": "Administrator",
"page_name": "visual-plant-floor",
"roles": [
{
"role": "Manufacturing User"
},
{
"role": "Manufacturing Manager"
},
{
"role": "Operator"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "Visual Plant Floor"
}

View File

@@ -1,6 +1,6 @@
{
"charts": [],
"content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"Ubj6zXcmIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"custom_blocks": [],
"docstatus": 0,
@@ -316,7 +316,7 @@
"type": "Link"
}
],
"modified": "2023-08-08 22:28:39.633891",
"modified": "2024-01-30 21:49:58.577218",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -336,6 +336,13 @@
"type": "URL",
"url": "https://frappe.school/courses/manufacturing?utm_source=in_app"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Plant Floor",
"link_to": "Plant Floor",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",

View File

@@ -7,6 +7,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
super.setup();
let me = this;
this.set_fields_onload_for_line_item();
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frappe.flags.hide_serial_batch_dialog = true;
@@ -105,6 +106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.ui.form.on(this.frm.doctype + " Item", {
items_add: function(frm, cdt, cdn) {
debugger
var item = frappe.get_doc(cdt, cdn);
if (!item.warehouse && frm.doc.set_warehouse) {
item.warehouse = frm.doc.set_warehouse;
@@ -118,6 +120,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.from_warehouse = frm.doc.set_from_warehouse;
}
if (item.docstatus === 0
&& frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
&& cint(frappe.user_defaults?.use_serial_batch_fields) === 1
) {
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
}
erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items');
}
});
@@ -222,7 +231,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
};
});
}
}
set_fields_onload_for_line_item() {
if (this.frm.is_new && this.frm.doc?.items) {
this.frm.doc.items.forEach(item => {
if (item.docstatus === 0
&& frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
&& cint(frappe.user_defaults?.use_serial_batch_fields) === 1
) {
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
}
})
}
}
toggle_enable_for_stock_uom(field) {
@@ -462,6 +483,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.doc.doctype === 'Delivery Note') {
show_batch_dialog = 1;
}
if (show_batch_dialog && item.use_serial_batch_fields === 1) {
show_batch_dialog = 0;
}
item.barcode = null;
@@ -502,6 +528,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
project: item.project || me.frm.doc.project,
qty: item.qty || 1,
net_rate: item.rate,
base_net_rate: item.base_net_rate,
stock_qty: item.stock_qty,
conversion_factor: item.conversion_factor,
weight_per_unit: item.weight_per_unit,
@@ -705,10 +732,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.serial_no = item.serial_no.replace(/,/g, '\n');
item.conversion_factor = item.conversion_factor || 1;
refresh_field("serial_no", item.name, item.parentfield);
if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
if (!doc.is_return) {
setTimeout(() => {
me.update_qty(cdt, cdn);
}, 10000);
}, 3000);
}
}
}
@@ -790,24 +817,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (me.frm.doc.price_list_currency == company_currency) {
me.frm.set_value('plc_conversion_rate', 1.0);
}
if (company_doc && company_doc.default_letter_head) {
if(me.frm.fields_dict.letter_head) {
me.frm.set_value("letter_head", company_doc.default_letter_head);
if (company_doc){
if (company_doc.default_letter_head) {
if(me.frm.fields_dict.letter_head) {
me.frm.set_value("letter_head", company_doc.default_letter_head);
}
}
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
me.frm.set_value("tc_name", company_doc.default_selling_terms);
}
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
"Material Request", "Purchase Receipt"];
// Purchase Invoice is excluded as per issue #3345
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
me.frm.set_value("tc_name", company_doc.default_buying_terms);
}
}
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
me.frm.set_value("tc_name", company_doc.default_selling_terms);
}
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
"Material Request", "Purchase Receipt"];
// Purchase Invoice is excluded as per issue #3345
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
me.frm.set_value("tc_name", company_doc.default_buying_terms);
}
frappe.run_serially([
() => me.frm.script_manager.trigger("currency"),
() => me.update_item_tax_map(),
@@ -1199,8 +1227,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let item = frappe.get_doc(cdt, cdn);
// item.pricing_rules = ''
frappe.run_serially([
() => this.remove_pricing_rule(item),
() => this.remove_pricing_rule_for_item(item),
() => this.conversion_factor(doc, cdt, cdn, true),
() => this.apply_price_list(item, true), //reapply price list before applying pricing rule
() => this.calculate_stock_uom_rate(doc, cdt, cdn),
() => this.apply_pricing_rule(item, true)
]);
@@ -1239,20 +1268,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
sync_bundle_data() {
let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.sync_bundle_data();
barcode_scanner.remove_item_from_localstorage();
}
}
before_save(doc) {
this.sync_bundle_data();
}
service_start_date(frm, cdt, cdn) {
var child = locals[cdt][cdn];
@@ -1447,8 +1462,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
ignore_pricing_rule() {
if(this.frm.doc.ignore_pricing_rule) {
var me = this;
var item_list = [];
let me = this;
let item_list = [];
$.each(this.frm.doc["items"] || [], function(i, d) {
if (d.item_code) {
@@ -1487,6 +1502,34 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
remove_pricing_rule_for_item(item) {
let me = this;
return this.frm.call({
method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item",
args: {
pricing_rules: item.pricing_rules,
item_details: {
"doctype": item.doctype,
"name": item.name,
"item_code": item.item_code,
"pricing_rules": item.pricing_rules,
"parenttype": item.parenttype,
"parent": item.parent,
"price_list_rate": item.price_list_rate
},
item_code: item.item_code,
rate: item.price_list_rate,
},
callback: function(r) {
if (!r.exc && r.message) {
me.remove_pricing_rule(r.message);
me.calculate_taxes_and_totals();
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
}
}
});
}
apply_pricing_rule(item, calculate_taxes_and_totals) {
var me = this;
var args = this._get_args(item);
@@ -1711,8 +1754,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.set_value("plc_conversion_rate", "");
}
var me = this;
var args = this._get_args(item);
let me = this;
let args = this._get_args(item);
if (!((args.items && args.items.length) || args.price_list)) {
return;
}
@@ -1754,7 +1797,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
if(item.remove_free_item) {
var items = [];
let items = [];
me.frm.doc.items.forEach(d => {
if(d.item_code != item.remove_free_item || !d.is_free_item) {
@@ -1872,7 +1915,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (item.item_code) {
// Use combination of name and item code in case same item is added multiple times
item_codes.push([item.item_code, item.name]);
item_rates[item.name] = item.net_rate;
item_rates[item.name] = item.base_net_rate;
item_tax_templates[item.name] = item.item_tax_template;
}
});

View File

@@ -5,6 +5,8 @@ import "./sms_manager";
import "./utils/party";
import "./controllers/stock_controller";
import "./payment/payments";
import "./templates/visual_plant_floor_template.html";
import "./plant_floor_visual/visual_plant";
import "./controllers/taxes_and_totals";
import "./controllers/transaction";
import "./templates/item_selector.html";

View File

@@ -2,7 +2,58 @@ frappe.provide("erpnext.financial_statements");
erpnext.financial_statements = {
"filters": get_filters(),
"baseData": null,
"formatter": function(value, row, column, data, default_formatter, filter) {
if(frappe.query_report.get_filter_value("selected_view") == "Growth" && data && column.colIndex >= 3){
//Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns.
const lastAnnualValue = row[column.colIndex - 1].content;
const currentAnnualvalue = data[column.fieldname];
if(currentAnnualvalue == undefined) return 'NA'; //making this not applicable for undefined/null values
let annualGrowth = 0;
if(lastAnnualValue == 0 && currentAnnualvalue > 0){
//If the previous year value is 0 and the current value is greater than 0
annualGrowth = 1;
}
else if(lastAnnualValue > 0){
annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue;
}
const growthPercent = (Math.round(annualGrowth*10000)/100); //calculating the rounded off percentage
value = $(`<span>${((growthPercent >=0)? '+':'' )+growthPercent+'%'}</span>`);
if(growthPercent < 0){
value = $(value).addClass("text-danger");
}
else{
value = $(value).addClass("text-success");
}
value = $(value).wrap("<p></p>").parent().html();
return value;
}
else if(frappe.query_report.get_filter_value("selected_view") == "Margin" && data){
if(column.fieldname =="account" && data.account_name == __("Income")){
//Taking the total income from each column (for all the financial years) as the base (100%)
this.baseData = row;
}
if(column.colIndex >= 2){
//Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns.
const currentAnnualvalue = data[column.fieldname];
const baseValue = this.baseData[column.colIndex].content;
if(currentAnnualvalue == undefined || baseValue <= 0) return 'NA';
const marginPercent = Math.round((currentAnnualvalue/baseValue)*10000)/100;
value = $(`<span>${marginPercent+'%'}</span>`);
if(marginPercent < 0)
value = $(value).addClass("text-danger");
else
value = $(value).addClass("text-success");
value = $(value).wrap("<p></p>").parent().html();
return value;
}
}
if (data && column.fieldname=="account") {
value = data.account_name || value;
@@ -74,22 +125,24 @@ erpnext.financial_statements = {
});
});
const views_menu = report.page.add_custom_button_group(__('Financial Statements'));
if (report.page){
const views_menu = report.page.add_custom_button_group(__('Financial Statements'));
report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
});
report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
});
report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
});
report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
});
report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
});
report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
});
}
}
};

View File

@@ -0,0 +1,157 @@
class VisualPlantFloor {
constructor({wrapper, skip_filters=false, plant_floor=null}, page=null) {
this.wrapper = wrapper;
this.plant_floor = plant_floor;
this.skip_filters = skip_filters;
this.make();
if (!this.skip_filters) {
this.page = page;
this.add_filter();
this.prepare_menu();
}
}
make() {
this.wrapper.append(`
<div class="plant-floor">
<div class="plant-floor-filter">
</div>
<div class="plant-floor-container col-sm-12">
</div>
</div>
`);
if (!this.skip_filters) {
this.filter_wrapper = this.wrapper.find('.plant-floor-filter');
this.visualization_wrapper = this.wrapper.find('.plant-floor-visualization');
} else if(this.plant_floor) {
this.wrapper.find('.plant-floor').css('border', 'none');
this.prepare_data();
}
}
prepare_data() {
frappe.call({
method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations',
args: {
plant_floor: this.plant_floor,
},
callback: (r) => {
this.workstations = r.message;
this.render_workstations();
}
});
}
add_filter() {
this.plant_floor = frappe.ui.form.make_control({
df: {
fieldtype: 'Link',
options: 'Plant Floor',
fieldname: 'plant_floor',
label: __('Plant Floor'),
reqd: 1,
onchange: () => {
this.render_plant_visualization();
}
},
parent: this.filter_wrapper,
render_input: true,
});
this.plant_floor.$wrapper.addClass('form-column col-sm-2');
this.workstation_type = frappe.ui.form.make_control({
df: {
fieldtype: 'Link',
options: 'Workstation Type',
fieldname: 'workstation_type',
label: __('Machine Type'),
onchange: () => {
this.render_plant_visualization();
}
},
parent: this.filter_wrapper,
render_input: true,
});
this.workstation_type.$wrapper.addClass('form-column col-sm-2');
this.workstation = frappe.ui.form.make_control({
df: {
fieldtype: 'Link',
options: 'Workstation',
fieldname: 'workstation',
label: __('Machine'),
onchange: () => {
this.render_plant_visualization();
},
get_query: () => {
if (this.workstation_type.get_value()) {
return {
filters: {
'workstation_type': this.workstation_type.get_value() || ''
}
}
}
}
},
parent: this.filter_wrapper,
render_input: true,
});
this.workstation.$wrapper.addClass('form-column col-sm-2');
this.workstation_status = frappe.ui.form.make_control({
df: {
fieldtype: 'Select',
options: '\nProduction\nOff\nIdle\nProblem\nMaintenance\nSetup',
fieldname: 'workstation_status',
label: __('Status'),
onchange: () => {
this.render_plant_visualization();
},
},
parent: this.filter_wrapper,
render_input: true,
});
}
render_plant_visualization() {
let plant_floor = this.plant_floor.get_value();
if (plant_floor) {
frappe.call({
method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations',
args: {
plant_floor: plant_floor,
workstation_type: this.workstation_type.get_value(),
workstation: this.workstation.get_value(),
workstation_status: this.workstation_status.get_value()
},
callback: (r) => {
this.workstations = r.message;
this.render_workstations();
}
});
}
}
render_workstations() {
this.wrapper.find('.plant-floor-container').empty();
let template = frappe.render_template("visual_plant_floor_template", {
workstations: this.workstations
});
$(template).appendTo(this.wrapper.find('.plant-floor-container'));
}
prepare_menu() {
this.page.add_menu_item(__('Refresh'), () => {
this.render_plant_visualization();
});
}
}
frappe.ui.VisualPlantFloor = VisualPlantFloor;

View File

@@ -0,0 +1,19 @@
{% $.each(workstations, (idx, row) => { %}
<div class="workstation-wrapper">
<div class="workstation-image">
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
<a class="workstation-image-link" href="{{row.workstation_link}}">
{% if(row.status_image) { %}
<img class="workstation-image-cls" src="{{row.status_image}}">
{% } else { %}
<div class="workstation-image-cls workstation-abbr">{{frappe.get_abbr(row.name, 2)}}</div>
{% } %}
</a>
</div>
</div>
<div class="workstation-card text-center">
<p style="background-color:{{row.background_color}};color:#fff">{{row.status}}</p>
<div>{{row.workstation_name}}</div>
</div>
</div>
{% }); %}

View File

@@ -1,12 +1,15 @@
erpnext.utils.BarcodeScanner = class BarcodeScanner {
constructor(opts) {
this.frm = opts.frm;
// frappe.flags.trigger_from_barcode_scanner is used for custom scripts
// field from which to capture input of scanned data
this.scan_field_name = opts.scan_field_name || "scan_barcode";
this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name];
this.barcode_field = opts.barcode_field || "barcode";
this.serial_no_field = opts.serial_no_field || "serial_no";
this.batch_no_field = opts.batch_no_field || "batch_no";
this.uom_field = opts.uom_field || "uom";
this.qty_field = opts.qty_field || "qty";
// field name on row which defines max quantity to be scanned e.g. picklist
@@ -105,53 +108,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.frm.has_items = false;
}
if (serial_no) {
this.is_duplicate_serial_no(row, item_code, serial_no)
.then((is_duplicate) => {
if (!is_duplicate) {
this.run_serially_tasks(row, data, resolve);
} else {
this.clean_up();
reject();
return;
}
});
} else {
this.run_serially_tasks(row, data, resolve);
if (this.is_duplicate_serial_no(row, serial_no)) {
this.clean_up();
reject();
return;
}
frappe.run_serially([
() => this.set_selector_trigger_flag(data),
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
this.show_scan_message(row.idx, row.item_code, qty);
}),
() => this.set_barcode_uom(row, uom),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_barcode(row, barcode),
() => this.clean_up(),
() => this.revert_selector_flag(),
() => resolve(row)
]);
});
}
run_serially_tasks(row, data, resolve) {
const {item_code, barcode, batch_no, serial_no, uom} = data;
// batch and serial selector is reduandant when all info can be added by scan
// this flag on item row is used by transaction.js to avoid triggering selector
set_selector_trigger_flag(data) {
const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
frappe.run_serially([
() => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
() => this.set_barcode(row, barcode),
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
this.show_scan_message(row.idx, row.item_code, qty);
}),
() => this.set_barcode_uom(row, uom),
() => this.clean_up(),
() => {
if (row.serial_and_batch_bundle && !this.frm.is_new()) {
this.frm.save();
}
const require_selecting_batch = has_batch_no && !batch_no;
const require_selecting_serial = has_serial_no && !serial_no;
frappe.flags.trigger_from_barcode_scanner = false;
},
() => resolve(row),
]);
if (!(require_selecting_batch || require_selecting_serial)) {
frappe.flags.hide_serial_batch_dialog = true;
}
}
revert_selector_flag() {
frappe.flags.hide_serial_batch_dialog = false;
frappe.flags.trigger_from_barcode_scanner = false;
}
set_item(row, item_code, barcode, batch_no, serial_no) {
return new Promise(resolve => {
const increment = async (value = 1) => {
const item_data = {item_code: item_code};
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
const item_data = {item_code: item_code, use_serial_batch_fields: 1.0};
frappe.flags.trigger_from_barcode_scanner = true;
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
await frappe.model.set_value(row.doctype, row.name, item_data);
return value;
};
@@ -160,6 +162,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
increment(value).then((value) => resolve(value));
});
} else if (this.frm.has_items) {
this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no);
} else {
increment().then((value) => resolve(value));
}
@@ -182,8 +186,9 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.model.set_value(row.doctype, row.name, item_data);
frappe.run_serially([
() => this.set_batch_no(row, this.dialog.get_value("batch_no")),
() => this.set_barcode(row, this.dialog.get_value("barcode")),
() => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")),
() => this.set_serial_no(row, this.dialog.get_value("serial_no")),
() => this.add_child_for_remaining_qty(row),
() => this.clean_up()
]);
@@ -337,144 +342,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
async set_serial_and_batch(row, item_code, serial_no, batch_no) {
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no);
} else if(row.serial_and_batch_bundle) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.update_serial_or_batch",
args: {
bundle_id: row.serial_and_batch_bundle,
serial_no: serial_no,
batch_no: batch_no,
},
})
}
}
async set_serial_no(row, serial_no) {
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
const existing_serial_nos = row[this.serial_no_field];
let new_serial_nos = "";
get_key_for_localstorage() {
let parts = this.frm.doc.name.split("-");
return parts[parts.length - 1] + this.frm.doc.doctype;
}
update_localstorage_scanned_data() {
let docname = this.frm.doc.name
if (localStorage[docname]) {
let items = JSON.parse(localStorage[docname]);
let existing_items = this.frm.doc.items.map(d => d.item_code);
if (!existing_items.length) {
localStorage.removeItem(docname);
return;
if (!!existing_serial_nos) {
new_serial_nos = existing_serial_nos + "\n" + serial_no;
} else {
new_serial_nos = serial_no;
}
for (let item_code in items) {
if (!existing_items.includes(item_code)) {
delete items[item_code];
}
}
localStorage[docname] = JSON.stringify(items);
await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
}
}
async set_bundle_in_localstorage(row, item_code, serial_no, batch_no) {
let docname = this.frm.doc.name
let entries = JSON.parse(localStorage.getItem(docname));
if (!entries) {
entries = {};
}
let key = item_code;
if (!entries[key]) {
entries[key] = [];
}
let existing_row = [];
if (!serial_no && batch_no) {
existing_row = entries[key].filter((e) => e.batch_no === batch_no);
if (existing_row.length) {
existing_row[0].qty += 1;
}
} else if (serial_no) {
existing_row = entries[key].filter((e) => e.serial_no === serial_no);
if (existing_row.length) {
frappe.throw(__("Serial No {0} has already scanned.", [serial_no]));
}
}
if (!existing_row.length) {
entries[key].push({
"serial_no": serial_no,
"batch_no": batch_no,
"qty": 1
});
}
localStorage.setItem(docname, JSON.stringify(entries));
// Auto remove from localstorage after 1 hour
setTimeout(() => {
localStorage.removeItem(docname);
}, 3600000)
}
remove_item_from_localstorage() {
let docname = this.frm.doc.name;
if (localStorage[docname]) {
localStorage.removeItem(docname);
}
}
async sync_bundle_data() {
let docname = this.frm.doc.name;
if (localStorage[docname]) {
let entries = JSON.parse(localStorage[docname]);
if (entries) {
for (let entry in entries) {
let row = this.frm.doc.items.filter((item) => {
if (item.item_code === entry) {
return true;
}
})[0];
if (row) {
this.create_serial_and_batch_bundle(row, entries, entry)
.then(() => {
if (!entries) {
localStorage.removeItem(docname);
}
});
}
}
}
}
}
async create_serial_and_batch_bundle(row, entries, key) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
args: {
entries: entries[key],
child_row: row,
doc: this.frm.doc,
warehouse: row.warehouse,
do_not_save: 1
},
callback: function(r) {
row.serial_and_batch_bundle = r.message.name;
delete entries[key];
}
})
}
async set_barcode_uom(row, uom) {
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
}
}
async set_batch_no(row, batch_no) {
if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
}
}
async set_barcode(row, barcode) {
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
@@ -490,58 +383,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
async is_duplicate_serial_no(row, item_code, serial_no) {
let is_duplicate = false;
const promise = new Promise((resolve, reject) => {
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
if (is_duplicate) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
is_duplicate_serial_no(row, serial_no) {
const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
resolve(is_duplicate);
} else if (row.serial_and_batch_bundle) {
this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
if (r.message) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
is_duplicate = r.message;
resolve(is_duplicate);
})
}
});
return await promise;
}
check_duplicate_serial_no_in_db(row, serial_no, response) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
args: {
serial_no: serial_no,
bundle_id: row.serial_and_batch_bundle
},
callback(r) {
response(r);
}
});
}
check_duplicate_serial_no_in_localstorage(item_code, serial_no) {
let docname = this.frm.doc.name
let entries = JSON.parse(localStorage.getItem(docname));
if (!entries) {
return false;
if (is_duplicate) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
let existing_row = [];
if (entries[item_code]) {
existing_row = entries[item_code].filter((e) => e.serial_no === serial_no);
}
return existing_row.length;
return is_duplicate;
}
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
@@ -587,4 +435,4 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
show_alert(msg, indicator, duration=3) {
frappe.show_alert({message: msg, indicator: indicator}, duration);
}
};
};

View File

@@ -25,6 +25,10 @@ erpnext.accounts.dimensions = {
},
setup_filters(frm, doctype) {
if (doctype == 'Payment Entry' && this.accounting_dimensions) {
frm.dimension_filters = this.accounting_dimensions
}
if (this.accounting_dimensions) {
this.accounting_dimensions.forEach((dimension) => {
frappe.model.with_doctype(dimension['document_type'], () => {

View File

@@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
let warehouse = this.item?.type_of_transaction === "Outward" ?
(this.item.warehouse || this.item.s_warehouse) : "";
if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') {
warehouse = this.get_warehouse();
}
return {
'item_code': this.item.item_code,
'warehouse': ["=", warehouse]

View File

@@ -490,3 +490,53 @@ body[data-route="pos"] {
.exercise-col {
padding: 10px;
}
.plant-floor, .workstation-wrapper, .workstation-card p {
border-radius: var(--border-radius-md);
border: 1px solid var(--border-color);
box-shadow: none;
background-color: var(--card-bg);
position: relative;
}
.plant-floor {
padding-bottom: 25px;
}
.plant-floor-filter {
padding-top: 10px;
display: flex;
flex-wrap: wrap;
}
.plant-floor-container {
display: grid;
grid-template-columns: repeat(6,minmax(0,1fr));
gap: var(--margin-xl);
}
@media screen and (max-width: 620px) {
.plant-floor-container {
grid-template-columns: repeat(2,minmax(0,1fr));
}
}
.plant-floor-container .workstation-card {
padding: 5px;
}
.plant-floor-container .workstation-image-link {
width: 100%;
font-size: 50px;
margin: var(--margin-sm);
min-height: 9rem;
}
.workstation-abbr {
display: flex;
background-color: var(--control-bg);
height:100%;
width:100%;
align-items: center;
justify-content: center;
}

View File

@@ -230,6 +230,7 @@ class Customer(TransactionBase):
if self.flags.is_new_doc:
self.link_lead_address_and_contact()
self.copy_communication()
self.update_customer_groups()
@@ -287,6 +288,17 @@ class Customer(TransactionBase):
linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name))
linked_doc.save(ignore_permissions=self.flags.ignore_permissions)
def copy_communication(self):
if not self.lead_name or not frappe.db.get_single_value(
"CRM Settings", "carry_forward_communication_and_comments"
):
return
from erpnext.crm.utils import copy_comments, link_communications
copy_comments("Lead", self.lead_name, self)
link_communications("Lead", self.lead_name, self)
def validate_name_with_customer_group(self):
if frappe.db.exists("Customer Group", self.name):
frappe.throw(

View File

@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"creation": "2013-06-20 11:53:21",
"description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials",
"description": "Aggregate a group of Items into another Item. This is useful if you are maintaining the stock of the packed items and not the bundled item",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -77,7 +77,7 @@
"icon": "fa fa-sitemap",
"idx": 1,
"links": [],
"modified": "2023-11-22 15:20:46.805114",
"modified": "2024-01-30 13:57:04.951788",
"modified_by": "Administrator",
"module": "Selling",
"name": "Product Bundle",

View File

@@ -391,7 +391,6 @@ def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_
balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0)
target.qty = balance_qty if balance_qty > 0 else 0
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
target.delivery_date = nowdate()
if obj.against_blanket_order:
target.against_blanket_order = obj.against_blanket_order

View File

@@ -87,7 +87,6 @@ class TestQuotation(FrappeTestCase):
self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name)
self.assertEqual(sales_order.customer, "_Test Customer")
sales_order.delivery_date = "2014-01-01"
sales_order.naming_series = "_T-Quotation-"
sales_order.transaction_date = nowdate()
sales_order.insert()
@@ -120,7 +119,6 @@ class TestQuotation(FrappeTestCase):
self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name)
self.assertEqual(sales_order.customer, "_Test Customer")
sales_order.delivery_date = "2014-01-01"
sales_order.naming_series = "_T-Quotation-"
sales_order.transaction_date = nowdate()
sales_order.insert()

View File

@@ -904,6 +904,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
target.run_method("set_use_serial_batch_fields")
if source.company_address:
target.update({"company_address": source.company_address})
@@ -1024,6 +1025,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
target.run_method("set_use_serial_batch_fields")
if source.company_address:
target.update({"company_address": source.company_address})
@@ -1606,7 +1608,11 @@ def create_pick_list(source_name, target_doc=None):
"Sales Order",
source_name,
{
"Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}},
"Sales Order": {
"doctype": "Pick List",
"field_map": {"set_warehouse": "parent_warehouse"},
"validation": {"docstatus": ["=", 1]},
},
"Sales Order Item": {
"doctype": "Pick List Item",
"field_map": {"parent": "sales_order", "name": "sales_order_item"},

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