Compare commits

..

107 Commits

Author SHA1 Message Date
Frappe PR Bot
259f3422d5 chore(release): Bumped to Version 14.34.0
# [14.34.0](https://github.com/frappe/erpnext/compare/v14.33.2...v14.34.0) (2023-08-09)

### Bug Fixes

* **accounts:** Translate columns in AP/AR report ([#36503](https://github.com/frappe/erpnext/issues/36503)) ([6739369](67393694de))
* AP and AR summary ([769d7d7](769d7d7554))
* check root type only when not none ([46bb309](46bb309b8a))
* cross connect delivery note and sales invoice ([#36183](https://github.com/frappe/erpnext/issues/36183)) ([8501a11](8501a1182a))
* Debit credit difference while submitting Sales Invoice ([#36523](https://github.com/frappe/erpnext/issues/36523)) ([240d866](240d866ef4))
* don't allow negative rate (backport [#36027](https://github.com/frappe/erpnext/issues/36027)) ([#36465](https://github.com/frappe/erpnext/issues/36465)) ([caa4f33](caa4f33169))
* enqueue submit/cancel action for stock entry having more than 50 line items (backport [#36532](https://github.com/frappe/erpnext/issues/36532)) ([#36536](https://github.com/frappe/erpnext/issues/36536)) ([9c108a8](9c108a8ef7))
* fetch ple for all party types ([674dba8](674dba8cd7))
* fetch ple with party type employee in AP ([1ca9aca](1ca9aca0d5))
* Fix query for financial statement report ([d1590f2](d1590f266b))
* get incoming rate instead of BOM rate (backport [#36496](https://github.com/frappe/erpnext/issues/36496)) ([#36506](https://github.com/frappe/erpnext/issues/36506)) ([bdfbccd](bdfbccd38e))
* handle None value in payment_term_outstanding ([b033b3b](b033b3b0d6))
* Lower deduction certificate for multi-company ([#36491](https://github.com/frappe/erpnext/issues/36491)) ([2216875](2216875bd6))
* payment allocation in invoice payment schedule ([#36440](https://github.com/frappe/erpnext/issues/36440)) ([0e87c86](0e87c86aab))
* search not working for so in the Production Plan ([#36459](https://github.com/frappe/erpnext/issues/36459)) ([8c57d56](8c57d56240))
* serial no not able to reject for the internal transfer ([#36467](https://github.com/frappe/erpnext/issues/36467)) ([c1819a4](c1819a4b21))
* stock entry decimal issue (backport [#36530](https://github.com/frappe/erpnext/issues/36530)) ([#36533](https://github.com/frappe/erpnext/issues/36533)) ([5b04708](5b04708164))
* stock reconciliation negative stock error (backport [#36544](https://github.com/frappe/erpnext/issues/36544)) ([#36549](https://github.com/frappe/erpnext/issues/36549)) ([00b9df0](00b9df0bc5))
* Tax withholding against order via Payment Entry ([#36493](https://github.com/frappe/erpnext/issues/36493)) ([a234b89](a234b8932e))
* use correct lang separator for frappe (backport [#36519](https://github.com/frappe/erpnext/issues/36519)) ([#36520](https://github.com/frappe/erpnext/issues/36520)) ([f9981d1](f9981d1ff3))
* **ux:** add `Ordered Qty` column in Get Items From > MR (backport [#36486](https://github.com/frappe/erpnext/issues/36486)) ([#36505](https://github.com/frappe/erpnext/issues/36505)) ([0d7a4b6](0d7a4b6ff6))

### Features

* ledger comparison report ([#36485](https://github.com/frappe/erpnext/issues/36485)) ([07f235c](07f235cf7d))
* **RFQ:** make sending attachments configurable (backport [#36359](https://github.com/frappe/erpnext/issues/36359)) ([#36535](https://github.com/frappe/erpnext/issues/36535)) ([5881960](5881960001))

### Performance Improvements

* asset depreciation entry posting ([#36461](https://github.com/frappe/erpnext/issues/36461)) ([cd1c175](cd1c175439))
* defer holiday list imports ([7adad42](7adad4272a))
2023-08-09 03:06:43 +00:00
Deepesh Garg
4f0bb5e643 Merge pull request #36546 from frappe/version-14-hotfix
chore: release v14
2023-08-09 08:35:06 +05:30
mergify[bot]
240d866ef4 fix: Debit credit difference while submitting Sales Invoice (#36523)
* fix: Debit credit difference while submitting Sales Invoice (#36523)

* fix: Debit credit difference while submitting Sales Invoice

* test(fix): Update gl entry comparison

* test(fix): Update gl entry comparison

(cherry picked from commit 492ea3bcc8)

# Conflicts:
#	erpnext/controllers/accounts_controller.py

* chore: resolve conflicts

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-08-09 00:04:47 +05:30
mergify[bot]
0e87c86aab fix: payment allocation in invoice payment schedule (#36440)
* fix: payment allocation in invoice payment schedule (#36440)

* fix: payment allocation in invoice payment schedule

* test: payment allocation for payment terms

* chore: linting issues

(cherry picked from commit edbefee10c)

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

* chore: resolve conflicts

---------

Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-08-09 00:04:08 +05:30
mergify[bot]
00b9df0bc5 fix: stock reconciliation negative stock error (backport #36544) (#36549)
fix: stock reconciliation negative stock error (#36544)

fix: stock reco negative stock error
(cherry picked from commit 0b36e7d10e)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-08-08 16:04:52 +05:30
Anand Baburajan
cd1c175439 perf: asset depreciation entry posting (#36461)
* perf: make post depr entries job daily_long

* perf: optimise post_depreciation_entries and make_depreciation_entry

* chore: more optimisation and dont fail all schedule dates if one date fails

* chore: don't post entries before acc_frozen_upto

* chore: using get_single_value

* refactor: destructure asset object
2023-08-08 15:09:55 +05:30
ruthra kumar
2ca2e67812 Merge pull request #36543 from frappe/mergify/bp/version-14-hotfix/pr-36528
refactor: use base_tax_withholding_net_total for treshold validation (backport #36528)
2023-08-08 14:41:53 +05:30
ruthra kumar
c26a52d791 refactor: use base_tax_withholding_net_total for treshold validation (#36528)
* refactor: use base_tax_withholding_net_total for treshold validation

* fix: only for non payment entry doctypes

(cherry picked from commit 11d5327d1b)
2023-08-08 08:45:22 +00:00
mergify[bot]
5881960001 feat(RFQ): make sending attachments configurable (backport #36359) (#36535)
* feat(RFQ): make sending attachments configurable (#36359)

(cherry picked from commit 8cc3df7c2c)

# Conflicts:
#	erpnext/buying/doctype/request_for_quotation/request_for_quotation.json

* chore: resolve conflicts

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-08-07 21:17:12 +05:30
mergify[bot]
9c108a8ef7 fix: enqueue submit/cancel action for stock entry having more than 50 line items (backport #36532) (#36536)
fix: enqueue submit/cancel action for stock entry having more than 50 line items (#36532)

(cherry picked from commit ecba6ee183)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-08-07 21:03:36 +05:30
mergify[bot]
5b04708164 fix: stock entry decimal issue (backport #36530) (#36533)
fix: stock entry decimal issue (#36530)

(cherry picked from commit 28dfc88789)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-08-07 19:11:19 +05:30
ruthra kumar
d76c2c5738 Merge pull request #36521 from frappe/mergify/bp/version-14-hotfix/pr-36485
feat: ledger comparison report (backport #36485)
2023-08-07 11:52:28 +05:30
ruthra kumar
07f235cf7d feat: ledger comparison report (#36485)
* feat: Accounting Ledger comparison report

* chore: barebones methods

* chore: working state

* chore: refactor internal logic

* chore: working multi select filter on Account

* chore: working voucher no filter

* chore: remove debugging statements

* chore: report with currency symbol

* chore: working start and end date filter

* test: basic report function

* refactor(test): test all filters

(cherry picked from commit b86747c9d4)
2023-08-07 06:00:24 +00:00
Ankush Menat
7adad4272a perf: defer holiday list imports
Only used for configuring but loaded whenever
get_doc("holiday list", ...) is done

(cherry picked from commit 2eea90a873)
2023-08-07 10:09:49 +05:30
mergify[bot]
f9981d1ff3 fix: use correct lang separator for frappe (backport #36519) (#36520)
* fix: use correct lang separator for frappe

(cherry picked from commit 0218ca538f)

* perf: defer babel import

Only required when configuring but will get loaded everywhere

(cherry picked from commit f574ac11ea)

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-08-07 10:08:59 +05:30
mergify[bot]
8770aa5955 chore: don't merge asset capitalization gl entries (backport #36514) (#36516)
chore: don't merge asset capitalization gl entries (#36514)

(cherry picked from commit 2d7d86039a)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-08-07 00:20:53 +05:30
Anand Baburajan
2d7d86039a chore: don't merge asset capitalization gl entries (#36514) 2023-08-06 23:34:02 +05:30
mergify[bot]
bdfbccd38e fix: get incoming rate instead of BOM rate (backport #36496) (#36506)
* fix: get incoming rate instead of BOM rate (#36496)

* fix: get incoming rate instead of BOM rate

* test: add test case for SCR rm rate

(cherry picked from commit 758b31d895)

# Conflicts:
#	erpnext/controllers/subcontracting_controller.py

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-08-06 14:51:23 +05:30
mergify[bot]
a234b8932e fix: Tax withholding against order via Payment Entry (#36493)
fix: Tax withholding against order via Payment Entry (#36493)

* fix: Tax withholding against order via Payment Entry

* test: Add test case

* fix: Nonetype exceptions

(cherry picked from commit 93767eb7fc)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-08-05 22:58:55 +05:30
mergify[bot]
2216875bd6 fix: Lower deduction certificate for multi-company (#36491)
fix: Lower deduction certificate for multi-company (#36491)

(cherry picked from commit 96035b87d5)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-08-05 22:44:10 +05:30
mergify[bot]
67393694de fix(accounts): Translate columns in AP/AR report (#36503)
fix(accounts): Translate columns in AP/AR report (#36503)

(cherry picked from commit 559d914c0b)

Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com>
2023-08-05 22:43:44 +05:30
mergify[bot]
0d7a4b6ff6 fix(ux): add Ordered Qty column in Get Items From > MR (backport #36486) (#36505)
fix(ux): add `Ordered Qty` column in Get Items From > MR (#36486)

(cherry picked from commit e179499764)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-08-05 19:31:55 +05:30
Deepesh Garg
e0fb398be1 Merge pull request #36488 from frappe/mergify/bp/version-14-hotfix/pr-36333
fix: AP report does not show expense claim payables (#36333)
2023-08-04 17:52:52 +05:30
mergify[bot]
cf2a3e2fc5 Contact Doctype don't have any field job_title. (#36156)
fix: Contact Doctype doesn't have any field called `job_title`

fix: Contact Doctype doesn't have any field called `job_title`
(cherry picked from commit 49be740736)

Co-authored-by: Sumit Jain <59503001+sumitjain236@users.noreply.github.com>
2023-08-04 17:52:29 +05:30
Gursheen Anand
a79b30e45f refactor: future payments query
(cherry picked from commit f5761e7965)
2023-08-04 12:14:04 +00:00
Gursheen Anand
769d7d7554 fix: AP and AR summary
(cherry picked from commit e355dea4b5)
2023-08-04 12:14:03 +00:00
Gursheen Anand
674dba8cd7 fix: fetch ple for all party types
(cherry picked from commit fd5c4e0a64)
2023-08-04 12:14:03 +00:00
Gursheen Anand
1ca9aca0d5 fix: fetch ple with party type employee in AP
(cherry picked from commit c47a37c3ab)
2023-08-04 12:14:02 +00:00
Deepesh Garg
8e8e59cecb Merge pull request #36484 from frappe/mergify/bp/version-14-hotfix/pr-36458
test: balance sheet report  (backport #36458)
2023-08-04 10:59:11 +05:30
Gursheen Anand
47d0e76999 test: balance sheet report
(cherry picked from commit 002bf77314)
2023-08-04 04:26:17 +00:00
Gursheen Anand
46bb309b8a fix: check root type only when not none
(cherry picked from commit cd98be6088)
2023-08-04 04:26:17 +00:00
mergify[bot]
dfd356a174 chore: better cost center validation for assets (backport #36477) (#36479)
chore: better cost center validation for assets (#36477)

(cherry picked from commit 38a612c62e)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-08-03 17:18:56 +05:30
Frappe PR Bot
2d43ba0c90 chore(release): Bumped to Version 14.33.2
## [14.33.2](https://github.com/frappe/erpnext/compare/v14.33.1...v14.33.2) (2023-08-03)

### Bug Fixes

* serial no not able to reject for the internal transfer (backport [#36467](https://github.com/frappe/erpnext/issues/36467)) ([#36475](https://github.com/frappe/erpnext/issues/36475)) ([a09777c](a09777c406))
2023-08-03 07:48:49 +00:00
mergify[bot]
a09777c406 fix: serial no not able to reject for the internal transfer (backport #36467) (#36475)
fix: serial no not able to reject for the internal transfer (#36467)

(cherry picked from commit c1819a4b21)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-08-03 13:16:55 +05:30
rohitwaghchaure
c1819a4b21 fix: serial no not able to reject for the internal transfer (#36467) 2023-08-03 12:19:25 +05:30
mergify[bot]
caa4f33169 fix: don't allow negative rate (backport #36027) (#36465)
* fix: don't allow negative rates (#36027)

* fix: don't allow negative rate

* test: don't allow negative rate

* fix: only check for -rate on items child table

(cherry picked from commit dedf24b86d)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py

* chore: resolve merge conflict

---------

Co-authored-by: Devin Slauenwhite <devin.slauenwhite@gmail.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2023-08-02 17:13:22 +05:30
ruthra kumar
e6dca0668c Merge pull request #36464 from frappe/mergify/bp/version-14-hotfix/pr-36455
fix: handle None value in payment_term_outstanding (backport #36455)
2023-08-02 16:54:38 +05:30
Husam Hammad
b033b3b0d6 fix: handle None value in payment_term_outstanding
* Fix payment entry bug: Handle None value in payment_term_outstanding

* fix: Handle None value in payment_term_outstanding V2

fix linting issue

(cherry picked from commit 27ebf14f9d)
2023-08-02 10:59:43 +00:00
Frappe PR Bot
75012c17d4 chore(release): Bumped to Version 14.33.1
## [14.33.1](https://github.com/frappe/erpnext/compare/v14.33.0...v14.33.1) (2023-08-02)

### Bug Fixes

* search not working for so in the Production Plan (backport [#36459](https://github.com/frappe/erpnext/issues/36459)) ([#36460](https://github.com/frappe/erpnext/issues/36460)) ([4605bc5](4605bc51ae))
2023-08-02 07:31:21 +00:00
mergify[bot]
4605bc51ae fix: search not working for so in the Production Plan (backport #36459) (#36460)
fix: search not working for so in the Production Plan (#36459)

fix: search not working for so
(cherry picked from commit 8c57d56240)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-08-02 12:59:31 +05:30
rohitwaghchaure
8c57d56240 fix: search not working for so in the Production Plan (#36459)
fix: search not working for so
2023-08-02 12:47:12 +05:30
Anand Baburajan
8501a1182a fix: cross connect delivery note and sales invoice (#36183)
* fix: cross connect delivery note and sales invoice

* chore: remove unnecessary non_standard_fieldname
2023-08-02 09:07:04 +05:30
Deepesh Garg
fb32120e36 Merge pull request #36454 from frappe/mergify/bp/version-14-hotfix/pr-36450
fix: Fix query for financial statement report (backport #36450)
2023-08-01 23:39:44 +05:30
Corentin Flr
d1590f266b fix: Fix query for financial statement report
(cherry picked from commit bd3fc7c434)
2023-08-01 18:07:16 +00:00
Frappe PR Bot
710ec18086 chore(release): Bumped to Version 14.33.0
# [14.33.0](https://github.com/frappe/erpnext/compare/v14.32.1...v14.33.0) (2023-08-01)

### Bug Fixes

* allow fully depreciated existing assets ([#36378](https://github.com/frappe/erpnext/issues/36378)) ([43b85c5](43b85c5711))
* change fieldtype from Currency to Float for the valuation rate in the stock report ([93bd4c7](93bd4c7ff3))
* cost center filter for fetching payments ([c9daa85](c9daa85985))
* Defined "Open" Status as default (backport [#36421](https://github.com/frappe/erpnext/issues/36421)) ([#36424](https://github.com/frappe/erpnext/issues/36424)) ([33a9477](33a947726d))
* filter by cost center in trial balance ([02428b4](02428b446d))
* german translations ([9c8e2a3](9c8e2a33e9))
* GL Entries should not be splitted based on cost center allocation in PCV ([ade13e6](ade13e6d36))
* group item reorder by (warehouse, material_request_type) (backport [#35818](https://github.com/frappe/erpnext/issues/35818)) ([#36425](https://github.com/frappe/erpnext/issues/36425)) ([516191b](516191bf2b))
* Ignore account closing balance for financial statement ([800417e](800417eeed))
* in payment_entry 'Unallocated Amount' cal is broken ([#36369](https://github.com/frappe/erpnext/issues/36369)) ([cd3b85c](cd3b85ccff))
* incorrect `idx` on JE's after reconciliation ([80eb875](80eb8754db))
* incorrect qty set in the serial no picker ([57b19a5](57b19a523e))
* incorrect usage `get_cached_value` on single doctypes ([8f72a68](8f72a6814b))
* **Item Group:** allow root deletion ([1a6be5e](1a6be5e19b))
* job card suggest holiday as start date (backport [#35958](https://github.com/frappe/erpnext/issues/35958)) ([#36423](https://github.com/frappe/erpnext/issues/36423)) ([29ddd26](29ddd26ba1))
* Job Card validation fixed when displaying total completed quantity ([7b3bcd3](7b3bcd3bc4))
* multiple issues related to Production Plan (backport [#36377](https://github.com/frappe/erpnext/issues/36377)) ([#36381](https://github.com/frappe/erpnext/issues/36381)) ([a799e1b](a799e1b217))
* not able to make material request (backport [#36416](https://github.com/frappe/erpnext/issues/36416)) ([#36426](https://github.com/frappe/erpnext/issues/36426)) ([99e7406](99e7406b5b))
* only publish repost progress to doc subscriber (backport [#36400](https://github.com/frappe/erpnext/issues/36400)) ([#36402](https://github.com/frappe/erpnext/issues/36402)) ([32bdb7c](32bdb7cccd))
* overallocation validation misfire on normal invoices ([#36349](https://github.com/frappe/erpnext/issues/36349)) ([09af485](09af485d54))
* paid_amount when the group is mode of payment ([50ef358](50ef35845a))
* process_owner is not link User (backport [#36420](https://github.com/frappe/erpnext/issues/36420)) ([#36422](https://github.com/frappe/erpnext/issues/36422)) ([289dc36](289dc36696))
* removed "fetch_from" (backport [#36365](https://github.com/frappe/erpnext/issues/36365)) ([#36386](https://github.com/frappe/erpnext/issues/36386)) ([6d051f5](6d051f5732))
* root type in account map for balance sheet ([#36303](https://github.com/frappe/erpnext/issues/36303)) ([5b56296](5b562961e3))
* show invoices name instead of object address ([e802f0c](e802f0c352))
* show tds & tcs separately ([619b0fe](619b0feb5f))
* test with None conditon in PE ([479cab0](479cab0336))
* typo in loyalty program throw message ([#36432](https://github.com/frappe/erpnext/issues/36432)) ([782a4d1](782a4d1fa8))

### Features

* add party type filter ([32fea64](32fea643cd))

### Performance Improvements

* avoid full table scan in sle count check ([#36428](https://github.com/frappe/erpnext/issues/36428)) ([04f9915](04f9915009))
* move project status reminder to hourly (backport [#36372](https://github.com/frappe/erpnext/issues/36372)) ([#36373](https://github.com/frappe/erpnext/issues/36373)) ([4ac60cd](4ac60cd73b))
* use `LEFT JOIN` instead of `NOT EXISTS` (backport [#36221](https://github.com/frappe/erpnext/issues/36221)) ([#36383](https://github.com/frappe/erpnext/issues/36383)) ([26a0b3b](26a0b3b380))
2023-08-01 18:06:33 +00:00
Deepesh Garg
a9dcf46882 Merge pull request #36442 from frappe/version-14-hotfix
chore: release v14
2023-08-01 23:34:31 +05:30
ruthra kumar
e8f24b8b4e Merge pull request #36437 from frappe/mergify/bp/version-14-hotfix/pr-36349
fix: overallocation validation misfire on normal invoices (backport #36349)
2023-08-01 15:55:42 +05:30
ruthra kumar
fcda6818ed Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-36349 2023-08-01 15:08:06 +05:30
ruthra kumar
f748eb74f1 Merge pull request #36367 from frappe/mergify/bp/version-14-hotfix/pr-36126
fix: incorrect `idx` on Journals after reconciliation (backport #36126)
2023-08-01 15:07:23 +05:30
ruthra kumar
5f677b9629 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-36126 2023-08-01 14:38:52 +05:30
ruthra kumar
d34f1c1776 Merge pull request #36439 from ruthra-kumar/merge_conflict_in_sr_sp_csv
chore: remove merge conflict
2023-08-01 14:31:11 +05:30
ruthra kumar
2dee97aa70 chore: remove merge conflict
remove merge conflict in sr-SP.csv
2023-08-01 14:18:14 +05:30
ruthra kumar
5ac7dca2ec Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-36126 2023-08-01 13:53:39 +05:30
ruthra kumar
ee02cde58d Merge pull request #36438 from ruthra-kumar/remove_merge_conflict_in_v14_hotfix
chore: remove merge conflict in translation file 'tr.csv'
2023-08-01 13:52:02 +05:30
ruthra kumar
14dc40360d chore: remove merge conflict in translation file
Remove merge conflict in v14-hotfix branch
2023-08-01 13:48:03 +05:30
ruthra kumar
09af485d54 fix: overallocation validation misfire on normal invoices (#36349)
* fix: overallocation validation misfire on normal invoices

* test: assert misfire doesn't happen

(cherry picked from commit ab933df5bb)
2023-08-01 07:43:04 +00:00
ruthra kumar
80eb8754db fix: incorrect idx on JE's after reconciliation
(cherry picked from commit 72f577aad2)
2023-08-01 11:49:27 +05:30
Anand Baburajan
43b85c5711 fix: allow fully depreciated existing assets (#36378) 2023-08-01 11:47:48 +05:30
mergify[bot]
04f9915009 perf: avoid full table scan in sle count check (#36428)
perf: avoid full table scan in sle count check (#36428)

(cherry picked from commit f31d07554d)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-08-01 11:20:26 +05:30
mergify[bot]
782a4d1fa8 fix: typo in loyalty program throw message (#36432)
* fix: typo in loyalty program throw message (#36432)

(cherry picked from commit 4f473eb090)

# Conflicts:
#	erpnext/translations/sr-SP.csv
#	erpnext/translations/tr.csv
#	erpnext/translations/zh-TW.csv

* chore: Resolve conflicts

---------

Co-authored-by: abdosaeed95 <118386543+abdosaeed95@users.noreply.github.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-08-01 11:19:38 +05:30
ruthra kumar
07f1b42c64 Merge pull request #36390 from frappe/mergify/bp/version-14-hotfix/pr-36382
refactor(test): introduce and make use of mixins in unit tests (backport #36382)
2023-08-01 11:08:20 +05:30
ruthra kumar
3a024294a9 Merge pull request #36436 from frappe/mergify/bp/version-14-hotfix/pr-36434
fix: incorrect usage `get_cached_value` on single doctypes (backport #36434)
2023-08-01 10:42:03 +05:30
mergify[bot]
5b562961e3 fix: root type in account map for balance sheet (#36303)
* fix: root type in account map for balance sheet (#36303)

* fix: root type in account map

* fix: fetch gle by root type in consolidated financial statement

* refactor: consolidated financial statement gle query

* fix: filter accounts by root type

(cherry picked from commit 11bd15e580)

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

* chore: Resolve conflicts

---------

Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-08-01 10:14:34 +05:30
ruthra kumar
8f72a6814b fix: incorrect usage get_cached_value on single doctypes
(cherry picked from commit ba15810639)
2023-08-01 10:12:55 +05:30
mergify[bot]
99e7406b5b fix: not able to make material request (backport #36416) (#36426)
fix: not able to make material request (#36416)

(cherry picked from commit f83a100a8d)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-07-31 19:43:29 +05:30
mergify[bot]
516191bf2b fix: group item reorder by (warehouse, material_request_type) (backport #35818) (#36425)
fix: group item reorder by (warehouse, material_request_type) (#35818)

* fix: group item reorder by (warehouse, material_request_type)

* fix: update reorder error message

* chore: linter

* fix: correct error message

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>

* chore: linter

---------

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

Co-authored-by: Devin Slauenwhite <devin.slauenwhite@gmail.com>
2023-07-31 18:36:09 +05:30
mergify[bot]
33a947726d fix: Defined "Open" Status as default (backport #36421) (#36424)
fix: Defined "Open" Status as default (#36421)

Defined "Open" Status as default of the child doctype (Quality Review Objective), because without it the main doctype (Quality Review) has "Passed" status.
This happens because in the "set_status" function, the status is updated according to the status of the child records.

(cherry picked from commit 652398fad2)

Co-authored-by: xdlumertz <alexandrelumertz@gmail.com>
2023-07-31 18:32:29 +05:30
mergify[bot]
29ddd26ba1 fix: job card suggest holiday as start date (backport #35958) (#36423)
fix: job card suggest holiday as start date (#35958)

(cherry picked from commit ce36d1f668)

Co-authored-by: Vimal <mailtovimal@gmail.com>
2023-07-31 18:22:44 +05:30
mergify[bot]
26a0b3b380 perf: use LEFT JOIN instead of NOT EXISTS (backport #36221) (#36383)
* perf: use `LEFT JOIN` instead of `NOT EXISTS`

(cherry picked from commit 58d867503b)

# Conflicts:
#	erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py

* fix: long queue `process_boms_cost_level_wise`

(cherry picked from commit 148d466ae5)

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-07-31 17:39:11 +05:30
mergify[bot]
289dc36696 fix: process_owner is not link User (backport #36420) (#36422)
fix: process_owner is not link User (#36420)

-Changed "fetch from" since field is not a binding field
-Change field "full_name" from Hidden to Read Only

(cherry picked from commit 05b07e098a)

Co-authored-by: xdlumertz <alexandrelumertz@gmail.com>
2023-07-31 17:38:02 +05:30
mergify[bot]
cd3b85ccff fix: in payment_entry 'Unallocated Amount' cal is broken (#36369)
fix: in payment_entry 'Unallocated Amount' cal is broken

(cherry picked from commit f9fa34ff40)

Co-authored-by: Ashish Shah <mr.ashish.shah@gmail.com>
2023-07-30 18:43:01 +05:30
Deepesh Garg
09966d10d4 Merge pull request #36409 from frappe/mergify/bp/version-14-hotfix/pr-36320
fix(Supplier): german translations (#36320)
2023-07-30 18:42:33 +05:30
barredterra
9c8e2a33e9 fix: german translations
(cherry picked from commit 3558c3d24e)
2023-07-30 09:11:08 +00:00
mergify[bot]
a799e1b217 fix: multiple issues related to Production Plan (backport #36377) (#36381)
* fix: multiple issues related to Production Plan

(cherry picked from commit 1c2148b637)

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

* chore: fixed conflicts

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2023-07-30 10:56:49 +05:30
rohitwaghchaure
4fad4c3fa4 Merge pull request #36404 from rohitwaghchaure/fixed-incorrect-qty-set-based-on-serial-no
fix: incorrect qty set in the serial no picker
2023-07-30 10:56:04 +05:30
Rohit Waghchaure
57b19a523e fix: incorrect qty set in the serial no picker 2023-07-29 19:42:23 +05:30
rohitwaghchaure
b72232b358 Merge pull request #36401 from frappe/mergify/bp/version-14-hotfix/pr-36375
fix: Job Card validation fixed when displaying total completed quantity (backport #36375)
2023-07-29 19:03:54 +05:30
mergify[bot]
32bdb7cccd fix: only publish repost progress to doc subscriber (backport #36400) (#36402)
* fix: only publish repost progress to doc subscriber (#36400)

Huge size of string gets blasted to everyone on site. Due to some memory
leak (cause unknown) till sockets are open the strings are also in
process' memory.

related https://github.com/frappe/frappe/issues/21863

(cherry picked from commit c0642cf528)

# Conflicts:
#	erpnext/stock/stock_ledger.py

* chore: conflicts

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-07-29 18:14:51 +05:30
ramonalmato
7b3bcd3bc4 fix: Job Card validation fixed when displaying total completed quantity
(cherry picked from commit 49981fecc7)
2023-07-29 09:32:10 +00:00
rohitwaghchaure
cfa07bed74 Merge pull request #36388 from frappe/mergify/bp/version-14-hotfix/pr-36380
fix: change fieldtype from Currency to Float for the valuation rate in reports (backport #36380)
2023-07-29 12:06:37 +05:30
ruthra kumar
2844d849e0 refactor(test): introduce and make use of mixins in unit tests (#36382)
* refactor(test): create and use test mixin

* chore(test): replace get_user_default with variable

(cherry picked from commit 3b58055410)
2023-07-28 15:33:20 +00:00
Rohit Waghchaure
93bd4c7ff3 fix: change fieldtype from Currency to Float for the valuation rate in the stock report
(cherry picked from commit c82cb379a5)
2023-07-28 15:06:59 +00:00
mergify[bot]
6d051f5732 fix: removed "fetch_from" (backport #36365) (#36386)
fix: removed "fetch_from"

* fix: removed ("fetch_from": "goal.objective")
The field ended up being disabled because of this.

(cherry picked from commit 1c687a4afd)

Co-authored-by: xdlumertz <alexandrelumertz@gmail.com>
2023-07-28 18:15:06 +05:30
mergify[bot]
4ac60cd73b perf: move project status reminder to hourly (backport #36372) (#36373)
perf: move project status reminder to hourly (#36372)

Only used for sending daily/weekly/bi-daily

[skip ci]

(cherry picked from commit e36c8ce5be)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-07-28 12:54:39 +05:30
Deepesh Garg
e8b93ef32f Merge pull request #36362 from frappe/mergify/bp/version-14-hotfix/pr-36092
fix: paid_amount when the group is mode of payment (backport #36092)
2023-07-27 22:35:13 +05:30
Deepesh Garg
ddcd2cfe80 Merge pull request #36358 from frappe/mergify/bp/version-14-hotfix/pr-36347
fix: Ignore account closing balance for financial statement (#36347)
2023-07-27 22:34:52 +05:30
Deepesh Garg
e25f4ffa50 Merge pull request #36361 from frappe/mergify/bp/version-14-hotfix/pr-36313
fix(Item Group): allow root deletion (#36313)
2023-07-27 22:24:56 +05:30
HarryPaulo
50ef35845a fix: paid_amount when the group is mode of payment
(cherry picked from commit 2268f7db43)
2023-07-27 15:53:00 +00:00
barredterra
1a6be5e19b fix(Item Group): allow root deletion
It was not possible to delete an empty, unused Item Group without any
children, if it was one of possibly multiple roots of the Item Group tree.
This fix allows deleting a root Item Group.

(cherry picked from commit fd2c272bed)
2023-07-27 15:45:34 +00:00
Deepesh Garg
be94402338 chore: resolve conflicts 2023-07-27 21:12:58 +05:30
Deepesh Garg
800417eeed fix: Ignore account closing balance for financial statement
(cherry picked from commit ccf1920a78)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
2023-07-27 14:25:06 +00:00
Deepesh Garg
dc603c3df0 Merge pull request #36297 from GursheenK/cost-center-check-for-payment-reconciliation
fix: cost center filter for fetching payments
2023-07-27 18:36:12 +05:30
Deepesh Garg
8bf957b9a1 Merge pull request #36342 from frappe/mergify/bp/version-14-hotfix/pr-36327
fix: GL Entries should not be split based on cost center allocation in PCV (#36327)
2023-07-27 12:00:59 +05:30
Nabin Hait
ade13e6d36 fix: GL Entries should not be splitted based on cost center allocation in PCV
(cherry picked from commit 666d961875)
2023-07-27 06:03:56 +00:00
Deepesh Garg
c3c7dd89fb Merge pull request #36321 from frappe/mergify/bp/version-14-hotfix/pr-36318
chore(Item Group): remove redundant autoname (backport #36318)
2023-07-26 22:27:29 +05:30
mergify[bot]
b105ba11b8 chore: adding totals in asset reports (backport #36334) (#36335)
chore: adding totals in asset reports (#36334)

(cherry picked from commit 5e7b05e566)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-07-26 22:25:39 +05:30
Deepesh Garg
faf692d344 Merge pull request #36325 from GursheenK/tds-tcs-payable-monthly-report
fix: show TDS & TCS separately in TDS payable monthly
2023-07-26 16:20:01 +05:30
ruthra kumar
aadd3f9e69 Merge pull request #36323 from frappe/mergify/bp/version-14-hotfix/pr-36298
fix: show invoices name instead of object address (backport #36298)
2023-07-26 12:20:15 +05:30
Deepesh Garg
4b49864995 Merge pull request #36324 from GursheenK/cost-center-filter-in-TB
fix: filter by cost center in trial balance
2023-07-26 11:55:03 +05:30
gouravengineer
e802f0c352 fix: show invoices name instead of object address
comma_and function in expecting a list but it gets a tuple so it is returning a object instead of a string

(cherry picked from commit cf93714a7c)
2023-07-26 11:39:13 +05:30
Gursheen Anand
619b0feb5f fix: show tds & tcs separately 2023-07-26 11:35:02 +05:30
Gursheen Anand
32fea643cd feat: add party type filter 2023-07-26 11:34:27 +05:30
Gursheen Anand
02428b446d fix: filter by cost center in trial balance 2023-07-26 11:04:46 +05:30
Gursheen Anand
479cab0336 fix: test with None conditon in PE 2023-07-26 09:56:48 +05:30
barredterra
f07b87c5eb chore(Item Group): remove redundant autoname
(cherry picked from commit 1691eee26e)
2023-07-26 04:15:22 +00:00
Gursheen Anand
c3b21a6c30 chore: linting issues 2023-07-25 17:04:03 +05:30
Gursheen Anand
c9daa85985 fix: cost center filter for fetching payments 2023-07-25 16:50:46 +05:30
149 changed files with 2840 additions and 1364 deletions

View File

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

View File

@@ -341,7 +341,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
)
accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
def _book_deferred_revenue_or_expense(
item,

View File

@@ -60,6 +60,7 @@
"closing_settings_tab",
"period_closing_settings_section",
"acc_frozen_upto",
"ignore_account_closing_balance",
"column_break_25",
"frozen_accounts_modifier",
"report_settings_sb",
@@ -408,6 +409,13 @@
"fieldname": "enable_fuzzy_matching",
"fieldtype": "Check",
"label": "Enable Fuzzy Matching"
},
{
"default": "0",
"description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ",
"fieldname": "ignore_account_closing_balance",
"fieldtype": "Check",
"label": "Ignore Account Closing Balance"
}
],
"icon": "icon-cog",
@@ -415,7 +423,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-06-15 18:47:46.430291",
"modified": "2023-07-27 15:05:34.000264",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -14,21 +14,32 @@ from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document):
def on_update(self):
frappe.clear_cache()
def validate(self):
frappe.db.set_default(
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
)
old_doc = self.get_doc_before_save()
clear_cache = False
frappe.db.set_default(
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
)
if old_doc.add_taxes_from_item_tax_template != self.add_taxes_from_item_tax_template:
frappe.db.set_default(
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
)
clear_cache = True
if old_doc.enable_common_party_accounting != self.enable_common_party_accounting:
frappe.db.set_default(
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
)
clear_cache = True
self.validate_stale_days()
self.enable_payment_schedule_in_print()
self.validate_pending_reposts()
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
self.enable_payment_schedule_in_print()
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
self.validate_pending_reposts()
if clear_cache:
frappe.clear_cache()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:

View File

@@ -141,7 +141,7 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
)
if points_to_redeem > loyalty_program_details.loyalty_points:
frappe.throw(_("You don't have enought Loyalty Points to redeem"))
frappe.throw(_("You don't have enough Loyalty Points to redeem"))
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)

View File

@@ -894,12 +894,12 @@ frappe.ui.form.on('Payment Entry', {
if(frm.doc.payment_type == "Receive"
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges
unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges)
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
} else if (frm.doc.payment_type == "Pay"
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
&& frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) {
unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions
unallocated_amount = (frm.doc.base_paid_amount + flt(frm.doc.base_total_taxes_and_charges) - (total_deductions
+ frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate;
}
}

View File

@@ -234,12 +234,14 @@ class PaymentEntry(AccountsController):
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
if d.payment_term and (
(flt(d.allocated_amount)) > 0
and flt(d.allocated_amount) > flt(latest.payment_term_outstanding)
if (
d.payment_term
and (
(flt(d.allocated_amount)) > 0
and latest.payment_term_outstanding
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
)
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
):
frappe.throw(
_(
@@ -249,6 +251,9 @@ class PaymentEntry(AccountsController):
)
)
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
# Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
@@ -631,7 +636,9 @@ class PaymentEntry(AccountsController):
if not self.apply_tax_withholding_amount:
return
net_total = self.paid_amount
order_amount = self.get_order_net_total()
net_total = flt(order_amount) + flt(self.unallocated_amount)
# Adding args as purchase invoice to get TDS amount
args = frappe._dict(
@@ -676,6 +683,20 @@ class PaymentEntry(AccountsController):
for d in to_remove:
self.remove(d)
def get_order_net_total(self):
if self.party_type == "Supplier":
doctype = "Purchase Order"
else:
doctype = "Sales Order"
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
tax_withholding_net_total = frappe.db.get_value(
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
)
return tax_withholding_net_total
def apply_taxes(self):
self.initialize_taxes()
self.determine_exclusive_rate()

View File

@@ -1156,6 +1156,52 @@ class TestPaymentEntry(FrappeTestCase):
si3.cancel()
si3.delete()
@change_settings(
"Accounts Settings",
{
"unlink_payment_on_cancellation_of_invoice": 1,
"delete_linked_ledger_entries": 1,
"allow_multi_currency_invoices_against_single_party_account": 1,
},
)
def test_overallocation_validation_shouldnt_misfire(self):
"""
Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled
"""
customer = create_customer()
create_payment_terms_template()
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
template.allocate_payment_based_on_payment_terms = 0
template.save()
# Validate allocation on base/company currency
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
si.payment_terms_template = "Test Receivable Template"
si.save().submit()
si.reload()
pe = get_payment_entry(si.doctype, si.name).save()
# There will no term based allocation
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.references[0].payment_term, None)
self.assertEqual(flt(pe.references[0].allocated_amount), flt(si.grand_total))
pe.save()
# specify a term
pe.references[0].payment_term = template.terms[0].payment_term
# no validation error should be thrown
pe.save()
pe.paid_amount = si.grand_total + 1
pe.references[0].allocated_amount = si.grand_total + 1
self.assertRaises(frappe.ValidationError, pe.save)
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
template.allocate_payment_based_on_payment_terms = 1
template.save()
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -232,7 +232,7 @@ class PurchaseInvoice(BuyingController):
)
if (
cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
and not self.is_return
and not self.is_internal_supplier
):
@@ -976,30 +976,6 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount")
)
def make_precision_loss_gl_entry(self, gl_entries):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
precision_loss = self.get("base_net_total") - flt(
self.get("net_total") * self.conversion_rate, self.precision("net_total")
)
if precision_loss:
gl_entries.append(
self.get_gl_dict(
{
"account": round_off_account,
"against": self.supplier,
"credit": precision_loss,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else self.cost_center or round_off_cost_center,
"remarks": _("Net total calculation precision loss"),
}
)
)
def get_asset_gl_entry(self, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")

View File

@@ -1670,6 +1670,52 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
automatically_fetch_payment_terms()
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
"allocate_payment_based_on_payment_terms",
0,
)
po = create_purchase_order(do_not_save=1)
po.payment_terms_template = "_Test Payment Term Template"
po.save()
po.submit()
pr = create_pr_against_po(po.name, received_qty=4)
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
"allocate_payment_based_on_payment_terms",
1,
)
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
automatically_fetch_payment_terms(enable=0)
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
"allocate_payment_based_on_payment_terms",
0,
)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(

View File

@@ -1075,6 +1075,7 @@ class SalesInvoice(SellingController):
self.make_internal_transfer_gl_entries(gl_entries)
self.make_item_gl_entries(gl_entries)
self.make_precision_loss_gl_entry(gl_entries)
self.make_discount_gl_entries(gl_entries)
# merge gl entries before adding pos entries

View File

@@ -15,6 +15,7 @@ def get_data():
},
"internal_links": {
"Sales Order": ["items", "sales_order"],
"Delivery Note": ["items", "delivery_note"],
"Timesheet": ["timesheets", "time_sheet"],
},
"transactions": [

View File

@@ -2049,28 +2049,27 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.total_taxes_and_charges, 228.82)
self.assertEqual(si.rounding_adjustment, -0.01)
expected_values = dict(
(d[0], d)
for d in [
[si.debit_to, 1500, 0.0],
["_Test Account Service Tax - _TC", 0.0, 114.41],
["_Test Account VAT - _TC", 0.0, 114.41],
["Sales - _TC", 0.0, 1271.18],
]
)
expected_values = [
["_Test Account Service Tax - _TC", 0.0, 114.41],
["_Test Account VAT - _TC", 0.0, 114.41],
[si.debit_to, 1500, 0.0],
["Round Off - _TC", 0.01, 0.01],
["Sales - _TC", 0.0, 1271.18],
]
gl_entries = frappe.db.sql(
"""select account, debit, credit
"""select account, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
group by account
order by account asc""",
si.name,
as_dict=1,
)
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.account)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
def test_rounding_adjustment_3(self):
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
@@ -2125,13 +2124,14 @@ class TestSalesInvoice(unittest.TestCase):
["_Test Account Service Tax - _TC", 0.0, 240.43],
["_Test Account VAT - _TC", 0.0, 240.43],
["Sales - _TC", 0.0, 4007.15],
["Round Off - _TC", 0.01, 0],
["Round Off - _TC", 0.02, 0.01],
]
)
gl_entries = frappe.db.sql(
"""select account, debit, credit
"""select account, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
group by account
order by account asc""",
si.name,
as_dict=1,
@@ -3316,6 +3316,13 @@ class TestSalesInvoice(unittest.TestCase):
)
self.assertRaises(frappe.ValidationError, si.submit)
def test_sales_return_negative_rate(self):
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
self.assertRaises(frappe.ValidationError, si.save)
si.items[0].rate = 10
si.save()
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()

View File

@@ -476,7 +476,12 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
if (threshold and inv.tax_withholding_net_total >= threshold) or (
if inv.doctype != "Payment Entry":
tax_withholding_net_total = inv.base_tax_withholding_net_total
else:
tax_withholding_net_total = inv.tax_withholding_net_total
if (threshold and tax_withholding_net_total >= threshold) or (
cumulative_threshold and supp_credit_amt >= cumulative_threshold
):
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(

View File

@@ -321,6 +321,42 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in reversed(orders):
d.cancel()
def test_tds_deduction_for_po_via_payment_entry(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.db.set_value(
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
)
order = create_purchase_order(supplier="Test TDS Supplier8", rate=40000, do_not_save=True)
# Add some tax on the order
order.append(
"taxes",
{
"category": "Total",
"charge_type": "Actual",
"account_head": "_Test Account VAT - _TC",
"cost_center": "Main - _TC",
"tax_amount": 8000,
"description": "Test",
"add_deduct_tax": "Add",
},
)
order.save()
order.apply_tds = 1
order.tax_withholding_category = "Cumulative Threshold TDS"
order.submit()
self.assertEqual(order.taxes[0].tax_amount, 4000)
payment = get_payment_entry(order.doctype, order.name)
payment.apply_tax_withholding_amount = 1
payment.tax_withholding_category = "Cumulative Threshold TDS"
payment.submit()
self.assertEqual(payment.taxes[0].tax_amount, 4000)
def test_multi_category_single_supplier(self):
frappe.db.set_value(
"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
@@ -578,6 +614,7 @@ def create_records():
"Test TDS Supplier5",
"Test TDS Supplier6",
"Test TDS Supplier7",
"Test TDS Supplier8",
]:
if frappe.db.exists("Supplier", name):
continue

View File

@@ -105,7 +105,8 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
if not gl_map:
return []
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
if gl_map[0].voucher_type != "Period Closing Voucher":
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
if merge_entries:
gl_map = merge_similar_entries(gl_map, precision)

View File

@@ -14,6 +14,7 @@ from frappe.contacts.doctype.address.address import (
from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Date, Sum
from frappe.utils import (
add_days,
add_months,
@@ -883,32 +884,35 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
def get_partywise_advanced_payment_amount(
party_type, posting_date=None, future_payment=0, company=None, party=None
party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
):
cond = "1=1"
gle = frappe.qb.DocType("GL Entry")
query = (
frappe.qb.from_(gle)
.select(gle.party)
.where(
(gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
)
.groupby(gle.party)
)
if account_type == "Receivable":
query = query.select(Sum(gle.credit).as_("amount"))
else:
query = query.select(Sum(gle.debit).as_("amount"))
if posting_date:
if future_payment:
cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date)
query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
else:
cond = "posting_date <= '{0}'".format(posting_date)
query = query.where(gle.posting_date <= posting_date)
if company:
cond += "and company = {0}".format(frappe.db.escape(company))
query = query.where(gle.company == company)
if party:
cond += "and party = {0}".format(frappe.db.escape(party))
query = query.where(gle.party == party)
data = frappe.db.sql(
""" SELECT party, sum({0}) as amount
FROM `tabGL Entry`
WHERE
party_type = %s and against_voucher is null
and is_cancelled = 0
and {1} GROUP BY party""".format(
("credit") if party_type == "Customer" else "debit", cond
),
party_type,
)
data = query.run(as_dict=True)
if data:
return frappe._dict(data)

View File

@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
def execute(filters=None):
args = {
"party_type": "Supplier",
"account_type": "Payable",
"naming_by": ["Buying Settings", "supp_master_name"],
}
return ReceivablePayableReport(filters).run(args)

View File

@@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum
def execute(filters=None):
args = {
"party_type": "Supplier",
"account_type": "Payable",
"naming_by": ["Buying Settings", "supp_master_name"],
}
return AccountsReceivableSummary(filters).run(args)

View File

@@ -7,7 +7,7 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date
from frappe.query_builder.functions import Date, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -34,7 +34,7 @@ from erpnext.accounts.utils import get_currency_precision
def execute(filters=None):
args = {
"party_type": "Customer",
"account_type": "Receivable",
"naming_by": ["Selling Settings", "cust_master_name"],
}
return ReceivablePayableReport(filters).run(args)
@@ -70,8 +70,11 @@ class ReceivablePayableReport(object):
"Company", self.filters.get("company"), "default_currency"
)
self.currency_precision = get_currency_precision() or 2
self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
self.party_type = self.filters.party_type
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
self.account_type = self.filters.account_type
self.party_type = frappe.db.get_all(
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_details = {}
self.invoices = set()
self.skip_total_row = 0
@@ -197,6 +200,7 @@ class ReceivablePayableReport(object):
# no invoice, this is an invoice / stand-alone payment / credit note
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
row.party_type = ple.party_type
return row
def update_voucher_balance(self, ple):
@@ -207,8 +211,9 @@ class ReceivablePayableReport(object):
return
# amount in "Party Currency", if its supplied. If not, amount in company currency
if self.filters.get(scrub(self.party_type)):
amount = ple.amount_in_account_currency
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
amount = ple.amount_in_account_currency
else:
amount = ple.amount
amount_in_account_currency = ple.amount_in_account_currency
@@ -362,7 +367,7 @@ class ReceivablePayableReport(object):
def get_invoice_details(self):
self.invoice_details = frappe._dict()
if self.party_type == "Customer":
if self.account_type == "Receivable":
si_list = frappe.db.sql(
"""
select name, due_date, po_no
@@ -390,7 +395,7 @@ class ReceivablePayableReport(object):
d.sales_person
)
if self.party_type == "Supplier":
if self.account_type == "Payable":
for pi in frappe.db.sql(
"""
select name, due_date, bill_no, bill_date
@@ -421,8 +426,10 @@ class ReceivablePayableReport(object):
# customer / supplier name
party_details = self.get_party_details(row.party) or {}
row.update(party_details)
if self.filters.get(scrub(self.filters.party_type)):
row.currency = row.account_currency
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
row.currency = row.account_currency
break
else:
row.currency = self.company_currency
@@ -532,65 +539,67 @@ class ReceivablePayableReport(object):
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
def get_future_payments_from_payment_entry(self):
return frappe.db.sql(
"""
select
ref.reference_name as invoice_no,
payment_entry.party,
payment_entry.party_type,
payment_entry.posting_date as future_date,
ref.allocated_amount as future_amount,
payment_entry.reference_no as future_ref
from
`tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref
on
(ref.parent = payment_entry.name)
where
payment_entry.docstatus < 2
and payment_entry.posting_date > %s
and payment_entry.party_type = %s
""",
(self.filters.report_date, self.party_type),
as_dict=1,
)
pe = frappe.qb.DocType("Payment Entry")
pe_ref = frappe.qb.DocType("Payment Entry Reference")
return (
frappe.qb.from_(pe)
.inner_join(pe_ref)
.on(pe_ref.parent == pe.name)
.select(
(pe_ref.reference_name).as_("invoice_no"),
pe.party,
pe.party_type,
(pe.posting_date).as_("future_date"),
(pe_ref.allocated_amount).as_("future_amount"),
(pe.reference_no).as_("future_ref"),
)
.where(
(pe.docstatus < 2)
& (pe.posting_date > self.filters.report_date)
& (pe.party_type.isin(self.party_type))
)
).run(as_dict=True)
def get_future_payments_from_journal_entry(self):
if self.filters.get("party"):
amount_field = (
"jea.debit_in_account_currency - jea.credit_in_account_currency"
if self.party_type == "Supplier"
else "jea.credit_in_account_currency - jea.debit_in_account_currency"
)
else:
amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit"
return frappe.db.sql(
"""
select
jea.reference_name as invoice_no,
je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account")
query = (
frappe.qb.from_(je)
.inner_join(jea)
.on(jea.parent == je.name)
.select(
jea.reference_name.as_("invoice_no"),
jea.party,
jea.party_type,
je.posting_date as future_date,
sum('{0}') as future_amount,
je.cheque_no as future_ref
from
`tabJournal Entry` as je inner join `tabJournal Entry Account` as jea
on
(jea.parent = je.name)
where
je.docstatus < 2
and je.posting_date > %s
and jea.party_type = %s
and jea.reference_name is not null and jea.reference_name != ''
group by je.name, jea.reference_name
having future_amount > 0
""".format(
amount_field
),
(self.filters.report_date, self.party_type),
as_dict=1,
je.posting_date.as_("future_date"),
je.cheque_no.as_("future_ref"),
)
.where(
(je.docstatus < 2)
& (je.posting_date > self.filters.report_date)
& (jea.party_type.isin(self.party_type))
& (jea.reference_name.isnotnull())
& (jea.reference_name != "")
)
)
if self.filters.get("party"):
if self.account_type == "Payable":
query = query.select(
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
)
else:
query = query.select(
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
)
else:
query = query.select(
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
)
query = query.having(qb.Field("future_amount") > 0)
return query.run(as_dict=True)
def allocate_future_payments(self, row):
# future payments are captured in additional columns
# this method allocates pending future payments against a voucher to
@@ -619,13 +628,17 @@ class ReceivablePayableReport(object):
row.future_ref = ", ".join(row.future_ref)
def get_return_entries(self):
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
party_field = scrub(self.filters.party_type)
if self.filters.get(party_field):
filters.update({party_field: self.filters.get(party_field)})
or_filters = {}
for party_type in self.party_type:
party_field = scrub(party_type)
if self.filters.get(party_field):
or_filters.update({party_field: self.filters.get(party_field)})
self.return_entries = frappe._dict(
frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1)
frappe.get_all(
doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1
)
)
def set_ageing(self, row):
@@ -716,6 +729,7 @@ class ReceivablePayableReport(object):
)
.where(ple.delinked == 0)
.where(Criterion.all(self.qb_selection_filter))
.where(Criterion.any(self.or_filters))
)
if self.filters.get("group_by_party"):
@@ -746,16 +760,18 @@ class ReceivablePayableReport(object):
def prepare_conditions(self):
self.qb_selection_filter = []
party_type_field = scrub(self.party_type)
self.qb_selection_filter.append(self.ple.party_type == self.party_type)
self.or_filters = []
for party_type in self.party_type:
party_type_field = scrub(party_type)
self.or_filters.append(self.ple.party_type == party_type)
self.add_common_filters(party_type_field=party_type_field)
self.add_common_filters(party_type_field=party_type_field)
if party_type_field == "customer":
self.add_customer_filters()
if party_type_field == "customer":
self.add_customer_filters()
elif party_type_field == "supplier":
self.add_supplier_filters()
elif party_type_field == "supplier":
self.add_supplier_filters()
if self.filters.cost_center:
self.get_cost_center_conditions()
@@ -784,11 +800,10 @@ class ReceivablePayableReport(object):
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
else:
# get GL with "receivable" or "payable" account_type
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
accounts = [
d.name
for d in frappe.get_all(
"Account", filters={"account_type": account_type, "company": self.filters.company}
"Account", filters={"account_type": self.account_type, "company": self.filters.company}
)
]
@@ -878,7 +893,7 @@ class ReceivablePayableReport(object):
def get_party_details(self, party):
if not party in self.party_details:
if self.party_type == "Customer":
if self.account_type == "Receivable":
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
if self.filters.get("sales_partner"):
@@ -901,14 +916,20 @@ class ReceivablePayableReport(object):
self.columns = []
self.add_column("Posting Date", fieldtype="Date")
self.add_column(
label=_(self.party_type),
label="Party Type",
fieldname="party_type",
fieldtype="Data",
width=100,
)
self.add_column(
label="Party",
fieldname="party",
fieldtype="Link",
options=self.party_type,
fieldtype="Dynamic Link",
options="party_type",
width=180,
)
self.add_column(
label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
label=self.account_type + " Account",
fieldname="party_account",
fieldtype="Link",
options="Account",
@@ -916,13 +937,19 @@ class ReceivablePayableReport(object):
)
if self.party_naming_by == "Naming Series":
if self.account_type == "Payable":
label = "Supplier Name"
fieldname = "supplier_name"
else:
label = "Customer Name"
fieldname = "customer_name"
self.add_column(
_("{0} Name").format(self.party_type),
fieldname=scrub(self.party_type) + "_name",
label=label,
fieldname=fieldname,
fieldtype="Data",
)
if self.party_type == "Customer":
if self.account_type == "Receivable":
self.add_column(
_("Customer Contact"),
fieldname="customer_primary_contact",
@@ -942,7 +969,7 @@ class ReceivablePayableReport(object):
self.add_column(label="Due Date", fieldtype="Date")
if self.party_type == "Supplier":
if self.account_type == "Payable":
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
@@ -952,7 +979,7 @@ class ReceivablePayableReport(object):
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
self.add_column(_("Paid Amount"), fieldname="paid")
if self.party_type == "Customer":
if self.account_type == "Receivable":
self.add_column(_("Credit Note"), fieldname="credit_note")
else:
# note: fieldname is still `credit_note`
@@ -970,7 +997,7 @@ class ReceivablePayableReport(object):
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
if self.filters.party_type == "Customer":
if self.filters.account_type == "Receivable":
self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data")
# comma separated list of linked delivery notes
@@ -991,7 +1018,7 @@ class ReceivablePayableReport(object):
if self.filters.sales_partner:
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
if self.filters.party_type == "Supplier":
if self.filters.account_type == "Payable":
self.add_column(
label=_("Supplier Group"),
fieldname="supplier_group",

View File

@@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
def execute(filters=None):
args = {
"party_type": "Customer",
"account_type": "Receivable",
"naming_by": ["Selling Settings", "cust_master_name"],
}
@@ -21,7 +21,10 @@ def execute(filters=None):
class AccountsReceivableSummary(ReceivablePayableReport):
def run(self, args):
self.party_type = args.get("party_type")
self.account_type = args.get("account_type")
self.party_type = frappe.db.get_all(
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_naming_by = frappe.db.get_value(
args.get("naming_by")[0], None, args.get("naming_by")[1]
)
@@ -35,13 +38,19 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.get_party_total(args)
party = None
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
party = self.filters.get(scrub(party_type))
party_advance_amount = (
get_partywise_advanced_payment_amount(
self.party_type,
self.filters.report_date,
self.filters.show_future_payments,
self.filters.company,
party=self.filters.get(scrub(self.party_type)),
party=party,
account_type=self.account_type,
)
or {}
)
@@ -57,9 +66,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
row.party = party
if self.party_naming_by == "Naming Series":
row.party_name = frappe.get_cached_value(
self.party_type, party, scrub(self.party_type) + "_name"
)
if self.account_type == "Payable":
doctype = "Supplier"
fieldname = "supplier_name"
else:
doctype = "Customer"
fieldname = "customer_name"
row.party_name = frappe.get_cached_value(doctype, party, fieldname)
row.update(party_dict)
@@ -93,6 +106,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# set territory, customer_group, sales person etc
self.set_party_details(d)
self.party_total[d.party].update({"party_type": d.party_type})
def init_party_total(self, row):
self.party_total.setdefault(
@@ -131,17 +145,27 @@ class AccountsReceivableSummary(ReceivablePayableReport):
def get_columns(self):
self.columns = []
self.add_column(
label=_(self.party_type),
label=_("Party Type"),
fieldname="party_type",
fieldtype="Data",
width=100,
)
self.add_column(
label=_("Party"),
fieldname="party",
fieldtype="Link",
options=self.party_type,
fieldtype="Dynamic Link",
options="party_type",
width=180,
)
if self.party_naming_by == "Naming Series":
self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data")
self.add_column(
label=_("Supplier Name") if self.account_type == "Payable" else _("Customer Name"),
fieldname="party_name",
fieldtype="Data",
)
credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note"
credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note"
self.add_column(_("Advance Amount"), fieldname="advance")
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
@@ -159,7 +183,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
if self.party_type == "Customer":
if self.account_type == "Receivable":
self.add_column(
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
)

View File

@@ -1,20 +1,23 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2016-04-08 14:49:58.133098",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:08:26.084484",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset Depreciation Ledger",
"owner": "Administrator",
"ref_doctype": "Asset",
"report_name": "Asset Depreciation Ledger",
"report_type": "Script Report",
"add_total_row": 1,
"columns": [],
"creation": "2016-04-08 14:49:58.133098",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"is_standard": "Yes",
"letterhead": null,
"modified": "2023-07-26 21:05:33.554778",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset Depreciation Ledger",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Asset",
"report_name": "Asset Depreciation Ledger",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"

View File

@@ -1,20 +1,23 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2016-04-08 14:56:37.235981",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:08:18.660476",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset Depreciations and Balances",
"owner": "Administrator",
"ref_doctype": "Asset",
"report_name": "Asset Depreciations and Balances",
"report_type": "Script Report",
"add_total_row": 1,
"columns": [],
"creation": "2016-04-08 14:56:37.235981",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"is_standard": "Yes",
"letterhead": null,
"modified": "2023-07-26 21:04:54.751077",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset Depreciations and Balances",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Asset",
"report_name": "Asset Depreciations and Balances",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"

View File

@@ -0,0 +1,51 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
from erpnext.accounts.report.balance_sheet.balance_sheet import execute
class TestBalanceSheet(FrappeTestCase):
def test_balance_sheet(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice,
make_sales_invoice,
)
from erpnext.accounts.utils import get_fiscal_year
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
pi = make_purchase_invoice(
company="_Test Company 6",
warehouse="Finished Goods - _TC6",
expense_account="Cost of Goods Sold - _TC6",
cost_center="Main - _TC6",
qty=10,
rate=100,
)
si = create_sales_invoice(
company="_Test Company 6",
debit_to="Debtors - _TC6",
income_account="Sales - _TC6",
cost_center="Main - _TC6",
qty=5,
rate=110,
)
filters = frappe._dict(
company="_Test Company 6",
period_start_date=today(),
period_end_date=today(),
periodicity="Yearly",
)
result = execute(filters)[1]
for account_dict in result:
if account_dict.get("account") == "Current Liabilities - _TC6":
self.assertEqual(account_dict.total, 1000)
if account_dict.get("account") == "Current Assets - _TC6":
self.assertEqual(account_dict.total, 550)

View File

@@ -6,6 +6,7 @@ from collections import defaultdict
import frappe
from frappe import _
from frappe.query_builder import Criterion
from frappe.utils import cint, flt, getdate
import erpnext
@@ -364,6 +365,7 @@ def get_data(
accounts_by_name,
accounts,
ignore_closing_entries=False,
root_type=root_type,
)
calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
@@ -608,6 +610,7 @@ def set_gl_entries_by_account(
accounts_by_name,
accounts,
ignore_closing_entries=False,
root_type=None,
):
"""Returns a dict like { "account": [gl entries], ... }"""
@@ -615,7 +618,6 @@ def set_gl_entries_by_account(
"Company", filters.get("company"), ["lft", "rgt"]
)
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters)
companies = frappe.db.sql(
""" select name, default_currency from `tabCompany`
where lft >= %(company_lft)s and rgt <= %(company_rgt)s""",
@@ -631,27 +633,43 @@ def set_gl_entries_by_account(
)
for d in companies:
gl_entries = frappe.db.sql(
"""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
acc.account_name, acc.account_number
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
order by gl.account, gl.posting_date""".format(
additional_conditions=additional_conditions
),
{
"from_date": from_date,
"to_date": to_date,
"lft": root_lft,
"rgt": root_rgt,
"company": d.name,
"finance_book": filters.get("finance_book"),
"company_fb": frappe.db.get_value("Company", d.name, "default_finance_book"),
},
as_dict=True,
gle = frappe.qb.DocType("GL Entry")
account = frappe.qb.DocType("Account")
query = (
frappe.qb.from_(gle)
.inner_join(account)
.on(account.name == gle.account)
.select(
gle.posting_date,
gle.account,
gle.debit,
gle.credit,
gle.is_opening,
gle.company,
gle.fiscal_year,
gle.debit_in_account_currency,
gle.credit_in_account_currency,
gle.account_currency,
account.account_name,
account.account_number,
)
.where(
(gle.company == d.name)
& (gle.is_cancelled == 0)
& (gle.posting_date <= to_date)
& (account.lft >= root_lft)
& (account.rgt <= root_rgt)
)
.orderby(gle.account, gle.posting_date)
)
if root_type:
query = query.where(account.root_type == root_type)
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d)
if additional_conditions:
query = query.where(Criterion.all(additional_conditions))
gl_entries = query.run(as_dict=True)
if filters and filters.get("presentation_currency") != d.default_currency:
currency_info["company"] = d.name
currency_info["company_currency"] = d.default_currency
@@ -721,23 +739,25 @@ def validate_entries(key, entry, accounts_by_name, accounts):
accounts.insert(idx + 1, args)
def get_additional_conditions(from_date, ignore_closing_entries, filters):
def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
gle = frappe.qb.DocType("GL Entry")
additional_conditions = []
if ignore_closing_entries:
additional_conditions.append("ifnull(gl.voucher_type, '')!='Period Closing Voucher'")
additional_conditions.append((gle.voucher_type != "Period Closing Voucher"))
if from_date:
additional_conditions.append("gl.posting_date >= %(from_date)s")
additional_conditions.append(gle.posting_date >= from_date)
finance_book = filters.get("finance_book")
company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book")
if filters.get("include_default_book_entries"):
additional_conditions.append(
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
)
additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
else:
additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
return additional_conditions
def add_total_row(out, root_type, balance_must_be, companies, company_currency):

View File

@@ -2,6 +2,7 @@ import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import nowdate
from erpnext.accounts.doctype.account.test_account import create_account
@@ -10,16 +11,15 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
Deferred_Revenue_and_Expense_Report,
)
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.stock.doctype.item.test_item import create_item
class TestDeferredRevenueAndExpense(unittest.TestCase):
class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
@classmethod
def setUpClass(self):
clear_accounts_and_items()
create_company()
self.maxDiff = None
def clear_old_entries(self):
@@ -51,55 +51,58 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
if deferred_invoices:
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
def test_deferred_revenue(self):
self.clear_old_entries()
def setup_deferred_accounts_and_items(self):
# created deferred expense accounts, if not found
self.deferred_revenue_account = create_account(
account_name="Deferred Revenue",
parent_account="Current Liabilities - " + self.company_abbr,
company=self.company,
)
# created deferred expense accounts, if not found
deferred_revenue_account = create_account(
account_name="Deferred Revenue",
parent_account="Current Liabilities - _CD",
company="_Test Company DR",
self.deferred_expense_account = create_account(
account_name="Deferred Expense",
parent_account="Current Assets - " + self.company_abbr,
company=self.company,
)
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
def setUp(self):
self.create_company()
self.create_customer("_Test Customer")
self.create_supplier("_Test Furniture Supplier")
self.setup_deferred_accounts_and_items()
self.clear_old_entries()
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Customer DR"
customer.type = "Individual"
customer.insert()
def tearDown(self):
frappe.db.rollback()
item = create_item(
"_Test Internet Subscription",
is_stock_item=0,
warehouse="All Warehouses - _CD",
company="_Test Company DR",
)
@change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
def test_deferred_revenue(self):
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
item = frappe.get_doc("Item", self.item)
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_revenue_account
item.deferred_revenue_account = self.deferred_revenue_account
item.no_of_months = 3
item.save()
si = create_sales_invoice(
item=item.name,
company="_Test Company DR",
customer="_Test Customer DR",
debit_to="Debtors - _CD",
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date="2021-05-01",
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
do_not_save=True,
rate=300,
price_list_rate=300,
)
si.items[0].income_account = "Sales - _CD"
si.items[0].income_account = self.income_account
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2021-05-01"
si.items[0].service_end_date = "2021-08-01"
si.items[0].deferred_revenue_account = deferred_revenue_account
si.items[0].income_account = "Sales - _CD"
si.items[0].deferred_revenue_account = self.deferred_revenue_account
si.save()
si.submit()
@@ -110,7 +113,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
start_date="2021-05-01",
end_date="2021-08-01",
type="Income",
company="_Test Company DR",
company=self.company,
)
)
pda.insert()
@@ -120,7 +123,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
self.filters = frappe._dict(
{
"company": frappe.defaults.get_user_default("Company"),
"company": self.company,
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
@@ -142,57 +145,36 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
]
self.assertEqual(report.period_total, expected)
@change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
def test_deferred_expense(self):
self.clear_old_entries()
# created deferred expense accounts, if not found
deferred_expense_account = create_account(
account_name="Deferred Expense",
parent_account="Current Assets - _CD",
company="_Test Company DR",
)
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
supplier = create_supplier(
supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
)
supplier.save()
item = create_item(
"_Test Office Desk",
is_stock_item=0,
warehouse="All Warehouses - _CD",
company="_Test Company DR",
)
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
item = frappe.get_doc("Item", self.item)
item.enable_deferred_expense = 1
item.deferred_expense_account = deferred_expense_account
item.deferred_expense_account = self.deferred_expense_account
item.no_of_months_exp = 3
item.save()
pi = make_purchase_invoice(
item=item.name,
company="_Test Company DR",
supplier="_Test Furniture Supplier",
item=self.item,
company=self.company,
supplier=self.supplier,
is_return=False,
update_stock=False,
posting_date=frappe.utils.datetime.date(2021, 5, 1),
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
do_not_save=True,
rate=300,
price_list_rate=300,
warehouse="All Warehouses - _CD",
warehouse=self.warehouse,
qty=1,
)
pi.set_posting_time = True
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2021-05-01"
pi.items[0].service_end_date = "2021-08-01"
pi.items[0].deferred_expense_account = deferred_expense_account
pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
pi.items[0].deferred_expense_account = self.deferred_expense_account
pi.items[0].expense_account = self.expense_account
pi.save()
pi.submit()
@@ -203,7 +185,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
start_date="2021-05-01",
end_date="2021-08-01",
type="Expense",
company="_Test Company DR",
company=self.company,
)
)
pda.insert()
@@ -213,7 +195,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
self.filters = frappe._dict(
{
"company": frappe.defaults.get_user_default("Company"),
"company": self.company,
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
@@ -235,52 +217,31 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
]
self.assertEqual(report.period_total, expected)
@change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
def test_zero_months(self):
self.clear_old_entries()
# created deferred expense accounts, if not found
deferred_revenue_account = create_account(
account_name="Deferred Revenue",
parent_account="Current Liabilities - _CD",
company="_Test Company DR",
)
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Customer DR"
customer.type = "Individual"
customer.insert()
item = create_item(
"_Test Internet Subscription",
is_stock_item=0,
warehouse="All Warehouses - _CD",
company="_Test Company DR",
)
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
item = frappe.get_doc("Item", self.item)
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_revenue_account
item.deferred_revenue_account = self.deferred_revenue_account
item.no_of_months = 0
item.save()
si = create_sales_invoice(
item=item.name,
company="_Test Company DR",
customer="_Test Customer DR",
debit_to="Debtors - _CD",
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date="2021-05-01",
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
do_not_save=True,
rate=300,
price_list_rate=300,
)
si.items[0].enable_deferred_revenue = 1
si.items[0].income_account = "Sales - _CD"
si.items[0].deferred_revenue_account = deferred_revenue_account
si.items[0].income_account = "Sales - _CD"
si.items[0].income_account = self.income_account
si.items[0].deferred_revenue_account = self.deferred_revenue_account
si.save()
si.submit()
@@ -291,7 +252,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
start_date="2021-05-01",
end_date="2021-08-01",
type="Income",
company="_Test Company DR",
company=self.company,
)
)
pda.insert()
@@ -301,7 +262,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
self.filters = frappe._dict(
{
"company": frappe.defaults.get_user_default("Company"),
"company": self.company,
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
@@ -322,30 +283,3 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
{"key": "aug_2021", "total": 0, "actual": 0},
]
self.assertEqual(report.period_total, expected)
def create_company():
company = frappe.db.exists("Company", "_Test Company DR")
if not company:
company = frappe.new_doc("Company")
company.company_name = "_Test Company DR"
company.default_currency = "INR"
company.chart_of_accounts = "Standard"
company.insert()
def clear_accounts_and_items():
item = qb.DocType("Item")
account = qb.DocType("Account")
customer = qb.DocType("Customer")
supplier = qb.DocType("Supplier")
qb.from_(account).delete().where(
(account.account_name == "Deferred Revenue")
| (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
).run()
qb.from_(item).delete().where(
(item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
).run()
qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()

View File

@@ -188,6 +188,7 @@ def get_data(
filters,
gl_entries_by_account,
ignore_closing_entries=ignore_closing_entries,
root_type=root_type,
)
calculate_values(
@@ -417,23 +418,44 @@ def set_gl_entries_by_account(
gl_entries_by_account,
ignore_closing_entries=False,
ignore_opening_entries=False,
root_type=None,
):
"""Returns a dict like { "account": [gl entries], ... }"""
gl_entries = []
account_filters = {
"company": company,
"is_group": 0,
"lft": (">=", root_lft),
"rgt": ("<=", root_rgt),
}
if root_type:
account_filters.update(
{
"root_type": root_type,
}
)
accounts_list = frappe.db.get_all(
"Account",
filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
filters=account_filters,
pluck="name",
)
if accounts_list:
# For balance sheet
if not from_date:
from_date = filters["period_start_date"]
ignore_closing_balances = frappe.db.get_single_value(
"Accounts Settings", "ignore_account_closing_balance"
)
if not from_date and not ignore_closing_balances:
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", from_date)},
filters={
"docstatus": 1,
"company": filters.company,
"posting_date": ("<", filters["period_start_date"]),
},
fields=["posting_date", "name"],
order_by="posting_date desc",
limit=1,

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
function get_filters() {
let filters = [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"period_start_date",
"label": __("Start Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
},
{
"fieldname":"period_end_date",
"label": __("End Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.get_today()
},
{
"fieldname":"account",
"label": __("Account"),
"fieldtype": "MultiSelectList",
"options": "Account",
get_data: function(txt) {
return frappe.db.get_link_options('Account', txt, {
company: frappe.query_report.get_filter_value("company"),
account_type: ['in', ["Receivable", "Payable"]]
});
}
},
{
"fieldname":"voucher_no",
"label": __("Voucher No"),
"fieldtype": "Data",
"width": 100,
},
]
return filters;
}
frappe.query_reports["General and Payment Ledger Comparison"] = {
"filters": get_filters()
};

View File

@@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2023-08-02 17:30:29.494907",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letterhead": null,
"modified": "2023-08-02 17:30:29.494907",
"modified_by": "Administrator",
"module": "Accounts",
"name": "General and Payment Ledger Comparison",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "General and Payment Ledger Comparison",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
}

View File

@@ -0,0 +1,221 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, qb
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Sum
class General_Payment_Ledger_Comparison(object):
"""
A Utility report to compare Voucher-wise balance between General and Payment Ledger
"""
def __init__(self, filters=None):
self.filters = filters
self.gle = []
self.ple = []
def get_accounts(self):
receivable_accounts = [
x[0]
for x in frappe.db.get_all(
"Account",
filters={"company": self.filters.company, "account_type": "Receivable"},
as_list=True,
)
]
payable_accounts = [
x[0]
for x in frappe.db.get_all(
"Account", filters={"company": self.filters.company, "account_type": "Payable"}, as_list=True
)
]
self.account_types = frappe._dict(
{
"receivable": frappe._dict({"accounts": receivable_accounts, "gle": [], "ple": []}),
"payable": frappe._dict({"accounts": payable_accounts, "gle": [], "ple": []}),
}
)
def generate_filters(self):
if self.filters.account:
self.account_types.receivable.accounts = []
self.account_types.payable.accounts = []
for acc in frappe.db.get_all(
"Account", filters={"name": ["in", self.filters.account]}, fields=["name", "account_type"]
):
if acc.account_type == "Receivable":
self.account_types.receivable.accounts.append(acc.name)
else:
self.account_types.payable.accounts.append(acc.name)
def get_gle(self):
gle = qb.DocType("GL Entry")
for acc_type, val in self.account_types.items():
if val.accounts:
filter_criterion = []
if self.filters.voucher_no:
filter_criterion.append((gle.voucher_no == self.filters.voucher_no))
if self.filters.period_start_date:
filter_criterion.append(gle.posting_date.gte(self.filters.period_start_date))
if self.filters.period_end_date:
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
if acc_type == "receivable":
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
else:
outstanding = (Sum(gle.credit) - Sum(gle.debit)).as_("outstanding")
self.account_types[acc_type].gle = (
qb.from_(gle)
.select(
gle.company,
gle.account,
gle.voucher_no,
gle.party,
outstanding,
)
.where(
(gle.company == self.filters.company)
& (gle.is_cancelled == 0)
& (gle.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
.groupby(gle.company, gle.account, gle.voucher_no, gle.party)
.run()
)
def get_ple(self):
ple = qb.DocType("Payment Ledger Entry")
for acc_type, val in self.account_types.items():
if val.accounts:
filter_criterion = []
if self.filters.voucher_no:
filter_criterion.append((ple.voucher_no == self.filters.voucher_no))
if self.filters.period_start_date:
filter_criterion.append(ple.posting_date.gte(self.filters.period_start_date))
if self.filters.period_end_date:
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
self.account_types[acc_type].ple = (
qb.from_(ple)
.select(
ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
)
.where(
(ple.company == self.filters.company)
& (ple.delinked == 0)
& (ple.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
.groupby(ple.company, ple.account, ple.voucher_no, ple.party)
.run()
)
def compare(self):
self.gle_balances = set()
self.ple_balances = set()
# consolidate both receivable and payable balances in one set
for acc_type, val in self.account_types.items():
self.gle_balances = set(val.gle) | self.gle_balances
self.ple_balances = set(val.ple) | self.ple_balances
self.diff1 = self.gle_balances.difference(self.ple_balances)
self.diff2 = self.ple_balances.difference(self.gle_balances)
self.diff = frappe._dict({})
for x in self.diff1:
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
for x in self.diff2:
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
def generate_data(self):
self.data = []
for key, val in self.diff.items():
self.data.append(
frappe._dict(
{
"voucher_no": key[2],
"party": key[3],
"gl_balance": val.gl_balance,
"pl_balance": val.pl_balance,
}
)
)
def get_columns(self):
self.columns = []
options = None
self.columns.append(
dict(
label=_("Voucher No"),
fieldname="voucher_no",
fieldtype="Data",
options=options,
width="100",
)
)
self.columns.append(
dict(
label=_("Party"),
fieldname="party",
fieldtype="Data",
options=options,
width="100",
)
)
self.columns.append(
dict(
label=_("GL Balance"),
fieldname="gl_balance",
fieldtype="Currency",
options="Company:company:default_currency",
width="100",
)
)
self.columns.append(
dict(
label=_("Payment Ledger Balance"),
fieldname="pl_balance",
fieldtype="Currency",
options="Company:company:default_currency",
width="100",
)
)
def run(self):
self.get_accounts()
self.generate_filters()
self.get_gle()
self.get_ple()
self.compare()
self.generate_data()
self.get_columns()
return self.columns, self.data
def execute(filters=None):
columns, data = [], []
rpt = General_Payment_Ledger_Comparison(filters)
columns, data = rpt.run()
return columns, data

View File

@@ -0,0 +1,100 @@
import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.general_and_payment_ledger_comparison.general_and_payment_ledger_comparison import (
execute,
)
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
def setUp(self):
self.create_company()
self.cleanup()
def tearDown(self):
frappe.db.rollback()
def cleanup(self):
doctypes = []
doctypes.append(qb.DocType("GL Entry"))
doctypes.append(qb.DocType("Payment Ledger Entry"))
doctypes.append(qb.DocType("Sales Invoice"))
for doctype in doctypes:
qb.from_(doctype).delete().where(doctype.company == self.company).run()
def test_01_basic_report_functionality(self):
sinv = create_sales_invoice(
company=self.company,
debit_to=self.debit_to,
expense_account=self.expense_account,
cost_center=self.cost_center,
income_account=self.income_account,
warehouse=self.warehouse,
)
# manually edit the payment ledger entry
ple = frappe.db.get_all(
"Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0}
)[0]
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", sinv.grand_total - 1)
filters = frappe._dict({"company": self.company})
columns, data = execute(filters=filters)
self.assertEqual(len(data), 1)
expected = {
"voucher_no": sinv.name,
"party": sinv.customer,
"gl_balance": sinv.grand_total,
"pl_balance": sinv.grand_total - 1,
}
self.assertEqual(expected, data[0])
# account filter
filters = frappe._dict({"company": self.company, "account": self.debit_to})
columns, data = execute(filters=filters)
self.assertEqual(len(data), 1)
self.assertEqual(expected, data[0])
filters = frappe._dict({"company": self.company, "account": self.creditors})
columns, data = execute(filters=filters)
self.assertEqual([], data)
# voucher_no filter
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name})
columns, data = execute(filters=filters)
self.assertEqual(len(data), 1)
self.assertEqual(expected, data[0])
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name + "-1"})
columns, data = execute(filters=filters)
self.assertEqual([], data)
# date range filter
filters = frappe._dict(
{
"company": self.company,
"period_start_date": sinv.posting_date,
"period_end_date": sinv.posting_date,
}
)
columns, data = execute(filters=filters)
self.assertEqual(len(data), 1)
self.assertEqual(expected, data[0])
filters = frappe._dict(
{
"company": self.company,
"period_start_date": add_days(sinv.posting_date, -1),
"period_end_date": add_days(sinv.posting_date, -1),
}
)
columns, data = execute(filters=filters)
self.assertEqual([], data)

View File

@@ -50,20 +50,20 @@ def get_pos_entries(filters, group_by_field):
order_by = "p.posting_date"
select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", ""
if group_by_field == "mode_of_payment":
select_mop_field = ", sip.mode_of_payment"
select_mop_field = ", sip.mode_of_payment, sip.base_amount - IF(sip.type='Cash', p.change_amount, 0) as paid_amount"
from_sales_invoice_payment = ", `tabSales Invoice Payment` sip"
group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount, 0) != 0 AND"
group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount - IF(sip.type='Cash', p.change_amount, 0), 0) != 0 AND"
order_by += ", sip.mode_of_payment"
elif group_by_field:
order_by += ", p.{}".format(group_by_field)
select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
return frappe.db.sql(
"""
SELECT
p.posting_date, p.name as pos_invoice, p.pos_profile,
p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount,
p.customer, p.is_return {select_mop_field}
p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
FROM
`tabPOS Invoice` p {from_sales_invoice_payment}
WHERE

View File

@@ -12,10 +12,28 @@ frappe.query_reports["TDS Payable Monthly"] = {
"default": frappe.defaults.get_default('company')
},
{
"fieldname":"supplier",
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier",
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Select",
"options": ["Supplier", "Customer"],
"reqd": 1,
"default": "Supplier",
"on_change": function(){
frappe.query_report.set_filter_value("party", "");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "Dynamic Link",
"get_options": function() {
var party_type = frappe.query_report.get_filter_value('party_type');
var party = frappe.query_report.get_filter_value('party');
if(party && !party_type) {
frappe.throw(__("Please select Party Type first"));
}
return party_type;
}
},
{
"fieldname":"from_date",

View File

@@ -33,77 +33,94 @@ def validate_filters(filters):
def get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map
):
supplier_map = get_supplier_pan_map()
party_map = get_party_pan_map(filters.get("party_type"))
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
out = []
for name, details in gle_map.items():
tds_deducted, total_amount_credited = 0, 0
tax_amount, total_amount = 0, 0
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
for entry in details:
supplier = entry.party or entry.against
party = entry.party or entry.against
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if voucher_type == "Journal Entry":
suppliers = journal_entry_party_map.get(name)
if suppliers:
supplier = suppliers[0]
party_list = journal_entry_party_map.get(name)
if party_list:
party = party_list[0]
if not tax_withholding_category:
tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = tax_rate_map.get(tax_withholding_category)
if entry.account in tds_accounts:
tds_deducted += entry.credit - entry.debit
tax_amount += entry.credit - entry.debit
if invoice_net_total_map.get(name):
total_amount_credited = invoice_net_total_map.get(name)
total_amount = invoice_net_total_map.get(name)
else:
total_amount_credited += entry.credit
total_amount += entry.credit
if tax_amount:
if party_map.get(party, {}).get("party_type") == "Supplier":
party_name = "supplier_name"
party_type = "supplier_type"
table_name = "Supplier"
else:
party_name = "customer_name"
party_type = "customer_type"
table_name = "Customer"
if tds_deducted:
row = {
"pan"
if frappe.db.has_column("Supplier", "pan")
else "tax_id": supplier_map.get(supplier, {}).get("pan"),
"supplier": supplier_map.get(supplier, {}).get("name"),
if frappe.db.has_column(table_name, "pan")
else "tax_id": party_map.get(party, {}).get("pan"),
"party": party_map.get(party, {}).get("name"),
}
if filters.naming_series == "Naming Series":
row.update({"supplier_name": supplier_map.get(supplier, {}).get("supplier_name")})
row.update({"party_name": party_map.get(party, {}).get(party_name)})
row.update(
{
"section_code": tax_withholding_category,
"entity_type": supplier_map.get(supplier, {}).get("supplier_type"),
"tds_rate": rate,
"total_amount_credited": total_amount_credited,
"tds_deducted": tds_deducted,
"entity_type": party_map.get(party, {}).get(party_type),
"rate": rate,
"total_amount": total_amount,
"tax_amount": tax_amount,
"transaction_date": posting_date,
"transaction_type": voucher_type,
"ref_no": name,
}
)
out.append(row)
return out
def get_supplier_pan_map():
supplier_map = frappe._dict()
suppliers = frappe.db.get_all(
"Supplier", fields=["name", "pan", "supplier_type", "supplier_name", "tax_withholding_category"]
)
def get_party_pan_map(party_type):
party_map = frappe._dict()
for d in suppliers:
supplier_map[d.name] = d
fields = ["name", "tax_withholding_category"]
if party_type == "Supplier":
fields += ["supplier_type", "supplier_name"]
else:
fields += ["customer_type", "customer_name"]
return supplier_map
if frappe.db.has_column(party_type, "pan"):
fields.append("pan")
party_details = frappe.db.get_all(party_type, fields=fields)
for party in party_details:
party.party_type = party_type
party_map[party.name] = party
return party_map
def get_gle_map(documents):
@@ -131,17 +148,17 @@ def get_columns(filters):
columns = [
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
{
"label": _("Supplier"),
"options": "Supplier",
"fieldname": "supplier",
"fieldtype": "Link",
"label": _(filters.get("party_type")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
]
if filters.naming_series == "Naming Series":
columns.append(
{"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180}
{"label": _("Party Name"), "fieldname": "party_name", "fieldtype": "Data", "width": 180}
)
columns.extend(
@@ -153,17 +170,22 @@ def get_columns(filters):
"fieldtype": "Link",
"width": 180,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
{"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 120},
{
"label": _("Total Amount Credited"),
"fieldname": "total_amount_credited",
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 90,
},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 90,
},
{
"label": _("Amount of TDS Deducted"),
"fieldname": "tds_deducted",
"label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 90,
},
@@ -173,13 +195,13 @@ def get_columns(filters):
"fieldtype": "Date",
"width": 90,
},
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 90},
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100},
{
"label": _("Reference No."),
"fieldname": "ref_no",
"fieldtype": "Dynamic Link",
"options": "transaction_type",
"width": 90,
"width": 180,
},
]
)
@@ -190,6 +212,7 @@ def get_columns(filters):
def get_tds_docs(filters):
tds_documents = []
purchase_invoices = []
sales_invoices = []
payment_entries = []
journal_entries = []
tax_category_map = frappe._dict()
@@ -209,10 +232,13 @@ def get_tds_docs(filters):
"against": ("not in", bank_accounts),
}
if filters.get("supplier"):
party = frappe.get_all(filters.get("party_type"), pluck="name")
query_filters.update({"against": ("in", party)})
if filters.get("party"):
del query_filters["account"]
del query_filters["against"]
or_filters = {"against": filters.get("supplier"), "party": filters.get("supplier")}
or_filters = {"against": filters.get("party"), "party": filters.get("party")}
tds_docs = frappe.get_all(
"GL Entry",
@@ -224,6 +250,8 @@ def get_tds_docs(filters):
for d in tds_docs:
if d.voucher_type == "Purchase Invoice":
purchase_invoices.append(d.voucher_no)
if d.voucher_type == "Sales Invoice":
sales_invoices.append(d.voucher_no)
elif d.voucher_type == "Payment Entry":
payment_entries.append(d.voucher_no)
elif d.voucher_type == "Journal Entry":
@@ -234,6 +262,9 @@ def get_tds_docs(filters):
if purchase_invoices:
get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, invoice_net_total_map)
if sales_invoices:
get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, invoice_net_total_map)
if payment_entries:
get_doc_info(payment_entries, "Payment Entry", tax_category_map)
@@ -267,6 +298,8 @@ def get_journal_entry_party_map(journal_entries):
def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None):
if doctype == "Purchase Invoice":
fields = ["name", "tax_withholding_category", "base_tax_withholding_net_total"]
if doctype == "Sales Invoice":
fields = ["name", "base_net_total"]
else:
fields = ["name", "tax_withholding_category"]
@@ -276,6 +309,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None
tax_category_map.update({entry.name: entry.tax_withholding_category})
if doctype == "Purchase Invoice":
invoice_net_total_map.update({entry.name: entry.base_tax_withholding_net_total})
if doctype == "Sales Invoice":
invoice_net_total_map.update({entry.name: entry.base_net_total})
def get_tax_rate_map(filters):

View File

@@ -142,14 +142,20 @@ def get_opening_balances(filters):
def get_rootwise_opening_balances(filters, report_type):
gle = []
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
fields=["posting_date", "name"],
order_by="posting_date desc",
limit=1,
last_period_closing_voucher = ""
ignore_closing_balances = frappe.db.get_single_value(
"Accounts Settings", "ignore_account_closing_balance"
)
if not ignore_closing_balances:
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
fields=["posting_date", "name"],
order_by="posting_date desc",
limit=1,
)
accounting_dimensions = get_accounting_dimensions(as_list=False)
if last_period_closing_voucher:

View File

@@ -0,0 +1,80 @@
import frappe
from erpnext.stock.doctype.item.test_item import create_item
class AccountsTestMixin:
def create_customer(self, customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.type = "Individual"
if currency:
customer.default_currency = currency
customer.save()
self.customer = customer.name
else:
self.customer = customer_name
def create_supplier(self, supplier_name, currency=None):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
supplier.supplier_type = "Individual"
supplier.supplier_group = "Local"
if currency:
supplier.default_currency = currency
supplier.save()
self.supplier = supplier.name
else:
self.supplier = supplier_name
def create_item(self, item_name, is_stock=0, warehouse=None, company=None):
item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company)
self.item = item.name
def create_company(self, company_name="_Test Company", abbr="_TC"):
self.company_abbr = abbr
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr
self.finished_warehouse = "Finished Goods - " + abbr
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
self.debit_usd = "Debtors USD - " + abbr
self.cash = "Cash - " + abbr
self.creditors = "Creditors - " + abbr
# create bank account
bank_account = "HDFC - " + abbr
if frappe.db.exists("Account", bank_account):
self.bank = bank_account
else:
bank_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": "HDFC",
"parent_account": "Bank Accounts - " + abbr,
"company": self.company,
}
)
bank_acc.save()
self.bank = bank_acc.name

View File

@@ -570,7 +570,11 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
# new row with references
new_row = journal_entry.append("accounts")
new_row.update((frappe.copy_doc(jv_detail)).as_dict())
# Copy field values into new row
[
new_row.set(field, jv_detail.get(field))
for field in frappe.get_meta("Journal Entry Account").get_fieldnames_with_value()
]
new_row.set(d["dr_or_cr"], d["allocated_amount"])
new_row.set(

View File

@@ -43,6 +43,7 @@
"column_break_33",
"opening_accumulated_depreciation",
"number_of_depreciations_booked",
"is_fully_depreciated",
"section_break_36",
"finance_books",
"section_break_33",
@@ -205,6 +206,7 @@
"fieldname": "disposal_date",
"fieldtype": "Date",
"label": "Disposal Date",
"no_copy": 1,
"read_only": 1
},
{
@@ -244,19 +246,17 @@
"label": "Is Existing Asset"
},
{
"depends_on": "is_existing_asset",
"depends_on": "eval:(doc.is_existing_asset)",
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"label": "Opening Accumulated Depreciation",
"no_copy": 1,
"options": "Company:company:default_currency"
},
{
"depends_on": "eval:(doc.is_existing_asset && doc.opening_accumulated_depreciation)",
"depends_on": "eval:(doc.is_existing_asset)",
"fieldname": "number_of_depreciations_booked",
"fieldtype": "Int",
"label": "Number of Depreciations Booked",
"no_copy": 1
"label": "Number of Depreciations Booked"
},
{
"collapsible": 1,
@@ -502,6 +502,13 @@
"options": "\nSuccessful\nFailed",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"depends_on": "eval:(doc.is_existing_asset)",
"fieldname": "is_fully_depreciated",
"fieldtype": "Check",
"label": "Is Fully Depreciated"
}
],
"idx": 72,
@@ -530,7 +537,7 @@
"table_fieldname": "accounts"
}
],
"modified": "2023-03-30 15:07:41.542374",
"modified": "2023-07-28 15:47:01.137996",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -574,4 +581,4 @@
"states": [],
"title_field": "asset_name",
"track_changes": 1
}
}

View File

@@ -148,17 +148,33 @@ class Asset(AccountsController):
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
def validate_cost_center(self):
if not self.cost_center:
return
cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company")
if cost_center_company != self.company:
frappe.throw(
_("Selected Cost Center {} doesn't belongs to {}").format(
frappe.bold(self.cost_center), frappe.bold(self.company)
),
title=_("Invalid Cost Center"),
if self.cost_center:
cost_center_company, cost_center_is_group = frappe.db.get_value(
"Cost Center", self.cost_center, ["company", "is_group"]
)
if cost_center_company != self.company:
frappe.throw(
_("Cost Center {} doesn't belong to Company {}").format(
frappe.bold(self.cost_center), frappe.bold(self.company)
),
title=_("Invalid Cost Center"),
)
if cost_center_is_group:
frappe.throw(
_(
"Cost Center {} is a group cost center and group cost centers cannot be used in transactions"
).format(frappe.bold(self.cost_center)),
title=_("Invalid Cost Center"),
)
else:
if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"):
frappe.throw(
_(
"Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
).format(frappe.bold(self.company)),
title=_("Missing Cost Center"),
)
def validate_in_use_date(self):
if not self.available_for_use_date:
@@ -207,8 +223,11 @@ class Asset(AccountsController):
if not self.calculate_depreciation:
return
elif not self.finance_books:
frappe.throw(_("Enter depreciation details"))
else:
if not self.finance_books:
frappe.throw(_("Enter depreciation details"))
if self.is_fully_depreciated:
frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets"))
if self.is_existing_asset:
return
@@ -588,7 +607,7 @@ class Asset(AccountsController):
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
frappe.throw(
_("Opening Accumulated Depreciation must be less than equal to {0}").format(
_("Opening Accumulated Depreciation must be less than or equal to {0}").format(
depreciable_amount
)
)
@@ -793,7 +812,9 @@ class Asset(AccountsController):
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation
if flt(value_after_depreciation) <= expected_value_after_useful_life:
if (
flt(value_after_depreciation) <= expected_value_after_useful_life or self.is_fully_depreciated
):
status = "Fully Depreciated"
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
status = "Partially Depreciated"
@@ -941,7 +962,9 @@ class Asset(AccountsController):
@frappe.whitelist()
def get_manual_depreciation_entries(self):
(_, _, depreciation_expense_account) = get_depreciation_accounts(self)
(_, _, depreciation_expense_account) = get_depreciation_accounts(
self.asset_category, self.company
)
gle = frappe.qb.DocType("GL Entry")
@@ -1180,10 +1203,10 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
def make_journal_entry(asset_name):
asset = frappe.get_doc("Asset", asset_name)
(
fixed_asset_account,
_,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset)
) = get_depreciation_accounts(asset.asset_category, asset.company)
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]

View File

@@ -4,6 +4,8 @@
import frappe
from frappe import _
from frappe.query_builder import Order
from frappe.query_builder.functions import Max, Min
from frappe.utils import (
add_months,
cint,
@@ -36,9 +38,40 @@ def post_depreciation_entries(date=None):
failed_asset_names = []
error_log_names = []
for asset_name in get_depreciable_assets(date):
depreciable_assets = get_depreciable_assets(date)
credit_and_debit_accounts_for_asset_category_and_company = {}
depreciation_cost_center_and_depreciation_series_for_company = (
get_depreciation_cost_center_and_depreciation_series_for_company()
)
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for asset in depreciable_assets:
asset_name, asset_category, asset_company, sch_start_idx, sch_end_idx = asset
if (
asset_category,
asset_company,
) not in credit_and_debit_accounts_for_asset_category_and_company:
credit_and_debit_accounts_for_asset_category_and_company.update(
{
(asset_category, asset_company): get_credit_and_debit_accounts_for_asset_category_and_company(
asset_category, asset_company
),
}
)
try:
make_depreciation_entry(asset_name, date)
make_depreciation_entry(
asset_name,
date,
sch_start_idx,
sch_end_idx,
credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)],
depreciation_cost_center_and_depreciation_series_for_company[asset_company],
accounting_dimensions,
)
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
@@ -54,115 +87,226 @@ def post_depreciation_entries(date=None):
def get_depreciable_assets(date):
return frappe.db.sql_list(
"""select distinct a.name
from tabAsset a, `tabDepreciation Schedule` ds
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
and a.status in ('Submitted', 'Partially Depreciated')
and ifnull(ds.journal_entry, '')=''""",
date,
a = frappe.qb.DocType("Asset")
ds = frappe.qb.DocType("Depreciation Schedule")
res = (
frappe.qb.from_(a)
.join(ds)
.on(a.name == ds.parent)
.select(a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx))
.where(a.calculate_depreciation == 1)
.where(a.docstatus == 1)
.where(a.status.isin(["Submitted", "Partially Depreciated"]))
.where(ds.journal_entry.isnull())
.where(ds.schedule_date <= date)
.groupby(a.name)
.orderby(a.creation, order=Order.desc)
)
acc_frozen_upto = get_acc_frozen_upto()
if acc_frozen_upto:
res = res.where(ds.schedule_date > acc_frozen_upto)
res = res.run()
return res
def get_acc_frozen_upto():
acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
if not acc_frozen_upto:
return
frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", "frozen_accounts_modifier"
)
if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator":
return getdate(acc_frozen_upto)
return
def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company):
(
_,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset_category, company)
credit_account, debit_account = get_credit_and_debit_accounts(
accumulated_depreciation_account, depreciation_expense_account
)
return (credit_account, debit_account)
def get_depreciation_cost_center_and_depreciation_series_for_company():
company_names = frappe.db.get_all("Company", pluck="name")
res = {}
for company_name in company_names:
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
res.update({company_name: (depreciation_cost_center, depreciation_series)})
return res
@frappe.whitelist()
def make_depreciation_entry(asset_name, date=None):
def make_depreciation_entry(
asset_name,
date=None,
sch_start_idx=None,
sch_end_idx=None,
credit_and_debit_accounts=None,
depreciation_cost_center_and_depreciation_series=None,
accounting_dimensions=None,
):
frappe.has_permission("Journal Entry", throw=True)
if not date:
date = today()
asset = frappe.get_doc("Asset", asset_name)
(
fixed_asset_account,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset)
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
if credit_and_debit_accounts:
credit_account, debit_account = credit_and_debit_accounts
else:
credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company(
asset.asset_category, asset.company
)
if depreciation_cost_center_and_depreciation_series:
depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series
else:
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
depreciation_cost_center = asset.cost_center or depreciation_cost_center
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
if not accounting_dimensions:
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for d in asset.get("schedules"):
if not d.journal_entry and getdate(d.schedule_date) <= getdate(date):
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series
je.posting_date = d.schedule_date
je.company = asset.company
je.finance_book = d.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
depreciation_posting_error = None
credit_account, debit_account = get_credit_and_debit_accounts(
accumulated_depreciation_account, depreciation_expense_account
for d in asset.get("schedules")[sch_start_idx or 0 : sch_end_idx or len(asset.get("schedules"))]:
try:
_make_journal_entry_for_depreciation(
asset,
date,
d,
sch_start_idx,
sch_end_idx,
depreciation_cost_center,
depreciation_series,
credit_account,
debit_account,
accounting_dimensions,
)
credit_entry = {
"account": credit_account,
"credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
}
debit_entry = {
"account": debit_account,
"debit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
}
for dimension in accounting_dimensions:
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
credit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
debit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)
je.flags.ignore_permissions = True
je.flags.planned_depr_entry = True
je.save()
d.db_set("journal_entry", je.name)
if not je.meta.get_workflow():
je.submit()
idx = cint(d.finance_book_id)
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation -= d.depreciation_amount
finance_books.db_update()
asset.db_set("depr_entry_posting_status", "Successful")
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
depreciation_posting_error = e
asset.set_status()
return asset
if not depreciation_posting_error:
asset.db_set("depr_entry_posting_status", "Successful")
return asset
raise depreciation_posting_error
def get_depreciation_accounts(asset):
def _make_journal_entry_for_depreciation(
asset,
date,
depr_schedule,
sch_start_idx,
sch_end_idx,
depreciation_cost_center,
depreciation_series,
credit_account,
debit_account,
accounting_dimensions,
):
if not (sch_start_idx and sch_end_idx) and not (
not depr_schedule.journal_entry and getdate(depr_schedule.schedule_date) <= getdate(date)
):
return
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series
je.posting_date = depr_schedule.schedule_date
je.company = asset.company
je.finance_book = depr_schedule.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(
asset.name, depr_schedule.depreciation_amount
)
credit_entry = {
"account": credit_account,
"credit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
}
debit_entry = {
"account": debit_account,
"debit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
}
for dimension in accounting_dimensions:
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
credit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
debit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)
je.flags.ignore_permissions = True
je.flags.planned_depr_entry = True
je.save()
depr_schedule.db_set("journal_entry", je.name)
if not je.meta.get_workflow():
je.submit()
idx = cint(depr_schedule.finance_book_id)
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation -= depr_schedule.depreciation_amount
finance_books.db_update()
def get_depreciation_accounts(asset_category, company):
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
accounts = frappe.db.get_value(
"Asset Category Account",
filters={"parent": asset.asset_category, "company_name": asset.company},
filters={"parent": asset_category, "company_name": company},
fieldname=[
"fixed_asset_account",
"accumulated_depreciation_account",
@@ -178,7 +322,7 @@ def get_depreciation_accounts(asset):
if not accumulated_depreciation_account or not depreciation_expense_account:
accounts = frappe.get_cached_value(
"Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"]
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
)
if not accumulated_depreciation_account:
@@ -193,7 +337,7 @@ def get_depreciation_accounts(asset):
):
frappe.throw(
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
asset.asset_category, asset.company
asset_category, company
)
)
@@ -369,6 +513,15 @@ def reverse_depreciation_entry_made_after_disposal(asset, date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
for account in reverse_journal_entry.accounts:
account.update(
{
"reference_type": "Asset",
"reference_name": asset.name,
}
)
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
@@ -524,8 +677,8 @@ def get_gl_entries_on_asset_disposal(
def get_asset_details(asset, finance_book=None):
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(
asset
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
asset.asset_category, asset.company
)
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
depreciation_cost_center = asset.cost_center or depreciation_cost_center

View File

@@ -325,7 +325,7 @@ class AssetCapitalization(StockController):
gl_entries = self.get_gl_entries()
if gl_entries:
make_gl_entries(gl_entries, from_repost=from_repost)
make_gl_entries(gl_entries, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -355,9 +355,6 @@ class AssetCapitalization(StockController):
gl_entries, target_account, target_against, precision
)
if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable:
return []
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
return gl_entries

View File

@@ -50,10 +50,10 @@ class AssetValueAdjustment(Document):
def make_depreciation_entry(self):
asset = frappe.get_doc("Asset", self.asset)
(
fixed_asset_account,
_,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset)
) = get_depreciation_accounts(asset.asset_category, asset.company)
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]

View File

@@ -1,13 +1,15 @@
{
"add_total_row": 0,
"add_total_row": 1,
"columns": [],
"creation": "2019-09-23 16:35:02.836134",
"disable_prepared_report": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2019-10-22 13:00:31.539726",
"letterhead": null,
"modified": "2023-07-26 21:03:20.722628",
"modified_by": "Administrator",
"module": "Assets",
"name": "Fixed Asset Register",

View File

@@ -369,7 +369,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "qty"]
child_columns: ["item_code", "qty", "ordered_qty"]
})
}, __("Get Items From"));

View File

@@ -25,6 +25,7 @@
"col_break_email_1",
"email_template",
"preview",
"send_attached_files",
"sec_break_email_2",
"message_for_supplier",
"terms_section_break",
@@ -285,13 +286,20 @@
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
},
{
"default": "1",
"description": "If enabled, all files attached to this document will be attached to each email",
"fieldname": "send_attached_files",
"fieldtype": "Check",
"label": "Send Attached Files"
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-01-31 23:22:06.684694",
"modified": "2023-07-27 16:41:48.468873",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@@ -209,7 +209,9 @@ class RequestforQuotation(BuyingController):
if preview:
return message
attachments = self.get_attachments()
attachments = None
if self.send_attached_files:
attachments = self.get_attachments()
self.send_email(data, sender, subject, message, attachments)

View File

@@ -31,6 +31,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
apply_pricing_rule_on_transaction,
get_applied_pricing_rules,
)
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import (
get_party_account,
get_party_account_currency,
@@ -1023,6 +1024,33 @@ class AccountsController(TransactionBase):
)
)
def make_precision_loss_gl_entry(self, gl_entries):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
precision_loss = self.get("base_net_total") - flt(
self.get("net_total") * self.conversion_rate, self.precision("net_total")
)
credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit"
against = self.supplier if self.doctype == "Purchase Invoice" else self.customer
if precision_loss:
gl_entries.append(
self.get_gl_dict(
{
"account": round_off_account,
"against": against,
credit_or_debit: precision_loss,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else self.cost_center or round_off_cost_center,
"remarks": _("Net total calculation precision loss"),
}
)
)
def update_against_document_in_jv(self):
"""
Links invoice and advance voucher:
@@ -1672,8 +1700,13 @@ class AccountsController(TransactionBase):
)
self.append("payment_schedule", data)
allocate_payment_based_on_payment_terms = frappe.db.get_value(
"Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms"
)
if not (
automatically_fetch_payment_terms
and allocate_payment_based_on_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
):
for d in self.get("payment_schedule"):
@@ -2217,21 +2250,29 @@ def get_advance_payment_entries(
reference_condition = ""
order_list = []
if not condition:
condition = ""
payment_entries_against_order = frappe.db.sql(
"""
select
'Payment Entry' as reference_type, t1.name as reference_name,
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
t2.reference_name as against_order, t1.posting_date,
t1.{0} as currency, t1.{4} as exchange_rate
t1.{0} as currency, t1.{5} as exchange_rate
from `tabPayment Entry` t1, `tabPayment Entry Reference` t2
where
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
and t2.reference_doctype = %s {2}
order by t1.posting_date {3}
and t2.reference_doctype = %s {2} {3}
order by t1.posting_date {4}
""".format(
currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field
currency_field,
party_account_field,
reference_condition,
condition,
limit_cond,
exchange_rate_field,
),
[party_account, payment_type, party_type, party, order_doctype] + order_list,
as_dict=1,

View File

@@ -475,6 +475,10 @@ class BuyingController(SubcontractingController):
if d.item_code not in stock_items:
continue
rejected_qty = 0.0
if flt(d.rejected_qty) != 0:
rejected_qty = flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty"))
if d.warehouse:
pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
@@ -495,6 +499,11 @@ class BuyingController(SubcontractingController):
},
)
if flt(rejected_qty) != 0:
from_warehouse_sle["actual_qty"] += -1 * rejected_qty
if d.rejected_serial_no:
from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip()
sl_entries.append(from_warehouse_sle)
sle = self.get_sl_entries(
@@ -520,6 +529,7 @@ class BuyingController(SubcontractingController):
else 0,
}
)
sl_entries.append(sle)
if d.from_warehouse and (
@@ -530,23 +540,30 @@ class BuyingController(SubcontractingController):
d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1}
)
if flt(rejected_qty) != 0:
from_warehouse_sle["actual_qty"] += -1 * rejected_qty
if d.rejected_serial_no:
from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip()
sl_entries.append(from_warehouse_sle)
if flt(d.rejected_qty) != 0:
if flt(rejected_qty) != 0:
sl_entries.append(
self.get_sl_entries(
d,
{
"warehouse": d.rejected_warehouse,
"actual_qty": flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")),
"actual_qty": rejected_qty,
"serial_no": cstr(d.rejected_serial_no).strip(),
"incoming_rate": 0.0,
"allow_zero_valuation_rate": True,
},
)
)
if self.get("is_old_subcontracting_flow"):
self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries(
sl_entries,
allow_negative_stock=allow_negative_stock,

View File

@@ -233,6 +233,9 @@ class StatusUpdater(Document):
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0:
frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code))
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
args["name"] = d.get(args["join_field"])

View File

@@ -460,7 +460,7 @@ class SubcontractingController(StockController):
"allow_zero_valuation": 1,
}
)
rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
rm_obj.rate = get_incoming_rate(args)
if self.doctype == self.subcontract_data.order_doctype:
rm_obj.required_qty = qty

View File

@@ -185,7 +185,7 @@ class Lead(SellingController, CRMNote):
"last_name": self.last_name,
"salutation": self.salutation,
"gender": self.gender,
"job_title": self.job_title,
"designation": self.job_title,
"company_name": self.company_name,
}
)

View File

@@ -413,11 +413,11 @@ scheduler_events = {
],
},
"all": [
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts",
],
"hourly": [
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
],
@@ -432,7 +432,6 @@ scheduler_events = {
"erpnext.controllers.accounts_controller.update_invoice_status",
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
"erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status",
"erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards",
"erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history",
@@ -459,6 +458,7 @@ scheduler_events = {
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",

View File

@@ -93,6 +93,7 @@ class BOMUpdateLog(Document):
else:
frappe.enqueue(
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
queue="long",
update_doc=self,
now=frappe.flags.in_test,
enqueue_after_commit=True,

View File

@@ -157,12 +157,19 @@ def get_next_higher_level_boms(
def get_leaf_boms() -> List[str]:
"Get BOMs that have no dependencies."
return frappe.db.sql_list(
"""select name from `tabBOM` bom
where docstatus=1 and is_active=1
and not exists(select bom_no from `tabBOM Item`
where parent=bom.name and ifnull(bom_no, '')!='')"""
)
bom = frappe.qb.DocType("BOM")
bom_item = frappe.qb.DocType("BOM Item")
boms = (
frappe.qb.from_(bom)
.left_join(bom_item)
.on((bom.name == bom_item.parent) & (bom_item.bom_no != ""))
.select(bom.name)
.where((bom.docstatus == 1) & (bom.is_active == 1) & (bom_item.bom_no.isnull()))
.distinct()
).run(pluck=True)
return boms
def _generate_dependence_map() -> defaultdict:

View File

@@ -498,12 +498,12 @@ class JobCard(Document):
if self.for_quantity and flt(total_completed_qty, precision) != flt(
self.for_quantity, precision
):
total_completed_qty = bold(_("Total Completed Qty"))
total_completed_qty_label = bold(_("Total Completed Qty"))
qty_to_manufacture = bold(_("Qty to Manufacture"))
frappe.throw(
_("The {0} ({1}) must be equal to {2} ({3})").format(
total_completed_qty,
total_completed_qty_label,
bold(flt(total_completed_qty, precision)),
qty_to_manufacture,
bold(self.for_quantity),

View File

@@ -9,19 +9,25 @@ frappe.ui.form.on('Production Plan', {
item.temporary_name = item.name;
});
},
setup(frm) {
frm.trigger("setup_queries");
frm.custom_make_buttons = {
'Work Order': 'Work Order / Subcontract PO',
'Material Request': 'Material Request',
};
},
frm.fields_dict['po_items'].grid.get_field('warehouse').get_query = function(doc) {
setup_queries(frm) {
frm.set_query("sales_order", "sales_orders", () => {
return {
query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query",
filters: {
company: doc.company
company: frm.doc.company,
}
}
}
});
frm.set_query('for_warehouse', function(doc) {
return {
@@ -42,32 +48,40 @@ frappe.ui.form.on('Production Plan', {
};
});
frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) {
frm.set_query("item_code", "po_items", (doc, cdt, cdn) => {
return {
query: "erpnext.controllers.queries.item_query",
filters:{
'is_stock_item': 1,
}
}
}
});
frm.fields_dict['po_items'].grid.get_field('bom_no').get_query = function(doc, cdt, cdn) {
frm.set_query("bom_no", "po_items", (doc, cdt, cdn) => {
var d = locals[cdt][cdn];
if (d.item_code) {
return {
query: "erpnext.controllers.queries.bom",
filters:{'item': cstr(d.item_code), 'docstatus': 1}
filters:{'item': d.item_code, 'docstatus': 1}
}
} else frappe.msgprint(__("Please enter Item first"));
}
});
frm.fields_dict['mr_items'].grid.get_field('warehouse').get_query = function(doc) {
frm.set_query("warehouse", "mr_items", (doc) => {
return {
filters: {
company: doc.company
}
}
}
});
frm.set_query("warehouse", "po_items", (doc) => {
return {
filters: {
company: doc.company
}
}
});
},
refresh(frm) {
@@ -436,7 +450,7 @@ frappe.ui.form.on("Production Plan Item", {
}
});
}
}
},
});
frappe.ui.form.on("Material Request Plan Item", {
@@ -467,31 +481,36 @@ frappe.ui.form.on("Material Request Plan Item", {
frappe.ui.form.on("Production Plan Sales Order", {
sales_order(frm, cdt, cdn) {
const { sales_order } = locals[cdt][cdn];
let row = locals[cdt][cdn];
const sales_order = row.sales_order;
if (!sales_order) {
return;
}
frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details",
args: { sales_order },
callback(r) {
const {transaction_date, customer, grand_total} = r.message;
frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date);
frappe.model.set_value(cdt, cdn, 'customer', customer);
frappe.model.set_value(cdt, cdn, 'grand_total', grand_total);
}
});
if (row.sales_order) {
frm.call({
method: "validate_sales_orders",
doc: frm.doc,
args: {
sales_order: row.sales_order,
},
callback(r) {
frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details",
args: { sales_order },
callback(r) {
const {transaction_date, customer, grand_total} = r.message;
frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date);
frappe.model.set_value(cdt, cdn, 'customer', customer);
frappe.model.set_value(cdt, cdn, 'grand_total', grand_total);
}
});
}
});
}
}
});
cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function() {
return{
filters: [
['Sales Order','docstatus', '=' ,1]
]
}
};
frappe.tour['Production Plan'] = [
{
fieldname: "get_items_from",

View File

@@ -228,10 +228,10 @@
},
{
"default": "0",
"description": "To know more about projected quantity, <a href=\"https://erpnext.com/docs/user/manual/en/stock/projected-quantity\" style=\"text-decoration: underline;\" target=\"_blank\">click here</a>.",
"description": "If enabled, the system won't create material requests for the available items.",
"fieldname": "ignore_existing_ordered_qty",
"fieldtype": "Check",
"label": "Ignore Existing Projected Quantity"
"label": "Ignore Available Stock"
},
{
"fieldname": "column_break_25",
@@ -339,7 +339,7 @@
"depends_on": "eval:doc.get_items_from == 'Sales Order'",
"fieldname": "combine_items",
"fieldtype": "Check",
"label": "Consolidate Items"
"label": "Consolidate Sales Order Items"
},
{
"fieldname": "section_break_25",
@@ -399,7 +399,7 @@
},
{
"default": "0",
"description": "System consider the projected quantity to check available or will be available sub-assembly items ",
"description": "If this checkbox is enabled, then the system won\u2019t run the MRP for the available sub-assembly items.",
"fieldname": "skip_available_sub_assembly_item",
"fieldtype": "Check",
"label": "Skip Available Sub Assembly Items"
@@ -422,7 +422,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-05-22 23:36:31.770517",
"modified": "2023-07-28 13:37:43.926686",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -39,6 +39,36 @@ class ProductionPlan(Document):
self.set_status()
self._rename_temporary_references()
validate_uom_is_integer(self, "stock_uom", "planned_qty")
self.validate_sales_orders()
@frappe.whitelist()
def validate_sales_orders(self, sales_order=None):
sales_orders = []
if sales_order:
sales_orders.append(sales_order)
else:
sales_orders = [row.sales_order for row in self.sales_orders if row.sales_order]
data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders})
title = _("Production Plan Already Submitted")
if not data:
msg = _("No items are available in the sales order {0} for production").format(sales_orders[0])
if len(sales_orders) > 1:
sales_orders = ", ".join(sales_orders)
msg = _("No items are available in sales orders {0} for production").format(sales_orders)
frappe.throw(msg, title=title)
data = [d[0] for d in data]
for sales_order in sales_orders:
if sales_order not in data:
frappe.throw(
_("No items are available in the sales order {0} for production").format(sales_order),
title=title,
)
def set_pending_qty_in_row_without_reference(self):
"Set Pending Qty in independent rows (not from SO or MR)."
@@ -205,6 +235,7 @@ class ProductionPlan(Document):
).as_("pending_qty"),
so_item.description,
so_item.name,
so_item.bom_no,
)
.distinct()
.where(
@@ -342,7 +373,7 @@ class ProductionPlan(Document):
"item_code": data.item_code,
"description": data.description or item_details.description,
"stock_uom": item_details and item_details.stock_uom or "",
"bom_no": item_details and item_details.bom_no or "",
"bom_no": data.bom_no or item_details and item_details.bom_no or "",
"planned_qty": data.pending_qty,
"pending_qty": data.pending_qty,
"planned_start_date": now_datetime(),
@@ -401,11 +432,50 @@ class ProductionPlan(Document):
def on_submit(self):
self.update_bin_qty()
self.update_sales_order()
def on_cancel(self):
self.db_set("status", "Cancelled")
self.delete_draft_work_order()
self.update_bin_qty()
self.update_sales_order()
def update_sales_order(self):
sales_orders = [row.sales_order for row in self.po_items if row.sales_order]
if sales_orders:
so_wise_planned_qty = self.get_so_wise_planned_qty(sales_orders)
for row in self.po_items:
if not row.sales_order and not row.sales_order_item:
continue
key = (row.sales_order, row.sales_order_item)
frappe.db.set_value(
"Sales Order Item",
row.sales_order_item,
"production_plan_qty",
flt(so_wise_planned_qty.get(key)),
)
@staticmethod
def get_so_wise_planned_qty(sales_orders):
so_wise_planned_qty = frappe._dict()
data = frappe.get_all(
"Production Plan Item",
fields=["sales_order", "sales_order_item", "SUM(planned_qty) as qty"],
filters={
"sales_order": ("in", sales_orders),
"docstatus": 1,
"sales_order_item": ("is", "set"),
},
group_by="sales_order, sales_order_item",
)
for row in data:
key = (row.sales_order, row.sales_order_item)
so_wise_planned_qty[key] = row.qty
return so_wise_planned_qty
def update_bin_qty(self):
for d in self.mr_items:
@@ -719,6 +789,9 @@ class ProductionPlan(Document):
sub_assembly_items_store = [] # temporary store to process all subassembly items
for row in self.po_items:
if self.skip_available_sub_assembly_item and not row.warehouse:
frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx))
if not row.item_code:
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
@@ -1142,7 +1215,7 @@ def get_sales_orders(self):
& (so.docstatus == 1)
& (so.status.notin(["Stopped", "Closed"]))
& (so.company == self.company)
& (so_item.qty > so_item.work_order_qty)
& (so_item.qty > so_item.production_plan_qty)
)
)
@@ -1566,7 +1639,6 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
def get_raw_materials_of_sub_assembly_items(
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
):
bei = frappe.qb.DocType("BOM Item")
bom = frappe.qb.DocType("BOM")
item = frappe.qb.DocType("Item")
@@ -1609,7 +1681,10 @@ def get_raw_materials_of_sub_assembly_items(
for item in items:
key = (item.item_code, item.bom_no)
if item.bom_no and key in sub_assembly_items:
if item.bom_no and key not in sub_assembly_items:
continue
if item.bom_no:
planned_qty = flt(sub_assembly_items[key])
get_raw_materials_of_sub_assembly_items(
item_details,
@@ -1626,3 +1701,42 @@ def get_raw_materials_of_sub_assembly_items(
item_details.setdefault(item.get("item_code"), item)
return item_details
@frappe.whitelist()
def sales_order_query(
doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None
):
frappe.has_permission("Production Plan", throw=True)
if not filters:
filters = {}
so_table = frappe.qb.DocType("Sales Order")
table = frappe.qb.DocType("Sales Order Item")
query = (
frappe.qb.from_(so_table)
.join(table)
.on(table.parent == so_table.name)
.select(table.parent)
.distinct()
.where((table.qty > table.production_plan_qty) & (table.docstatus == 1))
)
if filters.get("company"):
query = query.where(so_table.company == filters.get("company"))
if filters.get("sales_orders"):
query = query.where(so_table.name.isin(filters.get("sales_orders")))
if txt:
query = query.where(table.parent.like(f"%{txt}%"))
if page_len:
query = query.limit(page_len)
if start:
query = query.offset(start)
return query.run()

View File

@@ -225,6 +225,102 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(sales_orders, [])
def test_donot_allow_to_make_multiple_pp_against_same_so(self):
item = "Test SO Production Item 1"
create_item(item)
raw_material = "Test SO RM Production Item 1"
create_item(raw_material)
if not frappe.db.get_value("BOM", {"item": item}):
make_bom(item=item, raw_materials=[raw_material])
so = make_sales_order(item_code=item, qty=4)
pln = frappe.new_doc("Production Plan")
pln.company = so.company
pln.get_items_from = "Sales Order"
pln.append(
"sales_orders",
{
"sales_order": so.name,
"sales_order_date": so.transaction_date,
"customer": so.customer,
"grand_total": so.grand_total,
},
)
pln.get_so_items()
pln.submit()
pln = frappe.new_doc("Production Plan")
pln.company = so.company
pln.get_items_from = "Sales Order"
pln.append(
"sales_orders",
{
"sales_order": so.name,
"sales_order_date": so.transaction_date,
"customer": so.customer,
"grand_total": so.grand_total,
},
)
pln.get_so_items()
self.assertRaises(frappe.ValidationError, pln.save)
def test_so_based_bill_of_material(self):
item = "Test SO Production Item 1"
create_item(item)
raw_material = "Test SO RM Production Item 1"
create_item(raw_material)
bom1 = make_bom(item=item, raw_materials=[raw_material])
so = make_sales_order(item_code=item, qty=4)
# Create new BOM and assign to new sales order
bom2 = make_bom(item=item, raw_materials=[raw_material])
so2 = make_sales_order(item_code=item, qty=4)
pln1 = frappe.new_doc("Production Plan")
pln1.company = so.company
pln1.get_items_from = "Sales Order"
pln1.append(
"sales_orders",
{
"sales_order": so.name,
"sales_order_date": so.transaction_date,
"customer": so.customer,
"grand_total": so.grand_total,
},
)
pln1.get_so_items()
self.assertEqual(pln1.po_items[0].bom_no, bom1.name)
pln2 = frappe.new_doc("Production Plan")
pln2.company = so2.company
pln2.get_items_from = "Sales Order"
pln2.append(
"sales_orders",
{
"sales_order": so2.name,
"sales_order_date": so2.transaction_date,
"customer": so2.customer,
"grand_total": so2.grand_total,
},
)
pln2.get_so_items()
self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
def test_production_plan_combine_items(self):
"Test combining FG items in Production Plan."
item = "Test Production Item 1"

View File

@@ -114,7 +114,7 @@ class Workstation(Document):
if schedule_date in tuple(get_holidays(self.holiday_list)):
schedule_date = add_days(schedule_date, 1)
self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
return self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
return schedule_date

View File

@@ -510,30 +510,41 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
if(!list_value) {
new_line = '';
} else {
me.serial_list = list_value.replace(/\n/g, ' ').match(/\S+/g) || [];
me.serial_list = list_value.split(/\n/g) || [];
}
if(!me.serial_list.includes(new_number)) {
this.set_new_description('');
serial_no_list_field.set_value(me.serial_list.join('\n') + new_line + new_number);
me.serial_list = serial_no_list_field.get_value().replace(/\n/g, ' ').match(/\S+/g) || [];
me.serial_list = serial_no_list_field.get_value().split(/\n/g) || [];
} else {
this.set_new_description(new_number + ' is already selected.');
}
me.serial_list = me.serial_list.filter(serial => {
if (serial) {
return true;
}
});
qty_field.set_input(me.serial_list.length);
this.$input.val("");
this.in_local_change = 0;
}
},
{fieldtype: 'Column Break'},
{fieldtype: 'Section Break'},
{
fieldname: 'serial_no',
fieldtype: 'Small Text',
fieldtype: 'Text',
label: __(me.has_batch && !me.has_serial_no ? 'Selected Batch Numbers' : 'Selected Serial Numbers'),
onchange: function() {
me.serial_list = this.get_value()
.replace(/\n/g, ' ').match(/\S+/g) || [];
me.serial_list = this.get_value().split(/\n/g);
me.serial_list = me.serial_list.filter(serial => {
if (serial) {
return true;
}
});
this.layout.fields_dict.qty.set_input(me.serial_list.length);
}
}

View File

@@ -62,10 +62,10 @@
"fieldtype": "Column Break"
},
{
"fetch_from": "process_owner.full_name",
"fetch_from": "procedure.process_owner_full_name",
"fieldname": "full_name",
"fieldtype": "Data",
"hidden": 1,
"read_only": 1,
"label": "Full Name"
},
{
@@ -81,7 +81,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-26 15:27:47.247814",
"modified": "2023-07-31 08:10:47.247814",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Non Conformance",

View File

@@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "format:{####}",
"creation": "2019-05-26 15:03:43.996455",
"doctype": "DocType",
@@ -12,7 +13,6 @@
],
"fields": [
{
"fetch_from": "goal.objective",
"fieldname": "objective",
"fieldtype": "Text",
"in_list_view": 1,
@@ -38,14 +38,17 @@
}
],
"istable": 1,
"modified": "2019-05-26 16:12:54.832058",
"links": [],
"modified": "2023-07-28 18:10:23.351246",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Goal Objective",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -56,6 +56,7 @@
"fieldtype": "Column Break"
},
{
"default": "Open",
"columns": 2,
"fieldname": "status",
"fieldtype": "Select",
@@ -67,7 +68,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-27 16:28:20.908637",
"modified": "2023-07-31 09:20:20.908637",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Review Objective",
@@ -76,4 +77,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -34,6 +34,7 @@ class LowerDeductionCertificate(Document):
"supplier": self.supplier,
"tax_withholding_category": self.tax_withholding_category,
"name": ("!=", self.name),
"company": self.company,
},
["name", "valid_from", "valid_upto"],
as_dict=True,

View File

@@ -556,7 +556,7 @@ def make_material_request(source_name, target_doc=None):
# qty is for packed items, because packed items don't have stock_qty field
qty = source.get("qty")
target.project = source_parent.project
target.qty = qty - requested_item_qty.get(source.name, 0) - source.delivered_qty
target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty"))
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
args = target.as_dict().copy()
@@ -590,7 +590,7 @@ def make_material_request(source_name, target_doc=None):
"doctype": "Material Request Item",
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
"condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code)
and (doc.stock_qty - doc.delivered_qty) > requested_item_qty.get(doc.name, 0),
and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0),
"postprocess": update_item,
},
},

View File

@@ -552,6 +552,26 @@ class TestSalesOrder(FrappeTestCase):
workflow.is_active = 0
workflow.save()
def test_material_request_for_product_bundle(self):
# Create the Material Request from the sales order for the Packing Items
# Check whether the material request has the correct packing item or not.
if not frappe.db.exists("Item", "_Test Product Bundle Item New 1"):
bundle_item = make_item("_Test Product Bundle Item New 1", {"is_stock_item": 0})
bundle_item.append(
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
)
bundle_item.save(ignore_permissions=True)
make_item("_Packed Item New 2", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle Item New 1", ["_Packed Item New 2"], 2)
so = make_sales_order(
item_code="_Test Product Bundle Item New 1",
)
mr = make_material_request(so.name)
self.assertEqual(mr.items[0].item_code, "_Packed Item New 2")
def test_bin_details_of_packed_item(self):
# test Update Items with product bundle
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):

View File

@@ -82,6 +82,7 @@
"actual_qty",
"ordered_qty",
"planned_qty",
"production_plan_qty",
"column_break_69",
"work_order_qty",
"delivered_qty",
@@ -860,12 +861,19 @@
"fieldname": "material_request_item",
"fieldtype": "Data",
"label": "Material Request Item"
},
{
"fieldname": "production_plan_qty",
"fieldtype": "Float",
"label": "Production Plan Qty",
"no_copy": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-12-25 02:51:10.247569",
"modified": "2023-07-28 14:56:42.031636",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -6,12 +6,9 @@ import json
from datetime import date
import frappe
from babel import Locale
from frappe import _, throw
from frappe.model.document import Document
from frappe.utils import formatdate, getdate, today
from holidays import country_holidays
from holidays.utils import list_supported_countries
class OverlapError(frappe.ValidationError):
@@ -40,6 +37,8 @@ class HolidayList(Document):
@frappe.whitelist()
def get_supported_countries(self):
from holidays.utils import list_supported_countries
subdivisions_by_country = list_supported_countries()
countries = [
{"value": country, "label": local_country_name(country)}
@@ -52,6 +51,8 @@ class HolidayList(Document):
@frappe.whitelist()
def get_local_holidays(self):
from holidays import country_holidays
if not self.country:
throw(_("Please select a country"))
@@ -169,4 +170,6 @@ def is_holiday(holiday_list, date=None):
def local_country_name(country_code: str) -> str:
"""Return the localized country name for the given country code."""
return Locale.parse(frappe.local.lang).territories.get(country_code, country_code)
from babel import Locale
return Locale.parse(frappe.local.lang, sep="-").territories.get(country_code, country_code)

View File

@@ -8,6 +8,8 @@ from datetime import date, timedelta
import frappe
from frappe.utils import getdate
from erpnext.setup.doctype.holiday_list.holiday_list import local_country_name
class TestHolidayList(unittest.TestCase):
def test_holiday_list(self):
@@ -58,6 +60,16 @@ class TestHolidayList(unittest.TestCase):
self.assertIn(date(2023, 4, 10), holidays)
self.assertNotIn(date(2023, 5, 1), holidays)
def test_localized_country_names(self):
lang = frappe.local.lang
frappe.local.lang = "en-gb"
self.assertEqual(local_country_name("IN"), "India")
self.assertEqual(local_country_name("DE"), "Germany")
frappe.local.lang = "de"
self.assertEqual(local_country_name("DE"), "Deutschland")
frappe.local.lang = lang
def make_holiday_list(
name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None

View File

@@ -24,9 +24,6 @@ class ItemGroup(NestedSet, WebsiteGenerator):
no_breadcrumbs=1,
)
def autoname(self):
self.name = self.item_group_name
def validate(self):
super(ItemGroup, self).validate()
@@ -76,7 +73,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
return self.route
def on_trash(self):
NestedSet.on_trash(self)
NestedSet.on_trash(self, allow_root_deletion=True)
WebsiteGenerator.on_trash(self)
self.delete_child_item_groups_key()

View File

@@ -11,6 +11,7 @@ def get_data():
},
"internal_links": {
"Sales Order": ["items", "against_sales_order"],
"Sales Invoice": ["items", "against_sales_invoice"],
"Material Request": ["items", "material_request"],
"Purchase Order": ["items", "purchase_order"],
},

View File

@@ -395,16 +395,16 @@ class Item(Document):
def validate_warehouse_for_reorder(self):
"""Validate Reorder level table for duplicate and conditional mandatory"""
warehouse = []
warehouse_material_request_type: list[tuple[str, str]] = []
for d in self.get("reorder_levels"):
if not d.warehouse_group:
d.warehouse_group = d.warehouse
if d.get("warehouse") and d.get("warehouse") not in warehouse:
warehouse += [d.get("warehouse")]
if (d.get("warehouse"), d.get("material_request_type")) not in warehouse_material_request_type:
warehouse_material_request_type += [(d.get("warehouse"), d.get("material_request_type"))]
else:
frappe.throw(
_("Row {0}: An Reorder entry already exists for this warehouse {1}").format(
d.idx, d.warehouse
_("Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.").format(
d.idx, d.warehouse, d.material_request_type
),
DuplicateReorderRows,
)

View File

@@ -228,7 +228,8 @@ class MaterialRequest(BuyingController):
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
if mr_qty_allowance:
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty"))
if d.ordered_qty and d.ordered_qty > allowed_qty:
frappe.throw(
_(

View File

@@ -45,7 +45,7 @@ class PriceList(Document):
doc_before_save = self.get_doc_before_save()
currency_changed = self.currency != doc_before_save.currency
affects_cart = self.name == frappe.get_cached_value("E Commerce Settings", None, "price_list")
affects_cart = self.name == frappe.db.get_single_value("E Commerce Settings", "price_list")
if currency_changed and affects_cart:
validate_cart_settings()

View File

@@ -1258,6 +1258,70 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(query[0].value, 0)
def test_rejected_qty_for_internal_transfer(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
rejected_warehouse = create_warehouse(
"_Test Rejected Internal To Warehouse New", company=company
)
item_doc = make_item(
"Test Internal Transfer Item DS",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "SBNS.#####",
},
)
target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
pr = make_purchase_receipt(
item_code=item_doc.name,
company=company,
posting_date=add_days(today(), -1),
warehouse=from_warehouse,
qty=2,
rate=100,
)
dn1 = create_delivery_note(
item_code=item_doc.name,
company=company,
customer=customer,
serial_no=pr.items[0].serial_no,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=2,
rate=500,
warehouse=from_warehouse,
target_warehouse=target_warehouse,
)
sns = get_serial_nos(dn1.items[0].serial_no)
self.assertEqual(len(sns), 2)
pr1 = make_inter_company_purchase_receipt(dn1.name)
pr1.items[0].qty = 1.0
pr1.items[0].rejected_qty = 1.0
pr1.items[0].serial_no = sns[0]
pr1.items[0].rejected_serial_no = sns[1]
pr1.items[0].warehouse = to_warehouse
pr1.items[0].rejected_warehouse = rejected_warehouse
pr1.submit()
rejected_serial_no_wh = frappe.get_cached_value("Serial No", sns[1], "warehouse")
self.assertEqual(rejected_warehouse, rejected_serial_no_wh)
def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_receipt(
self,
):

View File

@@ -194,7 +194,7 @@ class StockEntry(StockController):
return False
# If line items are more than 100 or record is older than 6 months
if len(self.items) > 100 or month_diff(nowdate(), self.posting_date) > 6:
if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6:
return True
return False

View File

@@ -607,7 +607,7 @@ class StockReconciliation(StockController):
)
if sl_entries:
self.make_sl_entries(sl_entries)
self.make_sl_entries(sl_entries, allow_negative_stock=True)
def get_batch_qty_for_stock_reco(

View File

@@ -16,7 +16,7 @@ def execute(filters=None):
if not filters:
filters = {}
sle_count = frappe.db.count("Stock Ledger Entry", {"is_cancelled": 0})
sle_count = frappe.db.count("Stock Ledger Entry")
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))

View File

@@ -430,10 +430,9 @@ class StockBalanceReport(object):
{
"label": _("Valuation Rate"),
"fieldname": "val_rate",
"fieldtype": "Currency",
"fieldtype": "Float",
"width": 90,
"convertible": "rate",
"options": "currency",
},
{
"label": _("Company"),

View File

@@ -196,7 +196,7 @@ def get_columns(filters):
{
"label": _("Avg Rate (Balance Stock)"),
"fieldname": "valuation_rate",
"fieldtype": "Currency",
"fieldtype": "Float",
"width": 180,
"options": "Company:company:default_currency",
"convertible": "rate",
@@ -204,7 +204,7 @@ def get_columns(filters):
{
"label": _("Valuation Rate"),
"fieldname": "in_out_rate",
"fieldtype": "Currency",
"fieldtype": "Float",
"width": 140,
"options": "Company:company:default_currency",
"convertible": "rate",

View File

@@ -74,6 +74,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
args = sle_doc.as_dict()
args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False
if sle.get("voucher_type") == "Stock Reconciliation":
# preserve previous_qty_after_transaction for qty reposting
@@ -109,6 +110,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou
"sle_id": args.get("name"),
"creation": args.get("creation"),
},
allow_zero_rate=args.get("allow_zero_valuation_rate") or False,
allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher,
)
@@ -288,6 +290,8 @@ def update_args_in_repost_item_valuation(
frappe.publish_realtime(
"item_reposting_progress",
{"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
doctype=doc.doctype,
docname=doc.name,
)

View File

@@ -594,6 +594,67 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertNotEqual(scr.supplied_items[0].rate, prev_cost)
self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate)
def test_subcontracting_receipt_raw_material_rate(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
# Step - 1: Set Backflush Based On as "BOM"
set_backflush_based_on("BOM")
# Step - 2: Create FG and RM Items
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
rm_item1 = make_item(properties={"is_stock_item": 1}).name
rm_item2 = make_item(properties={"is_stock_item": 1}).name
# Step - 3: Create BOM for FG Item
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
for rm_item in bom.items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
bom = bom.name
# Step - 4: Create PO and SCO
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 100,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 100,
},
]
sco = get_subcontracting_order(service_items=service_items)
for rm_item in sco.supplied_items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
# Step - 5: Inward Raw Materials
rm_items = get_rm_items(sco.supplied_items)
for rm_item in rm_items:
rm_item["rate"] = 100
itemwise_details = make_stock_in_entry(rm_items=rm_items)
# Step - 6: Transfer RM's to Subcontractor
se = make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
for item in se.items:
self.assertEqual(item.qty, 100)
self.assertEqual(item.basic_rate, 100)
self.assertEqual(item.amount, item.qty * item.basic_rate)
# Step - 7: Create Subcontracting Receipt
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.submit()
scr.load_from_db()
for rm_item in scr.supplied_items:
self.assertEqual(rm_item.consumed_qty, 100)
self.assertEqual(rm_item.rate, 100)
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Jy kan nie projektipe &#39;eksterne&#39; uitvee nie,
You cannot edit root node.,U kan nie wortelknoop wysig nie.,
You cannot restart a Subscription that is not cancelled.,U kan nie &#39;n intekening herlaai wat nie gekanselleer is nie.,
You don't have enought Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie,
You don't have enough Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie,
You have already assessed for the assessment criteria {}.,U het reeds geassesseer vir die assesseringskriteria ().,
You have already selected items from {0} {1},Jy het reeds items gekies van {0} {1},
You have been invited to collaborate on the project: {0},U is genooi om saam te werk aan die projek: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',የፕሮጀክት አይነት «ውጫዊ» ን መሰረዝ አይችሉም.,
You cannot edit root node.,የስር ሥፍራ ማረም አይችሉም.,
You cannot restart a Subscription that is not cancelled.,የማይሰረዝ የደንበኝነት ምዝገባን ዳግም ማስጀመር አይችሉም.,
You don't have enought Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም,
You don't have enough Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም,
You have already assessed for the assessment criteria {}.,ቀድሞውንም ግምገማ መስፈርት ከገመገምን {}.,
You have already selected items from {0} {1},ከዚህ ቀደም ከ ንጥሎች ተመርጠዋል ሊሆን {0} {1},
You have been invited to collaborate on the project: {0},እርስዎ ፕሮጀክት ላይ ተባበር ተጋብዘዋል: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',لا يمكنك حذف مشروع من نوع 'خارجي',
You cannot edit root node.,لا يمكنك تحرير عقدة الجذر.,
You cannot restart a Subscription that is not cancelled.,لا يمكنك إعادة تشغيل اشتراك غير ملغى.,
You don't have enought Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها,
You don't have enough Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها,
You have already assessed for the assessment criteria {}.,لقد سبق أن قيمت معايير التقييم {}.,
You have already selected items from {0} {1},لقد حددت العناصر من {0} {1},
You have been invited to collaborate on the project: {0},لقد وجهت الدعوة إلى التعاون في هذا المشروع: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Не можете да изтриете Тип на проекта &quot;Външен&quot;,
You cannot edit root node.,Не можете да редактирате корен възел.,
You cannot restart a Subscription that is not cancelled.,"Не можете да рестартирате абонамент, който не е анулиран.",
You don't have enought Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите",
You don't have enough Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите",
You have already assessed for the assessment criteria {}.,Вече оценихте критериите за оценка {}.,
You have already selected items from {0} {1},Вие вече сте избрали елементи от {0} {1},
You have been invited to collaborate on the project: {0},Вие сте били поканени да си сътрудничат по проекта: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',আপনি প্রকল্প প্রকার &#39;বহিরাগত&#39; মুছে ফেলতে পারবেন না,
You cannot edit root node.,আপনি রুট নোড সম্পাদনা করতে পারবেন না।,
You cannot restart a Subscription that is not cancelled.,আপনি সাবস্ক্রিপশনটি বাতিল না করা পুনরায় শুরু করতে পারবেন না,
You don't have enought Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enought না,
You don't have enough Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enough না,
You have already assessed for the assessment criteria {}.,"আপনি ইতিমধ্যে মূল্যায়ন মানদণ্ডের জন্য মূল্যায়ন করে নিলে, {}।",
You have already selected items from {0} {1},আপনি ইতিমধ্যে থেকে আইটেম নির্বাচন করা আছে {0} {1},
You have been invited to collaborate on the project: {0},আপনি প্রকল্পের সহযোগীতা করার জন্য আমন্ত্রণ জানানো হয়েছে: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Ne možete obrisati tip projekta &#39;Spoljni&#39;,
You cannot edit root node.,Ne možete uređivati root čvor.,
You cannot restart a Subscription that is not cancelled.,Ne možete ponovo pokrenuti pretplatu koja nije otkazana.,
You don't have enought Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup,
You don't have enough Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup,
You have already assessed for the assessment criteria {}.,Ste već ocijenili za kriterije procjene {}.,
You have already selected items from {0} {1},Vi ste već odabrane stavke iz {0} {1},
You have been invited to collaborate on the project: {0},Vi ste pozvani da surađuju na projektu: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',No es pot eliminar el tipus de projecte &#39;Extern&#39;,
You cannot edit root node.,No podeu editar el node arrel.,
You cannot restart a Subscription that is not cancelled.,No podeu reiniciar una subscripció que no es cancel·la.,
You don't have enought Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar,
You don't have enough Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar,
You have already assessed for the assessment criteria {}.,Vostè ja ha avaluat pels criteris d&#39;avaluació {}.,
You have already selected items from {0} {1},Ja ha seleccionat articles de {0} {1},
You have been invited to collaborate on the project: {0},Se li ha convidat a col·laborar en el projecte: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Nelze odstranit typ projektu &quot;Externí&quot;,
You cannot edit root node.,Nelze upravit kořenový uzel.,
You cannot restart a Subscription that is not cancelled.,"Nelze znovu spustit odběr, který není zrušen.",
You don't have enought Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění,
You don't have enough Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění,
You have already assessed for the assessment criteria {}.,Již jste hodnotili kritéria hodnocení {}.,
You have already selected items from {0} {1},Již jste vybrané položky z {0} {1},
You have been invited to collaborate on the project: {0},Byli jste pozváni ke spolupráci na projektu: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Du kan ikke slette Project Type 'Ekstern',
You cannot edit root node.,Du kan ikke redigere root node.,
You cannot restart a Subscription that is not cancelled.,"Du kan ikke genstarte en abonnement, der ikke annulleres.",
You don't have enought Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse,
You don't have enough Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse,
You have already assessed for the assessment criteria {}.,Du har allerede vurderet for bedømmelseskriterierne {}.,
You have already selected items from {0} {1},Du har allerede valgt elementer fra {0} {1},
You have been invited to collaborate on the project: {0},Du er blevet inviteret til at samarbejde om sag: {0},
Can't render this file because it is too large.

View File

@@ -1147,6 +1147,7 @@ Get Items from Prescriptions,Holen Sie sich Artikel aus Verordnungen,
Get Items from Product Bundle,Artikel aus dem Produkt-Bundle übernehmen,
Get Suppliers,Holen Sie sich Lieferanten,
Get Suppliers By,Holen Sie sich Lieferanten durch,
Get Supplier Group Details,Werte aus Lieferantengruppe übernehmen,
Get Updates,Newsletter abonnieren,
Get customers from,Holen Sie Kunden von,
Get from Patient Encounter,Von der Patientenbegegnung erhalten,
@@ -3351,7 +3352,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Sie können den Projekttyp &#39;Extern&#39; nicht löschen,
You cannot edit root node.,Sie können den Stammknoten nicht bearbeiten.,
You cannot restart a Subscription that is not cancelled.,Sie können ein nicht abgebrochenes Abonnement nicht neu starten.,
You don't have enought Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen,
You don't have enough Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen,
You have already assessed for the assessment criteria {}.,Sie haben bereits für die Bewertungskriterien beurteilt.,
You have already selected items from {0} {1},Sie haben bereits Elemente aus {0} {1} gewählt,
You have been invited to collaborate on the project: {0},Sie wurden zur Zusammenarbeit für das Projekt {0} eingeladen.,
@@ -5066,6 +5067,7 @@ Credit To,Gutschreiben auf,
Party Account Currency,Währung des Kontos der Partei,
Against Expense Account,Zu Aufwandskonto,
Inter Company Invoice Reference,Unternehmensübergreifende Rechnungsreferenz,
Internal Supplier,Interner Lieferant,
Is Internal Supplier,Ist interner Lieferant,
Start date of current invoice's period,Startdatum der laufenden Rechnungsperiode,
End date of current invoice's period,Schlußdatum der laufenden Eingangsrechnungsperiode,
@@ -5514,6 +5516,8 @@ Tracking,Verfolgung,
Ref SQ,Ref-SQ,
Inter Company Order Reference,Inter Company Bestellreferenz,
Supplier Part Number,Lieferanten-Artikelnummer,
Supplier Primary Contact,Hauptkontakt des Lieferanten,
Supplier Primary Address,Hauptadresse des Lieferanten,
Billed Amt,Rechnungsbetrag,
Warehouse and Reference,Lager und Referenz,
To be delivered to customer,Zur Auslieferung an den Kunden,
@@ -7672,7 +7676,7 @@ Accounts Manager,Buchhalter,
Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag,
Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein,
Default Price List,Standardpreisliste,
Primary Address and Contact Detail,Primäre Adresse und Kontaktdetails,
Primary Address and Contact,Hauptadresse und -kontakt,
"Select, to make the customer searchable with these fields","Wählen Sie, um den Kunden mit diesen Feldern durchsuchbar zu machen",
Customer Primary Contact,Hauptkontakt des Kunden,
"Reselect, if the chosen contact is edited after save","Wählen Sie erneut, wenn der ausgewählte Kontakt nach dem Speichern bearbeitet wird",
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Δεν μπορείτε να διαγράψετε τον τύπο έργου &#39;Εξωτερικό&#39;,
You cannot edit root node.,Δεν μπορείτε να επεξεργαστείτε τον κόμβο ρίζας.,
You cannot restart a Subscription that is not cancelled.,Δεν μπορείτε να κάνετε επανεκκίνηση μιας συνδρομής που δεν ακυρώνεται.,
You don't have enought Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση,
You don't have enough Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση,
You have already assessed for the assessment criteria {}.,Έχετε ήδη αξιολογήσει τα κριτήρια αξιολόγησης {}.,
You have already selected items from {0} {1},Έχετε ήδη επιλεγμένα αντικείμενα από {0} {1},
You have been invited to collaborate on the project: {0},Έχετε προσκληθεί να συνεργαστούν για το έργο: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',No puede eliminar Tipo de proyecto &#39;Externo&#39;,
You cannot edit root node.,No puedes editar el nodo raíz.,
You cannot restart a Subscription that is not cancelled.,No puede reiniciar una suscripción que no está cancelada.,
You don't have enought Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear,
You don't have enough Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear,
You have already assessed for the assessment criteria {}.,Ya ha evaluado los criterios de evaluación {}.,
You have already selected items from {0} {1},Ya ha seleccionado artículos de {0} {1},
You have been invited to collaborate on the project: {0},Se le ha invitado a colaborar en el proyecto: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Te ei saa projekti tüübi &quot;Väline&quot; kustutada,
You cannot edit root node.,Sa ei saa redigeerida juursõlme.,
You cannot restart a Subscription that is not cancelled.,Te ei saa tellimust uuesti katkestada.,
You don't have enought Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada",
You don't have enough Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada",
You have already assessed for the assessment criteria {}.,Olete juba hinnanud hindamise kriteeriumid {}.,
You have already selected items from {0} {1},Olete juba valitud objektide {0} {1},
You have been invited to collaborate on the project: {0},Sind on kutsutud koostööd projekti: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',شما نمیتوانید نوع پروژه «خارجی» را حذف کنید,
You cannot edit root node.,نمی توانید گره ریشه را ویرایش کنید,
You cannot restart a Subscription that is not cancelled.,شما نمی توانید اشتراک را لغو کنید.,
You don't have enought Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید,
You don't have enough Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید,
You have already assessed for the assessment criteria {}.,شما در حال حاضر برای معیارهای ارزیابی ارزیابی {}.,
You have already selected items from {0} {1},شما در حال حاضر اقلام از انتخاب {0} {1},
You have been invited to collaborate on the project: {0},از شما دعوت شده برای همکاری در این پروژه: {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Et voi poistaa projektityyppiä &quot;Ulkoinen&quot;,
You cannot edit root node.,Et voi muokata juurisolmua.,
You cannot restart a Subscription that is not cancelled.,"Et voi uudelleenkäynnistää tilausta, jota ei peruuteta.",
You don't have enought Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi,
You don't have enough Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi,
You have already assessed for the assessment criteria {}.,Olet jo arvioitu arviointikriteerit {}.,
You have already selected items from {0} {1},Olet jo valitut kohteet {0} {1},
You have been invited to collaborate on the project: {0},Sinut on kutsuttu yhteistyöhön projektissa {0},
Can't render this file because it is too large.

View File

@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
You cannot delete Project Type 'External',Vous ne pouvez pas supprimer le Type de Projet 'Externe',
You cannot edit root node.,Vous ne pouvez pas modifier le nœud racine.,
You cannot restart a Subscription that is not cancelled.,Vous ne pouvez pas redémarrer un abonnement qui n'est pas annulé.,
You don't have enought Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger,
You don't have enough Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger,
You have already assessed for the assessment criteria {}.,Vous avez déjà évalué les critères d'évaluation {}.,
You have already selected items from {0} {1},Vous avez déjà choisi des articles de {0} {1},
You have been invited to collaborate on the project: {0},Vous avez été invité à collaborer sur le projet : {0},
Can't render this file because it is too large.

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