Compare commits

...

283 Commits

Author SHA1 Message Date
RitvikSardana
3714b795d6 fix: POS opening issue because of Product Bundle 2023-09-18 17:22:02 +05:30
rohitwaghchaure
e62b783f34 fix: don't set from warehouse for purchase material request (#37132) 2023-09-18 16:10:21 +05:30
ruthra kumar
8ef548f999 Merge pull request #37129 from frappe/mergify/bp/version-14-hotfix/pr-37127
refactor: better date filters in `Get Outstanding Invoices` dialog (backport #37127)
2023-09-18 13:51:48 +05:30
ruthra kumar
4b700b726f refactor: better date filters in Get Outstanding Invoices dialog
(cherry picked from commit 9004721859)
2023-09-18 07:52:18 +00:00
mergify[bot]
c41cb3930c fix: Don't allow merging accounts with different currency (#37074)
* fix: Don't allow merging accounts with different currency (#37074)

* fix: Don't allow merging accounts with different currency

* test: Update conflicting values

* test: Update conflicting values

(cherry picked from commit 5e21e7cd1d)

# Conflicts:
#	erpnext/accounts/doctype/account/account.js
#	erpnext/accounts/doctype/account/account.py

* chore: resolve conflicts

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-09-18 13:03:08 +05:30
ruthra kumar
46f94cf387 Merge pull request #37125 from frappe/mergify/bp/version-14-hotfix/pr-33502
feat: Toggle display of Account Balance in Chart of Accounts (backport #33502)
2023-09-18 11:08:51 +05:30
ruthra kumar
79321f56ca chore: resolve conflicts 2023-09-18 10:39:22 +05:30
ruthra kumar
18702841af refactor: Show Balance in COA based on Accounts Settings
(cherry picked from commit 23fbe86d51)
2023-09-18 04:44:55 +00:00
ruthra kumar
8b2328c6d3 refactor: show balance checkbox in Accounts Settings
(cherry picked from commit 1b78fae6fc)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
2023-09-18 04:44:55 +00:00
mergify[bot]
13aaff30a5 fix: company wise deferred accounting fields in item (#37023)
* fix: company wise deferred accounting fields in item (#37023)

* fix: move deferred accounts in accounting section

* fix: move deferred check boxes in item accounting

* fix: show company wise acc in filters

* fix: fetch item deferred account from child table

* fix: tests using deferred acc

* refactor: use cached value

* fix: cached value call

* feat: patch to migrate deferred acc

* fix: hardcode education module doctypes in patch

* chore: resolve conflicts

---------

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

# Conflicts:
#	erpnext/patches.txt
#	erpnext/patches/v14_0/delete_education_doctypes.py
#	erpnext/stock/doctype/item/item.json

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-09-18 09:57:11 +05:30
HENRY Florian
a563fed6dc fix(ux): move get_route_options_for_new_doc to refresh (#37092)
fix: move `get_route_options_for_new_doc` to `refresh`
2023-09-16 15:17:26 +05:30
s-aga-r
19a227a970 Merge pull request #37106 from s-aga-r/FIX-1334
fix: validate duplicate serial no in DN
2023-09-16 10:56:16 +05:30
mergify[bot]
727dcc5034 fix: ignore user permissions for Source Warehouse in MR (backport #37102) (#37110)
fix: ignore user permissions for `Source Warehouse` in MR (#37102)

fix: ignore user permissions for Source Warehouse in MR
(cherry picked from commit fc016680c9)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-15 21:47:15 +05:30
ruthra kumar
89b570ecf5 Merge pull request #37109 from frappe/mergify/bp/version-14-hotfix/pr-37108
fix: asset validation misfire on debit notes (backport #37108)
2023-09-15 21:44:45 +05:30
ruthra kumar
b33db6c79a fix: asset validation misfire on debit notes
(cherry picked from commit 097b9892dc)
2023-09-15 14:32:41 +00:00
s-aga-r
e5177a6e46 test: add test case for DN duplicate serial nos 2023-09-15 19:45:02 +05:30
s-aga-r
fffa13f22b fix: validate duplicate serial no in DN 2023-09-15 17:30:04 +05:30
rohitwaghchaure
f2395a9297 fix: precision issue and column name (#37073) 2023-09-14 14:28:05 +05:30
mergify[bot]
3ecdf028f2 fix: Remove redundant code (#37001)
fix: Remove redundant code (#37001)

fix: Remove redundant code
(cherry picked from commit 96363dbb07)

Co-authored-by: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com>
2023-09-13 21:03:45 +05:30
mergify[bot]
8772e40bae fix: Purchase Receipt Provisional Accounting GL Entries (backport #37046) (#37068)
* fix: Purchase Receipt Provisional Accounting GL Entries

(cherry picked from commit 6bab0eeaa1)

* test: Purchase Receipt Provisional Accounting GL Entries

(cherry picked from commit 1c78a5a9aa)

* fix(test): PR Provisional Accounting

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-13 18:16:33 +05:30
mergify[bot]
b56c9b91f1 fix: accepted warehouse and rejected warehouse can't be same (backport #36973) (#37071)
* fix: ignore user permissions for `From Warehouse` in PR

(cherry picked from commit d2f3286115)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.json
#	erpnext/buying/doctype/purchase_order_item/purchase_order_item.json

* chore: `conflicts`

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-13 11:26:13 +00:00
mergify[bot]
c2a0c1e989 fix: + btn not appearing for delivery note connection (backport #36980) (#37070)
fix: move SI and DI connected links to internal_and_external_links

(cherry picked from commit e1a94a9ba1)

Co-authored-by: anandbaburajan <anandbaburajan@gmail.com>
2023-09-13 16:06:08 +05:30
Deepesh Garg
461b607a6a Merge pull request #37060 from frappe/mergify/bp/version-14-hotfix/pr-37055
fix: Apply dimension filter, irrespective of dimension columns (#37055)
2023-09-13 11:20:39 +05:30
Deepesh Garg
9bc44a3b40 fix: Apply dimension filter, irrespective of dimesion columns
(cherry picked from commit 769db0b3bc)
2023-09-13 04:36:57 +00:00
ruthra kumar
50cfc68d2c Merge pull request #37058 from frappe/mergify/bp/version-14-hotfix/pr-37057
fix: Packed item incorrectly picks expired price on Sales Order (backport #37057)
2023-09-13 08:47:13 +05:30
ruthra kumar
aa0a756111 test: expired item price should not be picked
(cherry picked from commit 055156d28a)
2023-09-13 02:50:24 +00:00
ruthra kumar
413b40f5a7 fix: packed item using expired price
(cherry picked from commit 47ffa4983c)
2023-09-13 02:50:23 +00:00
rohitwaghchaure
d278b11603 feat: provision to set required by from Production Plan (#37039)
* feat: provision to set the Required By date from production plan

* test: added test case for validate schedule_date
2023-09-12 13:32:56 +05:30
mergify[bot]
66027877d3 fix: Parent Task link with Project Task (backport #37025) (#37033)
* feat: new field in `Task` to hold ref of Template Task

(cherry picked from commit b4bcd9ba3f)

# Conflicts:
#	erpnext/projects/doctype/task/task.json

* fix: set `Template Task` ref in `Project Task`

(cherry picked from commit d3295c43e3)

* fix: reload task before save

(cherry picked from commit 5cae2e79bd)

* test: add test case for Task having common subject

(cherry picked from commit 0d5c8f03bd)

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-12 09:04:37 +05:30
Deepesh Garg
1492ce507c Merge pull request #37011 from frappe/mergify/bp/version-14-hotfix/pr-36843
fix: show customer name for naming series in process soa (#36843)
2023-09-11 21:52:57 +05:30
mergify[bot]
21be889a77 fix(ux): docstatus filter for Reference Name in QI (backport #37024) (#37028)
fix(ux): docstatus filter for `Reference Name` in QI (#37024)

(cherry picked from commit d739ab6ee3)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-11 18:56:20 +05:30
Gursheen Kaur Anand
a35abf8403 chore: linting issues 2023-09-11 12:06:24 +05:30
Gursheen Kaur Anand
619644af04 chore: resolve conflicts 2023-09-11 11:25:30 +05:30
mergify[bot]
acd9c69201 feat: Add half-yearly asset maintenance periodicity. (backport #37006) (#37014)
feat: Add half-yearly asset maintenance periodicity. (#37006)

(cherry picked from commit 846ae32d92)

Co-authored-by: Bernd Oliver Sünderhauf <46800703+bosue@users.noreply.github.com>
2023-09-10 17:26:41 +05:30
Gursheen Anand
284181d766 fix: remove report field db set
(cherry picked from commit 060da2c5bc)
2023-09-10 06:51:58 +00:00
Gursheen Anand
f9f1ac3601 test: auto email for ar report
(cherry picked from commit a006b66e45)
2023-09-10 06:51:58 +00:00
Gursheen Anand
53270dd933 fix: generate pdf only when result exists
(cherry picked from commit f07f4ce86f)
2023-09-10 06:51:57 +00:00
Gursheen Anand
657ca7ff22 feat: add field for specifying pdf name
(cherry picked from commit 5c2a949593)

# Conflicts:
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
2023-09-10 06:51:57 +00:00
Gursheen Anand
2077b2cde4 fix: show letterhead and terms for AR pdf
(cherry picked from commit 0a9187ea42)
2023-09-10 06:51:57 +00:00
Deepesh Garg
7ea53cc316 Merge pull request #36964 from GursheenK/test_tds_payable_monthly
test: TDS payable monthly report
2023-09-10 11:42:19 +05:30
Deepesh Garg
7acf732a9c Merge pull request #36976 from frappe/mergify/bp/version-14-hotfix/pr-36898
fix: `company` is ambiguous (#36898)
2023-09-10 11:41:46 +05:30
Deepesh Garg
43d9a10093 Merge pull request #36985 from deepeshgarg007/employee_loan_repayment
fix: Update party type for payroll payable account
2023-09-07 10:17:19 +05:30
Anand Baburajan
2ae4463b76 fix: correct asset daily depr schedule calculation [v14] (#36991)
fix: correct asset daily depr schedule calculation
2023-09-06 22:20:29 +05:30
Deepesh Garg
62569b1b41 Merge pull request #36986 from frappe/mergify/bp/version-14-hotfix/pr-36983
chore: Update employee for tests (backport #36983)
2023-09-06 18:01:17 +05:30
Deepesh Garg
24a4815250 chore: Update employee for tests
(cherry picked from commit ae01d70b33)
2023-09-06 11:02:18 +00:00
Deepesh Garg
1894371b68 chore: Update function 2023-09-06 16:29:12 +05:30
mergify[bot]
e210b28f0d chore: asset finance books validation (backport #36979) (#36981)
* chore: asset finance books validation (#36979)

(cherry picked from commit 0077659e93)

* chore: fix tests

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-09-06 15:39:46 +05:30
Deepesh Garg
f251d6cb69 fix: Update party type for payroll payable account 2023-09-06 15:18:21 +05:30
mergify[bot]
4fede56d98 fix: use primary key for link lookup (backport #36919) (#36978)
fix: use primary key for link lookup (#36919)

(cherry picked from commit 8ce6b8179e)

Co-authored-by: Devin Slauenwhite <devin.slauenwhite@gmail.com>
2023-09-06 06:45:58 +00:00
Dany Robert
fe69d5364d fix: company is ambiguous
(cherry picked from commit 3e1065a561)
2023-09-06 04:58:54 +00:00
mergify[bot]
58163d5aa8 fix: ask for asset related accounts only when needed (backport #36960) (#36971)
fix: ask for asset related accounts only when needed (#36960)

* fix: only ask for asset_received_but_not_billed account when needed

* chore: remove unnecessary if condition

* fix: only ask for expenses_included_in_asset_valuation account when needed

(cherry picked from commit 174f95d699)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-09-05 18:14:44 +05:30
Gursheen Anand
dbeb132688 refactor: use accounts mixin 2023-09-05 17:33:30 +05:30
mergify[bot]
e3d64fc553 fix: index error on Receivable report based on payment terms (#36963)
fix: index error on Receivable report based on payment terms

cr note's don't have payment terms. So, skip for them.

(cherry picked from commit b9c556c4a9)

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2023-09-05 16:32:58 +05:30
mergify[bot]
c125dea0f1 fix: ignore mandatory fields while saving WO (backport #36954) (#36970)
fix: ignore mandatory fields while saving WO (#36954)

(cherry picked from commit f809e12747)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-05 16:29:03 +05:30
mergify[bot]
119273639c fix: added validation for unique serial numbers in pos invoice (#36302)
fix: added validation for unique serial numbers in pos invoice (#36302)

* fix: added validation for unique serial numbers in pos invoice

* fix: updated title of validation

* fix: removed extra whitespace

* fix: added validation for duplicate batch numbers

---------

Co-authored-by: Ritvik Sardana <ritviksardana@Ritviks-MacBook-Air.local>
(cherry picked from commit a165b37fd7)

Co-authored-by: RitvikSardana <65544983+RitvikSardana@users.noreply.github.com>
2023-09-05 16:21:33 +05:30
Gursheen Kaur Anand
fc79af5926 fix: prorate factor for subscription plan (#36953) 2023-09-05 16:14:16 +05:30
Gursheen Anand
035eaa5e40 test: tds payable monthly 2023-09-05 15:24:59 +05:30
Anand Baburajan
09e2f24329 fix: allow payment_account of loan repayment to be edited (#36948) 2023-09-05 12:44:14 +05:30
ruthra kumar
a0c65342a1 Merge pull request #36952 from frappe/mergify/bp/version-14-hotfix/pr-36950
refactor: gain/loss je should use same posting date as payment (backport #36950)
2023-09-05 09:24:10 +05:30
ruthra kumar
01eae2b758 refactor: gain/loss should use same posting date as payment
(cherry picked from commit f7865da4d2)
2023-09-05 03:00:02 +00:00
ruthra kumar
b43c6ff1ae Merge pull request #36946 from GursheenK/tds_payable_monthly_si_withholding_category
fix: TDS payable monthly SI withholding category
2023-09-04 19:01:00 +05:30
Gursheen Anand
18f8f7f09c fix: remove withholding category from common fields 2023-09-04 17:30:10 +05:30
ruthra kumar
2d2bcd37cb Merge pull request #36942 from frappe/mergify/bp/version-14-hotfix/pr-36940
fix: invalid gain/loss JE created on base currency Expense Claim (backport #36940)
2023-09-04 15:20:23 +05:30
ruthra kumar
068f1b5a6b fix: invalid gain/loss JE created on base currency Expense Claim
(cherry picked from commit 75d95acb23)
2023-09-04 08:27:37 +00:00
Ankush Menat
fca154fd60 Merge pull request #36939 from frappe/mergify/bp/version-14-hotfix/pr-36869
fix: ignore_user_permissions set to 1 for parent field of tree doctypes (backport #36869)
2023-09-04 13:27:13 +05:30
RitvikSardana
451cc7bc12 fix: added ignore_user_permissions to parent field of tree doctypes
(cherry picked from commit de433d8626)
2023-09-04 07:17:07 +00:00
mergify[bot]
11e67c7dc0 refactor: remove Recalculate Rate from SCR Item (backport #36929) (#36931)
* refactor: remove `Recalculate Rate` from SCR Item (#36929)

(cherry picked from commit cd8ddae7c5)

# Conflicts:
#	erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
#	erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
#	erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-03 19:05:05 +05:30
mergify[bot]
5c8bee0a95 fix: when create doc from item dashboard default uom (buying or selling) is not correctly selected (backport #36892) (#36928)
fix: when create doc from item dashboard default uom (buying or selling) is not correctly selected (#36892)

fix: when create doc from item dashboard defaut uom is not correctly selected
(cherry picked from commit 24e1144de5)

Co-authored-by: HENRY Florian <florian.henry@open-concept.pro>
2023-09-03 16:38:44 +05:30
ruthra kumar
553ff11de6 Merge pull request #36925 from frappe/mergify/bp/version-14-hotfix/pr-36911
fix: deduplicate gain/loss JE creation for journals as payment (backport #36911)
2023-09-03 10:56:36 +05:30
ruthra kumar
5523bc5081 chore: resolve merge conflict 2023-09-03 10:28:58 +05:30
ruthra kumar
c8d81cc52d test: cost center inheritance from payment
(cherry picked from commit 0366928db5)
2023-09-03 04:44:17 +00:00
ruthra kumar
d24c8b1bbc refactor: use payment's CC for gain/loss if company default is unset
(cherry picked from commit d6a3b9a5c7)

# Conflicts:
#	erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
2023-09-03 04:44:16 +00:00
ruthra kumar
7fd96d0116 test: extend test to cancellation
(cherry picked from commit 79fa562004)
2023-09-03 04:44:15 +00:00
ruthra kumar
475750302b test: deduplicate gain/loss JE on reconciling journals against inv
(cherry picked from commit cb6da6ec59)
2023-09-03 04:44:14 +00:00
ruthra kumar
9168b3b0e8 fix: deduplicate gain/loss JE creation for journals as payment
(cherry picked from commit 79c6f0165b)

# Conflicts:
#	erpnext/controllers/accounts_controller.py
2023-09-03 04:44:13 +00:00
Anand Baburajan
0ff1546b9b chore: patch to correct asset values if je has workflow [v14] (#36915)
chore: patch to correct asset values if je has workflow
2023-09-02 18:32:16 +05:30
mergify[bot]
345c6084e5 feat(RFQ): optionally send document print (#36363)
feat(RFQ): optionally send document print (#36363)
2023-09-02 17:53:08 +05:30
Deepesh Garg
3820953022 Merge pull request #36917 from frappe/mergify/bp/version-14-hotfix/pr-36900
fix: reduce threshold for background job in PCV (#36900)
2023-09-02 17:46:44 +05:30
Deepesh Garg
78051e7f0f Merge pull request #36918 from frappe/mergify/bp/version-14-hotfix/pr-36908
fix: only show "Unreconcile" if reconciled (#36908)
2023-09-02 17:46:27 +05:30
barredterra
61752ac2b4 fix: only show "Unreconcile" if reconciled
(cherry picked from commit 91e574609f)
2023-09-02 11:43:12 +00:00
Gursheen Anand
5a226a8395 fix: reduce threshold for bg job fn
(cherry picked from commit b6e6f2ef8d)
2023-09-02 11:42:08 +00:00
ruthra kumar
543e4c87ea Merge pull request #36912 from frappe/mergify/bp/version-14-hotfix/pr-36859
fix: account payable currency and value (backport #36859)
2023-09-02 14:38:27 +05:30
Deepesh Garg
0a0b94e1cb Merge pull request #36884 from Nihantra-Patel/fiscal_year_trends_reports
fix: Set the default filter in All Trends Report
2023-09-02 14:37:14 +05:30
RitvikSardana
98c26403c1 fix: account payable currency and value (#36859)
* fix: account payable currency and value

* fix: added party_type and party in accounts payable report

* chore: code cleanup

* fix: customer group test case failure

* fix: added test case of the issue

* fix: filter toggle for party_type

* fix: filter toggle for party_type

* chore: fix typo

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit e599f75a51)
2023-09-02 07:39:33 +00:00
Deepesh Garg
d19716142e Merge pull request #36904 from frappe/mergify/bp/version-14-hotfix/pr-36889
fix: fetch currency in discount accounting SI (#36889)
2023-09-01 22:51:35 +05:30
ruthra kumar
83e3402fea Merge pull request #36907 from frappe/mergify/bp/version-14-hotfix/pr-36899
fix: difference amount calculation logic in Payment Entry UI (backport #36899)
2023-09-01 17:50:22 +05:30
ruthra kumar
9bc2b419e3 fix: difference amount in UI should not be calculated
(cherry picked from commit a7e0709ae8)
2023-09-01 12:09:21 +00:00
ruthra kumar
b9a5d54e03 Merge pull request #36894 from frappe/mergify/bp/version-14-hotfix/pr-36888
fix: calcuate received/paid amount on exchange rate change in Payment Entry (backport #36888)
2023-09-01 17:37:41 +05:30
Gursheen Anand
a8b58800bb fix: fetch discount amount for gle in base currency
(cherry picked from commit 112cfe6dfa)
2023-09-01 09:42:52 +00:00
ruthra kumar
0a632660e0 fix: calcuate received/paid amount on rate change in PE
(cherry picked from commit 64d835374b)
2023-08-31 15:36:44 +00:00
Nihantra C. Patel
132957f59e fix: Set the default filter in All Trends Report 2023-08-31 13:38:42 +05:30
Nihantra C. Patel
420536ca52 fix: Set the default filter in All Trends Report 2023-08-31 13:37:30 +05:30
mergify[bot]
22247cfa17 fix: added valuation field type (Float/Currency) in the filter (backport #36866) (#36868)
fix: added valuation field type (Float/Currency) in the filter (#36866)

(cherry picked from commit dea802dc41)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-08-29 16:57:59 +05:30
mergify[bot]
d2091cc22c fix: create entries for only PR items present in LCV (#36852)
fix: create entries for only PR items present in LCV (#36852)

* fix: check if item code exists in lcv before creating gle

* refactor: use qb to fetch lcv items

(cherry picked from commit 26e8b8f959)

Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
2023-08-29 11:01:40 +05:30
mergify[bot]
9789b7bdef fix: error in report when data is not available to load chart in report (backport #36842) (#36853)
fix: error in report when data is not available to load chart in report (#36842)

(cherry picked from commit 3a2933db4d)

Co-authored-by: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com>
2023-08-28 18:17:50 +05:30
ruthra kumar
e8d7d30682 Merge pull request #36847 from frappe/mergify/bp/version-14-hotfix/pr-36844
fix: allocation error on partial payment against sales order (backport #36844)
2023-08-28 15:43:30 +05:30
ruthra kumar
05f657e690 test: assert rounded amount is calculated
(cherry picked from commit 2fdbe82835)
2023-08-28 09:42:41 +00:00
ruthra kumar
0350c69856 test: allocation err misfire on Sales Order
(cherry picked from commit 67a0969b78)
2023-08-28 09:42:40 +00:00
ruthra kumar
adc87f16a3 fix: fetch rounded total while pulling reference details on SO
(cherry picked from commit 714b8289c1)
2023-08-28 09:42:40 +00:00
mergify[bot]
bd41cb221b fix: Asset Category filter is not working in asset depreciation(#36806)
fix: Asset Category filter is not working in asset depreciation

fix: Asset Category filter is not working in asset depreciation and balances

Co-authored-by: ubuntu <viralkansodiya167@gmail.com>
(cherry picked from commit 388a42ec7e)

Co-authored-by: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com>
2023-08-28 11:53:11 +05:30
ruthra kumar
7a5b454e97 Merge pull request #36832 from frappe/mergify/bp/version-14-hotfix/pr-36830
test: Exchange Rate Revaluation functions and its impact on ledger (backport #36830)
2023-08-27 16:05:03 +05:30
ruthra kumar
256c3c81a4 test: Exchange Rate Revaluation functions and its impact on ledger
(cherry picked from commit d40504b973)
2023-08-27 09:41:12 +00:00
mergify[bot]
9b2a84f259 chore: update fr translation for Naming Series (#36785)
* chore: update fr translation for Naming Series (#36785)

* chore: update fr translation for Naming Series

* chore: update fr translation

* chore: update fr translation

* chore: update fr translation

(cherry picked from commit e462edc628)

# Conflicts:
#	erpnext/translations/fr.csv

* chore: resolve conflicts

---------

Co-authored-by: HENRY Florian <florian.henry@open-concept.pro>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-08-26 19:29:57 +05:30
mergify[bot]
c07548a612 fix: missing company flag for regional fn (#36791)
fix: missing company flag for regional fn (#36791)

* fix: missing company flag for regional fn

(cherry picked from commit 9bc5952dd5)

Co-authored-by: Dany Robert <danyrt@wahni.com>
2023-08-26 19:29:15 +05:30
Gourav Saini
0f98cc85e9 fix: Allow to make return against sales invoice which has closed sales order
fix: Allow to make return against sales invoice which has closed sales order
2023-08-26 18:04:01 +05:30
Deepesh Garg
aad91c02f3 Merge pull request #36815 from GursheenK/v_14_tax_withholding_jvs_with_no_partytype
fix: fetch JVs in tax withholding report with no party type
2023-08-25 19:41:31 +05:30
ruthra kumar
587283e787 Merge pull request #36822 from frappe/mergify/bp/version-14-hotfix/pr-36821
test: use mixin and increase coverage in receivable report (backport #36821)
2023-08-25 18:46:13 +05:30
ruthra kumar
78b0a52d41 test: increase coverage in ar/ap report
(cherry picked from commit ce81ffd844)
2023-08-25 12:42:23 +00:00
ruthra kumar
c4d338a59b refactor(test): make use of mixin in ar/ap report tests
(cherry picked from commit bb7bed4c1a)
2023-08-25 12:42:22 +00:00
Gursheen Anand
bc6bd81f87 fix: fetch JVs in tax withholding report without party values 2023-08-25 15:05:26 +05:30
ViralKansodiya
fd4159423d fix: error listindexoutofrange when save a production plan (#36807)
fix: error listindexoutof range when save a production plan
2023-08-25 13:43:53 +05:30
Deepesh Garg
95a6e1d855 Merge pull request #36809 from frappe/mergify/bp/version-14-hotfix/pr-36799
fix: Tax withholding reversal on Debit Notes (#36799)
2023-08-25 09:41:07 +05:30
Deepesh Garg
e8dc63c89c fix: Tax withholding reversal on Debit Notes
(cherry picked from commit 6d9cebfee9)
2023-08-24 14:02:32 +00:00
mergify[bot]
6edfcf4de8 fix: SCR return status (backport #36793) (#36796)
fix: SCR return status (#36793)

(cherry picked from commit 723563c167)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-08-24 10:42:59 +05:30
mergify[bot]
4fa07777e9 feat(MR): Project and Cost Center in Connections (backport #36794) (#36795)
feat(MR): Project and Cost Center in Connections (#36794)

(cherry picked from commit 54ffe41b54)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-08-24 10:15:05 +05:30
ruthra kumar
b1ebd81f77 Merge pull request #36778 from frappe/mergify/bp/version-14-hotfix/pr-36650
perf: improve responsiveness of payment reconciliation tool (backport #36650)
2023-08-23 15:08:47 +05:30
ruthra kumar
20c45c7975 chore: linter fix 2023-08-23 14:15:35 +05:30
ruthra kumar
4a4cba0715 chore: resolve conflict 2023-08-23 13:22:10 +05:30
ruthra kumar
ab9da5281e refactor: filter for journal entries
(cherry picked from commit d01f0f2e96)
2023-08-23 03:43:29 +00:00
ruthra kumar
c5080abd46 refactor: filter on advance payments
(cherry picked from commit 86bac2cf52)

# Conflicts:
#	erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
#	erpnext/controllers/accounts_controller.py
2023-08-23 03:43:29 +00:00
ruthra kumar
a8c53eeb93 refactor: filter on cr/dr notes
(cherry picked from commit 52f609e67a)
2023-08-23 03:43:28 +00:00
ruthra kumar
d727a13562 refactor: trigger on value change
(cherry picked from commit e48f8139eb)
2023-08-23 03:43:28 +00:00
ruthra kumar
4556c36736 refactor: limit output to 50 in reconciliation tool
(cherry picked from commit 7a381affce)

# Conflicts:
#	erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
2023-08-23 03:43:28 +00:00
Deepesh Garg
f16386bd07 Merge pull request #36776 from frappe/mergify/bp/version-14-hotfix/pr-36758
fix: Accounts Payable Currency bug (#36758)
2023-08-22 22:37:39 +05:30
RitvikSardana
94612b90ef fix: Accounts Payable Currency bug
(cherry picked from commit 9349bc77c5)
2023-08-22 17:00:23 +00:00
rohitwaghchaure
6a9935c00e fix: Procurement Tracker report not showing material request items (#36768) 2023-08-22 19:25:50 +05:30
rohitwaghchaure
873ee384a1 fix: not able to make stock entry (#36759) 2023-08-22 16:57:08 +05:30
Deepesh Garg
13b1263723 Merge pull request #36738 from frappe/mergify/bp/version-14-hotfix/pr-36737
fix: add missing items label back (#36737)
2023-08-22 07:58:41 +05:30
ruthra kumar
6761874e28 Merge pull request #36749 from frappe/mergify/bp/version-14-hotfix/pr-36748
chore: clean up stale code in reconciliation tool (backport #36748)
2023-08-22 06:47:41 +05:30
ruthra kumar
19eb6af65f chore: clean up stale code in reconciliation tool
(cherry picked from commit e93b927051)
2023-08-22 00:50:48 +00:00
ruthra kumar
0dc0e287a3 Merge pull request #36741 from frappe/mergify/bp/version-14-hotfix/pr-36727
fix: broken advance field in Accounts Receivable summary rpt (backport #36727)
2023-08-22 06:05:54 +05:30
Anand Baburajan
a0575ed2b0 fix: incorrect schedule in asset value adjustment (#36725)
* fix: incorrect schedule in asset value adjustment

* chore: remove unnecessary commented code

* test: check schedules in test

* test: improving the test

* chore: better function name

* chore: use None instead of 0 for default value after depr

* chore: typo
2023-08-22 01:40:42 +05:30
ruthra kumar
cb9aad3e87 fix: incorrect gl balance on multi company setup 2023-08-21 21:35:26 +05:30
ruthra kumar
37cee42561 test: add test for receivable summary report
(cherry picked from commit af52f21ece)
2023-08-21 15:24:11 +00:00
ruthra kumar
928e475824 refactor: use payment ledger to fetch advance amount
(cherry picked from commit 0dc5e5c430)
2023-08-21 15:24:11 +00:00
ruthra kumar
296a4d7a12 fix: broken advance field in Accounts Receivable summary rpt
(cherry picked from commit 896b123fb1)
2023-08-21 15:24:10 +00:00
ruthra kumar
fe78076cde Merge pull request #36740 from frappe/mergify/bp/version-14-hotfix/pr-36728
fix: include gain/loss journal in AR/AP reports (backport #36728)
2023-08-21 20:20:35 +05:30
Deepesh Garg
d082e68e83 Merge pull request #36707 from frappe/mergify/bp/version-14-hotfix/pr-36149
fix: make offsetting entry for acc dimensions in general ledger (#36149)
2023-08-21 18:02:56 +05:30
ruthra kumar
4606079568 fix: include gain/loss journal in AR/AP reports
(cherry picked from commit e3104f1898)
2023-08-21 11:53:41 +00:00
Ankush Menat
3634e80341 fix: add missing items labels back (#36737)
[skip ci]

(cherry picked from commit 86cac1e1d2)
2023-08-21 10:37:24 +00:00
mergify[bot]
e1bd9a7e8d fix: don't throw if item does not have default BOM (backport #36709) (#36734)
* fix: don't throw if item does not have default BOM

(cherry picked from commit 268c19e745)

* fix: throw if `BOM No` is not set

(cherry picked from commit 2e22b019a0)

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-08-21 15:21:11 +05:30
Deepesh Garg
72d9dc6c85 chore: resolve more conflicts 2023-08-21 10:04:20 +05:30
Deepesh Garg
9ec11d9502 Merge pull request #36726 from frappe/mergify/bp/version-14-hotfix/pr-36696
fix: mode of payment fetched from pos profile company in POS (#36696)
2023-08-20 16:05:05 +05:30
Deepesh Garg
2f92981afe chore: resolve conflicts 2023-08-20 15:47:17 +05:30
Ritvik Sardana
c74a414313 fix: mode of payment fetched from pos profile company in POS
(cherry picked from commit 1bdd43d0f6)
2023-08-20 10:07:56 +00:00
ruthra kumar
e8f1c82089 Merge pull request #36672 from frappe/mergify/bp/version-14-hotfix/pr-36469
feat: utility to repost accounting ledgers without cancellation (backport #36469)
2023-08-19 19:54:55 +05:30
ruthra kumar
6366f5aadb Merge pull request #36679 from frappe/mergify/bp/version-14-hotfix/pr-36649
perf: pull latest details only for referenced vouchers (backport #36649)
2023-08-19 19:29:00 +05:30
Deepesh Garg
933d4bfceb Merge pull request #36711 from frappe/mergify/bp/version-14-hotfix/pr-36710
fix: broken consolidated report due to finance book filter (#36710)
2023-08-19 10:25:11 +05:30
rohitwaghchaure
620b21fec5 fix: timeout error coming during reposting (#36715) 2023-08-18 18:33:04 +05:30
ruthra kumar
5bd2a0923f fix: broken consolidated report due to finance book filter
(cherry picked from commit 96847db0ec)
2023-08-18 09:25:18 +00:00
Deepesh Garg
e62ffa990d fix: Add company filters for account
(cherry picked from commit ecca9cb023)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
2023-08-18 07:08:21 +00:00
Gursheen Anand
01ae513f70 fix: dimension name in remark
(cherry picked from commit 4f9242d699)
2023-08-18 07:08:21 +00:00
Gursheen Anand
37ef6e959b fix: reset dimension defaults when company changedin test
(cherry picked from commit 3f5afb9cac)
2023-08-18 07:08:21 +00:00
Gursheen Anand
2a467a9fbf fix: clear dimension defaults after test
(cherry picked from commit 23e56d3ec1)
2023-08-18 07:08:20 +00:00
Gursheen Anand
7ac35b496a fix: fetch acc dimensions correctly when fieldname is different from name
(cherry picked from commit e19a6f5dcb)
2023-08-18 07:08:20 +00:00
Gursheen Anand
cdb66bf198 fix: duplicate acc dimension in test
(cherry picked from commit b3f6d991b5)
2023-08-18 07:08:20 +00:00
Gursheen Anand
8530a28c62 test: PI offsetting entry for accounting dimension
(cherry picked from commit 77deac4fb9)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
2023-08-18 07:08:20 +00:00
Gursheen Anand
2c8c3a022c fix: divide offsetting amount only when account exists
(cherry picked from commit 3a3ffa2307)
2023-08-18 07:08:19 +00:00
Gursheen Anand
1269f2d301 fix: divide offsetting amount for multiple dimensions
(cherry picked from commit 1e1e4b93c1)
2023-08-18 07:08:19 +00:00
Gursheen Anand
11ba553dbd fix: dict value for dimension for gl entries defined without the dimension
(cherry picked from commit ed3bef1840)
2023-08-18 07:08:19 +00:00
Gursheen Anand
248d4082c0 test: TB report balanced whenfiltered using acc dimension
(cherry picked from commit 4004427892)
2023-08-18 07:08:19 +00:00
Gursheen Anand
f578c3219d fix: make offsetting entry for all doctypes
(cherry picked from commit 22ba12172f)
2023-08-18 07:08:18 +00:00
Gursheen Anand
c1f1a21714 fix: fetch accounting dimension details specific to company
(cherry picked from commit 4e09de4db2)
2023-08-18 07:08:18 +00:00
Gursheen Anand
3198f2669d fix: make offsetting entry for acc dimensions
(cherry picked from commit d3759b3971)
2023-08-18 07:08:18 +00:00
Deepesh Garg
a623469778 Merge pull request #36645 from frappe/mergify/bp/version-14-hotfix/pr-36495
fix: Document Name link validation in Bank Reconciliation Tool (#36495)
2023-08-18 09:27:10 +05:30
Deepesh Garg
ee5a1d91ff chore: resolve conflicts 2023-08-17 20:45:53 +05:30
mergify[bot]
0a4947a8f6 fix(ux): change batch selection message to alert (backport #36500) (#36697)
fix(ux): change batch selection message to alert (#36500)

* fix(ux): change batch selection message to alert

* chore: linters

(cherry picked from commit 641fe7738c)

Co-authored-by: Dany Robert <danyrt@wahni.com>
2023-08-17 18:03:58 +05:30
mergify[bot]
9668615f7e feat: Tick on checkbox to include draft timesheets (backport #36577) (#36640)
feat: Tick on checkbox to include draft timesheets (#36577)

feat: Tick on Check box to include Draft Timesheets
(cherry picked from commit 75652799cd)

Co-authored-by: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com>
2023-08-17 17:13:51 +05:30
Deepesh Garg
506ac10119 Merge pull request #36678 from frappe/mergify/bp/version-14-hotfix/pr-36677
fix(UX): Ignore prepared report (#36677)
2023-08-17 16:08:36 +05:30
mergify[bot]
36147ec02c fix(RFQ): make "update password" and "submit quotation" buttons the same size (backport #36667) (#36686)
fix(RFQ): make "update password" and "submit quotation" buttons the same size (#36667)

fix(RFQ): button styling

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-08-17 16:00:17 +05:30
Shariq Ansari
3dd642441b Merge pull request #36689 from frappe/mergify/bp/version-14-hotfix/pr-36685
fix: check tax and charges if it is passed (backport #36685)
2023-08-17 13:10:42 +05:30
Shariq Ansari
f186015f58 chore: linter fix
(cherry picked from commit 21c1141fdb)
2023-08-17 07:40:10 +00:00
Shariq Ansari
1f76c6972b fix: check tax and charges if it is passed
(cherry picked from commit 7ec6909159)
2023-08-17 07:40:10 +00:00
mergify[bot]
c308bd5309 feat(RFQ): make email message fully configurable (backport #36353) (#36531)
* feat(RFQ): make email message fully configurable (#36353)

feat: make RFQ message fully configurable
(cherry picked from commit 21080afd92)

* fix(RFQ): hide description in print and report

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-08-17 12:11:37 +05:30
ruthra kumar
3f33d4cf76 chore: resolve conflicts 2023-08-17 11:07:59 +05:30
ruthra kumar
47345e81a1 perf: pull latest details only for referenced vouchers
(cherry picked from commit deb0d71294)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.py
2023-08-17 03:31:54 +00:00
Deepesh Garg
3e23e1fb91 fix(UX): Ignore prepared report
(cherry picked from commit 124c0dbd88)
2023-08-16 16:03:01 +00:00
ruthra kumar
b6134749c3 chore: resolve conflict 2023-08-16 16:44:07 +05:30
ruthra kumar
f8d6fe6be0 feat: utility to repost accounting ledgers without cancellation (#36469)
* feat: introduce doctypes for repost

* refactor: basic filters and validations

* chore: basic validations

* chore: added barebones function to generate ledger entries

* chore: repost on submit

* chore: repost in background

* chore: include payment entry and journal entry

* chore: ignore repost doc on cancel

* chore: preview method

* chore: rudimentary form of preview

* refactor: preview template

* refactor: basic background colors to differentiate old and new

* chore: remove commented code

* test: basic functionality

* chore: fix conflict

* chore: prevent repost on invoices with deferred accounting

* refactor(test): rename and test basic validations and methods

* refactor(test): test all validations

* fix(test): use proper name account name

* refactor(test): fix failing test case

* refactor(test): clear old entries

* refactor(test): simpler logic to clear old records

* refactor(test): make use of deletion flag

* refactor(test): split into multiple test cases

(cherry picked from commit e64b004eca)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry/journal_entry.js
2023-08-16 11:04:45 +00:00
Deepesh Garg
67c8350f70 Merge branch 'version-14' into version-14-hotfix 2023-08-16 11:08:08 +05:30
ruthra kumar
cba1c63beb Merge pull request #36651 from frappe/mergify/bp/version-14-hotfix/pr-36642
refactor: toggle for negative item rates in Selling Settings (backport #36642)
2023-08-16 10:30:24 +05:30
ruthra kumar
2f0e7fcb4a Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-36642 2023-08-16 09:58:31 +05:30
ruthra kumar
44c1b91ca7 Merge pull request #36633 from frappe/mergify/bp/version-14-hotfix/pr-35644
refactor: booking exchange gain/loss amount through journal (backport #35644)
2023-08-16 09:41:25 +05:30
mergify[bot]
1deebe8757 fix: Tax withholding post LDC limit consumed (#36611)
* fix: Tax withholding post LDC limit consumed (#36611)

* fix: Tax withholding post LDC limit consumed

* fix: LDC condition check

(cherry picked from commit 985ff9781b)

# Conflicts:
#	erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py

* chore: resolve conflicts

* chore: linting issues

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-08-16 08:09:38 +05:30
mergify[bot]
99777d3fa4 fix: re-add permission that was unintentionally removed (#36663)
fix: re-add permission that was unintentionally removed

Remove `Reversal OF ITC` and re-add permissions. Both of them
unintended changes

(cherry picked from commit 45662fa646)

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2023-08-16 08:09:04 +05:30
mergify[bot]
33d5250cec chore: add validation for depreciation expense account in asset category (backport #36659) (#36661)
chore: add validation for depreciation expense account in asset category (#36659)

(cherry picked from commit e0c79d3b53)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-08-15 18:27:01 +05:30
ruthra kumar
e55c160438 chore: resolve conflicts 2023-08-15 08:56:37 +05:30
ruthra kumar
3a82eb4ccf refactor: toggle for negative rates in Selling Settings
(cherry picked from commit a0fc68538f)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
#	erpnext/patches.txt
#	erpnext/selling/doctype/selling_settings/selling_settings.json
2023-08-15 02:43:54 +00:00
Devin Slauenwhite
ca34b63470 feat: Reallow customizing company abbreviation on setup. (#36646)
Co-authored-by: Bernd Oliver Sünderhauf <pancho@mailbox.org>
2023-08-14 21:44:03 +05:30
Kevin Shenk
83cbc1bef6 fix: Document Name link validation in Bank Reconciliation Tool (#36495)
fix: format_row broke Document Name link validation

#35540 broke Voucher Matching, leading to an invalid link exception on submission. This is because the format_row() function overwrites the row data instead of just providing a formatter on the DataTable column, and therefore passes through the formatted (linked) column data instead of the Document Name only.

This patch moves the appropriate frappe.form.formatters.Link function to a dedicated format hook on the DataTable columns definition, both fixing the error and retaining the functionality of #35540.

(cherry picked from commit 7ab55b1bb2)

# Conflicts:
#	erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
2023-08-14 13:34:31 +00:00
mergify[bot]
716d5c0b98 fix: standard formula to calculate the "difference" (#36612)
fix: standard formula to calculate the "difference" (#36612)

(cherry picked from commit 843e77e72d)

Co-authored-by: HarryPaulo <paulo_fabris@hotmail.com>
2023-08-14 18:55:16 +05:30
Gursheen Kaur Anand
90b390c2c5 feat: add voucher totals in tds payable report (#36568)
* feat: voucher totals in tds payable monthly

* fix: naming series column in tds payable report

* fix: tds computation summary columns
2023-08-14 18:15:21 +05:30
ViralKansodiya
b131f70ed6 fix: Button Alignment center in hero slider (#36607)
fix: speling in CSS (Button alignment center is not working on hero slider)#36561
2023-08-14 16:20:58 +05:30
ruthra kumar
18cf93d1c8 refactor(test): import missing functions 2023-08-14 11:49:46 +05:30
ruthra kumar
2e6bfa36de fix(test): replace hardcoded reference to adv with dynamic one 2023-08-14 11:30:35 +05:30
ruthra kumar
b3f4c14a26 chore: resolve conflict in payment_reconciliation.py
backport will merge the better remarks PR
https://github.com/frappe/erpnext/pull/36573 wil exchange gain/loss
booking refactor
2023-08-14 10:56:48 +05:30
ruthra kumar
946aadb0c0 chore: resolve conflict in test_sales_invoice.py 2023-08-14 10:52:49 +05:30
ruthra kumar
f92453ae45 chore: resolve merge conflict in accounts/utils.py and its tests 2023-08-14 10:50:31 +05:30
ruthra kumar
7469018d3e chore: resolve merge conflict 2023-08-14 10:37:56 +05:30
ruthra kumar
61afffc908 chore: cancel gain/loss je while posting reverse gl
(cherry picked from commit 46ea814400)
2023-08-14 04:51:46 +00:00
ruthra kumar
ed0881dacb chore: don't make gain/loss journal for base currency transactions
(cherry picked from commit 567c0ce1e8)
2023-08-14 04:51:46 +00:00
ruthra kumar
efb293398a chore(test): use existing company for unit test
(cherry picked from commit 804afaa647)
2023-08-14 04:51:46 +00:00
ruthra kumar
8d32a1f4b3 chore: rename some internal variables
(cherry picked from commit d9d6856153)
2023-08-14 04:51:46 +00:00
ruthra kumar
4c527d6bba chore: add msgprint for exc JE
(cherry picked from commit acc7322874)
2023-08-14 04:51:45 +00:00
ruthra kumar
2a61d854d3 chore: use frappetestcase
(cherry picked from commit 47bbb37291)
2023-08-14 04:51:45 +00:00
ruthra kumar
052abcb075 refactor(test): assert ledger outstanding
(cherry picked from commit 025091161e)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2023-08-14 04:51:45 +00:00
ruthra kumar
3542df70f6 fix(test): test case breakage in Github Actions
(cherry picked from commit bfa54d5335)
2023-08-14 04:51:45 +00:00
ruthra kumar
c5c440b7bc test: assert ledger after cr note cancellation
(cherry picked from commit ae424fdfed)
2023-08-14 04:51:45 +00:00
ruthra kumar
349601b4b9 fix: cr/dr note should be posted for exc gain/loss
(cherry picked from commit 95543225cf)
2023-08-14 04:51:44 +00:00
ruthra kumar
09e9b16b93 test: cr notes against invoice
(cherry picked from commit e3d2a2c5bd)
2023-08-14 04:51:44 +00:00
ruthra kumar
39c439dc4b fix: incorrect gain/loss on allocation change on reconciliation tool
(cherry picked from commit 506a5775f9)
2023-08-14 04:51:44 +00:00
ruthra kumar
72a507f888 refactor: create gain/loss on Cr/Dr notes with different exc rates
(cherry picked from commit ba1f065765)
2023-08-14 04:51:44 +00:00
ruthra kumar
22dbe52586 refactor: convert class method to standalone function
(cherry picked from commit 1ea1bfebc4)
2023-08-14 04:51:44 +00:00
ruthra kumar
1999132c28 refactor: split make_exchage_gain_loss_journal into smaller function
(cherry picked from commit c0b3b069b5)
2023-08-14 04:51:43 +00:00
ruthra kumar
57af6d9c21 refactor: cr/dr note will be on single exchange rate
(cherry picked from commit c87332d5da)

# Conflicts:
#	erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
2023-08-14 04:51:43 +00:00
ruthra kumar
a9faa92796 chore: type info
(cherry picked from commit 6628632fbb)
2023-08-14 04:51:43 +00:00
ruthra kumar
d9219dc7d2 chore(test): fix broken test case
(cherry picked from commit 37895a361c)
2023-08-14 04:51:43 +00:00
ruthra kumar
395f87a0f8 chore(test): fix broken unit test
(cherry picked from commit 70dd9d0671)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2023-08-14 04:51:43 +00:00
ruthra kumar
4094fbc3e5 test: journals against sales invoice
(cherry picked from commit f3363e813a)
2023-08-14 04:51:42 +00:00
ruthra kumar
513721c338 refactor: handle diff amount in various names
(cherry picked from commit f4a65cccc4)
2023-08-14 04:51:42 +00:00
ruthra kumar
077d98e0fa refactor: unit tests for journals
(cherry picked from commit 5695d6a5a6)
2023-08-14 04:51:42 +00:00
ruthra kumar
8be312b73e refactor: dr/cr logic for journals as payments
(cherry picked from commit 0567243772)
2023-08-14 04:51:42 +00:00
ruthra kumar
197e5881aa refactor: assert payment ledger outstanding in both currencies
(cherry picked from commit 73cc1ba654)
2023-08-14 04:51:41 +00:00
ruthra kumar
7c3fc7eb3b refactor: cancel gain/loss JE on Journal as payment cancellation
(cherry picked from commit 6e18bb6456)
2023-08-14 04:51:41 +00:00
ruthra kumar
01953bc0e3 refactor: linkage between journal as payment and gain/loss journal
(cherry picked from commit f119a1e115)

# Conflicts:
#	erpnext/accounts/doctype/gl_entry/gl_entry.py
2023-08-14 04:51:41 +00:00
ruthra kumar
075a7dfe2e chore: code cleanup
(cherry picked from commit cd42b26839)
2023-08-14 04:51:41 +00:00
ruthra kumar
86aead3d45 refactor: remove call for setting deductions in payment entry
(cherry picked from commit 1bcb728c85)

# Conflicts:
#	erpnext/accounts/utils.py
2023-08-14 04:51:40 +00:00
ruthra kumar
e20b213737 refactor(test): difference amount no updated for exchange gain/loss
(cherry picked from commit 72bc5b3a11)

# Conflicts:
#	erpnext/accounts/test/test_utils.py
2023-08-14 04:51:40 +00:00
ruthra kumar
4025442491 refactor(test): exc gain/loss journal for advance in purchase invoice
(cherry picked from commit 5b06bd1af4)
2023-08-14 04:51:40 +00:00
ruthra kumar
f6bb6b78db refactor: only post on base currency for exchange gain/loss
(cherry picked from commit 78bc712756)
2023-08-14 04:51:40 +00:00
ruthra kumar
00a26ea6e6 refactor(test): payment will have same exch rate - no gain/loss
while making payment entry using reference to sales/purchase invoice,
it herits the parent docs exchange rate. so, there will be no exchange
gain/loss

(cherry picked from commit ee2d1fa36e)
2023-08-14 04:51:39 +00:00
ruthra kumar
e2c35f8c85 refactor(test): assert Exc journal when reconciling Journa to invoic
(cherry picked from commit 389cadf157)
2023-08-14 04:51:39 +00:00
ruthra kumar
72e88d22ed chore: remove debugging statements and fixing failing unit tests
(cherry picked from commit ee3ce82ea8)
2023-08-14 04:51:39 +00:00
ruthra kumar
220bf24555 refactor: exc booking logic for Journal Entry
(cherry picked from commit 7b516f8463)
2023-08-14 04:51:39 +00:00
ruthra kumar
1f76dde025 refactor(test): exc gain/loss booked through journal
(cherry picked from commit 00a2e42a47)
2023-08-14 04:51:38 +00:00
ruthra kumar
59b7e96255 refactor: assert exchange gain/loss amount in reference table
(cherry picked from commit 4ff53e1062)
2023-08-14 04:51:38 +00:00
ruthra kumar
d030f4a100 refactor: remove unused variable, pe should pull in parent exc rate
1. 'reference_doc' variable is never set. Hence, removing.
2. set_exchange_rate() relies on ref_doc, which was never
set due to point [1]. Replacing it with 'doc'.
3. Sales/Purchase Invoice has 'conversion_rate' field for tracking
exchange rate. Added a get statement for them as well.

(cherry picked from commit 92ae9c2201)
2023-08-14 04:51:38 +00:00
ruthra kumar
6c6acef78e refactor: helper method
(cherry picked from commit c1184585ed)
2023-08-14 04:51:38 +00:00
ruthra kumar
a9da619903 chore: fix logic for purchase invoice and some typos
(cherry picked from commit 34b5e849a2)
2023-08-14 04:51:38 +00:00
ruthra kumar
72005bdb39 refactor: add new reference type in journal entry account
(cherry picked from commit 13febcac81)
2023-08-14 04:51:38 +00:00
ruthra kumar
287af687cf chore: patch to update property setter for Journal Entry Accounts
(cherry picked from commit 0587338435)
2023-08-14 04:51:37 +00:00
ruthra kumar
db46987d4b refactor: replace with new method in purchase invoice
(cherry picked from commit 7e94a1c51b)
2023-08-14 04:51:37 +00:00
ruthra kumar
44110860b4 test: different scenarios for exchange booking
(cherry picked from commit 5e1cd1f227)
2023-08-14 04:51:37 +00:00
ruthra kumar
c06a6bfc09 refactor: book exchange gain/loss through journal
(cherry picked from commit 81cd7873d3)
2023-08-14 04:51:37 +00:00
mergify[bot]
ac0fff7e94 fix: AR/AP report based on payment terms (#36574)
fix: AR/AP report based on payment terms (#36574)

* fix: AR/AP report based on payment terms

* fix: AR/AP report based on payment terms

(cherry picked from commit fbb5058531)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-08-14 09:46:41 +05:30
mergify[bot]
3044f46c52 fix: wrong currency on financial-statement based reports (#36524)
fix: wrong currency on financial-statement based reports (#36524)

* add missing field options on financial_statement Total field

* format financial statement code

(cherry picked from commit ce25f9e8c9)

Co-authored-by: Naufal Afif <naufalafif58@gmail.com>
2023-08-13 17:11:47 +05:30
Deepesh Garg
0a832bc979 Merge pull request #36609 from frappe/mergify/bp/version-14-hotfix/pr-36564
fix: Make default sales update frequency as monthly instead of each transaction (#36564)
2023-08-13 13:20:27 +05:30
Deepesh Garg
e5bc888798 Merge pull request #36614 from deepeshgarg007/loan_repayment_cancel_v14
fix: Allow backdated repayment cancels for term loans
2023-08-13 11:57:49 +05:30
ruthra kumar
9172985a9b Merge pull request #36621 from frappe/mergify/bp/version-14-hotfix/pr-36309
fix: allocation validation blocks partial payment for SO and PO (backport #36309)
2023-08-13 11:39:27 +05:30
Deepesh Garg
a3032910a7 chore: resolve conflicts 2023-08-13 11:37:40 +05:30
ruthra kumar
ce08f038d2 fix: validation blocks partial payment for SO and PO
(cherry picked from commit cb2bfabb6f)
2023-08-13 10:58:55 +05:30
ruthra kumar
df632d7ecb Merge pull request #36620 from frappe/mergify/bp/version-14-hotfix/pr-36590
fix: disallow mulitple SO with same Purchase Order No if not enabled in Settings (backport #36590)
2023-08-13 08:48:27 +05:30
ruthra kumar
1693905703 Merge pull request #36619 from frappe/mergify/bp/version-14-hotfix/pr-36593
chore: update permissions for Process Payment Reconciliation (backport #36593)
2023-08-13 08:43:00 +05:30
ruthra kumar
21d3fb0625 chore: resolve merge conflict 2023-08-13 08:24:01 +05:30
ruthra kumar
c83f10f638 refactor(test): don't set po_no by default
(cherry picked from commit 64614cd915)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
2023-08-13 02:40:42 +00:00
ruthra kumar
b901cfdbe2 fix: disallow mulitple SO with same PO No
(cherry picked from commit dbd3fdbb41)
2023-08-13 02:40:42 +00:00
ruthra kumar
cbcdf30840 chore: update permissions for Process Payment Reconciliation
(cherry picked from commit cd28d15292)
2023-08-13 02:40:38 +00:00
mergify[bot]
8b13185c25 fix: fetch Stock UOM from Item if not set (backport #36606) (#36617)
fix: fetch `Stock UOM` from Item if not set (#36606)

(cherry picked from commit 539cfd08f0)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-08-12 23:25:05 +05:30
Deepesh Garg
1377cf4cf1 fix: Allow backdated repayment cancels for term loans 2023-08-12 20:01:04 +05:30
Deepesh Garg
4ca1f3b9cf fix: Make default sales update frequency as monthly instead of each transaction
(cherry picked from commit 32863b4922)

# Conflicts:
#	erpnext/selling/doctype/selling_settings/selling_settings.json
2023-08-11 17:10:07 +00:00
Deepesh Garg
347c67a0f1 Merge pull request #36608 from frappe/mergify/bp/version-14-hotfix/pr-36582
fix: Group Account total not showing in Financial Statements (#36582)
2023-08-11 22:33:35 +05:30
Deepesh Garg
2912648151 fix: Group Account total not showing in Financial Statements
(cherry picked from commit baf5cddd1b)
2023-08-11 16:27:53 +00:00
mergify[bot]
63c061e5e8 fix: wrap none type rate under flt (backport #36602) (#36604)
fix: wrap none type rate under flt (#36602)

(cherry picked from commit 627986efa1)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-08-11 12:55:25 +00:00
mergify[bot]
96663d7e28 chore: set default filter dates if missing (backport #36597) (#36598)
chore: set default filter dates if missing (#36597)

(cherry picked from commit 98e82e0d99)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-08-11 09:19:14 +00:00
rohitwaghchaure
ee04c6d5c5 fix: allow negative stock condition for batch item (#36586)
* fix: allow negative stock condition for batch item

* fix: test case
2023-08-11 13:57:27 +05:30
ruthra kumar
5488785fd8 Merge pull request #36579 from frappe/mergify/bp/version-14-hotfix/pr-36573
refactor: 'is system generated' field and better remarks in Journal Entry (backport #36573)
2023-08-11 09:43:53 +05:30
ruthra kumar
25ac0ce1cc Merge pull request #36580 from frappe/mergify/bp/version-14-hotfix/pr-36578
fix: unhide `uom` and `stock_uom` fields in print view (backport #36578)
2023-08-11 09:16:07 +05:30
ruthra kumar
727a379581 chore: remove unwanted 'Reversal of ITC' and merge conflicts 2023-08-11 09:12:11 +05:30
Anand Baburajan
29181274c8 feat: daily asset depreciation method (#36587)
* feat: daily asset depreciation method

* chore: hide depr schedule if no schedules
2023-08-10 22:47:16 +05:30
rohitwaghchaure
2800ad39d2 fix: incorrect available qty for backdated stock reco with batch (#36581) 2023-08-10 17:22:14 +05:30
ruthra kumar
b49309c160 fix: unhide uom and stock_uom fields in print view
(cherry picked from commit 11cd163db7)
2023-08-10 10:18:36 +00:00
ruthra kumar
a04471024d refactor: enable 'no-copy'
(cherry picked from commit 4ed4b0240d)
2023-08-10 10:09:26 +00:00
ruthra kumar
9b0d30c56b refactor: set flag display condition
(cherry picked from commit de17eaef38)
2023-08-10 10:09:25 +00:00
ruthra kumar
00c7dbceaa refactor: add is_system_generated field to Journal Entry
(cherry picked from commit 3997aa77d4)

# Conflicts:
#	erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
2023-08-10 10:09:25 +00:00
ruthra kumar
5443592d84 fix: better remarks on Cr note created by Reconciliation
(cherry picked from commit 47cb349362)

# Conflicts:
#	erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
2023-08-10 10:09:25 +00:00
rohitwaghchaure
a864e07d4f fix: precision issue while submitting the stock entry (#36575)
fix: precision issue while submmiting the stock entry
2023-08-10 13:32:31 +05:30
mergify[bot]
19cfcea78e fix: don't show disabled items in Item Shortage Report (backport #36550) (#36571)
fix: don't show disabled items in `Item Shortage Report` (#36550)

(cherry picked from commit 4a7fc1506f)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-08-10 11:29:20 +05:30
Ankush Menat
8083c0b59e fix: move company rename to long queue
(cherry picked from commit 5169006085)
2023-08-10 10:03:22 +05:30
Anand Baburajan
8abc0adb18 chore: add warning for lending separation (#36569) 2023-08-09 15:07:40 +00:00
mergify[bot]
fe41be953d perf(invoice): Faster return amount query (backport #36556) (#36557)
perf(invoice): Faster return amount query (#36556)

perf: Faster return amount query
(cherry picked from commit b0c79a0467)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-08-09 14:07:48 +05:30
s-aga-r
c2b5417cb8 Merge pull request #36552 from frappe/mergify/bp/version-14-hotfix/pr-36551
fix(RFQ): link to supplier portal (backport #36551)
2023-08-09 13:55:28 +05:30
barredterra
d273948d7a test(RFQ): get_link
(cherry picked from commit 68ad62f7d0)
2023-08-08 11:25:39 +00:00
barredterra
eb2f68ec98 fix(RFQ): link to supplier portal
(cherry picked from commit fd91f2c2e0)
2023-08-08 11:25:39 +00:00
177 changed files with 6311 additions and 1432 deletions

View File

@@ -117,9 +117,6 @@ frappe.ui.form.on('Account', {
args: {
old: frm.doc.name,
new: data.name,
is_group: frm.doc.is_group,
root_type: frm.doc.root_type,
company: frm.doc.company
},
callback: function(r) {
if(!r.exc) {

View File

@@ -18,6 +18,10 @@ class BalanceMismatchError(frappe.ValidationError):
pass
class InvalidAccountMergeError(frappe.ValidationError):
pass
class Account(NestedSet):
nsm_parent_field = "parent_account"
@@ -444,24 +448,35 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new, is_group, root_type, company):
def merge_account(old, new):
# Validate properties before merging
if not frappe.db.exists("Account", new):
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
if not new_account:
throw(_("Account {0} does not exist").format(new))
val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"]))
if val != [cint(is_group), root_type, company]:
if (
cint(new_account.is_group),
new_account.root_type,
new_account.company,
cstr(new_account.account_currency),
) != (
cint(old_account.is_group),
old_account.root_type,
old_account.company,
cstr(old_account.account_currency),
):
throw(
_(
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""
)
msg=_(
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency"""
),
title=("Invalid Accounts"),
exc=InvalidAccountMergeError,
)
if is_group and frappe.db.get_value("Account", new, "parent_account") == old:
frappe.db.set_value(
"Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account")
)
if old_account.is_group and new_account.parent_account == old:
new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account"))
frappe.rename_doc("Account", old, new, merge=1, force=1)

View File

@@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = {
accounts = nodes;
}
const get_balances = frappe.call({
method: 'erpnext.accounts.utils.get_account_balances',
args: {
accounts: accounts,
company: cur_tree.args.company
},
});
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
if(value) {
get_balances.then(r => {
if (!r.message || r.message.length == 0) return;
const get_balances = frappe.call({
method: 'erpnext.accounts.utils.get_account_balances',
args: {
accounts: accounts,
company: cur_tree.args.company
},
});
for (let account of r.message) {
get_balances.then(r => {
if (!r.message || r.message.length == 0) return;
const node = cur_tree.nodes && cur_tree.nodes[account.value];
if (!node || node.is_root) continue;
for (let account of r.message) {
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? "Dr": "Cr";
const format = (value, currency) => format_currency(Math.abs(value), currency);
const node = cur_tree.nodes && cur_tree.nodes[account.value];
if (!node || node.is_root) continue;
if (account.balance!==undefined) {
node.parent && node.parent.find('.balance-area').remove();
$('<span class="balance-area pull-right">'
+ (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
+ format(account.balance, account.company_currency)
+ " " + dr_or_cr
+ '</span>').insertBefore(node.$ul);
}
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? "Dr": "Cr";
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) {
node.parent && node.parent.find('.balance-area').remove();
$('<span class="balance-area pull-right">'
+ (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
+ format(account.balance, account.company_currency)
+ " " + dr_or_cr
+ '</span>').insertBefore(node.$ul);
}
}
});
}
});
},

View File

@@ -7,7 +7,11 @@ import unittest
import frappe
from frappe.test_runner import make_test_records
from erpnext.accounts.doctype.account.account import merge_account, update_account_number
from erpnext.accounts.doctype.account.account import (
InvalidAccountMergeError,
merge_account,
update_account_number,
)
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
test_dependencies = ["Company"]
@@ -47,49 +51,53 @@ class TestAccount(unittest.TestCase):
frappe.delete_doc("Account", "1211-11-4 - 6 - Debtors 1 - Test - - _TC")
def test_merge_account(self):
if not frappe.db.exists("Account", "Current Assets - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Current Assets"
acc.is_group = 1
acc.parent_account = "Application of Funds (Assets) - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Securities and Deposits - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Securities and Deposits"
acc.parent_account = "Current Assets - _TC"
acc.is_group = 1
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Earnest Money - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Earnest Money"
acc.parent_account = "Securities and Deposits - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Cash In Hand - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Cash In Hand"
acc.is_group = 1
acc.parent_account = "Current Assets - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Accumulated Depreciation - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Accumulated Depreciation"
acc.parent_account = "Fixed Assets - _TC"
acc.company = "_Test Company"
acc.account_type = "Accumulated Depreciation"
acc.insert()
create_account(
account_name="Current Assets",
is_group=1,
parent_account="Application of Funds (Assets) - _TC",
company="_Test Company",
)
create_account(
account_name="Securities and Deposits",
is_group=1,
parent_account="Current Assets - _TC",
company="_Test Company",
)
create_account(
account_name="Earnest Money",
parent_account="Securities and Deposits - _TC",
company="_Test Company",
)
create_account(
account_name="Cash In Hand",
is_group=1,
parent_account="Current Assets - _TC",
company="_Test Company",
)
create_account(
account_name="Receivable INR",
parent_account="Current Assets - _TC",
company="_Test Company",
account_currency="INR",
)
create_account(
account_name="Receivable USD",
parent_account="Current Assets - _TC",
company="_Test Company",
account_currency="USD",
)
doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
self.assertEqual(parent, "Securities and Deposits - _TC")
merge_account(
"Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
)
merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC")
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
# Parent account of the child account changes after merging
@@ -98,30 +106,28 @@ class TestAccount(unittest.TestCase):
# Old account doesn't exist after merging
self.assertFalse(frappe.db.exists("Account", "Securities and Deposits - _TC"))
doc = frappe.get_doc("Account", "Current Assets - _TC")
# Raise error as is_group property doesn't match
self.assertRaises(
frappe.ValidationError,
InvalidAccountMergeError,
merge_account,
"Current Assets - _TC",
"Accumulated Depreciation - _TC",
doc.is_group,
doc.root_type,
doc.company,
)
doc = frappe.get_doc("Account", "Capital Stock - _TC")
# Raise error as root_type property doesn't match
self.assertRaises(
frappe.ValidationError,
InvalidAccountMergeError,
merge_account,
"Capital Stock - _TC",
"Softwares - _TC",
doc.is_group,
doc.root_type,
doc.company,
)
# Raise error as currency doesn't match
self.assertRaises(
InvalidAccountMergeError,
merge_account,
"Receivable INR - _TC",
"Receivable USD - _TC",
)
def test_account_sync(self):
@@ -400,11 +406,20 @@ def create_account(**kwargs):
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
)
if account:
return account
account = frappe.get_doc("Account", account)
account.update(
dict(
is_group=kwargs.get("is_group", 0),
parent_account=kwargs.get("parent_account"),
)
)
account.save()
return account.name
else:
account = frappe.get_doc(
dict(
doctype="Account",
is_group=kwargs.get("is_group", 0),
account_name=kwargs.get("account_name"),
account_type=kwargs.get("account_type"),
parent_account=kwargs.get("parent_account"),

View File

@@ -15,6 +15,17 @@ frappe.ui.form.on('Accounting Dimension', {
};
});
frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) {
let d = locals[cdt][cdn];
return {
filters: {
company: d.company,
root_type: ["in", ["Asset", "Liability"]],
is_group: 0
}
}
});
if (!frm.is_new()) {
frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () {
frappe.set_route("List", frm.doc.document_type);

View File

@@ -39,6 +39,8 @@ class AccountingDimension(Document):
if not self.is_new():
self.validate_document_type_change()
self.validate_dimension_defaults()
def validate_document_type_change(self):
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
if doctype_before_save != self.document_type:
@@ -46,6 +48,14 @@ class AccountingDimension(Document):
message += _("Please create a new Accounting Dimension if required.")
frappe.throw(message)
def validate_dimension_defaults(self):
companies = []
for default in self.get("dimension_defaults"):
if default.company not in companies:
companies.append(default.company)
else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def after_insert(self):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)

View File

@@ -8,7 +8,10 @@
"reference_document",
"default_dimension",
"mandatory_for_bs",
"mandatory_for_pl"
"mandatory_for_pl",
"column_break_lqns",
"automatically_post_balancing_accounting_entry",
"offsetting_account"
],
"fields": [
{
@@ -50,6 +53,23 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Mandatory For Profit and Loss Account"
},
{
"default": "0",
"fieldname": "automatically_post_balancing_accounting_entry",
"fieldtype": "Check",
"label": "Automatically post balancing accounting entry"
},
{
"fieldname": "offsetting_account",
"fieldtype": "Link",
"label": "Offsetting Account",
"mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry",
"options": "Account"
},
{
"fieldname": "column_break_lqns",
"fieldtype": "Column Break"
}
],
"istable": 1,

View File

@@ -66,7 +66,9 @@
"report_settings_sb",
"banking_tab",
"enable_party_matching",
"enable_fuzzy_matching"
"enable_fuzzy_matching",
"tab_break_dpet",
"show_balance_in_coa"
],
"fields": [
{
@@ -416,6 +418,17 @@
"fieldname": "ignore_account_closing_balance",
"fieldtype": "Check",
"label": "Ignore Account Closing Balance"
},
{
"fieldname": "tab_break_dpet",
"fieldtype": "Tab Break",
"label": "Chart Of Accounts"
},
{
"default": "1",
"fieldname": "show_balance_in_coa",
"fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts"
}
],
"icon": "icon-cog",

View File

@@ -13,10 +13,11 @@ frappe.ui.form.on("Bank Transaction", {
});
},
refresh(frm) {
frm.add_custom_button(__('Unreconcile Transaction'), () => {
frm.call('remove_payment_entries')
.then( () => frm.refresh() );
});
if (!frm.is_dirty() && frm.doc.payment_entries.length > 0) {
frm.add_custom_button(__("Unreconcile Transaction"), () => {
frm.call("remove_payment_entries").then(() => frm.refresh());
});
}
},
bank_account: function (frm) {
set_bank_statement_filter(frm);

View File

@@ -3,6 +3,296 @@
import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, today
class TestExchangeRateRevaluation(unittest.TestCase):
pass
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
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
from erpnext.accounts.party import get_party_account
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.stock.doctype.item.test_item import create_item
class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_usd_receivable_account()
self.create_item()
self.create_customer()
self.clear_old_entries()
self.set_system_and_company_settings()
def tearDown(self):
frappe.db.rollback()
def set_system_and_company_settings(self):
# set number and currency precision
system_settings = frappe.get_doc("System Settings")
system_settings.float_precision = 2
system_settings.currency_precision = 2
system_settings.save()
# Using Exchange Gain/Loss account for unrealized as well.
company_doc = frappe.get_doc("Company", self.company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
company_doc.save()
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_01_revaluation_of_forex_balance(self):
"""
Test Forex account balance and Journal creation post Revaluation
"""
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = (self.company,)
err.posting_date = today()
accounts = err.get_accounts_data()
err.extend("accounts", accounts)
row = err.accounts[0]
row.new_exchange_rate = 85
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
)
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
err.set_total_gain_loss()
err = err.save().submit()
# Create JV for ERR
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
je = je.submit()
je.reload()
self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
self.assertEqual(je.total_debit, 8500.0)
self.assertEqual(je.total_credit, 8500.0)
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=["sum(debit)-sum(credit) as balance"],
)[0]
self.assertEqual(acc_balance.balance, 8500.0)
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_02_accounts_only_with_base_currency_balance(self):
"""
Test Revaluation on Forex account with balance only in base currency
"""
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
pe = get_payment_entry(si.doctype, si.name)
pe.source_exchange_rate = 85
pe.received_amount = 8500
pe.save().submit()
# Cancel the auto created gain/loss JE to simulate balance only in base currency
je = frappe.db.get_all(
"Journal Entry Account", filters={"reference_name": si.name}, pluck="parent"
)[0]
frappe.get_doc("Journal Entry", je).cancel()
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = (self.company,)
err.posting_date = today()
err.fetch_and_calculate_accounts_data()
err = err.save().submit()
# Create JV for ERR
self.assertTrue(err.check_journal_entry_condition())
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()
je.reload()
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
self.assertEqual(len(je.accounts), 2)
# Only base currency fields will be posted to
for acc in je.accounts:
self.assertEqual(acc.debit_in_account_currency, 0)
self.assertEqual(acc.credit_in_account_currency, 0)
self.assertEqual(je.total_debit, 500.0)
self.assertEqual(je.total_credit, 500.0)
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
],
)[0]
# account shouldn't have balance in base and account currency
self.assertEqual(acc_balance.balance, 0.0)
self.assertEqual(acc_balance.balance_in_account_currency, 0.0)
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_03_accounts_only_with_account_currency_balance(self):
"""
Test Revaluation on Forex account with balance only in account currency
"""
precision = frappe.db.get_single_value("System Settings", "currency_precision")
# posting on previous date to make sure that ERR picks up the Payment entry's exchange
# rate while calculating gain/loss for account currency balance
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=add_days(today(), -1),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = 95
pe.source_exchange_rate = 84.211
pe.received_amount = 8000
pe.references = []
pe.save().submit()
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
],
)[0]
# account should have balance only in account currency
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0) # in USD
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = (self.company,)
err.posting_date = today()
err.fetch_and_calculate_accounts_data()
err.set_total_gain_loss()
err = err.save().submit()
# Create JV for ERR
self.assertTrue(err.check_journal_entry_condition())
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()
je.reload()
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
self.assertEqual(len(je.accounts), 2)
# Only account currency fields will be posted to
for acc in je.accounts:
self.assertEqual(flt(acc.debit, precision), 0.0)
self.assertEqual(flt(acc.credit, precision), 0.0)
row = [x for x in je.accounts if x.account == self.debtors_usd][0]
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
row = [x for x in je.accounts if x.account != self.debtors_usd][0]
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
self.assertEqual(flt(je.total_debit, precision), 0.0)
self.assertEqual(flt(je.total_credit, precision), 0.0)
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
],
)[0]
# account shouldn't have balance in base and account currency post revaluation
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0)
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_04_get_account_details_function(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
from erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation import (
get_account_details,
)
account_details = get_account_details(
self.company, si.posting_date, self.debtors_usd, "Customer", self.customer, 0.05
)
# not checking for new exchange rate and balances as it is dependent on live exchange rates
expected_data = {
"account_currency": "USD",
"balance_in_base_currency": 8000.0,
"balance_in_account_currency": 100.0,
"current_exchange_rate": 80.0,
"zero_balance": False,
"new_balance_in_account_currency": 100.0,
}
for key, val in expected_data.items():
self.assertEqual(expected_data.get(key), account_details.get(key))

View File

@@ -58,7 +58,14 @@ class GLEntry(Document):
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
if frappe.db.get_value("Account", self.account, "account_type") not in [
if (
self.voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
):
return
if frappe.get_cached_value("Account", self.account, "account_type") not in [
"Receivable",
"Payable",
]:

View File

@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger'];
},
refresh: function(frm) {

View File

@@ -9,6 +9,7 @@
"engine": "InnoDB",
"field_order": [
"entry_type_and_date",
"is_system_generated",
"title",
"voucher_type",
"naming_series",
@@ -533,13 +534,22 @@
"label": "Process Deferred Accounting",
"options": "Process Deferred Accounting",
"read_only": 1
},
{
"default": "0",
"depends_on": "eval:doc.is_system_generated == 1;",
"fieldname": "is_system_generated",
"fieldtype": "Check",
"label": "Is System Generated",
"no_copy": 1,
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [],
"modified": "2023-03-01 14:58:59.286591",
"modified": "2023-08-10 14:32:22.366895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -18,6 +18,7 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
)
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
get_balance_on,
get_stock_accounts,
@@ -87,15 +88,16 @@ class JournalEntry(AccountsController):
self.update_invoice_discounting()
def on_cancel(self):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
unlink_ref_doc_from_payment_entries(self)
# References for this Journal are removed on the `on_cancel` event in accounts_controller
super(JournalEntry, self).on_cancel()
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Payment Ledger Entry",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
)
self.make_gl_entries(1)
self.update_advance_paid()
@@ -487,11 +489,12 @@ class JournalEntry(AccountsController):
)
if not against_entries:
frappe.throw(
_(
"Journal Entry {0} does not have account {1} or already matched against other voucher"
).format(d.reference_name, d.account)
)
if self.voucher_type != "Exchange Gain Or Loss":
frappe.throw(
_(
"Journal Entry {0} does not have account {1} or already matched against other voucher"
).format(d.reference_name, d.account)
)
else:
dr_or_cr = "debit" if d.credit > 0 else "credit"
valid = False
@@ -574,7 +577,9 @@ class JournalEntry(AccountsController):
else:
party_account = against_voucher[1]
if against_voucher[0] != cstr(d.party) or party_account != d.account:
if (
against_voucher[0] != cstr(d.party) or party_account != d.account
) and self.voucher_type != "Exchange Gain Or Loss":
frappe.throw(
_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
d.idx,
@@ -756,18 +761,23 @@ class JournalEntry(AccountsController):
)
):
# Modified to include the posting date for which to retreive the exchange rate
d.exchange_rate = get_exchange_rate(
self.posting_date,
d.account,
d.account_currency,
self.company,
d.reference_type,
d.reference_name,
d.debit,
d.credit,
d.exchange_rate,
)
ignore_exchange_rate = False
if self.get("flags") and self.flags.get("ignore_exchange_rate"):
ignore_exchange_rate = True
if not ignore_exchange_rate:
# Modified to include the posting date for which to retreive the exchange rate
d.exchange_rate = get_exchange_rate(
self.posting_date,
d.account,
d.account_currency,
self.company,
d.reference_type,
d.reference_name,
d.debit,
d.credit,
d.exchange_rate,
)
if not d.exchange_rate:
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
@@ -775,6 +785,9 @@ class JournalEntry(AccountsController):
def create_remarks(self):
r = []
if self.flags.skip_remarks_creation:
return
if self.user_remark:
r.append(_("Note: {0}").format(self.user_remark))
@@ -923,6 +936,8 @@ class JournalEntry(AccountsController):
merge_entries=merge_entries,
update_outstanding=update_outstanding,
)
if cancel:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
@frappe.whitelist()
def get_balance(self, difference_account=None):

View File

@@ -5,6 +5,7 @@
import unittest
import frappe
from frappe.tests.utils import change_settings
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -13,6 +14,7 @@ from erpnext.exceptions import InvalidAccountCurrency
class TestJournalEntry(unittest.TestCase):
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_journal_entry_with_against_jv(self):
jv_invoice = frappe.copy_doc(test_records[2])
base_jv = frappe.copy_doc(test_records[0])

View File

@@ -203,7 +203,7 @@
"fieldtype": "Select",
"label": "Reference Type",
"no_copy": 1,
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement"
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry"
},
{
"fieldname": "reference_name",
@@ -284,7 +284,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-10-26 20:03:10.906259",
"modified": "2023-06-16 14:11:13.507807",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -49,9 +49,6 @@ def start_merge(docname):
merge_account(
row.account,
ledger_merge.account,
ledger_merge.is_group,
ledger_merge.root_type,
ledger_merge.company,
)
row.db_set("merged", 1)
frappe.db.commit()

View File

@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@@ -526,15 +526,21 @@ frappe.ui.form.on('Payment Entry', {
},
source_exchange_rate: function(frm) {
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_amount) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
// target exchange rate should always be same as source if both account currencies is same
if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
} else if (company_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.base_paid_amount);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
}
frm.events.set_unallocated_amount(frm);
// set_unallocated_amount is called by below method,
// no need trigger separately
frm.events.set_total_allocated_amount(frm);
}
// Make read only if Accounts Settings doesn't allow stale rates
@@ -543,6 +549,7 @@ frappe.ui.form.on('Payment Entry', {
target_exchange_rate: function(frm) {
frm.set_paid_amount_based_on_received_amount = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.received_amount) {
frm.set_value("base_received_amount",
@@ -552,9 +559,14 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) {
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
} else if (company_currency == frm.doc.paid_from_account_currency) {
frm.set_value("paid_amount", frm.doc.base_received_amount);
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
}
frm.events.set_unallocated_amount(frm);
// set_unallocated_amount is called by below method,
// no need trigger separately
frm.events.set_total_allocated_amount(frm);
}
frm.set_paid_amount_based_on_received_amount = false;
@@ -870,12 +882,18 @@ frappe.ui.form.on('Payment Entry', {
},
set_total_allocated_amount: function(frm) {
let exchange_rate = 1;
if (frm.doc.payment_type == "Receive") {
exchange_rate = frm.doc.source_exchange_rate;
} else if (frm.doc.payment_type == "Pay") {
exchange_rate = frm.doc.target_exchange_rate;
}
var total_allocated_amount = 0.0;
var base_total_allocated_amount = 0.0;
$.each(frm.doc.references || [], function(i, row) {
if (row.allocated_amount) {
total_allocated_amount += flt(row.allocated_amount);
base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(row.exchange_rate),
base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(exchange_rate),
precision("base_paid_amount"));
}
});

View File

@@ -24,7 +24,12 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
)
from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
get_balance_on,
get_outstanding_invoices,
)
from erpnext.controllers.accounts_controller import (
AccountsController,
get_supplier_block_status,
@@ -61,7 +66,7 @@ class PaymentEntry(AccountsController):
def validate(self):
self.setup_party_account_field()
self.set_missing_values()
self.set_missing_ref_details()
self.set_missing_ref_details(force=True)
self.validate_payment_type()
self.validate_party_details()
self.set_exchange_rate()
@@ -100,7 +105,10 @@ class PaymentEntry(AccountsController):
"Payment Ledger Entry",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
)
super(PaymentEntry, self).on_cancel()
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
@@ -179,84 +187,87 @@ class PaymentEntry(AccountsController):
return False
def validate_allocated_amount_with_latest_data(self):
latest_references = get_outstanding_reference_documents(
{
"posting_date": self.posting_date,
"company": self.company,
"party_type": self.party_type,
"payment_type": self.payment_type,
"party": self.party,
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
"get_outstanding_invoices": True,
"get_orders_to_be_billed": True,
}
)
if self.references:
uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references])
vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
latest_references = get_outstanding_reference_documents(
{
"posting_date": self.posting_date,
"company": self.company,
"party_type": self.party_type,
"payment_type": self.payment_type,
"party": self.party,
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
"get_outstanding_invoices": True,
"get_orders_to_be_billed": True,
"vouchers": vouchers,
}
)
# Group latest_references by (voucher_type, voucher_no)
latest_lookup = {}
for d in latest_references:
d = frappe._dict(d)
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
# Group latest_references by (voucher_type, voucher_no)
latest_lookup = {}
for d in latest_references:
d = frappe._dict(d)
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
for idx, d in enumerate(self.get("references"), start=1):
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
for idx, d in enumerate(self.get("references"), start=1):
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
# If term based allocation is enabled, throw
if (
d.payment_term is None or d.payment_term == ""
) and self.term_based_allocation_enabled_for_reference(
d.reference_doctype, d.reference_name
):
frappe.throw(
_(
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
).format(frappe.bold(d.reference_name), frappe.bold(idx))
)
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
latest = latest.get(d.payment_term) or latest.get(None)
# The reference has already been fully paid
if not latest:
frappe.throw(
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
)
# The reference has already been partly paid
elif latest.outstanding_amount < latest.invoice_amount and flt(
d.outstanding_amount, d.precision("outstanding_amount")
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
frappe.throw(
_(
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
).format(_(d.reference_doctype), d.reference_name)
)
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
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(
_(
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
).format(
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
# If term based allocation is enabled, throw
if (
d.payment_term is None or d.payment_term == ""
) and self.term_based_allocation_enabled_for_reference(
d.reference_doctype, d.reference_name
):
frappe.throw(
_(
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
).format(frappe.bold(d.reference_name), frappe.bold(idx))
)
)
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
latest = latest.get(d.payment_term) or latest.get(None)
# 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))
# The reference has already been fully paid
if not latest:
frappe.throw(
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
)
# The reference has already been partly paid
elif latest.outstanding_amount < latest.invoice_amount and flt(
d.outstanding_amount, d.precision("outstanding_amount")
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
frappe.throw(
_(
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
).format(_(d.reference_doctype), d.reference_name)
)
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
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(
_(
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
).format(
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
)
)
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))
def delink_advance_entry_references(self):
for reference in self.references:
@@ -361,7 +372,7 @@ class PaymentEntry(AccountsController):
else:
if ref_doc:
if self.paid_from_account_currency == ref_doc.currency:
self.source_exchange_rate = ref_doc.get("exchange_rate")
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
if not self.source_exchange_rate:
self.source_exchange_rate = get_exchange_rate(
@@ -374,7 +385,7 @@ class PaymentEntry(AccountsController):
elif self.paid_to and not self.target_exchange_rate:
if ref_doc:
if self.paid_to_account_currency == ref_doc.currency:
self.target_exchange_rate = ref_doc.get("exchange_rate")
self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
if not self.target_exchange_rate:
self.target_exchange_rate = get_exchange_rate(
@@ -783,10 +794,30 @@ class PaymentEntry(AccountsController):
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
)
else:
# Use source/target exchange rate, so no difference amount is calculated.
# then update exchange gain/loss amount in reference table
# if there is an exchange gain/loss amount in reference table, submit a JE for that
exchange_rate = 1
if self.payment_type == "Receive":
exchange_rate = self.source_exchange_rate
elif self.payment_type == "Pay":
exchange_rate = self.target_exchange_rate
base_allocated_amount += flt(
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
)
# on rare case, when `exchange_rate` is unset, gain/loss amount is incorrectly calculated
# for base currency transactions
if d.exchange_rate is None:
d.exchange_rate = 1
allocated_amount_in_pe_exchange_rate = flt(
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
)
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
return base_allocated_amount
def set_total_allocated_amount(self):
@@ -977,6 +1008,10 @@ class PaymentEntry(AccountsController):
gl_entries = self.build_gl_map()
gl_entries = process_gl_map(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
if cancel:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
else:
self.make_exchange_gain_loss_journal()
def add_party_gl_entries(self, gl_entries):
if self.party_account:
@@ -1420,6 +1455,14 @@ def get_outstanding_reference_documents(args):
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0]))
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1]))
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
@@ -1438,6 +1481,7 @@ def get_outstanding_reference_documents(args):
min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"),
accounting_dimensions=accounting_dimensions_filter,
vouchers=args.get("vouchers") or None,
)
outstanding_invoices = split_invoices_based_on_payment_terms(
@@ -1836,10 +1880,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
if not total_amount:
if party_account_currency == company_currency:
# for handling cases that don't have multi-currency (base field)
total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total")
total_amount = (
ref_doc.get("base_rounded_total")
or ref_doc.get("rounded_total")
or ref_doc.get("base_grand_total")
or ref_doc.get("grand_total")
)
exchange_rate = 1
else:
total_amount = ref_doc.get("grand_total")
total_amount = ref_doc.get("rounded_total") or ref_doc.get("grand_total")
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc.
@@ -1878,7 +1927,6 @@ def get_payment_entry(
payment_type=None,
reference_date=None,
):
reference_doc = None
doc = frappe.get_doc(dt, dn)
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
@@ -2019,7 +2067,7 @@ def get_payment_entry(
update_accounting_dimensions(pe, doc)
if party_account and bank:
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_exchange_rate(ref_doc=doc)
pe.set_amounts()
if discount_amount:
@@ -2132,7 +2180,7 @@ def set_paid_amount_and_received_amount(
if bank_amount:
received_amount = bank_amount
else:
if company_currency != bank.account_currency:
if bank and company_currency != bank.account_currency:
received_amount = paid_amount / doc.get("conversion_rate", 1)
else:
received_amount = paid_amount * doc.get("conversion_rate", 1)
@@ -2141,7 +2189,7 @@ def set_paid_amount_and_received_amount(
if bank_amount:
paid_amount = bank_amount
else:
if company_currency != bank.account_currency:
if bank and company_currency != bank.account_currency:
paid_amount = received_amount / doc.get("conversion_rate", 1)
else:
# if party account currency and bank currency is different then populate paid amount as well

View File

@@ -31,6 +31,16 @@ class TestPaymentEntry(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def get_journals_for(self, voucher_type: str, voucher_no: str) -> list:
journals = []
if voucher_type and voucher_no:
journals = frappe.db.get_all(
"Journal Entry Account",
filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1},
fields=["parent"],
)
return journals
def test_payment_entry_against_order(self):
so = make_sales_order()
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
@@ -591,21 +601,15 @@ class TestPaymentEntry(FrappeTestCase):
pe.target_exchange_rate = 45.263
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
pe.append(
"deductions",
{
"account": "_Test Exchange Gain/Loss - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": 94.80,
},
)
pe.save()
self.assertEqual(flt(pe.difference_amount, 2), 0.0)
self.assertEqual(flt(pe.unallocated_amount, 2), 0.0)
# the exchange gain/loss amount is captured in reference table and a separate Journal will be submitted for them
# payment entry will not be generating difference amount
self.assertEqual(flt(pe.references[0].exchange_gain_loss, 2), -94.74)
def test_payment_entry_retrieves_last_exchange_rate(self):
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
save_new_records,
@@ -792,33 +796,28 @@ class TestPaymentEntry(FrappeTestCase):
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
pe.source_exchange_rate = 55
pe.append(
"deductions",
{
"account": "_Test Exchange Gain/Loss - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": -500,
},
)
pe.save()
self.assertEqual(pe.unallocated_amount, 0)
self.assertEqual(pe.difference_amount, 0)
self.assertEqual(pe.references[0].exchange_gain_loss, 500)
pe.submit()
expected_gle = dict(
(d[0], d)
for d in [
["_Test Receivable USD - _TC", 0, 5000, si.name],
["_Test Receivable USD - _TC", 0, 5500, si.name],
["_Test Bank USD - _TC", 5500, 0, None],
["_Test Exchange Gain/Loss - _TC", 0, 500, None],
]
)
self.validate_gl_entries(pe.name, expected_gle)
# Exchange gain/loss should have been posted through a journal
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
self.assertEqual(exc_je_for_si, exc_je_for_pe)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
@@ -1202,6 +1201,24 @@ class TestPaymentEntry(FrappeTestCase):
template.allocate_payment_based_on_payment_terms = 1
template.save()
def test_allocation_validation_for_sales_order(self):
so = make_sales_order(do_not_save=True)
so.items[0].rate = 99.55
so.save().submit()
self.assertGreater(so.rounded_total, 0.0)
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_from = "Debtors - _TC"
pe.paid_amount = 45.55
pe.references[0].allocated_amount = 45.55
pe.save().submit()
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_from = "Debtors - _TC"
# No validation error should be thrown here.
pe.save().submit()
so.reload()
self.assertEqual(so.advance_paid, so.rounded_total)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -151,6 +151,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.refresh();
}
invoice_name() {
this.frm.trigger("get_unreconciled_entries");
}
payment_name() {
this.frm.trigger("get_unreconciled_entries");
}
clear_child_tables() {
this.frm.clear_table("invoices");
this.frm.clear_table("payments");

View File

@@ -26,8 +26,10 @@
"bank_cash_account",
"cost_center",
"sec_break1",
"invoice_name",
"invoices",
"column_break_15",
"payment_name",
"payments",
"sec_break2",
"allocation"
@@ -136,6 +138,7 @@
"label": "Minimum Invoice Amount"
},
{
"default": "50",
"description": "System will fetch all the entries if limit value is zero.",
"fieldname": "invoice_limit",
"fieldtype": "Int",
@@ -166,6 +169,7 @@
"label": "Maximum Payment Amount"
},
{
"default": "50",
"description": "System will fetch all the entries if limit value is zero.",
"fieldname": "payment_limit",
"fieldtype": "Int",
@@ -185,13 +189,23 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "invoice_name",
"fieldtype": "Data",
"label": "Filter on Invoice"
},
{
"fieldname": "payment_name",
"fieldtype": "Data",
"label": "Filter on Payment"
}
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"issingle": 1,
"links": [],
"modified": "2022-04-29 15:37:10.246831",
"modified": "2023-08-15 05:35:50.109290",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
@@ -218,4 +232,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -5,8 +5,9 @@
import frappe
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
@@ -14,6 +15,7 @@ from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_rec
)
from erpnext.accounts.utils import (
QueryPaymentLedger,
create_gain_loss_journal,
get_outstanding_invoices,
reconcile_against_document,
)
@@ -57,6 +59,9 @@ class PaymentReconciliation(Document):
def get_payment_entries(self):
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True)
if self.payment_name:
condition += "name like '%%{0}%%'".format(self.payment_name)
payment_entries = get_advance_payment_entries(
self.party_type,
self.party,
@@ -72,6 +77,9 @@ class PaymentReconciliation(Document):
def get_jv_entries(self):
condition = self.get_conditions()
if self.payment_name:
condition += f" and t1.name like '%%{self.payment_name}%%'"
if self.get("cost_center"):
condition += f" and t2.cost_center = '{self.cost_center}' "
@@ -92,7 +100,7 @@ class PaymentReconciliation(Document):
"Journal Entry" as reference_type, t1.name as reference_name,
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
t2.account_currency as currency
t2.account_currency as currency, t2.cost_center as cost_center
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
@@ -129,6 +137,15 @@ class PaymentReconciliation(Document):
def get_return_invoices(self):
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
doc = qb.DocType(voucher_type)
conditions = []
conditions.append(doc.docstatus == 1)
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
conditions.append(doc.is_return == 1)
if self.payment_name:
conditions.append(doc.name.like(f"%{self.payment_name}%"))
self.return_invoices = (
qb.from_(doc)
.select(
@@ -136,11 +153,7 @@ class PaymentReconciliation(Document):
doc.name.as_("voucher_no"),
doc.return_against,
)
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
)
.where(Criterion.all(conditions))
.run(as_dict=True)
)
@@ -183,6 +196,7 @@ class PaymentReconciliation(Document):
"amount": -(inv.outstanding_in_account_currency),
"posting_date": inv.posting_date,
"currency": inv.currency,
"cost_center": inv.cost_center,
}
)
)
@@ -209,6 +223,8 @@ class PaymentReconciliation(Document):
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
accounting_dimensions=self.accounting_dimension_filter_conditions,
limit=self.invoice_limit,
voucher_no=self.invoice_name,
)
cr_dr_notes = (
@@ -260,6 +276,11 @@ class PaymentReconciliation(Document):
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
if payment_entry[0].get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
payment_entry[0]["exchange_rate"] = invoice_exchange_map.get(
payment_entry[0].get("reference_name")
)
new_difference_amount = self.get_difference_amount(
payment_entry[0], invoice[0], allocated_amount
)
@@ -324,6 +345,7 @@ class PaymentReconciliation(Document):
"allocated_amount": allocated_amount,
"difference_amount": pay.get("difference_amount"),
"currency": inv.get("currency"),
"cost_center": pay.get("cost_center"),
}
)
@@ -347,12 +369,6 @@ class PaymentReconciliation(Document):
payment_details = self.get_payment_details(row, dr_or_cr)
reconciled_entry.append(payment_details)
if payment_details.difference_amount and row.reference_type not in [
"Sales Invoice",
"Purchase Invoice",
]:
self.make_difference_entry(payment_details)
if entry_list:
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
@@ -385,59 +401,6 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries()
def make_difference_entry(self, row):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
journal_entry.company = self.company
journal_entry.posting_date = nowdate()
journal_entry.multi_currency = 1
party_account_currency = frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
)
difference_account_currency = frappe.get_cached_value(
"Account", row.difference_account, "account_currency"
)
# Account Currency has balance
dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
journal_account = frappe._dict(
{
"account": self.receivable_payable_account,
"party_type": self.party_type,
"party": self.party,
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"reference_type": row.against_voucher_type,
"reference_name": row.against_voucher,
dr_or_cr: flt(row.difference_amount),
dr_or_cr + "_in_account_currency": 0,
}
)
journal_entry.append("accounts", journal_account)
journal_account = frappe._dict(
{
"account": row.difference_account,
"account_currency": difference_account_currency,
"exchange_rate": 1,
"cost_center": erpnext.get_default_cost_center(self.company),
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
reverse_dr_or_cr: flt(row.difference_amount),
}
)
journal_entry.append("accounts", journal_account)
journal_entry.save()
journal_entry.submit()
return journal_entry
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
{
@@ -457,6 +420,7 @@ class PaymentReconciliation(Document):
"allocated_amount": flt(row.get("allocated_amount")),
"difference_amount": flt(row.get("difference_amount")),
"difference_account": row.get("difference_account"),
"cost_center": row.get("cost_center"),
}
)
@@ -603,16 +567,6 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company):
def get_difference_row(inv):
if inv.difference_amount != 0 and inv.difference_account:
difference_row = {
"account": inv.difference_account,
inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
"cost_center": erpnext.get_default_cost_center(company),
}
return difference_row
for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
@@ -639,7 +593,9 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
inv.dr_or_cr: abs(inv.allocated_amount),
"reference_type": inv.against_voucher_type,
"reference_name": inv.against_voucher,
"cost_center": erpnext.get_default_cost_center(company),
"cost_center": inv.cost_center or erpnext.get_default_cost_center(company),
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}",
"exchange_rate": inv.exchange_rate,
},
{
"account": inv.account,
@@ -652,14 +608,45 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
),
"reference_type": inv.voucher_type,
"reference_name": inv.voucher_no,
"cost_center": erpnext.get_default_cost_center(company),
"cost_center": inv.cost_center or erpnext.get_default_cost_center(company),
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}",
"exchange_rate": inv.exchange_rate,
},
],
}
)
if difference_entry := get_difference_row(inv):
jv.append("accounts", difference_entry)
jv.flags.ignore_mandatory = True
jv.flags.skip_remarks_creation = True
jv.flags.ignore_exchange_rate = True
jv.is_system_generated = True
jv.remark = None
jv.submit()
if inv.difference_amount != 0:
# make gain/loss journal
if inv.party_type == "Customer":
dr_or_cr = "credit" if inv.difference_amount < 0 else "debit"
else:
dr_or_cr = "debit" if inv.difference_amount < 0 else "credit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
create_gain_loss_journal(
company,
today(),
inv.party_type,
inv.party,
inv.account,
inv.difference_account,
inv.difference_amount,
dr_or_cr,
reverse_dr_or_cr,
inv.voucher_type,
inv.voucher_no,
None,
inv.against_voucher_type,
inv.against_voucher,
None,
inv.cost_center,
)

View File

@@ -686,14 +686,24 @@ class TestPaymentReconciliation(FrappeTestCase):
# Check if difference journal entry gets generated for difference amount after reconciliation
pr.reconcile()
total_debit_amount = frappe.db.get_all(
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(debit) as amount",
"sum(credit) as amount",
group_by="reference_name",
)[0].amount
self.assertEqual(flt(total_debit_amount, 2), -500)
# total credit includes the exchange gain/loss amount
self.assertEqual(flt(total_credit_amount, 2), 8500)
jea_parent = frappe.db.get_all(
"Journal Entry Account",
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
fields=["parent"],
)[0]
self.assertEqual(
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
)
def test_difference_amount_via_payment_entry(self):
# Make Sale Invoice

View File

@@ -22,7 +22,8 @@
"column_break_7",
"difference_account",
"exchange_rate",
"currency"
"currency",
"cost_center"
],
"fields": [
{
@@ -144,11 +145,17 @@
"fieldtype": "Float",
"label": "Exchange Rate",
"read_only": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
}
],
"istable": 1,
"links": [],
"modified": "2022-12-24 21:01:14.882747",
"modified": "2023-09-03 07:52:33.684217",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",

View File

@@ -16,7 +16,8 @@
"sec_break1",
"remark",
"currency",
"exchange_rate"
"exchange_rate",
"cost_center"
],
"fields": [
{
@@ -98,11 +99,17 @@
"fieldtype": "Float",
"hidden": 1,
"label": "Exchange Rate"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
}
],
"istable": 1,
"links": [],
"modified": "2022-11-08 18:18:36.268760",
"modified": "2023-09-03 07:43:29.965353",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Payment",

View File

@@ -144,8 +144,7 @@ class TestPaymentRequest(unittest.TestCase):
(d[0], d)
for d in [
["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
[pr.payment_account, 6290.0, 0, None],
["_Test Exchange Gain/Loss - _TC", 0, 1290, None],
[pr.payment_account, 5000.0, 0, None],
]
)

View File

@@ -126,7 +126,7 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self, get_opening_entries=False):
gl_entries = self.get_gl_entries()
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
if len(gl_entries) > 5000:
if len(gl_entries + closing_entries) > 3000:
frappe.enqueue(
process_gl_entries,
gl_entries=gl_entries,

View File

@@ -153,7 +153,7 @@ frappe.ui.form.on('POS Closing Entry', {
frappe.ui.form.on('POS Closing Entry Detail', {
closing_amount: (frm, cdt, cdn) => {
const row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount));
frappe.model.set_value(cdt, cdn, "difference", flt(row.closing_amount - row.expected_amount));
}
})

View File

@@ -130,6 +130,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
args: { "pos_profile": frm.pos_profile },
callback: ({ message: profile }) => {
this.update_customer_groups_settings(profile?.customer_groups);
this.frm.set_value("company", profile?.company);
},
});
}

View File

@@ -1,6 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
# For license information, please see license.txt
import collections
import frappe
from frappe import _
@@ -43,6 +43,7 @@ class POSInvoice(SalesInvoice):
self.validate_debit_to_acc()
self.validate_write_off_account()
self.validate_change_amount()
self.validate_duplicate_serial_and_batch_no()
self.validate_change_account()
self.validate_item_cost_centers()
self.validate_warehouse()
@@ -54,6 +55,7 @@ class POSInvoice(SalesInvoice):
self.validate_pos()
self.validate_payment_amount()
self.validate_loyalty_transaction()
self.validate_company_with_pos_company()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -154,6 +156,27 @@ class POSInvoice(SalesInvoice):
title=_("Item Unavailable"),
)
def validate_duplicate_serial_and_batch_no(self):
serial_nos = []
batch_nos = []
for row in self.get("items"):
if row.serial_no:
serial_nos = row.serial_no.split("\n")
if row.batch_no and not row.serial_no:
batch_nos.append(row.batch_no)
if serial_nos:
for key, value in collections.Counter(serial_nos).items():
if value > 1:
frappe.throw(_("Duplicate Serial No {0} found").format("key"))
if batch_nos:
for key, value in collections.Counter(batch_nos).items():
if value > 1:
frappe.throw(_("Duplicate Batch No {0} found").format("key"))
def validate_pos_reserved_batch_qty(self, item):
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no}
@@ -370,6 +393,14 @@ class POSInvoice(SalesInvoice):
if total_amount_in_payments and total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
def validate_company_with_pos_company(self):
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
frappe.throw(
_("Company {} does not match with POS Profile Company {}").format(
self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company")
)
)
def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
@@ -448,6 +479,7 @@ class POSInvoice(SalesInvoice):
profile = {}
if self.pos_profile:
profile = frappe.get_doc("POS Profile", self.pos_profile)
self.company = profile.get("company")
if not self.get("payments") and not for_validate:
update_multi_mode_option(self, profile)
@@ -651,7 +683,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.stock_qty
max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):

View File

@@ -49,6 +49,7 @@
"column_break_21",
"start_date",
"section_break_33",
"pdf_name",
"subject",
"column_break_28",
"cc_to",
@@ -273,7 +274,7 @@
"fieldname": "help_text",
"fieldtype": "HTML",
"label": "Help Text",
"options": "<br>\n<h4>Note</h4>\n<ul>\n<li>\nYou can use <a href=\"https://jinja.palletsprojects.com/en/2.11.x/\" target=\"_blank\">Jinja tags</a> in <b>Subject</b> and <b>Body</b> fields for dynamic values.\n</li><li>\n All fields in this doctype are available under the <b>doc</b> object and all fields for the customer to whom the mail will go to is available under the <b>customer</b> object.\n</li></ul>\n<h4> Examples</h4>\n<!-- {% raw %} -->\n<ul>\n <li><b>Subject</b>:<br><br><pre><code>Statement Of Accounts for {{ customer.name }}</code></pre><br></li>\n <li><b>Body</b>: <br><br>\n<pre><code>Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.</code> </pre></li>\n</ul>\n<!-- {% endraw %} -->"
"options": "<br>\n<h4>Note</h4>\n<ul>\n<li>\nYou can use <a href=\"https://jinja.palletsprojects.com/en/2.11.x/\" target=\"_blank\">Jinja tags</a> in <b>Subject</b> and <b>Body</b> fields for dynamic values.\n</li><li>\n All fields in this doctype are available under the <b>doc</b> object and all fields for the customer to whom the mail will go to is available under the <b>customer</b> object.\n</li></ul>\n<h4> Examples</h4>\n<!-- {% raw %} -->\n<ul>\n <li><b>Subject</b>:<br><br><pre><code>Statement Of Accounts for {{ customer.customer_name }}</code></pre><br></li>\n <li><b>Body</b>: <br><br>\n<pre><code>Hello {{ customer.customer_name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.</code> </pre></li>\n</ul>\n<!-- {% endraw %} -->"
},
{
"fieldname": "subject",
@@ -368,10 +369,15 @@
"fieldname": "based_on_payment_terms",
"fieldtype": "Check",
"label": "Based On Payment Terms"
},
{
"fieldname": "pdf_name",
"fieldtype": "Data",
"label": "PDF Name"
}
],
"links": [],
"modified": "2023-06-23 10:13:15.051950",
"modified": "2023-08-28 12:59:53.071334",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -26,7 +26,13 @@ class ProcessStatementOfAccounts(Document):
if not self.subject:
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
if not self.body:
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
if self.report == "General Ledger":
body_str = " from {{ doc.from_date }} to {{ doc.to_date }}."
else:
body_str = " until {{ doc.posting_date }}."
self.body = "Hello {{ customer.customer_name }},<br>PFA your Statement Of Accounts" + body_str
if not self.pdf_name:
self.pdf_name = "{{ customer.customer_name }}"
validate_template(self.subject)
validate_template(self.body)
@@ -57,11 +63,6 @@ def get_report_pdf(doc, consolidated=True):
filters = get_common_filters(doc)
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
else:
filters.update(get_ar_filters(doc, entry))
if doc.report == "General Ledger":
col, res = get_soa(filters)
for x in [0, -2, -1]:
@@ -69,8 +70,11 @@ def get_report_pdf(doc, consolidated=True):
if len(res) == 3:
continue
else:
filters.update(get_ar_filters(doc, entry))
ar_res = get_ar_soa(filters)
col, res = ar_res[0], ar_res[1]
if not res:
continue
statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
@@ -139,6 +143,7 @@ def get_ar_filters(doc, entry):
return {
"report_date": doc.posting_date if doc.posting_date else None,
"customer": entry.customer,
"customer_name": entry.customer_name if entry.customer_name else None,
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
"sales_partner": doc.sales_partner if doc.sales_partner else None,
"sales_person": doc.sales_person if doc.sales_person else None,
@@ -362,16 +367,20 @@ def download_statements(document_name):
@frappe.whitelist()
def send_emails(document_name, from_scheduler=False):
def send_emails(document_name, from_scheduler=False, posting_date=None):
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
report = get_report_pdf(doc, consolidated=False)
if report:
for customer, report_pdf in report.items():
attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}]
context = get_context(customer, doc)
filename = frappe.render_template(doc.pdf_name, context)
attachments = [{"fname": filename + ".pdf", "fcontent": report_pdf}]
recipients, cc = get_recipients_and_cc(customer, doc)
context = get_context(customer, doc)
if not recipients:
continue
subject = frappe.render_template(doc.subject, context)
message = frappe.render_template(doc.body, context)
@@ -390,7 +399,7 @@ def send_emails(document_name, from_scheduler=False):
)
if doc.enable_auto_email and from_scheduler:
new_to_date = getdate(today())
new_to_date = getdate(posting_date or today())
if doc.frequency == "Weekly":
new_to_date = add_days(new_to_date, 7)
else:
@@ -399,8 +408,11 @@ def send_emails(document_name, from_scheduler=False):
doc.add_comment(
"Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())
)
doc.db_set("to_date", new_to_date, commit=True)
doc.db_set("from_date", new_from_date, commit=True)
if doc.report == "General Ledger":
doc.db_set("to_date", new_to_date, commit=True)
doc.db_set("from_date", new_from_date, commit=True)
else:
doc.db_set("posting_date", new_to_date, commit=True)
return True
else:
return False
@@ -410,7 +422,8 @@ def send_emails(document_name, from_scheduler=False):
def send_auto_email():
selected = frappe.get_list(
"Process Statement Of Accounts",
filters={"to_date": format_date(today()), "enable_auto_email": 1},
filters={"enable_auto_email": 1},
or_filters={"to_date": format_date(today()), "posting_date": format_date(today())},
)
for entry in selected:
send_emails(entry.name, from_scheduler=True)

View File

@@ -8,9 +8,24 @@
}
</style>
<div id="header-html" class="hidden-pdf">
{% if letter_head.content %}
<div class="letter-head text-center">{{ letter_head.content }}</div>
<hr style="height:2px;border-width:0;color:black;background-color:black;">
{% endif %}
</div>
<div id="footer-html" class="visible-pdf">
{% if letter_head.footer %}
<div class="letter-head-footer">
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
{{ letter_head.footer }}
</div>
{% endif %}
</div>
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
<h4 class="text-center">
{{ filters.customer }}
{{ filters.customer_name }}
</h4>
<h6 class="text-center">
{% if (filters.tax_id) %}
@@ -341,4 +356,9 @@
</tbody>
</table>
{% endif %}
{% if terms_and_conditions %}
<div>
{{ terms_and_conditions }}
</div>
{% endif %}
<p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>

View File

@@ -1,9 +1,42 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
import frappe
from frappe.utils import add_days, getdate, today
from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import (
send_emails,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestProcessStatementOfAccounts(unittest.TestCase):
pass
def setUp(self):
self.si = create_sales_invoice()
self.process_soa = create_process_soa()
def test_auto_email_for_process_soa_ar(self):
send_emails(self.process_soa.name, from_scheduler=True)
self.process_soa.load_from_db()
self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7)))
def tearDown(self):
frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
def create_process_soa():
frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
process_soa = frappe.new_doc("Process Statement Of Accounts")
soa_dict = {
"name": "Test Process SOA",
"company": "_Test Company",
}
process_soa.update(soa_dict)
process_soa.set("customers", [{"customer": "_Test Customer"}])
process_soa.enable_auto_email = 1
process_soa.frequency = "Weekly"
process_soa.report = "Accounts Receivable"
process_soa.save()
return process_soa

View File

@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload();
// Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal) {
// show credit_to in print format

View File

@@ -269,9 +269,7 @@ class PurchaseInvoice(BuyingController):
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
stock_items = self.get_stock_items()
asset_items = [d.is_fixed_asset for d in self.items if d.is_fixed_asset]
if len(asset_items) > 0:
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
asset_received_but_not_billed = None
if self.update_stock:
self.validate_item_code()
@@ -365,6 +363,8 @@ class PurchaseInvoice(BuyingController):
)
item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail:
if not asset_received_but_not_billed:
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
@@ -543,6 +543,7 @@ class PurchaseInvoice(BuyingController):
merge_entries=False,
from_repost=from_repost,
)
self.make_exchange_gain_loss_journal()
elif self.docstatus == 2:
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -587,7 +588,6 @@ class PurchaseInvoice(BuyingController):
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
@@ -768,21 +768,22 @@ class PurchaseInvoice(BuyingController):
# Amount added through landed-cost-voucher
if landed_cost_entries:
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": item.expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount["base_amount"]),
"credit_in_account_currency": flt(amount["amount"]),
"project": item.project or self.project,
},
item=item,
if (item.item_code, item.name) in landed_cost_entries:
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": item.expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount["base_amount"]),
"credit_in_account_currency": flt(amount["amount"]),
"project": item.project or self.project,
},
item=item,
)
)
)
# sub-contracting warehouse
if flt(item.rm_supp_cost):
@@ -977,8 +978,9 @@ class PurchaseInvoice(BuyingController):
)
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")
arbnb_account = None
eiiav_account = None
asset_eiiav_currency = None
for item in self.get("items"):
if item.is_fixed_asset:
@@ -990,6 +992,8 @@ class PurchaseInvoice(BuyingController):
"Asset Received But Not Billed",
"Fixed Asset",
]:
if not arbnb_account:
arbnb_account = self.get_company_default("asset_received_but_not_billed")
item.expense_account = arbnb_account
if not self.update_stock:
@@ -1012,7 +1016,10 @@ class PurchaseInvoice(BuyingController):
)
if item.item_tax_amount:
asset_eiiav_currency = get_account_currency(eiiav_account)
if not eiiav_account or not asset_eiiav_currency:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
asset_eiiav_currency = get_account_currency(eiiav_account)
gl_entries.append(
self.get_gl_dict(
{
@@ -1055,7 +1062,10 @@ class PurchaseInvoice(BuyingController):
)
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
asset_eiiav_currency = get_account_currency(eiiav_account)
if not eiiav_account or not asset_eiiav_currency:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
asset_eiiav_currency = get_account_currency(eiiav_account)
gl_entries.append(
self.get_gl_dict(
{
@@ -1075,47 +1085,46 @@ class PurchaseInvoice(BuyingController):
)
)
# When update stock is checked
# Assets are bought through this document then it will be linked to this document
if self.update_stock:
if flt(item.landed_cost_voucher_amount):
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": cwip_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
if flt(item.landed_cost_voucher_amount):
if not eiiav_account:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
gl_entries.append(
self.get_gl_dict(
{
"account": cwip_account,
"against": eiiav_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": cwip_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
# update gross amount of assets bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value(
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
gl_entries.append(
self.get_gl_dict(
{
"account": cwip_account,
"against": eiiav_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
# update gross amount of assets bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
return gl_entries
@@ -1422,6 +1431,8 @@ class PurchaseInvoice(BuyingController):
"Repost Item Valuation",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
)

View File

@@ -1153,7 +1153,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True)
item.enable_deferred_expense = 1
item.deferred_expense_account = deferred_account
item.item_defaults[0].deferred_expense_account = deferred_account
item.save()
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
@@ -1264,10 +1264,11 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pi.save()
pi.submit()
creditors_account = pi.credit_to
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 37500.0],
["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", -2500.0],
["_Test Payable USD - _TC", -37500.0],
]
gl_entries = frappe.db.sql(
@@ -1284,6 +1285,31 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
pi.reload()
self.assertEqual(pi.outstanding_amount, 0)
total_debit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": creditors_account, "docstatus": 1, "reference_name": pi.name},
"sum(debit) as amount",
group_by="reference_name",
)[0].amount
self.assertEqual(flt(total_debit_amount, 2), 2500)
jea_parent = frappe.db.get_all(
"Journal Entry Account",
filters={
"account": creditors_account,
"docstatus": 1,
"reference_name": pi.name,
"debit": 2500,
"debit_in_account_currency": 0,
},
fields=["parent"],
)[0]
self.assertEqual(
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
)
pi_2 = make_purchase_invoice(
supplier="_Test Supplier USD",
currency="USD",
@@ -1308,10 +1334,12 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pi_2.save()
pi_2.submit()
pi_2.reload()
self.assertEqual(pi_2.outstanding_amount, 0)
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 36500.0],
["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", -1500.0],
["_Test Payable USD - _TC", -36500.0],
]
gl_entries = frappe.db.sql(
@@ -1342,12 +1370,39 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
total_debit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name},
"sum(debit) as amount",
group_by="reference_name",
)[0].amount
self.assertEqual(flt(total_debit_amount, 2), 1500)
jea_parent_2 = frappe.db.get_all(
"Journal Entry Account",
filters={
"account": creditors_account,
"docstatus": 1,
"reference_name": pi_2.name,
"debit": 1500,
"debit_in_account_currency": 0,
},
fields=["parent"],
)[0]
self.assertEqual(
frappe.db.get_value("Journal Entry", jea_parent_2.parent, "voucher_type"),
"Exchange Gain Or Loss",
)
pi.reload()
pi.cancel()
self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent.parent, "docstatus"), 2)
pi_2.reload()
pi_2.cancel()
self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent_2.parent, "docstatus"), 2)
pay.reload()
pay.cancel()
@@ -1716,23 +1771,101 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
0,
)
def test_offsetting_entries_for_accounting_dimensions(self):
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.report.trial_balance.test_trial_balance import (
clear_dimension_defaults,
create_accounting_dimension,
disable_dimension,
)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(
"""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s
order by posting_date asc, account asc""",
(voucher_no, posting_date),
as_dict=1,
create_account(
account_name="Offsetting",
company="_Test Company",
parent_account="Temporary Accounts - _TC",
)
create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC")
branch1 = frappe.new_doc("Branch")
branch1.branch = "Location 1"
branch1.insert(ignore_if_duplicate=True)
branch2 = frappe.new_doc("Branch")
branch2.branch = "Location 2"
branch2.insert(ignore_if_duplicate=True)
pi = make_purchase_invoice(
company="_Test Company",
customer="_Test Supplier",
do_not_save=True,
do_not_submit=True,
rate=1000,
price_list_rate=1000,
qty=1,
)
pi.branch = branch1.branch
pi.items[0].branch = branch2.branch
pi.save()
pi.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), branch2.branch],
["Creditors - _TC", 0.0, 1000, nowdate(), branch1.branch],
["Offsetting - _TC", 1000, 0.0, nowdate(), branch1.branch],
["Offsetting - _TC", 0.0, 1000, nowdate(), branch2.branch],
]
check_gl_entries(
self,
pi.name,
expected_gle,
nowdate(),
voucher_type="Purchase Invoice",
additional_columns=["branch"],
)
clear_dimension_defaults("Branch")
disable_dimension()
def check_gl_entries(
doc,
voucher_no,
expected_gle,
posting_date,
voucher_type="Purchase Invoice",
additional_columns=None,
):
gl = frappe.qb.DocType("GL Entry")
query = (
frappe.qb.from_(gl)
.select(gl.account, gl.debit, gl.credit, gl.posting_date)
.where(
(gl.voucher_type == voucher_type)
& (gl.voucher_no == voucher_no)
& (gl.posting_date >= posting_date)
& (gl.is_cancelled == 0)
)
.orderby(gl.posting_date, gl.account, gl.creation)
)
if additional_columns:
for col in additional_columns:
query = query.select(gl[col])
gl_entries = query.run(as_dict=True)
for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account)
doc.assertEqual(expected_gle[i][1], gle.debit)
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
if additional_columns:
j = 4
for col in additional_columns:
doc.assertEqual(expected_gle[i][j], gle[col])
j += 1
def create_tax_witholding_category(category_name, company, account):
from erpnext.accounts.utils import get_fiscal_year

View File

@@ -0,0 +1,44 @@
<style>
.print-format {
padding: 4mm;
font-size: 8.0pt !important;
}
.print-format td {
vertical-align:middle !important;
}
.old {
background-color: #FFB3C0;
}
.new {
background-color: #B3FFCC;
}
</style>
<table class="table table-bordered table-condensed">
<colgroup>
{% for col in gl_columns%}
<col style="width: 18mm;">
{% endfor %}
</colgroup>
<thead>
<tr>
{% for col in gl_columns%}
<td>{{ col.label }}</td>
{% endfor %}
</tr>
</thead>
{% for gl in gl_data%}
{% if gl["old"]%}
<tr class="old">
{% else %}
<tr class="new">
{% endif %}
{% for col in gl_columns %}
<td class="text-right">
{{ gl[col.fieldname] }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Repost Accounting Ledger", {
setup: function(frm) {
frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
return {
filters: {
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
}
}
}
frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
if (doc.company) {
return {
filters: {
company: doc.company,
docstatus: 1
}
}
}
}
},
refresh: function(frm) {
frm.add_custom_button(__('Show Preview'), () => {
frm.call({
method: 'generate_preview',
doc: frm.doc,
freeze: true,
freeze_message: __('Generating Preview'),
callback: function(r) {
if (r && r.message) {
let content = r.message;
let opts = {
title: "Preview",
subtitle: "preview",
content: content,
print_settings: {orientation: "landscape"},
columns: [],
data: [],
}
frappe.render_grid(opts);
}
}
});
});
}
});

View File

@@ -0,0 +1,81 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:ACC-REPOST-{#####}",
"creation": "2023-07-04 13:07:32.923675",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"column_break_vpup",
"delete_cancelled_entries",
"section_break_metl",
"vouchers",
"amended_from"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Repost Accounting Ledger",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "vouchers",
"fieldtype": "Table",
"label": "Vouchers",
"options": "Repost Accounting Ledger Items"
},
{
"fieldname": "column_break_vpup",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_metl",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "delete_cancelled_entries",
"fieldtype": "Check",
"label": "Delete Cancelled Ledger Entries"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-07-27 15:47:58.975034",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,183 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.utils.data import comma_and
class RepostAccountingLedger(Document):
def __init__(self, *args, **kwargs):
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
self._allowed_types = set(
["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
)
def validate(self):
self.validate_vouchers()
self.validate_for_closed_fiscal_year()
self.validate_for_deferred_accounting()
def validate_for_deferred_accounting(self):
sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
docs_with_deferred_revenue = frappe.db.get_all(
"Sales Invoice Item",
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
fields=["parent"],
as_list=1,
)
purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
docs_with_deferred_expense = frappe.db.get_all(
"Purchase Invoice Item",
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
fields=["parent"],
as_list=1,
)
if docs_with_deferred_revenue or docs_with_deferred_expense:
frappe.throw(
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
frappe.bold(
comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
)
)
)
def validate_for_closed_fiscal_year(self):
if self.vouchers:
latest_pcv = (
frappe.db.get_all(
"Period Closing Voucher",
filters={"company": self.company},
order_by="posting_date desc",
pluck="posting_date",
limit=1,
)
or None
)
if not latest_pcv:
return
for vtype in self._allowed_types:
if names := [x.voucher_no for x in self.vouchers if x.voucher_type == vtype]:
latest_voucher = frappe.db.get_all(
vtype,
filters={"name": ["in", names]},
pluck="posting_date",
order_by="posting_date desc",
limit=1,
)[0]
if latest_voucher and latest_pcv[0] >= latest_voucher:
frappe.throw(_("Cannot Resubmit Ledger entries for vouchers in Closed fiscal year."))
def validate_vouchers(self):
if self.vouchers:
# Validate voucher types
voucher_types = set([x.voucher_type for x in self.vouchers])
if disallowed_types := voucher_types.difference(self._allowed_types):
frappe.throw(
_("{0} types are not allowed. Only {1} are.").format(
frappe.bold(comma_and(list(disallowed_types))),
frappe.bold(comma_and(list(self._allowed_types))),
)
)
def get_existing_ledger_entries(self):
vouchers = [x.voucher_no for x in self.vouchers]
gl = qb.DocType("GL Entry")
existing_gles = (
qb.from_(gl)
.select(gl.star)
.where((gl.voucher_no.isin(vouchers)) & (gl.is_cancelled == 0))
.run(as_dict=True)
)
self.gles = frappe._dict({})
for gle in existing_gles:
self.gles.setdefault((gle.voucher_type, gle.voucher_no), frappe._dict({})).setdefault(
"existing", []
).append(gle.update({"old": True}))
def generate_preview_data(self):
self.gl_entries = []
self.get_existing_ledger_entries()
for x in self.vouchers:
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
if doc.doctype in ["Payment Entry", "Journal Entry"]:
gle_map = doc.build_gl_map()
else:
gle_map = doc.get_gl_entries()
old_entries = self.gles.get((x.voucher_type, x.voucher_no))
if old_entries:
self.gl_entries.extend(old_entries.existing)
self.gl_entries.extend(gle_map)
@frappe.whitelist()
def generate_preview(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
gl_columns = []
gl_data = []
self.generate_preview_data()
if self.gl_entries:
filters = {"company": self.company, "include_dimensions": 1}
for x in get_gl_columns(filters):
if x["fieldname"] == "gl_entry":
x["fieldname"] = "name"
gl_columns.append(x)
gl_data = self.gl_entries
rendered_page = frappe.render_template(
"erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html",
{"gl_columns": gl_columns, "gl_data": gl_data},
)
return rendered_page
def on_submit(self):
job_name = "repost_accounting_ledger_" + self.name
frappe.enqueue(
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
account_repost_doc=self.name,
is_async=True,
job_name=job_name,
)
frappe.msgprint(_("Repost has started in the background"))
@frappe.whitelist()
def start_repost(account_repost_doc=str) -> None:
if account_repost_doc:
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
if repost_doc.docstatus == 1:
# Prevent repost on invoices with deferred accounting
repost_doc.validate_for_deferred_accounting()
for x in repost_doc.vouchers:
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
if repost_doc.delete_cancelled_entries:
frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name})
frappe.db.delete(
"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
)
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
if not repost_doc.delete_cancelled_entries:
doc.docstatus = 2
doc.make_gl_entries_on_cancel()
doc.docstatus = 1
doc.make_gl_entries()
elif doc.doctype in ["Payment Entry", "Journal Entry"]:
if not repost_doc.delete_cancelled_entries:
doc.make_gl_entries(1)
doc.make_gl_entries()
frappe.db.commit()

View File

@@ -0,0 +1,202 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, nowdate, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
def teadDown(self):
frappe.db.rollback()
def test_01_basic_functions(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
)
preq = frappe.get_doc(
make_payment_request(
dt=si.doctype,
dn=si.name,
payment_request_type="Inward",
party_type="Customer",
party=si.customer,
)
)
preq.save().submit()
# Test Validation Error
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = True
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append(
"vouchers", {"voucher_type": preq.doctype, "voucher_no": preq.name}
) # this should throw validation error
self.assertRaises(frappe.ValidationError, ral.save)
ral.vouchers.pop()
preq.cancel()
preq.delete()
pe = get_payment_entry(si.doctype, si.name)
pe.save().submit()
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save()
# manually set an incorrect debit amount in DB
gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to})
frappe.db.set_value("GL Entry", gle[0], "debit", 90)
gl = qb.DocType("GL Entry")
res = (
qb.from_(gl)
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
.run()
)
# Assert incorrect ledger balance
self.assertNotEqual(res[0], (si.name, 100, 100))
# Submit repost document
ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
res = (
qb.from_(gl)
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
.run()
)
# Ledger should reflect correct amount post repost
self.assertEqual(res[0], (si.name, 100, 100))
def test_02_deferred_accounting_valiations(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
do_not_submit=True,
)
si.items[0].enable_deferred_revenue = True
si.items[0].deferred_revenue_account = self.deferred_revenue
si.items[0].service_start_date = nowdate()
si.items[0].service_end_date = add_days(nowdate(), 90)
si.save().submit()
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
self.assertRaises(frappe.ValidationError, ral.save)
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
def test_04_pcv_validation(self):
# Clear old GL entries so PCV can be submitted.
gl = frappe.qb.DocType("GL Entry")
qb.from_(gl).delete().where(gl.company == self.company).run()
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
)
pcv = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": today(),
"posting_date": today(),
"company": self.company,
"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
"cost_center": self.cost_center,
"closing_account_head": self.retained_earnings,
"remarks": "test",
}
)
pcv.save().submit()
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
self.assertRaises(frappe.ValidationError, ral.save)
pcv.reload()
pcv.cancel()
pcv.delete()
def test_03_deletion_flag_and_preview_function(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
)
pe = get_payment_entry(si.doctype, si.name)
pe.save().submit()
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save()
# assert preview data is generated
preview = ral.generate_preview()
self.assertIsNotNone(preview)
ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
# with deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = True
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit()
start_repost(ral.name)
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))

View File

@@ -0,0 +1,40 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-07-04 14:14:01.243848",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"voucher_type",
"voucher_no"
],
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Voucher No",
"options": "voucher_type"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-07-04 14:15:51.165584",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger Items",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class RepostAccountingLedgerItems(Document):
pass

View File

@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format

View File

@@ -714,6 +714,7 @@
"fieldtype": "Table",
"hide_days": 1,
"hide_seconds": 1,
"label": "Items",
"oldfieldname": "entries",
"oldfieldtype": "Table",
"options": "Sales Invoice Item",

View File

@@ -23,7 +23,7 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
)
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_disposal_account_and_cost_center,
@@ -399,6 +399,8 @@ class SalesInvoice(SellingController):
"Repost Item Valuation",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Payment Ledger Entry",
)
@@ -1046,7 +1048,10 @@ class SalesInvoice(SellingController):
merge_entries=False,
from_repost=from_repost,
)
self.make_exchange_gain_loss_journal()
elif self.docstatus == 2:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if update_outstanding == "No":
@@ -1071,7 +1076,6 @@ class SalesInvoice(SellingController):
self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.make_item_gl_entries(gl_entries)
@@ -1665,15 +1669,13 @@ class SalesInvoice(SellingController):
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
def get_returned_amount(self):
from frappe.query_builder.functions import Coalesce, Sum
from frappe.query_builder.functions import Sum
doc = frappe.qb.DocType(self.doctype)
returned_amount = (
frappe.qb.from_(doc)
.select(Sum(doc.grand_total))
.where(
(doc.docstatus == 1) & (doc.is_return == 1) & (Coalesce(doc.return_against, "") == self.name)
)
.where((doc.docstatus == 1) & (doc.is_return == 1) & (doc.return_against == self.name))
).run()
return abs(returned_amount[0][0]) if returned_amount[0][0] else 0

View File

@@ -15,9 +15,11 @@ def get_data():
},
"internal_links": {
"Sales Order": ["items", "sales_order"],
"Delivery Note": ["items", "delivery_note"],
"Timesheet": ["timesheets", "time_sheet"],
},
"internal_and_external_links": {
"Delivery Note": ["items", "delivery_note"],
},
"transactions": [
{
"label": _("Payment"),

View File

@@ -2322,7 +2322,7 @@ class TestSalesInvoice(unittest.TestCase):
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.item_defaults[0].deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
@@ -3102,7 +3102,7 @@ class TestSalesInvoice(unittest.TestCase):
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
item.deferred_revenue_account = deferred_account
item.item_defaults[0].deferred_revenue_account = deferred_account
item.save()
si = create_sales_invoice(
@@ -3213,17 +3213,10 @@ class TestSalesInvoice(unittest.TestCase):
account.disabled = 0
account.save()
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_gain_loss_with_advance_entry(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
unlink_enabled = frappe.db.get_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
)
frappe.db.set_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1
)
jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
jv.accounts[0].exchange_rate = 70
@@ -3256,17 +3249,28 @@ class TestSalesInvoice(unittest.TestCase):
)
si.save()
si.submit()
expected_gle = [
["_Test Receivable USD - _TC", 7500.0, 500],
["Exchange Gain/Loss - _TC", 500.0, 0.0],
["Sales - _TC", 0.0, 7500.0],
["_Test Receivable USD - _TC", 7500.0, 0.0, nowdate()],
["Sales - _TC", 0.0, 7500.0, nowdate()],
]
check_gl_entries(self, si.name, expected_gle, nowdate())
frappe.db.set_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
si.reload()
self.assertEqual(si.outstanding_amount, 0)
journals = frappe.db.get_all(
"Journal Entry Account",
filters={"reference_type": "Sales Invoice", "reference_name": si.name, "docstatus": 1},
pluck="parent",
)
journals = [x for x in journals if x != jv.name]
self.assertEqual(len(journals), 1)
je_type = frappe.get_cached_value("Journal Entry", journals[0], "voucher_type")
self.assertEqual(je_type, "Exchange Gain Or Loss")
ledger_outstanding = frappe.db.get_all(
"Payment Ledger Entry",
filters={"against_voucher_no": si.name, "delinked": 0},
fields=["sum(amount), sum(amount_in_account_currency)"],
as_list=1,
)
def test_batch_expiry_for_sales_invoice_return(self):
@@ -3316,6 +3320,7 @@ class TestSalesInvoice(unittest.TestCase):
)
self.assertRaises(frappe.ValidationError, si.submit)
@change_settings("Selling Settings", {"allow_negative_rates_for_items": 0})
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)

View File

@@ -694,3 +694,23 @@ class TestSubscription(unittest.TestCase):
# Check the currency of the created invoice
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency")
self.assertEqual(currency, "USD")
def test_plan_rate_for_midmonth_start_date(self):
subscription = frappe.new_doc("Subscription")
subscription.party_type = "Supplier"
subscription.party = "_Test Supplier"
subscription.generate_invoice_at_period_start = 1
subscription.follow_calendar_months = 1
subscription.generate_new_invoices_past_due_date = 1
subscription.start_date = "2023-04-08"
subscription.end_date = "2024-02-27"
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
subscription.save()
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
pi = frappe.get_doc("Purchase Invoice", subscription.invoices[0].invoice)
self.assertEqual(pi.total, 55333.33)
subscription.delete()

View File

@@ -57,18 +57,17 @@ def get_plan_rate(
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
if prorate:
prorate_factor = flt(
date_diff(start_date, get_first_day(start_date))
/ date_diff(get_last_day(start_date), get_first_day(start_date)),
1,
)
prorate_factor += flt(
date_diff(get_last_day(end_date), end_date)
/ date_diff(get_last_day(end_date), get_first_day(end_date)),
1,
)
cost -= plan.cost * prorate_factor
cost -= plan.cost * get_prorate_factor(start_date, end_date)
return cost
def get_prorate_factor(start_date, end_date):
total_days_to_skip = date_diff(start_date, get_first_day(start_date))
total_days_in_month = int(get_last_day(start_date).strftime("%d"))
prorate_factor = flt(total_days_to_skip / total_days_in_month)
total_days_to_skip = date_diff(get_last_day(end_date), end_date)
total_days_in_month = int(get_last_day(end_date).strftime("%d"))
prorate_factor += flt(total_days_to_skip / total_days_in_month)
return prorate_factor

View File

@@ -262,14 +262,20 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
if tax_deducted:
net_total = inv.tax_withholding_net_total
if ldc:
tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total)
limit_consumed = get_limit_consumed(ldc, parties)
if is_valid_certificate(ldc, posting_date, limit_consumed):
tax_amount = get_lower_deduction_amount(
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
)
else:
tax_amount = net_total * tax_details.rate / 100
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
tax_amount = net_total * tax_details.rate / 100
# once tds is deducted, not need to add vouchers in the invoice
voucher_wise_amount = {}
else:
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
elif party_type == "Customer":
if tax_deducted:
@@ -416,7 +422,7 @@ def get_deducted_tax(taxable_vouchers, tax_details):
return sum(entries)
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
tds_amount = 0
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
@@ -496,15 +502,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
net_total += inv.tax_withholding_net_total
supp_credit_amt = net_total - cumulative_threshold
if ldc and is_valid_certificate(
ldc.valid_from,
ldc.valid_upto,
inv.get("posting_date") or inv.get("transaction_date"),
tax_deducted,
inv.tax_withholding_net_total,
ldc.certificate_limit,
):
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
tds_amount = get_lower_deduction_amount(
supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details
)
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
@@ -582,8 +583,7 @@ def get_invoice_total_without_tcs(inv, tax_details):
return inv.grand_total - tcs_tax_row_amount
def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
tds_amount = 0
def get_limit_consumed(ldc, parties):
limit_consumed = frappe.db.get_value(
"Purchase Invoice",
{
@@ -597,37 +597,29 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
"sum(tax_withholding_net_total)",
)
if is_valid_certificate(
ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, ldc.certificate_limit
):
tds_amount = get_ltds_amount(
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
)
return tds_amount
return limit_consumed
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
def get_lower_deduction_amount(
current_amount, limit_consumed, certificate_limit, rate, tax_details
):
if certificate_limit - flt(limit_consumed) - flt(current_amount) >= 0:
return current_amount * rate / 100
else:
ltds_amount = certificate_limit - flt(deducted_amount)
ltds_amount = certificate_limit - flt(limit_consumed)
tds_amount = current_amount - ltds_amount
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
def is_valid_certificate(
valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit
):
valid = False
def is_valid_certificate(ldc, posting_date, limit_consumed):
available_amount = flt(ldc.certificate_limit) - flt(limit_consumed)
if (
getdate(ldc.valid_from) <= getdate(posting_date) <= getdate(ldc.valid_upto)
) and available_amount > 0:
return True
available_amount = flt(certificate_limit) - flt(deducted_amount)
if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
valid = True
return valid
return False
def normal_round(number):

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.tests.utils import change_settings
from frappe.utils import today
@@ -18,6 +19,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
# create relevant supplier, etc
create_records()
create_tax_withholding_category_records()
make_pan_no_field()
def tearDown(self):
cancel_invoices()
@@ -456,6 +458,40 @@ class TestTaxWithholdingCategory(unittest.TestCase):
pe2.cancel()
pe3.cancel()
def test_lower_deduction_certificate_application(self):
frappe.db.set_value(
"Supplier",
"Test LDC Supplier",
{
"tax_withholding_category": "Test Service Category",
"pan": "ABCTY1234D",
},
)
create_lower_deduction_certificate(
supplier="Test LDC Supplier",
certificate_no="1AE0423AAJ",
tax_withholding_category="Test Service Category",
tax_rate=2,
limit=50000,
)
pi1 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
pi1.submit()
self.assertEqual(pi1.taxes[0].tax_amount, 700)
pi2 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
pi2.submit()
self.assertEqual(pi2.taxes[0].tax_amount, 2300)
pi3 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
pi3.submit()
self.assertEqual(pi3.taxes[0].tax_amount, 3500)
pi1.cancel()
pi2.cancel()
pi3.cancel()
def cancel_invoices():
purchase_invoices = frappe.get_all(
@@ -615,6 +651,7 @@ def create_records():
"Test TDS Supplier6",
"Test TDS Supplier7",
"Test TDS Supplier8",
"Test LDC Supplier",
]:
if frappe.db.exists("Supplier", name):
continue
@@ -811,3 +848,39 @@ def create_tax_withholding_category(
"accounts": [{"company": "_Test Company", "account": account}],
}
).insert()
def create_lower_deduction_certificate(
supplier, tax_withholding_category, tax_rate, certificate_no, limit
):
fiscal_year = get_fiscal_year(today(), company="_Test Company")
if not frappe.db.exists("Lower Deduction Certificate", certificate_no):
frappe.get_doc(
{
"doctype": "Lower Deduction Certificate",
"company": "_Test Company",
"supplier": supplier,
"certificate_no": certificate_no,
"tax_withholding_category": tax_withholding_category,
"fiscal_year": fiscal_year[0],
"valid_from": fiscal_year[1],
"valid_upto": fiscal_year[2],
"rate": tax_rate,
"certificate_limit": limit,
}
).insert()
def make_pan_no_field():
pan_field = {
"Supplier": [
{
"fieldname": "pan",
"label": "PAN",
"fieldtype": "Data",
"translatable": 0,
}
]
}
create_custom_fields(pan_field, update=1)

View File

@@ -28,6 +28,7 @@ def make_gl_entries(
):
if gl_map:
if not cancel:
make_acc_dimensions_offsetting_entry(gl_map)
validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
@@ -51,6 +52,63 @@ def make_gl_entries(
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
def make_acc_dimensions_offsetting_entry(gl_map):
accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(
gl_map, gl_map[0].company
)
no_of_dimensions = len(accounting_dimensions_to_offset)
if no_of_dimensions == 0:
return
offsetting_entries = []
for gle in gl_map:
for dimension in accounting_dimensions_to_offset:
offsetting_entry = gle.copy()
debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0
credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0
offsetting_entry.update(
{
"account": dimension.offsetting_account,
"debit": debit,
"credit": credit,
"debit_in_account_currency": debit,
"credit_in_account_currency": credit,
"remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name),
"against_voucher": None,
}
)
offsetting_entry["against_voucher_type"] = None
offsetting_entries.append(offsetting_entry)
gl_map += offsetting_entries
def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
acc_dimension = frappe.qb.DocType("Accounting Dimension")
dimension_detail = frappe.qb.DocType("Accounting Dimension Detail")
acc_dimensions = (
frappe.qb.from_(acc_dimension)
.inner_join(dimension_detail)
.on(acc_dimension.name == dimension_detail.parent)
.select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account)
.where(
(acc_dimension.disabled == 0)
& (dimension_detail.company == company)
& (dimension_detail.automatically_post_balancing_accounting_entry == 1)
)
).run(as_dict=True)
accounting_dimensions_to_offset = []
for acc_dimension in acc_dimensions:
values = set([entry.get(acc_dimension.fieldname) for entry in gl_map])
if len(values) > 1:
accounting_dimensions_to_offset.append(acc_dimension)
return accounting_dimensions_to_offset
def validate_disabled_accounts(gl_map):
accounts = [d.account for d in gl_map if d.account]

View File

@@ -14,7 +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.query_builder.functions import Abs, Date, Sum
from frappe.utils import (
add_days,
add_months,
@@ -884,35 +884,34 @@ 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, account_type=None
party_type, posting_date=None, future_payment=0, company=None, party=None
):
gle = frappe.qb.DocType("GL Entry")
ple = frappe.qb.DocType("Payment Ledger Entry")
query = (
frappe.qb.from_(gle)
.select(gle.party)
frappe.qb.from_(ple)
.select(ple.party, Abs(Sum(ple.amount).as_("amount")))
.where(
(gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
(ple.party_type.isin(party_type))
& (ple.amount < 0)
& (ple.against_voucher_no == ple.voucher_no)
& (ple.delinked == 0)
)
.groupby(gle.party)
.groupby(ple.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:
query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date))
else:
query = query.where(gle.posting_date <= posting_date)
query = query.where(ple.posting_date <= posting_date)
if company:
query = query.where(gle.company == company)
query = query.where(ple.company == company)
if party:
query = query.where(gle.party == party)
query = query.where(ple.party == party)
data = query.run(as_dict=True)
data = query.run()
if data:
return frappe._dict(data)

View File

@@ -37,24 +37,6 @@ frappe.query_reports["Accounts Payable"] = {
}
}
},
{
"fieldname": "supplier",
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier",
on_change: () => {
var supplier = frappe.query_report.get_filter_value('supplier');
if (supplier) {
frappe.db.get_value('Supplier', supplier, "tax_id", function(value) {
frappe.query_report.set_filter_value('tax_id', value["tax_id"]);
});
} else {
frappe.query_report.set_filter_value('tax_id', "");
}
frappe.query_report.refresh();
}
},
{
"fieldname": "party_account",
"label": __("Payable Account"),
@@ -112,11 +94,38 @@ frappe.query_reports["Accounts Payable"] = {
"fieldtype": "Link",
"options": "Payment Terms Template"
},
{
"fieldname": "party_type",
"label": __("Party Type"),
"fieldtype": "Link",
"options": "Party Type",
get_query: () => {
return {
filters: {
'account_type': 'Payable'
}
};
},
on_change: () => {
frappe.query_report.set_filter_value('party', "");
let party_type = frappe.query_report.get_filter_value('party_type');
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "Dynamic Link",
"options": "party_type",
},
{
"fieldname": "supplier_group",
"label": __("Supplier Group"),
"fieldtype": "Link",
"options": "Supplier Group"
"options": "Supplier Group",
"hidden": 1
},
{
"fieldname": "group_by_party",
@@ -133,12 +142,6 @@ frappe.query_reports["Accounts Payable"] = {
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "tax_id",
"label": __("Tax Id"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname": "show_future_payments",
"label": __("Show Future Payments"),

View File

@@ -0,0 +1,67 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, today
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
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
from erpnext.accounts.report.accounts_payable.accounts_payable import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.create_supplier(currency="USD", supplier_name="Test Supplier2")
self.create_usd_payable_account()
def tearDown(self):
frappe.db.rollback()
def test_accounts_receivable_with_supplier(self):
pi = self.create_purchase_invoice(do_not_submit=True)
pi.currency = "USD"
pi.conversion_rate = 80
pi.credit_to = self.creditors_usd
pi = pi.save().submit()
filters = {
"company": self.company,
"party_type": "Supplier",
"party": self.supplier,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
data = execute(filters)
self.assertEqual(data[1][0].get("outstanding"), 300)
self.assertEqual(data[1][0].get("currency"), "USD")
def create_purchase_invoice(self, do_not_submit=False):
frappe.set_user("Administrator")
pi = make_purchase_invoice(
item=self.item,
company=self.company,
supplier=self.supplier,
is_return=False,
update_stock=False,
posting_date=frappe.utils.datetime.date(2021, 5, 1),
do_not_save=1,
rate=300,
price_list_rate=300,
qty=1,
)
pi = pi.save()
if not do_not_submit:
pi = pi.submit()
return pi

View File

@@ -46,8 +46,7 @@ frappe.query_reports["Accounts Receivable"] = {
var customer = frappe.query_report.get_filter_value('customer');
var company = frappe.query_report.get_filter_value('company');
if (customer) {
frappe.db.get_value('Customer', customer, ["tax_id", "customer_name", "payment_terms"], function(value) {
frappe.query_report.set_filter_value('tax_id', value["tax_id"]);
frappe.db.get_value('Customer', customer, ["customer_name", "payment_terms"], function(value) {
frappe.query_report.set_filter_value('customer_name', value["customer_name"]);
frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]);
});
@@ -59,7 +58,6 @@ frappe.query_reports["Accounts Receivable"] = {
}
}, "Customer");
} else {
frappe.query_report.set_filter_value('tax_id', "");
frappe.query_report.set_filter_value('customer_name', "");
frappe.query_report.set_filter_value('credit_limit', "");
frappe.query_report.set_filter_value('payment_terms', "");
@@ -172,12 +170,6 @@ frappe.query_reports["Accounts Receivable"] = {
"label": __("Show Sales Person"),
"fieldtype": "Check",
},
{
"fieldname": "tax_id",
"label": __("Tax Id"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname": "show_remarks",
"label": __("Show Remarks"),

View File

@@ -211,9 +211,8 @@ class ReceivablePayableReport(object):
return
# amount in "Party Currency", if its supplied. If not, amount in company currency
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
amount = ple.amount_in_account_currency
if self.filters.get("party_type") and self.filters.get("party"):
amount = ple.amount_in_account_currency
else:
amount = ple.amount
amount_in_account_currency = ple.amount_in_account_currency
@@ -426,22 +425,20 @@ class ReceivablePayableReport(object):
# customer / supplier name
party_details = self.get_party_details(row.party) or {}
row.update(party_details)
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
row.currency = row.account_currency
break
if self.filters.get("party_type") and self.filters.get("party"):
row.currency = row.account_currency
else:
row.currency = self.company_currency
def allocate_outstanding_based_on_payment_terms(self, row):
self.get_payment_terms(row)
for term in row.payment_terms:
# update "paid" and "oustanding" for this term
# update "paid" and "outstanding" for this term
if not term.paid:
self.allocate_closing_to_term(row, term, "paid")
# update "credit_note" and "oustanding" for this term
# update "credit_note" and "outstanding" for this term
if term.outstanding:
self.allocate_closing_to_term(row, term, "credit_note")
@@ -453,7 +450,8 @@ class ReceivablePayableReport(object):
"""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
ps.description, ps.paid_amount, ps.discounted_amount
from `tab{0}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
@@ -469,6 +467,14 @@ class ReceivablePayableReport(object):
original_row = frappe._dict(row)
row.payment_terms = []
# Cr Note's don't have Payment Terms
if not payment_terms_details:
return
# Advance allocated during invoicing is not considered in payment terms
# Deduct that from paid amount pre allocation
row.paid -= flt(payment_terms_details[0].total_advance)
# If no or single payment terms, no need to split the row
if len(payment_terms_details) <= 1:
return
@@ -483,7 +489,7 @@ class ReceivablePayableReport(object):
) and d.currency == d.party_account_currency:
invoiced = d.payment_amount
else:
invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision)
invoiced = d.base_payment_amount
row.payment_terms.append(
term.update(
@@ -761,6 +767,7 @@ class ReceivablePayableReport(object):
def prepare_conditions(self):
self.qb_selection_filter = []
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)
@@ -796,6 +803,12 @@ class ReceivablePayableReport(object):
if self.filters.get(party_type_field):
self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
if self.filters.get("party_type"):
self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type)
if self.filters.get("party"):
self.qb_selection_filter.append(self.filters.party == self.ple.party)
if self.filters.party_account:
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
else:
@@ -1086,7 +1099,10 @@ class ReceivablePayableReport(object):
.where(
(je.company == self.filters.company)
& (je.posting_date.lte(self.filters.report_date))
& (je.voucher_type == "Exchange Rate Revaluation")
& (
(je.voucher_type == "Exchange Rate Revaluation")
| (je.voucher_type == "Exchange Gain Or Loss")
)
)
.run()
)

View File

@@ -8,20 +8,17 @@ from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestAccountsReceivable(FrappeTestCase):
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def setUp(self):
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
self.create_usd_account()
self.create_company()
self.create_customer()
self.create_item()
self.create_usd_receivable_account()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
@@ -49,29 +46,84 @@ class TestAccountsReceivable(FrappeTestCase):
debtors_usd.account_type = debtors.account_type
self.debtors_usd = debtors_usd.save().name
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_save=1,
)
if not no_payment_schedule:
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
)
si = si.save()
if not do_not_submit:
si = si.submit()
return si
def create_payment_entry(self, docname):
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
pe.paid_from = self.debit_to
pe.insert()
pe.submit()
def create_credit_note(self, docname):
credit_note = create_sales_invoice(
company=self.company,
customer=self.customer,
item=self.item,
qty=-1,
debit_to=self.debit_to,
cost_center=self.cost_center,
is_return=1,
return_against=docname,
)
return credit_note
def test_accounts_receivable(self):
filters = {
"company": "_Test Company 2",
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": True,
}
# check invoice grand total and invoiced column's value for 3 payment terms
name = make_sales_invoice().name
si = self.create_sales_invoice()
name = si.name
report = execute(filters)
expected_data = [[100, 30], [100, 50], [100, 20]]
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
for i in range(3):
row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
# check invoice grand total, invoiced, paid and outstanding column's value after payment
make_payment(name)
self.create_payment_entry(si.name)
report = execute(filters)
expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]]
@@ -84,10 +136,10 @@ class TestAccountsReceivable(FrappeTestCase):
)
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
make_credit_note(name)
self.create_credit_note(si.name)
report = execute(filters)
expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"]
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
row = report[1][0]
self.assertEqual(
@@ -108,21 +160,20 @@ class TestAccountsReceivable(FrappeTestCase):
"""
so = make_sales_order(
company="_Test Company 2",
customer="_Test Customer 2",
warehouse="Finished Goods - _TC2",
currency="EUR",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
company=self.company,
customer=self.customer,
warehouse=self.warehouse,
debit_to=self.debit_to,
income_account=self.income_account,
expense_account=self.expense_account,
cost_center=self.cost_center,
)
pe = get_payment_entry(so.doctype, so.name)
pe = pe.save().submit()
filters = {
"company": "_Test Company 2",
"company": self.company,
"based_on_payment_terms": 0,
"report_date": today(),
"range1": 30,
@@ -147,34 +198,32 @@ class TestAccountsReceivable(FrappeTestCase):
)
@change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_exchange_revaluation_for_party(self):
"""
Exchange Revaluation for party on Receivable/Payable shoule be included
Exchange Revaluation for party on Receivable/Payable should be included
"""
company = "_Test Company 2"
customer = "_Test Customer 2"
# Using Exchange Gain/Loss account for unrealized as well.
company_doc = frappe.get_doc("Company", company)
company_doc = frappe.get_doc("Company", self.company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
company_doc.save()
si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
si.conversion_rate = 0.90
si.conversion_rate = 80
si.debit_to = self.debtors_usd
si = si.save().submit()
# Exchange Revaluation
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = company
err.company = self.company
err.posting_date = today()
accounts = err.get_accounts_data()
err.extend("accounts", accounts)
err.accounts[0].new_exchange_rate = 0.95
err.accounts[0].new_exchange_rate = 85
row = err.accounts[0]
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
@@ -189,7 +238,7 @@ class TestAccountsReceivable(FrappeTestCase):
je = je.submit()
filters = {
"company": company,
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
@@ -198,7 +247,7 @@ class TestAccountsReceivable(FrappeTestCase):
}
report = execute(filters)
expected_data_for_err = [0, -5, 0, 5]
expected_data_for_err = [0, -500, 0, 500]
row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
self.assertEqual(
expected_data_for_err,
@@ -214,46 +263,43 @@ class TestAccountsReceivable(FrappeTestCase):
"""
Payment against credit/debit note should be considered against the parent invoice
"""
company = "_Test Company 2"
customer = "_Test Customer 2"
si1 = make_sales_invoice()
si1 = self.create_sales_invoice()
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
pe.paid_from = "Debtors - _TC2"
pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash)
pe.paid_from = self.debit_to
pe.insert()
pe.submit()
cr_note = make_credit_note(si1.name)
cr_note = self.create_credit_note(si1.name)
si2 = make_sales_invoice()
si2 = self.create_sales_invoice()
# manually link cr_note with si2 using journal entry
je = frappe.new_doc("Journal Entry")
je.company = company
je.company = self.company
je.voucher_type = "Credit Note"
je.posting_date = today()
debit_account = "Debtors - _TC2"
debit_entry = {
"account": debit_account,
"account": self.debit_to,
"party_type": "Customer",
"party": customer,
"party": self.customer,
"debit": 100,
"debit_in_account_currency": 100,
"reference_type": cr_note.doctype,
"reference_name": cr_note.name,
"cost_center": "Main - _TC2",
"cost_center": self.cost_center,
}
credit_entry = {
"account": debit_account,
"account": self.debit_to,
"party_type": "Customer",
"party": customer,
"party": self.customer,
"credit": 100,
"credit_in_account_currency": 100,
"reference_type": si2.doctype,
"reference_name": si2.name,
"cost_center": "Main - _TC2",
"cost_center": self.cost_center,
}
je.append("accounts", debit_entry)
@@ -261,7 +307,7 @@ class TestAccountsReceivable(FrappeTestCase):
je = je.save().submit()
filters = {
"company": company,
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
@@ -271,64 +317,254 @@ class TestAccountsReceivable(FrappeTestCase):
report = execute(filters)
self.assertEqual(report[1], [])
def test_group_by_party(self):
si1 = self.create_sales_invoice(do_not_submit=True)
si1.posting_date = add_days(today(), -1)
si1.save().submit()
si2 = self.create_sales_invoice(do_not_submit=True)
si2.items[0].rate = 85
si2.save().submit()
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"group_by_party": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 5)
si = create_sales_invoice(
company="_Test Company 2",
customer="_Test Customer 2",
currency="EUR",
warehouse="Finished Goods - _TC2",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
do_not_save=1,
)
# assert voucher rows
expected_voucher_rows = [
[100.0, 100.0, 100.0, 100.0],
[85.0, 85.0, 85.0, 85.0],
]
voucher_rows = []
for x in report[0:2]:
voucher_rows.append(
[x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency]
)
self.assertEqual(expected_voucher_rows, voucher_rows)
if not no_payment_schedule:
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
# assert total rows
expected_total_rows = [
[self.customer, 185.0, 185.0], # party total
{}, # empty row for padding
["Total", 185.0, 185.0], # grand total
]
party_total_row = report[2]
self.assertEqual(
expected_total_rows[0],
[
party_total_row.get("party"),
party_total_row.get("invoiced"),
party_total_row.get("outstanding"),
],
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
empty_row = report[3]
self.assertEqual(expected_total_rows[1], empty_row)
grand_total_row = report[4]
self.assertEqual(
expected_total_rows[2],
[
grand_total_row.get("party"),
grand_total_row.get("invoiced"),
grand_total_row.get("outstanding"),
],
)
si = si.save()
def test_future_payments(self):
si = self.create_sales_invoice()
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.paid_amount = 90.0
pe.references[0].allocated_amount = 90.0
pe.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_future_payments": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
if not do_not_submit:
si = si.submit()
expected_data = [100.0, 100.0, 10.0, 90.0]
return si
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
pe.cancel()
# full payment in future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.save().submit()
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, 0.0, 100.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
def make_payment(docname):
pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40)
pe.paid_from = "Debtors - _TC2"
pe.insert()
pe.submit()
pe.cancel()
# over payment in future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.paid_amount = 110
pe.save().submit()
report = execute(filters)[1]
self.assertEqual(len(report), 2)
expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]]
for idx, row in enumerate(report):
self.assertEqual(
expected_data[idx],
[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
)
def test_sales_person(self):
sales_person = (
frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True})
.insert()
.submit()
)
si = self.create_sales_invoice(do_not_submit=True)
si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
si.save().submit()
def make_credit_note(docname):
credit_note = create_sales_invoice(
company="_Test Company 2",
customer="_Test Customer 2",
currency="EUR",
qty=-1,
warehouse="Finished Goods - _TC2",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
is_return=1,
return_against=docname,
)
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"sales_person": sales_person.name,
"show_sales_person": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
return credit_note
expected_data = [100.0, 100.0, sales_person.name]
row = report[0]
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person])
def test_cost_center_filter(self):
si = self.create_sales_invoice()
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"cost_center": self.cost_center,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, self.cost_center]
row = report[0]
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center])
def test_customer_group_filter(self):
si = self.create_sales_invoice()
cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"customer_group": cus_group,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, cus_group]
row = report[0]
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group])
filters.update({"customer_group": "Individual"})
report = execute(filters)[1]
self.assertEqual(len(report), 0)
def test_party_account_filter(self):
si1 = self.create_sales_invoice()
self.customer2 = (
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
)
.insert()
.submit()
)
si2 = self.create_sales_invoice(do_not_submit=True)
si2.posting_date = add_days(today(), -1)
si2.customer = self.customer2
si2.currency = "USD"
si2.conversion_rate = 80
si2.debit_to = self.debtors_usd
si2.save().submit()
# Filter on company currency receivable account
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"party_account": self.debit_to,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, self.debit_to, si1.currency]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
)
# Filter on USD receivable account
filters.update({"party_account": self.debtors_usd})
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
)
# without filter on party account
filters.pop("party_account")
report = execute(filters)[1]
self.assertEqual(len(report), 2)
expected_data = [
[8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency],
[100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency],
]
for idx, row in enumerate(report):
self.assertEqual(
expected_data[idx],
[
row.invoiced,
row.outstanding,
row.invoiced_in_account_currency,
row.outstanding_in_account_currency,
row.party_account,
row.account_currency,
],
)

View File

@@ -50,13 +50,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.filters.show_future_payments,
self.filters.company,
party=party,
account_type=self.account_type,
)
or {}
)
if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date)
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
for party, party_dict in self.party_total.items():
if party_dict.outstanding == 0:
@@ -233,12 +232,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(label="Total Amount Due", fieldname="total_due")
def get_gl_balance(report_date):
def get_gl_balance(report_date, company):
return frappe._dict(
frappe.db.get_all(
"GL Entry",
fields=["party", "sum(debit - credit)"],
filters={"posting_date": ("<=", report_date), "is_cancelled": 0},
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party",
as_list=1,
)

View File

@@ -0,0 +1,203 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.maxDiff = None
self.create_company()
self.create_customer()
self.create_item()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
def test_01_receivable_summary_output(self):
"""
Test for Invoices, Paid, Advance and Outstanding
"""
filters = {
"company": self.company,
"customer": self.customer,
"posting_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=200,
price_list_rate=200,
)
customer_group, customer_territory = frappe.db.get_all(
"Customer",
filters={"name": self.customer},
fields=["customer_group", "territory"],
as_list=True,
)[0]
report = execute(filters)
rpt_output = report[1]
expected_data = {
"party_type": "Customer",
"advance": 0,
"party": self.customer,
"invoiced": 200.0,
"paid": 0.0,
"credit_note": 0.0,
"outstanding": 200.0,
"range1": 200.0,
"range2": 0.0,
"range3": 0.0,
"range4": 0.0,
"range5": 0.0,
"total_due": 200.0,
"future_amount": 0.0,
"sales_person": [],
"currency": si.currency,
"territory": customer_territory,
"customer_group": customer_group,
}
self.assertEqual(len(rpt_output), 1)
self.assertDictEqual(rpt_output[0], expected_data)
# simulate advance payment
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = 50
pe.references[0].allocated_amount = 0 # this essitially removes the reference
pe.save().submit()
# update expected data with advance
expected_data.update(
{
"advance": 50.0,
"outstanding": 150.0,
"range1": 150.0,
"total_due": 150.0,
}
)
report = execute(filters)
rpt_output = report[1]
self.assertEqual(len(rpt_output), 1)
self.assertDictEqual(rpt_output[0], expected_data)
# make partial payment
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = 125
pe.references[0].allocated_amount = 125
pe.save().submit()
# update expected data after advance and partial payment
expected_data.update(
{"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0}
)
report = execute(filters)
rpt_output = report[1]
self.assertEqual(len(rpt_output), 1)
self.assertDictEqual(rpt_output[0], expected_data)
@change_settings("Selling Settings", {"cust_master_name": "Naming Series"})
def test_02_various_filters_and_output(self):
filters = {
"company": self.company,
"customer": self.customer,
"posting_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=200,
price_list_rate=200,
)
# make partial payment
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = 150
pe.references[0].allocated_amount = 150
pe.save().submit()
customer_group, customer_territory = frappe.db.get_all(
"Customer",
filters={"name": self.customer},
fields=["customer_group", "territory"],
as_list=True,
)[0]
report = execute(filters)
rpt_output = report[1]
expected_data = {
"party_type": "Customer",
"advance": 0,
"party": self.customer,
"party_name": self.customer,
"invoiced": 200.0,
"paid": 150.0,
"credit_note": 0.0,
"outstanding": 50.0,
"range1": 50.0,
"range2": 0.0,
"range3": 0.0,
"range4": 0.0,
"range5": 0.0,
"total_due": 50.0,
"future_amount": 0.0,
"sales_person": [],
"currency": si.currency,
"territory": customer_territory,
"customer_group": customer_group,
}
self.assertEqual(len(rpt_output), 1)
self.assertDictEqual(rpt_output[0], expected_data)
# with gl balance filter
filters.update({"show_gl_balance": True})
expected_data.update({"gl_balance": 50.0, "diff": 0.0})
report = execute(filters)
rpt_output = report[1]
self.assertEqual(len(rpt_output), 1)
self.assertDictEqual(rpt_output[0], expected_data)
# with gl balance and future payments filter
filters.update({"show_future_payments": True})
expected_data.update({"remaining_balance": 50.0})
report = execute(filters)
rpt_output = report[1]
self.assertEqual(len(rpt_output), 1)
self.assertDictEqual(rpt_output[0], expected_data)
# invoice fully paid
pe = get_payment_entry(si.doctype, si.name).save().submit()
report = execute(filters)
rpt_output = report[1]
self.assertEqual(len(rpt_output), 0)

View File

@@ -58,6 +58,9 @@ def get_data(filters):
def get_asset_categories(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
return frappe.db.sql(
"""
SELECT asset_category,
@@ -98,15 +101,25 @@ def get_asset_categories(filters):
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
group by asset_category
""",
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
""".format(
condition
),
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"asset_category": filters.get("asset_category"),
},
as_dict=1,
)
def get_assets(filters):
condition = ""
if filters.get("asset_category"):
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
return frappe.db.sql(
"""
SELECT results.asset_category,
@@ -138,7 +151,7 @@ def get_assets(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
group by a.asset_category
union
SELECT a.asset_category,
@@ -154,10 +167,12 @@ def get_assets(filters):
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
group by a.asset_category) as results
group by results.asset_category
""",
""".format(
condition
),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
as_dict=1,
)

View File

@@ -749,13 +749,18 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
if from_date:
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")
finance_books = []
finance_books.append("")
if filter_fb := filters.get("finance_book"):
finance_books.append(filter_fb)
if filters.get("include_default_book_entries"):
additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
if company_fb := frappe.get_cached_value("Company", d.name, "default_finance_book"):
finance_books.append(company_fb)
additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
else:
additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
return additional_conditions

View File

@@ -81,7 +81,7 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
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 = self.deferred_revenue_account
item.item_defaults[0].deferred_revenue_account = self.deferred_revenue_account
item.no_of_months = 3
item.save()
@@ -150,7 +150,7 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
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 = self.deferred_expense_account
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
item.no_of_months_exp = 3
item.save()

View File

@@ -335,12 +335,10 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
for period in period_list:
total_row.setdefault(period.key, 0.0)
total_row[period.key] += row.get(period.key, 0.0)
row[period.key] = row.get(period.key, 0.0)
total_row.setdefault("total", 0.0)
total_row["total"] += flt(row["total"])
total_row["opening_balance"] += row["opening_balance"]
row["total"] = ""
if "total" in total_row:
out.append(total_row)
@@ -639,7 +637,13 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None):
if periodicity != "Yearly":
if not accumulated_values:
columns.append(
{"fieldname": "total", "label": _("Total"), "fieldtype": "Currency", "width": 150}
{
"fieldname": "total",
"label": _("Total"),
"fieldtype": "Currency",
"width": 150,
"options": "currency",
}
)
return columns

View File

@@ -272,20 +272,19 @@ def get_conditions(filters):
if match_conditions:
conditions.append(match_conditions)
if filters.get("include_dimensions"):
accounting_dimensions = get_accounting_dimensions(as_list=False)
accounting_dimensions = get_accounting_dimensions(as_list=False)
if accounting_dimensions:
for dimension in accounting_dimensions:
if not dimension.disabled:
if filters.get(dimension.fieldname):
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname)
)
conditions.append("{0} in %({0})s".format(dimension.fieldname))
else:
conditions.append("{0} in %({0})s".format(dimension.fieldname))
if accounting_dimensions:
for dimension in accounting_dimensions:
if not dimension.disabled:
if filters.get(dimension.fieldname):
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname)
)
conditions.append("{0} in %({0})s".format(dimension.fieldname))
else:
conditions.append("{0} in %({0})s".format(dimension.fieldname))
return "and {}".format(" and ".join(conditions)) if conditions else ""

View File

@@ -287,7 +287,7 @@ def get_conditions(filters):
conditions = ""
for opts in (
("company", " and company=%(company)s"),
("company", " and `tabPurchase Invoice`.company=%(company)s"),
("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"),
("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"),
("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"),

View File

@@ -332,7 +332,7 @@ def get_conditions(filters, additional_conditions=None):
conditions = ""
for opts in (
("company", " and company=%(company)s"),
("company", " and `tabSales Invoice`.company=%(company)s"),
("customer", " and `tabSales Invoice`.customer = %(customer)s"),
("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"),
("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"),

View File

@@ -12,17 +12,35 @@ frappe.query_reports["TDS Computation Summary"] = {
"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;
},
"get_query": function() {
return {
"filters": {
"tax_withholding_category": ["!=",""],
}
}
}
},
},
{
"fieldname":"from_date",

View File

@@ -9,9 +9,14 @@ from erpnext.accounts.utils import get_fiscal_year
def execute(filters=None):
validate_filters(filters)
if filters.get("party_type") == "Customer":
party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name")
else:
party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name")
filters.update({"naming_series": party_naming_by})
validate_filters(filters)
columns = get_columns(filters)
(
@@ -25,7 +30,7 @@ def execute(filters=None):
res = get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_total_map
)
final_result = group_by_supplier_and_category(res)
final_result = group_by_party_and_category(res, filters)
return columns, final_result
@@ -43,60 +48,67 @@ def validate_filters(filters):
filters["fiscal_year"] = from_year
def group_by_supplier_and_category(data):
supplier_category_wise_map = {}
def group_by_party_and_category(data, filters):
party_category_wise_map = {}
for row in data:
supplier_category_wise_map.setdefault(
(row.get("supplier"), row.get("section_code")),
party_category_wise_map.setdefault(
(row.get("party"), row.get("section_code")),
{
"pan": row.get("pan"),
"supplier": row.get("supplier"),
"supplier_name": row.get("supplier_name"),
"tax_id": row.get("tax_id"),
"party": row.get("party"),
"party_name": row.get("party_name"),
"section_code": row.get("section_code"),
"entity_type": row.get("entity_type"),
"tds_rate": row.get("tds_rate"),
"total_amount_credited": 0.0,
"tds_deducted": 0.0,
"rate": row.get("rate"),
"total_amount": 0.0,
"tax_amount": 0.0,
},
)
supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[
"total_amount_credited"
] += row.get("total_amount_credited", 0.0)
party_category_wise_map.get((row.get("party"), row.get("section_code")))[
"total_amount"
] += row.get("total_amount", 0.0)
supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[
"tds_deducted"
] += row.get("tds_deducted", 0.0)
party_category_wise_map.get((row.get("party"), row.get("section_code")))[
"tax_amount"
] += row.get("tax_amount", 0.0)
final_result = get_final_result(supplier_category_wise_map)
final_result = get_final_result(party_category_wise_map)
return final_result
def get_final_result(supplier_category_wise_map):
def get_final_result(party_category_wise_map):
out = []
for key, value in supplier_category_wise_map.items():
for key, value in party_category_wise_map.items():
out.append(value)
return out
def get_columns(filters):
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
{"label": _("PAN"), "fieldname": "pan", "fieldtype": "Data", "width": 90},
{"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": _(filters.party_type + " Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
}
)
columns.extend(
@@ -109,18 +121,23 @@ def get_columns(filters):
"width": 180,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
{"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90},
{
"label": _("Total Amount Credited"),
"fieldname": "total_amount_credited",
"fieldtype": "Float",
"width": 90,
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 120,
},
{
"label": _("Amount of TDS Deducted"),
"fieldname": "tds_deducted",
"label": _("Total Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 90,
"width": 120,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 120,
},
]
)

View File

@@ -33,7 +33,14 @@ frappe.query_reports["TDS Payable Monthly"] = {
frappe.throw(__("Please select Party Type first"));
}
return party_type;
}
},
"get_query": function() {
return {
"filters": {
"tax_withholding_category": ["!=",""],
}
}
},
},
{
"fieldname":"from_date",

View File

@@ -7,19 +7,26 @@ from frappe import _
def execute(filters=None):
if filters.get("party_type") == "Customer":
party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name")
else:
party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
filters.update({"naming_series": party_naming_by})
validate_filters(filters)
(
tds_docs,
tds_accounts,
tax_category_map,
journal_entry_party_map,
invoice_net_total_map,
net_total_map,
) = get_tds_docs(filters)
columns = get_columns(filters)
res = get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
)
return columns, res
@@ -31,7 +38,7 @@ def validate_filters(filters):
def get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
):
party_map = get_party_pan_map(filters.get("party_type"))
tax_rate_map = get_tax_rate_map(filters)
@@ -39,7 +46,7 @@ def get_result(
out = []
for name, details in gle_map.items():
tax_amount, total_amount = 0, 0
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
@@ -60,8 +67,8 @@ def get_result(
if entry.account in tds_accounts:
tax_amount += entry.credit - entry.debit
if invoice_net_total_map.get(name):
total_amount = invoice_net_total_map.get(name)
if net_total_map.get(name):
total_amount, grand_total, base_total = net_total_map.get(name)
else:
total_amount += entry.credit
@@ -69,15 +76,13 @@ def get_result(
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"
row = {
"pan"
if frappe.db.has_column(table_name, "pan")
if frappe.db.has_column(filters.party_type, "pan")
else "tax_id": party_map.get(party, {}).get("pan"),
"party": party_map.get(party, {}).get("name"),
}
@@ -91,6 +96,8 @@ def get_result(
"entity_type": party_map.get(party, {}).get(party_type),
"rate": rate,
"total_amount": total_amount,
"grand_total": grand_total,
"base_total": base_total,
"tax_amount": tax_amount,
"transaction_date": posting_date,
"transaction_type": voucher_type,
@@ -144,9 +151,9 @@ def get_gle_map(documents):
def get_columns(filters):
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
{
"label": _(filters.get("party_type")),
"fieldname": "party",
@@ -158,25 +165,30 @@ def get_columns(filters):
if filters.naming_series == "Naming Series":
columns.append(
{"label": _("Party Name"), "fieldname": "party_name", "fieldtype": "Data", "width": 180}
{
"label": _(filters.party_type + " Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
}
)
columns.extend(
[
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 180,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 120},
{
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 90,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
@@ -184,15 +196,27 @@ def get_columns(filters):
"width": 90,
},
{
"label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 90,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 90,
},
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"label": _("Grand Total"),
"fieldname": "grand_total",
"fieldtype": "Float",
"width": 90,
},
{
"label": _("Base Total"),
"fieldname": "base_total",
"fieldtype": "Float",
"width": 90,
},
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100},
@@ -216,7 +240,7 @@ def get_tds_docs(filters):
payment_entries = []
journal_entries = []
tax_category_map = frappe._dict()
invoice_net_total_map = frappe._dict()
net_total_map = frappe._dict()
or_filters = frappe._dict()
journal_entry_party_map = frappe._dict()
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
@@ -233,7 +257,7 @@ def get_tds_docs(filters):
}
party = frappe.get_all(filters.get("party_type"), pluck="name")
query_filters.update({"against": ("in", party)})
or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"})
if filters.get("party"):
del query_filters["account"]
@@ -260,24 +284,24 @@ def get_tds_docs(filters):
tds_documents.append(d.voucher_no)
if purchase_invoices:
get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, invoice_net_total_map)
get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, net_total_map)
if sales_invoices:
get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, invoice_net_total_map)
get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, net_total_map)
if payment_entries:
get_doc_info(payment_entries, "Payment Entry", tax_category_map)
get_doc_info(payment_entries, "Payment Entry", tax_category_map, net_total_map)
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
get_doc_info(journal_entries, "Journal Entry", tax_category_map)
get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map)
return (
tds_documents,
tds_accounts,
tax_category_map,
journal_entry_party_map,
invoice_net_total_map,
net_total_map,
)
@@ -285,7 +309,11 @@ def get_journal_entry_party_map(journal_entries):
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
{"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")},
{
"parent": ("in", journal_entries),
"party_type": ("in", ("Supplier", "Customer")),
"party": ("is", "set"),
},
["parent", "party"],
):
if d.parent not in journal_entry_party_map:
@@ -295,22 +323,40 @@ def get_journal_entry_party_map(journal_entries):
return journal_entry_party_map
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"]
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
common_fields = ["name"]
fields_dict = {
"Purchase Invoice": [
"tax_withholding_category",
"base_tax_withholding_net_total",
"grand_total",
"base_total",
],
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
"Payment Entry": [
"tax_withholding_category",
"paid_amount",
"paid_amount_after_tax",
"base_paid_amount",
],
"Journal Entry": ["tax_withholding_category", "total_amount"],
}
entries = frappe.get_all(doctype, filters={"name": ("in", vouchers)}, fields=fields)
entries = frappe.get_all(
doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
)
for entry in entries:
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})
value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]
elif doctype == "Sales Invoice":
value = [entry.base_net_total, entry.grand_total, entry.base_total]
elif doctype == "Payment Entry":
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
else:
value = [entry.total_amount] * 3
net_total_map.update({entry.name: value})
def get_tax_rate_map(filters):

View File

@@ -0,0 +1,111 @@
# Copyright (c) 2022, 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.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
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
from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import (
create_tax_withholding_category,
)
from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.clear_old_entries()
create_tax_accounts()
create_tcs_category()
def test_tax_withholding_for_customers(self):
si = create_sales_invoice(rate=1000)
pe = create_tcs_payment_entry()
filters = frappe._dict(
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
)
result = execute(filters)[1]
expected_values = [
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
[si.name, "TCS", 0.075, 1000, 0.53, 1000.53],
]
self.check_expected_values(result, expected_values)
def check_expected_values(self, result, expected_values):
for i in range(len(result)):
voucher = frappe._dict(result[i])
voucher_expected_values = expected_values[i]
self.assertEqual(voucher.ref_no, voucher_expected_values[0])
self.assertEqual(voucher.section_code, voucher_expected_values[1])
self.assertEqual(voucher.rate, voucher_expected_values[2])
self.assertEqual(voucher.base_total, voucher_expected_values[3])
self.assertEqual(voucher.tax_amount, voucher_expected_values[4])
self.assertEqual(voucher.grand_total, voucher_expected_values[5])
def tearDown(self):
self.clear_old_entries()
def create_tax_accounts():
account_names = ["TCS", "TDS"]
for account in account_names:
frappe.get_doc(
{
"doctype": "Account",
"company": "_Test Company",
"account_name": account,
"parent_account": "Duties and Taxes - _TC",
"report_type": "Balance Sheet",
"root_type": "Liability",
}
).insert(ignore_if_duplicate=True)
def create_tcs_category():
fiscal_year = get_fiscal_year(today(), company="_Test Company")
from_date = fiscal_year[1]
to_date = fiscal_year[2]
tax_category = create_tax_withholding_category(
category_name="TCS",
rate=0.075,
from_date=from_date,
to_date=to_date,
account="TCS - _TC",
cumulative_threshold=300,
)
customer = frappe.get_doc("Customer", "_Test Customer")
customer.tax_withholding_category = "TCS"
customer.save()
def create_tcs_payment_entry():
payment_entry = create_payment_entry(
payment_type="Receive",
party_type="Customer",
party="_Test Customer",
paid_from="Debtors - _TC",
paid_to="Cash - _TC",
paid_amount=2550,
)
payment_entry.append(
"taxes",
{
"account_head": "TCS - _TC",
"charge_type": "Actual",
"tax_amount": 0.53,
"add_deduct_tax": "Add",
"description": "Test",
"cost_center": "Main - _TC",
},
)
payment_entry.submit()
return payment_entry

View File

@@ -0,0 +1,118 @@
# Copyright (c) 2022, 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.trial_balance.trial_balance import execute
class TestTrialBalance(FrappeTestCase):
def setUp(self):
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.utils import get_fiscal_year
self.company = create_company()
create_cost_center(
cost_center_name="Test Cost Center",
company="Trial Balance Company",
parent_cost_center="Trial Balance Company - TBC",
)
create_account(
account_name="Offsetting",
company="Trial Balance Company",
parent_account="Temporary Accounts - TBC",
)
self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0]
create_accounting_dimension()
def test_offsetting_entries_for_accounting_dimensions(self):
"""
Checks if Trial Balance Report is balanced when filtered using a particular Accounting Dimension
"""
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'")
frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'")
branch1 = frappe.new_doc("Branch")
branch1.branch = "Location 1"
branch1.insert(ignore_if_duplicate=True)
branch2 = frappe.new_doc("Branch")
branch2.branch = "Location 2"
branch2.insert(ignore_if_duplicate=True)
si = create_sales_invoice(
company=self.company,
debit_to="Debtors - TBC",
cost_center="Test Cost Center - TBC",
income_account="Sales - TBC",
do_not_submit=1,
)
si.branch = "Location 1"
si.items[0].branch = "Location 2"
si.save()
si.submit()
filters = frappe._dict(
{"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]}
)
total_row = execute(filters)[1][-1]
self.assertEqual(total_row["debit"], total_row["credit"])
def tearDown(self):
clear_dimension_defaults("Branch")
disable_dimension()
def create_company(**args):
args = frappe._dict(args)
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": args.company_name or "Trial Balance Company",
"country": args.country or "India",
"default_currency": args.currency or "INR",
}
)
company.insert(ignore_if_duplicate=True)
return company.name
def create_accounting_dimension(**args):
args = frappe._dict(args)
document_type = args.document_type or "Branch"
if frappe.db.exists("Accounting Dimension", document_type):
accounting_dimension = frappe.get_doc("Accounting Dimension", document_type)
accounting_dimension.disabled = 0
else:
accounting_dimension = frappe.new_doc("Accounting Dimension")
accounting_dimension.document_type = document_type
accounting_dimension.insert()
accounting_dimension.set("dimension_defaults", [])
accounting_dimension.append(
"dimension_defaults",
{
"company": args.company or "Trial Balance Company",
"automatically_post_balancing_accounting_entry": 1,
"offsetting_account": args.offsetting_account or "Offsetting - TBC",
},
)
accounting_dimension.save()
def disable_dimension(**args):
args = frappe._dict(args)
document_type = args.document_type or "Branch"
dimension = frappe.get_doc("Accounting Dimension", document_type)
dimension.disabled = 1
dimension.save()
def clear_dimension_defaults(dimension_name):
accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name)
accounting_dimension.dimension_defaults = []
accounting_dimension.save()

View File

@@ -1,10 +1,11 @@
import frappe
from frappe import qb
from erpnext.stock.doctype.item.test_item import create_item
class AccountsTestMixin:
def create_customer(self, customer_name, currency=None):
def create_customer(self, customer_name="_Test Customer", currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
@@ -17,7 +18,7 @@ class AccountsTestMixin:
else:
self.customer = customer_name
def create_supplier(self, supplier_name, currency=None):
def create_supplier(self, supplier_name="_Test Supplier", currency=None):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
@@ -31,7 +32,7 @@ class AccountsTestMixin:
else:
self.supplier = supplier_name
def create_item(self, item_name, is_stock=0, warehouse=None, company=None):
def create_item(self, item_name="_Test Item", 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
@@ -59,22 +60,104 @@ class AccountsTestMixin:
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
self.retained_earnings = "Retained Earnings - " + 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(
# Deferred revenue, expense and bank accounts
other_accounts = [
frappe._dict(
{
"doctype": "Account",
"attribute_name": "deferred_revenue",
"account_name": "Deferred Revenue",
"parent_account": "Current Liabilities - " + abbr,
}
),
frappe._dict(
{
"attribute_name": "deferred_expense",
"account_name": "Deferred Expense",
"parent_account": "Current Assets - " + abbr,
}
),
frappe._dict(
{
"attribute_name": "bank",
"account_name": "HDFC",
"parent_account": "Bank Accounts - " + abbr,
"company": self.company,
}
),
]
for acc in other_accounts:
acc_name = acc.account_name + " - " + abbr
if frappe.db.exists("Account", acc_name):
setattr(self, acc.attribute_name, acc_name)
else:
new_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": acc.account_name,
"parent_account": acc.parent_account,
"company": self.company,
}
)
new_acc.save()
setattr(self, acc.attribute_name, new_acc.name)
def create_usd_receivable_account(self):
account_name = "Debtors USD"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Receivable - " + self.company_abbr
acc.company = self.company
acc.account_currency = "USD"
acc.account_type = "Receivable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
bank_acc.save()
self.bank = bank_acc.name
acc = frappe.get_doc("Account", name)
self.debtors_usd = acc.name
def create_usd_payable_account(self):
account_name = "Creditors USD"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Payable - " + self.company_abbr
acc.company = self.company
acc.account_currency = "USD"
acc.account_type = "Payable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.creditors_usd = acc.name
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
"Sales Order",
"Exchange Rate Revaluation",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()

View File

@@ -3,6 +3,8 @@ import unittest
import frappe
from frappe.test_runner import make_test_objects
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.party import get_party_shipping_address
from erpnext.accounts.utils import (
get_future_stock_vouchers,
@@ -73,6 +75,56 @@ class TestUtils(unittest.TestCase):
sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers)))
self.assertEqual(sorted_vouchers, vouchers)
def test_update_reference_in_payment_entry(self):
item = make_item().name
purchase_invoice = make_purchase_invoice(
item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32, do_not_submit=1
)
purchase_invoice.credit_to = "_Test Payable USD - _TC"
purchase_invoice.submit()
payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
payment_entry.paid_amount = 15725
payment_entry.deductions = []
payment_entry.save()
# below is the difference between base_received_amount and base_paid_amount
self.assertEqual(payment_entry.difference_amount, -4855.0)
payment_entry.target_exchange_rate = 62.9
payment_entry.save()
# below is due to change in exchange rate
self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0)
payment_entry.references = []
self.assertEqual(payment_entry.difference_amount, 0.0)
payment_entry.submit()
payment_reconciliation = frappe.new_doc("Payment Reconciliation")
payment_reconciliation.company = payment_entry.company
payment_reconciliation.party_type = "Supplier"
payment_reconciliation.party = purchase_invoice.supplier
payment_reconciliation.receivable_payable_account = payment_entry.paid_to
payment_reconciliation.get_unreconciled_entries()
payment_reconciliation.allocate_entries(
{
"payments": [d.__dict__ for d in payment_reconciliation.payments],
"invoices": [d.__dict__ for d in payment_reconciliation.invoices],
}
)
for d in payment_reconciliation.invoices:
# Reset invoice outstanding_amount because allocate_entries will zero this value out.
d.outstanding_amount = d.amount
for d in payment_reconciliation.allocation:
d.difference_account = "Exchange Gain/Loss - _TC"
payment_reconciliation.reconcile()
payment_entry.load_from_db()
self.assertEqual(len(payment_entry.references), 1)
self.assertEqual(payment_entry.difference_amount, 0)
ADDRESS_RECORDS = [
{

View File

@@ -458,7 +458,12 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
# update ref in advance entry
if voucher_type == "Journal Entry":
update_reference_in_journal_entry(entry, doc, do_not_save=True)
referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False)
# advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss
# amount and account in args
# referenced_row is used to deduplicate gain/loss journal
entry.update({"referenced_row": referenced_row})
doc.make_exchange_gain_loss_journal([entry])
else:
update_reference_in_payment_entry(
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
@@ -602,6 +607,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
if not do_not_save:
journal_entry.save(ignore_permissions=True)
return new_row.name
def update_reference_in_payment_entry(
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
@@ -612,9 +619,7 @@ def update_reference_in_payment_entry(
"total_amount": d.grand_total,
"outstanding_amount": d.outstanding_amount,
"allocated_amount": d.allocated_amount,
"exchange_rate": d.exchange_rate
if not d.exchange_gain_loss
else payment_entry.get_exchange_rate(),
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation
}
@@ -635,33 +640,48 @@ def update_reference_in_payment_entry(
new_row.docstatus = 1
new_row.update(reference_details)
payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
payment_entry.set_amounts()
if d.difference_amount and d.difference_account:
account_details = {
"account": d.difference_account,
"cost_center": payment_entry.cost_center
or frappe.get_cached_value("Company", payment_entry.company, "cost_center"),
}
if d.difference_amount:
account_details["amount"] = d.difference_amount
payment_entry.set_gain_or_loss(account_details=account_details)
payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
if not skip_ref_details_update_for_pe:
payment_entry.set_missing_ref_details()
payment_entry.set_amounts()
payment_entry.make_exchange_gain_loss_journal()
if not do_not_save:
payment_entry.save(ignore_permissions=True)
def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
"""
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
"""
if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
journals = frappe.db.get_all(
"Journal Entry Account",
filters={
"reference_type": parent_doc.doctype,
"reference_name": parent_doc.name,
"docstatus": 1,
},
fields=["parent"],
as_list=1,
)
if journals:
gain_loss_journals = frappe.db.get_all(
"Journal Entry",
filters={
"name": ["in", [x[0] for x in journals]],
"voucher_type": "Exchange Gain Or Loss",
"docstatus": 1,
},
as_list=1,
)
for doc in gain_loss_journals:
frappe.get_doc("Journal Entry", doc[0]).cancel()
def unlink_ref_doc_from_payment_entries(ref_doc):
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name)
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
@@ -868,6 +888,9 @@ def get_outstanding_invoices(
min_outstanding=None,
max_outstanding=None,
accounting_dimensions=None,
vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering
limit=None, # passed by reconciliation tool
voucher_no=None, # filter passed by reconciliation tool
):
ple = qb.DocType("Payment Ledger Entry")
@@ -893,12 +916,15 @@ def get_outstanding_invoices(
ple_query = QueryPaymentLedger()
invoice_list = ple_query.get_voucher_outstandings(
vouchers=vouchers,
common_filter=common_filter,
posting_date=posting_date,
min_outstanding=min_outstanding,
max_outstanding=max_outstanding,
get_invoices=True,
accounting_dimensions=accounting_dimensions or [],
limit=limit,
voucher_no=voucher_no,
)
for d in invoice_list:
@@ -1630,12 +1656,13 @@ class QueryPaymentLedger(object):
self.voucher_posting_date = []
self.min_outstanding = None
self.max_outstanding = None
self.limit = self.voucher_no = None
def reset(self):
# clear filters
self.vouchers.clear()
self.common_filter.clear()
self.min_outstanding = self.max_outstanding = None
self.min_outstanding = self.max_outstanding = self.limit = None
# clear result
self.voucher_outstandings.clear()
@@ -1649,6 +1676,7 @@ class QueryPaymentLedger(object):
filter_on_voucher_no = []
filter_on_against_voucher_no = []
if self.vouchers:
voucher_types = set([x.voucher_type for x in self.vouchers])
voucher_nos = set([x.voucher_no for x in self.vouchers])
@@ -1659,6 +1687,10 @@ class QueryPaymentLedger(object):
filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
if self.voucher_no:
filter_on_voucher_no.append(ple.voucher_no.like(f"%{self.voucher_no}%"))
filter_on_against_voucher_no.append(ple.against_voucher_no.like(f"%{self.voucher_no}%"))
# build outstanding amount filter
filter_on_outstanding_amount = []
if self.min_outstanding:
@@ -1692,6 +1724,7 @@ class QueryPaymentLedger(object):
ple.posting_date,
ple.due_date,
ple.account_currency.as_("currency"),
ple.cost_center.as_("cost_center"),
Sum(ple.amount).as_("amount"),
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
)
@@ -1754,6 +1787,7 @@ class QueryPaymentLedger(object):
).as_("paid_amount_in_account_currency"),
Table("vouchers").due_date,
Table("vouchers").currency,
Table("vouchers").cost_center.as_("cost_center"),
)
.where(Criterion.all(filter_on_outstanding_amount))
)
@@ -1774,6 +1808,11 @@ class QueryPaymentLedger(object):
)
)
if self.limit:
self.cte_query_voucher_amount_and_outstanding = (
self.cte_query_voucher_amount_and_outstanding.limit(self.limit)
)
# execute SQL
self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
@@ -1787,6 +1826,8 @@ class QueryPaymentLedger(object):
get_payments=False,
get_invoices=False,
accounting_dimensions=None,
limit=None,
voucher_no=None,
):
"""
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
@@ -1808,6 +1849,82 @@ class QueryPaymentLedger(object):
self.max_outstanding = max_outstanding
self.get_payments = get_payments
self.get_invoices = get_invoices
self.limit = limit
self.voucher_no = voucher_no
self.query_for_outstanding()
return self.voucher_outstandings
def create_gain_loss_journal(
company,
posting_date,
party_type,
party,
party_account,
gain_loss_account,
exc_gain_loss,
dr_or_cr,
reverse_dr_or_cr,
ref1_dt,
ref1_dn,
ref1_detail_no,
ref2_dt,
ref2_dn,
ref2_detail_no,
cost_center,
) -> str:
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
journal_entry.company = company
journal_entry.posting_date = posting_date or nowdate()
journal_entry.multi_currency = 1
journal_entry.is_system_generated = True
party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency")
if not gain_loss_account:
frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}").format(company))
gain_loss_account_currency = get_account_currency(gain_loss_account)
company_currency = frappe.get_cached_value("Company", company, "default_currency")
if gain_loss_account_currency != company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, company_currency))
journal_account = frappe._dict(
{
"account": party_account,
"party_type": party_type,
"party": party,
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"reference_type": ref1_dt,
"reference_name": ref1_dn,
"reference_detail_no": ref1_detail_no,
dr_or_cr: abs(exc_gain_loss),
dr_or_cr + "_in_account_currency": 0,
}
)
journal_entry.append("accounts", journal_account)
journal_account = frappe._dict(
{
"account": gain_loss_account,
"account_currency": gain_loss_account_currency,
"exchange_rate": 1,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"reference_type": ref2_dt,
"reference_name": ref2_dn,
"reference_detail_no": ref2_detail_no,
reverse_dr_or_cr + "_in_account_currency": 0,
reverse_dr_or_cr: abs(exc_gain_loss),
}
)
journal_entry.append("accounts", journal_account)
journal_entry.save()
journal_entry.submit()
return journal_entry.name

View File

@@ -318,6 +318,7 @@
"label": "Depreciation Schedule"
},
{
"depends_on": "schedules",
"fieldname": "schedules",
"fieldtype": "Table",
"label": "Depreciation Schedule",
@@ -537,7 +538,7 @@
"table_fieldname": "accounts"
}
],
"modified": "2023-07-28 15:47:01.137996",
"modified": "2023-08-10 20:25:09.913073",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -40,6 +40,7 @@ class Asset(AccountsController):
self.validate_item()
self.validate_cost_center()
self.set_missing_values()
self.validate_finance_books()
if not self.split_from:
self.prepare_depreciation_data()
self.validate_gross_and_purchase_amount()
@@ -81,18 +82,27 @@ class Asset(AccountsController):
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)
)
def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None):
def prepare_depreciation_data(
self,
date_of_disposal=None,
date_of_return=None,
value_after_depreciation=None,
ignore_booked_entry=False,
):
if self.calculate_depreciation:
self.value_after_depreciation = 0
self.set_depreciation_rate()
if self.should_prepare_depreciation_schedule():
self.make_depreciation_schedule(date_of_disposal)
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
self.make_depreciation_schedule(date_of_disposal, value_after_depreciation)
self.set_accumulated_depreciation(date_of_disposal, date_of_return, ignore_booked_entry)
else:
self.finance_books = []
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
self.opening_accumulated_depreciation
)
if value_after_depreciation:
self.value_after_depreciation = value_after_depreciation
else:
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
self.opening_accumulated_depreciation
)
def should_prepare_depreciation_schedule(self):
if not self.get("schedules"):
@@ -197,6 +207,27 @@ class Asset(AccountsController):
finance_books = get_item_details(self.item_code, self.asset_category)
self.set("finance_books", finance_books)
def validate_finance_books(self):
if not self.calculate_depreciation or len(self.finance_books) == 1:
return
finance_books = set()
for d in self.finance_books:
if d.finance_book in finance_books:
frappe.throw(
_("Row #{}: Please use a different Finance Book.").format(d.idx),
title=_("Duplicate Finance Book"),
)
else:
finance_books.add(d.finance_book)
if not d.finance_book:
frappe.throw(
_("Row #{}: Finance Book should not be empty since you're using multiple.").format(d.idx),
title=_("Missing Finance Book"),
)
def validate_asset_values(self):
if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
@@ -285,7 +316,7 @@ class Asset(AccountsController):
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
)
def make_depreciation_schedule(self, date_of_disposal):
def make_depreciation_schedule(self, date_of_disposal, value_after_depreciation=None):
if not self.get("schedules"):
self.schedules = []
@@ -295,24 +326,30 @@ class Asset(AccountsController):
start = self.clear_depreciation_schedule()
for finance_book in self.get("finance_books"):
self._make_depreciation_schedule(finance_book, start, date_of_disposal)
self._make_depreciation_schedule(
finance_book, start, date_of_disposal, value_after_depreciation
)
if len(self.get("finance_books")) > 1 and any(start):
self.sort_depreciation_schedule()
def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
def _make_depreciation_schedule(
self, finance_book, start, date_of_disposal, value_after_depreciation=None
):
self.validate_asset_finance_books(finance_book)
value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book)
if not value_after_depreciation:
value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book)
finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - cint(
final_number_of_depreciations = cint(finance_book.total_number_of_depreciations) - cint(
self.number_of_depreciations_booked
)
has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
final_number_of_depreciations += 1
has_wdv_or_dd_non_yearly_pro_rata = False
if (
@@ -328,7 +365,9 @@ class Asset(AccountsController):
depreciation_amount = 0
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
number_of_pending_depreciations = final_number_of_depreciations - start[finance_book.idx - 1]
for n in range(start[finance_book.idx - 1], final_number_of_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
@@ -345,10 +384,11 @@ class Asset(AccountsController):
n,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
number_of_pending_depreciations,
)
if not has_pro_rata or (
n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
):
schedule_date = add_months(
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
@@ -416,7 +456,7 @@ class Asset(AccountsController):
)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
elif has_pro_rata and n == cint(final_number_of_depreciations) - 1:
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(
@@ -447,7 +487,7 @@ class Asset(AccountsController):
# Adjust depreciation amount in the last period based on the expected value after useful life
if finance_book.expected_value_after_useful_life and (
(
n == cint(number_of_pending_depreciations) - 1
n == cint(final_number_of_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life
)
or value_after_depreciation < finance_book.expected_value_after_useful_life
@@ -690,7 +730,10 @@ class Asset(AccountsController):
if s.finance_book_id == d.finance_book_id
and (s.depreciation_method == "Straight Line" or s.depreciation_method == "Manual")
]
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
if i > 0 and self.flags.decrease_in_asset_value_due_to_value_adjustment:
accumulated_depreciation = self.get("schedules")[i - 1].accumulated_depreciation_amount
else:
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(
self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation
)
@@ -1289,29 +1332,43 @@ def get_total_days(date, frequency):
return date_diff(date, period_start_date)
@erpnext.allow_regional
def get_depreciation_amount(
asset,
depreciable_value,
row,
fb_row,
schedule_idx=0,
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
number_of_pending_depreciations=0,
):
if row.depreciation_method in ("Straight Line", "Manual"):
return get_straight_line_or_manual_depr_amount(asset, row)
frappe.flags.company = asset.company
if fb_row.depreciation_method in ("Straight Line", "Manual"):
return get_straight_line_or_manual_depr_amount(
asset, fb_row, schedule_idx, number_of_pending_depreciations
)
else:
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row
)
return get_wdv_or_dd_depr_amount(
depreciable_value,
row.rate_of_depreciation,
row.frequency_of_depreciation,
rate_of_depreciation,
fb_row.frequency_of_depreciation,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
)
def get_straight_line_or_manual_depr_amount(asset, row):
@erpnext.allow_regional
def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row):
return fb_row.rate_of_depreciation
def get_straight_line_or_manual_depr_amount(
asset, row, schedule_idx, number_of_pending_depreciations
):
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
@@ -1322,13 +1379,88 @@ def get_straight_line_or_manual_depr_amount(asset, row):
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
if row.daily_depreciation:
daily_depr_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / date_diff(
get_last_day(
add_months(
row.depreciation_start_date,
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
* row.frequency_of_depreciation,
)
),
add_days(
get_last_day(
add_months(
row.depreciation_start_date,
flt(
row.total_number_of_depreciations
- asset.number_of_depreciations_booked
- number_of_pending_depreciations
- 1
)
* row.frequency_of_depreciation,
)
),
1,
),
)
to_date = get_last_day(
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
)
from_date = add_days(
get_last_day(
add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation)
),
1,
)
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
else:
return (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / number_of_pending_depreciations
# if the Depreciation Schedule is being prepared for the first time
else:
return (
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
if row.daily_depreciation:
daily_depr_amount = (
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
) / date_diff(
get_last_day(
add_months(
row.depreciation_start_date,
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
* row.frequency_of_depreciation,
)
),
add_days(
get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), 1
),
)
to_date = get_last_day(
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
)
from_date = add_days(
get_last_day(
add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation)
),
1,
)
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
else:
return (
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
def get_wdv_or_dd_depr_amount(

View File

@@ -730,6 +730,40 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(schedules, expected_schedules)
def test_schedule_for_straight_line_method_with_daily_depreciation(self):
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2023-01-01",
purchase_date="2023-01-01",
gross_purchase_amount=12000,
depreciation_start_date="2023-01-31",
total_number_of_depreciations=12,
frequency_of_depreciation=1,
daily_depreciation=1,
)
expected_schedules = [
["2023-01-31", 1021.98, 1021.98],
["2023-02-28", 923.08, 1945.06],
["2023-03-31", 1021.98, 2967.04],
["2023-04-30", 989.01, 3956.05],
["2023-05-31", 1021.98, 4978.03],
["2023-06-30", 989.01, 5967.04],
["2023-07-31", 1021.98, 6989.02],
["2023-08-31", 1021.98, 8011.0],
["2023-09-30", 989.01, 9000.01],
["2023-10-31", 1021.98, 10021.99],
["2023-11-30", 989.01, 11011.0],
["2023-12-31", 989.0, 12000.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset.get("schedules")
]
self.assertEqual(schedules, expected_schedules)
def test_schedule_for_double_declining_method(self):
asset = create_asset(
calculate_depreciation=1,
@@ -1298,6 +1332,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
@@ -1308,6 +1343,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
@@ -1318,6 +1354,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 3",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
@@ -1347,6 +1384,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
@@ -1357,6 +1395,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
@@ -1613,6 +1652,15 @@ def create_asset_data():
if not frappe.db.exists("Location", "Test Location"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 1"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 1"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 2"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 2"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 3"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 3"}).insert()
def create_asset(**args):
args = frappe._dict(args)
@@ -1653,6 +1701,7 @@ def create_asset(**args):
"total_number_of_depreciations": args.total_number_of_depreciations or 5,
"expected_value_after_useful_life": args.expected_value_after_useful_life or 0,
"depreciation_start_date": args.depreciation_start_date,
"daily_depreciation": args.daily_depreciation or 0,
},
)

View File

@@ -399,14 +399,11 @@ class AssetCapitalization(StockController):
def get_gl_entries_for_consumed_asset_items(
self, gl_entries, target_account, target_against, precision
):
self.are_all_asset_items_non_depreciable = True
# Consumed Assets
for item in self.asset_items:
asset = frappe.get_doc("Asset", item.asset)
if asset.calculate_depreciation:
self.are_all_asset_items_non_depreciable = False
depreciate_asset(asset, self.posting_date)
asset.reload()

View File

@@ -33,6 +33,7 @@ frappe.ui.form.on('Asset Category', {
var d = locals[cdt][cdn];
return {
"filters": {
"account_type": "Depreciation",
"root_type": ["in", ["Expense", "Income"]],
"is_group": 0,
"company": d.company_name

View File

@@ -53,7 +53,7 @@ class AssetCategory(Document):
account_type_map = {
"fixed_asset_account": {"account_type": ["Fixed Asset"]},
"accumulated_depreciation_account": {"account_type": ["Accumulated Depreciation"]},
"depreciation_expense_account": {"root_type": ["Expense", "Income"]},
"depreciation_expense_account": {"account_type": ["Depreciation"]},
"capital_work_in_progress_account": {"account_type": ["Capital Work in Progress"]},
}
for d in self.accounts:

View File

@@ -8,6 +8,7 @@
"finance_book",
"depreciation_method",
"total_number_of_depreciations",
"daily_depreciation",
"column_break_5",
"frequency_of_depreciation",
"depreciation_start_date",
@@ -79,12 +80,19 @@
"fieldname": "rate_of_depreciation",
"fieldtype": "Percent",
"label": "Rate of Depreciation"
},
{
"default": "0",
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
"fieldname": "daily_depreciation",
"fieldtype": "Check",
"label": "Daily Depreciation"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-17 12:59:05.743683",
"modified": "2023-08-10 18:56:09.022246",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",
@@ -93,5 +101,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -80,14 +80,16 @@ def calculate_next_due_date(
next_due_date = add_days(start_date, 7)
if periodicity == "Monthly":
next_due_date = add_months(start_date, 1)
if periodicity == "Quarterly":
next_due_date = add_months(start_date, 3)
if periodicity == "Half-yearly":
next_due_date = add_months(start_date, 6)
if periodicity == "Yearly":
next_due_date = add_years(start_date, 1)
if periodicity == "2 Yearly":
next_due_date = add_years(start_date, 2)
if periodicity == "3 Yearly":
next_due_date = add_years(start_date, 3)
if periodicity == "Quarterly":
next_due_date = add_months(start_date, 3)
if end_date and (
(start_date and start_date >= end_date)
or (last_completion_date and last_completion_date >= end_date)

View File

@@ -71,7 +71,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Periodicity",
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly",
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly\n2 Yearly\n3 Yearly",
"reqd": 1
},
{
@@ -153,4 +153,4 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

@@ -5,15 +5,12 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, date_diff, flt, formatdate, getdate
from frappe.utils import flt, formatdate, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.assets.doctype.asset.asset import (
get_asset_value_after_depreciation,
get_depreciation_amount,
)
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
@@ -25,10 +22,10 @@ class AssetValueAdjustment(Document):
def on_submit(self):
self.make_depreciation_entry()
self.reschedule_depreciations(self.new_asset_value)
self.update_asset(self.new_asset_value)
def on_cancel(self):
self.reschedule_depreciations(self.current_asset_value)
self.update_asset(self.current_asset_value)
def validate_date(self):
asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
@@ -71,12 +68,16 @@ class AssetValueAdjustment(Document):
"account": accumulated_depreciation_account,
"credit_in_account_currency": self.difference_amount,
"cost_center": depreciation_cost_center or self.cost_center,
"reference_type": "Asset",
"reference_name": asset.name,
}
debit_entry = {
"account": depreciation_expense_account,
"debit_in_account_currency": self.difference_amount,
"cost_center": depreciation_cost_center or self.cost_center,
"reference_type": "Asset",
"reference_name": asset.name,
}
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
@@ -106,44 +107,11 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name)
def reschedule_depreciations(self, asset_value):
def update_asset(self, asset_value):
asset = frappe.get_doc("Asset", self.asset)
country = frappe.get_value("Company", self.company, "country")
for d in asset.finance_books:
d.value_after_depreciation = asset_value
asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
if d.depreciation_method in ("Straight Line", "Manual"):
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
total_days = date_diff(end_date, self.date)
rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
total_days
)
from_date = self.date
else:
no_of_depreciations = len(
[
s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry)
]
)
value_after_depreciation = d.value_after_depreciation
for data in asset.schedules:
if cint(data.finance_book_id) == d.idx and not data.journal_entry:
if d.depreciation_method in ("Straight Line", "Manual"):
days = date_diff(data.schedule_date, from_date)
depreciation_amount = days * rate_per_day
from_date = data.schedule_date
else:
depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
if depreciation_amount:
value_after_depreciation -= flt(depreciation_amount)
data.depreciation_amount = depreciation_amount
d.db_update()
asset.set_accumulated_depreciation(ignore_booked_entry=True)
for asset_data in asset.schedules:
if not asset_data.journal_entry:
asset_data.db_update()
asset.prepare_depreciation_data(value_after_depreciation=asset_value, ignore_booked_entry=True)
asset.flags.ignore_validate_update_after_submit = True
asset.save()

View File

@@ -4,9 +4,10 @@
import unittest
import frappe
from frappe.utils import add_days, get_last_day, nowdate
from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -46,40 +47,44 @@ class TestAssetValueAdjustment(unittest.TestCase):
def test_asset_depreciation_value_adjustment(self):
pr = make_purchase_receipt(
item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location"
)
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset_doc = frappe.get_doc("Asset", asset_name)
asset_doc.calculate_depreciation = 1
month_end_date = get_last_day(nowdate())
purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
asset_doc.available_for_use_date = purchase_date
asset_doc.purchase_date = purchase_date
asset_doc.available_for_use_date = "2023-01-15"
asset_doc.purchase_date = "2023-01-15"
asset_doc.calculate_depreciation = 1
asset_doc.append(
"finance_books",
{
"expected_value_after_useful_life": 200,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10,
"depreciation_start_date": month_end_date,
"total_number_of_depreciations": 12,
"frequency_of_depreciation": 1,
"depreciation_start_date": "2023-01-31",
},
)
asset_doc.submit()
post_depreciation_entries(getdate("2023-08-21"))
current_value = get_asset_value_after_depreciation(asset_doc.name)
adj_doc = make_asset_value_adjustment(
asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
asset=asset_doc.name,
current_asset_value=current_value,
new_asset_value=50000.0,
date="2023-08-21",
)
adj_doc.submit()
asset_doc.reload()
expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 50000.0),
("_Test Depreciations - _TC", 50000.0, 0.0),
("_Test Accumulated Depreciations - _TC", 0.0, 4625.29),
("_Test Depreciations - _TC", 4625.29, 0.0),
)
gle = frappe.db.sql(
@@ -91,6 +96,29 @@ class TestAssetValueAdjustment(unittest.TestCase):
self.assertSequenceEqual(gle, expected_gle)
expected_schedules = [
["2023-01-31", 5474.73, 5474.73],
["2023-02-28", 9983.33, 15458.06],
["2023-03-31", 9983.33, 25441.39],
["2023-04-30", 9983.33, 35424.72],
["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71],
["2023-08-31", 8300.0, 73674.71],
["2023-09-30", 8300.0, 81974.71],
["2023-10-31", 8300.0, 90274.71],
["2023-11-30", 8300.0, 98574.71],
["2023-12-31", 8300.0, 106874.71],
["2024-01-15", 8300.0, 115174.71],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset_doc.get("schedules")
]
self.assertEqual(schedules, expected_schedules)
def make_asset_value_adjustment(**args):
args = frappe._dict(args)

View File

@@ -39,6 +39,7 @@
{
"fieldname": "parent_location",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Parent Location",
"options": "Location",
"search_index": 1
@@ -141,11 +142,11 @@
],
"is_tree": 1,
"links": [],
"modified": "2020-05-08 16:11:11.375701",
"modified": "2023-08-29 12:49:33.290527",
"modified_by": "Administrator",
"module": "Assets",
"name": "Location",
"name_case": "Title Case",
"naming_rule": "By fieldname",
"nsm_parent_field": "parent_location",
"owner": "Administrator",
"permissions": [
@@ -224,5 +225,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -7,13 +7,14 @@ from itertools import chain
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cstr, flt, formatdate, getdate
from frappe.utils import add_months, cstr, flt, formatdate, getdate, nowdate, today
from erpnext.accounts.report.financial_statements import (
get_fiscal_year_data,
get_period_list,
validate_fiscal_year,
)
from erpnext.accounts.utils import get_fiscal_year
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
@@ -37,15 +38,26 @@ def get_conditions(filters):
if filters.get("company"):
conditions["company"] = filters.company
if filters.filter_based_on == "Date Range":
if not filters.from_date and not filters.to_date:
filters.from_date = add_months(nowdate(), -12)
filters.to_date = nowdate()
conditions[date_field] = ["between", [filters.from_date, filters.to_date]]
if filters.filter_based_on == "Fiscal Year":
elif filters.filter_based_on == "Fiscal Year":
if not filters.from_fiscal_year and not filters.to_fiscal_year:
default_fiscal_year = get_fiscal_year(today())[0]
filters.from_fiscal_year = default_fiscal_year
filters.to_fiscal_year = default_fiscal_year
fiscal_year = get_fiscal_year_data(filters.from_fiscal_year, filters.to_fiscal_year)
validate_fiscal_year(fiscal_year, filters.from_fiscal_year, filters.to_fiscal_year)
filters.year_start_date = getdate(fiscal_year.year_start_date)
filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets")
if filters.get("asset_category"):
@@ -144,6 +156,8 @@ def get_data(filters):
def prepare_chart_data(data, filters):
if not data:
return
labels_values_map = {}
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
filters_filter_based_on = "Date Range"

View File

@@ -1171,6 +1171,7 @@
"depends_on": "is_internal_supplier",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Set From Warehouse",
"options": "Warehouse"
},
@@ -1271,7 +1272,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2023-05-24 11:16:41.195340",
"modified": "2023-09-13 16:21:07.361700",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -878,6 +878,7 @@
"depends_on": "eval:parent.is_internal_supplier",
"fieldname": "from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "From Warehouse",
"options": "Warehouse"
},
@@ -902,7 +903,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-11-29 16:47:41.364387",
"modified": "2023-09-13 16:22:40.825092",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -245,19 +245,21 @@ frappe.ui.form.on("Request for Quotation",{
]
});
dialog.fields_dict['supplier'].df.onchange = () => {
var supplier = dialog.get_value('supplier');
frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => {
dialog.fields_dict["supplier"].df.onchange = () => {
frm.call("get_supplier_email_preview", {
supplier: dialog.get_value("supplier"),
}).then(({ message }) => {
dialog.fields_dict.email_preview.$wrapper.empty();
dialog.fields_dict.email_preview.$wrapper.append(result.message);
dialog.fields_dict.email_preview.$wrapper.append(
message.message
);
dialog.set_value("subject", message.subject);
});
}
};
dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">This is a preview of the email to be sent. A PDF of the document will
automatically be attached with the email.</p>`);
dialog.set_value("subject", frm.doc.subject);
dialog.show();
}
})

View File

@@ -20,12 +20,12 @@
"items_section",
"items",
"supplier_response_section",
"salutation",
"subject",
"col_break_email_1",
"email_template",
"preview",
"col_break_email_1",
"html_llwp",
"send_attached_files",
"send_document_print",
"sec_break_email_2",
"message_for_supplier",
"terms_section_break",
@@ -237,23 +237,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "email_template.subject",
"fetch_if_empty": 1,
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject",
"print_hide": 1
},
{
"description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.",
"fieldname": "salutation",
"fieldtype": "Link",
"label": "Salutation",
"no_copy": 1,
"options": "Salutation",
"print_hide": 1
},
{
"fieldname": "col_break_email_1",
"fieldtype": "Column Break"
@@ -287,19 +270,35 @@
"fieldtype": "Data",
"label": "Named Place"
},
{
"fieldname": "html_llwp",
"fieldtype": "HTML",
"options": "<p>In your <b>Email Template</b>, you can use the following special variables:\n</p>\n<ul>\n <li>\n <code>{{ update_password_link }}</code>: A link where your supplier can set a new password to log into your portal.\n </li>\n <li>\n <code>{{ portal_link }}</code>: A link to this RFQ in your supplier portal.\n </li>\n <li>\n <code>{{ supplier_name }}</code>: The company name of your supplier.\n </li>\n <li>\n <code>{{ contact.salutation }} {{ contact.last_name }}</code>: The contact person of your supplier.\n </li><li>\n <code>{{ user_fullname }}</code>: Your full name.\n </li>\n </ul>\n<p></p>\n<p>Apart from these, you can access all values in this RFQ, like <code>{{ message_for_supplier }}</code> or <code>{{ terms }}</code>.</p>",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"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"
},
{
"default": "0",
"description": "If enabled, a print of this document will be attached to each email",
"fieldname": "send_document_print",
"fieldtype": "Check",
"label": "Send Document Print",
"print_hide": 1
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-07-27 16:41:48.468873",
"modified": "2023-08-09 12:20:26.850623",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@@ -116,7 +116,10 @@ class RequestforQuotation(BuyingController):
route = frappe.db.get_value(
"Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
)
return get_url("/app/{0}/".format(route) + self.name)
if not route:
frappe.throw(_("Please add Request for Quotation to the sidebar in Portal Settings."))
return get_url(f"{route}/{self.name}")
def update_supplier_part_no(self, supplier):
self.vendor = supplier
@@ -179,40 +182,47 @@ class RequestforQuotation(BuyingController):
if full_name == "Guest":
full_name = "Administrator"
# send document dict and some important data from suppliers row
# to render message_for_supplier from any template
doc_args = self.as_dict()
doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
# Get Contact Full Name
supplier_name = None
if data.get("contact"):
contact_name = frappe.db.get_value(
"Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
)
supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
contact = frappe.get_doc("Contact", data.get("contact"))
doc_args["contact"] = contact.as_dict()
args = {
"update_password_link": update_password_link,
"message": frappe.render_template(self.message_for_supplier, doc_args),
"rfq_link": rfq_link,
"user_fullname": full_name,
"supplier_name": supplier_name or data.get("supplier_name"),
"supplier_salutation": self.salutation or "Dear Mx.",
}
subject = self.subject or _("Request for Quotation")
template = "templates/emails/request_for_quotation.html"
doc_args.update(
{
"supplier": data.get("supplier"),
"supplier_name": data.get("supplier_name"),
"update_password_link": f'<a href="{update_password_link}" class="btn btn-default btn-xs" target="_blank">{_("Set Password")}</a>',
"portal_link": f'<a href="{rfq_link}" class="btn btn-default btn-xs" target="_blank"> {_("Submit your Quotation")} </a>',
"user_fullname": full_name,
}
)
email_template = frappe.get_doc("Email Template", self.email_template)
message = frappe.render_template(email_template.response_, doc_args)
subject = frappe.render_template(email_template.subject, doc_args)
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
message = frappe.get_template(template).render(args)
if preview:
return message
return {"message": message, "subject": subject}
attachments = None
attachments = []
if self.send_attached_files:
attachments = self.get_attachments()
if self.send_document_print:
supplier_language = frappe.db.get_value("Supplier", data.supplier, "language")
system_language = frappe.db.get_single_value("System Settings", "language")
attachments.append(
frappe.attach_print(
self.doctype,
self.name,
doc=self,
print_format=self.meta.default_print_format or "Standard",
lang=supplier_language or system_language,
letterhead=self.letter_head,
)
)
self.send_email(data, sender, subject, message, attachments)
def send_email(self, data, sender, subject, message, attachments):
@@ -222,7 +232,6 @@ class RequestforQuotation(BuyingController):
recipients=data.email_id,
sender=sender,
attachments=attachments,
print_format=self.meta.default_print_format or "Standard",
send_email=True,
doctype=self.doctype,
name=self.name,

View File

@@ -2,11 +2,14 @@
# See license.txt
from urllib.parse import urlparse
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
RequestforQuotation,
create_supplier_quotation,
get_pdf,
make_supplier_quotation_from_rfq,
@@ -125,13 +128,18 @@ class TestRequestforQuotation(FrappeTestCase):
rfq.status = "Draft"
rfq.submit()
def test_get_link(self):
rfq = make_request_for_quotation()
parsed_link = urlparse(rfq.get_link())
self.assertEqual(parsed_link.path, f"/rfq/{rfq.name}")
def test_get_pdf(self):
rfq = make_request_for_quotation()
get_pdf(rfq.name, rfq.get("suppliers")[0].supplier)
self.assertEqual(frappe.local.response.type, "pdf")
def make_request_for_quotation(**args):
def make_request_for_quotation(**args) -> "RequestforQuotation":
"""
:param supplier_data: List containing supplier data
"""

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