Compare commits

..

204 Commits

Author SHA1 Message Date
Frappe PR Bot
7a6c0e5283 chore(release): Bumped to Version 14.67.1
## [14.67.1](https://github.com/frappe/erpnext/compare/v14.67.0...v14.67.1) (2024-04-12)

### Bug Fixes

* Subcontracting Receipt GL Entries (backport [#40773](https://github.com/frappe/erpnext/issues/40773)) (backport [#40978](https://github.com/frappe/erpnext/issues/40978)) ([#40982](https://github.com/frappe/erpnext/issues/40982)) ([4672f59](4672f59599))
2024-04-12 10:01:51 +00:00
mergify[bot]
4672f59599 fix: Subcontracting Receipt GL Entries (backport #40773) (backport #40978) (#40982)
* fix: Subcontracting Receipt GL Entries (backport #40773) (#40978)

* fix: Subcontracting Receipt GL Entries

(cherry picked from commit 9808ae92a4)

* chore: linter

---------

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

# Conflicts:
#	erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py

* chore: `conflicts`

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2024-04-12 15:30:27 +05:30
Frappe PR Bot
cc6bacb190 chore(release): Bumped to Version 14.67.0
# [14.67.0](https://github.com/frappe/erpnext/compare/v14.66.4...v14.67.0) (2024-04-10)

### Bug Fixes

* Get pro-rata depr amount based on correct days ([10d7600](10d760089e))
* group warehouse added in the stock reconciliation ([9dea6d3](9dea6d3393))
* ignore dimension validation for cancelled entries ([9745724](9745724d41))
* incorrect currency symbol in General Ledger print ([5896e75](5896e755bf))
* incorrect operator causing incorrect validation ([7e1ab75](7e1ab75b38))
* query_report.trigger_refresh is not a function ([a6145fa](a6145fa13c))
* translatable web footer ([#40834](https://github.com/frappe/erpnext/issues/40834)) ([b55d859](b55d8597e8))
* use reference type name to update exc rate ([09cda60](09cda60bdf))

### Features

* ledger health doctype ([b667a02](b667a02470))
* new hook `fields_for_group_similar_items` to group additional fields for print formats ([#40831](https://github.com/frappe/erpnext/issues/40831)) ([0172880](0172880fd3))

### Performance Improvements

* memory consumption for the Batch-Wise Balance History report ([3165682](3165682d7a))
2024-04-10 14:22:01 +00:00
Deepesh Garg
cebde2662d Merge pull request #40919 from frappe/version-14-hotfix
chore: release v14
2024-04-10 19:50:19 +05:30
ruthra kumar
73d2595aa6 Merge pull request #40937 from frappe/mergify/bp/version-14-hotfix/pr-40936
fix: ignore dimension validation for cancelled entries (backport #40936)
2024-04-10 15:28:00 +05:30
ruthra kumar
9745724d41 fix: ignore dimension validation for cancelled entries
(cherry picked from commit 971c867f29)
2024-04-10 08:28:35 +00:00
ruthra kumar
a31b2e17f5 Merge pull request #40929 from frappe/mergify/bp/version-14-hotfix/pr-40695
feat: Periodically monitor ledger health (backport #40695)
2024-04-10 07:17:29 +05:30
ruthra kumar
46c76b166d chore: resolve conflicts 2024-04-09 19:59:09 +05:30
ruthra kumar
6079c5636e Merge pull request #40927 from frappe/mergify/bp/version-14-hotfix/pr-40926
fix: incorrect currency symbol in General Ledger print (backport #40926)
2024-04-09 19:54:29 +05:30
ruthra kumar
5808feaccf chore: use super() instead of super(__class__, self)
(cherry picked from commit 24d37d22a3)
2024-04-09 12:25:55 +00:00
ruthra kumar
98ad034b21 chore: make ledger health doctype read_only
(cherry picked from commit dc79213bb3)
2024-04-09 12:25:55 +00:00
ruthra kumar
277717a2e0 test: ledger monitoring function
(cherry picked from commit 4776d660b5)
2024-04-09 12:25:55 +00:00
ruthra kumar
0591f72e01 chore: schedule job to run daily
(cherry picked from commit f96cf111ed)
2024-04-09 12:25:54 +00:00
ruthra kumar
1af6b4256f chore: permission and UI changes
(cherry picked from commit 1a43ed763b)
2024-04-09 12:25:53 +00:00
ruthra kumar
6273a31b8c refactor: only run checks on specified companies
(cherry picked from commit 00eeacd06a)

# Conflicts:
#	erpnext/accounts/utils.py
2024-04-09 12:25:53 +00:00
ruthra kumar
f86c035f88 refactor: make health check configurable for companies
(cherry picked from commit 704925549b)
2024-04-09 12:25:52 +00:00
ruthra kumar
9981a900b2 refactor: control monitoring through settings page
(cherry picked from commit a42482ce35)

# Conflicts:
#	erpnext/accounts/utils.py
2024-04-09 12:25:52 +00:00
ruthra kumar
7a6ffccecc chore: settings page for health monitor
(cherry picked from commit b2fb7843d1)
2024-04-09 12:25:47 +00:00
ruthra kumar
58698c9aa4 refactor: barebones method to run checks
(cherry picked from commit 8c8d9be810)

# Conflicts:
#	erpnext/accounts/utils.py
2024-04-09 12:25:47 +00:00
ruthra kumar
bbe4ca7d74 refactor: flag for general and payment ledger mismatch
(cherry picked from commit d620b9eae8)
2024-04-09 12:25:46 +00:00
ruthra kumar
153fc91478 refactor: date on which vouchers was reported
(cherry picked from commit 402ffc6d27)
2024-04-09 12:25:45 +00:00
ruthra kumar
b667a02470 feat: ledger health doctype
(cherry picked from commit 9ed74dd8cc)
2024-04-09 12:25:45 +00:00
ruthra kumar
5896e755bf fix: incorrect currency symbol in General Ledger print
(cherry picked from commit 429e036e8c)
2024-04-09 12:16:41 +00:00
rohitwaghchaure
dd6c53df96 Merge pull request #40910 from rohitwaghchaure/fixed-perf-issue-for-batchwise-balance-history
perf: memory consumption for the Batch-Wise Balance History report
2024-04-09 17:36:43 +05:30
Rohit Waghchaure
3165682d7a perf: memory consumption for the Batch-Wise Balance History report 2024-04-08 19:03:48 +05:30
ruthra kumar
aff71970e5 Merge pull request #40904 from frappe/mergify/bp/version-14-hotfix/pr-40878
refactor: merge taxes from delivery note to Sales Invoice (backport #40878)
2024-04-08 16:18:48 +05:30
Sagar Vora
2b5ae1dbae Merge pull request #40906 from frappe/mergify/bp/version-14-hotfix/pr-40831 2024-04-08 10:22:43 +00:00
Sagar Vora
24709ab400 chore: fix conflict 2024-04-08 15:50:58 +05:30
Smit Vora
0172880fd3 feat: new hook fields_for_group_similar_items to group additional fields for print formats (#40831)
(cherry picked from commit f7c9e1538b)

# Conflicts:
#	erpnext/hooks.py
2024-04-08 10:18:15 +00:00
ruthra kumar
b76c00de68 chore: resolve conflict 2024-04-08 15:44:13 +05:30
ruthra kumar
ae1858f465 test: tax merging from 2 Delivery Note to Sales Invoice
(cherry picked from commit 39a48a2e2a)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2024-04-08 09:09:36 +00:00
ruthra kumar
22b16a6b0e refactor: merge taxes from delivery note to Sales Invoice
(cherry picked from commit 550cbbd91c)
2024-04-08 09:09:34 +00:00
Frappe PR Bot
8d8feadeae chore(release): Bumped to Version 14.66.4
## [14.66.4](https://github.com/frappe/erpnext/compare/v14.66.3...v14.66.4) (2024-04-07)

### Bug Fixes

* incorrect operator causing incorrect validation ([60ba62e](60ba62e882))
2024-04-07 12:34:56 +00:00
rohitwaghchaure
5629ad21d2 Merge pull request #40898 from frappe/mergify/bp/version-14/pr-40893
fix: incorrect operator causing incorrect validation (backport #40883) (backport #40893)
2024-04-07 18:03:33 +05:30
Nabin Hait
1eb720a7b0 Merge pull request #40897 from nabinhait/pro-rata-depr-fix
fix: Get pro-rata depr amount based on correct days
2024-04-07 15:23:42 +05:30
rohitwaghchaure
c4e950f6b5 Merge pull request #40835 from frappe/mergify/bp/version-14-hotfix/pr-40834
fix: translatable web footer (backport #40834)
2024-04-07 14:35:45 +05:30
rohitwaghchaure
d4313f7109 Merge pull request #40859 from frappe/mergify/bp/version-14-hotfix/pr-40848
fix: group warehouse added in the stock reconciliation (backport #40848)
2024-04-07 14:34:33 +05:30
Rohit Waghchaure
60ba62e882 fix: incorrect operator causing incorrect validation
(cherry picked from commit 6b317b0c0d)
(cherry picked from commit 7e1ab75b38)
2024-04-07 09:04:00 +00:00
rohitwaghchaure
26d7354973 Merge pull request #40893 from frappe/mergify/bp/version-14-hotfix/pr-40883
fix: incorrect operator causing incorrect validation (backport #40883)
2024-04-07 14:33:10 +05:30
Nabin Hait
10d760089e fix: Get pro-rata depr amount based on correct days 2024-04-07 14:08:45 +05:30
Rohit Waghchaure
7e1ab75b38 fix: incorrect operator causing incorrect validation
(cherry picked from commit 6b317b0c0d)
2024-04-07 06:34:07 +00:00
rohitwaghchaure
0f9af4b82c Merge pull request #40888 from frappe/mergify/bp/version-14-hotfix/pr-40887
fix: query_report.trigger_refresh is not a function (backport #40887)
2024-04-07 12:02:16 +05:30
Rohit Waghchaure
a6145fa13c fix: query_report.trigger_refresh is not a function
(cherry picked from commit 30bbb58ca1)
2024-04-07 06:27:49 +00:00
ruthra kumar
cc2f861bca Merge pull request #40868 from frappe/mergify/bp/version-14-hotfix/pr-40856
fix: unwanted Exc Gain/Loss journals on Payment against Journal entry (backport #40856)
2024-04-05 11:01:48 +05:30
ruthra kumar
8f2fd5dec9 test: payment against JE reconciliation with different rates
(cherry picked from commit fe84558b77)
2024-04-05 05:03:56 +00:00
ruthra kumar
09cda60bdf fix: use reference type name to update exc rate
(cherry picked from commit c15690e475)
2024-04-05 05:03:56 +00:00
Rohit Waghchaure
9dea6d3393 fix: group warehouse added in the stock reconciliation
(cherry picked from commit 8f53bc0096)
2024-04-04 10:09:55 +00:00
Frappe PR Bot
fb076d34b1 chore(release): Bumped to Version 14.66.3
## [14.66.3](https://github.com/frappe/erpnext/compare/v14.66.2...v14.66.3) (2024-04-03)

### Bug Fixes

* cost center shouldn't update debit/credit in Exc gain/loss JV ([f66b53b](f66b53b2c1))
* do not fetch received items in purchase receipt ([e778d7e](e778d7e690))
* linter issues ([515f933](515f933362))
* markdown to text editor set for supplier quotation ([b5629d2](b5629d2d4e))
* payment_order_status should be no_copy ([8dff18e](8dff18eab4))
* Priority not copied from project template ([4f4470b](4f4470b9d2))
* show currency symbol in base currency in fixed asset register report ([cfe5f00](cfe5f009f6))
* show future payments allocated sales returns is considered as payment ([803ed90](803ed904a9))
* Warehouse linked company name in multicompany setup (backport [#40779](https://github.com/frappe/erpnext/issues/40779)) ([#40780](https://github.com/frappe/erpnext/issues/40780)) ([ceac42d](ceac42dfaa))
2024-04-03 10:32:43 +00:00
Deepesh Garg
fba888fe94 Merge pull request #40821 from frappe/version-14-hotfix
chore: release v14
2024-04-03 16:01:29 +05:30
Raffael Meyer
b55d8597e8 fix: translatable web footer (#40834)
(cherry picked from commit f3bcdbe5bd)
2024-04-02 15:16:18 +00:00
Nabin Hait
81081663c4 Merge pull request #40826 from frappe/mergify/bp/version-14-hotfix/pr-40584
fix: show currency symbol in base currency in fixed asset register report (backport #40584)
2024-04-02 18:29:58 +05:30
rohitwaghchaure
2f66542d71 Merge pull request #40824 from rohitwaghchaure/fixed-remove-enqueue-submit-cancel
chore: remove enqueue submit and cancel from the stock entry
2024-04-02 16:34:57 +05:30
Rohit Waghchaure
abc0294cbf chore: remove enqueue submit and cancel from stock entry 2024-04-02 16:10:28 +05:30
Nabin Hait
515f933362 fix: linter issues
(cherry picked from commit 21a40a2236)
2024-04-02 10:27:57 +00:00
Nabin Hait
cfe5f009f6 fix: show currency symbol in base currency in fixed asset register report
(cherry picked from commit 8b6220efd8)
2024-04-02 10:27:56 +00:00
ruthra kumar
cf74f8312e Merge pull request #40803 from frappe/mergify/bp/version-14-hotfix/pr-40181
fix: show future payments consider allocated sales returns (backport #40181)
2024-04-01 14:13:59 +05:30
Devin Slauenwhite
803ed904a9 fix: show future payments allocated sales returns is considered as payment
(cherry picked from commit 3381d0d945)
2024-04-01 07:34:41 +00:00
Devin Slauenwhite
72db656054 test: show future payments allocated sales returns is considered as payment
(cherry picked from commit daaa00bd4a)
2024-04-01 07:34:40 +00:00
rohitwaghchaure
44b72e7fc8 Merge pull request #40737 from frappe/mergify/bp/version-14-hotfix/pr-40731
fix: markdown to text editor set for supplier quotation (backport #40731)
2024-03-31 21:14:38 +05:30
rohitwaghchaure
31017774f6 chore: fix conflicts 2024-03-31 11:42:05 +05:30
rohitwaghchaure
9b8bb4d6a2 chore: fix conflicts 2024-03-31 11:41:44 +05:30
mergify[bot]
ceac42dfaa fix: Warehouse linked company name in multicompany setup (backport #40779) (#40780)
fix: Warehouse linked company name in multicompany setup (#40779)

(cherry picked from commit 679aea607b)

Co-authored-by: jeshani <56191568+jeshani@users.noreply.github.com>
2024-03-30 10:18:16 +05:30
ruthra kumar
998cc0a966 Merge pull request #40668 from frappe/mergify/bp/version-14-hotfix/pr-40634
refactor: better pop up message for Internal Customer validation (backport #40634)
2024-03-29 13:50:35 +05:30
ruthra kumar
e722c9a34a Merge pull request #40766 from frappe/mergify/bp/version-14-hotfix/pr-40758
fix: payment_order_status should be no_copy (backport #40758)
2024-03-29 13:49:08 +05:30
Ashish Shah
8dff18eab4 fix: payment_order_status should be no_copy
payment_order_status should be no_copy

(cherry picked from commit adee2ba541)
2024-03-29 07:57:03 +00:00
ruthra kumar
7363884ab3 Merge pull request #40762 from frappe/mergify/bp/version-14-hotfix/pr-40760
fix: cost center shouldn't update debit/credit in Exc gain/loss JV (backport #40760)
2024-03-29 11:47:05 +05:30
ruthra kumar
f66b53b2c1 fix: cost center shouldn't update debit/credit in Exc gain/loss JV
(cherry picked from commit 398d3022ef)
2024-03-29 06:13:47 +00:00
Frappe PR Bot
c1a95c05bc chore(release): Bumped to Version 14.66.2
## [14.66.2](https://github.com/frappe/erpnext/compare/v14.66.1...v14.66.2) (2024-03-29)

### Bug Fixes

* use sql to clear comments ([0322c1a](0322c1a36e))
2024-03-29 05:41:39 +00:00
ruthra kumar
db56d259cc Merge pull request #40747 from frappe/mergify/bp/version-14/pr-40739
refactor: use sql to clear comments (backport #40739)
2024-03-29 11:10:22 +05:30
ruthra kumar
0322c1a36e fix: use sql to clear comments
(cherry picked from commit 1f46c1530e)
2024-03-29 10:50:05 +05:30
ruthra kumar
bb35c334c0 Merge pull request #40744 from frappe/mergify/bp/version-14-hotfix/pr-40739
refactor: use sql to clear comments (backport #40739)
2024-03-28 19:42:02 +05:30
ruthra kumar
4d6fad26f3 refactor: use sql to clear comments
(cherry picked from commit 1f46c1530e)
2024-03-28 12:12:12 +00:00
Nihantra Patel
b5629d2d4e fix: markdown to text editor set for supplier quotation
(cherry picked from commit 9828d34b19)

# Conflicts:
#	erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
#	erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
2024-03-28 07:16:11 +00:00
rohitwaghchaure
b1105fb0ab Merge pull request #40721 from frappe/mergify/bp/version-14-hotfix/pr-40718
fix: do not fetch received items in purchase receipt (backport #40718)
2024-03-27 23:01:12 +05:30
Rohit Waghchaure
e778d7e690 fix: do not fetch received items in purchase receipt
(cherry picked from commit 5bff434902)
2024-03-27 17:11:25 +00:00
Frappe PR Bot
5827bdcbc2 chore(release): Bumped to Version 14.66.1
## [14.66.1](https://github.com/frappe/erpnext/compare/v14.66.0...v14.66.1) (2024-03-27)

### Bug Fixes

* Priority not copied from project template ([af6dd3b](af6dd3bcfb))
2024-03-27 11:21:16 +00:00
rohitwaghchaure
94f15797f5 Merge pull request #40712 from frappe/mergify/bp/version-14/pr-40710
fix: Priority not copied from project template (backport #40708) (backport #40710)
2024-03-27 16:50:07 +05:30
Rohit Waghchaure
af6dd3bcfb fix: Priority not copied from project template
(cherry picked from commit 33fd7b8a1f)
(cherry picked from commit 4f4470b9d2)
2024-03-27 10:49:11 +00:00
rohitwaghchaure
e947d65cb2 Merge pull request #40710 from frappe/mergify/bp/version-14-hotfix/pr-40708
fix: Priority not copied from project template (backport #40708)
2024-03-27 16:17:57 +05:30
Rohit Waghchaure
4f4470b9d2 fix: Priority not copied from project template
(cherry picked from commit 33fd7b8a1f)
2024-03-27 10:18:21 +00:00
Frappe PR Bot
4ea3876f30 chore(release): Bumped to Version 14.66.0
# [14.66.0](https://github.com/frappe/erpnext/compare/v14.65.8...v14.66.0) (2024-03-27)

### Bug Fixes

* achieved targets for sales partners ([f9ab763](f9ab763cc8))
* Add default finance book check in P&L statement ([4485121](4485121255))
* incorrect total qty in job card ([fb252ec](fb252ec29a))
* pass empty string email content of pos invoice ([ee7bd98](ee7bd98878))
* permissions during bulk transaction logs ([99faafb](99faafb5be))
* rate not fetching from the item price ([9b3c4ac](9b3c4ac575))
* rate reset to zero ([2f839fb](2f839fbf6d))
* remove microsecond from posting datetime ([#40017](https://github.com/frappe/erpnext/issues/40017)) ([c3f9338](c3f9338430))
* show correct variance for durations with no vouchers ([fe3bee4](fe3bee44e2))
* style for tax breakup ([e922ac7](e922ac7c31))
* use Text Editor for rendering tax breakup table ([bba1ac5](bba1ac5735))
* validate gl for previous fiscal year ([82b6133](82b613353b))
* wrong buying amount if delivered and billed qty varies ([2b31113](2b311131ba))

### Features

* bisect doctype ([5ad3918](5ad3918232))
* nodes doctype ([6afb6ff](6afb6ff4df))

### Performance Improvements

* add in some indexes (backport [#40590](https://github.com/frappe/erpnext/issues/40590)) ([#40604](https://github.com/frappe/erpnext/issues/40604)) ([390d3a6](390d3a6a9f))
* new column posting datetime in SLE to optimize stock ledger related queries ([5e68ebd](5e68ebd030))
2024-03-27 03:27:23 +00:00
Deepesh Garg
bd3b1bbbb8 Merge pull request #40677 from frappe/version-14-hotfix
chore: release v14
2024-03-27 08:56:10 +05:30
rohitwaghchaure
4a1cd943bd Merge pull request #40687 from frappe/mergify/bp/version-14-hotfix/pr-40684
fix: incorrect total qty in job card (backport #40684)
2024-03-26 19:54:15 +05:30
Rohit Waghchaure
fb252ec29a fix: incorrect total qty in job card
(cherry picked from commit c3546cf8e2)
2024-03-26 14:03:40 +00:00
Wolfram Schmidt
2834d25ce3 refactor: better pop message for Internal Customer validation
-added more meaningful description to message when using "internal Customer" scenario

(cherry picked from commit b132892b25)
2024-03-26 09:55:04 +05:30
Deepesh Garg
208a46fbe4 Merge pull request #40505 from frappe/mergify/bp/version-14-hotfix/pr-40503
fix: achieved targets for territory / partners bpv15 (#40503)
2024-03-26 09:03:14 +05:30
Deepesh Garg
2723e1f614 Merge pull request #40652 from frappe/mergify/bp/version-14-hotfix/pr-40640
fix: Add default finance book check-in P&L statement (#40640)
2024-03-25 17:08:57 +05:30
Deepesh Garg
4485121255 fix: Add default finance book check in P&L statement
(cherry picked from commit 5be3417fdf)
2024-03-25 09:14:41 +00:00
Frappe PR Bot
cf770e784d chore(release): Bumped to Version 14.65.8
## [14.65.8](https://github.com/frappe/erpnext/compare/v14.65.7...v14.65.8) (2024-03-25)

### Bug Fixes

* reset update outstanding flag for old records ([73c534c](73c534cf1a))
2024-03-25 02:57:34 +00:00
ruthra kumar
fda9c03d4d Merge pull request #40644 from frappe/mergify/bp/version-14/pr-40622
fix: reset update_outstanding_for_self flag for old records (backport #40622)
2024-03-25 08:26:21 +05:30
ruthra kumar
adec93de18 refactor: hide on print formats
(cherry picked from commit fdcdc8a56e)
2024-03-25 07:47:20 +05:30
ruthra kumar
73c534cf1a fix: reset update outstanding flag for old records
(cherry picked from commit a88bf8419e)
2024-03-25 07:47:16 +05:30
ruthra kumar
0ec1a88748 Merge pull request #40625 from frappe/mergify/bp/version-14-hotfix/pr-40622
refactor: reset update_outstanding_for_self flag for old records (backport #40622)
2024-03-24 16:24:37 +05:30
rohitwaghchaure
91400e1371 Merge pull request #40628 from frappe/mergify/bp/version-14-hotfix/pr-40017
fix: remove microsecond from posting datetime (backport #40017)
2024-03-23 18:39:04 +05:30
mergify[bot]
39711df27c Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-40503 2024-03-23 09:10:52 +00:00
Deepesh Garg
03eb4d77ce Merge pull request #40514 from mmdanny89/email-point-of-sale
fix: pass empty string email content of pos invoice
2024-03-23 14:39:58 +05:30
rohitwaghchaure
c3f9338430 fix: remove microsecond from posting datetime (#40017)
(cherry picked from commit 0b04d04da3)
2024-03-23 03:07:37 +00:00
ruthra kumar
dbb4391f1e refactor: hide on print formats
(cherry picked from commit fdcdc8a56e)
2024-03-22 14:54:03 +00:00
ruthra kumar
22cec21c6f refactor: reset flag for old records
(cherry picked from commit a88bf8419e)
2024-03-22 14:54:03 +00:00
ruthra kumar
ab7f80625b Merge pull request #40615 from frappe/mergify/bp/version-14-hotfix/pr-38496
feat: utility to debug financial reports - Bisect Accounting Statements (backport #38496)
2024-03-22 16:23:22 +05:30
rohitwaghchaure
a4ddc4ee1b Merge pull request #40036 from frappe/mergify/bp/version-14-hotfix/pr-39800
perf: new column Posting Datetime in SLE to optimize stock ledger related queries (backport #39800)
2024-03-22 16:06:53 +05:30
Frappe PR Bot
1c6f5b6525 chore(release): Bumped to Version 14.65.7
## [14.65.7](https://github.com/frappe/erpnext/compare/v14.65.6...v14.65.7) (2024-03-22)

### Bug Fixes

* rate reset to zero ([e09b3ad](e09b3ad720))
2024-03-22 10:32:42 +00:00
rohitwaghchaure
a6b887312b Merge pull request #40620 from frappe/mergify/bp/version-14/pr-40618
fix: rate reset to zero (backport #40616) (backport #40618)
2024-03-22 16:01:27 +05:30
Rohit Waghchaure
e09b3ad720 fix: rate reset to zero
(cherry picked from commit 6821baa850)
(cherry picked from commit 2f839fbf6d)
2024-03-22 10:12:07 +00:00
rohitwaghchaure
07929d20aa Merge pull request #40618 from frappe/mergify/bp/version-14-hotfix/pr-40616
fix: rate reset to zero (backport #40616)
2024-03-22 15:41:07 +05:30
Rohit Waghchaure
2f839fbf6d fix: rate reset to zero
(cherry picked from commit 6821baa850)
2024-03-22 09:50:51 +00:00
ruthra kumar
9f3b6d21c3 chore: resolve JS linter issue 2024-03-22 14:42:57 +05:30
ruthra kumar
4eb1fbfe42 chore: resolve linter issues
(cherry picked from commit 0890b414b1)
2024-03-22 08:51:33 +00:00
ruthra kumar
3a38aacc30 refactor: cache results
(cherry picked from commit 14c8c8c33d)
2024-03-22 08:51:33 +00:00
ruthra kumar
48bfb9d315 refactor: flag to differentiate generated and default values
(cherry picked from commit 0925706d5e)
2024-03-22 08:51:33 +00:00
ruthra kumar
5d90b0fd1d refactor: save results in node
(cherry picked from commit ca14ae8f1b)
2024-03-22 08:51:32 +00:00
ruthra kumar
3cab242e9f chore: restrict only to administrator and type info
(cherry picked from commit 90c6d4dc85)
2024-03-22 08:51:32 +00:00
ruthra kumar
54e7f303e9 chore: change data type for summary fields
(cherry picked from commit 228aa1a244)
2024-03-22 08:51:32 +00:00
ruthra kumar
cd0a7fed41 refactor: adding labels to important section
(cherry picked from commit 993e2bfbf9)
2024-03-22 08:51:32 +00:00
ruthra kumar
84609abd65 chore: UI cleanup
(cherry picked from commit ea3071db66)
2024-03-22 08:51:31 +00:00
ruthra kumar
a76d19fe8f chore: add screen freeze on wait
(cherry picked from commit 395299803f)
2024-03-22 08:51:31 +00:00
ruthra kumar
ebea0fd203 chore: code cleanup
(cherry picked from commit 5e2d21c033)
2024-03-22 08:51:31 +00:00
ruthra kumar
f9c30968bf chore: hide internal variables section
(cherry picked from commit c4c3090f46)
2024-03-22 08:51:31 +00:00
ruthra kumar
a9a510547e chore: rename btree and remove debugging statements
(cherry picked from commit 6492019383)
2024-03-22 08:51:31 +00:00
ruthra kumar
c2f6f9d37f chore: hide internal fields and better painting logic for heatmap
(cherry picked from commit f6831fba13)
2024-03-22 08:51:30 +00:00
ruthra kumar
9d5c010071 refactor: working heatmap
(cherry picked from commit 16db6c2f47)
2024-03-22 08:51:30 +00:00
ruthra kumar
3cfc778bdb chore: hide some internal fields
(cherry picked from commit bd3dc6482e)
2024-03-22 08:51:30 +00:00
ruthra kumar
f06014afc3 refactor: calculate summary on tree navigation
(cherry picked from commit f7b7b2b438)
2024-03-22 08:51:30 +00:00
ruthra kumar
bee5744309 refactor: use DB to store tree and state
(cherry picked from commit 99fbd8ad18)
2024-03-22 08:51:30 +00:00
ruthra kumar
bc1f25b897 chore: use doctype as btree
(cherry picked from commit 9d20256366)
2024-03-22 08:51:30 +00:00
ruthra kumar
6afb6ff4df feat: nodes doctype
(cherry picked from commit 85f2a6dd54)
2024-03-22 08:51:29 +00:00
ruthra kumar
609ecbe804 chore: remove unwanted code
(cherry picked from commit de2eba0d98)
2024-03-22 08:51:29 +00:00
ruthra kumar
674d822985 refactor: add basic navigation
1. remove unnecessary columns
2. added basic tree navigation

(cherry picked from commit 705ef4f5a3)
2024-03-22 08:51:29 +00:00
ruthra kumar
66fae6466f refactor: ability to build and load tree from DB
(cherry picked from commit b2dde55f2c)
2024-03-22 08:51:29 +00:00
ruthra kumar
81f456bdc7 refactor: introduce node class
(cherry picked from commit d53b34c0ce)
2024-03-22 08:51:29 +00:00
ruthra kumar
a177137d6d refactor: more buttons
(cherry picked from commit a427029151)
2024-03-22 08:51:28 +00:00
ruthra kumar
d81be8d855 refactor: date validation
(cherry picked from commit 2de3e6ce6d)
2024-03-22 08:51:28 +00:00
ruthra kumar
0399acdc8e refactor: support for BFS and DFS
(cherry picked from commit 03a38ed025)
2024-03-22 08:51:28 +00:00
ruthra kumar
50963d9ad5 refactor: simplify DFS
(cherry picked from commit 26503a205f)
2024-03-22 08:51:28 +00:00
ruthra kumar
1639974948 refactor: Depth First Search(DFS)
(cherry picked from commit 5a25c80f2e)
2024-03-22 08:51:28 +00:00
ruthra kumar
57ec0d6b41 refactor: some logic
(cherry picked from commit 4c8a8c3bcd)
2024-03-22 08:51:27 +00:00
ruthra kumar
5ad3918232 feat: bisect doctype
(cherry picked from commit decdbd2782)
2024-03-22 08:51:27 +00:00
ruthra kumar
9e15ecfe34 refactor: primitive summary for p&l and balance sheet
(cherry picked from commit eb4c476490)
2024-03-22 08:51:27 +00:00
ruthra kumar
da17496e3d Merge pull request #40607 from frappe/mergify/bp/version-14-hotfix/pr-40520
fix: permissions during bulk transaction logs (backport #40520)
2024-03-22 10:33:49 +05:30
David
99faafb5be fix: permissions during bulk transaction logs
(cherry picked from commit 49dd4c1ef3)
2024-03-22 04:38:17 +00:00
ruthra kumar
6d39a7890e Merge pull request #40596 from frappe/mergify/bp/version-14-hotfix/pr-40386
fix: wrong buying amount if delivered and billed qty varies (backport #40386)
2024-03-22 09:04:26 +05:30
mergify[bot]
390d3a6a9f perf: add in some indexes (backport #40590) (#40604)
* perf: add in some indexes (#40590)

`Sales Invoice Item.purchase_order`
`Delivery Note Item.purchase_order`

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 1cd38c860a)

# Conflicts:
#	erpnext/selling/doctype/sales_order_item/sales_order_item.json
#	erpnext/stock/doctype/delivery_note_item/delivery_note_item.json

* chore: `conflicts`

* chore: `conflicts`

---------

Co-authored-by: Akhil Narang <me@akhilnarang.dev>
Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2024-03-22 08:51:33 +05:30
Frappe PR Bot
14f7aef4d0 chore(release): Bumped to Version 14.65.6
## [14.65.6](https://github.com/frappe/erpnext/compare/v14.65.5...v14.65.6) (2024-03-21)

### Bug Fixes

* rate not fetching from the item price ([18279a6](18279a6dbd))
2024-03-21 17:33:34 +00:00
rohitwaghchaure
5f84e0d7e8 Merge pull request #40601 from frappe/mergify/bp/version-14/pr-40599
fix: rate not fetching from the item price (backport #40595) (backport #40599)
2024-03-21 23:02:23 +05:30
Rohit Waghchaure
18279a6dbd fix: rate not fetching from the item price
(cherry picked from commit d893a465d7)
(cherry picked from commit 9b3c4ac575)
2024-03-21 17:12:01 +00:00
rohitwaghchaure
5117ba5abf Merge pull request #40599 from frappe/mergify/bp/version-14-hotfix/pr-40595
fix: rate not fetching from the item price (backport #40595)
2024-03-21 22:40:58 +05:30
Rohit Waghchaure
9b3c4ac575 fix: rate not fetching from the item price
(cherry picked from commit d893a465d7)
2024-03-21 16:49:46 +00:00
ruthra kumar
d3a6153077 test: buying amt calculation if DN and SI differ in qty
(cherry picked from commit ccb51ded95)
2024-03-21 15:46:03 +00:00
Dany Robert
2b311131ba fix: wrong buying amount if delivered and billed qty varies
(cherry picked from commit b8da0d9334)
2024-03-21 15:46:02 +00:00
Gursheen Kaur Anand
c072cd941d Merge pull request #40583 from frappe/mergify/bp/version-14-hotfix/pr-40572
fix: PCV validation for previous fiscal year (backport #40572)
2024-03-21 12:44:09 +05:30
ruthra kumar
757cf8a76f Merge pull request #40581 from frappe/mergify/bp/version-14-hotfix/pr-40580
refactor: config changes in Transaction Deletion Record (backport #40580)
2024-03-21 11:07:17 +05:30
Gursheen Anand
82b613353b fix: validate gl for previous fiscal year
(cherry picked from commit a1d108c062)
2024-03-21 05:30:35 +00:00
ruthra kumar
5791c50fdf refactor: config changes in Transaction Deletion Record
(cherry picked from commit 4ba67fb3ec)
2024-03-21 05:17:16 +00:00
ruthra kumar
67481ad062 Merge pull request #40574 from frappe/mergify/bp/version-14-hotfix/pr-39717
refactor: Transaction Deletion record for large volumes (backport #39717)
2024-03-21 10:18:07 +05:30
ruthra kumar
1515bb7f0b refactor: replace get_job with create_job_id utility method 2024-03-21 09:53:55 +05:30
Gursheen Kaur Anand
33e0371c4d Merge pull request #40571 from frappe/mergify/bp/version-14-hotfix/pr-40563
fix: use `Text Editor` for rendering tax breakup table (backport #40563)
2024-03-20 20:39:19 +05:30
Rohit Waghchaure
1df54114a8 chore: fixed test case 2024-03-20 20:20:44 +05:30
Gursheen Anand
cf3b0ee41e chore: resolve conflicts 2024-03-20 20:15:20 +05:30
Gursheen Anand
957f55f6b1 chore: resolve conflicts 2024-03-20 20:12:36 +05:30
Rohit Waghchaure
77349a0af6 chore: fix test case 2024-03-20 20:09:22 +05:30
Rohit Waghchaure
c36a5d1ba3 chore: fix backport issues 2024-03-20 20:09:22 +05:30
rohitwaghchaure
44d4096ba1 chore: fix conflicts 2024-03-20 20:09:22 +05:30
rohitwaghchaure
a694a92df9 chore: fix conflicts 2024-03-20 20:09:22 +05:30
rohitwaghchaure
1c7128e77b chore: fix conflicts 2024-03-20 20:09:22 +05:30
rohitwaghchaure
c147ec168f chore: fix conflicts 2024-03-20 20:09:22 +05:30
rohitwaghchaure
717442c01f chore: fix conflicts 2024-03-20 20:09:22 +05:30
Rohit Waghchaure
ce7a53f810 chore: remove microsecond from posting_datetime
(cherry picked from commit a73ba2c0d2)

# Conflicts:
#	erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
2024-03-20 20:08:37 +05:30
Rohit Waghchaure
1767dada8d test: test cases to test clash timestamp entries
(cherry picked from commit f04676aaed)
2024-03-20 20:08:36 +05:30
Rohit Waghchaure
5e68ebd030 perf: new column posting datetime in SLE to optimize stock ledger related queries
(cherry picked from commit d80ca523a4)
2024-03-20 20:07:36 +05:30
ruthra kumar
87fdb4e720 chore: resolve conflicts 2024-03-20 19:58:18 +05:30
ruthra kumar
e5722a772a chore: fix linting issue in JS
(cherry picked from commit 02c522b7cd)
2024-03-20 14:13:21 +00:00
ruthra kumar
35fcd032ad refactor(test): test cases modified to handle new approach
(cherry picked from commit 81309576b0)

# Conflicts:
#	erpnext/setup/demo.py
#	erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py
2024-03-20 14:13:21 +00:00
ruthra kumar
1fe14334f3 refactor: ability to process in single transaction
(cherry picked from commit a158b825d7)
2024-03-20 14:13:20 +00:00
ruthra kumar
b429f75d65 refactor: link running doc validation to company master
(cherry picked from commit 5a3afea8c7)

# Conflicts:
#	erpnext/setup/doctype/company/company.js
#	erpnext/setup/doctype/company/company.py
2024-03-20 14:13:20 +00:00
ruthra kumar
fab5c1170d chore: rename entry point
(cherry picked from commit 5fe0b20be1)
2024-03-20 14:13:19 +00:00
ruthra kumar
d496a1e58e chore: move status and error log to their own section
(cherry picked from commit 3cec62d4f8)
2024-03-20 14:13:19 +00:00
ruthra kumar
6ae9eb250f refactor: minor UI tweaks
(cherry picked from commit 0455d0c46c)
2024-03-20 14:13:19 +00:00
ruthra kumar
5e590389f3 refactor: exception propogation
(cherry picked from commit 4a55240e63)
2024-03-20 14:13:18 +00:00
ruthra kumar
1110dd93ff chore: code cleanup
(cherry picked from commit eea260b9f9)
2024-03-20 14:13:17 +00:00
ruthra kumar
0ea9ce7a5a refactor: better method naming
(cherry picked from commit 30463657bf)
2024-03-20 14:13:17 +00:00
ruthra kumar
cf6cb80d18 refactor: barebones hook on all doctypes with 'company' field
(cherry picked from commit ec194ef076)
2024-03-20 14:13:16 +00:00
ruthra kumar
e908003358 refactor: make sure only one task is running for doc
(cherry picked from commit 78c9cc63b1)
2024-03-20 14:13:16 +00:00
ruthra kumar
e142daca7f chore: hide docfield in list view
(cherry picked from commit 98afb4d468)
2024-03-20 14:13:15 +00:00
ruthra kumar
962105bc87 refactor: validations to prevent duplicate jobs
(cherry picked from commit 31a2da552b)
2024-03-20 14:13:15 +00:00
ruthra kumar
e600109872 refactor: no copy on summary table and more validations
(cherry picked from commit 55e93b3fe1)
2024-03-20 14:13:15 +00:00
ruthra kumar
97ed905627 refactor: reset all flags and remove unwanted code
(cherry picked from commit 2dbe68a09d)
2024-03-20 14:13:14 +00:00
ruthra kumar
c38cfd14f3 chore: show correct status in list view
(cherry picked from commit 1014940953)
2024-03-20 14:13:14 +00:00
ruthra kumar
341e467056 refactor: validate status before running events
(cherry picked from commit 86b5e2e277)
2024-03-20 14:13:14 +00:00
ruthra kumar
833df2c76b refactor: make Excluded doctype table read only
(cherry picked from commit 7c4cff2649)
2024-03-20 14:13:13 +00:00
ruthra kumar
a303788c71 chore: remove unwanted UI code
(cherry picked from commit b98a5e4edc)
2024-03-20 14:13:13 +00:00
ruthra kumar
0d791f594f refactor: use separate child table for summary
(cherry picked from commit 49d3bcbc8d)
2024-03-20 14:13:13 +00:00
ruthra kumar
b1367e839c refactor: chained callback
(cherry picked from commit b12ca65fcc)
2024-03-20 14:13:12 +00:00
ruthra kumar
e56138ddb0 refactor: reorder flags in Tasks section
(cherry picked from commit cccb2d5141)
2024-03-20 14:13:12 +00:00
ruthra kumar
c9d77044b0 refactor: use flags to decide on current stage
(cherry picked from commit 6a77d86a53)

# Conflicts:
#	erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py
2024-03-20 14:13:12 +00:00
ruthra kumar
5542985e25 refactor: UI trigger
(cherry picked from commit 8944ab8b6a)
2024-03-20 14:13:11 +00:00
ruthra kumar
52d22d8b55 refactor: tasks section and UI niceties
(cherry picked from commit d0dc2c6e77)
2024-03-20 14:13:11 +00:00
ruthra kumar
7280a76f73 refactor: set status and trigger job on submit
(cherry picked from commit 6fbb67b1d2)
2024-03-20 14:13:10 +00:00
ruthra kumar
07c98146c5 refactor: more options for 'status' and move it to top
(cherry picked from commit 0d65d878de)

# Conflicts:
#	erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
2024-03-20 14:13:10 +00:00
Ankush Menat
e922ac7c31 fix: style for tax breakup
(cherry picked from commit 967540da18)

# Conflicts:
#	erpnext/public/scss/erpnext.scss
2024-03-20 13:12:16 +00:00
Gursheen Anand
bba1ac5735 fix: use Text Editor for rendering tax breakup table
(cherry picked from commit 1c63983873)

# Conflicts:
#	erpnext/accounts/doctype/pos_invoice/pos_invoice.py
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
#	erpnext/buying/doctype/purchase_order/purchase_order.json
#	erpnext/buying/doctype/purchase_order/purchase_order.py
#	erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
#	erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
#	erpnext/selling/doctype/quotation/quotation.json
#	erpnext/selling/doctype/quotation/quotation.py
#	erpnext/selling/doctype/sales_order/sales_order.py
#	erpnext/stock/doctype/delivery_note/delivery_note.json
#	erpnext/stock/doctype/delivery_note/delivery_note.py
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
2024-03-20 13:12:15 +00:00
Danny
ee7bd98878 fix: pass empty string email content of pos invoice 2024-03-18 10:47:42 -04:00
Gursheen Anand
fe3bee44e2 fix: show correct variance for durations with no vouchers
(cherry picked from commit bc78bc33f1)
2024-03-18 05:53:57 +00:00
Gursheen Anand
c3b8c00070 test: sales partner targets with invoices
(cherry picked from commit 1a3ea0c108)
2024-03-18 05:53:57 +00:00
Gursheen Anand
f9ab763cc8 fix: achieved targets for sales partners
(cherry picked from commit 1ac888715c)
2024-03-18 05:53:57 +00:00
102 changed files with 3049 additions and 605 deletions

View File

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

View File

@@ -0,0 +1,100 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Bisect Accounting Statements", {
onload(frm) {
frm.trigger("render_heatmap");
},
refresh(frm) {
frm.add_custom_button(__("Bisect Left"), () => {
frm.trigger("bisect_left");
});
frm.add_custom_button(__("Bisect Right"), () => {
frm.trigger("bisect_right");
});
frm.add_custom_button(__("Up"), () => {
frm.trigger("move_up");
});
frm.add_custom_button(__("Build Tree"), () => {
frm.trigger("build_tree");
});
},
render_heatmap(frm) {
let bisect_heatmap = frm.get_field("bisect_heatmap").$wrapper;
bisect_heatmap.addClass("bisect_heatmap_location");
// milliseconds in a day
let msiad = 24 * 60 * 60 * 1000;
let datapoints = {};
let fr_dt = new Date(frm.doc.from_date).getTime();
let to_dt = new Date(frm.doc.to_date).getTime();
let bisect_start = new Date(frm.doc.current_from_date).getTime();
let bisect_end = new Date(frm.doc.current_to_date).getTime();
for (let x = fr_dt; x <= to_dt; x += msiad) {
let epoch_in_seconds = x / 1000;
if (bisect_start <= x && x <= bisect_end) {
datapoints[epoch_in_seconds] = 1.0;
} else {
datapoints[epoch_in_seconds] = 0.0;
}
}
new frappe.Chart(".bisect_heatmap_location", {
type: "heatmap",
data: {
dataPoints: datapoints,
start: new Date(frm.doc.from_date),
end: new Date(frm.doc.to_date),
},
countLabel: "Bisecting",
discreteDomains: 1,
});
},
bisect_left(frm) {
frm.call({
doc: frm.doc,
method: "bisect_left",
freeze: true,
freeze_message: __("Bisecting Left ..."),
callback: (r) => {
frm.trigger("render_heatmap");
},
});
},
bisect_right(frm) {
frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Bisecting Right ..."),
method: "bisect_right",
callback: (r) => {
frm.trigger("render_heatmap");
},
});
},
move_up(frm) {
frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Moving up in tree ..."),
method: "move_up",
callback: (r) => {
frm.trigger("render_heatmap");
},
});
},
build_tree(frm) {
frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Rebuilding BTree for period ..."),
method: "build_tree",
callback: (r) => {
frm.trigger("render_heatmap");
},
});
},
});

View File

@@ -0,0 +1,194 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-09-15 21:28:28.054773",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_cvfg",
"company",
"column_break_hcam",
"from_date",
"column_break_qxbi",
"to_date",
"column_break_iwny",
"algorithm",
"section_break_8ph9",
"current_node",
"section_break_ngid",
"bisect_heatmap",
"section_break_hmsy",
"bisecting_from",
"current_from_date",
"column_break_uqyd",
"bisecting_to",
"current_to_date",
"section_break_hbyo",
"heading_cppb",
"p_l_summary",
"column_break_aivo",
"balance_sheet_summary",
"b_s_summary",
"column_break_gvwx",
"difference_heading",
"difference"
],
"fields": [
{
"fieldname": "column_break_qxbi",
"fieldtype": "Column Break"
},
{
"fieldname": "from_date",
"fieldtype": "Datetime",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Datetime",
"label": "To Date"
},
{
"default": "BFS",
"fieldname": "algorithm",
"fieldtype": "Select",
"label": "Algorithm",
"options": "BFS\nDFS"
},
{
"fieldname": "column_break_iwny",
"fieldtype": "Column Break"
},
{
"fieldname": "current_node",
"fieldtype": "Link",
"label": "Current Node",
"options": "Bisect Nodes"
},
{
"fieldname": "section_break_hmsy",
"fieldtype": "Section Break"
},
{
"fieldname": "current_from_date",
"fieldtype": "Datetime",
"read_only": 1
},
{
"fieldname": "current_to_date",
"fieldtype": "Datetime",
"read_only": 1
},
{
"fieldname": "column_break_uqyd",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_hbyo",
"fieldtype": "Section Break"
},
{
"fieldname": "p_l_summary",
"fieldtype": "Float",
"read_only": 1
},
{
"fieldname": "b_s_summary",
"fieldtype": "Float",
"read_only": 1
},
{
"fieldname": "difference",
"fieldtype": "Float",
"read_only": 1
},
{
"fieldname": "column_break_aivo",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_gvwx",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "column_break_hcam",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ngid",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_8ph9",
"fieldtype": "Section Break",
"hidden": 1
},
{
"fieldname": "bisect_heatmap",
"fieldtype": "HTML",
"label": "Heatmap"
},
{
"fieldname": "heading_cppb",
"fieldtype": "Heading",
"label": "Profit and Loss Summary"
},
{
"fieldname": "balance_sheet_summary",
"fieldtype": "Heading",
"label": "Balance Sheet Summary"
},
{
"fieldname": "difference_heading",
"fieldtype": "Heading",
"label": "Difference"
},
{
"fieldname": "bisecting_from",
"fieldtype": "Heading",
"label": "Bisecting From"
},
{
"fieldname": "bisecting_to",
"fieldtype": "Heading",
"label": "Bisecting To"
},
{
"fieldname": "section_break_cvfg",
"fieldtype": "Section Break"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-12-01 16:49:54.073890",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bisect Accounting Statements",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Administrator",
"share": 1,
"write": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,226 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import datetime
from collections import deque
from math import floor
import frappe
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
from frappe.utils.data import guess_date_format
class BisectAccountingStatements(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
algorithm: DF.Literal["BFS", "DFS"]
b_s_summary: DF.Float
company: DF.Link | None
current_from_date: DF.Datetime | None
current_node: DF.Link | None
current_to_date: DF.Datetime | None
difference: DF.Float
from_date: DF.Datetime | None
p_l_summary: DF.Float
to_date: DF.Datetime | None
# end: auto-generated types
def validate(self):
self.validate_dates()
def validate_dates(self):
if getdate(self.from_date) > getdate(self.to_date):
frappe.throw(
_("From Date: {0} cannot be greater than To date: {1}").format(
frappe.bold(self.from_date), frappe.bold(self.to_date)
)
)
def bfs(self, from_date: datetime, to_date: datetime):
# Make Root node
node = frappe.new_doc("Bisect Nodes")
node.root = None
node.period_from_date = from_date
node.period_to_date = to_date
node.insert()
period_queue = deque([node])
while period_queue:
cur_node = period_queue.popleft()
delta = cur_node.period_to_date - cur_node.period_from_date
if delta.days == 0:
continue
else:
cur_floor = floor(delta.days / 2)
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
left_node = frappe.new_doc("Bisect Nodes")
left_node.period_from_date = cur_node.period_from_date
left_node.period_to_date = next_to_date
left_node.root = cur_node.name
left_node.generated = False
left_node.insert()
cur_node.left_child = left_node.name
period_queue.append(left_node)
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
right_node = frappe.new_doc("Bisect Nodes")
right_node.period_from_date = next_from_date
right_node.period_to_date = cur_node.period_to_date
right_node.root = cur_node.name
right_node.generated = False
right_node.insert()
cur_node.right_child = right_node.name
period_queue.append(right_node)
cur_node.save()
def dfs(self, from_date: datetime, to_date: datetime):
# Make Root node
node = frappe.new_doc("Bisect Nodes")
node.root = None
node.period_from_date = from_date
node.period_to_date = to_date
node.insert()
period_stack = [node]
while period_stack:
cur_node = period_stack.pop()
delta = cur_node.period_to_date - cur_node.period_from_date
if delta.days == 0:
continue
else:
cur_floor = floor(delta.days / 2)
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
left_node = frappe.new_doc("Bisect Nodes")
left_node.period_from_date = cur_node.period_from_date
left_node.period_to_date = next_to_date
left_node.root = cur_node.name
left_node.generated = False
left_node.insert()
cur_node.left_child = left_node.name
period_stack.append(left_node)
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
right_node = frappe.new_doc("Bisect Nodes")
right_node.period_from_date = next_from_date
right_node.period_to_date = cur_node.period_to_date
right_node.root = cur_node.name
right_node.generated = False
right_node.insert()
cur_node.right_child = right_node.name
period_stack.append(right_node)
cur_node.save()
@frappe.whitelist()
def build_tree(self):
frappe.db.delete("Bisect Nodes")
# Convert str to datetime format
dt_format = guess_date_format(self.from_date)
from_date = datetime.datetime.strptime(self.from_date, dt_format)
to_date = datetime.datetime.strptime(self.to_date, dt_format)
if self.algorithm == "BFS":
self.bfs(from_date, to_date)
if self.algorithm == "DFS":
self.dfs(from_date, to_date)
# set root as current node
root = frappe.db.get_all("Bisect Nodes", filters={"root": ["is", "not set"]})[0]
self.get_report_summary()
self.current_node = root.name
self.current_from_date = self.from_date
self.current_to_date = self.to_date
self.save()
def get_report_summary(self):
filters = {
"company": self.company,
"filter_based_on": "Date Range",
"period_start_date": self.current_from_date,
"period_end_date": self.current_to_date,
"periodicity": "Yearly",
}
pl_summary = frappe.get_doc("Report", "Profit and Loss Statement")
self.p_l_summary = pl_summary.execute_script_report(filters=filters)[5]
bs_summary = frappe.get_doc("Report", "Balance Sheet")
self.b_s_summary = bs_summary.execute_script_report(filters=filters)[5]
self.difference = abs(self.p_l_summary - self.b_s_summary)
def update_node(self):
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
current_node.balance_sheet_summary = self.b_s_summary
current_node.profit_loss_summary = self.p_l_summary
current_node.difference = self.difference
current_node.generated = True
current_node.save()
def current_node_has_summary_info(self):
"Assertion method"
return frappe.db.get_value("Bisect Nodes", self.current_node, "generated")
def fetch_summary_info_from_current_node(self):
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
self.p_l_summary = current_node.balance_sheet_summary
self.b_s_summary = current_node.profit_loss_summary
self.difference = abs(self.p_l_summary - self.b_s_summary)
def fetch_or_calculate(self):
if self.current_node_has_summary_info():
self.fetch_summary_info_from_current_node()
else:
self.get_report_summary()
self.update_node()
@frappe.whitelist()
def bisect_left(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
if cur_node.left_child is not None:
lft_node = frappe.get_doc("Bisect Nodes", cur_node.left_child)
self.current_node = cur_node.left_child
self.current_from_date = lft_node.period_from_date
self.current_to_date = lft_node.period_to_date
self.fetch_or_calculate()
self.save()
else:
frappe.msgprint(_("No more children on Left"))
@frappe.whitelist()
def bisect_right(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
if cur_node.right_child is not None:
rgt_node = frappe.get_doc("Bisect Nodes", cur_node.right_child)
self.current_node = cur_node.right_child
self.current_from_date = rgt_node.period_from_date
self.current_to_date = rgt_node.period_to_date
self.fetch_or_calculate()
self.save()
else:
frappe.msgprint(_("No more children on Right"))
@frappe.whitelist()
def move_up(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
if cur_node.root is not None:
root = frappe.get_doc("Bisect Nodes", cur_node.root)
self.current_node = cur_node.root
self.current_from_date = root.period_from_date
self.current_to_date = root.period_to_date
self.fetch_or_calculate()
self.save()
else:
frappe.msgprint(_("Reached Root"))

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 TestBisectAccountingStatements(FrappeTestCase):
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Bisect Nodes", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,97 @@
{
"actions": [],
"autoname": "autoincrement",
"creation": "2023-09-27 14:56:38.112462",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"root",
"left_child",
"right_child",
"period_from_date",
"period_to_date",
"difference",
"balance_sheet_summary",
"profit_loss_summary",
"generated"
],
"fields": [
{
"fieldname": "root",
"fieldtype": "Link",
"label": "Root",
"options": "Bisect Nodes"
},
{
"fieldname": "left_child",
"fieldtype": "Link",
"label": "Left Child",
"options": "Bisect Nodes"
},
{
"fieldname": "right_child",
"fieldtype": "Link",
"label": "Right Child",
"options": "Bisect Nodes"
},
{
"fieldname": "period_from_date",
"fieldtype": "Datetime",
"label": "Period_from_date"
},
{
"fieldname": "period_to_date",
"fieldtype": "Datetime",
"label": "Period To Date"
},
{
"fieldname": "difference",
"fieldtype": "Float",
"label": "Difference"
},
{
"fieldname": "balance_sheet_summary",
"fieldtype": "Float",
"label": "Balance Sheet Summary"
},
{
"fieldname": "profit_loss_summary",
"fieldtype": "Float",
"label": "Profit and Loss Summary"
},
{
"default": "0",
"fieldname": "generated",
"fieldtype": "Check",
"label": "Generated"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-12-01 17:46:12.437996",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bisect Nodes",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,29 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class BisectNodes(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
balance_sheet_summary: DF.Float
difference: DF.Float
generated: DF.Check
left_child: DF.Link | None
name: DF.Int | None
period_from_date: DF.Datetime | None
period_to_date: DF.Datetime | None
profit_loss_summary: DF.Float
right_child: DF.Link | None
root: DF.Link | None
# end: auto-generated types
pass

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 TestBisectNodes(FrappeTestCase):
pass

View File

@@ -135,6 +135,7 @@ class GLEntry(Document):
and self.company == dimension.company
and dimension.mandatory_for_pl
and not dimension.disabled
and not self.is_cancelled
):
if not self.get(dimension.fieldname):
frappe.throw(
@@ -148,6 +149,7 @@ class GLEntry(Document):
and self.company == dimension.company
and dimension.mandatory_for_bs
and not dimension.disabled
and not self.is_cancelled
):
if not self.get(dimension.fieldname):
frappe.throw(

View File

@@ -428,7 +428,10 @@ frappe.ui.form.on("Journal Entry Account", {
}
},
cost_center: function (frm, dt, dn) {
erpnext.journal_entry.set_account_details(frm, dt, dn);
// Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0'
if (frm.doc.voucher_type != "Exchange Gain Or Loss") {
erpnext.journal_entry.set_account_details(frm, dt, dn);
}
},
account: function (frm, dt, dn) {

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Ledger Health", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,70 @@
{
"actions": [],
"autoname": "autoincrement",
"creation": "2024-03-26 17:01:47.443986",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"voucher_type",
"voucher_no",
"checked_on",
"debit_credit_mismatch",
"general_and_payment_ledger_mismatch"
],
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Data",
"label": "Voucher Type"
},
{
"fieldname": "voucher_no",
"fieldtype": "Data",
"label": "Voucher No"
},
{
"default": "0",
"fieldname": "debit_credit_mismatch",
"fieldtype": "Check",
"label": "Debit-Credit mismatch"
},
{
"fieldname": "checked_on",
"fieldtype": "Datetime",
"label": "Checked On"
},
{
"default": "0",
"fieldname": "general_and_payment_ledger_mismatch",
"fieldtype": "Check",
"label": "General and Payment Ledger mismatch"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-04-09 11:16:07.044484",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Ledger Health",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,25 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LedgerHealth(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
checked_on: DF.Datetime | None
debit_credit_mismatch: DF.Check
general_and_payment_ledger_mismatch: DF.Check
name: DF.Int | None
voucher_no: DF.Data | None
voucher_type: DF.Data | None
# end: auto-generated types
pass

View File

@@ -0,0 +1,109 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import run_ledger_health_checks
class TestLedgerHealth(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.configure_monitoring_tool()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
def configure_monitoring_tool(self):
monitor_settings = frappe.get_doc("Ledger Health Monitor")
monitor_settings.enable_health_monitor = True
monitor_settings.enable_for_last_x_days = 60
monitor_settings.debit_credit_mismatch = True
monitor_settings.general_and_payment_ledger_mismatch = True
exists = [x for x in monitor_settings.companies if x.company == self.company]
if not exists:
monitor_settings.append("companies", {"company": self.company})
monitor_settings.save()
def clear_old_entries(self):
super().clear_old_entries()
lh = qb.DocType("Ledger Health")
qb.from_(lh).delete().run()
def create_journal(self):
je = frappe.new_doc("Journal Entry")
je.company = self.company
je.voucher_type = "Journal Entry"
je.posting_date = nowdate()
je.append(
"accounts",
{
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
"debit_in_account_currency": 10000,
},
)
je.append("accounts", {"account": self.income_account, "credit_in_account_currency": 10000})
je.save().submit()
self.je = je
def test_debit_credit_mismatch(self):
self.create_journal()
# manually cause debit-credit mismatch
gle = frappe.db.get_all(
"GL Entry", filters={"voucher_no": self.je.name, "account": self.income_account}
)[0]
frappe.db.set_value("GL Entry", gle.name, "credit", 8000)
run_ledger_health_checks()
expected = {
"voucher_type": self.je.doctype,
"voucher_no": self.je.name,
"debit_credit_mismatch": True,
"general_and_payment_ledger_mismatch": False,
}
actual = frappe.db.get_all(
"Ledger Health",
fields=[
"voucher_type",
"voucher_no",
"debit_credit_mismatch",
"general_and_payment_ledger_mismatch",
],
)
self.assertEqual(len(actual), 1)
self.assertEqual(expected, actual[0])
def test_gl_and_pl_mismatch(self):
self.create_journal()
# manually cause GL and PL discrepancy
ple = frappe.db.get_all("Payment Ledger Entry", filters={"voucher_no": self.je.name})[0]
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", 11000)
run_ledger_health_checks()
expected = {
"voucher_type": self.je.doctype,
"voucher_no": self.je.name,
"debit_credit_mismatch": False,
"general_and_payment_ledger_mismatch": True,
}
actual = frappe.db.get_all(
"Ledger Health",
fields=[
"voucher_type",
"voucher_no",
"debit_credit_mismatch",
"general_and_payment_ledger_mismatch",
],
)
self.assertEqual(len(actual), 1)
self.assertEqual(expected, actual[0])

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Ledger Health Monitor", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,104 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-03-27 09:38:07.427997",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enable_health_monitor",
"monitor_section",
"monitor_for_last_x_days",
"debit_credit_mismatch",
"general_and_payment_ledger_mismatch",
"section_break_xdsp",
"companies"
],
"fields": [
{
"default": "0",
"fieldname": "enable_health_monitor",
"fieldtype": "Check",
"label": "Enable Health Monitor"
},
{
"fieldname": "monitor_section",
"fieldtype": "Section Break",
"label": "Configuration"
},
{
"default": "0",
"fieldname": "debit_credit_mismatch",
"fieldtype": "Check",
"label": "Debit-Credit Mismatch"
},
{
"default": "0",
"fieldname": "general_and_payment_ledger_mismatch",
"fieldtype": "Check",
"label": "Discrepancy between General and Payment Ledger"
},
{
"default": "60",
"fieldname": "monitor_for_last_x_days",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Monitor for Last 'X' days",
"reqd": 1
},
{
"fieldname": "section_break_xdsp",
"fieldtype": "Section Break",
"label": "Companies"
},
{
"fieldname": "companies",
"fieldtype": "Table",
"options": "Ledger Health Monitor Company"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-27 10:14:16.511681",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Ledger Health Monitor",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,28 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LedgerHealthMonitor(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
from erpnext.accounts.doctype.ledger_health_monitor_company.ledger_health_monitor_company import (
LedgerHealthMonitorCompany,
)
companies: DF.Table[LedgerHealthMonitorCompany]
debit_credit_mismatch: DF.Check
enable_health_monitor: DF.Check
general_and_payment_ledger_mismatch: DF.Check
monitor_for_last_x_days: DF.Int
# end: auto-generated types
pass

View File

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

View File

@@ -0,0 +1,32 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-03-27 10:04:45.727054",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 10:06:22.806155",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Ledger Health Monitor Company",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,23 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LedgerHealthMonitorCompany(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
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View File

@@ -576,6 +576,7 @@
"fieldtype": "Select",
"hidden": 1,
"label": "Payment Order Status",
"no_copy": 1,
"options": "Initiated\nPayment Ordered",
"read_only": 1
},

View File

@@ -338,7 +338,7 @@ class PaymentEntry(AccountsController):
self,
force: bool = False,
update_ref_details_only_for: list | None = None,
ref_exchange_rate: float | None = None,
reference_exchange_details: dict | None = None,
) -> None:
for d in self.get("references"):
if d.allocated_amount:
@@ -352,8 +352,12 @@ class PaymentEntry(AccountsController):
)
# Only update exchange rate when the reference is Journal Entry
if ref_exchange_rate and d.reference_doctype == "Journal Entry":
ref_details.update({"exchange_rate": ref_exchange_rate})
if (
reference_exchange_details
and d.reference_doctype == reference_exchange_details.reference_doctype
and d.reference_name == reference_exchange_details.reference_name
):
ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate})
for field, value in ref_details.items():
if d.exchange_gain_loss:

View File

@@ -121,7 +121,8 @@ class PeriodClosingVoucher(AccountsController):
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
if previous_fiscal_year and not frappe.db.exists(
"GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company}
"GL Entry",
{"posting_date": ("<=", last_year_closing), "company": self.company, "is_cancelled": 0},
):
return

View File

@@ -2170,7 +2170,8 @@
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
"label": "Update Outstanding for Self",
"no_copy": 1
"no_copy": 1,
"print_hide": 1
}
],
"icon": "fa fa-file-text",
@@ -2183,7 +2184,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-03-20 16:02:52.237732",
"modified": "2024-03-22 17:50:34.395602",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -1817,9 +1817,9 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc
companies = [d.company for d in companies]
if not company in companies:
frappe.throw(
_("{0} not allowed to transact with {1}. Please change the Company.").format(
partytype, company
)
_(
"{0} not allowed to transact with {1}. Please change the Company or add the Company in the 'Allowed To Transact With'-Section in the Customer record."
).format(_(partytype), company)
)

View File

@@ -3438,6 +3438,105 @@ class TestSalesInvoice(FrappeTestCase):
si.items[0].rate = 10
si.save()
def test_taxes_merging_from_delivery_note(self):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
dn1 = create_delivery_note(do_not_submit=1)
dn1.items[0].qty = 10
dn1.items[0].rate = 100
dn1.append(
"taxes",
{
"charge_type": "Actual",
"account_head": "Freight and Forwarding Charges - _TC",
"description": "movement charges",
"tax_amount": 100,
},
)
dn1.append(
"taxes",
{
"charge_type": "Actual",
"account_head": "Marketing Expenses - _TC",
"description": "marketing",
"tax_amount": 150,
},
)
dn1.save().submit()
dn2 = create_delivery_note(do_not_submit=1)
dn2.items[0].qty = 5
dn2.items[0].rate = 100
dn2.append(
"taxes",
{
"charge_type": "Actual",
"account_head": "Freight and Forwarding Charges - _TC",
"description": "movement charges",
"tax_amount": 20,
},
)
dn2.append(
"taxes",
{
"charge_type": "Actual",
"account_head": "Miscellaneous Expenses - _TC",
"description": "marketing",
"tax_amount": 60,
},
)
dn2.save().submit()
# si = make_sales_invoice(dn1.name)
si = create_sales_invoice(do_not_submit=True)
si.customer = dn1.customer
si.items.clear()
from frappe.model.mapper import map_docs
map_docs(
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
source_names=frappe.json.dumps([dn1.name, dn2.name]),
target_doc=si,
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
)
si.save().submit()
expected = [
{
"charge_type": "Actual",
"account_head": "Freight and Forwarding Charges - _TC",
"tax_amount": 120.0,
"total": 1520.0,
"base_total": 1520.0,
},
{
"charge_type": "Actual",
"account_head": "Marketing Expenses - _TC",
"tax_amount": 150.0,
"total": 1670.0,
"base_total": 1670.0,
},
{
"charge_type": "Actual",
"account_head": "Miscellaneous Expenses - _TC",
"tax_amount": 60.0,
"total": 1610.0,
"base_total": 1610.0,
},
]
actual = [
dict(
charge_type=x.charge_type,
account_head=x.account_head,
tax_amount=x.tax_amount,
total=x.total,
base_total=x.base_total,
)
for x in si.taxes
]
self.assertEqual(expected, actual)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()

View File

@@ -0,0 +1,58 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-02-04 10:53:32.307930",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"doctype_name",
"docfield_name",
"no_of_docs",
"done"
],
"fields": [
{
"fieldname": "doctype_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "DocType",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "docfield_name",
"fieldtype": "Data",
"label": "DocField",
"read_only": 1
},
{
"fieldname": "no_of_docs",
"fieldtype": "Int",
"in_list_view": 1,
"label": "No of Docs",
"read_only": 1
},
{
"default": "0",
"fieldname": "done",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Done",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-02-05 17:35:09.556054",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Transaction Deletion Record Details",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,26 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class TransactionDeletionRecordDetails(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
docfield_name: DF.Data | None
doctype_name: DF.Link
done: DF.Check
no_of_docs: DF.Int
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View File

@@ -673,7 +673,7 @@ class ReceivablePayableReport(object):
else:
future_amount_field = "future_amount_in_base_currency"
if row.remaining_balance > 0 and future.get(future_amount_field):
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_field] = future.get(future_amount_field) - row.outstanding

View File

@@ -469,11 +469,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
)
def test_future_payments(self):
sr = self.create_sales_invoice(do_not_submit=True)
sr.is_return = 1
sr.items[0].qty = -1
sr.items[0].rate = 10
sr.calculate_taxes_and_totals()
sr.submit()
si = self.create_sales_invoice()
pe = get_payment_entry(si.doctype, si.name)
pe.append(
"references",
{
"reference_doctype": sr.doctype,
"reference_name": sr.name,
"due_date": sr.due_date,
"total_amount": sr.grand_total,
"outstanding_amount": sr.outstanding_amount,
"allocated_amount": sr.outstanding_amount,
},
)
pe.posting_date = add_days(today(), 1)
pe.paid_amount = 90.0
pe.references[0].allocated_amount = 90.0
pe.paid_amount = 80
pe.references[0].allocated_amount = 90.0 # pe.paid_amount + sr.grand_total
pe.save().submit()
filters = {
"company": self.company,
@@ -485,16 +504,21 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"show_future_payments": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
self.assertEqual(len(report), 2)
expected_data = [100.0, 100.0, 10.0, 90.0]
expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [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]
)
rows = report[:2]
for row in rows:
self.assertEqual(
expected_data[row.voucher_no],
[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
)
pe.cancel()
sr.load_from_db() # Outstanding amount is updated so a updated timestamp is needed.
sr.cancel()
# full payment in future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)

View File

@@ -97,11 +97,11 @@ def execute(filters=None):
chart = get_chart_data(filters, columns, asset, liability, equity)
report_summary = get_report_summary(
report_summary, primitive_summary = get_report_summary(
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
)
return columns, data, message, chart, report_summary
return columns, data, message, chart, report_summary, primitive_summary
def get_provisional_profit_loss(
@@ -217,7 +217,7 @@ def get_report_summary(
"datatype": "Currency",
"currency": currency,
},
]
], (net_asset - net_liability + net_equity)
def get_chart_data(filters, columns, asset, liability, equity):

View File

@@ -55,10 +55,10 @@
</span>
</td>
<td style="text-align: right">
{%= format_currency(data[i].debit, filters.presentation_currency) %}
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
</td>
<td style="text-align: right">
{%= format_currency(data[i].credit, filters.presentation_currency) %}
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
</td>
{% } else { %}
<td></td>

View File

@@ -669,20 +669,20 @@ class GrossProfitGenerator(object):
elif row.sales_order and row.so_detail:
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
if incoming_amount:
return incoming_amount
return flt(row.qty) * incoming_amount
else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Avg
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = (
frappe.qb.from_(delivery_note_item)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
.select(Avg(delivery_note_item.incoming_rate))
.where(delivery_note_item.docstatus == 1)
.where(delivery_note_item.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order)
@@ -965,7 +965,7 @@ class GrossProfitGenerator(object):
& (sle.is_cancelled == 0)
)
.orderby(sle.item_code)
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
.orderby(sle.warehouse, sle.posting_datetime, sle.creation, order=Order.desc)
.run(as_dict=True)
)

View File

@@ -460,3 +460,95 @@ class TestGrossProfit(FrappeTestCase):
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])
def test_different_rates_in_si_and_dn(self):
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
"""
Test gp calculation when invoice and delivery note differ in qty and aren't connected
SO -- INV
|
DN
"""
se = make_stock_entry(
company=self.company,
item_code=self.item,
target=self.warehouse,
qty=3,
basic_rate=700,
do_not_submit=True,
)
item = se.items[0]
se.append(
"items",
{
"item_code": item.item_code,
"s_warehouse": item.s_warehouse,
"t_warehouse": item.t_warehouse,
"qty": 10,
"basic_rate": 700,
"conversion_factor": item.conversion_factor or 1.0,
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
"serial_no": item.serial_no,
"batch_no": item.batch_no,
"cost_center": item.cost_center,
"expense_account": item.expense_account,
},
)
se = se.save().submit()
so = make_sales_order(
customer=self.customer,
company=self.company,
warehouse=self.warehouse,
item=self.item,
rate=800,
qty=10,
do_not_save=False,
do_not_submit=False,
)
from erpnext.selling.doctype.sales_order.sales_order import (
make_delivery_note,
make_sales_invoice,
)
dn1 = make_delivery_note(so.name)
dn1.items[0].qty = 4
dn1.items[0].rate = 800
dn1.save().submit()
dn2 = make_delivery_note(so.name)
dn2.items[0].qty = 6
dn2.items[0].rate = 800
dn2.save().submit()
sinv = make_sales_invoice(so.name)
sinv.items[0].qty = 4
sinv.items[0].rate = 800
sinv.save().submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
expected_entry = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 4.0,
"avg._selling_rate": 800.0,
"valuation_rate": 700.0,
"selling_amount": 3200.0,
"buying_amount": 2800.0,
"gross_profit": 400.0,
"gross_profit_%": 12.5,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])

View File

@@ -59,10 +59,11 @@ def get_pos_entries(filters, group_by_field):
order_by += ", p.{}".format(group_by_field)
select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
# nosemgrep
return frappe.db.sql(
"""
SELECT
p.posting_date, p.name as pos_invoice, p.pos_profile,
p.posting_date, p.name as pos_invoice, p.pos_profile, p.company,
p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
FROM
`tabPOS Invoice` p {from_sales_invoice_payment}
@@ -207,14 +208,14 @@ def get_columns(filters):
"label": _("Grand Total"),
"fieldname": "grand_total",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 120,
},
{
"label": _("Paid Amount"),
"fieldname": "paid_amount",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 120,
},
{
@@ -224,6 +225,13 @@ def get_columns(filters):
"width": 150,
},
{"label": _("Is Return"), "fieldname": "is_return", "fieldtype": "Data", "width": 80},
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 120,
},
]
return columns

View File

@@ -26,3 +26,10 @@ frappe.require("assets/erpnext/js/financial_statements.js", function () {
default: 1,
});
});
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
});

View File

@@ -66,11 +66,11 @@ def execute(filters=None):
currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency"
)
report_summary = get_report_summary(
report_summary, primitive_summary = get_report_summary(
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
)
return columns, data, None, chart, report_summary
return columns, data, None, chart, report_summary, primitive_summary
def get_report_summary(
@@ -112,7 +112,7 @@ def get_report_summary(
"datatype": "Currency",
"currency": currency,
},
]
], net_profit
def get_net_profit_loss(income, expense, period_list, company, currency=None, consolidated=False):

View File

@@ -13,11 +13,13 @@ from frappe.query_builder import AliasedQuery, Criterion, Table
from frappe.query_builder.functions import Round, Sum
from frappe.query_builder.utils import DocType
from frappe.utils import (
add_days,
cint,
create_batch,
cstr,
flt,
formatdate,
get_datetime,
get_number_format_info,
getdate,
now,
@@ -682,7 +684,19 @@ def update_reference_in_payment_entry(
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
if not skip_ref_details_update_for_pe:
payment_entry.set_missing_ref_details(ref_exchange_rate=d.exchange_rate or None)
reference_exchange_details = frappe._dict()
if d.against_voucher_type == "Journal Entry" and d.exchange_rate:
reference_exchange_details.update(
{
"reference_doctype": d.against_voucher_type,
"reference_name": d.against_voucher,
"exchange_rate": d.exchange_rate,
}
)
payment_entry.set_missing_ref_details(
update_ref_details_only_for=[(d.against_voucher_type, d.against_voucher)],
reference_exchange_details=reference_exchange_details,
)
payment_entry.set_amounts()
payment_entry.make_exchange_gain_loss_journal(
@@ -1372,8 +1386,7 @@ def sort_stock_vouchers_by_posting_date(
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
.groupby(sle.voucher_type, sle.voucher_no)
.orderby(sle.posting_date)
.orderby(sle.posting_time)
.orderby(sle.posting_datetime)
.orderby(sle.creation)
).run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
@@ -2073,3 +2086,44 @@ def create_gain_loss_journal(
journal_entry.save()
journal_entry.submit()
return journal_entry.name
def run_ledger_health_checks():
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
if health_monitor_settings.enable_health_monitor:
period_end = getdate()
period_start = add_days(period_end, -abs(health_monitor_settings.monitor_for_last_x_days))
run_date = get_datetime()
# Debit-Credit mismatch report
if health_monitor_settings.debit_credit_mismatch:
for x in health_monitor_settings.companies:
filters = {"company": x.company, "from_date": period_start, "to_date": period_end}
voucher_wise = frappe.get_doc("Report", "Voucher-wise Balance")
res = voucher_wise.execute_script_report(filters=filters)
for x in res[1]:
doc = frappe.new_doc("Ledger Health")
doc.voucher_type = x.voucher_type
doc.voucher_no = x.voucher_no
doc.debit_credit_mismatch = True
doc.checked_on = run_date
doc.save()
# General Ledger and Payment Ledger discrepancy
if health_monitor_settings.general_and_payment_ledger_mismatch:
for x in health_monitor_settings.companies:
filters = {
"company": x.company,
"period_start_date": period_start,
"period_end_date": period_end,
}
gl_pl_comparison = frappe.get_doc("Report", "General and Payment Ledger Comparison")
res = gl_pl_comparison.execute_script_report(filters=filters)
for x in res[1]:
doc = frappe.new_doc("Ledger Health")
doc.voucher_type = x.voucher_type
doc.voucher_no = x.voucher_no
doc.general_and_payment_ledger_mismatch = True
doc.checked_on = run_date
doc.save()

View File

@@ -437,6 +437,7 @@ class Asset(AccountsController):
depreciation_amount,
from_date,
date_of_disposal,
original_schedule_date=schedule_date,
)
if depreciation_amount > 0:
@@ -1118,14 +1119,20 @@ class Asset(AccountsController):
return flt((100 * (1 - depreciation_rate)), float_precision)
def get_pro_rata_amt(
self, row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False
self,
row,
depreciation_amount,
from_date,
to_date,
has_wdv_or_dd_non_yearly_pro_rata=False,
original_schedule_date=None,
):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
if has_wdv_or_dd_non_yearly_pro_rata:
total_days = get_total_days(to_date, 12)
total_days = get_total_days(original_schedule_date or to_date, 12)
else:
total_days = get_total_days(to_date, row.frequency_of_depreciation)
total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
@@ -1445,32 +1452,35 @@ def get_straight_line_or_manual_depr_amount(
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
if row.daily_prorata_based:
daily_depr_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / date_diff(
get_last_day(
add_months(
row.depreciation_start_date,
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
* row.frequency_of_depreciation,
)
),
add_days(
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
total_days = (
date_diff(
get_last_day(
add_months(
row.depreciation_start_date,
flt(
row.total_number_of_depreciations
- asset.number_of_depreciations_booked
- number_of_pending_depreciations
- 1
)
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
* row.frequency_of_depreciation,
)
),
1,
),
add_days(
get_last_day(
add_months(
row.depreciation_start_date,
flt(
row.total_number_of_depreciations
- asset.number_of_depreciations_booked
- number_of_pending_depreciations
- 1
)
* row.frequency_of_depreciation,
)
),
1,
),
)
+ 1
)
daily_depr_amount = amount / total_days
to_date = get_last_day(
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
@@ -1490,22 +1500,28 @@ def get_straight_line_or_manual_depr_amount(
# if the Depreciation Schedule is being prepared for the first time
else:
if row.daily_prorata_based:
daily_depr_amount = (
amount = (
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
) / date_diff(
get_last_day(
add_months(
row.depreciation_start_date,
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
* row.frequency_of_depreciation,
)
),
add_days(
get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), 1
),
)
total_days = (
date_diff(
get_last_day(
add_months(
row.depreciation_start_date,
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
* row.frequency_of_depreciation,
)
),
add_days(
get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), 1
),
)
+ 1
)
daily_depr_amount = amount / total_days
to_date = get_last_day(
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)

View File

@@ -219,7 +219,11 @@ class TestAsset(AssetSetup):
asset.precision("gross_purchase_amount"),
)
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
asset.finance_books[0],
9000,
get_last_day(add_months(purchase_date, 1)),
date,
original_schedule_date=get_last_day(nowdate()),
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
self.assertEquals(
@@ -287,7 +291,11 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
asset.finance_books[0],
9000,
get_last_day(add_months(purchase_date, 1)),
date,
original_schedule_date=get_last_day(nowdate()),
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
@@ -349,7 +357,7 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]]
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1737.7, 37737.7]]
for i, schedule in enumerate(asset.schedules):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
@@ -360,7 +368,7 @@ class TestAsset(AssetSetup):
expected_gle = (
(
"_Test Accumulated Depreciations - _TC",
37742.47,
37737.7,
0.0,
),
(
@@ -371,7 +379,7 @@ class TestAsset(AssetSetup):
(
"_Test Gain/Loss on Asset Disposal - _TC",
0.0,
17742.47,
17737.7,
),
("Debtors - _TC", 40000.0, 0.0),
)
@@ -691,18 +699,18 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
["2023-01-31", 1021.98, 1021.98],
["2023-02-28", 923.08, 1945.06],
["2023-03-31", 1021.98, 2967.04],
["2023-04-30", 989.01, 3956.05],
["2023-05-31", 1021.98, 4978.03],
["2023-06-30", 989.01, 5967.04],
["2023-07-31", 1021.98, 6989.02],
["2023-08-31", 1021.98, 8011.0],
["2023-09-30", 989.01, 9000.01],
["2023-10-31", 1021.98, 10021.99],
["2023-11-30", 989.01, 11011.0],
["2023-12-31", 989.0, 12000.0],
["2023-01-31", 1019.18, 1019.18],
["2023-02-28", 920.55, 1939.73],
["2023-03-31", 1019.18, 2958.91],
["2023-04-30", 986.3, 3945.21],
["2023-05-31", 1019.18, 4964.39],
["2023-06-30", 986.3, 5950.69],
["2023-07-31", 1019.18, 6969.87],
["2023-08-31", 1019.18, 7989.05],
["2023-09-30", 986.3, 8975.35],
["2023-10-31", 1019.18, 9994.53],
["2023-11-30", 986.3, 10980.83],
["2023-12-31", 1019.17, 12000.0],
]
schedules = [

View File

@@ -149,6 +149,7 @@ def get_data(filters):
"asset_category": asset.asset_category,
"purchase_date": asset.purchase_date,
"asset_value": asset_value,
"company": asset.company,
}
data.append(row)
@@ -379,30 +380,37 @@ def get_columns(filters):
"label": _("Gross Purchase Amount"),
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 250,
},
{
"label": _("Opening Accumulated Depreciation"),
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 250,
},
{
"label": _("Depreciated Amount"),
"fieldname": "depreciated_amount",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 250,
},
{
"label": _("Asset Value"),
"fieldname": "asset_value",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 250,
},
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 120,
},
]
return [
@@ -433,28 +441,28 @@ def get_columns(filters):
"label": _("Gross Purchase Amount"),
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 100,
},
{
"label": _("Asset Value"),
"fieldname": "asset_value",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 100,
},
{
"label": _("Opening Accumulated Depreciation"),
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 90,
},
{
"label": _("Depreciated Amount"),
"fieldname": "depreciated_amount",
"fieldtype": "Currency",
"options": "company:currency",
"options": "Company:company:default_currency",
"width": 100,
},
{
@@ -479,4 +487,11 @@ def get_columns(filters):
"options": "Location",
"width": 100,
},
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 120,
},
]

View File

@@ -461,7 +461,7 @@
},
{
"fieldname": "other_charges_calculation",
"fieldtype": "Markdown Editor",
"fieldtype": "Text Editor",
"label": "Taxes and Charges Calculation",
"no_copy": 1,
"oldfieldtype": "HTML",
@@ -927,7 +927,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-03-20 16:03:59.069145",
"modified": "2024-03-28 10:20:30.231915",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
@@ -995,4 +995,4 @@
"states": [],
"timeline_field": "supplier",
"title_field": "title"
}
}

View File

@@ -3,6 +3,7 @@
import json
from collections import defaultdict
import frappe
from frappe import _, bold, qb, throw
@@ -1947,21 +1948,26 @@ class AccountsController(TransactionBase):
)
def group_similar_items(self):
group_item_qty = {}
group_item_amount = {}
grouped_items = {}
# to update serial number in print
count = 0
fields_to_group = frappe.get_hooks("fields_for_group_similar_items")
fields_to_group = set(fields_to_group)
for item in self.items:
group_item_qty[item.item_code] = group_item_qty.get(item.item_code, 0) + item.qty
group_item_amount[item.item_code] = group_item_amount.get(item.item_code, 0) + item.amount
item_values = grouped_items.setdefault(item.item_code, defaultdict(int))
for field in fields_to_group:
item_values[field] += item.get(field, 0)
duplicate_list = []
for item in self.items:
if item.item_code in group_item_qty:
if item.item_code in grouped_items:
count += 1
item.qty = group_item_qty[item.item_code]
item.amount = group_item_amount[item.item_code]
for field in fields_to_group:
item.set(field, grouped_items[item.item_code][field])
if item.qty:
item.rate = flt(flt(item.amount) / flt(item.qty), item.precision("rate"))
@@ -1969,7 +1975,7 @@ class AccountsController(TransactionBase):
item.rate = 0
item.idx = count
del group_item_qty[item.item_code]
del grouped_items[item.item_code]
else:
duplicate_list.append(item)
for item in duplicate_list:
@@ -3313,6 +3319,37 @@ def check_if_child_table_updated(
return False
def merge_taxes(source_taxes, target_doc):
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
update_item_wise_tax_detail,
)
existing_taxes = target_doc.get("taxes") or []
idx = 1
for tax in source_taxes:
found = False
for t in existing_taxes:
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
update_item_wise_tax_detail(t, tax)
found = True
if not found:
tax.charge_type = "Actual"
tax.idx = idx
idx += 1
tax.included_in_print_rate = 0
tax.dont_recompute_tax = 1
tax.row_id = ""
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
tax.item_wise_tax_detail = tax.item_wise_tax_detail
existing_taxes.append(tax)
target_doc.set("taxes", existing_taxes)
@erpnext.allow_regional
def validate_regional(doc):
pass

View File

@@ -669,7 +669,7 @@ class StockController(AccountsController):
self.validate_multi_currency()
self.validate_packed_items()
if self.get("is_internal_supplier"):
if self.get("is_internal_supplier") and self.docstatus == 1:
self.validate_internal_transfer_qty()
else:
self.validate_internal_transfer_warehouse()

View File

@@ -56,7 +56,8 @@ 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 = Journals against Journals
50 series - Journals against Journals
60 series - Journals against Payment Entries
90 series - Dimension inheritence
"""
@@ -1574,3 +1575,70 @@ class TestAccountsController(FrappeTestCase):
exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name)
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_je, [])
def test_60_payment_entry_against_journal(self):
# Invoices
exc_rate1 = 75
exc_rate2 = 77
amount = 1
je1 = self.create_journal_entry(
acc1=self.debit_usd,
acc1_exc_rate=exc_rate1,
acc2=self.cash,
acc1_amount=amount,
acc2_amount=(amount * 75),
acc2_exc_rate=1,
)
je1.accounts[0].party_type = "Customer"
je1.accounts[0].party = self.customer
je1 = je1.save().submit()
je2 = self.create_journal_entry(
acc1=self.debit_usd,
acc1_exc_rate=exc_rate2,
acc2=self.cash,
acc1_amount=amount,
acc2_amount=(amount * exc_rate2),
acc2_exc_rate=1,
)
je2.accounts[0].party_type = "Customer"
je2.accounts[0].party = self.customer
je2 = je2.save().submit()
# Payment
pe = self.create_payment_entry(amount=2, source_exc_rate=exc_rate1).save().submit()
pr = self.create_payment_reconciliation()
pr.receivable_payable_account = self.debit_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 2)
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 no outstanding in both currencies
self.assert_ledger_outstanding(je1.doctype, je1.name, 0.0, 0.0)
self.assert_ledger_outstanding(je2.doctype, je2.name, 0.0, 0.0)
# Exchange Gain/Loss Journal should've been created only for JE2
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
self.assertEqual(exc_je_for_je1, [])
self.assertEqual(len(exc_je_for_je2), 1)
# Cancel Payment
pe.reload()
pe.cancel()
self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount)
self.assert_ledger_outstanding(je2.doctype, je2.name, (amount * exc_rate2), amount)
# Exchange Gain/Loss Journal should've been cancelled
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
self.assertEqual(exc_je_for_je1, [])
self.assertEqual(exc_je_for_je2, [])

View File

@@ -299,7 +299,10 @@ period_closing_doctypes = [
doc_events = {
"*": {
"validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
"validate": [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
"erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.check_for_running_deletion_job",
],
},
tuple(period_closing_doctypes): {
"validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save",
@@ -446,6 +449,7 @@ scheduler_events = {
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
"erpnext.accounts.utils.run_ledger_health_checks",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
@@ -640,3 +644,5 @@ extend_bootinfo = [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes",
"erpnext.startup.boot.bootinfo",
]
fields_for_group_similar_items = ["qty", "amount"]

View File

@@ -978,8 +978,7 @@ def get_valuation_rate(data):
frappe.qb.from_(sle)
.select(sle.valuation_rate)
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
.orderby(sle.posting_date, order=frappe.qb.desc)
.orderby(sle.posting_time, order=frappe.qb.desc)
.orderby(sle.posting_datetime, order=frappe.qb.desc)
.orderby(sle.creation, order=frappe.qb.desc)
.limit(1)
).run(as_dict=True)

View File

@@ -375,7 +375,7 @@ class JobCard(Document):
{
"to_time": get_datetime(args.get("complete_time")),
"operation": args.get("sub_operation"),
"completed_qty": args.get("completed_qty") or 0.0,
"completed_qty": (args.get("completed_qty") if last_row.idx == row.idx else 0.0),
}
)
elif args.get("start_time"):

View File

@@ -58,7 +58,7 @@ def get_data(filters):
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
data = frappe.get_all(
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
)
res = []

View File

@@ -274,6 +274,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes #22-02-2024
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v14_0.delete_healthcare_doctypes
@@ -354,11 +355,11 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency"
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
erpnext.patches.v14_0.update_total_asset_cost_field
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
erpnext.patches.v14_0.update_flag_for_return_invoices
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1)
erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset

View File

@@ -12,6 +12,10 @@ def execute():
creation_date = "2024-01-25"
si = qb.DocType("Sales Invoice")
# unset flag, as migration would have set it for all records, as the field was introduced with default '1'
qb.update(si).set(si.update_outstanding_for_self, False).run()
if cr_notes := (
qb.from_(si)
.select(si.name)
@@ -37,6 +41,10 @@ def execute():
).run()
pi = qb.DocType("Purchase Invoice")
# unset flag, as migration would have set it for all records, as the field was introduced with default '1'
qb.update(pi).set(pi.update_outstanding_for_self, False).run()
if dr_notes := (
qb.from_(pi)
.select(pi.name)

View File

@@ -0,0 +1,19 @@
import frappe
def execute():
frappe.db.sql(
"""
UPDATE `tabStock Ledger Entry`
SET posting_datetime = DATE_FORMAT(timestamp(posting_date, posting_time), '%Y-%m-%d %H:%i:%s')
"""
)
drop_indexes()
def drop_indexes():
if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"):
return
frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`")

View File

@@ -87,6 +87,7 @@ class Project(Document):
is_group=task_details.is_group,
color=task_details.color,
template_task=task_details.name,
priority=task_details.priority,
)
).insert()

View File

@@ -23,7 +23,11 @@ class TestProject(FrappeTestCase):
task1 = task_exists("Test Template Task with No Parent and Dependency")
if not task1:
task1 = create_task(
subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3
subject="Test Template Task with No Parent and Dependency",
is_template=1,
begin=5,
duration=3,
priority="High",
)
template = make_project_template(
@@ -32,11 +36,12 @@ class TestProject(FrappeTestCase):
project = get_project(project_name, template)
tasks = frappe.get_all(
"Task",
["subject", "exp_end_date", "depends_on_tasks"],
["subject", "exp_end_date", "depends_on_tasks", "priority"],
dict(project=project.name),
order_by="creation asc",
)
self.assertEqual(tasks[0].priority, "High")
self.assertEqual(tasks[0].subject, "Test Template Task with No Parent and Dependency")
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
self.assertEqual(len(tasks), 1)

View File

@@ -122,6 +122,7 @@ def create_task(
begin=0,
duration=0,
save=True,
priority=None,
):
if not frappe.db.exists("Task", subject):
task = frappe.new_doc("Task")
@@ -139,6 +140,7 @@ def create_task(
task.duration = duration
task.is_group = is_group
task.parent_task = parent_task
task.priority = priority
if save:
task.save()
else:

View File

@@ -819,7 +819,7 @@ erpnext.utils.map_current_doc = function (opts) {
if (opts.source_doctype) {
let data_fields = [];
if (opts.source_doctype == "Purchase Receipt") {
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
data_fields.push({
fieldname: "merge_taxes",
fieldtype: "Check",
@@ -845,7 +845,10 @@ erpnext.utils.map_current_doc = function (opts) {
return;
}
opts.source_name = values;
if (opts.allow_child_item_selection || opts.source_doctype == "Purchase Receipt") {
if (
opts.allow_child_item_selection ||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
) {
// args contains filtered child docnames
opts.args = args;
}

View File

@@ -832,7 +832,8 @@
"label": "Purchase Order",
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "column_break_89",
@@ -875,7 +876,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-24 19:07:17.715231",
"modified": "2024-03-21 18:15:56.625005",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -259,6 +259,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
subject: __(frm.meta.name) + ": " + doc.name,
doctype: doc.doctype,
name: doc.name,
content: "",
send_email: 1,
print_format,
sender_full_name: frappe.user.full_name(),

View File

@@ -197,6 +197,8 @@ def prepare_data(
):
details[p_key] += r.get(qty_or_amount_field, 0)
details[variance_key] = details.get(p_key) - details.get(target_key)
else:
details[variance_key] = details.get(p_key) - details.get(target_key)
details["total_achieved"] += details.get(p_key)
details["total_variance"] = details.get("total_achieved") - details.get("total_target")
@@ -209,31 +211,32 @@ def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_fi
parent_doc = frappe.qb.DocType(filters.get("doctype"))
child_doc = frappe.qb.DocType(filters.get("doctype") + " Item")
sales_team = frappe.qb.DocType("Sales Team")
query = (
frappe.qb.from_(parent_doc)
.inner_join(child_doc)
.on(child_doc.parent == parent_doc.name)
.inner_join(sales_team)
.on(sales_team.parent == parent_doc.name)
.select(
child_doc.item_group,
(child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"),
(child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"),
sales_team.sales_person,
parent_doc[date_field],
)
.where(
(parent_doc.docstatus == 1)
& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
)
)
query = frappe.qb.from_(parent_doc).inner_join(child_doc).on(child_doc.parent == parent_doc.name)
if sales_field == "sales_person":
query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data))
sales_team = frappe.qb.DocType("Sales Team")
stock_qty = child_doc.stock_qty * sales_team.allocated_percentage / 100
net_amount = child_doc.base_net_amount * sales_team.allocated_percentage / 100
sales_field_col = sales_team[sales_field]
query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name)
else:
query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data))
stock_qty = child_doc.stock_qty
net_amount = child_doc.base_net_amount
sales_field_col = parent_doc[sales_field]
query = query.select(
child_doc.item_group,
parent_doc[date_field],
(stock_qty).as_("stock_qty"),
(net_amount).as_("base_net_amount"),
sales_field_col,
).where(
(parent_doc.docstatus == 1)
& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
& (sales_field_col.isin(sales_users_or_territory_data))
)
return query.run(as_dict=True)

View File

@@ -0,0 +1,57 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.utils import get_fiscal_year
from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.sales_partner_target_variance_based_on_item_group import (
execute,
)
from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import (
create_sales_target_doc,
create_target_distribution,
)
class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase):
def setUp(self):
self.fiscal_year = get_fiscal_year(nowdate())[0]
def tearDown(self):
frappe.db.rollback()
def test_achieved_target_and_variance_for_partner(self):
# Create a Target Distribution
distribution = create_target_distribution(self.fiscal_year)
# Create Sales Partner with targets for the current fiscal year
sales_partner = create_sales_target_doc(
"Sales Partner", "partner_name", "Sales Partner 1", self.fiscal_year, distribution.name
)
# Create a Sales Invoice for the Partner
si = create_sales_invoice(
rate=1000,
qty=20,
do_not_submit=True,
)
si.sales_partner = sales_partner
si.commission_rate = 5
si.submit()
# Check Achieved Target and Variance for the Sales Partner
result = execute(
frappe._dict(
{
"fiscal_year": self.fiscal_year,
"doctype": "Sales Invoice",
"period": "Yearly",
"target_on": "Quantity",
}
)
)[1]
row = frappe._dict(result[0])
self.assertSequenceEqual(
[flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)],
[50, 20, -30],
)

View File

@@ -18,17 +18,17 @@ class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase):
def test_achieved_target_and_variance(self):
# Create a Target Distribution
distribution = frappe.new_doc("Monthly Distribution")
distribution.distribution_id = "Target Report Distribution"
distribution.fiscal_year = self.fiscal_year
distribution.get_months()
distribution.insert()
distribution = create_target_distribution(self.fiscal_year)
# Create sales people with targets
person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name)
person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name)
# Create sales people with targets for the current fiscal year
person_1 = create_sales_target_doc(
"Sales Person", "sales_person_name", "Sales Person 1", self.fiscal_year, distribution.name
)
person_2 = create_sales_target_doc(
"Sales Person", "sales_person_name", "Sales Person 2", self.fiscal_year, distribution.name
)
# Create a Sales Order with 50-50 contribution
# Create a Sales Order with 50-50 contribution between both Sales people
so = make_sales_order(
rate=1000,
qty=20,
@@ -69,10 +69,20 @@ class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase):
)
def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id):
sales_person = frappe.new_doc("Sales Person")
sales_person.sales_person_name = sales_person_name
sales_person.append(
def create_target_distribution(fiscal_year):
distribution = frappe.new_doc("Monthly Distribution")
distribution.distribution_id = "Target Report Distribution"
distribution.fiscal_year = fiscal_year
distribution.get_months()
return distribution.insert()
def create_sales_target_doc(
sales_field_dt, sales_field_name, sales_field_value, fiscal_year, distribution_id
):
sales_target_doc = frappe.new_doc(sales_field_dt)
sales_target_doc.set(sales_field_name, sales_field_value)
sales_target_doc.append(
"targets",
{
"fiscal_year": fiscal_year,
@@ -81,4 +91,6 @@ def create_sales_person_with_target(sales_person_name, fiscal_year, distribution
"distribution_id": distribution_id,
},
)
return sales_person.insert()
if sales_field_dt == "Sales Partner":
sales_target_doc.commission_rate = 5
return sales_target_doc.insert()

228
erpnext/setup/demo.py Normal file
View File

@@ -0,0 +1,228 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import json
import os
from random import randint
import frappe
from frappe import _
from frappe.utils import add_days, getdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
def setup_demo_data():
from frappe.utils.telemetry import capture
capture("demo_data_creation_started", "erpnext")
try:
company = create_demo_company()
process_masters()
make_transactions(company)
frappe.cache.delete_keys("bootinfo")
frappe.publish_realtime("demo_data_complete")
except Exception:
frappe.log_error("Failed to create demo data")
capture("demo_data_creation_failed", "erpnext", properties={"exception": frappe.get_traceback()})
raise
capture("demo_data_creation_completed", "erpnext")
@frappe.whitelist()
def clear_demo_data():
from frappe.utils.telemetry import capture
frappe.only_for("System Manager")
capture("demo_data_erased", "erpnext")
try:
company = frappe.db.get_single_value("Global Defaults", "demo_company")
create_transaction_deletion_record(company)
clear_masters()
delete_company(company)
default_company = frappe.db.get_single_value("Global Defaults", "default_company")
frappe.db.set_default("company", default_company)
except Exception:
frappe.db.rollback()
frappe.log_error("Failed to erase demo data")
frappe.throw(
_("Failed to erase demo data, please delete the demo company manually."),
title=_("Could Not Delete Demo Data"),
)
def create_demo_company():
company = frappe.db.get_all("Company")[0].name
company_doc = frappe.get_doc("Company", company)
# Make a dummy company
new_company = frappe.new_doc("Company")
new_company.company_name = company_doc.company_name + " (Demo)"
new_company.abbr = company_doc.abbr + "D"
new_company.enable_perpetual_inventory = 1
new_company.default_currency = company_doc.default_currency
new_company.country = company_doc.country
new_company.chart_of_accounts_based_on = "Standard Template"
new_company.chart_of_accounts = company_doc.chart_of_accounts
new_company.insert()
# Set Demo Company as default to
frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name)
frappe.db.set_default("company", new_company.name)
bank_account = create_bank_account({"company_name": new_company.name})
frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name)
return new_company.name
def process_masters():
for doctype in frappe.get_hooks("demo_master_doctypes"):
data = read_data_file_using_hooks(doctype)
if data:
for item in json.loads(data):
create_demo_record(item)
def create_demo_record(doctype):
frappe.get_doc(doctype).insert(ignore_permissions=True)
def make_transactions(company):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
from erpnext.accounts.utils import FiscalYearError
try:
start_date = get_fiscal_year(date=getdate())[1]
except FiscalYearError:
# User might have setup fiscal year for previous or upcoming years
active_fiscal_years = frappe.db.get_all("Fiscal Year", filters={"disabled": 0}, as_list=1)
if active_fiscal_years:
start_date = frappe.db.get_value("Fiscal Year", active_fiscal_years[0][0], "year_start_date")
else:
frappe.throw(_("There are no active Fiscal Years for which Demo Data can be generated."))
for doctype in frappe.get_hooks("demo_transaction_doctypes"):
data = read_data_file_using_hooks(doctype)
if data:
for item in json.loads(data):
create_transaction(item, company, start_date)
convert_order_to_invoices()
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0)
def create_transaction(doctype, company, start_date):
document_type = doctype.get("doctype")
warehouse = get_warehouse(company)
if document_type == "Purchase Order":
posting_date = get_random_date(start_date, 1, 25)
else:
posting_date = get_random_date(start_date, 31, 350)
doctype.update(
{
"company": company,
"set_posting_time": 1,
"transaction_date": posting_date,
"schedule_date": posting_date,
"delivery_date": posting_date,
"set_warehouse": warehouse,
}
)
doc = frappe.get_doc(doctype)
doc.save(ignore_permissions=True)
doc.submit()
def convert_order_to_invoices():
for document in ["Purchase Order", "Sales Order"]:
# Keep some orders intentionally unbilled/unpaid
for i, order in enumerate(
frappe.db.get_all(
document, filters={"docstatus": 1}, fields=["name", "transaction_date"], limit=6
)
):
if document == "Purchase Order":
invoice = make_purchase_invoice(order.name)
elif document == "Sales Order":
invoice = make_sales_invoice(order.name)
invoice.set_posting_time = 1
invoice.posting_date = order.transaction_date
invoice.due_date = order.transaction_date
invoice.bill_date = order.transaction_date
if invoice.get("payment_schedule"):
invoice.payment_schedule[0].due_date = order.transaction_date
invoice.update_stock = 1
invoice.submit()
if i % 2 != 0:
payment = get_payment_entry(invoice.doctype, invoice.name)
payment.posting_date = order.transaction_date
payment.reference_no = invoice.name
payment.submit()
def get_random_date(start_date, start_range, end_range):
return add_days(start_date, randint(start_range, end_range))
def create_transaction_deletion_record(company):
transaction_deletion_record = frappe.new_doc("Transaction Deletion Record")
transaction_deletion_record.company = company
transaction_deletion_record.process_in_single_transaction = True
transaction_deletion_record.save(ignore_permissions=True)
transaction_deletion_record.submit()
transaction_deletion_record.start_deletion_tasks()
def clear_masters():
for doctype in frappe.get_hooks("demo_master_doctypes")[::-1]:
data = read_data_file_using_hooks(doctype)
if data:
for item in json.loads(data):
clear_demo_record(item)
def clear_demo_record(document):
document_type = document.get("doctype")
del document["doctype"]
valid_columns = frappe.get_meta(document_type).get_valid_columns()
filters = document
for key in list(filters):
if key not in valid_columns:
filters.pop(key, None)
doc = frappe.get_doc(document_type, filters)
doc.delete(ignore_permissions=True)
def delete_company(company):
frappe.db.set_single_value("Global Defaults", "demo_company", "")
frappe.delete_doc("Company", company, ignore_permissions=True)
def read_data_file_using_hooks(doctype):
path = os.path.join(os.path.dirname(__file__), "demo_data")
with open(os.path.join(path, doctype + ".json"), "r") as f:
data = f.read()
return data
def get_warehouse(company):
warehouses = frappe.db.get_all("Warehouse", {"company": company, "is_group": 0})
return warehouses[randint(0, 3)].name

View File

@@ -169,43 +169,49 @@ frappe.ui.form.on("Company", {
},
delete_company_transactions: function (frm) {
frappe.verify_password(function () {
var d = frappe.prompt(
{
fieldtype: "Data",
fieldname: "company_name",
label: __("Please enter the company name to confirm"),
reqd: 1,
description: __(
"Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone."
),
},
function (data) {
if (data.company_name !== frm.doc.name) {
frappe.msgprint(__("Company name not same"));
return;
}
frappe.call({
method: "erpnext.setup.doctype.company.company.create_transaction_deletion_request",
args: {
company: data.company_name,
},
freeze: true,
callback: function (r, rt) {
if (!r.exc)
frappe.msgprint(
__("Successfully deleted all transactions related to this company!")
);
},
onerror: function () {
frappe.msgprint(__("Wrong Password"));
},
frappe.call({
method: "erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.is_deletion_doc_running",
args: {
company: frm.doc.name,
},
freeze: true,
callback: function (r) {
if (!r.exc) {
frappe.verify_password(function () {
var d = frappe.prompt(
{
fieldtype: "Data",
fieldname: "company_name",
label: __("Please enter the company name to confirm"),
reqd: 1,
description: __(
"Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone."
),
},
function (data) {
if (data.company_name !== frm.doc.name) {
frappe.msgprint(__("Company name not same"));
return;
}
frappe.call({
method: "erpnext.setup.doctype.company.company.create_transaction_deletion_request",
args: {
company: data.company_name,
},
freeze: true,
callback: function (r, rt) {},
onerror: function () {
frappe.msgprint(__("Wrong Password"));
},
});
},
__("Delete all the Transactions for this Company"),
__("Delete")
);
d.get_primary_btn().addClass("btn-danger");
});
},
__("Delete all the Transactions for this Company"),
__("Delete")
);
d.get_primary_btn().addClass("btn-danger");
}
},
});
},
});

View File

@@ -11,7 +11,7 @@ from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import cint, formatdate, get_timestamp, today
from frappe.utils import cint, formatdate, get_link_to_form, get_timestamp, today
from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency
@@ -812,6 +812,19 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad
@frappe.whitelist()
def create_transaction_deletion_request(company):
from erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record import (
is_deletion_doc_running,
)
is_deletion_doc_running(company)
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
tdr.insert()
tdr.submit()
tdr.start_deletion_tasks()
frappe.msgprint(
_("A Transaction Deletion Document: {0} is triggered for {0}").format(
get_link_to_form("Transaction Deletion Record", tdr.name)
),
frappe.bold(company),
)

View File

@@ -28,6 +28,7 @@ class TestTransactionDeletionRecord(unittest.TestCase):
for i in range(5):
create_task("Dunder Mifflin Paper Co")
tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co")
tdr.reload()
for doctype in tdr.doctypes:
if doctype.doctype_name == "Task":
self.assertEqual(doctype.no_of_docs, 5)
@@ -49,7 +50,9 @@ def create_company(company_name):
def create_transaction_deletion_request(company):
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
tdr.insert()
tdr.process_in_single_transaction = True
tdr.submit()
tdr.start_deletion_tasks()
return tdr

View File

@@ -10,20 +10,24 @@ frappe.ui.form.on("Transaction Deletion Record", {
callback: function (r) {
doctypes_to_be_ignored_array = r.message;
populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm);
frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false);
frm.refresh_field("doctypes_to_be_ignored");
},
});
}
frm.get_field("doctypes_to_be_ignored").grid.cannot_add_rows = true;
frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false);
frm.refresh_field("doctypes_to_be_ignored");
},
refresh: function (frm) {
frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false);
frm.refresh_field("doctypes_to_be_ignored");
if (frm.doc.docstatus == 1 && ["Queued", "Failed"].find((x) => x == frm.doc.status)) {
let execute_btn = frm.doc.status == "Queued" ? __("Start Deletion") : __("Retry");
frm.add_custom_button(execute_btn, () => {
// Entry point for chain of events
frm.call({
method: "start_deletion_tasks",
doc: frm.doc,
});
});
}
},
});

View File

@@ -7,10 +7,21 @@
"engine": "InnoDB",
"field_order": [
"company",
"section_break_qpwb",
"status",
"error_log",
"tasks_section",
"delete_bin_data",
"delete_leads_and_addresses",
"reset_company_default_values",
"clear_notifications",
"initialize_doctypes_table",
"delete_transactions",
"section_break_tbej",
"doctypes",
"doctypes_to_be_ignored",
"amended_from",
"status"
"process_in_single_transaction"
],
"fields": [
{
@@ -25,14 +36,16 @@
"fieldname": "doctypes",
"fieldtype": "Table",
"label": "Summary",
"options": "Transaction Deletion Record Item",
"no_copy": 1,
"options": "Transaction Deletion Record Details",
"read_only": 1
},
{
"fieldname": "doctypes_to_be_ignored",
"fieldtype": "Table",
"label": "Excluded DocTypes",
"options": "Transaction Deletion Record Item"
"options": "Transaction Deletion Record Item",
"read_only": 1
},
{
"fieldname": "amended_from",
@@ -46,18 +59,96 @@
{
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Draft\nCompleted"
"no_copy": 1,
"options": "Queued\nRunning\nFailed\nCompleted\nCancelled",
"read_only": 1
},
{
"fieldname": "section_break_tbej",
"fieldtype": "Section Break"
},
{
"fieldname": "tasks_section",
"fieldtype": "Section Break",
"label": "Tasks"
},
{
"default": "0",
"fieldname": "delete_bin_data",
"fieldtype": "Check",
"label": "Delete Bins",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "delete_leads_and_addresses",
"fieldtype": "Check",
"label": "Delete Leads and Addresses",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "clear_notifications",
"fieldtype": "Check",
"label": "Clear Notifications",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "reset_company_default_values",
"fieldtype": "Check",
"label": "Reset Company Default Values",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "delete_transactions",
"fieldtype": "Check",
"label": "Delete Transactions",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "initialize_doctypes_table",
"fieldtype": "Check",
"label": "Initialize Summary Table",
"no_copy": 1,
"read_only": 1
},
{
"depends_on": "eval: doc.error_log",
"fieldname": "error_log",
"fieldtype": "Long Text",
"label": "Error Log"
},
{
"fieldname": "section_break_qpwb",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "process_in_single_transaction",
"fieldtype": "Check",
"hidden": 1,
"label": "Process in Single Transaction",
"no_copy": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-08-04 20:15:59.071493",
"modified": "2024-03-21 10:29:19.456413",
"modified_by": "Administrator",
"module": "Setup",
"name": "Transaction Deletion Record",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -76,5 +167,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,18 +1,31 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from collections import OrderedDict
import frappe
from frappe import _, qb
from frappe.desk.notifications import clear_notifications
from frappe.model.document import Document
from frappe.utils import cint, create_batch
from frappe.utils import cint, comma_and, create_batch, get_link_to_form
from frappe.utils.background_jobs import create_job_id, is_job_enqueued
class TransactionDeletionRecord(Document):
def __init__(self, *args, **kwargs):
super(TransactionDeletionRecord, self).__init__(*args, **kwargs)
self.batch_size = 5000
# Tasks are listed by their execution order
self.task_to_internal_method_map = OrderedDict(
{
"Delete Bins": "delete_bins",
"Delete Leads and Addresses": "delete_lead_addresses",
"Reset Company Values": "reset_company_values",
"Clear Notifications": "delete_notifications",
"Initialize Summary Table": "initialize_doctypes_to_be_deleted_table",
"Delete Transactions": "delete_company_transactions",
}
)
def validate(self):
frappe.only_for("System Manager")
@@ -29,104 +42,266 @@ class TransactionDeletionRecord(Document):
title=_("Not Allowed"),
)
def generate_job_name_for_task(self, task=None):
method = self.task_to_internal_method_map[task]
return f"{self.name}_{method}"
def generate_job_name_for_next_tasks(self, task=None):
job_names = []
current_task_idx = list(self.task_to_internal_method_map).index(task)
for idx, task in enumerate(self.task_to_internal_method_map.keys(), 0):
# generate job_name for next tasks
if idx > current_task_idx:
job_names.append(self.generate_job_name_for_task(task))
return job_names
def generate_job_name_for_all_tasks(self):
job_names = []
for task in self.task_to_internal_method_map.keys():
job_names.append(self.generate_job_name_for_task(task))
return job_names
def before_submit(self):
if queued_docs := frappe.db.get_all(
"Transaction Deletion Record",
filters={"company": self.company, "status": ("in", ["Running", "Queued"]), "docstatus": 1},
pluck="name",
):
frappe.throw(
_(
"Cannot enqueue multi docs for one company. {0} is already queued/running for company: {1}"
).format(
comma_and([get_link_to_form("Transaction Deletion Record", x) for x in queued_docs]),
frappe.bold(self.company),
)
)
if not self.doctypes_to_be_ignored:
self.populate_doctypes_to_be_ignored_table()
self.delete_bins()
self.delete_lead_addresses()
self.reset_company_values()
clear_notifications()
self.delete_company_transactions()
def reset_task_flags(self):
self.clear_notifications = 0
self.delete_bin_data = 0
self.delete_leads_and_addresses = 0
self.delete_transactions = 0
self.initialize_doctypes_table = 0
self.reset_company_default_values = 0
def before_save(self):
self.status = ""
self.doctypes.clear()
self.reset_task_flags()
def on_submit(self):
self.db_set("status", "Queued")
def on_cancel(self):
self.db_set("status", "Cancelled")
def enqueue_task(self, task: str | None = None):
if task and task in self.task_to_internal_method_map:
# make sure that none of next tasks are already running
job_names = self.generate_job_name_for_next_tasks(task=task)
self.validate_running_task_for_doc(job_names=job_names)
# Generate Job Id to uniquely identify each task for this document
job_id = self.generate_job_name_for_task(task)
if self.process_in_single_transaction:
self.execute_task(task_to_execute=task)
else:
frappe.enqueue(
"frappe.utils.background_jobs.run_doc_method",
doctype=self.doctype,
name=self.name,
doc_method="execute_task",
job_id=job_id,
queue="long",
enqueue_after_commit=True,
task_to_execute=task,
)
def execute_task(self, task_to_execute: str | None = None):
if task_to_execute:
method = self.task_to_internal_method_map[task_to_execute]
if task := getattr(self, method, None):
try:
task()
except Exception as err:
frappe.db.rollback()
traceback = frappe.get_traceback(with_context=True)
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value(self.doctype, self.name, "error_log", message)
frappe.db.set_value(self.doctype, self.name, "status", "Failed")
def delete_notifications(self):
self.validate_doc_status()
if not self.clear_notifications:
clear_notifications()
self.db_set("clear_notifications", 1)
self.enqueue_task(task="Initialize Summary Table")
def populate_doctypes_to_be_ignored_table(self):
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
for doctype in doctypes_to_be_ignored_list:
self.append("doctypes_to_be_ignored", {"doctype_name": doctype})
def delete_bins(self):
frappe.db.sql(
"""delete from `tabBin` where warehouse in
(select name from tabWarehouse where company=%s)""",
self.company,
)
def validate_running_task_for_doc(self, job_names: list = None):
# at most only one task should be runnning
running_tasks = []
for x in job_names:
if is_job_enqueued(x):
running_tasks.append(create_job_id(x))
def delete_lead_addresses(self):
"""Delete addresses to which leads are linked"""
leads = frappe.get_all("Lead", filters={"company": self.company})
leads = ["'%s'" % row.get("name") for row in leads]
addresses = []
if leads:
addresses = frappe.db.sql_list(
"""select parent from `tabDynamic Link` where link_name
in ({leads})""".format(
leads=",".join(leads)
if running_tasks:
frappe.throw(
_("{0} is already running for {1}").format(
comma_and([get_link_to_form("RQ Job", x) for x in running_tasks]), self.name
)
)
if addresses:
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
frappe.db.sql(
"""delete from `tabAddress` where name in ({addresses}) and
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
and dl1.link_doctype<>dl2.link_doctype)""".format(
addresses=",".join(addresses)
)
def validate_doc_status(self):
if self.status != "Running":
frappe.throw(
_("{0} is not running. Cannot trigger events for this Document").format(
get_link_to_form("Transaction Deletion Record", self.name)
)
)
frappe.db.sql(
"""delete from `tabDynamic Link` where link_doctype='Lead'
and parenttype='Address' and link_name in ({leads})""".format(
@frappe.whitelist()
def start_deletion_tasks(self):
# This method is the entry point for the chain of events that follow
self.db_set("status", "Running")
self.enqueue_task(task="Delete Bins")
def delete_bins(self):
self.validate_doc_status()
if not self.delete_bin_data:
frappe.db.sql(
"""delete from `tabBin` where warehouse in
(select name from tabWarehouse where company=%s)""",
self.company,
)
self.db_set("delete_bin_data", 1)
self.enqueue_task(task="Delete Leads and Addresses")
def delete_lead_addresses(self):
"""Delete addresses to which leads are linked"""
self.validate_doc_status()
if not self.delete_leads_and_addresses:
leads = frappe.get_all("Lead", filters={"company": self.company})
leads = ["'%s'" % row.get("name") for row in leads]
addresses = []
if leads:
addresses = frappe.db.sql_list(
"""select parent from `tabDynamic Link` where link_name
in ({leads})""".format(
leads=",".join(leads)
)
)
frappe.db.sql(
"""update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format(
leads=",".join(leads)
if addresses:
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
frappe.db.sql(
"""delete from `tabAddress` where name in ({addresses}) and
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
and dl1.link_doctype<>dl2.link_doctype)""".format(
addresses=",".join(addresses)
)
)
frappe.db.sql(
"""delete from `tabDynamic Link` where link_doctype='Lead'
and parenttype='Address' and link_name in ({leads})""".format(
leads=",".join(leads)
)
)
frappe.db.sql(
"""update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format(
leads=",".join(leads)
)
)
)
self.db_set("delete_leads_and_addresses", 1)
self.enqueue_task(task="Reset Company Values")
def reset_company_values(self):
company_obj = frappe.get_doc("Company", self.company)
company_obj.total_monthly_sales = 0
company_obj.sales_monthly_history = None
company_obj.save()
self.validate_doc_status()
if not self.reset_company_default_values:
company_obj = frappe.get_doc("Company", self.company)
company_obj.total_monthly_sales = 0
company_obj.sales_monthly_history = None
company_obj.save()
self.db_set("reset_company_default_values", 1)
self.enqueue_task(task="Clear Notifications")
def initialize_doctypes_to_be_deleted_table(self):
self.validate_doc_status()
if not self.initialize_doctypes_table:
doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
tables = self.get_all_child_doctypes()
for docfield in docfields:
if docfield["parent"] != self.doctype:
no_of_docs = self.get_number_of_docs_linked_with_specified_company(
docfield["parent"], docfield["fieldname"]
)
if no_of_docs > 0:
# Initialize
self.populate_doctypes_table(tables, docfield["parent"], docfield["fieldname"], 0)
self.db_set("initialize_doctypes_table", 1)
self.enqueue_task(task="Delete Transactions")
def delete_company_transactions(self):
doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
self.validate_doc_status()
if not self.delete_transactions:
doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
tables = self.get_all_child_doctypes()
for docfield in docfields:
if docfield["parent"] != self.doctype:
no_of_docs = self.get_number_of_docs_linked_with_specified_company(
docfield["parent"], docfield["fieldname"]
)
if no_of_docs > 0:
self.delete_version_log(docfield["parent"], docfield["fieldname"])
reference_docs = frappe.get_all(
docfield["parent"], filters={docfield["fieldname"]: self.company}
tables = self.get_all_child_doctypes()
for docfield in self.doctypes:
if docfield.doctype_name != self.doctype and not docfield.done:
no_of_docs = self.get_number_of_docs_linked_with_specified_company(
docfield.doctype_name, docfield.docfield_name
)
reference_doc_names = [r.name for r in reference_docs]
if no_of_docs > 0:
reference_docs = frappe.get_all(
docfield.doctype_name, filters={docfield.docfield_name: self.company}, limit=self.batch_size
)
reference_doc_names = [r.name for r in reference_docs]
self.delete_communications(docfield["parent"], reference_doc_names)
self.delete_comments(docfield["parent"], reference_doc_names)
self.unlink_attachments(docfield["parent"], reference_doc_names)
self.delete_version_log(docfield.doctype_name, reference_doc_names)
self.delete_communications(docfield.doctype_name, reference_doc_names)
self.delete_comments(docfield.doctype_name, reference_doc_names)
self.unlink_attachments(docfield.doctype_name, reference_doc_names)
self.delete_child_tables(docfield.doctype_name, reference_doc_names)
self.delete_docs_linked_with_specified_company(docfield.doctype_name, reference_doc_names)
processed = int(docfield.no_of_docs) + len(reference_doc_names)
frappe.db.set_value(docfield.doctype, docfield.name, "no_of_docs", processed)
else:
# reset naming series
naming_series = frappe.db.get_value("DocType", docfield.doctype_name, "autoname")
if naming_series:
if "#" in naming_series:
self.update_naming_series(naming_series, docfield.doctype_name)
frappe.db.set_value(docfield.doctype, docfield.name, "done", 1)
self.populate_doctypes_table(tables, docfield["parent"], no_of_docs)
self.delete_child_tables(docfield["parent"], docfield["fieldname"])
self.delete_docs_linked_with_specified_company(docfield["parent"], docfield["fieldname"])
naming_series = frappe.db.get_value("DocType", docfield["parent"], "autoname")
if naming_series:
if "#" in naming_series:
self.update_naming_series(naming_series, docfield["parent"])
pending_doctypes = frappe.db.get_all(
"Transaction Deletion Record Details",
filters={"parent": self.name, "done": 0},
pluck="doctype_name",
)
if pending_doctypes:
# as method is enqueued after commit, calling itself will not make validate_doc_status to throw
# recursively call this task to delete all transactions
self.enqueue_task(task="Delete Transactions")
else:
self.db_set("status", "Completed")
self.db_set("delete_transactions", 1)
self.db_set("error_log", None)
def get_doctypes_to_be_ignored_list(self):
singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name")
@@ -155,25 +330,24 @@ class TransactionDeletionRecord(Document):
def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname):
return frappe.db.count(doctype, {company_fieldname: self.company})
def populate_doctypes_table(self, tables, doctype, no_of_docs):
def populate_doctypes_table(self, tables, doctype, fieldname, no_of_docs):
self.flags.ignore_validate_update_after_submit = True
if doctype not in tables:
self.append("doctypes", {"doctype_name": doctype, "no_of_docs": no_of_docs})
def delete_child_tables(self, doctype, company_fieldname):
parent_docs_to_be_deleted = frappe.get_all(
doctype, {company_fieldname: self.company}, pluck="name"
)
self.append(
"doctypes", {"doctype_name": doctype, "docfield_name": fieldname, "no_of_docs": no_of_docs}
)
self.save(ignore_permissions=True)
def delete_child_tables(self, doctype, reference_doc_names):
child_tables = frappe.get_all(
"DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options"
)
for batch in create_batch(parent_docs_to_be_deleted, self.batch_size):
for table in child_tables:
frappe.db.delete(table, {"parent": ["in", batch]})
for table in child_tables:
frappe.db.delete(table, {"parent": ["in", reference_doc_names]})
def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
frappe.db.delete(doctype, {company_fieldname: self.company})
def delete_docs_linked_with_specified_company(self, doctype, reference_doc_names):
frappe.db.delete(doctype, {"name": ("in", reference_doc_names)})
def update_naming_series(self, naming_series, doctype_name):
if "." in naming_series:
@@ -194,17 +368,11 @@ class TransactionDeletionRecord(Document):
frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix))
def delete_version_log(self, doctype, company_fieldname):
dt = qb.DocType(doctype)
names = qb.from_(dt).select(dt.name).where(dt[company_fieldname] == self.company).run(as_list=1)
names = [x[0] for x in names]
if names:
versions = qb.DocType("Version")
for batch in create_batch(names, self.batch_size):
qb.from_(versions).delete().where(
(versions.ref_doctype == doctype) & (versions.docname.isin(batch))
).run()
def delete_version_log(self, doctype, docnames):
versions = qb.DocType("Version")
qb.from_(versions).delete().where(
(versions.ref_doctype == doctype) & (versions.docname.isin(docnames))
).run()
def delete_communications(self, doctype, reference_doc_names):
communications = frappe.get_all(
@@ -220,17 +388,11 @@ class TransactionDeletionRecord(Document):
frappe.delete_doc("Communication", batch, ignore_permissions=True)
def delete_comments(self, doctype, reference_doc_names):
comments = frappe.get_all(
"Comment",
filters={"reference_doctype": doctype, "reference_name": ["in", reference_doc_names]},
)
comment_names = [c.name for c in comments]
if not comment_names:
return
for batch in create_batch(comment_names, self.batch_size):
frappe.delete_doc("Comment", batch, ignore_permissions=True)
if reference_doc_names:
comment = qb.DocType("Comment")
qb.from_(comment).delete().where(
(comment.reference_doctype == doctype) & (comment.reference_name.isin(reference_doc_names))
).run()
def unlink_attachments(self, doctype, reference_doc_names):
files = frappe.get_all(
@@ -276,3 +438,34 @@ def get_doctypes_to_be_ignored():
doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or [])
return doctypes_to_be_ignored
@frappe.whitelist()
def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None):
if company:
if running_deletion_jobs := frappe.db.get_all(
"Transaction Deletion Record",
filters={"docstatus": 1, "company": company, "status": "Running"},
):
if not err_msg:
err_msg = ""
frappe.throw(
title=_("Deletion in Progress!"),
msg=_("Transaction Deletion Document: {0} is running for this Company. {1}").format(
get_link_to_form("Transaction Deletion Record", running_deletion_jobs[0].name), err_msg
),
)
def check_for_running_deletion_job(doc, method=None):
# Check if DocType has 'company' field
df = qb.DocType("DocField")
if (
not_allowed := qb.from_(df)
.select(df.parent)
.where((df.fieldname == "company") & (df.parent == doc.doctype))
.run()
):
is_deletion_doc_running(
doc.company, _("Cannot make any transactions until the deletion job is completed")
)

View File

@@ -2,11 +2,15 @@
// License: GNU General Public License v3. See license.txt
frappe.listview_settings["Transaction Deletion Record"] = {
add_fields: ["status"],
get_indicator: function (doc) {
if (doc.docstatus == 0) {
return [__("Draft"), "red"];
} else {
return [__("Completed"), "green"];
}
let colors = {
Queued: "orange",
Completed: "green",
Running: "blue",
Failed: "red",
};
let status = doc.status;
return [__(status), colors[status], "status,=," + status];
},
};

View File

@@ -5,8 +5,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"doctype_name",
"no_of_docs"
"doctype_name"
],
"fields": [
{
@@ -16,18 +15,12 @@
"label": "DocType",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "no_of_docs",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Number of Docs"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-08 23:10:46.166744",
"modified": "2024-02-04 10:56:27.413691",
"modified_by": "Administrator",
"module": "Setup",
"name": "Transaction Deletion Record Item",
@@ -35,5 +28,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -44,7 +44,7 @@ class ClosingStockBalance(Document):
& (
(table.from_date.between(self.from_date, self.to_date))
| (table.to_date.between(self.from_date, self.to_date))
| (table.from_date >= self.from_date and table.to_date >= self.to_date)
| ((table.from_date >= self.from_date) & (table.to_date >= self.to_date))
)
)
)

View File

@@ -10,7 +10,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.utils import cint, flt
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes
from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
@@ -623,7 +623,7 @@ def get_returned_qty_map(delivery_note):
@frappe.whitelist()
def make_sales_invoice(source_name, target_doc=None):
def make_sales_invoice(source_name, target_doc=None, args=None):
doc = frappe.get_doc("Delivery Note", source_name)
to_make_invoice_qty_map = {}
@@ -637,6 +637,9 @@ def make_sales_invoice(source_name, target_doc=None):
if len(target.get("items")) == 0:
frappe.throw(_("All these items have already been Invoiced/Returned"))
if args and args.get("merge_taxes"):
merge_taxes(source.get("taxes") or [], target)
target.run_method("calculate_taxes_and_totals")
# set company address
@@ -701,7 +704,11 @@ def make_sales_invoice(source_name, target_doc=None):
if not doc.get("is_return")
else get_pending_qty(d) > 0,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges",
"add_if_empty": True,
"ignore": args.get("merge_taxes") if args else 0,
},
"Sales Team": {
"doctype": "Sales Team",
"field_map": {"incentives": "incentives"},
@@ -965,6 +972,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")):
target.append("taxes", tax)
if not target.get("items"):
frappe.throw(_("All items have already been received"))
def update_details(source_doc, target_doc, source_parent):
target_doc.inter_company_invoice_reference = source_doc.name
if target_doc.doctype == "Purchase Receipt":
@@ -1020,6 +1030,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
shipping_address_name=target_doc.shipping_address_name,
)
def update_item(source, target, source_parent):
if source_parent.doctype == "Delivery Note" and source.received_qty:
target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty)
doclist = get_mapped_doc(
doctype,
source_name,
@@ -1043,6 +1057,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"Material_request_item": "material_request_item",
},
"field_no_map": ["warehouse"],
"condition": lambda item: item.received_qty < item.qty + item.returned_qty,
"postprocess": update_item,
},
},
target_doc,

View File

@@ -809,7 +809,8 @@
"label": "Purchase Order",
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "column_break_82",
@@ -870,7 +871,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:37:38.638144",
"modified": "2024-03-21 18:15:07.603672",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

View File

@@ -14,6 +14,7 @@ import erpnext
from erpnext.accounts.utils import get_account_currency
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import merge_taxes
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
@@ -974,37 +975,6 @@ def get_item_wise_returned_qty(pr_doc):
)
def merge_taxes(source_taxes, target_doc):
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
update_item_wise_tax_detail,
)
existing_taxes = target_doc.get("taxes") or []
idx = 1
for tax in source_taxes:
found = False
for t in existing_taxes:
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
update_item_wise_tax_detail(t, tax)
found = True
if not found:
tax.charge_type = "Actual"
tax.idx = idx
idx += 1
tax.included_in_print_rate = 0
tax.dont_recompute_tax = 1
tax.row_id = ""
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
tax.item_wise_tax_detail = tax.item_wise_tax_detail
existing_taxes.append(tax)
target_doc.set("taxes", existing_taxes)
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None, args=None):
from erpnext.accounts.party import get_payment_terms_template

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, today
from frappe.utils import add_days, cint, cstr, flt, nowtime, today
from pypika import functions as fn
import erpnext
@@ -2224,6 +2224,95 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.reload()
self.assertEqual(pr.per_billed, 100)
def test_sle_qty_after_transaction(self):
item = make_item(
"_Test Item Qty After Transaction",
properties={"is_stock_item": 1, "valuation_method": "FIFO"},
).name
posting_date = today()
posting_time = nowtime()
# Step 1: Create Purchase Receipt
pr = make_purchase_receipt(
item_code=item,
qty=1,
rate=100,
posting_date=posting_date,
posting_time=posting_time,
do_not_save=1,
)
for i in range(9):
pr.append(
"items",
{
"item_code": item,
"qty": 1,
"rate": 100,
"warehouse": pr.items[0].warehouse,
"cost_center": pr.items[0].cost_center,
"expense_account": pr.items[0].expense_account,
"uom": pr.items[0].uom,
"stock_uom": pr.items[0].stock_uom,
"conversion_factor": pr.items[0].conversion_factor,
},
)
self.assertEqual(len(pr.items), 10)
pr.save()
pr.submit()
data = frappe.get_all(
"Stock Ledger Entry",
fields=["qty_after_transaction", "creation", "posting_datetime"],
filters={"voucher_no": pr.name, "is_cancelled": 0},
order_by="creation",
)
for index, d in enumerate(data):
self.assertEqual(d.qty_after_transaction, 1 + index)
# Step 2: Create Purchase Receipt
pr = make_purchase_receipt(
item_code=item,
qty=1,
rate=100,
posting_date=posting_date,
posting_time=posting_time,
do_not_save=1,
)
for i in range(9):
pr.append(
"items",
{
"item_code": item,
"qty": 1,
"rate": 100,
"warehouse": pr.items[0].warehouse,
"cost_center": pr.items[0].cost_center,
"expense_account": pr.items[0].expense_account,
"uom": pr.items[0].uom,
"stock_uom": pr.items[0].stock_uom,
"conversion_factor": pr.items[0].conversion_factor,
},
)
self.assertEqual(len(pr.items), 10)
pr.save()
pr.submit()
data = frappe.get_all(
"Stock Ledger Entry",
fields=["qty_after_transaction", "creation", "posting_datetime"],
filters={"voucher_no": pr.name, "is_cancelled": 0},
order_by="creation",
)
for index, d in enumerate(data):
self.assertEqual(d.qty_after_transaction, 11 + index)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -9,17 +9,7 @@ import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
comma_or,
cstr,
flt,
format_time,
formatdate,
getdate,
month_diff,
nowdate,
)
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
@@ -168,41 +158,6 @@ class StockEntry(StockController):
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
def submit(self):
if self.is_enqueue_action():
frappe.msgprint(
_(
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage"
)
)
self.queue_action("submit", timeout=2000)
else:
self._submit()
def cancel(self):
if self.is_enqueue_action():
frappe.msgprint(
_(
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage"
)
)
self.queue_action("cancel", timeout=2000)
else:
self._cancel()
def is_enqueue_action(self, force=False) -> bool:
if force:
return True
if frappe.flags.in_test:
return False
# If line items are more than 100 or record is older than 6 months
if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6:
return True
return False
def on_submit(self):
self.update_stock_ledger()

View File

@@ -1671,24 +1671,22 @@ class TestStockEntry(FrappeTestCase):
item_code = "Test Negative Item - 001"
item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)
make_stock_entry(
se1 = make_stock_entry(
item_code=item_code,
posting_date=add_days(today(), -3),
posting_time="00:00:00",
purpose="Material Receipt",
target="_Test Warehouse - _TC",
qty=10,
to_warehouse="_Test Warehouse - _TC",
do_not_save=True,
)
make_stock_entry(
se2 = make_stock_entry(
item_code=item_code,
posting_date=today(),
posting_time="00:00:00",
purpose="Material Receipt",
source="_Test Warehouse - _TC",
qty=8,
from_warehouse="_Test Warehouse - _TC",
do_not_save=True,
)
sr_doc = create_stock_reconciliation(
@@ -1704,36 +1702,6 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(frappe.ValidationError, sr_doc.submit)
def test_enqueue_action(self):
frappe.flags.in_test = False
item_code = "Test Enqueue Item - 001"
create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)
doc = make_stock_entry(
item_code=item_code,
posting_date=add_to_date(today(), months=-7),
posting_time="00:00:00",
purpose="Material Receipt",
qty=10,
to_warehouse="_Test Warehouse - _TC",
do_not_submit=True,
)
self.assertTrue(doc.is_enqueue_action())
doc = make_stock_entry(
item_code=item_code,
posting_date=today(),
posting_time="00:00:00",
purpose="Material Receipt",
qty=10,
to_warehouse="_Test Warehouse - _TC",
do_not_submit=True,
)
self.assertFalse(doc.is_enqueue_action())
frappe.flags.in_test = True
def test_auto_reorder_level(self):
from erpnext.stock.reorder_item import reorder_item

View File

@@ -11,6 +11,7 @@
"warehouse",
"posting_date",
"posting_time",
"posting_datetime",
"is_adjustment_entry",
"column_break_6",
"voucher_type",
@@ -96,7 +97,6 @@
"oldfieldtype": "Date",
"print_width": "100px",
"read_only": 1,
"search_index": 1,
"width": "100px"
},
{
@@ -249,7 +249,6 @@
"options": "Company",
"print_width": "150px",
"read_only": 1,
"search_index": 1,
"width": "150px"
},
{
@@ -316,6 +315,11 @@
"fieldname": "is_adjustment_entry",
"fieldtype": "Check",
"label": "Is Adjustment Entry"
},
{
"fieldname": "posting_datetime",
"fieldtype": "Datetime",
"label": "Posting Datetime"
}
],
"hide_toolbar": 1,
@@ -324,7 +328,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-13 09:56:13.021696",
"modified": "2024-02-07 09:18:13.999231",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",

View File

@@ -52,6 +52,12 @@ class StockLedgerEntry(Document):
self.validate_with_last_transaction_posting_time()
self.validate_inventory_dimension_negative_stock()
def set_posting_datetime(self):
from erpnext.stock.utils import get_combine_datetime
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
self.db_set("posting_datetime", self.posting_datetime)
def validate_inventory_dimension_negative_stock(self):
if self.is_cancelled:
return
@@ -122,6 +128,7 @@ class StockLedgerEntry(Document):
return inv_dimension_dict
def on_submit(self):
self.set_posting_datetime()
self.check_stock_frozen_date()
self.calculate_batch_qty()
@@ -293,9 +300,7 @@ class StockLedgerEntry(Document):
def on_doctype_update():
frappe.db.add_index(
"Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index"
)
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse")
frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"])

View File

@@ -2,6 +2,7 @@
# See license.txt
import json
import time
from uuid import uuid4
import frappe
@@ -1066,7 +1067,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
frappe.qb.from_(sle)
.select("qty_after_transaction")
.where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.posting_datetime)
.orderby(sle.creation)
).run(pluck=True)
@@ -1143,6 +1144,89 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
except Exception as e:
self.fail("Double processing of qty for clashing timestamp.")
def test_previous_sle_with_clashed_timestamp(self):
item = make_item().name
warehouse = "_Test Warehouse - _TC"
reciept1 = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=100,
rate=10,
posting_date="2021-01-01",
posting_time="02:00:00",
)
time.sleep(3)
reciept2 = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=5,
posting_date="2021-01-01",
rate=10,
posting_time="02:00:00.1234",
)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": reciept1.name},
fields=["qty_after_transaction", "actual_qty"],
)
self.assertEqual(sle[0].qty_after_transaction, 100)
self.assertEqual(sle[0].actual_qty, 100)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": reciept2.name},
fields=["qty_after_transaction", "actual_qty"],
)
self.assertEqual(sle[0].qty_after_transaction, 105)
self.assertEqual(sle[0].actual_qty, 5)
def test_backdated_sle_with_same_timestamp(self):
item = make_item().name
warehouse = "_Test Warehouse - _TC"
reciept1 = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=5,
posting_date="2021-01-01",
rate=10,
posting_time="02:00:00.1234",
)
time.sleep(3)
# backdated entry with same timestamp but different ms part
reciept2 = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=100,
rate=10,
posting_date="2021-01-01",
posting_time="02:00:00",
)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": reciept1.name},
fields=["qty_after_transaction", "actual_qty"],
)
self.assertEqual(sle[0].qty_after_transaction, 5)
self.assertEqual(sle[0].actual_qty, 5)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": reciept2.name},
fields=["qty_after_transaction", "actual_qty"],
)
self.assertEqual(sle[0].qty_after_transaction, 105)
self.assertEqual(sle[0].actual_qty, 100)
@change_settings("System Settings", {"float_precision": 3, "currency_precision": 2})
def test_transfer_invariants(self):
"""Extact stock value should be transferred."""

View File

@@ -744,7 +744,9 @@ def get_items(
warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False
):
ignore_empty_stock = cint(ignore_empty_stock)
items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})]
items = []
if item_code and warehouse:
items = get_item_and_warehouses(item_code, warehouse)
if not item_code:
items = get_items_for_stock_reco(warehouse, company)
@@ -789,6 +791,20 @@ def get_items(
return res
def get_item_and_warehouses(item_code, warehouse):
from frappe.utils.nestedset import get_descendants_of
items = []
if frappe.get_cached_value("Warehouse", warehouse, "is_group"):
childrens = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft")
for ch_warehouse in childrens:
items.append(frappe._dict({"item_code": item_code, "warehouse": ch_warehouse}))
else:
items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})]
return items
def get_items_for_stock_reco(warehouse, company):
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
items = frappe.db.sql(
@@ -803,7 +819,7 @@ def get_items_for_stock_reco(warehouse, company):
and i.is_stock_item = 1
and i.has_variants = 0
and exists(
select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse
select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse and is_group = 0
)
""",
as_dict=1,
@@ -818,7 +834,7 @@ def get_items_for_stock_reco(warehouse, company):
where
i.name = id.parent
and exists(
select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse
select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse and is_group = 0
)
and i.is_stock_item = 1
and i.has_variants = 0
@@ -880,7 +896,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
frappe._dict(
{
"item_code": row[0],
"warehouse": warehouse,
"warehouse": row[3],
"qty": row[8],
"item_name": row[1],
"batch_no": row[4],

View File

@@ -49,6 +49,7 @@ frappe.ui.form.on("Warehouse", {
frm.add_custom_button(__("Stock Balance"), function () {
frappe.set_route("query-report", "Stock Balance", {
warehouse: frm.doc.name,
company: frm.doc.company,
});
});

View File

@@ -859,7 +859,9 @@ def get_price_list_rate(args, item_doc, out=None):
):
if args.price_list and args.rate:
insert_item_price(args)
return out
if not price_list_rate:
return out
out.price_list_rate = (
flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate)

View File

@@ -117,34 +117,37 @@ def get_stock_ledger_entries(filters):
if filters.get(field):
query = query.where(sle[field] == filters.get(field))
return query.run(as_dict=True)
return query
def get_item_warehouse_batch_map(filters, float_precision):
sle = get_stock_ledger_entries(filters)
iwb_map = {}
with frappe.db.unbuffered_cursor():
sle = get_stock_ledger_entries(filters)
sle = sle.run(as_dict=True, as_iterator=True)
from_date = getdate(filters["from_date"])
to_date = getdate(filters["to_date"])
iwb_map = {}
for d in sle:
iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault(
d.batch_no, frappe._dict({"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0})
)
qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no]
if d.posting_date < from_date:
qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) + flt(
d.actual_qty, float_precision
from_date = getdate(filters["from_date"])
to_date = getdate(filters["to_date"])
for d in sle:
iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault(
d.batch_no, frappe._dict({"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0})
)
elif d.posting_date >= from_date and d.posting_date <= to_date:
if flt(d.actual_qty) > 0:
qty_dict.in_qty = flt(qty_dict.in_qty, float_precision) + flt(d.actual_qty, float_precision)
else:
qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) + abs(
flt(d.actual_qty, float_precision)
qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no]
if d.posting_date < from_date:
qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) + flt(
d.actual_qty, float_precision
)
elif d.posting_date >= from_date and d.posting_date <= to_date:
if flt(d.actual_qty) > 0:
qty_dict.in_qty = flt(qty_dict.in_qty, float_precision) + flt(d.actual_qty, float_precision)
else:
qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) + abs(
flt(d.actual_qty, float_precision)
)
qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision)
qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision)
return iwb_map

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.query_builder import Field
from frappe.query_builder.functions import CombineDatetime, Min
from frappe.query_builder.functions import Min
from frappe.utils import add_days, getdate, today
import erpnext
@@ -75,7 +75,7 @@ def get_data(report_filters):
& (sle.company == report_filters.company)
& (sle.is_cancelled == 0)
)
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), sle.creation)
.orderby(sle.posting_datetime, sle.creation)
).run(as_dict=True)
for d in data:

View File

@@ -9,9 +9,6 @@ frappe.query_reports["Item Prices"] = {
fieldtype: "Select",
options: "Enabled Items only\nDisabled Items only\nAll Items",
default: "Enabled Items only",
on_change: function (query_report) {
query_report.trigger_refresh();
},
},
],
};

View File

@@ -213,13 +213,11 @@ def get_stock_ledger_entries(filters, items):
query = (
frappe.qb.from_(sle)
.force_index("posting_sort_index")
.left_join(sle2)
.on(
(sle.item_code == sle2.item_code)
& (sle.warehouse == sle2.warehouse)
& (sle.posting_date < sle2.posting_date)
& (sle.posting_time < sle2.posting_time)
& (sle.posting_datetime < sle2.posting_datetime)
& (sle.name < sle2.name)
)
.select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company)

View File

@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, TypedDict
import frappe
from frappe import _
from frappe.query_builder import Order
from frappe.query_builder.functions import Coalesce, CombineDatetime
from frappe.query_builder.functions import Coalesce
from frappe.utils import add_days, cint, date_diff, flt, getdate
from frappe.utils.nestedset import get_descendants_of
@@ -283,7 +283,7 @@ class StockBalanceReport(object):
item_table.item_name,
)
.where((sle.docstatus < 2) & (sle.is_cancelled == 0))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.posting_datetime)
.orderby(sle.creation)
.orderby(sle.actual_qty)
)

View File

@@ -276,7 +276,7 @@ def get_stock_ledger_entries(filters, items):
frappe.qb.from_(sle)
.select(
sle.item_code,
CombineDatetime(sle.posting_date, sle.posting_time).as_("date"),
sle.posting_datetime.as_("date"),
sle.warehouse,
sle.posting_date,
sle.posting_time,

View File

@@ -7,13 +7,14 @@ from typing import Optional, Set, Tuple
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.utils import (
get_combine_datetime,
get_incoming_outgoing_rate_for_cancel,
get_incoming_rate,
get_or_make_bin,
@@ -69,6 +70,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
args = sle_doc.as_dict()
args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False
args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time)
if sle.get("voucher_type") == "Stock Reconciliation":
# preserve previous_qty_after_transaction for qty reposting
@@ -431,12 +433,14 @@ class update_entries_after(object):
self.process_sle(sle)
def get_sle_against_current_voucher(self):
self.args["time_format"] = "%H:%i:%s"
self.args["posting_datetime"] = get_combine_datetime(
self.args.posting_date, self.args.posting_time
)
return frappe.db.sql(
"""
select
*, timestamp(posting_date, posting_time) as "timestamp"
*, posting_datetime as "timestamp"
from
`tabStock Ledger Entry`
where
@@ -444,8 +448,7 @@ class update_entries_after(object):
and warehouse = %(warehouse)s
and is_cancelled = 0
and (
posting_date = %(posting_date)s and
time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
posting_datetime = %(posting_datetime)s
)
order by
creation ASC
@@ -1186,11 +1189,11 @@ class update_entries_after(object):
def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
"""get stock ledger entries filtered by specific posting datetime conditions"""
args["time_format"] = "%H:%i:%s"
if not args.get("posting_date"):
args["posting_date"] = "1900-01-01"
if not args.get("posting_time"):
args["posting_time"] = "00:00"
args["posting_datetime"] = "1900-01-01 00:00:00"
if not args.get("posting_datetime"):
args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
voucher_condition = ""
if exclude_current_voucher:
@@ -1199,23 +1202,20 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
sle = frappe.db.sql(
"""
select *, timestamp(posting_date, posting_time) as "timestamp"
select *, posting_datetime as "timestamp"
from `tabStock Ledger Entry`
where item_code = %(item_code)s
and warehouse = %(warehouse)s
and is_cancelled = 0
{voucher_condition}
and (
posting_date < %(posting_date)s or
(
posting_date = %(posting_date)s and
time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
)
posting_datetime {operator} %(posting_datetime)s
)
order by timestamp(posting_date, posting_time) desc, creation desc
order by posting_datetime desc, creation desc
limit 1
for update""".format(
operator=operator, voucher_condition=voucher_condition
operator=operator,
voucher_condition=voucher_condition,
),
args,
as_dict=1,
@@ -1256,9 +1256,7 @@ def get_stock_ledger_entries(
extra_cond=None,
):
"""get stock ledger entries filtered by specific posting datetime conditions"""
conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
operator
)
conditions = " and posting_datetime {0} %(posting_datetime)s".format(operator)
if previous_sle.get("warehouse"):
conditions += " and warehouse = %(warehouse)s"
elif previous_sle.get("warehouse_condition"):
@@ -1284,9 +1282,11 @@ def get_stock_ledger_entries(
)
if not previous_sle.get("posting_date"):
previous_sle["posting_date"] = "1900-01-01"
if not previous_sle.get("posting_time"):
previous_sle["posting_time"] = "00:00"
previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
else:
previous_sle["posting_datetime"] = get_combine_datetime(
previous_sle["posting_date"], previous_sle["posting_time"]
)
if operator in (">", "<=") and previous_sle.get("name"):
conditions += " and name!=%(name)s"
@@ -1299,12 +1299,12 @@ def get_stock_ledger_entries(
return frappe.db.sql(
"""
select *, timestamp(posting_date, posting_time) as "timestamp"
select *, posting_datetime as "timestamp"
from `tabStock Ledger Entry`
where item_code = %%(item_code)s
and is_cancelled = 0
%(conditions)s
order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
order by posting_datetime %(order)s, creation %(order)s
%(limit)s %(for_update)s"""
% {
"conditions": conditions,
@@ -1330,7 +1330,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
"posting_date",
"posting_time",
"voucher_detail_no",
"timestamp(posting_date, posting_time) as timestamp",
"posting_datetime as timestamp",
],
as_dict=1,
)
@@ -1340,15 +1340,18 @@ def get_batch_incoming_rate(
item_code, warehouse, batch_no, posting_date, posting_time, creation=None
):
import datetime
sle = frappe.qb.DocType("Stock Ledger Entry")
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
posting_date, posting_time
)
posting_datetime = get_combine_datetime(posting_date, posting_time)
if not creation:
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
timestamp_condition = sle.posting_datetime < posting_datetime
if creation:
timestamp_condition |= (
CombineDatetime(sle.posting_date, sle.posting_time)
== CombineDatetime(posting_date, posting_time)
sle.posting_datetime == get_combine_datetime(posting_date, posting_time)
) & (sle.creation < creation)
batch_details = (
@@ -1411,7 +1414,7 @@ def get_valuation_rate(
AND valuation_rate >= 0
AND is_cancelled = 0
AND NOT (voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""",
order by posting_datetime desc, name desc limit 1""",
(item_code, warehouse, voucher_no, voucher_type),
)
@@ -1472,7 +1475,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
datetime_limit_condition = ""
qty_shift = args.actual_qty
args["time_format"] = "%H:%i:%s"
args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
# find difference/shift in qty caused by stock reconciliation
if args.voucher_type == "Stock Reconciliation":
@@ -1482,8 +1485,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
next_stock_reco_detail = get_next_stock_reco(args)
if next_stock_reco_detail:
detail = next_stock_reco_detail[0]
# add condition to update SLEs before this date & time
datetime_limit_condition = get_datetime_limit_condition(detail)
frappe.db.sql(
@@ -1496,13 +1497,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
and voucher_no != %(voucher_no)s
and is_cancelled = 0
and (
posting_date > %(posting_date)s or
(
posting_date = %(posting_date)s and
time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
)
posting_datetime > %(posting_datetime)s
)
{datetime_limit_condition}
{datetime_limit_condition}
""",
args,
)
@@ -1557,20 +1554,11 @@ def get_next_stock_reco(kwargs):
& (sle.voucher_no != kwargs.get("voucher_no"))
& (sle.is_cancelled == 0)
& (
(
CombineDatetime(sle.posting_date, sle.posting_time)
> CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
)
| (
(
CombineDatetime(sle.posting_date, sle.posting_time)
== CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
)
& (sle.creation > kwargs.get("creation"))
)
sle.posting_datetime
>= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
)
)
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.posting_datetime)
.orderby(sle.creation)
.limit(1)
)
@@ -1582,11 +1570,13 @@ def get_next_stock_reco(kwargs):
def get_datetime_limit_condition(detail):
posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time)
return f"""
and
(timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
(posting_datetime < '{posting_datetime}'
or (
timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
posting_datetime = '{posting_datetime}'
and creation < '{detail.creation}'
)
)"""
@@ -1659,14 +1649,11 @@ def get_future_sle_with_negative_qty(sle):
(SLE.item_code == sle.item_code)
& (SLE.warehouse == sle.warehouse)
& (SLE.voucher_no != sle.voucher_no)
& (
CombineDatetime(SLE.posting_date, SLE.posting_time)
>= CombineDatetime(sle.posting_date, sle.posting_time)
)
& (SLE.posting_datetime >= get_combine_datetime(sle.posting_date, sle.posting_time))
& (SLE.is_cancelled == 0)
& (SLE.qty_after_transaction < 0)
)
.orderby(CombineDatetime(SLE.posting_date, SLE.posting_time))
.orderby(SLE.posting_datetime)
.limit(1)
)
@@ -1681,20 +1668,20 @@ def get_future_sle_with_negative_batch_qty(args):
"""
with batch_ledger as (
select
posting_date, posting_time, voucher_type, voucher_no,
sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
from `tabStock Ledger Entry`
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and batch_no=%(batch_no)s
and is_cancelled = 0
order by posting_date, posting_time, creation
order by posting_datetime, creation
)
select * from batch_ledger
where
cumulative_total < 0.0
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and posting_datetime >= %(posting_datetime)s
limit 1
""",
args,
@@ -1746,6 +1733,7 @@ def is_internal_transfer(sle):
def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
table = frappe.qb.DocType("Stock Ledger Entry")
posting_datetime = get_combine_datetime(posting_date, posting_time)
query = (
frappe.qb.from_(table)
@@ -1754,10 +1742,7 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time,
(table.is_cancelled == 0)
& (table.item_code == item_code)
& (table.warehouse == warehouse)
& (
(table.posting_date < posting_date)
| ((table.posting_date == posting_date) & (table.posting_time <= posting_time))
)
& (table.posting_datetime <= posting_datetime)
)
)

View File

@@ -8,7 +8,7 @@ from typing import Dict, Optional
import frappe
from frappe import _
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime
import erpnext
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
@@ -619,3 +619,18 @@ def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Option
):
scan_result.update(item_info)
return scan_result
def get_combine_datetime(posting_date, posting_time):
import datetime
if isinstance(posting_date, str):
posting_date = getdate(posting_date)
if isinstance(posting_time, str):
posting_time = get_time(posting_time)
if isinstance(posting_time, datetime.timedelta):
posting_time = (datetime.datetime.min + posting_time).time()
return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0)

View File

@@ -307,8 +307,6 @@ class SubcontractingReceipt(SubcontractingController):
return process_gl_map(gl_entries)
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
warehouse_with_no_account = []
for item in self.items:
@@ -326,29 +324,39 @@ class SubcontractingReceipt(SubcontractingController):
"stock_value_difference",
)
warehouse_account_name = warehouse_account[item.warehouse]["account"]
warehouse_account_currency = warehouse_account[item.warehouse]["account_currency"]
accepted_warehouse_account = warehouse_account[item.warehouse]["account"]
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get(
"account_currency"
)
remarks = self.get("remarks") or _("Accounting Entry for Stock")
# FG Warehouse Account (Debit)
# Accepted Warehouse Account (Debit)
self.add_gl_entry(
gl_entries=gl_entries,
account=warehouse_account_name,
account=accepted_warehouse_account,
cost_center=item.cost_center,
debit=stock_value_diff,
credit=0.0,
remarks=remarks,
against_account=stock_rbnb,
account_currency=warehouse_account_currency,
against_account=item.expense_account,
account_currency=get_account_currency(accepted_warehouse_account),
project=item.project,
item=item,
)
# Expense Account (Credit)
self.add_gl_entry(
gl_entries=gl_entries,
account=item.expense_account,
cost_center=item.cost_center,
debit=0.0,
credit=stock_value_diff,
remarks=remarks,
against_account=accepted_warehouse_account,
account_currency=get_account_currency(item.expense_account),
project=item.project,
item=item,
)
# Supplier Warehouse Account (Credit)
if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
if flt(item.rm_supp_cost) and supplier_warehouse_account:
# Supplier Warehouse Account (Credit)
self.add_gl_entry(
gl_entries=gl_entries,
account=supplier_warehouse_account,
@@ -356,40 +364,64 @@ class SubcontractingReceipt(SubcontractingController):
debit=0.0,
credit=flt(item.rm_supp_cost),
remarks=remarks,
against_account=warehouse_account_name,
account_currency=supplier_warehouse_account_currency,
against_account=item.expense_account,
account_currency=get_account_currency(supplier_warehouse_account),
project=item.project,
item=item,
)
# Expense Account (Credit)
if flt(item.service_cost_per_qty):
# Expense Account (Debit)
self.add_gl_entry(
gl_entries=gl_entries,
account=item.expense_account,
cost_center=item.cost_center,
debit=0.0,
credit=flt(item.service_cost_per_qty) * flt(item.qty),
debit=flt(item.rm_supp_cost),
credit=0.0,
remarks=remarks,
against_account=warehouse_account_name,
against_account=supplier_warehouse_account,
account_currency=get_account_currency(item.expense_account),
project=item.project,
item=item,
)
# Loss Account (Credit)
divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount"))
# Expense Account (Debit)
if item.additional_cost_per_qty:
self.add_gl_entry(
gl_entries=gl_entries,
account=item.expense_account,
cost_center=self.cost_center or self.get_company_default("cost_center"),
debit=item.qty * item.additional_cost_per_qty,
credit=0.0,
remarks=remarks,
against_account=None,
account_currency=get_account_currency(item.expense_account),
)
if divisional_loss:
loss_account = item.expense_account
if divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")):
loss_account = self.get_company_default("stock_adjustment_account", ignore_validation=True)
# Loss Account (Credit)
self.add_gl_entry(
gl_entries=gl_entries,
account=loss_account,
cost_center=item.cost_center,
debit=0.0,
credit=divisional_loss,
remarks=remarks,
against_account=item.expense_account,
account_currency=get_account_currency(loss_account),
project=item.project,
item=item,
)
# Expense Account (Debit)
self.add_gl_entry(
gl_entries=gl_entries,
account=item.expense_account,
cost_center=item.cost_center,
debit=divisional_loss,
credit=0.0,
remarks=remarks,
against_account=warehouse_account_name,
account_currency=get_account_currency(loss_account),
against_account=loss_account,
account_currency=get_account_currency(item.expense_account),
project=item.project,
item=item,
)
@@ -399,7 +431,6 @@ class SubcontractingReceipt(SubcontractingController):
):
warehouse_with_no_account.append(item.warehouse)
# Additional Costs Expense Accounts (Credit)
for row in self.additional_costs:
credit_amount = (
flt(row.base_amount)
@@ -407,6 +438,7 @@ class SubcontractingReceipt(SubcontractingController):
else flt(row.amount)
)
# Additional Cost Expense Account (Credit)
self.add_gl_entry(
gl_entries=gl_entries,
account=row.expense_account,
@@ -415,6 +447,7 @@ class SubcontractingReceipt(SubcontractingController):
credit=credit_amount,
remarks=remarks,
against_account=None,
account_currency=get_account_currency(row.expense_account),
)
if warehouse_with_no_account:

View File

@@ -442,26 +442,15 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(scr.company)), 1)
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
self.assertTrue(gl_entries)
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
supplier_warehouse_ac = get_inventory_account(scr.company, scr.supplier_warehouse)
expense_account = scr.items[0].expense_account
if fg_warehouse_ac == supplier_warehouse_ac:
expected_values = {
fg_warehouse_ac: [2100.0, 1000.0], # FG Amount (D), RM Cost (C)
expense_account: [0.0, 1000.0], # Service Cost (C)
additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C)
}
else:
expected_values = {
fg_warehouse_ac: [2100.0, 0.0], # FG Amount (D)
supplier_warehouse_ac: [0.0, 1000.0], # RM Cost (C)
expense_account: [0.0, 1000.0], # Service Cost (C)
additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C)
}
expected_values = {
fg_warehouse_ac: [2100.0, 1000],
expense_account: [1100, 2100],
additional_costs_expense_account: [0.0, 100.0],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
@@ -471,6 +460,52 @@ class TestSubcontractingReceipt(FrappeTestCase):
scr.cancel()
self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name))
def test_subcontracting_receipt_with_zero_service_cost(self):
warehouse = "Stores - TCP1"
service_items = [
{
"warehouse": warehouse,
"item_code": "Subcontracted Service Item 7",
"qty": 10,
"rate": 0,
"fg_item": "Subcontracted Item SA7",
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(
company="_Test Company with perpetual inventory",
warehouse=warehouse,
supplier_warehouse="Work In Progress - TCP1",
service_items=service_items,
)
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.submit()
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
self.assertTrue(gl_entries)
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
expense_account = scr.items[0].expense_account
expected_values = {
fg_warehouse_ac: [1000, 1000],
expense_account: [1000, 1000],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
scr.reload()
scr.cancel()
def test_supplied_items_consumed_qty(self):
# Set Backflush Based On as "Material Transferred for Subcontracting" to transfer RM's more than the required qty
set_backflush_based_on("Material Transferred for Subcontract")

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