Compare commits

..

226 Commits

Author SHA1 Message Date
Frappe PR Bot
4573408866 chore(release): Bumped to Version 15.49.2
## [15.49.2](https://github.com/frappe/erpnext/compare/v15.49.1...v15.49.2) (2025-01-23)

### Bug Fixes

* batch qty calculation (backport [#45367](https://github.com/frappe/erpnext/issues/45367)) ([#45388](https://github.com/frappe/erpnext/issues/45388)) ([42f7ee9](42f7ee905c))
* JobCardTimeLog' object has no attribute 'remaining_time_in_mins' ([4542a75](4542a759ae))
2025-01-23 08:43:35 +00:00
rohitwaghchaure
94aefcc62d Merge pull request #45397 from frappe/mergify/bp/version-15/pr-45395
fix: JobCardTimeLog' object has no attribute 'remaining_time_in_mins' (backport #45394) (backport #45395)
2025-01-23 14:12:17 +05:30
rohitwaghchaure
9fc1c3550f Merge pull request #45396 from frappe/mergify/bp/version-15/pr-45388
fix: batch qty calculation (backport #45367) (backport #45388)
2025-01-23 14:11:50 +05:30
Rohit Waghchaure
4542a759ae fix: JobCardTimeLog' object has no attribute 'remaining_time_in_mins'
(cherry picked from commit 41dda35db7)
(cherry picked from commit ef15429d98)
2025-01-23 08:11:21 +00:00
mergify[bot]
42f7ee905c fix: batch qty calculation (backport #45367) (#45388)
fix: batch qty calculation

(cherry picked from commit f07a71a882)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
(cherry picked from commit 767529f0ec)
2025-01-23 08:11:20 +00:00
Frappe PR Bot
89f7834517 chore(release): Bumped to Version 15.49.1
## [15.49.1](https://github.com/frappe/erpnext/compare/v15.49.0...v15.49.1) (2025-01-22)

### Bug Fixes

* Do no query GLs if no PCVs are posted ([5d04501](5d04501454))
2025-01-22 09:18:16 +00:00
Deepesh Garg
da8a712249 Merge pull request #45373 from frappe/mergify/bp/version-15/pr-45371
fix: Do no query GLs if no PCVs are posted (#45371)
2025-01-22 14:46:59 +05:30
Deepesh Garg
5d04501454 fix: Do no query GLs if no PCVs are posted
(cherry picked from commit f4d1a54588)
2025-01-22 09:15:09 +00:00
Frappe PR Bot
b9fcc786fa chore(release): Bumped to Version 15.49.0
# [15.49.0](https://github.com/frappe/erpnext/compare/v15.48.4...v15.49.0) (2025-01-22)

### Bug Fixes

* Ambiguous column error while creating Sales Return ([#45275](https://github.com/frappe/erpnext/issues/45275)) ([842d72f](842d72f7c4))
* apply ruff formatter changes ([8e33d93](8e33d93273))
* calculate AED exchange rate based on pegged value with USD ([05e4255](05e42558d1))
* check if tds deducted based on Purchase Taxes and Charges ([#45161](https://github.com/frappe/erpnext/issues/45161)) ([ade8799](ade8799358))
* check if tds deducted based on Purchase Taxes and Charges (backport [#45161](https://github.com/frappe/erpnext/issues/45161)) ([#45277](https://github.com/frappe/erpnext/issues/45277)) ([3abca03](3abca03fc1))
* company in quality inspection ([c0cf1fe](c0cf1fed00))
* conflict ([bdaf376](bdaf3761c0))
* do not reset picked items ([fd83b52](fd83b52513))
* Does not allow to create Sub-Asseblies of Sub Assemblies ([d3c9092](d3c9092266))
* ensure multiple PCVs in same fiscal year are considered in patch ([fcd914c](fcd914cfa0))
* fetching items from blanket order to sales/purchase order ([#45262](https://github.com/frappe/erpnext/issues/45262)) ([7fc19e1](7fc19e19be))
* fixed typo in manufacturing settings and field rename ([#45238](https://github.com/frappe/erpnext/issues/45238)) ([7641627](7641627b71))
* getting scrap items from sub assemblies by fetching scrap items for parent BOM ([da7b691](da7b69109a))
* ignore crm deal in tax_rule search filter (backport [#45134](https://github.com/frappe/erpnext/issues/45134)) ([#45138](https://github.com/frappe/erpnext/issues/45138)) ([afab5be](afab5be63f))
* include pos invoice in modifing key for returned item validation ([70ed8b7](70ed8b78d2))
* linters ([61303db](61303db16d))
* precision on work order total qty ([#45341](https://github.com/frappe/erpnext/issues/45341)) ([5346820](53468202de))
* **Project:** re-phrase welcome email ([#45175](https://github.com/frappe/erpnext/issues/45175)) ([c94430a](c94430a472))
* round off tax withholding amount ([#45271](https://github.com/frappe/erpnext/issues/45271)) ([3c10d80](3c10d809b0))
* sales return for multi-uom ([#45303](https://github.com/frappe/erpnext/issues/45303)) ([e965b6e](e965b6ef45))
* set invoice start date to subscription start date (backport [#45342](https://github.com/frappe/erpnext/issues/45342)) ([#45343](https://github.com/frappe/erpnext/issues/45343)) ([7045c2c](7045c2cc3d))
* status of the serial no for the raw materials ([d956051](d956051e69))
* translation fixes ([e4f09ce](e4f09ce543))
* updated modified timestamp for stock entry type (backport [#45280](https://github.com/frappe/erpnext/issues/45280)) ([#45315](https://github.com/frappe/erpnext/issues/45315)) ([96a3a45](96a3a45078))
* use currency defined in plan for subscription invoice ([#45104](https://github.com/frappe/erpnext/issues/45104)) ([a27eac1](a27eac1ef6))
* use currency defined in plan for subscription invoice (backport [#45104](https://github.com/frappe/erpnext/issues/45104)) ([#45273](https://github.com/frappe/erpnext/issues/45273)) ([2c46be4](2c46be4cfb))
* validate linked sales person ([85910ec](85910ec2f9))

### Features

* add option to update modified on communication recieved (backport [#45307](https://github.com/frappe/erpnext/issues/45307)) ([#45310](https://github.com/frappe/erpnext/issues/45310)) ([bf8d6ae](bf8d6ae6bf))
* add Syscohada charts of accounts ([2667649](26676491e5))
* Added valuation of quantity for each age group in stock ageing … (backport [#45076](https://github.com/frappe/erpnext/issues/45076)) ([#45208](https://github.com/frappe/erpnext/issues/45208)) ([dd58e4c](dd58e4cb53))

### Performance Improvements

* Ignore is_opening column in GL Queries ([#45327](https://github.com/frappe/erpnext/issues/45327)) ([b7de26d](b7de26d123))

### Reverts

* avoid change to translatable string ([93e3847](93e3847e36))
2025-01-22 03:32:58 +00:00
ruthra kumar
15da04d7de Merge pull request #45357 from frappe/version-15-hotfix
chore: release v15
2025-01-22 09:01:34 +05:30
mergify[bot]
bf8d6ae6bf feat: add option to update modified on communication recieved (backport #45307) (#45310)
* feat: add option to update modified on communication recieved (#45307)

(cherry picked from commit aa38895caf)

# Conflicts:
#	erpnext/crm/doctype/crm_settings/crm_settings.json

* chore: resolve conflict

---------

Co-authored-by: Ejaaz Khan <67804911+iamejaaz@users.noreply.github.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2025-01-21 20:21:46 +05:30
mergify[bot]
96a3a45078 fix: updated modified timestamp for stock entry type (backport #45280) (#45315)
* fix: updated modified timestamp for stock entry type (#45280)

(cherry picked from commit 2d45d0e5d5)

# Conflicts:
#	erpnext/stock/doctype/stock_entry_type/stock_entry_type.json

* fix: resolved conflict

---------

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
2025-01-21 20:20:58 +05:30
Dany Robert
53468202de fix: precision on work order total qty (#45341)
* fix: precision on work order total qty

* chore: linters
2025-01-21 20:18:26 +05:30
ruthra kumar
10d99f4bc9 Merge pull request #45360 from frappe/mergify/bp/version-15-hotfix/pr-45242
fix: include pos invoice in modifing key for returned item validation (backport #45242)
2025-01-21 16:58:21 +05:30
venkat102
70ed8b78d2 fix: include pos invoice in modifing key for returned item validation
(cherry picked from commit 2936139c79)
2025-01-21 11:06:17 +00:00
ruthra kumar
abf93693e9 Merge pull request #45356 from frappe/mergify/bp/version-15-hotfix/pr-45215
fix: calculate AED exchange rate based on pegged value with USD (backport #45215)
2025-01-21 14:55:27 +05:30
ruthra kumar
61a506927f Merge pull request #45355 from frappe/mergify/bp/version-15-hotfix/pr-45327
perf: Ignore is_opening column in GL Queries (backport #45327)
2025-01-21 14:34:08 +05:30
ruthra kumar
87f1376d86 refactor: use dictionary for better expandability
(cherry picked from commit 2e535955b3)
2025-01-21 08:29:26 +00:00
ruthra kumar
05e5e43c06 refactor: fix type error
(cherry picked from commit dd923332cb)
2025-01-21 08:29:26 +00:00
Kavin
05e42558d1 fix: calculate AED exchange rate based on pegged value with USD
(cherry picked from commit 455ef6f084)
2025-01-21 08:29:26 +00:00
ruthra kumar
06c0547ca4 Merge pull request #45312 from frappe/mergify/bp/version-15-hotfix/pr-45175
fix(Project): re-phrase welcome email (backport #45175)
2025-01-21 13:56:51 +05:30
ruthra kumar
f97f7cead4 Merge pull request #45317 from frappe/mergify/bp/version-15-hotfix/pr-45262
fix: fetching items from blanket order to sales/purchase order (backport #45262)
2025-01-21 13:55:51 +05:30
ruthra kumar
c76ac04ddd Merge pull request #45314 from frappe/mergify/bp/version-15-hotfix/pr-45271
fix: round off tax withholding amount (backport #45271)
2025-01-21 13:55:03 +05:30
Deepesh Garg
b7de26d123 perf: Ignore is_opening column in GL Queries (#45327)
* perf: Ignore is_opening column in GL Queries

* chore: Remove unwanted changes

* chore: Remove unwanted changes

* chore: Remove unwanted changes

* chore: Remove unwanted changes

* chore: Remove unwanted changes

* chore: Remove unwanted changes

(cherry picked from commit 993f40fa43)
2025-01-21 08:20:18 +00:00
ruthra kumar
e63d3257a7 Merge pull request #45338 from frappe/mergify/bp/version-15-hotfix/pr-44453
feat: Create Syscohada charts of accounts (backport #44453)
2025-01-21 13:17:17 +05:30
mergify[bot]
7045c2cc3d fix: set invoice start date to subscription start date (backport #45342) (#45343)
fix: set invoice start date to subscription start date (#45342)

(cherry picked from commit 68fb1b28eb)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-01-20 17:34:20 +05:30
ruthra kumar
03219142e0 Merge pull request #45339 from frappe/mergify/bp/version-15-hotfix/pr-45334
fix: translation fixes (backport #45334)
2025-01-20 13:11:34 +05:30
ruthra kumar
085d80521c Merge pull request #45316 from frappe/mergify/bp/version-15-hotfix/pr-45275
fix: Ambiguous column error while creating Sales Return (backport #45275)
2025-01-20 12:13:39 +05:30
mahsem
61303db16d fix: linters
(cherry picked from commit 3697ba0772)
2025-01-20 06:42:02 +00:00
mahsem
e4f09ce543 fix: translation fixes
(cherry picked from commit 1d81a9f933)
2025-01-20 06:42:02 +00:00
Nelly Traore
26676491e5 feat: add Syscohada charts of accounts
(cherry picked from commit aa8254963c)
2025-01-20 06:39:12 +00:00
ruthra kumar
6120999632 Merge pull request #45337 from frappe/mergify/bp/version-15-hotfix/pr-45167
fix: validate linked sales person (backport #45167)
2025-01-20 12:08:26 +05:30
Sudharsanan11
85910ec2f9 fix: validate linked sales person
(cherry picked from commit e614f07795)
2025-01-20 06:20:29 +00:00
rohitwaghchaure
011519e3ea Merge pull request #45325 from frappe/mergify/bp/version-15-hotfix/pr-44783
fix: do not reset picked items (backport #44783)
2025-01-19 13:49:08 +05:30
Rohit Waghchaure
fd83b52513 fix: do not reset picked items
(cherry picked from commit 34a80bfcd3)
2025-01-18 08:54:05 +00:00
rohitwaghchaure
5b3a97d8a5 Merge pull request #45324 from frappe/mergify/bp/version-15-hotfix/pr-45298
fix: company in quality inspection (backport #45298)
2025-01-18 12:48:59 +05:30
Rohit Waghchaure
c0cf1fed00 fix: company in quality inspection
(cherry picked from commit 397cd79e1e)
2025-01-18 06:45:58 +00:00
rohitwaghchaure
9e353e218b Merge pull request #45321 from frappe/mergify/bp/version-15-hotfix/pr-45238
fix: fixed typo in manufacturing settings and field rename (backport #45238)
2025-01-17 19:50:55 +05:30
barredterra
93e3847e36 revert: avoid change to translatable string 2025-01-17 14:34:44 +01:00
rohitwaghchaure
2226cc556f Merge pull request #45309 from frappe/mergify/bp/version-15-hotfix/pr-45303
fix: sales return for multi-uom (backport #45303)
2025-01-17 18:41:04 +05:30
ruthra kumar
e84fdd9a7a Merge pull request #45319 from frappe/mergify/bp/version-15-hotfix/pr-44811
fix!: ensure multiple PCVs in same fiscal year are considered in patch (backport #44811)
2025-01-17 17:29:53 +05:30
Diptanil Saha
7641627b71 fix: fixed typo in manufacturing settings and field rename (#45238)
* chore: field rename and patch entry

* chore: patch file rename and description improvement

---------

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
(cherry picked from commit 97e3770872)
2025-01-17 11:56:17 +00:00
Sagar Vora
fcd914cfa0 fix: ensure multiple PCVs in same fiscal year are considered in patch
(cherry picked from commit 84e0b41c4f)
2025-01-17 11:32:06 +00:00
mergify[bot]
afab5be63f fix: ignore crm deal in tax_rule search filter (backport #45134) (#45138)
fix: ignore crm deal in tax_rule search filter (#45134)

(cherry picked from commit dc5bff9008)

Co-authored-by: Venkatesh <47534423+venkat102@users.noreply.github.com>
2025-01-17 17:01:28 +05:30
mergify[bot]
dd58e4cb53 feat: Added valuation of quantity for each age group in stock ageing … (backport #45076) (#45208)
* feat: Added valuation of quantity for each age group in stock ageing report

(cherry picked from commit 2f80c4dee5)

* fix: Test case for ageing report

(cherry picked from commit 839b79ffd0)

* fix: Fixed more test cases

(cherry picked from commit f996f71d16)

* fix: Fixed final test case

(cherry picked from commit 1f2d7da426)

* test: Valuation of ageing stock

(cherry picked from commit dbb572eec1)

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-01-17 16:56:58 +05:30
Diptanil Saha
7fc19e19be fix: fetching items from blanket order to sales/purchase order (#45262)
* fix: blanket order with zero item quantity

* fix: item quantity validation

* refactor: resolve linter issue

(cherry picked from commit 98d401bee4)
2025-01-17 11:24:06 +00:00
Nabin Hait
842d72f7c4 fix: Ambiguous column error while creating Sales Return (#45275)
(cherry picked from commit ec487c14d9)
2025-01-17 11:22:22 +00:00
Lakshit Jain
3c10d809b0 fix: round off tax withholding amount (#45271)
(cherry picked from commit ada272a29b)
2025-01-17 11:21:15 +00:00
Patrick Eißler
c94430a472 fix(Project): re-phrase welcome email (#45175)
(cherry picked from commit 8d66142865)
2025-01-17 11:19:08 +00:00
rohitwaghchaure
e965b6ef45 fix: sales return for multi-uom (#45303)
(cherry picked from commit 9163f60191)
2025-01-17 11:07:38 +00:00
rohitwaghchaure
c789171a15 Merge pull request #45293 from frappe/mergify/bp/version-15-hotfix/pr-45283
fix: Does not allow to create Sub-Asseblies of Sub Assemblies (backport #45283)
2025-01-16 16:58:27 +05:30
Rohit Waghchaure
d3c9092266 fix: Does not allow to create Sub-Asseblies of Sub Assemblies
(cherry picked from commit 318a945d66)
2025-01-16 09:28:49 +00:00
rohitwaghchaure
a11c15a6cb Merge pull request #42928 from Axentorllc/Fix-BOM-Scrap
fix: getting scrap items from sub assemblies by fetching scrap items for parent BOM
2025-01-16 14:55:54 +05:30
rohitwaghchaure
b6ff79f34c Merge pull request #45281 from frappe/mergify/bp/version-15-hotfix/pr-45278
fix: status of the serial no for the raw materials (backport #45278)
2025-01-16 11:54:50 +05:30
rohitwaghchaure
e2d711540f chore: fix conflicts 2025-01-15 18:17:04 +05:30
Rohit Waghchaure
d956051e69 fix: status of the serial no for the raw materials
(cherry picked from commit 9607b16dcf)

# Conflicts:
#	erpnext/stock/doctype/serial_no/serial_no.json
2025-01-15 12:45:05 +00:00
Smit Vora
2c46be4cfb fix: use currency defined in plan for subscription invoice (backport #45104) (#45273)
Co-authored-by: ljain112 <ljain112@gmail.com>
fix: use currency defined in plan for subscription invoice (#45104)
2025-01-15 18:09:01 +05:30
Smit Vora
3abca03fc1 fix: check if tds deducted based on Purchase Taxes and Charges (backport #45161) (#45277)
fix: check if tds deducted based on Purchase Taxes and Charges (#45161)
2025-01-15 18:08:18 +05:30
Frappe PR Bot
d1fee96f75 chore(release): Bumped to Version 15.48.4
## [15.48.4](https://github.com/frappe/erpnext/compare/v15.48.3...v15.48.4) (2025-01-15)

### Bug Fixes

* auto fetch batch and serial no for draft stock transactions ([2f2554e](2f2554e9e5))
* batch number search on pos ([#45209](https://github.com/frappe/erpnext/issues/45209)) ([fe5c458](fe5c458c45))
* change string to be able to translate ([#45090](https://github.com/frappe/erpnext/issues/45090)) ([7a3687c](7a3687ca8e))
* deduct tds on excess amount if checked ([07c3605](07c3605905))
* delivery_document_no column issue ([0df1808](0df18080c7))
* do not add ordered items from Quotation to new Sales Order ([f414fa4](f414fa4981))
* don't create invoice if invoice start date is in future ([3f6d774](3f6d7741d9))
* incorrect label in Item-wise sales register ([36d1fbd](36d1fbd6a3))
* incorrect valuation for sales return with different warhouse ([9ee5651](9ee5651848))
* incorrect valuation rate for PI based revaluation ([f7e3854](f7e3854641))
* linter issue ([af21bca](af21bca231))
* minor update for readability ([abfcfdf](abfcfdfe7e))
* not able to see create Quality Inspection button ([a79cae1](a79cae1fef))
* pass right existing address ([80e6112](80e6112549))
* precision loss causing process loss variance ([d84601b](d84601b2a3))
* Semgrep rules ([1d5a73a](1d5a73a325))
* set billing and shipping address on change of company ([42eb88f](42eb88f5f6))
* Skip WIP Warehouse transfer ([bb0695a](bb0695a883))
* test case ([f5667f5](f5667f56e4))
* test case ([7d66e4e](7d66e4efb0))
* tests ([8ba42cf](8ba42cfbf0))
* timeout error for work order ([122b966](122b966a7b))
* **Timesheet:** ignore permissions when updating Task and Project (backport [#45168](https://github.com/frappe/erpnext/issues/45168)) ([#45170](https://github.com/frappe/erpnext/issues/45170)) ([f7448c6](f7448c6f79))
* typo in manufacturing settings (backport [#45190](https://github.com/frappe/erpnext/issues/45190)) ([#45193](https://github.com/frappe/erpnext/issues/45193)) ([47c6e5a](47c6e5a931))
* update  discounting on mixed conditions ([38cb5a9](38cb5a98bf))
2025-01-15 12:28:46 +00:00
rohitwaghchaure
5e13dcfe55 Merge pull request #45263 from frappe/version-15-hotfix
chore: release v15
2025-01-15 17:57:26 +05:30
rohitwaghchaure
80b80e77d8 Merge pull request #45274 from frappe/mergify/bp/version-15-hotfix/pr-45270
fix: Skip WIP Warehouse transfer (backport #45270)
2025-01-15 17:37:33 +05:30
Rohit Waghchaure
f5667f56e4 fix: test case 2025-01-15 16:20:17 +05:30
ljain112
bdaf3761c0 fix: conflict 2025-01-15 15:39:48 +05:30
Lakshit Jain
ade8799358 fix: check if tds deducted based on Purchase Taxes and Charges (#45161)
(cherry picked from commit 37a5767be5)
2025-01-15 10:03:19 +00:00
Mihir Kandoi
4e1d4005d9 test: Added new test to check wip skip
(cherry picked from commit 6edb454eea)
2025-01-15 09:37:03 +00:00
Mihir Kandoi
8ba42cfbf0 fix: tests
(cherry picked from commit 09d26a835f)
2025-01-15 09:37:03 +00:00
Mihir Kandoi
bb0695a883 fix: Skip WIP Warehouse transfer
(cherry picked from commit bbb5f8056b)
2025-01-15 09:37:03 +00:00
Lakshit Jain
a27eac1ef6 fix: use currency defined in plan for subscription invoice (#45104)
(cherry picked from commit a4453fb77b)
2025-01-15 09:23:51 +00:00
rohitwaghchaure
88c10fa7b6 Merge pull request #45268 from frappe/mergify/bp/version-15-hotfix/pr-45259
fix: incorrect valuation for sales return with different warhouse (backport #45259)
2025-01-14 20:35:44 +05:30
rohitwaghchaure
1c6fe9da3b chore: fix conflicts 2025-01-14 18:36:27 +05:30
Rohit Waghchaure
9ee5651848 fix: incorrect valuation for sales return with different warhouse
(cherry picked from commit 3a2e816759)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
2025-01-14 13:04:36 +00:00
rohitwaghchaure
1be8051f7a Merge pull request #45258 from frappe/mergify/bp/version-15-hotfix/pr-45241
fix: auto fetch batch and serial no for draft stock transactions (backport #45241)
2025-01-14 18:33:08 +05:30
Rohit Waghchaure
af21bca231 fix: linter issue 2025-01-14 18:15:32 +05:30
ruthra kumar
d508ea2420 Merge pull request #45267 from frappe/mergify/bp/version-15-hotfix/pr-45211
refactor: allow users to configure interval for Semi-Auto payment reconciliation (backport #45211)
2025-01-14 17:35:56 +05:30
ruthra kumar
e1405a5c4f refactor: allow users to configure interval for Semi-Auto payment reconciliation (#45211)
* refactor: configurable interval for reconciliation trigger

* refactor: set default value for interval

* refactor: configurable queue size

* refactor: use patch to setup default cron job

* refactor: use 'after_migrate' to setup cron for reconciliation

User specified interval will be used

* chore: type casting

* refactor: use scheduler_event to persist cron

* chore: rename field

* chore: use configured queue size

* chore: remove unwanted field

(cherry picked from commit ce9c606f71)
2025-01-14 11:46:48 +00:00
ruthra kumar
f91eb3ef9f Merge pull request #45265 from frappe/mergify/bp/version-15-hotfix/pr-45182
refactor: configurable reconciliation dates for Advance Payments (backport #45182)
2025-01-14 16:57:56 +05:30
ruthra kumar
14d1f67ba9 chore: resolve conflicts 2025-01-14 15:24:24 +05:30
ruthra kumar
18946f8495 refactor: only update reconcile_effect_on advance in separate acc
(cherry picked from commit 9ee5fcc602)
2025-01-14 09:50:26 +00:00
ruthra kumar
8f431041f8 refactor: backwards compatibility
(cherry picked from commit 9fa1865cb7)
2025-01-14 09:50:25 +00:00
ruthra kumar
e7571c1a32 refactor: save reconcile effect on reference table
(cherry picked from commit a4271aa5d1)
2025-01-14 09:50:25 +00:00
ruthra kumar
a438520549 refactor: store reconciliation date in reference
Helps with reposting

(cherry picked from commit 7e7775aa44)
2025-01-14 09:50:25 +00:00
ruthra kumar
2cdfa91725 test: ensure reconciliation date config takes effect
(cherry picked from commit e0517852bc)
2025-01-14 09:50:24 +00:00
ruthra kumar
be8e1c0520 refactor: hide old checkbox
(cherry picked from commit c8e93e7a61)
2025-01-14 09:50:24 +00:00
ruthra kumar
74f00bb51b refactor: payment entry to handle posting date on configuation
(cherry picked from commit bb8d2c994c)
2025-01-14 09:50:24 +00:00
ruthra kumar
34b336ca80 refactor: test cases updated
(cherry picked from commit fb6c72a247)
2025-01-14 09:50:23 +00:00
ruthra kumar
55d699ee01 refactor: patch to migrate checkbox to select
(cherry picked from commit a8a8ac71b6)

# Conflicts:
#	erpnext/patches.txt
2025-01-14 09:50:23 +00:00
ruthra kumar
ad9e5d41b2 refactor: introduce select fields in company and payment entry
(cherry picked from commit 8b2c981fc3)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.py
2025-01-14 09:50:22 +00:00
ruthra kumar
8ab39f5160 Merge pull request #45261 from frappe/mergify/bp/version-15-hotfix/pr-45260
fix: incorrect label in Item-wise sales register (backport #45260)
2025-01-14 14:05:58 +05:30
ruthra kumar
36d1fbd6a3 fix: incorrect label in Item-wise sales register
(cherry picked from commit d9013e1054)
2025-01-14 08:26:06 +00:00
ruthra kumar
b4bcb7f9ac Merge pull request #45257 from frappe/mergify/bp/version-15-hotfix/pr-45090
fix: change string to be able to translate (backport #45090)
2025-01-14 13:35:13 +05:30
rohitwaghchaure
2676e0ea1f chore: fix conflicts 2025-01-14 12:49:17 +05:30
Rohit Waghchaure
2f2554e9e5 fix: auto fetch batch and serial no for draft stock transactions
(cherry picked from commit 88ab9be79c)

# Conflicts:
#	erpnext/stock/get_item_details.py
2025-01-14 07:06:32 +00:00
mahsem
7a3687ca8e fix: change string to be able to translate (#45090)
* fix: change_string_to_translate

* fix: debit note translation

* chore: update 'modified' field

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit a3165c5719)
2025-01-14 06:26:50 +00:00
ruthra kumar
f954b241a8 Merge pull request #45234 from frappe/mergify/bp/version-15-hotfix/pr-45209
fix: batch number search on pos (backport #45209)
2025-01-14 11:42:04 +05:30
ruthra kumar
13c3483669 Merge pull request #45256 from frappe/mergify/bp/version-15-hotfix/pr-45078
fix: don't create invoice if `current_invoice_start ` is in future (backport #45078)
2025-01-14 10:43:19 +05:30
ruthra kumar
74a256ad57 Merge pull request #45255 from frappe/mergify/bp/version-15-hotfix/pr-45074
fix: set billing and shipping address on change of company (backport #45074)
2025-01-14 10:22:42 +05:30
ruthra kumar
f897999d79 Merge pull request #45254 from frappe/mergify/bp/version-15-hotfix/pr-45001
fix: deduct tds on excess amount if checked (backport #45001)
2025-01-14 10:21:57 +05:30
Sanket322
abfcfdfe7e fix: minor update for readability
(cherry picked from commit 61d4593236)
2025-01-14 04:43:28 +00:00
Sanket322
3f6d7741d9 fix: don't create invoice if invoice start date is in future
(cherry picked from commit 058fdca981)
2025-01-14 04:43:28 +00:00
ruthra kumar
9757c92635 Merge pull request #45252 from frappe/mergify/bp/version-15-hotfix/pr-44798
fix: update  discounting on mixed conditions (backport #44798)
2025-01-14 10:09:52 +05:30
Sanket322
80e6112549 fix: pass right existing address
(cherry picked from commit ce99764772)
2025-01-14 04:35:20 +00:00
Sanket322
42eb88f5f6 fix: set billing and shipping address on change of company
(cherry picked from commit f46f1bead4)
2025-01-14 04:35:20 +00:00
ljain112
07c3605905 fix: deduct tds on excess amount if checked
(cherry picked from commit a203e3ffaf)
2025-01-14 04:33:18 +00:00
DHINESH00
1d5a73a325 fix: Semgrep rules
(cherry picked from commit 547c8004eb)
2025-01-14 04:29:07 +00:00
DHINESH00
38cb5a98bf fix: update discounting on mixed conditions
(cherry picked from commit d541259da9)
2025-01-14 04:29:06 +00:00
Frappe PR Bot
74650217c1 chore(release): Bumped to Version 15.48.3
## [15.48.3](https://github.com/frappe/erpnext/compare/v15.48.2...v15.48.3) (2025-01-13)

### Bug Fixes

* delivery_document_no column issue ([47a622d](47a622d0cb))
* incorrect valuation rate for PI based revaluation ([23b846e](23b846ef39))
* test case ([b75c736](b75c7364d6))
2025-01-13 08:20:40 +00:00
rohitwaghchaure
47ab3c5dbe Merge pull request #45235 from frappe/mergify/bp/version-15/pr-45218
fix: incorrect valuation rate for PI based revaluation (backport #45207) (backport #45218)
2025-01-13 13:49:25 +05:30
rohitwaghchaure
b6f5208e29 Merge pull request #45236 from frappe/mergify/bp/version-15/pr-45217
fix: delivery_document_no column issue (backport #45214) (backport #45217)
2025-01-13 13:49:11 +05:30
Rohit Waghchaure
b75c7364d6 fix: test case
(cherry picked from commit 7d66e4efb0)
2025-01-13 06:56:46 +00:00
rohitwaghchaure
85c2d32de6 chore: fix conflicts
(cherry picked from commit bb170c024f)
2025-01-13 06:56:46 +00:00
rohitwaghchaure
120bccdad3 chore: fix conflicts
(cherry picked from commit 9426a32184)
2025-01-13 06:56:45 +00:00
Rohit Waghchaure
47a622d0cb fix: delivery_document_no column issue
(cherry picked from commit 61efb2bb39)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
#	erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
(cherry picked from commit 0df18080c7)
2025-01-13 06:56:45 +00:00
Rohit Waghchaure
23b846ef39 fix: incorrect valuation rate for PI based revaluation
(cherry picked from commit 14ce2337df)
(cherry picked from commit f7e3854641)
2025-01-13 06:56:40 +00:00
rohitwaghchaure
136333377a Merge pull request #45217 from frappe/mergify/bp/version-15-hotfix/pr-45214
fix: delivery_document_no column issue (backport #45214)
2025-01-13 12:25:50 +05:30
rohitwaghchaure
4ee6a4eab6 Merge pull request #45218 from frappe/mergify/bp/version-15-hotfix/pr-45207
fix: incorrect valuation rate for PI based revaluation (backport #45207)
2025-01-13 12:25:44 +05:30
mergify[bot]
f7448c6f79 fix(Timesheet): ignore permissions when updating Task and Project (backport #45168) (#45170)
* fix(Timesheet): ignore permissions when updating Task and Project (#45168)

(cherry picked from commit 9e760e54a5)

# Conflicts:
#	erpnext/projects/doctype/timesheet/timesheet.py

* chore: resolve conflicts

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-01-13 12:08:17 +05:30
Diptanil Saha
fe5c458c45 fix: batch number search on pos (#45209)
* fix: price of items with batch number not having seperate item price on pos search bar

* fix: introduced batch number based sorting

(cherry picked from commit e529f82392)
2025-01-13 06:37:26 +00:00
mergify[bot]
47c6e5a931 fix: typo in manufacturing settings (backport #45190) (#45193)
fix: typo in manufacturing settings

(cherry picked from commit a9b761f862)

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2025-01-13 11:59:27 +05:30
mergify[bot]
0665bc4a28 chore: removal of decapitalization feature (backport #45162) (#45173)
* chore: removal of decapitalization feature (#45162)

* chore: removal of decapitalization feature

* fix: rearrangement of asset capitalization doctype fields

(cherry picked from commit 7ea73d8265)

# Conflicts:
#	erpnext/assets/doctype/asset_capitalization/asset_capitalization.json

* fix: resolved conflicts

---------

Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com>
2025-01-13 11:58:53 +05:30
Rohit Waghchaure
7d66e4efb0 fix: test case 2025-01-11 11:06:13 +05:30
rohitwaghchaure
bb170c024f chore: fix conflicts 2025-01-11 11:00:20 +05:30
rohitwaghchaure
9426a32184 chore: fix conflicts 2025-01-11 10:59:26 +05:30
Rohit Waghchaure
f7e3854641 fix: incorrect valuation rate for PI based revaluation
(cherry picked from commit 14ce2337df)
2025-01-11 04:39:31 +00:00
Rohit Waghchaure
0df18080c7 fix: delivery_document_no column issue
(cherry picked from commit 61efb2bb39)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
#	erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
2025-01-11 04:38:31 +00:00
Frappe PR Bot
625ff4b276 chore(release): Bumped to Version 15.48.2
## [15.48.2](https://github.com/frappe/erpnext/compare/v15.48.1...v15.48.2) (2025-01-10)

### Bug Fixes

* precision loss causing process loss variance ([ac18c56](ac18c56a0b))
* timeout error for work order ([d6614f2](d6614f2848))
2025-01-10 06:31:50 +00:00
rohitwaghchaure
da11589b74 Merge pull request #45203 from frappe/mergify/bp/version-15/pr-45197
fix: precision loss causing process loss variance (backport #45197)
2025-01-10 12:00:25 +05:30
rohitwaghchaure
7344e46717 Merge pull request #45205 from frappe/mergify/bp/version-15/pr-45179
fix: timeout error for work order (backport #45177) (backport #45179)
2025-01-10 11:59:50 +05:30
Rohit Waghchaure
d6614f2848 fix: timeout error for work order
(cherry picked from commit b4ceda6f2c)
(cherry picked from commit 122b966a7b)
2025-01-10 06:10:09 +00:00
FATHIH MOHAMMED
ac18c56a0b fix: precision loss causing process loss variance
(cherry picked from commit d84601b2a3)
2025-01-10 05:49:26 +00:00
rohitwaghchaure
f5c9ab054f Merge pull request #45197 from FathihMohammed/qty_precision_fix
fix: precision loss causing process loss variance
2025-01-10 11:18:21 +05:30
ruthra kumar
698b6e6caf Merge pull request #45140 from frappe/mergify/bp/version-15-hotfix/pr-45100
chore: removal of tally migration feature (backport #45100)
2025-01-09 19:56:16 +05:30
FATHIH MOHAMMED
d84601b2a3 fix: precision loss causing process loss variance 2025-01-09 13:27:27 +00:00
rohitwaghchaure
ef3e442684 Merge pull request #45186 from frappe/mergify/bp/version-15-hotfix/pr-45180
fix: do not add ordered items from Quotation to new Sales Order (backport #45180)
2025-01-09 16:26:56 +05:30
Frappe PR Bot
246f4373b5 chore(release): Bumped to Version 15.48.1
## [15.48.1](https://github.com/frappe/erpnext/compare/v15.48.0...v15.48.1) (2025-01-09)

### Bug Fixes

* not able to see create Quality Inspection button ([5d7a3b5](5d7a3b5979))
2025-01-09 09:31:15 +00:00
rohitwaghchaure
430c4825c0 Merge pull request #45187 from frappe/mergify/bp/version-15/pr-45184
fix: not able to see create Quality Inspection button (backport #45183) (backport #45184)
2025-01-09 14:59:54 +05:30
Rohit Waghchaure
5d7a3b5979 fix: not able to see create Quality Inspection button
(cherry picked from commit b291835ccd)
(cherry picked from commit a79cae1fef)
2025-01-09 09:21:29 +00:00
rohitwaghchaure
01c7956329 Merge pull request #45184 from frappe/mergify/bp/version-15-hotfix/pr-45183
fix: not able to see create Quality Inspection button (backport #45183)
2025-01-09 14:50:24 +05:30
Rohit Waghchaure
f414fa4981 fix: do not add ordered items from Quotation to new Sales Order
(cherry picked from commit 2e930eb97b)
2025-01-09 09:09:55 +00:00
Rohit Waghchaure
a79cae1fef fix: not able to see create Quality Inspection button
(cherry picked from commit b291835ccd)
2025-01-09 09:07:50 +00:00
rohitwaghchaure
5e32796987 Merge pull request #45179 from frappe/mergify/bp/version-15-hotfix/pr-45177
fix: timeout error for work order (backport #45177)
2025-01-09 14:06:58 +05:30
Rohit Waghchaure
122b966a7b fix: timeout error for work order
(cherry picked from commit b4ceda6f2c)
2025-01-09 08:18:25 +00:00
Frappe PR Bot
6a869139a6 chore(release): Bumped to Version 15.48.0
# [15.48.0](https://github.com/frappe/erpnext/compare/v15.47.5...v15.48.0) (2025-01-08)

### Bug Fixes

* add monthly distributation and write query in qb ([03b06fc](03b06fc3ff))
* Alternative Item button dissapearing on Save event ([642b897](642b89782d))
* Alternative Items button in Work Order ([3194807](3194807a41))
* Bank Reconciliation Statement Report Company Filter ([622bfa6](622bfa6633))
* consider expired batches in stock reco ([c924feb](c924feb0d0))
* discount resetting on date change ([af53381](af53381163)), closes [#44989](https://github.com/frappe/erpnext/issues/44989)
* Hold_to_On_Hold ([9ecafdc](9ecafdc680))
* ignore currency validation while canceling the voucher ([8874f4a](8874f4a9e4))
* ignore party account validation while canceling the voucher ([0f1c6ff](0f1c6ff1c9))
* invoice against purchase receipt with returned quantity ([9daabfc](9daabfca8a))
* issue in returning components against the SCO ([87405f0](87405f0753))
* load price list rate for pos search term ([452dffa](452dffab48))
* load search term price with customer default price list ([59af144](59af144e29))
* Missing company filter breaks `get_account_balance` in Bank Reco ([931b516](931b5166a8))
* Override pre-commit behaviour due to conflicts with CI ([754845a](754845a935))
* **Project:** make status in confirmation dialog translatable ([#45118](https://github.com/frappe/erpnext/issues/45118)) ([0001d86](0001d868c7))
* removed unmerged patches ([4227d76](4227d76f08))
* resolved conflicts ([e9be858](e9be8583b0))
* resolved conflicts ([7318748](7318748a4c))
* Returned Qty in Work Order Consumed Materials report ([f7b501b](f7b501b29b))
* serial and batch no. buttons on pos ([#45048](https://github.com/frappe/erpnext/issues/45048)) ([54e3a74](54e3a74936))
* show new button in coa if create access ([89155f5](89155f529e))
* update customer contact details on pos (backport [#45071](https://github.com/frappe/erpnext/issues/45071)) ([#45106](https://github.com/frappe/erpnext/issues/45106)) ([01254da](01254da4e0))

### Features

* validate discount date in payment schedule (backport [#44646](https://github.com/frappe/erpnext/issues/44646)) ([#44727](https://github.com/frappe/erpnext/issues/44727)) ([500deff](500deff3e9))
* work in progress status for asset ([#45066](https://github.com/frappe/erpnext/issues/45066)) ([1ea36bb](1ea36bba88))
2025-01-08 10:29:45 +00:00
ruthra kumar
e240ff4f6c Merge pull request #45131 from frappe/version-15-hotfix
chore: release v15
2025-01-08 15:58:27 +05:30
Khushi Rawat
9e60d1ad79 Merge pull request #45149 from frappe/mergify/bp/version-15-hotfix/pr-45066
feat: work in progress status for asset (backport #45066)
2025-01-08 11:45:46 +05:30
Khushi Rawat
4227d76f08 fix: removed unmerged patches 2025-01-08 11:23:38 +05:30
Khushi Rawat
e9be8583b0 fix: resolved conflicts 2025-01-08 11:08:27 +05:30
Khushi Rawat
7318748a4c fix: resolved conflicts 2025-01-08 11:07:30 +05:30
ruthra kumar
19dc9975dd Merge pull request #45157 from frappe/mergify/bp/version-15-hotfix/pr-45154
fix: possible deadlock while using auto reconciliation (backport #45154)
2025-01-08 10:05:34 +05:30
ruthra kumar
36dbb867ed chore: resolve conflict 2025-01-08 09:19:53 +05:30
ruthra kumar
e578ab2c86 refactor: remove tally migration doctype 2025-01-08 09:16:53 +05:30
ruthra kumar
c45f22c776 Merge pull request #45139 from frappe/mergify/bp/version-15-hotfix/pr-45130
refactor: change sales invoice button position (backport #45130)
2025-01-08 09:01:59 +05:30
ruthra kumar
acd66fa00c chore: remove 'Experimental' tag
(cherry picked from commit 4620025dcd)

# Conflicts:
#	erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json
2025-01-08 03:31:35 +00:00
ruthra kumar
04384c47f8 Merge pull request #45153 from frappe/mergify/bp/version-15-hotfix/pr-45112
fix: Missing company filter breaks `get_account_balance` in Bank Reco (backport #45112)
2025-01-08 09:00:01 +05:30
marination
754845a935 fix: Override pre-commit behaviour due to conflicts with CI
(cherry picked from commit d7bf73cffa)
2025-01-08 02:47:22 +00:00
marination
931b5166a8 fix: Missing company filter breaks get_account_balance in Bank Reco
(cherry picked from commit 8de0fe78ea)
2025-01-08 02:47:21 +00:00
Khushi Rawat
1ea36bba88 feat: work in progress status for asset (#45066)
* feat: work in progress status for asset

* fix: test case correction

* fix(patch): added patch to update status of assets

* fix: updated tests

(cherry picked from commit 6850019649)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.json
#	erpnext/patches.txt
2025-01-07 18:14:22 +00:00
rohitwaghchaure
807d591e7e Merge pull request #45145 from frappe/mergify/bp/version-15-hotfix/pr-45144
fix: issue in returning components against the SCO (backport #45144)
2025-01-07 23:41:03 +05:30
Rohit Waghchaure
87405f0753 fix: issue in returning components against the SCO
(cherry picked from commit 729ce1dc50)
2025-01-07 13:02:09 +00:00
rohitwaghchaure
e36bd5fe26 Merge pull request #45143 from frappe/mergify/bp/version-15-hotfix/pr-45048
fix: serial and batch no. buttons on pos (backport #45048)
2025-01-07 18:32:05 +05:30
Diptanil Saha
54e3a74936 fix: serial and batch no. buttons on pos (#45048)
(cherry picked from commit 31dd32dcdf)
2025-01-07 12:36:46 +00:00
Diptanil Saha
296d5d2295 chore: removal of tally migration feature (#45100)
(cherry picked from commit 9f77793f16)

# Conflicts:
#	erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json
#	erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
#	erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.py
2025-01-07 12:00:01 +00:00
Ejaaz Khan
f654c2d156 refactor: change sales invoice button position (#45130)
(cherry picked from commit a0f17f8e73)
2025-01-07 11:49:24 +00:00
ruthra kumar
cac35246f1 Merge pull request #45123 from frappe/mergify/bp/version-15-hotfix/pr-45121
fix: discount resetting on date change (backport #45121)
2025-01-07 11:32:09 +05:30
ruthra kumar
af53381163 fix: discount resetting on date change
revert #44989

(cherry picked from commit 886281f81a)
2025-01-07 05:51:48 +00:00
ruthra kumar
1c06831b11 Merge pull request #45109 from frappe/mergify/bp/version-15-hotfix/pr-45107
fix: Returned Qty in Work Order Consumed Materials report (backport #45107)
2025-01-07 10:12:43 +05:30
ruthra kumar
616a2b7675 Merge pull request #45120 from frappe/mergify/bp/version-15-hotfix/pr-45118
fix(Project): make status in confirmation dialog translatable (backport #45118)
2025-01-07 10:11:56 +05:30
Raffael Meyer
0001d868c7 fix(Project): make status in confirmation dialog translatable (#45118)
(cherry picked from commit 9eede907f8)
2025-01-06 22:13:47 +00:00
mergify[bot]
0abace7911 refactor(Project): extract custom button function (backport #45116) (#45117)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-01-06 23:04:30 +01:00
Rohit Waghchaure
f7b501b29b fix: Returned Qty in Work Order Consumed Materials report
(cherry picked from commit 30d68a31e0)
2025-01-06 10:37:32 +00:00
mergify[bot]
01254da4e0 fix: update customer contact details on pos (backport #45071) (#45106)
fix: update customer contact details on pos (#45071)

* fix: update customer contact details on pos

* refactor: removed console log statement

(cherry picked from commit d79e561248)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-01-06 15:01:23 +05:30
rohitwaghchaure
dadc8266dc Merge pull request #45098 from frappe/mergify/bp/version-15-hotfix/pr-45084
fix: Alternative Items button in Work Order (backport #45084)
2025-01-06 14:48:02 +05:30
ruthra kumar
100b4e9274 Merge pull request #45103 from frappe/mergify/bp/version-15-hotfix/pr-45006
fix: pos search by term items price (backport #45006)
2025-01-06 14:43:56 +05:30
diptanilsaha
59af144e29 fix: load search term price with customer default price list
(cherry picked from commit 2beb485d77)
2025-01-06 08:39:58 +00:00
diptanilsaha
452dffab48 fix: load price list rate for pos search term
(cherry picked from commit 4b6cae156e)
2025-01-06 08:39:57 +00:00
ruthra kumar
eff12cbfbe Merge pull request #45101 from frappe/mergify/bp/version-15-hotfix/pr-45099
fix: show new button in coa if create access (backport #45099)
2025-01-06 13:56:43 +05:30
sokumon
89155f529e fix: show new button in coa if create access
(cherry picked from commit 3125bc8a16)
2025-01-06 08:20:01 +00:00
Mihir Kandoi
642b89782d fix: Alternative Item button dissapearing on Save event
(cherry picked from commit b8838bd9b9)
2025-01-06 06:52:16 +00:00
Mihir Kandoi
3194807a41 fix: Alternative Items button in Work Order
(cherry picked from commit e28382afc1)
2025-01-06 06:52:16 +00:00
ruthra kumar
2333d33362 Merge pull request #45095 from frappe/mergify/bp/version-15-hotfix/pr-44943
fix: Bank Reconciliation Statement Report Company Filter (backport #44943)
2025-01-06 10:51:48 +05:30
creative-paramu
622bfa6633 fix: Bank Reconciliation Statement Report Company Filter
(cherry picked from commit 50c92034ba)
2025-01-06 10:30:24 +05:30
ruthra kumar
e22771c729 Merge pull request #45080 from frappe/mergify/bp/version-15-hotfix/pr-44983
fix: add monthly distributation and write query in qb (backport #44983)
2025-01-06 10:28:18 +05:30
Frappe PR Bot
08e58e13ac chore(release): Bumped to Version 15.47.5
## [15.47.5](https://github.com/frappe/erpnext/compare/v15.47.4...v15.47.5) (2025-01-06)

### Bug Fixes

* ignore currency validation while canceling the voucher ([8076942](807694206b))
* ignore party account validation while canceling the voucher ([bc0be5a](bc0be5a00a))
2025-01-06 04:42:56 +00:00
ruthra kumar
c513c7f73d Merge pull request #45094 from frappe/mergify/bp/version-15/pr-45051
fix: ignore currency validation while canceling the voucher (backport #45051)
2025-01-06 10:11:41 +05:30
ruthra kumar
5b066f4a59 Merge pull request #45096 from frappe/mergify/bp/version-15-hotfix/pr-44952
fix: Hold_to_On_Hold (backport #44952)
2025-01-06 10:09:35 +05:30
mahsem
9ecafdc680 fix: Hold_to_On_Hold
(cherry picked from commit 92b1f314ef)
2025-01-06 03:15:30 +00:00
ruthra kumar
05763d226a Merge pull request #45093 from frappe/mergify/bp/version-15-hotfix/pr-45051
fix: ignore currency validation while canceling the voucher (backport #45051)
2025-01-06 08:44:46 +05:30
venkat102
bc0be5a00a fix: ignore party account validation while canceling the voucher
(cherry picked from commit 49885f8eae)
2025-01-06 02:59:36 +00:00
venkat102
807694206b fix: ignore currency validation while canceling the voucher
(cherry picked from commit 15d488b9aa)
2025-01-06 02:59:35 +00:00
venkat102
0f1c6ff1c9 fix: ignore party account validation while canceling the voucher
(cherry picked from commit 49885f8eae)
2025-01-06 02:55:50 +00:00
venkat102
8874f4a9e4 fix: ignore currency validation while canceling the voucher
(cherry picked from commit 15d488b9aa)
2025-01-06 02:55:49 +00:00
rohitwaghchaure
21a83c508a Merge pull request #45089 from frappe/mergify/bp/version-15-hotfix/pr-45087
fix: invoice against purchase receipt with returned quantity (backport #45087)
2025-01-05 19:02:07 +05:30
rohitwaghchaure
90b8860a40 chore: fix conflicts 2025-01-05 16:24:15 +05:30
Rohit Waghchaure
9daabfca8a fix: invoice against purchase receipt with returned quantity
(cherry picked from commit d5babf4237)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
2025-01-04 15:08:10 +00:00
rohitwaghchaure
10a4b54a67 Merge pull request #45086 from frappe/mergify/bp/version-15-hotfix/pr-45083
fix: consider expired batches in stock reco (backport #45083)
2025-01-04 20:37:16 +05:30
Rohit Waghchaure
c924feb0d0 fix: consider expired batches in stock reco
(cherry picked from commit f51c9f578c)
2025-01-04 13:57:04 +00:00
Joseph Mania
cfa062df86 refactor: phone number field when channel is phone on Payment Request (#44949)
Co-authored-by: maniamartial <martialamania19@gmail.com>
2025-01-04 05:23:41 +05:30
ruthra kumar
2e67a33412 refactor: store result in variable before enumeration
helps to inspect result while debugging

(cherry picked from commit b60bd17d1d)
2025-01-03 23:48:40 +00:00
Sanket322
03b06fc3ff fix: add monthly distributation and write query in qb
(cherry picked from commit 27195c7c96)
2025-01-03 23:48:40 +00:00
mergify[bot]
500deff3e9 feat: validate discount date in payment schedule (backport #44646) (#44727)
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2025-01-03 13:19:34 +01:00
Frappe PR Bot
c3d1e122be chore(release): Bumped to Version 15.47.4
## [15.47.4](https://github.com/frappe/erpnext/compare/v15.47.3...v15.47.4) (2025-01-03)

### Bug Fixes

* Added patch and fallback code to prevent future issues similiar to helpdesk ticket 28246 ([ab87265](ab87265395))
* Added patch and fallback code to prevent future issues similiar to helpdesk ticket 28246 ([66544bf](66544bfa10))
* BOM cost update issue ([00102a1](00102a15e3))
* Fixed logic in if condition causing tests to fail ([b3b8083](b3b808335f))
* Removed patch as instructed by mentor ([3049027](3049027f43))
* Removed patch as instructed by mentor ([163af91](163af91c37))
* removed unknown patch? ([f9d038e](f9d038ee4a))
* removed unused code ([d31b0a5](d31b0a507f))
* slow stock transactions (backport [#45025](https://github.com/frappe/erpnext/issues/45025)) ([#45027](https://github.com/frappe/erpnext/issues/45027)) ([85ba96e](85ba96e0f3))
* **style:** set image width in BOM ([6247d5a](6247d5aadb))
* validate components and their qty as per BOM in the stock entry ([b5f6926](b5f6926140))
2025-01-03 01:24:44 +00:00
ruthra kumar
d5fed84182 Merge pull request #45063 from frappe/version-15-hotfix
chore: release v15
2025-01-03 06:53:23 +05:30
ruthra kumar
7f96aee06f Merge pull request #45060 from frappe/mergify/bp/version-15/pr-45056
chore: partial revert #44989 (backport #45056)
2025-01-02 21:10:08 +05:30
ruthra kumar
2c487af2df Merge pull request #45058 from frappe/mergify/bp/version-15-hotfix/pr-45056
chore: partial revert #44989 (backport #45056)
2025-01-02 21:04:22 +05:30
ruthra kumar
a5a219cd6e chore: partial revert #44989
(cherry picked from commit 63d547fb4a)
2025-01-02 15:29:55 +00:00
ruthra kumar
4dfc5a664a chore: partial revert #44989
(cherry picked from commit 63d547fb4a)
2025-01-02 15:27:40 +00:00
rohitwaghchaure
d3ea8b8e77 Merge pull request #45054 from frappe/mergify/bp/version-15-hotfix/pr-45053
fix(style): set image width in BOM (backport #45053)
2025-01-02 19:07:29 +05:30
sokumon
6247d5aadb fix(style): set image width in BOM
(cherry picked from commit b634ba7f54)
2025-01-02 13:31:21 +00:00
rohitwaghchaure
85167bf934 Merge pull request #45044 from frappe/mergify/bp/version-15-hotfix/pr-45043
fix: validate components and their qty as per BOM in the stock entry (backport #45043)
2025-01-02 14:19:38 +05:30
Rohit Waghchaure
b5f6926140 fix: validate components and their qty as per BOM in the stock entry
(cherry picked from commit b1de82ddad)
2025-01-02 08:16:03 +00:00
rohitwaghchaure
c615df5ac4 Merge pull request #45042 from frappe/mergify/bp/version-15-hotfix/pr-45039
fix: removed unused code (backport #45039)
2025-01-02 12:36:25 +05:30
rohitwaghchaure
d26d0c6282 Merge pull request #45031 from frappe/mergify/bp/version-15-hotfix/pr-44999
fix: Added patch and fallback code to prevent future issues similar … (backport #44999)
2025-01-02 12:18:18 +05:30
Rohit Waghchaure
d31b0a507f fix: removed unused code
(cherry picked from commit dc5f2d35ac)
2025-01-02 06:47:38 +00:00
rohitwaghchaure
bd12c1475a Merge pull request #45038 from frappe/mergify/bp/version-15-hotfix/pr-45036
fix: Auto BOM cost update issue (backport #45036)
2025-01-02 12:15:50 +05:30
Mihir Kandoi
f9d038ee4a fix: removed unknown patch? 2025-01-02 10:51:11 +05:30
Rohit Waghchaure
00102a15e3 fix: BOM cost update issue
(cherry picked from commit 28ea3ddd51)
2025-01-02 04:55:22 +00:00
Mihir Kandoi
3049027f43 fix: Removed patch as instructed by mentor 2025-01-01 17:13:49 +05:30
Mihir Kandoi
ab87265395 fix: Added patch and fallback code to prevent future issues similiar to helpdesk ticket 28246 2025-01-01 17:12:10 +05:30
Mihir Kandoi
163af91c37 fix: Removed patch as instructed by mentor
(cherry picked from commit d1d01482df)

# Conflicts:
#	erpnext/patches.txt
2025-01-01 11:06:27 +00:00
Mihir Kandoi
b3b808335f fix: Fixed logic in if condition causing tests to fail
(cherry picked from commit 575fb43f9c)
2025-01-01 11:06:27 +00:00
Mihir Kandoi
66544bfa10 fix: Added patch and fallback code to prevent future issues similiar to helpdesk ticket 28246
(cherry picked from commit 65dc3505c4)

# Conflicts:
#	erpnext/patches.txt
2025-01-01 11:06:27 +00:00
mergify[bot]
85ba96e0f3 fix: slow stock transactions (backport #45025) (#45027)
fix: slow stock transactions (#45025)

(cherry picked from commit e92af10f14)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-01-01 15:11:01 +05:30
Kerolles Fathy
8e33d93273 fix: apply ruff formatter changes 2024-12-11 14:14:02 +02:00
KerollesFathy
6a84f0b027 chore: resync version-15-hotfix with feature branch 2024-12-11 13:48:01 +02:00
KerollesFathy
5db66bd6dd test: get scrap items from sub assemblies consider the parent BOM 2024-08-28 12:59:18 +03:00
KerollesFathy
b62df307d3 style: format code 2024-08-26 18:09:09 +03:00
Carlos9K
da7b69109a fix: getting scrap items from sub assemblies by fetching scrap items for parent BOM 2024-08-26 18:00:45 +03:00
161 changed files with 113366 additions and 2230 deletions

View File

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

View File

@@ -237,19 +237,22 @@ frappe.treeview_settings["Account"] = {
},
post_render: function (treeview) {
frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree;
treeview.page.set_primary_action(
__("New"),
function () {
let root_company = treeview.page.fields_dict.root_company.get_value();
if (root_company) {
frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]);
} else {
treeview.new_node();
}
},
"add"
);
if (treeview.can_create) {
treeview.page.set_primary_action(
__("New"),
function () {
let root_company = treeview.page.fields_dict.root_company.get_value();
if (root_company) {
frappe.throw(__("Please add the account to root level Company - {0}"), [
root_company,
]);
} else {
treeview.new_node();
}
},
"add"
);
}
},
toolbar: [
{

View File

@@ -0,0 +1,34 @@
import json
from pathlib import Path
syscohada_countries = [
"bj", # Bénin
"bf", # Burkina-Faso
"cm", # Cameroun
"cf", # Centrafrique
"ci", # Côte d'Ivoire
"cg", # Congo
"km", # Comores
"ga", # Gabon
"gn", # Guinée
"gw", # Guinée-Bissau
"gq", # Guinée Equatoriale
"ml", # Mali
"ne", # Niger
"cd", # République Démocratique du Congo
"sn", # Sénégal
"td", # Tchad
"tg", # Togo
]
folder = Path(__file__).parent
generic_charts = Path(folder).glob("syscohada*.json")
for file in generic_charts:
with open(file) as f:
chart = json.load(f)
for country in syscohada_countries:
chart["country_code"] = country
json_object = json.dumps(chart, indent=4)
with open(Path(folder, file.name.replace("syscohada", country)), "w") as outfile:
outfile.write(json_object)

View File

@@ -12,7 +12,7 @@ frappe.ui.form.on("Accounts Settings", {
msg += " ";
msg += __("Please enable only if the understand the effects of enabling this.");
msg += "<br>";
msg += "Do you still want to enable immutable ledger?";
msg += __("Do you still want to enable immutable ledger?");
frappe.confirm(
msg,

View File

@@ -40,9 +40,13 @@
"show_payment_schedule_in_print",
"currency_exchange_section",
"allow_stale",
"column_break_yuug",
"stale_days",
"section_break_jpd0",
"auto_reconcile_payments",
"stale_days",
"auto_reconciliation_job_trigger",
"reconciliation_queue_size",
"column_break_resa",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
"over_billing_allowance",
@@ -72,6 +76,7 @@
"reports_tab",
"remarks_section",
"general_ledger_remarks_length",
"ignore_is_opening_check_for_reporting",
"column_break_lvjk",
"receivable_payable_remarks_length",
"payment_request_settings",
@@ -489,6 +494,35 @@
"fieldname": "create_pr_in_draft_status",
"fieldtype": "Check",
"label": "Create in Draft Status"
},
{
"fieldname": "column_break_yuug",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_resa",
"fieldtype": "Column Break"
},
{
"default": "15",
"description": "Interval should be between 1 to 59 MInutes",
"fieldname": "auto_reconciliation_job_trigger",
"fieldtype": "Int",
"label": "Auto Reconciliation Job Trigger"
},
{
"default": "5",
"description": "Documents Processed on each trigger. Queue Size should be between 5 and 100",
"fieldname": "reconciliation_queue_size",
"fieldtype": "Int",
"label": "Reconciliation Queue Size"
},
{
"default": "0",
"description": "Ignores legacy Is Opening field in GL Entry that allows adding opening balance post the system is in use while generating reports",
"fieldname": "ignore_is_opening_check_for_reporting",
"fieldtype": "Check",
"label": "Ignore Is Opening check for reporting"
}
],
"icon": "icon-cog",
@@ -496,7 +530,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-07-26 06:48:52.714630",
"modified": "2025-01-18 21:24:19.840745",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -10,6 +10,7 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document
from frappe.utils import cint
from erpnext.accounts.utils import sync_auto_reconcile_config
from erpnext.stock.utils import check_pending_reposting
@@ -27,6 +28,7 @@ class AccountsSettings(Document):
allow_multi_currency_invoices_against_single_party_account: DF.Check
allow_stale: DF.Check
auto_reconcile_payments: DF.Check
auto_reconciliation_job_trigger: DF.Int
automatically_fetch_payment_terms: DF.Check
automatically_process_deferred_accounting_entry: DF.Check
book_asset_depreciation_entry_automatically: DF.Check
@@ -46,11 +48,13 @@ class AccountsSettings(Document):
frozen_accounts_modifier: DF.Link | None
general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check
ignore_is_opening_check_for_reporting: DF.Check
make_payment_via_journal_entry: DF.Check
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
post_change_gl_entries: DF.Check
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None
round_row_wise_tax: DF.Check
show_balance_in_coa: DF.Check
@@ -90,6 +94,8 @@ class AccountsSettings(Document):
if clear_cache:
frappe.clear_cache()
self.validate_and_sync_auto_reconcile_config()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
frappe.msgprint(
@@ -114,3 +120,17 @@ class AccountsSettings(Document):
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)
def validate_and_sync_auto_reconcile_config(self):
if self.has_value_changed("auto_reconciliation_job_trigger"):
if (
cint(self.auto_reconciliation_job_trigger) > 0
and cint(self.auto_reconciliation_job_trigger) < 60
):
sync_auto_reconcile_config(self.auto_reconciliation_job_trigger)
else:
frappe.throw(_("Cron Interval should be between 1 and 59 Min"))
if self.has_value_changed("reconciliation_queue_size"):
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
frappe.throw(_("Queue Size should be between 5 and 100"))

View File

@@ -120,6 +120,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
args: {
bank_account: frm.doc.bank_account,
till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1),
company: frm.doc.company,
},
callback: (response) => {
frm.set_value("account_opening_balance", response.message);
@@ -135,6 +136,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
args: {
bank_account: frm.doc.bank_account,
till_date: frm.doc.bank_statement_to_date,
company: frm.doc.company,
},
callback: (response) => {
frm.cleared_balance = response.message;

View File

@@ -79,10 +79,17 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
@frappe.whitelist()
def get_account_balance(bank_account, till_date):
def get_account_balance(bank_account, till_date, company):
# returns account balance till the specified date
account = frappe.db.get_value("Bank Account", bank_account, "account")
filters = frappe._dict({"account": account, "report_date": till_date, "include_pos_transactions": 1})
filters = frappe._dict(
{
"account": account,
"report_date": till_date,
"include_pos_transactions": 1,
"company": company,
}
)
data = get_entries(filters)
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
@@ -94,11 +101,7 @@ def get_account_balance(bank_account, till_date):
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
bank_bal = (
flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
)
return bank_bal
return flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
@frappe.whitelist()

View File

@@ -490,13 +490,20 @@ def get_actual_expense(args):
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
distribution = {}
if monthly_distribution:
for d in frappe.db.sql(
"""select mdp.month, mdp.percentage_allocation
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
where mdp.parent=md.name and md.fiscal_year=%s""",
fiscal_year,
as_dict=1,
):
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
md = frappe.qb.DocType("Monthly Distribution")
res = (
frappe.qb.from_(mdp)
.join(md)
.on(mdp.parent == md.name)
.select(mdp.month, mdp.percentage_allocation)
.where(md.fiscal_year == fiscal_year)
.where(md.name == monthly_distribution)
.run(as_dict=True)
)
for d in res:
distribution.setdefault(d.month, d.percentage_allocation)
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")

View File

@@ -275,6 +275,9 @@ class GLEntry(Document):
validate_account_party_type(self)
def validate_currency(self):
if self.is_cancelled:
return
company_currency = erpnext.get_company_currency(self.company)
account_currency = get_account_currency(self.account)

View File

@@ -21,6 +21,7 @@
"party_name",
"book_advance_payments_in_separate_party_account",
"reconcile_on_advance_payment_date",
"advance_reconciliation_takes_effect_on",
"column_break_11",
"bank_account",
"party_bank_account",
@@ -782,6 +783,16 @@
"options": "No\nYes",
"print_hide": 1,
"search_index": 1
},
{
"default": "Oldest Of Invoice Or Advance",
"fetch_from": "company.reconciliation_takes_effect_on",
"fieldname": "advance_reconciliation_takes_effect_on",
"fieldtype": "Select",
"hidden": 1,
"label": "Advance Reconciliation Takes Effect On",
"no_copy": 1,
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
}
],
"index_web_pages_for_search": 1,
@@ -795,7 +806,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2024-11-07 11:19:19.320883",
"modified": "2025-01-13 16:03:47.169699",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -1401,16 +1401,26 @@ class PaymentEntry(AccountsController):
"voucher_detail_no": invoice.name,
}
if self.reconcile_on_advance_payment_date:
posting_date = self.posting_date
if invoice.reconcile_effect_on:
posting_date = invoice.reconcile_effect_on
else:
date_field = "posting_date"
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
if getdate(posting_date) < getdate(self.posting_date):
# For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction
if self.advance_reconciliation_takes_effect_on == "Advance Payment Date":
posting_date = self.posting_date
elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
posting_date = frappe.db.get_value(
invoice.reference_doctype, invoice.reference_name, date_field
)
if getdate(posting_date) < getdate(self.posting_date):
posting_date = self.posting_date
elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date":
posting_date = nowdate()
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
args_dict["account"] = account

View File

@@ -13,6 +13,7 @@
"payment_term_outstanding",
"account_type",
"payment_type",
"reconcile_effect_on",
"column_break_4",
"total_amount",
"outstanding_amount",
@@ -144,12 +145,18 @@
"is_virtual": 1,
"label": "Payment Request Outstanding",
"read_only": 1
},
{
"fieldname": "reconcile_effect_on",
"fieldtype": "Date",
"label": "Reconcile Effect On",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-09-16 18:11:50.019343",
"modified": "2025-01-13 15:56:18.895082",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -30,6 +30,7 @@ class PaymentEntryReference(Document):
payment_term: DF.Link | None
payment_term_outstanding: DF.Float
payment_type: DF.Data | None
reconcile_effect_on: DF.Date | None
reference_doctype: DF.Link
reference_name: DF.DynamicLink
total_amount: DF.Float

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
from frappe.utils.data import getdate as convert_to_date
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -1671,7 +1672,7 @@ class TestPaymentReconciliation(FrappeTestCase):
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": self.advance_payable_account,
"reconcile_on_advance_payment_date": 1,
"reconciliation_takes_effect_on": "Advance Payment Date",
},
)
@@ -1720,7 +1721,7 @@ class TestPaymentReconciliation(FrappeTestCase):
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_received_account": self.advance_receivable_account,
"reconcile_on_advance_payment_date": 0,
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
},
)
amount = 200.0
@@ -1829,7 +1830,7 @@ class TestPaymentReconciliation(FrappeTestCase):
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": self.advance_payable_account,
"reconcile_on_advance_payment_date": 0,
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
},
)
amount = 200.0
@@ -2048,6 +2049,102 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pr.get("invoices"), [])
self.assertEqual(pr.get("payments"), [])
def test_advance_reconciliation_effect_on_same_date(self):
frappe.db.set_value(
"Company",
self.company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_received_account": self.advance_receivable_account,
"reconciliation_takes_effect_on": "Reconciliation Date",
},
)
inv_date = convert_to_date(add_days(nowdate(), -1))
adv_date = convert_to_date(add_days(nowdate(), -2))
si = self.create_sales_invoice(posting_date=inv_date, qty=1, rate=200)
pe = self.create_payment_entry(posting_date=adv_date, amount=80).save().submit()
pr = self.create_payment_reconciliation()
pr.from_invoice_date = add_days(nowdate(), -1)
pr.to_invoice_date = nowdate()
pr.from_payment_date = add_days(nowdate(), -2)
pr.to_payment_date = nowdate()
pr.default_advance_account = self.advance_receivable_account
# reconcile multiple payments against invoice
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
si.reload()
self.assertEqual(si.status, "Partly Paid")
# check PR tool output post reconciliation
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 120)
self.assertEqual(pr.get("payments"), [])
# Assert Ledger Entries
gl_entries = frappe.db.get_all(
"GL Entry",
filters={"voucher_no": pe.name},
fields=["account", "posting_date", "voucher_no", "against_voucher", "debit", "credit"],
order_by="account, against_voucher, debit",
)
expected_gl = [
{
"account": self.advance_receivable_account,
"posting_date": adv_date,
"voucher_no": pe.name,
"against_voucher": pe.name,
"debit": 0.0,
"credit": 80.0,
},
{
"account": self.advance_receivable_account,
"posting_date": convert_to_date(nowdate()),
"voucher_no": pe.name,
"against_voucher": pe.name,
"debit": 80.0,
"credit": 0.0,
},
{
"account": self.debit_to,
"posting_date": convert_to_date(nowdate()),
"voucher_no": pe.name,
"against_voucher": si.name,
"debit": 0.0,
"credit": 80.0,
},
{
"account": self.bank,
"posting_date": adv_date,
"voucher_no": pe.name,
"against_voucher": None,
"debit": 80.0,
"credit": 0.0,
},
]
self.assertEqual(expected_gl, gl_entries)
# cancel PE
pe.reload()
pe.cancel()
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 0)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200)
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):

View File

@@ -58,7 +58,9 @@
"payment_account",
"payment_channel",
"payment_order",
"amended_from"
"amended_from",
"column_break_iiuv",
"phone_number"
],
"fields": [
{
@@ -376,6 +378,7 @@
"read_only": 1
},
{
"depends_on": "eval: doc.payment_channel==\"Phone\"",
"fetch_from": "payment_gateway_account.payment_channel",
"fieldname": "payment_channel",
"fieldtype": "Select",
@@ -429,13 +432,22 @@
"fieldtype": "Data",
"label": "Party Name",
"read_only": 1
},
{
"fieldname": "column_break_iiuv",
"fieldtype": "Column Break"
},
{
"fieldname": "phone_number",
"fieldtype": "Data",
"label": "Phone Number"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-10-23 12:23:40.117336",
"modified": "2024-12-27 21:29:10.361894",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",

View File

@@ -224,6 +224,7 @@ class PaymentRequest(Document):
sender=self.email_to,
currency=self.currency,
payment_gateway=self.payment_gateway,
phone_number=self.phone_number,
)
controller.validate_transaction_currency(self.currency)
@@ -635,6 +636,7 @@ def make_payment_request(**args):
"party": args.get("party") or ref_doc.get("customer"),
"bank_account": bank_account,
"party_name": args.get("party_name") or ref_doc.get("customer_name"),
"phone_number": args.get("phone_number") if args.get("phone_number") else None,
}
)

View File

@@ -835,7 +835,8 @@ class TestPOSInvoice(unittest.TestCase):
{
"item_code": item.name,
"warehouse": pos_inv2.items[0].warehouse,
"voucher_type": "Delivery Note",
"voucher_type": "POS Invoice",
"voucher_no": pos_inv2.name,
"qty": 2,
"avg_rate": 300,
"batches": frappe._dict({"TestBatch 01": 2}),

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"autoname": "format:ACC-PPR-{#####}",
"beta": 1,
"creation": "2023-03-30 21:28:39.793927",
"default_view": "List",
"doctype": "DocType",
@@ -158,7 +157,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-08-27 14:48:56.715320",
"modified": "2025-01-08 08:22:14.798085",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",
@@ -192,4 +191,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "company"
}
}

View File

@@ -210,9 +210,9 @@ def trigger_reconciliation_for_queued_docs():
docs_to_trigger = []
unique_filters = set()
queue_size = 5
queue_size = frappe.db.get_single_value("Accounts Settings", "reconciliation_queue_size") or 5
fields = ["company", "party_type", "party", "receivable_payable_account"]
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
def get_filters_as_tuple(fields, doc):
filters = ()

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"autoname": "format:PPR-LOG-{##}",
"beta": 1,
"creation": "2023-03-13 15:00:09.149681",
"default_view": "List",
"doctype": "DocType",
@@ -110,7 +109,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-11-02 11:32:12.254018",
"modified": "2025-01-08 08:22:19.104975",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log",

View File

@@ -1623,7 +1623,7 @@
{
"default": "1",
"depends_on": "eval: doc.is_return && doc.return_against",
"description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"description": "Debit Note will update it's own outstanding amount, even if 'Return Against' is specified.",
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
"label": "Update Outstanding for Self"
@@ -1633,7 +1633,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2024-10-25 18:13:01.944477",
"modified": "2025-01-14 11:39:04.564610",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -2494,6 +2494,34 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(len(actual), 3)
self.assertEqual(expected, actual)
def test_invoice_against_returned_pr(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_purchase_invoice_from_pr,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_return_against_rejected_warehouse,
)
item = make_item("_Test Item For Invoice Against Returned PR", properties={"is_stock_item": 1}).name
original_value = frappe.db.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
pr = make_purchase_receipt(item_code=item, qty=5, rejected_qty=5, rate=100)
pr_return = make_purchase_return_against_rejected_warehouse(pr.name)
pr_return.submit()
pi = make_purchase_invoice_from_pr(pr.name)
pi.save()
self.assertEqual(pi.items[0].qty, 5.0)
frappe.db.set_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", original_value
)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -994,47 +994,51 @@ frappe.ui.form.on("Sales Invoice", {
refresh: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
frm.add_custom_button(__("Fetch Timesheet"), function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
frm.events.add_timesheet_data(frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
});
d.hide();
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
frm.events.add_timesheet_data(frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
});
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
if (frm.doc.is_debit_note) {

View File

@@ -2161,7 +2161,7 @@
{
"default": "1",
"depends_on": "eval: doc.is_return && doc.return_against",
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"description": "Credit Note will update it's own outstanding amount, even if 'Return Against' is specified.",
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
"label": "Update Outstanding for Self",
@@ -2186,7 +2186,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-11-26 12:34:09.110690",
"modified": "2025-01-14 11:38:30.446370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -39,7 +39,7 @@ from erpnext.controllers.selling_controller import SellingController
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.setup.doctype.company.company import update_company_current_month_sales
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -321,9 +321,7 @@ class SalesInvoice(SellingController):
self.set_against_income_account()
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if not self.is_return:
self.validate_serial_numbers()
else:
if self.is_return:
self.timesheets = []
self.update_packing_list()
self.set_billing_hours_and_amount()
@@ -367,7 +365,7 @@ class SalesInvoice(SellingController):
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
elif asset.status in ("Scrapped", "Cancelled", "Capitalized", "Decapitalized") or (
elif asset.status in ("Scrapped", "Cancelled", "Capitalized") or (
asset.status == "Sold" and not self.is_return
):
frappe.throw(
@@ -1706,53 +1704,6 @@ class SalesInvoice(SellingController):
self.set("write_off_amount", reference_doc.get("write_off_amount"))
self.due_date = None
def validate_serial_numbers(self):
"""
validate serial number agains Delivery Note and Sales Invoice
"""
self.set_serial_no_against_delivery_note()
self.validate_serial_against_delivery_note()
def set_serial_no_against_delivery_note(self):
for item in self.items:
if item.serial_no and item.delivery_note and item.qty != len(get_serial_nos(item.serial_no)):
item.serial_no = get_delivery_note_serial_no(item.item_code, item.qty, item.delivery_note)
def validate_serial_against_delivery_note(self):
"""
validate if the serial numbers in Sales Invoice Items are same as in
Delivery Note Item
"""
for item in self.items:
if not item.delivery_note or not item.dn_detail:
continue
serial_nos = frappe.db.get_value("Delivery Note Item", item.dn_detail, "serial_no") or ""
dn_serial_nos = set(get_serial_nos(serial_nos))
serial_nos = item.serial_no or ""
si_serial_nos = set(get_serial_nos(serial_nos))
serial_no_diff = si_serial_nos - dn_serial_nos
if serial_no_diff:
dn_link = frappe.utils.get_link_to_form("Delivery Note", item.delivery_note)
serial_no_msg = ", ".join(frappe.bold(d) for d in serial_no_diff)
msg = _("Row #{0}: The following Serial Nos are not present in Delivery Note {1}:").format(
item.idx, dn_link
)
msg += " " + serial_no_msg
frappe.throw(msg=msg, title=_("Serial Nos Mismatch"))
if item.serial_no and cint(item.qty) != len(si_serial_nos):
frappe.throw(
_("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
item.idx, item.qty, item.item_code, len(si_serial_nos)
)
)
def update_project(self):
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
if self.project and self.project not in unique_projects:

View File

@@ -114,10 +114,10 @@ class Subscription(Document):
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
_current_invoice_start = add_days(self.trial_period_end, 1)
elif self.trial_period_start and self.is_trialling():
_current_invoice_start = self.trial_period_start
elif date:
_current_invoice_start = date
elif self.trial_period_start and self.is_trialling():
_current_invoice_start = self.trial_period_start
else:
_current_invoice_start = nowdate()
@@ -414,8 +414,8 @@ class Subscription(Document):
if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
invoice.apply_tds = 1
# Add party currency to invoice
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
# Add currency to invoice
invoice.currency = frappe.db.get_value("Subscription Plan", {"name": self.plans[0].plan}, "currency")
# Add dimensions in invoice for subscription:
accounting_dimensions = get_accounting_dimensions()
@@ -697,7 +697,7 @@ class Subscription(Document):
self.status = "Cancelled"
self.cancelation_date = nowdate()
if to_generate_invoice:
if to_generate_invoice and self.cancelation_date >= self.current_invoice_start:
self.generate_invoice(self.current_invoice_start, self.cancelation_date)
self.save()

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils.data import (
add_days,
add_months,
@@ -470,6 +470,28 @@ class TestSubscription(FrappeTestCase):
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
self.assertEqual(currency, "USD")
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1},
)
def test_multi_currency_subscription_with_default_company_currency(self):
party = "Test Subscription Customer Multi Currency"
frappe.db.set_value("Customer", party, "default_currency", "USD")
subscription = create_subscription(
start_date="2018-01-01",
generate_invoice_at="Beginning of the current subscription period",
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}],
party=party,
)
subscription.process(posting_date="2018-01-01")
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, "Unpaid")
# Check the currency of the created invoice
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
self.assertEqual(currency, "USD")
def test_subscription_recovery(self):
"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
subscription = create_subscription(
@@ -581,6 +603,12 @@ def create_parties():
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"})
customer.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer Multi Currency"):
customer = frappe.new_doc("Customer")
customer.customer_name = "Test Subscription Customer Multi Currency"
customer.default_currency = "USD"
customer.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer John Doe"):
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Subscription Customer John Doe"

View File

@@ -156,6 +156,9 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
}
)
if cint(tax_details.round_off_tax_amount):
inv.round_off_applicable_accounts_for_tax_withholding = tax_details.account_head
if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances, voucher_wise_amount
else:
@@ -302,6 +305,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0
if party_type == "Supplier":
# if tds account is changed.
if not tax_deducted:
tax_deducted = is_tax_deducted_on_the_basis_of_inv(vouchers)
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
if tax_deducted:
net_total = inv.tax_withholding_net_total
@@ -336,6 +343,18 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
def is_tax_deducted_on_the_basis_of_inv(vouchers):
return frappe.db.exists(
"Purchase Taxes and Charges",
{
"parent": ["in", vouchers],
"is_tax_withholding_account": 1,
"parenttype": "Purchase Invoice",
"base_tax_amount_after_discount_amount": [">", 0],
},
)
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
field = (
@@ -555,9 +574,11 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
else:
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
if (threshold and tax_withholding_net_total >= threshold) or (
has_cumulative_threshold_breached = (
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
):
)
if (threshold and tax_withholding_net_total >= threshold) or (has_cumulative_threshold_breached):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = (
@@ -565,9 +586,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
)
supp_credit_amt += net_total
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
tax_details.tax_on_excess_amount
):
if has_cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):

View File

@@ -61,6 +61,49 @@ class TestTaxWithholdingCategory(FrappeTestCase):
for d in reversed(invoices):
d.cancel()
def test_tds_with_account_changed(self):
frappe.db.set_value(
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Multi Account TDS Category"
)
invoices = []
# create invoices for lower than single threshold tax rate
for _ in range(2):
pi = create_purchase_invoice(supplier="Test TDS Supplier")
pi.submit()
invoices.append(pi)
# create another invoice whose total when added to previously created invoice,
# surpasses cumulative threshhold
pi = create_purchase_invoice(supplier="Test TDS Supplier")
pi.submit()
# assert equal tax deduction on total invoice amount until now
self.assertEqual(pi.taxes_and_charges_deducted, 3000)
self.assertEqual(pi.grand_total, 7000)
invoices.append(pi)
# account changed
frappe.db.set_value(
"Tax Withholding Account",
{"parent": "Multi Account TDS Category"},
"account",
"_Test Account VAT - _TC",
)
# TDS should be on invoice only even though account is changed
pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
pi.submit()
# assert equal tax deduction on total invoice amount until now
self.assertEqual(pi.taxes_and_charges_deducted, 500)
invoices.append(pi)
# delete invoices to avoid clashing
for d in reversed(invoices):
d.cancel()
def test_single_threshold_tds(self):
invoices = []
frappe.db.set_value(
@@ -1061,6 +1104,16 @@ def create_tax_withholding_category_records():
consider_party_ledger_amount=1,
)
create_tax_withholding_category(
category_name="Multi Account TDS Category",
rate=10,
from_date=from_date,
to_date=to_date,
account="TDS - _TC",
single_threshold=0,
cumulative_threshold=30000,
)
def create_tax_withholding_category(
category_name,

View File

@@ -676,7 +676,7 @@ def set_taxes(
):
from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template
args = {party_type.lower(): party, "company": company}
args = {frappe.scrub(party_type): party, "company": company}
if tax_category:
args["tax_category"] = tax_category
@@ -696,10 +696,10 @@ def set_taxes(
else:
args.update(get_party_details(party, party_type))
if party_type in ("Customer", "Lead", "Prospect"):
if party_type in ("Customer", "Lead", "Prospect", "CRM Deal"):
args.update({"tax_type": "Sales"})
if party_type in ["Lead", "Prospect"]:
if party_type in ["Lead", "Prospect", "CRM Deal"]:
args["customer"] = None
del args[frappe.scrub(party_type)]
else:
@@ -760,6 +760,9 @@ def validate_party_frozen_disabled(party_type, party_name):
def validate_account_party_type(self):
if self.is_cancelled:
return
if self.party_type and self.party:
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type and (account_type not in ["Receivable", "Payable"]):

View File

@@ -142,7 +142,8 @@ def get_journal_entries(filters):
where jvd.parent = jv.name and jv.docstatus=1
and jvd.account = %(account)s and jv.posting_date <= %(report_date)s
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No'""",
and ifnull(jv.is_opening, 'No') = 'No'
and jv.company = %(company)s """,
filters,
as_dict=1,
)
@@ -163,6 +164,7 @@ def get_payment_entries(filters):
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date <= %(report_date)s
and ifnull(clearance_date, '4000-01-01') > %(report_date)s
and company = %(company)s
""",
filters,
as_dict=1,
@@ -181,6 +183,7 @@ def get_pos_entries(filters):
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
and account.name = sip.account and si.posting_date <= %(report_date)s and
ifnull(sip.clearance_date, '4000-01-01') > %(report_date)s
and si.company = %(company)s
order by
si.posting_date ASC, si.name DESC
""",

View File

@@ -510,12 +510,16 @@ def get_accounting_entries(
.where(gl_entry.company == filters.company)
)
ignore_is_opening = frappe.db.get_single_value(
"Accounts Settings", "ignore_is_opening_check_for_reporting"
)
if doctype == "GL Entry":
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
query = query.where(gl_entry.is_cancelled == 0)
query = query.where(gl_entry.posting_date <= to_date)
if ignore_opening_entries:
if ignore_opening_entries and not ignore_is_opening:
query = query.where(gl_entry.is_opening == "No")
else:
query = query.select(gl_entry.closing_date.as_("posting_date"))

View File

@@ -208,6 +208,10 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters):
conditions = []
ignore_is_opening = frappe.db.get_single_value(
"Accounts Settings", "ignore_is_opening_check_for_reporting"
)
if filters.get("account"):
filters.account = get_accounts_with_children(filters.account)
if filters.account:
@@ -270,9 +274,15 @@ def get_conditions(filters):
or filters.get("party")
or filters.get("group_by") in ["Group by Account", "Group by Party"]
):
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
if not ignore_is_opening:
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
else:
conditions.append("posting_date >=%(from_date)s")
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
if not ignore_is_opening:
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
else:
conditions.append("posting_date <=%(to_date)s")
if filters.get("project"):
conditions.append("project in %(project)s")

View File

@@ -318,7 +318,7 @@ def get_columns(additional_table_columns, filters):
"width": 100,
},
{
"label": _("Tax Rate"),
"label": _("Rate"),
"fieldname": "rate",
"fieldtype": "Float",
"options": "currency",

View File

@@ -14,14 +14,14 @@
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "Trial Balance",
"report_type": "Script Report",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}

View File

@@ -89,6 +89,10 @@ def get_data(filters):
)
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
ignore_is_opening = frappe.db.get_single_value(
"Accounts Settings", "ignore_is_opening_check_for_reporting"
)
if not accounts:
return None
@@ -96,7 +100,7 @@ def get_data(filters):
gl_entries_by_account = {}
opening_balances = get_opening_balances(filters)
opening_balances = get_opening_balances(filters, ignore_is_opening)
# add filter inside list so that the query in financial_statements.py doesn't break
if filters.project:
@@ -114,7 +118,13 @@ def get_data(filters):
ignore_opening_entries=True,
)
calculate_values(accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values"))
calculate_values(
accounts,
gl_entries_by_account,
opening_balances,
filters.get("show_net_values"),
ignore_is_opening=ignore_is_opening,
)
accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, parent_children_map, company_currency)
@@ -125,15 +135,15 @@ def get_data(filters):
return data
def get_opening_balances(filters):
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet")
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss")
def get_opening_balances(filters, ignore_is_opening):
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet", ignore_is_opening)
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss", ignore_is_opening)
balance_sheet_opening.update(pl_opening)
return balance_sheet_opening
def get_rootwise_opening_balances(filters, report_type):
def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
gle = []
last_period_closing_voucher = ""
@@ -159,16 +169,24 @@ def get_rootwise_opening_balances(filters, report_type):
report_type,
accounting_dimensions,
period_closing_voucher=last_period_closing_voucher[0].name,
ignore_is_opening=ignore_is_opening,
)
# Report getting generate from the mid of a fiscal year
if getdate(last_period_closing_voucher[0].period_end_date) < getdate(add_days(filters.from_date, -1)):
start_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
gle += get_opening_balance(
"GL Entry", filters, report_type, accounting_dimensions, start_date=start_date
"GL Entry",
filters,
report_type,
accounting_dimensions,
start_date=start_date,
ignore_is_opening=ignore_is_opening,
)
else:
gle = get_opening_balance("GL Entry", filters, report_type, accounting_dimensions)
gle = get_opening_balance(
"GL Entry", filters, report_type, accounting_dimensions, ignore_is_opening=ignore_is_opening
)
opening = frappe._dict()
for d in gle:
@@ -187,7 +205,13 @@ def get_rootwise_opening_balances(filters, report_type):
def get_opening_balance(
doctype, filters, report_type, accounting_dimensions, period_closing_voucher=None, start_date=None
doctype,
filters,
report_type,
accounting_dimensions,
period_closing_voucher=None,
start_date=None,
ignore_is_opening=0,
):
closing_balance = frappe.qb.DocType(doctype)
account = frappe.qb.DocType("Account")
@@ -223,11 +247,16 @@ def get_opening_balance(
(closing_balance.posting_date >= start_date)
& (closing_balance.posting_date < filters.from_date)
)
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
if not ignore_is_opening:
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
else:
opening_balance = opening_balance.where(
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
)
if not ignore_is_opening:
opening_balance = opening_balance.where(
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
)
else:
opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date)
if doctype == "GL Entry":
opening_balance = opening_balance.where(closing_balance.is_cancelled == 0)
@@ -298,7 +327,7 @@ def get_opening_balance(
return gle
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values):
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values, ignore_is_opening=0):
init = {
"opening_debit": 0.0,
"opening_credit": 0.0,
@@ -316,7 +345,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
d["opening_credit"] = opening_balances.get(d.name, {}).get("opening_credit", 0)
for entry in gl_entries_by_account.get(d.name, []):
if cstr(entry.is_opening) != "Yes":
if cstr(entry.is_opening) != "Yes" or ignore_is_opening:
d["debit"] += flt(entry.debit)
d["credit"] += flt(entry.credit)

View File

@@ -712,6 +712,23 @@ def update_reference_in_payment_entry(
}
update_advance_paid = []
# Update Reconciliation effect date in reference
if payment_entry.book_advance_payments_in_separate_party_account:
if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = payment_entry.posting_date
elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field)
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
reconcile_on = payment_entry.posting_date
elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date":
reconcile_on = nowdate()
reference_details.update({"reconcile_effect_on": reconcile_on})
if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
@@ -2233,3 +2250,38 @@ def run_ledger_health_checks():
doc.general_and_payment_ledger_mismatch = True
doc.checked_on = run_date
doc.save()
def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15):
auto_reconciliation_job_trigger = auto_reconciliation_job_trigger or frappe.db.get_single_value(
"Accounts Settings", "auto_reconciliation_job_trigger"
)
method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs"
sch_event = frappe.get_doc(
"Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method}
)
if frappe.db.get_value("Scheduled Job Type", {"method": method}):
frappe.get_doc(
"Scheduled Job Type",
{
"method": method,
},
).update(
{
"cron_format": f"0/{auto_reconciliation_job_trigger} * * * *",
"scheduler_event": sch_event.name,
}
).save()
else:
frappe.get_doc(
{
"doctype": "Scheduled Job Type",
"method": method,
"scheduler_event": sch_event.name,
"cron_format": f"0/{auto_reconciliation_job_trigger} * * * *",
"create_log": True,
"stopped": False,
"frequency": "Cron",
}
).save()

View File

@@ -378,7 +378,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized",
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"read_only": 1
},
{
@@ -595,7 +595,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2024-08-26 23:28:29.095139",
"modified": "2024-12-26 14:23:20.968882",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -111,7 +111,7 @@ class Asset(AccountsController):
"Issue",
"Receipt",
"Capitalized",
"Decapitalized",
"Work In Progress",
]
supplier: DF.Link | None
total_asset_cost: DF.Currency

View File

@@ -1,5 +1,6 @@
frappe.listview_settings["Asset"] = {
add_fields: ["status"],
add_fields: ["status", "docstatus"],
has_indicator_for_draft: 1,
get_indicator: function (doc) {
if (doc.status === "Fully Depreciated") {
return [__("Fully Depreciated"), "green", "status,=,Fully Depreciated"];
@@ -7,8 +8,10 @@ frappe.listview_settings["Asset"] = {
return [__("Partially Depreciated"), "grey", "status,=,Partially Depreciated"];
} else if (doc.status === "Sold") {
return [__("Sold"), "green", "status,=,Sold"];
} else if (["Capitalized", "Decapitalized"].includes(doc.status)) {
return [__(doc.status), "grey", "status,=," + doc.status];
} else if (doc.status === "Work In Progress") {
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
} else if (doc.status === "Capitalized") {
return [__("Capitalized"), "grey", "status,=,Capitalized"];
} else if (doc.status === "Scrapped") {
return [__("Scrapped"), "grey", "status,=,Scrapped"];
} else if (doc.status === "In Maintenance") {
@@ -21,7 +24,7 @@ frappe.listview_settings["Asset"] = {
return [__("Receipt"), "green", "status,=,Receipt"];
} else if (doc.status === "Submitted") {
return [__("Submitted"), "blue", "status,=,Submitted"];
} else if (doc.status === "Draft") {
} else if (doc.status === "Draft" || doc.docstatus === 0) {
return [__("Draft"), "red", "status,=,Draft"];
}
},

View File

@@ -436,7 +436,7 @@ def scrap_asset(asset_name):
if asset.docstatus != 1:
frappe.throw(_("Asset {0} must be submitted").format(asset.name))
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"):
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"):
frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
date = today()

View File

@@ -1725,6 +1725,10 @@ def create_asset(**args):
},
)
if asset.is_composite_asset:
asset.gross_purchase_amount = 0
asset.purchase_amount = 0
if not args.do_not_save:
try:
asset.insert(ignore_if_duplicate=True)

View File

@@ -36,11 +36,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
me.setup_warehouse_query();
me.frm.set_query("target_item_code", function () {
if (me.frm.doc.entry_type == "Capitalization") {
return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 1 });
} else {
return erpnext.queries.item({ is_stock_item: 1, is_fixed_asset: 0 });
}
return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 1 });
});
me.frm.set_query("target_asset", function () {
@@ -51,7 +47,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
me.frm.set_query("asset", "asset_items", function () {
var filters = {
status: ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]],
status: ["not in", ["Draft", "Scrapped", "Sold", "Capitalized"]],
docstatus: 1,
};

View File

@@ -8,30 +8,26 @@
"engine": "InnoDB",
"field_order": [
"title",
"company",
"naming_series",
"entry_type",
"target_item_name",
"target_is_fixed_asset",
"target_has_batch_no",
"target_has_serial_no",
"column_break_9",
"capitalization_method",
"target_item_code",
"target_asset_location",
"target_item_name",
"target_asset",
"target_asset_name",
"target_warehouse",
"target_qty",
"target_stock_uom",
"target_batch_no",
"target_serial_no",
"column_break_5",
"finance_book",
"target_asset_location",
"column_break_9",
"company",
"posting_date",
"posting_time",
"set_posting_time",
"finance_book",
"target_batch_no",
"target_serial_no",
"amended_from",
"target_is_fixed_asset",
"target_has_batch_no",
"target_has_serial_no",
"section_break_16",
"stock_items",
"stock_items_total",
@@ -58,12 +54,12 @@
"label": "Title"
},
{
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')",
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || doc.capitalization_method=='Create a new composite asset'",
"fieldname": "target_item_code",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Item Code",
"mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'",
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
"options": "Item"
},
{
@@ -84,22 +80,18 @@
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || doc.capitalization_method=='Choose a WIP composite asset'",
"fieldname": "target_asset",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Asset",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'",
"mandatory_depends_on": "eval:doc.capitalization_method=='Choose a WIP composite asset'",
"no_copy": 1,
"options": "Asset",
"read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')"
"read_only_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'"
},
{
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.capitalization_method=='Choose a WIP composite asset')",
"fetch_from": "target_asset.asset_name",
"fieldname": "target_asset_name",
"fieldtype": "Data",
@@ -162,7 +154,7 @@
"read_only": 1
},
{
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length))",
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Consumed Stock Items"
@@ -173,14 +165,6 @@
"label": "Stock Items",
"options": "Asset Capitalization Stock Item"
},
{
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fieldname": "target_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
"mandatory_depends_on": "eval:doc.entry_type=='Decapitalization'",
"options": "Warehouse"
},
{
"depends_on": "target_has_batch_no",
"fieldname": "target_batch_no",
@@ -190,20 +174,9 @@
},
{
"default": "1",
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fieldname": "target_qty",
"fieldtype": "Float",
"label": "Target Qty",
"read_only_depends_on": "eval:doc.entry_type=='Capitalization'"
},
{
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fetch_from": "target_item_code.stock_uom",
"fieldname": "target_stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
"label": "Target Qty"
},
{
"default": "0",
@@ -241,16 +214,6 @@
"label": "Assets",
"options": "Asset Capitalization Asset Item"
},
{
"default": "Capitalization",
"fieldname": "entry_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Entry Type",
"options": "Capitalization\nDecapitalization",
"reqd": 1
},
{
"fieldname": "stock_items_total",
"fieldtype": "Currency",
@@ -272,7 +235,7 @@
"options": "Finance Book"
},
{
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.service_items && doc.service_items.length))",
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
"fieldname": "service_expenses_section",
"fieldtype": "Section Break",
"label": "Service Expenses"
@@ -337,26 +300,24 @@
"read_only": 1
},
{
"depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
"depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
"fieldname": "target_asset_location",
"fieldtype": "Link",
"label": "Target Asset Location",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
"options": "Location"
},
{
"depends_on": "eval:doc.entry_type=='Capitalization'",
"fieldname": "capitalization_method",
"fieldtype": "Select",
"label": "Capitalization Method",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
"options": "\nCreate a new composite asset\nChoose a WIP composite asset"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-10-03 22:55:59.461456",
"modified": "2025-01-08 13:14:33.008458",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization",
@@ -400,4 +361,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -41,7 +41,6 @@ force_fields = [
"target_is_fixed_asset",
"target_has_serial_no",
"target_has_batch_no",
"target_stock_uom",
"stock_uom",
"fixed_asset_account",
"valuation_rate",
@@ -73,7 +72,6 @@ class AssetCapitalization(StockController):
capitalization_method: DF.Literal["", "Create a new composite asset", "Choose a WIP composite asset"]
company: DF.Link
cost_center: DF.Link | None
entry_type: DF.Literal["Capitalization", "Decapitalization"]
finance_book: DF.Link | None
naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
posting_date: DF.Date
@@ -96,8 +94,6 @@ class AssetCapitalization(StockController):
target_item_name: DF.Data | None
target_qty: DF.Float
target_serial_no: DF.SmallText | None
target_stock_uom: DF.Link | None
target_warehouse: DF.Link | None
title: DF.Data | None
total_value: DF.Currency
# end: auto-generated types
@@ -190,31 +186,18 @@ class AssetCapitalization(StockController):
def validate_target_item(self):
target_item = frappe.get_cached_doc("Item", self.target_item_code)
if not target_item.is_fixed_asset and not target_item.is_stock_item:
frappe.throw(
_("Target Item {0} is neither a Fixed Asset nor a Stock Item").format(target_item.name)
)
if self.entry_type == "Capitalization" and not target_item.is_fixed_asset:
if not target_item.is_fixed_asset:
frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name))
elif self.entry_type == "Decapitalization" and not target_item.is_stock_item:
frappe.throw(_("Target Item {0} must be a Stock Item").format(target_item.name))
if target_item.is_fixed_asset:
self.target_qty = 1
if flt(self.target_qty) <= 0:
frappe.throw(_("Target Qty must be a positive number"))
if not target_item.is_stock_item:
self.target_warehouse = None
if not target_item.has_batch_no:
self.target_batch_no = None
if not target_item.has_serial_no:
self.target_serial_no = ""
if target_item.is_stock_item and not self.target_warehouse:
frappe.throw(_("Target Warehouse is mandatory for Decapitalization"))
self.validate_item(target_item)
def validate_target_asset(self):
@@ -231,7 +214,7 @@ class AssetCapitalization(StockController):
)
)
if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"):
if target_asset.status in ("Scrapped", "Sold", "Capitalized"):
frappe.throw(
_("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status)
)
@@ -273,7 +256,7 @@ class AssetCapitalization(StockController):
asset = self.get_asset_for_validation(d.asset)
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized"):
frappe.throw(
_("Row #{0}: Consumed Asset {1} cannot be {2}").format(
d.idx, asset.name, asset.status
@@ -314,9 +297,6 @@ class AssetCapitalization(StockController):
d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
def validate_source_mandatory(self):
if not self.target_is_fixed_asset and not self.get("asset_items"):
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
if self.capitalization_method == "Create a new composite asset" and not (
self.get("stock_items") or self.get("asset_items")
):
@@ -420,18 +400,6 @@ class AssetCapitalization(StockController):
)
sl_entries.append(sle)
if self.entry_type == "Decapitalization" and not self.target_is_fixed_asset:
sle = self.get_sl_entries(
self,
{
"item_code": self.target_item_code,
"warehouse": self.target_warehouse,
"actual_qty": flt(self.target_qty),
"incoming_rate": flt(self.target_incoming_rate),
},
)
sl_entries.append(sle)
# reverse sl entries if cancel
if self.docstatus == 2:
sl_entries.reverse()
@@ -474,21 +442,18 @@ class AssetCapitalization(StockController):
return gl_entries
def get_target_account(self):
if self.target_is_fixed_asset:
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
if is_cwip_accounting_enabled(asset_category):
target_account = get_asset_category_account(
"capital_work_in_progress_account",
asset_category=asset_category,
company=self.company,
)
return target_account if target_account else self.target_fixed_asset_account
else:
return self.target_fixed_asset_account
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
if is_cwip_accounting_enabled(asset_category):
target_account = get_asset_category_account(
"capital_work_in_progress_account",
asset_category=asset_category,
company=self.company,
)
return target_account if target_account else self.target_fixed_asset_account
else:
return self.warehouse_account[self.target_warehouse]["account"]
return self.target_fixed_asset_account
def get_gl_entries_for_consumed_stock_items(self, gl_entries, target_account, target_against, precision):
# Consumed Stock Items
@@ -589,33 +554,9 @@ class AssetCapitalization(StockController):
item=self,
)
)
else:
# Target Stock Item
sle_list = self.sle_map.get(self.name)
for sle in sle_list:
stock_value_difference = flt(sle.stock_value_difference, precision)
account = self.warehouse_account[sle.warehouse]["account"]
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": ", ".join(target_against),
"cost_center": self.cost_center,
"project": self.get("project"),
"remarks": self.get("remarks") or "Accounting Entry for Stock",
"debit": stock_value_difference,
},
self.warehouse_account[sle.warehouse]["account_currency"],
item=self,
)
)
def create_target_asset(self):
if (
self.entry_type != "Capitalization"
or self.capitalization_method != "Create a new composite asset"
):
if self.capitalization_method != "Create a new composite asset":
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
@@ -638,6 +579,7 @@ class AssetCapitalization(StockController):
self.target_fixed_asset_account = get_asset_category_account(
"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
)
asset_doc.set_status("Work In Progress")
add_asset_activity(
asset_doc.name,
@@ -653,17 +595,15 @@ class AssetCapitalization(StockController):
)
def update_target_asset(self):
if (
self.entry_type != "Capitalization"
or self.capitalization_method != "Choose a WIP composite asset"
):
if self.capitalization_method != "Choose a WIP composite asset":
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_amount = total_target_asset_value
asset_doc.gross_purchase_amount += total_target_asset_value
asset_doc.purchase_amount += total_target_asset_value
asset_doc.set_status("Work In Progress")
asset_doc.flags.ignore_validate = True
asset_doc.save()
@@ -698,14 +638,6 @@ class AssetCapitalization(StockController):
get_link_to_form("Asset Capitalization", self.name)
),
)
else:
asset.set_status("Decapitalized")
add_asset_activity(
asset.name,
_("Asset decapitalized after Asset Capitalization {0} was submitted").format(
get_link_to_form("Asset Capitalization", self.name)
),
)
else:
asset.set_status()
add_asset_activity(
@@ -727,16 +659,12 @@ def get_target_item_details(item_code=None, company=None):
# Set Item Details
out.target_item_name = item.item_name
out.target_stock_uom = item.stock_uom
out.target_is_fixed_asset = cint(item.is_fixed_asset)
out.target_has_batch_no = cint(item.has_batch_no)
out.target_has_serial_no = cint(item.has_serial_no)
if out.target_is_fixed_asset:
out.target_qty = 1
out.target_warehouse = None
else:
out.target_asset = None
if not out.target_has_batch_no:
out.target_batch_no = None

View File

@@ -61,7 +61,6 @@ class TestAssetCapitalization(unittest.TestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset_location="Test Location",
@@ -76,7 +75,6 @@ class TestAssetCapitalization(unittest.TestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
@@ -96,6 +94,7 @@ class TestAssetCapitalization(unittest.TestCase):
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_amount, total_amount)
self.assertEqual(target_asset.status, "Work In Progress")
# Test Consumed Asset values
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
@@ -151,7 +150,6 @@ class TestAssetCapitalization(unittest.TestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset_location="Test Location",
@@ -166,7 +164,6 @@ class TestAssetCapitalization(unittest.TestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
@@ -243,7 +240,6 @@ class TestAssetCapitalization(unittest.TestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Choose a WIP composite asset",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
@@ -255,7 +251,6 @@ class TestAssetCapitalization(unittest.TestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset")
self.assertEqual(asset_capitalization.target_qty, 1)
@@ -270,6 +265,7 @@ class TestAssetCapitalization(unittest.TestCase):
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_amount, total_amount)
self.assertEqual(target_asset.status, "Work In Progress")
# Test General Ledger Entries
expected_gle = {
@@ -295,110 +291,6 @@ class TestAssetCapitalization(unittest.TestCase):
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
def test_decapitalization_with_depreciation(self):
# Variables
purchase_date = "2020-01-01"
depreciation_start_date = "2020-12-31"
capitalization_date = "2021-06-30"
total_number_of_depreciations = 3
expected_value_after_useful_life = 10_000
consumed_asset_purchase_value = 100_000
consumed_asset_current_value = 70_000
consumed_asset_value_before_disposal = 55_000
target_qty = 10
target_incoming_rate = 5500
depreciation_before_disposal_amount = 15_000
accumulated_depreciation = 45_000
# to accomodate for depreciation on disposal calculation minor difference
consumed_asset_value_before_disposal = 55_123.29
target_incoming_rate = 5512.329
depreciation_before_disposal_amount = 14_876.71
accumulated_depreciation = 44_876.71
# Create assets
consumed_asset = create_depreciation_asset(
asset_name="Asset Capitalization Consumable Asset",
asset_value=consumed_asset_purchase_value,
purchase_date=purchase_date,
depreciation_start_date=depreciation_start_date,
depreciation_method="Straight Line",
total_number_of_depreciations=total_number_of_depreciations,
frequency_of_depreciation=12,
expected_value_after_useful_life=expected_value_after_useful_life,
company="_Test Company with perpetual inventory",
submit=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Active")
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Decapitalization",
posting_date=capitalization_date, # half a year
target_item_code="Capitalization Target Stock Item",
target_qty=target_qty,
consumed_asset=consumed_asset.name,
company="_Test Company with perpetual inventory",
submit=1,
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Decapitalization")
self.assertEqual(
asset_capitalization.asset_items[0].current_asset_value, consumed_asset_current_value
)
self.assertEqual(
asset_capitalization.asset_items[0].asset_value, consumed_asset_value_before_disposal
)
self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value_before_disposal)
self.assertEqual(asset_capitalization.total_value, consumed_asset_value_before_disposal)
self.assertEqual(asset_capitalization.target_incoming_rate, target_incoming_rate)
# Test Consumed Asset values
consumed_asset.reload()
self.assertEqual(consumed_asset.status, "Decapitalized")
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
self.assertEqual(second_asset_depr_schedule.status, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
depr_schedule_of_consumed_asset = second_asset_depr_schedule.get("depreciation_schedule")
consumed_depreciation_schedule = [
d
for d in depr_schedule_of_consumed_asset
if getdate(d.schedule_date) == getdate(capitalization_date)
]
self.assertTrue(consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry)
self.assertEqual(
consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount
)
# Test General Ledger Entries
expected_gle = {
"_Test Warehouse - TCP1": consumed_asset_value_before_disposal,
"_Test Accumulated Depreciations - TCP1": accumulated_depreciation,
"_Test Fixed Asset - TCP1": -consumed_asset_purchase_value,
}
actual_gle = get_actual_gle_dict(asset_capitalization.name)
self.assertEqual(actual_gle, expected_gle)
# Cancel Asset Capitalization and make test entries and status are reversed
asset_capitalization.reload()
asset_capitalization.cancel()
self.assertEqual(consumed_asset.db_get("status"), "Partially Depreciated")
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
def test_capitalize_only_service_item(self):
company = "_Test Company"
# Variables
@@ -418,7 +310,6 @@ class TestAssetCapitalization(unittest.TestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Choose a WIP composite asset",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
@@ -466,13 +357,11 @@ def create_asset_capitalization(**args):
target_item_code = target_asset.item_code or args.target_item_code
company = target_asset.company or args.company or "_Test Company"
warehouse = args.warehouse or create_warehouse("_Test Warehouse", company=company)
target_warehouse = args.target_warehouse or warehouse
source_warehouse = args.source_warehouse or warehouse
asset_capitalization = frappe.new_doc("Asset Capitalization")
asset_capitalization.update(
{
"entry_type": args.entry_type or "Capitalization",
"capitalization_method": args.capitalization_method or None,
"company": company,
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
@@ -480,7 +369,6 @@ def create_asset_capitalization(**args):
"target_item_code": target_item_code,
"target_asset": target_asset.name,
"target_asset_location": "Test Location",
"target_warehouse": target_warehouse,
"target_qty": flt(args.target_qty) or 1,
"target_batch_no": args.target_batch_no,
"target_serial_no": args.target_serial_no,

View File

@@ -13,6 +13,7 @@ from frappe.utils import (
flt,
get_first_day,
get_last_day,
get_link_to_form,
getdate,
is_last_day_of_the_month,
month_diff,
@@ -1062,7 +1063,7 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
if not current_asset_depr_schedule_doc:
frappe.throw(
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
asset_doc.name, row.finance_book
get_link_to_form("Asset", asset_doc.name), row.finance_book
)
)
@@ -1108,7 +1109,7 @@ def get_temp_asset_depr_schedule_doc(
if not current_asset_depr_schedule_doc:
frappe.throw(
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
asset_doc.name, row.finance_book
get_link_to_form("Asset", asset_doc.name), row.finance_book
)
)

View File

@@ -66,12 +66,12 @@ def get_conditions(filters):
conditions["cost_center"] = filters.get("cost_center")
if status:
# In Store assets are those that are not sold or scrapped or capitalized or decapitalized
# In Store assets are those that are not sold or scrapped or capitalized
operand = "not in"
if status not in "In Location":
operand = "in"
conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized", "Decapitalized"])
conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized"])
return conditions
@@ -272,9 +272,9 @@ def get_asset_depreciation_amount_map(filters, finance_book):
query = query.where(asset.cost_center == filters.cost_center)
if filters.status:
if filters.status == "In Location":
query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized"]))
else:
query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized"]))
if finance_book:
query = query.where((gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull()))
else:

View File

@@ -777,7 +777,14 @@ class AccountsController(TransactionBase):
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
if item.get(fieldname) is None or fieldname in force_item_fields:
if (
item.get(fieldname) is None
or fieldname in force_item_fields
or (
fieldname in ["serial_no", "batch_no"]
and item.get("use_serial_batch_fields")
)
):
item.set(fieldname, value)
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
@@ -2353,6 +2360,7 @@ class AccountsController(TransactionBase):
return
for d in self.get("payment_schedule"):
d.validate_from_to_dates("discount_date", "due_date")
if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
frappe.throw(
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(

View File

@@ -110,7 +110,7 @@ def validate_returned_items(doc):
for d in doc.get("items"):
key = d.item_code
raise_exception = False
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice"]:
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice", "POS Invoice"]:
field = frappe.scrub(doc.doctype) + "_item"
if d.get(field):
key = (d.item_code, d.get(field))
@@ -259,7 +259,7 @@ def get_already_returned_items(doc):
)
data = frappe.db.sql(
f"""
select {column}, {field}
select {column}, child.{field}
from
`tab{doc.doctype} Item` child, `tab{doc.doctype}` par
where
@@ -1020,7 +1020,7 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
if not qty_field:
qty_field = "qty"
qty_field = "stock_qty"
if not warehouse_field:
warehouse_field = "warehouse"
@@ -1109,7 +1109,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
warehouse_field = "warehouse"
if not qty_field:
qty_field = "qty"
qty_field = "stock_qty"
warehouse = child_doc.get(warehouse_field)
if parent_doc.get("is_internal_customer"):

View File

@@ -103,6 +103,16 @@ class SubcontractingController(StockController):
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
)
if (
self.doctype == "Subcontracting Order" and not item.sc_conversion_factor
): # this condition will only be true if user has recently updated from develop branch
service_item_qty = frappe.get_value(
"Subcontracting Order Service Item",
filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
fieldname=["qty"],
)
item.sc_conversion_factor = service_item_qty / item.qty
if (
self.doctype not in "Subcontracting Receipt"
and item.qty
@@ -1247,6 +1257,7 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
"item_code": row.item_details["rm_item_code"],
"subcontracted_item": row.item_details["main_item_code"],
"serial_no": "\n".join(row.serial_no) if row.serial_no else "",
"use_serial_batch_fields": 1,
}
)
@@ -1287,10 +1298,13 @@ def make_return_stock_entry_for_subcontract(
if not value.qty:
continue
if item_details := value.get("item_details"):
item_details["serial_and_batch_bundle"] = None
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field, batch_no)
add_items_in_ste(ste_doc, value, qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)

View File

@@ -30,6 +30,11 @@ class calculate_taxes_and_totals:
"Accounts Settings", "round_row_wise_tax"
)
if doc.get("round_off_applicable_accounts_for_tax_withholding"):
frappe.flags.round_off_applicable_accounts.append(
doc.round_off_applicable_accounts_for_tax_withholding
)
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)

View File

@@ -282,6 +282,79 @@ class TestSubcontractingController(FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def test_return_non_consumed_batch_materials(self):
"""
- Set backflush based on Material Transfer.
- Create SCO for item Subcontracted Item SA2.
- Transfer the batched components from Stores to Supplier warehouse with serial nos.
- Transfer extra qty of component for the subcontracted item Subcontracted Item SA2.
- Create SCR for full qty against the SCO and change the qty of raw material.
- After that return the non consumed material back to the store from supplier's warehouse.
"""
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
set_backflush_based_on("Material Transferred for Subcontract")
service_item = make_item("Subcontracted Service FG Item A", properties={"is_stock_item": 0}).name
fg_item = make_item(
"Subcontracted FG Item SA2", properties={"is_stock_item": 1, "is_sub_contracted_item": 1}
).name
rm_item = make_item(
"Subcontracted Batch RM Item SA2",
properties={
"is_stock_item": 1,
"create_new_batch": 1,
"has_batch_no": 1,
"batch_number_series": "BATCH-RM-IRM-.####",
},
).name
make_bom(item=fg_item, raw_materials=[rm_item], rate=100, currency="INR")
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": service_item,
"qty": 5,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 5,
},
]
sco = get_subcontracting_order(service_items=service_items)
rm_items = get_rm_items(sco.supplied_items)
rm_items[0]["qty"] += 1
itemwise_details = make_stock_in_entry(rm_items=rm_items)
for item in rm_items:
item["sco_rm_detail"] = sco.items[0].name
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
scr1 = make_subcontracting_receipt(sco.name)
scr1.save()
scr1.supplied_items[0].consumed_qty = 5
scr1.submit()
for key, value in get_supplied_items(scr1).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 5)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5]))
sco.load_from_db()
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
doc.save()
self.assertEqual(doc.items[0].qty, 1)
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
self.assertTrue(doc.items[0].batch_no)
self.assertTrue(doc.items[0].use_serial_batch_fields)
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def test_return_non_consumed_materials(self):
"""
- Set backflush based on Material Transfer.

View File

@@ -17,7 +17,9 @@
"quotation_section",
"default_valid_till",
"section_break_13",
"carry_forward_communication_and_comments"
"carry_forward_communication_and_comments",
"column_break_junk",
"update_timestamp_on_new_communication"
],
"fields": [
{
@@ -77,7 +79,7 @@
{
"fieldname": "section_break_13",
"fieldtype": "Section Break",
"label": "Other Settings"
"label": "Activity"
},
{
"default": "0",
@@ -85,13 +87,24 @@
"fieldname": "carry_forward_communication_and_comments",
"fieldtype": "Check",
"label": "Carry Forward Communication and Comments"
},
{
"fieldname": "column_break_junk",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Update the modified timestamp on new communications received in Lead & Opportunity.",
"fieldname": "update_timestamp_on_new_communication",
"fieldtype": "Check",
"label": "Update timestamp on new communication"
}
],
"icon": "fa fa-cog",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-06-06 11:22:08.464253",
"modified": "2025-01-16 16:12:14.889455",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM Settings",

View File

@@ -20,6 +20,7 @@ class CRMSettings(Document):
carry_forward_communication_and_comments: DF.Check
close_opportunity_after_days: DF.Int
default_valid_till: DF.Data | None
update_timestamp_on_new_communication: DF.Check
# end: auto-generated types
def validate(self):

View File

@@ -84,6 +84,20 @@ def link_communications_with_prospect(communication, method):
row.db_update()
def update_modified_timestamp(communication, method):
if communication.reference_doctype and communication.reference_name:
if communication.sent_or_received == "Received" and frappe.db.get_single_value(
"CRM Settings", "update_timestamp_on_new_communication"
):
frappe.db.set_value(
dt=communication.reference_doctype,
dn=communication.reference_name,
field="modified",
val=now(),
update_modified=False,
)
def get_linked_prospect(reference_doctype, reference_name):
prospect = None
if reference_doctype == "Lead":

View File

@@ -29,7 +29,7 @@ frappe.ui.form.on("Plaid Settings", {
"Bank Transaction",
"",
true,
"Bank Transaction"
__("Bank Transaction")
);
frappe.msgprint({

View File

@@ -1,364 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.provide("erpnext.tally_migration");
frappe.ui.form.on("Tally Migration", {
onload: function (frm) {
let reload_status = true;
frappe.realtime.on("tally_migration_progress_update", function (data) {
if (reload_status) {
frappe.model.with_doc(frm.doc.doctype, frm.doc.name, () => {
frm.refresh_header();
});
reload_status = false;
}
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
let error_occurred = data.count === -1;
if (data.count == data.total || error_occurred) {
window.setTimeout(
(title) => {
frm.dashboard.hide_progress(title);
frm.reload_doc();
if (error_occurred) {
frappe.msgprint({
message: __("An error has occurred during {0}. Check {1} for more details", [
repl(
"<a href='/app/tally-migration/%(tally_document)s' class='variant-click'>%(tally_document)s</a>",
{
tally_document: frm.docname,
}
),
"<a href='/app/error-log' class='variant-click'>Error Log</a>",
]),
title: __("Tally Migration Error"),
indicator: "red",
});
}
},
2000,
data.title
);
}
});
},
refresh: function (frm) {
frm.trigger("show_logs_preview");
erpnext.tally_migration.failed_import_log = JSON.parse(frm.doc.failed_import_log);
erpnext.tally_migration.fixed_errors_log = JSON.parse(frm.doc.fixed_errors_log);
["default_round_off_account", "default_warehouse", "default_cost_center"].forEach((account) => {
frm.toggle_reqd(account, frm.doc.is_master_data_imported === 1);
frm.toggle_enable(account, frm.doc.is_day_book_data_processed != 1);
});
if (frm.doc.master_data && !frm.doc.is_master_data_imported) {
if (frm.doc.is_master_data_processed) {
if (frm.doc.status != "Importing Master Data") {
frm.events.add_button(frm, __("Import Master Data"), "import_master_data");
}
} else {
if (frm.doc.status != "Processing Master Data") {
frm.events.add_button(frm, __("Process Master Data"), "process_master_data");
}
}
}
if (frm.doc.day_book_data && !frm.doc.is_day_book_data_imported) {
if (frm.doc.is_day_book_data_processed) {
if (frm.doc.status != "Importing Day Book Data") {
frm.events.add_button(frm, __("Import Day Book Data"), "import_day_book_data");
}
} else {
if (frm.doc.status != "Processing Day Book Data") {
frm.events.add_button(frm, __("Process Day Book Data"), "process_day_book_data");
}
}
}
},
erpnext_company: function (frm) {
frappe.db.exists("Company", frm.doc.erpnext_company).then((exists) => {
if (exists) {
frappe.msgprint(
__(
"Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts",
[frm.doc.erpnext_company]
)
);
}
});
},
add_button: function (frm, label, method) {
frm.add_custom_button(label, () => {
frm.call({
doc: frm.doc,
method: method,
freeze: true,
});
frm.reload_doc();
});
},
render_html_table(frm, shown_logs, hidden_logs, field) {
if (shown_logs && shown_logs.length > 0) {
frm.toggle_display(field, true);
} else {
frm.toggle_display(field, false);
return;
}
let rows = erpnext.tally_migration.get_html_rows(shown_logs, field);
let rows_head, table_caption;
let table_footer =
hidden_logs && hidden_logs.length > 0
? `<tr class="text-muted">
<td colspan="4">And ${hidden_logs.length} more others</td>
</tr>`
: "";
if (field === "fixed_error_log_preview") {
rows_head = `<th width="75%">${__("Meta Data")}</th>
<th width="10%">${__("Unresolve")}</th>`;
table_caption = "Resolved Issues";
} else {
rows_head = `<th width="75%">${__("Error Message")}</th>
<th width="10%">${__("Create")}</th>`;
table_caption = "Error Log";
}
frm.get_field(field).$wrapper.html(`
<table class="table table-bordered">
<caption>${table_caption}</caption>
<tr class="text-muted">
<th width="5%">${__("#")}</th>
<th width="10%">${__("DocType")}</th>
${rows_head}
</tr>
${rows}
${table_footer}
</table>
`);
},
show_error_summary(frm) {
let summary = erpnext.tally_migration.failed_import_log.reduce((summary, row) => {
if (row.doc) {
if (summary[row.doc.doctype]) {
summary[row.doc.doctype] += 1;
} else {
summary[row.doc.doctype] = 1;
}
}
return summary;
}, {});
console.table(summary);
},
show_logs_preview(frm) {
let empty = "[]";
let import_log = frm.doc.failed_import_log || empty;
let completed_log = frm.doc.fixed_errors_log || empty;
let render_section = !(import_log === completed_log && import_log === empty);
frm.toggle_display("import_log_section", render_section);
if (render_section) {
frm.trigger("show_error_summary");
frm.trigger("show_errored_import_log");
frm.trigger("show_fixed_errors_log");
}
},
show_errored_import_log(frm) {
let import_log = erpnext.tally_migration.failed_import_log;
let logs = import_log.slice(0, 20);
let hidden_logs = import_log.slice(20);
frm.events.render_html_table(frm, logs, hidden_logs, "failed_import_preview");
},
show_fixed_errors_log(frm) {
let completed_log = erpnext.tally_migration.fixed_errors_log;
let logs = completed_log.slice(0, 20);
let hidden_logs = completed_log.slice(20);
frm.events.render_html_table(frm, logs, hidden_logs, "fixed_error_log_preview");
},
});
erpnext.tally_migration.getError = (traceback) => {
/* Extracts the Error Message from the Python Traceback or Solved error */
let is_multiline = traceback.trim().indexOf("\n") != -1;
let message;
if (is_multiline) {
let exc_error_idx = traceback.trim().lastIndexOf("\n") + 1;
let error_line = traceback.substr(exc_error_idx);
let split_str_idx = error_line.indexOf(":") > 0 ? error_line.indexOf(":") + 1 : 0;
message = error_line.slice(split_str_idx).trim();
} else {
message = traceback;
}
return message;
};
erpnext.tally_migration.cleanDoc = (obj) => {
/* Strips all null and empty values of your JSON object */
let temp = obj;
$.each(temp, function (key, value) {
if (value === "" || value === null) {
delete obj[key];
} else if (Object.prototype.toString.call(value) === "[object Object]") {
erpnext.tally_migration.cleanDoc(value);
} else if ($.isArray(value)) {
$.each(value, function (k, v) {
erpnext.tally_migration.cleanDoc(v);
});
}
});
return temp;
};
erpnext.tally_migration.unresolve = (document) => {
/* Mark document migration as unresolved ie. move to failed error log */
let frm = cur_frm;
let failed_log = erpnext.tally_migration.failed_import_log;
let fixed_log = erpnext.tally_migration.fixed_errors_log;
let modified_fixed_log = fixed_log.filter((row) => {
if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) {
return row;
}
});
failed_log.push({ doc: document, exc: `Marked unresolved on ${Date()}` });
frm.doc.failed_import_log = JSON.stringify(failed_log);
frm.doc.fixed_errors_log = JSON.stringify(modified_fixed_log);
frm.dirty();
frm.save();
};
erpnext.tally_migration.resolve = (document) => {
/* Mark document migration as resolved ie. move to fixed error log */
let frm = cur_frm;
let failed_log = erpnext.tally_migration.failed_import_log;
let fixed_log = erpnext.tally_migration.fixed_errors_log;
let modified_failed_log = failed_log.filter((row) => {
if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) {
return row;
}
});
fixed_log.push({ doc: document, exc: `Solved on ${Date()}` });
frm.doc.failed_import_log = JSON.stringify(modified_failed_log);
frm.doc.fixed_errors_log = JSON.stringify(fixed_log);
frm.dirty();
frm.save();
};
erpnext.tally_migration.create_new_doc = (document) => {
/* Mark as resolved and create new document */
erpnext.tally_migration.resolve(document);
return frappe.call({
type: "POST",
method: "erpnext.erpnext_integrations.doctype.tally_migration.tally_migration.new_doc",
args: {
document,
},
freeze: true,
callback: function (r) {
if (!r.exc) {
frappe.model.sync(r.message);
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = true;
frappe.set_route("Form", r.message.doctype, r.message.name);
}
},
});
};
erpnext.tally_migration.get_html_rows = (logs, field) => {
let index = 0;
let rows = logs
.map(({ doc, exc }) => {
let id = frappe.dom.get_unique_id();
let traceback = exc;
let error_message = erpnext.tally_migration.getError(traceback);
index++;
let show_traceback = `
<button class="btn btn-default btn-xs m-3" type="button" data-toggle="collapse" data-target="#${id}-traceback" aria-expanded="false" aria-controls="${id}-traceback">
${__("Show Traceback")}
</button>
<div class="collapse margin-top" id="${id}-traceback">
<div class="well">
<pre style="font-size: smaller;">${traceback}</pre>
</div>
</div>`;
let show_doc = `
<button class='btn btn-default btn-xs m-3' type='button' data-toggle='collapse' data-target='#${id}-doc' aria-expanded='false' aria-controls='${id}-doc'>
${__("Show Document")}
</button>
<div class="collapse margin-top" id="${id}-doc">
<div class="well">
<pre style="font-size: smaller;">${JSON.stringify(erpnext.tally_migration.cleanDoc(doc), null, 1)}</pre>
</div>
</div>`;
let create_button = `
<button class='btn btn-default btn-xs m-3' type='button' onclick='erpnext.tally_migration.create_new_doc(${JSON.stringify(
doc
)})'>
${__("Create Document")}
</button>`;
let mark_as_unresolved = `
<button class='btn btn-default btn-xs m-3' type='button' onclick='erpnext.tally_migration.unresolve(${JSON.stringify(
doc
)})'>
${__("Mark as unresolved")}
</button>`;
if (field === "fixed_error_log_preview") {
return `<tr>
<td>${index}</td>
<td>
<div>${doc.doctype}</div>
</td>
<td>
<div>${error_message}</div>
<div>${show_doc}</div>
</td>
<td>
<div>${mark_as_unresolved}</div>
</td>
</tr>`;
} else {
return `<tr>
<td>${index}</td>
<td>
<div>${doc.doctype}</div>
</td>
<td>
<div>${error_message}</div>
<div>${show_traceback}</div>
<div>${show_doc}</div>
</td>
<td>
<div>${create_button}</div>
</td>
</tr>`;
}
})
.join("");
return rows;
};

View File

@@ -1,279 +0,0 @@
{
"actions": [],
"beta": 1,
"creation": "2019-02-01 14:27:09.485238",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status",
"master_data",
"is_master_data_processed",
"is_master_data_imported",
"column_break_2",
"tally_creditors_account",
"tally_debtors_account",
"company_section",
"tally_company",
"default_uom",
"column_break_8",
"erpnext_company",
"processed_files_section",
"chart_of_accounts",
"parties",
"addresses",
"column_break_17",
"uoms",
"items",
"vouchers",
"accounts_section",
"default_warehouse",
"default_round_off_account",
"column_break_21",
"default_cost_center",
"day_book_section",
"day_book_data",
"column_break_27",
"is_day_book_data_processed",
"is_day_book_data_imported",
"import_log_section",
"failed_import_log",
"fixed_errors_log",
"failed_import_preview",
"fixed_error_log_preview"
],
"fields": [
{
"fieldname": "status",
"fieldtype": "Data",
"hidden": 1,
"label": "Status"
},
{
"description": "Data exported from Tally that consists of the Chart of Accounts, Customers, Suppliers, Addresses, Items and UOMs",
"fieldname": "master_data",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Master Data"
},
{
"default": "Sundry Creditors",
"description": "Creditors Account set in Tally",
"fieldname": "tally_creditors_account",
"fieldtype": "Data",
"label": "Tally Creditors Account",
"read_only_depends_on": "eval:doc.is_master_data_processed==1",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"default": "Sundry Debtors",
"description": "Debtors Account set in Tally",
"fieldname": "tally_debtors_account",
"fieldtype": "Data",
"label": "Tally Debtors Account",
"read_only_depends_on": "eval:doc.is_master_data_processed==1",
"reqd": 1
},
{
"depends_on": "is_master_data_processed",
"fieldname": "company_section",
"fieldtype": "Section Break"
},
{
"description": "Company Name as per Imported Tally Data",
"fieldname": "tally_company",
"fieldtype": "Data",
"label": "Tally Company",
"read_only": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"description": "Your Company set in ERPNext",
"fieldname": "erpnext_company",
"fieldtype": "Data",
"label": "ERPNext Company",
"read_only_depends_on": "eval:doc.is_master_data_processed==1"
},
{
"fieldname": "processed_files_section",
"fieldtype": "Section Break",
"hidden": 1,
"label": "Processed Files"
},
{
"fieldname": "chart_of_accounts",
"fieldtype": "Attach",
"label": "Chart of Accounts"
},
{
"fieldname": "parties",
"fieldtype": "Attach",
"label": "Parties"
},
{
"fieldname": "addresses",
"fieldtype": "Attach",
"label": "Addresses"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "uoms",
"fieldtype": "Attach",
"label": "UOMs"
},
{
"fieldname": "items",
"fieldtype": "Attach",
"label": "Items"
},
{
"fieldname": "vouchers",
"fieldtype": "Attach",
"label": "Vouchers"
},
{
"depends_on": "is_master_data_imported",
"description": "The accounts are set by the system automatically but do confirm these defaults",
"fieldname": "accounts_section",
"fieldtype": "Section Break",
"label": "Accounts"
},
{
"fieldname": "default_warehouse",
"fieldtype": "Link",
"label": "Default Warehouse",
"options": "Warehouse"
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"fieldname": "default_cost_center",
"fieldtype": "Link",
"label": "Default Cost Center",
"options": "Cost Center"
},
{
"default": "0",
"fieldname": "is_master_data_processed",
"fieldtype": "Check",
"label": "Is Master Data Processed",
"read_only": 1
},
{
"default": "0",
"fieldname": "is_day_book_data_processed",
"fieldtype": "Check",
"label": "Is Day Book Data Processed",
"read_only": 1
},
{
"default": "0",
"fieldname": "is_day_book_data_imported",
"fieldtype": "Check",
"label": "Is Day Book Data Imported",
"read_only": 1
},
{
"default": "0",
"fieldname": "is_master_data_imported",
"fieldtype": "Check",
"label": "Is Master Data Imported",
"read_only": 1
},
{
"depends_on": "is_master_data_imported",
"fieldname": "day_book_section",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"description": "Day Book Data exported from Tally that consists of all historic transactions",
"fieldname": "day_book_data",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Day Book Data"
},
{
"default": "Unit",
"description": "UOM in case unspecified in imported data",
"fieldname": "default_uom",
"fieldtype": "Link",
"label": "Default UOM",
"options": "UOM",
"read_only_depends_on": "eval:doc.is_master_data_imported==1"
},
{
"default": "[]",
"fieldname": "failed_import_log",
"fieldtype": "Code",
"hidden": 1,
"options": "JSON"
},
{
"fieldname": "failed_import_preview",
"fieldtype": "HTML",
"label": "Failed Import Log"
},
{
"fieldname": "import_log_section",
"fieldtype": "Section Break",
"label": "Import Log"
},
{
"fieldname": "default_round_off_account",
"fieldtype": "Link",
"label": "Default Round Off Account",
"options": "Account"
},
{
"default": "[]",
"fieldname": "fixed_errors_log",
"fieldtype": "Code",
"hidden": 1,
"options": "JSON"
},
{
"fieldname": "fixed_error_log_preview",
"fieldtype": "HTML",
"label": "Fixed Error Log"
}
],
"links": [],
"modified": "2020-04-28 00:29:18.039826",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Tally Migration",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,768 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import re
import sys
import traceback
import zipfile
from decimal import Decimal
import frappe
from bs4 import BeautifulSoup as bs
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import (
create_custom_fields as _create_custom_fields,
)
from frappe.model.document import Document
from frappe.utils.data import format_datetime
from erpnext import encode_company_abbr
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
from erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer import (
unset_existing_data,
)
PRIMARY_ACCOUNT = "Primary"
VOUCHER_CHUNK_SIZE = 500
@frappe.whitelist()
def new_doc(document):
document = json.loads(document)
doctype = document.pop("doctype")
document.pop("name", None)
doc = frappe.new_doc(doctype)
doc.update(document)
return doc
class TallyMigration(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
addresses: DF.Attach | None
chart_of_accounts: DF.Attach | None
day_book_data: DF.Attach | None
default_cost_center: DF.Link | None
default_round_off_account: DF.Link | None
default_uom: DF.Link | None
default_warehouse: DF.Link | None
erpnext_company: DF.Data | None
failed_import_log: DF.Code | None
fixed_errors_log: DF.Code | None
is_day_book_data_imported: DF.Check
is_day_book_data_processed: DF.Check
is_master_data_imported: DF.Check
is_master_data_processed: DF.Check
items: DF.Attach | None
master_data: DF.Attach | None
parties: DF.Attach | None
status: DF.Data | None
tally_company: DF.Data | None
tally_creditors_account: DF.Data
tally_debtors_account: DF.Data
uoms: DF.Attach | None
vouchers: DF.Attach | None
# end: auto-generated types
def validate(self):
failed_import_log = json.loads(self.failed_import_log)
sorted_failed_import_log = sorted(failed_import_log, key=lambda row: row["doc"]["creation"])
self.failed_import_log = json.dumps(sorted_failed_import_log)
def autoname(self):
if not self.name:
self.name = "Tally Migration on " + format_datetime(self.creation)
def get_collection(self, data_file):
def sanitize(string):
return re.sub("&#4;", "", string)
def emptify(string):
string = re.sub(r"<\w+/>", "", string)
string = re.sub(r"<([\w.]+)>\s*<\/\1>", "", string)
string = re.sub(r"\r\n", "", string)
return string
master_file = frappe.get_doc("File", {"file_url": data_file})
master_file_path = master_file.get_full_path()
if zipfile.is_zipfile(master_file_path):
with zipfile.ZipFile(master_file_path) as zf:
encoded_content = zf.read(zf.namelist()[0])
try:
content = encoded_content.decode("utf-8-sig")
except UnicodeDecodeError:
content = encoded_content.decode("utf-16")
master = bs(sanitize(emptify(content)), "xml")
collection = master.BODY.IMPORTDATA.REQUESTDATA
return collection
def dump_processed_data(self, data):
for key, value in data.items():
f = frappe.get_doc(
{
"doctype": "File",
"file_name": key + ".json",
"attached_to_doctype": self.doctype,
"attached_to_name": self.name,
"content": json.dumps(value),
"is_private": True,
}
)
try:
f.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
setattr(self, key, f.file_url)
def set_account_defaults(self):
self.default_cost_center, self.default_round_off_account = frappe.db.get_value(
"Company", self.erpnext_company, ["cost_center", "round_off_account"]
)
self.default_warehouse = frappe.db.get_value("Stock Settings", "Stock Settings", "default_warehouse")
def _process_master_data(self):
def get_company_name(collection):
return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string.strip()
def get_coa_customers_suppliers(collection):
root_type_map = {
"Application of Funds (Assets)": "Asset",
"Expenses": "Expense",
"Income": "Income",
"Source of Funds (Liabilities)": "Liability",
}
roots = set(root_type_map.keys())
accounts = list(get_groups(collection.find_all("GROUP"))) + list(
get_ledgers(collection.find_all("LEDGER"))
)
children, parents = get_children_and_parent_dict(accounts)
group_set = [acc[1] for acc in accounts if acc[2]]
children, customers, suppliers = remove_parties(parents, children, group_set)
try:
coa = traverse({}, children, roots, roots, group_set)
except RecursionError:
self.log(
_(
"Error occured while parsing Chart of Accounts: Please make sure that no two accounts have the same name"
)
)
for account in coa:
coa[account]["root_type"] = root_type_map[account]
return coa, customers, suppliers
def get_groups(accounts):
for account in accounts:
if account["NAME"] in (self.tally_creditors_account, self.tally_debtors_account):
yield get_parent(account), account["NAME"], 0
else:
yield get_parent(account), account["NAME"], 1
def get_ledgers(accounts):
for account in accounts:
# If Ledger doesn't have PARENT field then don't create Account
# For example "Profit & Loss A/c"
if account.PARENT:
yield account.PARENT.string.strip(), account["NAME"], 0
def get_parent(account):
if account.PARENT:
return account.PARENT.string.strip()
return {
("Yes", "No"): "Application of Funds (Assets)",
("Yes", "Yes"): "Expenses",
("No", "Yes"): "Income",
("No", "No"): "Source of Funds (Liabilities)",
}[(account.ISDEEMEDPOSITIVE.string.strip(), account.ISREVENUE.string.strip())]
def get_children_and_parent_dict(accounts):
children, parents = {}, {}
for parent, account, _is_group in accounts:
children.setdefault(parent, set()).add(account)
parents.setdefault(account, set()).add(parent)
parents[account].update(parents.get(parent, []))
return children, parents
def remove_parties(parents, children, group_set):
customers, suppliers = set(), set()
for account in parents:
found = False
if self.tally_creditors_account in parents[account]:
found = True
if account not in group_set:
suppliers.add(account)
if self.tally_debtors_account in parents[account]:
found = True
if account not in group_set:
customers.add(account)
if found:
children.pop(account, None)
return children, customers, suppliers
def traverse(tree, children, accounts, roots, group_set):
for account in accounts:
if account in group_set or account in roots:
if account in children:
tree[account] = traverse({}, children, children[account], roots, group_set)
else:
tree[account] = {"is_group": 1}
else:
tree[account] = {}
return tree
def get_parties_addresses(collection, customers, suppliers):
parties, addresses = [], []
for account in collection.find_all("LEDGER"):
party_type = None
links = []
if account.NAME.string.strip() in customers:
party_type = "Customer"
parties.append(
{
"doctype": party_type,
"customer_name": account.NAME.string.strip(),
"tax_id": account.INCOMETAXNUMBER.string.strip()
if account.INCOMETAXNUMBER
else None,
"customer_group": "All Customer Groups",
"territory": "All Territories",
"customer_type": "Individual",
}
)
links.append({"link_doctype": party_type, "link_name": account["NAME"]})
if account.NAME.string.strip() in suppliers:
party_type = "Supplier"
parties.append(
{
"doctype": party_type,
"supplier_name": account.NAME.string.strip(),
"pan": account.INCOMETAXNUMBER.string.strip()
if account.INCOMETAXNUMBER
else None,
"supplier_group": "All Supplier Groups",
"supplier_type": "Individual",
}
)
links.append({"link_doctype": party_type, "link_name": account["NAME"]})
if party_type:
address = "\n".join([a.string.strip() for a in account.find_all("ADDRESS")])
addresses.append(
{
"doctype": "Address",
"address_line1": address[:140].strip(),
"address_line2": address[140:].strip(),
"country": account.COUNTRYNAME.string.strip() if account.COUNTRYNAME else None,
"state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None,
"gst_state": account.LEDSTATENAME.string.strip()
if account.LEDSTATENAME
else None,
"pin_code": account.PINCODE.string.strip() if account.PINCODE else None,
"mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
"phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
"gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None,
"links": links,
}
)
return parties, addresses
def get_stock_items_uoms(collection):
uoms = []
for uom in collection.find_all("UNIT"):
uoms.append({"doctype": "UOM", "uom_name": uom.NAME.string.strip()})
items = []
for item in collection.find_all("STOCKITEM"):
stock_uom = item.BASEUNITS.string.strip() if item.BASEUNITS else self.default_uom
items.append(
{
"doctype": "Item",
"item_code": item.NAME.string.strip(),
"stock_uom": stock_uom.strip(),
"is_stock_item": 0,
"item_group": "All Item Groups",
"item_defaults": [{"company": self.erpnext_company}],
}
)
return items, uoms
try:
self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5)
collection = self.get_collection(self.master_data)
company = get_company_name(collection)
self.tally_company = company
self.erpnext_company = company
self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5)
chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection)
self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5)
parties, addresses = get_parties_addresses(collection, customers, suppliers)
self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5)
items, uoms = get_stock_items_uoms(collection)
data = {
"chart_of_accounts": chart_of_accounts,
"parties": parties,
"addresses": addresses,
"items": items,
"uoms": uoms,
}
self.publish("Process Master Data", _("Done"), 5, 5)
self.dump_processed_data(data)
self.is_master_data_processed = 1
except Exception:
self.publish("Process Master Data", _("Process Failed"), -1, 5)
self.log()
finally:
self.set_status()
def publish(self, title, message, count, total):
frappe.publish_realtime(
"tally_migration_progress_update",
{"title": title, "message": message, "count": count, "total": total},
user=self.modified_by,
)
def _import_master_data(self):
def create_company_and_coa(coa_file_url):
coa_file = frappe.get_doc("File", {"file_url": coa_file_url})
frappe.local.flags.ignore_chart_of_accounts = True
try:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": self.erpnext_company,
"default_currency": "INR",
"enable_perpetual_inventory": 0,
}
).insert()
except frappe.DuplicateEntryError:
company = frappe.get_doc("Company", self.erpnext_company)
unset_existing_data(self.erpnext_company)
frappe.local.flags.ignore_chart_of_accounts = False
create_charts(company.name, custom_chart=json.loads(coa_file.get_content()))
company.create_default_warehouses()
def create_parties_and_addresses(parties_file_url, addresses_file_url):
parties_file = frappe.get_doc("File", {"file_url": parties_file_url})
for party in json.loads(parties_file.get_content()):
try:
party_doc = frappe.get_doc(party)
party_doc.insert()
except Exception:
self.log(party_doc)
addresses_file = frappe.get_doc("File", {"file_url": addresses_file_url})
for address in json.loads(addresses_file.get_content()):
try:
address_doc = frappe.get_doc(address)
address_doc.insert(ignore_mandatory=True)
except Exception:
self.log(address_doc)
def create_items_uoms(items_file_url, uoms_file_url):
uoms_file = frappe.get_doc("File", {"file_url": uoms_file_url})
for uom in json.loads(uoms_file.get_content()):
if not frappe.db.exists(uom):
try:
uom_doc = frappe.get_doc(uom)
uom_doc.insert()
except Exception:
self.log(uom_doc)
items_file = frappe.get_doc("File", {"file_url": items_file_url})
for item in json.loads(items_file.get_content()):
try:
item_doc = frappe.get_doc(item)
item_doc.insert()
except Exception:
self.log(item_doc)
try:
self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4)
create_company_and_coa(self.chart_of_accounts)
self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4)
create_parties_and_addresses(self.parties, self.addresses)
self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4)
create_items_uoms(self.items, self.uoms)
self.publish("Import Master Data", _("Done"), 4, 4)
self.set_account_defaults()
self.is_master_data_imported = 1
frappe.db.commit()
except Exception:
self.publish("Import Master Data", _("Process Failed"), -1, 5)
frappe.db.rollback()
self.log()
finally:
self.set_status()
def _process_day_book_data(self):
def get_vouchers(collection):
vouchers = []
for voucher in collection.find_all("VOUCHER"):
if voucher.ISCANCELLED.string.strip() == "Yes":
continue
inventory_entries = (
voucher.find_all("INVENTORYENTRIES.LIST")
+ voucher.find_all("ALLINVENTORYENTRIES.LIST")
+ voucher.find_all("INVENTORYENTRIESIN.LIST")
+ voucher.find_all("INVENTORYENTRIESOUT.LIST")
)
if (
voucher.VOUCHERTYPENAME.string.strip() not in ["Journal", "Receipt", "Payment", "Contra"]
and inventory_entries
):
function = voucher_to_invoice
else:
function = voucher_to_journal_entry
try:
processed_voucher = function(voucher)
if processed_voucher:
vouchers.append(processed_voucher)
frappe.db.commit()
except Exception:
frappe.db.rollback()
self.log(voucher)
return vouchers
def voucher_to_journal_entry(voucher):
accounts = []
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all(
"LEDGERENTRIES.LIST"
)
for entry in ledger_entries:
account = {
"account": encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company),
"cost_center": self.default_cost_center,
}
if entry.ISPARTYLEDGER.string.strip() == "Yes":
party_details = get_party(entry.LEDGERNAME.string.strip())
if party_details:
party_type, party_account = party_details
account["party_type"] = party_type
account["account"] = party_account
account["party"] = entry.LEDGERNAME.string.strip()
amount = Decimal(entry.AMOUNT.string.strip())
if amount > 0:
account["credit_in_account_currency"] = str(abs(amount))
else:
account["debit_in_account_currency"] = str(abs(amount))
accounts.append(account)
journal_entry = {
"doctype": "Journal Entry",
"tally_guid": voucher.GUID.string.strip(),
"tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "",
"posting_date": voucher.DATE.string.strip(),
"company": self.erpnext_company,
"accounts": accounts,
}
return journal_entry
def voucher_to_invoice(voucher):
if voucher.VOUCHERTYPENAME.string.strip() in ["Sales", "Credit Note"]:
doctype = "Sales Invoice"
party_field = "customer"
account_field = "debit_to"
account_name = encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
price_list_field = "selling_price_list"
elif voucher.VOUCHERTYPENAME.string.strip() in ["Purchase", "Debit Note"]:
doctype = "Purchase Invoice"
party_field = "supplier"
account_field = "credit_to"
account_name = encode_company_abbr(self.tally_creditors_account, self.erpnext_company)
price_list_field = "buying_price_list"
else:
# Do not handle vouchers other than "Purchase", "Debit Note", "Sales" and "Credit Note"
# Do not handle Custom Vouchers either
return
invoice = {
"doctype": doctype,
party_field: voucher.PARTYNAME.string.strip(),
"tally_guid": voucher.GUID.string.strip(),
"tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "",
"posting_date": voucher.DATE.string.strip(),
"due_date": voucher.DATE.string.strip(),
"items": get_voucher_items(voucher, doctype),
"taxes": get_voucher_taxes(voucher),
account_field: account_name,
price_list_field: "Tally Price List",
"set_posting_time": 1,
"disable_rounded_total": 1,
"company": self.erpnext_company,
}
return invoice
def get_voucher_items(voucher, doctype):
inventory_entries = (
voucher.find_all("INVENTORYENTRIES.LIST")
+ voucher.find_all("ALLINVENTORYENTRIES.LIST")
+ voucher.find_all("INVENTORYENTRIESIN.LIST")
+ voucher.find_all("INVENTORYENTRIESOUT.LIST")
)
if doctype == "Sales Invoice":
account_field = "income_account"
elif doctype == "Purchase Invoice":
account_field = "expense_account"
items = []
for entry in inventory_entries:
qty, uom = entry.ACTUALQTY.string.strip().split()
items.append(
{
"item_code": entry.STOCKITEMNAME.string.strip(),
"description": entry.STOCKITEMNAME.string.strip(),
"qty": qty.strip(),
"uom": uom.strip(),
"conversion_factor": 1,
"price_list_rate": entry.RATE.string.strip().split("/")[0],
"cost_center": self.default_cost_center,
"warehouse": self.default_warehouse,
account_field: encode_company_abbr(
entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string.strip(),
self.erpnext_company,
),
}
)
return items
def get_voucher_taxes(voucher):
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all(
"LEDGERENTRIES.LIST"
)
taxes = []
for entry in ledger_entries:
if entry.ISPARTYLEDGER.string.strip() == "No":
tax_account = encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company)
taxes.append(
{
"charge_type": "Actual",
"account_head": tax_account,
"description": tax_account,
"tax_amount": entry.AMOUNT.string.strip(),
"cost_center": self.default_cost_center,
}
)
return taxes
def get_party(party):
if frappe.db.exists({"doctype": "Supplier", "supplier_name": party}):
return "Supplier", encode_company_abbr(self.tally_creditors_account, self.erpnext_company)
elif frappe.db.exists({"doctype": "Customer", "customer_name": party}):
return "Customer", encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
try:
self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3)
collection = self.get_collection(self.day_book_data)
self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3)
vouchers = get_vouchers(collection)
self.publish("Process Day Book Data", _("Done"), 3, 3)
self.dump_processed_data({"vouchers": vouchers})
self.is_day_book_data_processed = 1
except Exception:
self.publish("Process Day Book Data", _("Process Failed"), -1, 5)
self.log()
finally:
self.set_status()
def _import_day_book_data(self):
def create_fiscal_years(vouchers):
from frappe.utils.data import add_years, getdate
earliest_date = getdate(min(voucher["posting_date"] for voucher in vouchers))
oldest_year = frappe.get_all(
"Fiscal Year", fields=["year_start_date", "year_end_date"], order_by="year_start_date"
)[0]
while earliest_date < oldest_year.year_start_date:
new_year = frappe.get_doc({"doctype": "Fiscal Year"})
new_year.year_start_date = add_years(oldest_year.year_start_date, -1)
new_year.year_end_date = add_years(oldest_year.year_end_date, -1)
if new_year.year_start_date.year == new_year.year_end_date.year:
new_year.year = new_year.year_start_date.year
else:
new_year.year = f"{new_year.year_start_date.year}-{new_year.year_end_date.year}"
new_year.save()
oldest_year = new_year
def create_custom_fields():
_create_custom_fields(
{
("Journal Entry", "Purchase Invoice", "Sales Invoice"): [
{
"fieldtype": "Data",
"fieldname": "tally_guid",
"read_only": 1,
"label": "Tally GUID",
},
{
"fieldtype": "Data",
"fieldname": "tally_voucher_no",
"read_only": 1,
"label": "Tally Voucher Number",
},
]
}
)
def create_price_list():
frappe.get_doc(
{
"doctype": "Price List",
"price_list_name": "Tally Price List",
"selling": 1,
"buying": 1,
"enabled": 1,
"currency": "INR",
}
).insert()
try:
frappe.db.set_value(
"Account",
encode_company_abbr(self.tally_creditors_account, self.erpnext_company),
"account_type",
"Payable",
)
frappe.db.set_value(
"Account",
encode_company_abbr(self.tally_debtors_account, self.erpnext_company),
"account_type",
"Receivable",
)
frappe.db.set_value(
"Company", self.erpnext_company, "round_off_account", self.default_round_off_account
)
vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
vouchers = json.loads(vouchers_file.get_content())
create_fiscal_years(vouchers)
create_price_list()
create_custom_fields()
total = len(vouchers)
is_last = False
for index in range(0, total, VOUCHER_CHUNK_SIZE):
if index + VOUCHER_CHUNK_SIZE >= total:
is_last = True
frappe.enqueue_doc(
self.doctype,
self.name,
"_import_vouchers",
queue="long",
timeout=3600,
start=index + 1,
total=total,
is_last=is_last,
)
except Exception:
self.log()
finally:
self.set_status()
def _import_vouchers(self, start, total, is_last=False):
frappe.flags.in_migrate = True
vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
vouchers = json.loads(vouchers_file.get_content())
chunk = vouchers[start : start + VOUCHER_CHUNK_SIZE]
for index, voucher in enumerate(chunk, start=start):
try:
voucher_doc = frappe.get_doc(voucher)
voucher_doc.insert()
voucher_doc.submit()
self.publish("Importing Vouchers", _("{} of {}").format(index, total), index, total)
frappe.db.commit()
except Exception:
frappe.db.rollback()
self.log(voucher_doc)
if is_last:
self.status = ""
self.is_day_book_data_imported = 1
self.save()
frappe.db.set_value("Price List", "Tally Price List", "enabled", 0)
frappe.flags.in_migrate = False
@frappe.whitelist()
def process_master_data(self):
self.set_status("Processing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600)
@frappe.whitelist()
def import_master_data(self):
self.set_status("Importing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600)
@frappe.whitelist()
def process_day_book_data(self):
self.set_status("Processing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600)
@frappe.whitelist()
def import_day_book_data(self):
self.set_status("Importing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600)
def log(self, data=None):
if isinstance(data, frappe.model.document.Document):
if sys.exc_info()[1].__class__ != frappe.DuplicateEntryError:
failed_import_log = json.loads(self.failed_import_log)
doc = data.as_dict()
failed_import_log.append({"doc": doc, "exc": traceback.format_exc()})
self.failed_import_log = json.dumps(failed_import_log, separators=(",", ":"))
self.save()
frappe.db.commit()
else:
data = data or self.status
message = "\n".join(
[
"Data:",
json.dumps(data, default=str, indent=4),
"--" * 50,
"\nException:",
traceback.format_exc(),
]
)
return frappe.log_error(title="Tally Migration Error", message=message)
def set_status(self, status=""):
self.status = status
self.save()

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestTallyMigration(unittest.TestCase):
pass

View File

@@ -351,7 +351,10 @@ doc_events = {
"erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
"erpnext.support.doctype.issue.issue.set_first_response_time",
],
"after_insert": "erpnext.crm.utils.link_communications_with_prospect",
"after_insert": [
"erpnext.crm.utils.link_communications_with_prospect",
"erpnext.crm.utils.update_modified_timestamp",
],
},
"Event": {
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
@@ -412,7 +415,6 @@ scheduler_events = {
"cron": {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
"erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",

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