From dddc29fdc1a6bb634ed12cc9ea6b364784ebb7c6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 17:55:24 +0530 Subject: [PATCH 01/10] feat: initialize party link for customer & suppliers --- .../accounts/doctype/party_link/__init__.py | 0 .../accounts/doctype/party_link/party_link.js | 41 ++++++++++ .../doctype/party_link/party_link.json | 78 +++++++++++++++++++ .../accounts/doctype/party_link/party_link.py | 12 +++ .../doctype/party_link/test_party_link.py | 8 ++ 5 files changed, 139 insertions(+) create mode 100644 erpnext/accounts/doctype/party_link/__init__.py create mode 100644 erpnext/accounts/doctype/party_link/party_link.js create mode 100644 erpnext/accounts/doctype/party_link/party_link.json create mode 100644 erpnext/accounts/doctype/party_link/party_link.py create mode 100644 erpnext/accounts/doctype/party_link/test_party_link.py diff --git a/erpnext/accounts/doctype/party_link/__init__.py b/erpnext/accounts/doctype/party_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/party_link/party_link.js b/erpnext/accounts/doctype/party_link/party_link.js new file mode 100644 index 0000000000..966a5f5d30 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.js @@ -0,0 +1,41 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Party Link', { + refresh: function(frm) { + frm.set_query('party_type', 'links', () => { + return { + filters: { + name: ['in', party_types] + } + }; + }); + + frm.set_query('primary_role', () => { + return { + filters: { + name: ['in', ['Customer', 'Supplier']] + } + }; + }); + + frm.set_query('secondary_role', () => { + let party_types = Object.keys(frappe.boot.party_account_types) + .filter(p => p != frm.doc.primary_role); + return { + filters: { + name: ['in', party_types] + } + }; + }); + }, + + primary_role(frm) { + frm.set_value('primary_party', ''); + frm.set_value('secondary_role', ''); + }, + + secondary_role(frm) { + frm.set_value('secondary_party', ''); + } +}); diff --git a/erpnext/accounts/doctype/party_link/party_link.json b/erpnext/accounts/doctype/party_link/party_link.json new file mode 100644 index 0000000000..2053dc0f00 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.json @@ -0,0 +1,78 @@ +{ + "actions": [], + "autoname": "ACC-PT-LNK-.###.", + "creation": "2021-08-18 21:06:53.027695", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "primary_role", + "secondary_role", + "column_break_2", + "primary_party", + "secondary_party" + ], + "fields": [ + { + "fieldname": "primary_role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Primary Role", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "depends_on": "primary_role", + "fieldname": "secondary_role", + "fieldtype": "Link", + "label": "Secondary Role", + "mandatory_depends_on": "primary_role", + "options": "DocType" + }, + { + "depends_on": "primary_role", + "fieldname": "primary_party", + "fieldtype": "Dynamic Link", + "label": "Primary Party", + "mandatory_depends_on": "primary_role", + "options": "primary_role" + }, + { + "depends_on": "secondary_role", + "fieldname": "secondary_party", + "fieldtype": "Dynamic Link", + "label": "Secondary Party", + "mandatory_depends_on": "secondary_role", + "options": "secondary_role" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-08-19 17:53:43.456752", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Party Link", + "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", + "title_field": "primary_party", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py new file mode 100644 index 0000000000..80f86e75a0 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -0,0 +1,12 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + +class PartyLink(Document): + def validate(self): + if self.primary_role not in ['Customer', 'Supplier']: + frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."), + title=_("Invalid Primary Role")) diff --git a/erpnext/accounts/doctype/party_link/test_party_link.py b/erpnext/accounts/doctype/party_link/test_party_link.py new file mode 100644 index 0000000000..a3ea3959ba --- /dev/null +++ b/erpnext/accounts/doctype/party_link/test_party_link.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestPartyLink(unittest.TestCase): + pass From cad08bc428346f1f09c0f6b8eaf59a9bf95ca3df Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 17:56:04 +0530 Subject: [PATCH 02/10] feat: toggle to enable common party accounting --- .../doctype/accounts_settings/accounts_settings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index a246ae51a4..7d0ecfbafd 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -19,6 +19,7 @@ "delete_linked_ledger_entries", "book_asset_depreciation_entry_automatically", "unlink_advance_payment_on_cancelation_of_order", + "enable_common_party_accounting", "post_change_gl_entries", "enable_discount_accounting", "tax_settings_section", @@ -268,6 +269,12 @@ "fieldname": "enable_discount_accounting", "fieldtype": "Check", "label": "Enable Discount Accounting" + }, + { + "default": "0", + "fieldname": "enable_common_party_accounting", + "fieldtype": "Check", + "label": "Enable Common Party Accounting" } ], "icon": "icon-cog", @@ -275,7 +282,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-08-09 13:08:04.335416", + "modified": "2021-08-19 11:17:38.788054", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 977b09b6ba35eeb0295c8397bfb982824355c699 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 17:57:30 +0530 Subject: [PATCH 03/10] feat: auto create advance entry on invoice submission --- .../purchase_invoice/purchase_invoice.py | 2 + .../doctype/sales_invoice/sales_invoice.py | 2 + erpnext/controllers/accounts_controller.py | 62 ++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index a16795e628..e2f02f37ee 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -415,6 +415,8 @@ class PurchaseInvoice(BuyingController): self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) + self.process_common_party_accounting() + def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: gl_entries = self.get_gl_entries() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5fa622856b..f29f7bef22 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -253,6 +253,8 @@ class SalesInvoice(SellingController): if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_submit") + self.process_common_party_accounting() + def validate_pos_return(self): if self.is_pos and self.is_return: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4c243d0cc4..1b23289062 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -14,7 +14,7 @@ from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_a from erpnext.utilities.transaction_base import TransactionBase from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.sales_and_purchase_return import validate_return -from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled +from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled, get_party_account from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction, apply_pricing_rule_for_free_items, get_applied_pricing_rules) from erpnext.exceptions import InvalidCurrency @@ -1367,6 +1367,66 @@ class AccountsController(TransactionBase): return False + def process_common_party_accounting(self): + is_invoice = self.doctype in ['Sales Invoice', 'Purchase Invoice'] + if not is_invoice: + return + + if frappe.db.get_single_value('Accounts Settings', 'enable_common_party_accounting'): + party_link = self.get_common_party_link() + if party_link and self.outstanding_amount: + self.create_advance_and_reconcile(party_link) + + def get_common_party_link(self): + party_type, party = self.get_party() + party_link = frappe.db.exists('Party Link', {'secondary_role': party_type, 'secondary_party': party}) + if party_link: + return frappe.db.get_value('Party Link', party_link, ['primary_role', 'primary_party'], as_dict=True) + + def create_advance_and_reconcile(self, party_link): + secondary_party_type, secondary_party = self.get_party() + primary_party_type, primary_party = party_link.primary_role, party_link.primary_party + + primary_account = get_party_account(primary_party_type, primary_party, self.company) + secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) + + jv = frappe.new_doc('Journal Entry') + jv.voucher_type = 'Journal Entry' + jv.naming_series = 'ACC-JV-.YYYY.-' + jv.posting_date = self.posting_date + jv.company = self.company + jv.remark = 'Adjustment for {} {}'.format(self.doctype, self.name) + + reconcilation_entry = frappe._dict() + advance_entry = frappe._dict() + cost_center = erpnext.get_default_cost_center(self.company) + + reconcilation_entry.account = secondary_account + reconcilation_entry.party_type = secondary_party_type + reconcilation_entry.party = secondary_party + reconcilation_entry.reference_type = self.doctype + reconcilation_entry.reference_name = self.name + reconcilation_entry.cost_center = cost_center + + advance_entry.account = primary_account + advance_entry.party_type = primary_party_type + advance_entry.party = primary_party + advance_entry.cost_center = cost_center + advance_entry.is_advance = 'Yes' + + if self.doctype == 'Sales Invoice': + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + advance_entry.debit_in_account_currency = self.outstanding_amount + else: + advance_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + + jv.append('accounts', reconcilation_entry) + jv.append('accounts', advance_entry) + + jv.save() + jv.submit() + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) From 71e72541846f6294bd0ccee3f76fb2448d8cfa9b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 17:57:54 +0530 Subject: [PATCH 04/10] test: creation of advance entry on invoice submission --- .../sales_invoice/test_sales_invoice.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 4d1e0c3e06..8bb25dc663 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2174,6 +2174,50 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertTrue(schedule.journal_entry) + def test_sales_invoice_against_supplier(self): + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import make_customer + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + # create a customer + customer = make_customer(customer="_Test Common Supplier") + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Supplier").name + + # create a party link between customer & supplier + # set primary role as supplier + party_link = frappe.new_doc("Party Link") + party_link.primary_role = "Supplier" + party_link.primary_party = supplier + party_link.secondary_role = "Customer" + party_link.secondary_party = customer + party_link.save() + + # enable common party accounting + frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) + + # create a sales invoice + si = create_sales_invoice(customer=customer) + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, 'Paid') + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all('Journal Entry Account', { + 'account': si.debit_to, + 'party_type': 'Customer', + 'party': si.customer, + 'reference_type': si.doctype, + 'reference_name': si.name + }, pluck='credit_in_account_currency') + + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + party_link.delete() + frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' From 8e79c48db87da4bb180f4e67333d73501e88b9dd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 18:10:02 +0530 Subject: [PATCH 05/10] fix: remove unwanted filter query --- erpnext/accounts/doctype/party_link/party_link.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/accounts/doctype/party_link/party_link.js b/erpnext/accounts/doctype/party_link/party_link.js index 966a5f5d30..6da9291d64 100644 --- a/erpnext/accounts/doctype/party_link/party_link.js +++ b/erpnext/accounts/doctype/party_link/party_link.js @@ -3,14 +3,6 @@ frappe.ui.form.on('Party Link', { refresh: function(frm) { - frm.set_query('party_type', 'links', () => { - return { - filters: { - name: ['in', party_types] - } - }; - }); - frm.set_query('primary_role', () => { return { filters: { From c101b42d4a086c1e328fcdd442d4af33bde1f9c0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 20:08:28 +0530 Subject: [PATCH 06/10] feat: validate multiple links --- erpnext/accounts/doctype/party_link/party_link.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py index 80f86e75a0..7d58506ce7 100644 --- a/erpnext/accounts/doctype/party_link/party_link.py +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -10,3 +10,17 @@ class PartyLink(Document): if self.primary_role not in ['Customer', 'Supplier']: frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."), title=_("Invalid Primary Role")) + + existing_party_link = frappe.get_all('Party Link', { + 'primary_party': self.secondary_party + }, pluck="primary_role") + if existing_party_link: + frappe.throw(_('{} {} is already linked with another {}') + .format(self.secondary_role, self.secondary_party, existing_party_link[0])) + + existing_party_link = frappe.get_all('Party Link', { + 'secondary_party': self.primary_party + }, pluck="primary_role") + if existing_party_link: + frappe.throw(_('{} {} is already linked with another {}') + .format(self.primary_role, self.primary_party, existing_party_link[0])) From a2f8d6c31acbfeac4cc3abfedbaf59375f6fe962 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 20:10:19 +0530 Subject: [PATCH 07/10] fix: party link permissions --- .../doctype/party_link/party_link.json | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/party_link/party_link.json b/erpnext/accounts/doctype/party_link/party_link.json index 2053dc0f00..a1bb15f0d6 100644 --- a/erpnext/accounts/doctype/party_link/party_link.json +++ b/erpnext/accounts/doctype/party_link/party_link.json @@ -52,7 +52,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-08-19 17:53:43.456752", + "modified": "2021-08-25 20:08:56.761150", "modified_by": "Administrator", "module": "Accounts", "name": "Party Link", @@ -69,6 +69,30 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 } ], "sort_field": "modified", From 7254368fc52dd9f828dfe3c3d9570ba3274f0ade Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 20:15:23 +0530 Subject: [PATCH 08/10] perf: reduce number of queries to get party link --- erpnext/controllers/accounts_controller.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 55e6f0482c..798b994920 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1375,9 +1375,12 @@ class AccountsController(TransactionBase): def get_common_party_link(self): party_type, party = self.get_party() - party_link = frappe.db.exists('Party Link', {'secondary_role': party_type, 'secondary_party': party}) - if party_link: - return frappe.db.get_value('Party Link', party_link, ['primary_role', 'primary_party'], as_dict=True) + return frappe.db.get_value( + doctype='Party Link', + filters={'secondary_role': party_type, 'secondary_party': party}, + fieldname=['primary_role', 'primary_party'], + as_dict=True + ) def create_advance_and_reconcile(self, party_link): secondary_party_type, secondary_party = self.get_party() From c6c7a8b5cf182b9314aa726296a1e34767450939 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 20:17:04 +0530 Subject: [PATCH 09/10] fix: cost center & naming series --- erpnext/controllers/accounts_controller.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 798b994920..f4af8932b6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1391,26 +1391,24 @@ class AccountsController(TransactionBase): jv = frappe.new_doc('Journal Entry') jv.voucher_type = 'Journal Entry' - jv.naming_series = 'ACC-JV-.YYYY.-' jv.posting_date = self.posting_date jv.company = self.company jv.remark = 'Adjustment for {} {}'.format(self.doctype, self.name) reconcilation_entry = frappe._dict() advance_entry = frappe._dict() - cost_center = erpnext.get_default_cost_center(self.company) reconcilation_entry.account = secondary_account reconcilation_entry.party_type = secondary_party_type reconcilation_entry.party = secondary_party reconcilation_entry.reference_type = self.doctype reconcilation_entry.reference_name = self.name - reconcilation_entry.cost_center = cost_center + reconcilation_entry.cost_center = self.cost_center advance_entry.account = primary_account advance_entry.party_type = primary_party_type advance_entry.party = primary_party - advance_entry.cost_center = cost_center + advance_entry.cost_center = self.cost_center advance_entry.is_advance = 'Yes' if self.doctype == 'Sales Invoice': From be7a38b662b82aac11a6a085f02eeffecd10872f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 21:35:32 +0530 Subject: [PATCH 10/10] fix: cost center in test_sales_invoice_against_supplier --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index b48dc924f7..83f0222026 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2185,7 +2185,7 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) # create a sales invoice - si = create_sales_invoice(customer=customer) + si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC") # check outstanding of sales invoice si.reload()