From f17ea2ccabc5e42b4f3e9b3fe0373670109f75ad Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 11 Dec 2020 21:30:39 +0530 Subject: [PATCH] fix: Accounting for internal transfer invoices within same company (#24021) * fix: Accounting for internal transfer invoices within same company * fix: warehouse fetching * fix: Linting issues * fix: GL entry fixes and validation for intercompany account * fix: Account naming changes and other fixes * fix: Add test for internal transfer * fix: Test Case * fix: Add description for fields * fix: Commonfied code * fix: Map warehouse and serial no --- .../purchase_invoice/purchase_invoice.js | 10 ++ .../purchase_invoice/purchase_invoice.json | 32 ++++- .../purchase_invoice/purchase_invoice.py | 94 ++++++++------ .../purchase_invoice/purchase_invoice_list.js | 16 +-- .../doctype/sales_invoice/sales_invoice.js | 10 ++ .../doctype/sales_invoice/sales_invoice.json | 25 +++- .../doctype/sales_invoice/sales_invoice.py | 85 +++++++++---- .../sales_invoice/sales_invoice_list.js | 4 +- .../sales_invoice/test_sales_invoice.py | 115 +++++++++++++++++- erpnext/buying/doctype/supplier/supplier.py | 6 + erpnext/controllers/accounts_controller.py | 34 ++++++ erpnext/controllers/buying_controller.py | 21 +++- erpnext/controllers/stock_controller.py | 14 ++- erpnext/controllers/taxes_and_totals.py | 14 ++- .../public/js/controllers/taxes_and_totals.js | 11 +- erpnext/selling/doctype/customer/customer.py | 8 +- erpnext/setup/doctype/company/company.js | 3 +- erpnext/setup/doctype/company/company.json | 29 ++--- 18 files changed, 415 insertions(+), 116 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 1d41d0fa2a..7830cfd370 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -15,6 +15,16 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ return (doc.qty<=doc.received_qty) ? "green" : "orange"; }); } + + this.frm.set_query("unrealized_profit_loss_account", function() { + return { + filters: { + company: doc.company, + is_group: 0, + root_type: "Liability", + } + }; + }); }, onload: function() { this._super(); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 2df77a84c7..c64ffd878c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:39", @@ -127,6 +126,7 @@ "write_off_cost_center", "advances_section", "allocate_advances_automatically", + "adjust_advance_taxes", "get_advances", "advances", "payment_schedule_section", @@ -152,9 +152,11 @@ "is_opening", "against_expense_account", "column_break_63", + "unrealized_profit_loss_account", "status", "inter_company_invoice_reference", "is_internal_supplier", + "represents_company", "remarks", "subscription_section", "from_date", @@ -1223,7 +1225,7 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", - "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled", + "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1 }, { @@ -1330,13 +1332,37 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "default": "0", + "description": "Taxes paid while advance payment will be adjusted against this invoice", + "fieldname": "adjust_advance_taxes", + "fieldtype": "Check", + "label": "Adjust Advance Taxes" + }, + { + "depends_on": "eval:doc.is_internal_supplier", + "description": "Unrealized Profit / Loss account for intra-company transfers", + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.is_internal_supplier", + "description": "Company which internal supplier represents", + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:57:18.266978", + "modified": "2020-12-11 12:46:12.796378", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 8bd788890a..d94d261c6b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -206,8 +206,8 @@ class PurchaseInvoice(BuyingController): ["Purchase Receipt", "purchase_receipt", "pr_detail"] ]) - def validate_warehouse(self): - if self.update_stock: + def validate_warehouse(self, for_validate=True): + if self.update_stock and for_validate: for d in self.get('items'): if not d.warehouse: frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}"). @@ -233,7 +233,7 @@ class PurchaseInvoice(BuyingController): if self.update_stock: self.validate_item_code() - self.validate_warehouse() + self.validate_warehouse(for_validate) if auto_accounting_for_stock: warehouse_account = get_warehouse_account_map(self.company) @@ -449,6 +449,7 @@ class PurchaseInvoice(BuyingController): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) @@ -457,7 +458,6 @@ class PurchaseInvoice(BuyingController): self.make_payment_gl_entries(gl_entries) self.make_write_off_gl_entry(gl_entries) self.make_gle_for_rounding_adjustment(gl_entries) - return gl_entries def check_asset_cwip_enabled(self): @@ -474,31 +474,30 @@ class PurchaseInvoice(BuyingController): # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total - if grand_total: - # Didnot use base_grand_total to book rounding loss gle - grand_total_in_company_currency = flt(grand_total * self.conversion_rate, - self.precision("grand_total")) - gl_entries.append( - self.get_gl_dict({ - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "due_date": self.due_date, - "against": self.against_expense_account, - "credit": grand_total_in_company_currency, - "credit_in_account_currency": grand_total_in_company_currency \ - if self.party_account_currency==self.company_currency else grand_total, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "project": self.project, - "cost_center": self.cost_center - }, self.party_account_currency, item=self) - ) + if grand_total and not self.is_internal_transfer(): + # Didnot use base_grand_total to book rounding loss gle + grand_total_in_company_currency = flt(grand_total * self.conversion_rate, + self.precision("grand_total")) + gl_entries.append( + self.get_gl_dict({ + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "due_date": self.due_date, + "against": self.against_expense_account, + "credit": grand_total_in_company_currency, + "credit_in_account_currency": grand_total_in_company_currency \ + if self.party_account_currency==self.company_currency else grand_total, + "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, + "against_voucher_type": self.doctype, + "project": self.project, + "cost_center": self.cost_center + }, self.party_account_currency, item=self) + ) def make_item_gl_entries(self, gl_entries): # item gl entries stock_items = self.get_stock_items() - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") if self.update_stock and self.auto_accounting_for_stock: warehouse_account = get_warehouse_account_map(self.company) @@ -526,7 +525,6 @@ class PurchaseInvoice(BuyingController): item, voucher_wise_stock_value, account_currency) if item.from_warehouse: - gl_entries.append(self.get_gl_dict({ "account": warehouse_account[item.warehouse]['account'], "against": warehouse_account[item.from_warehouse]["account"], @@ -546,16 +544,18 @@ class PurchaseInvoice(BuyingController): "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")), }, warehouse_account[item.from_warehouse]["account_currency"], item=item)) - gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": flt(item.base_net_amount, item.precision("base_net_amount")), - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project - }, account_currency, item=item) - ) + # Do not book expense for transfer within same company transfer + if not self.is_internal_transfer(): + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "against": self.supplier, + "debit": flt(item.base_net_amount, item.precision("base_net_amount")), + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project + }, account_currency, item=item) + ) else: gl_entries.append( @@ -832,7 +832,8 @@ class PurchaseInvoice(BuyingController): }, account_currency, item=tax) ) # accumulate valuation tax - if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): + if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \ + and not self.is_internal_transfer(): if self.auto_accounting_for_stock and not tax.cost_center: frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) valuation_tax.setdefault(tax.name, 0) @@ -876,8 +877,19 @@ class PurchaseInvoice(BuyingController): "against": self.supplier, "credit": valuation_tax[tax.name], "remarks": self.remarks or "Accounting Entry for Stock" - }, item=tax) - ) + }, item=tax)) + + def make_internal_transfer_gl_entries(self, gl_entries): + if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): + account_currency = get_account_currency(self.unrealized_profit_loss_account) + gl_entries.append( + self.get_gl_dict({ + "account": self.unrealized_profit_loss_account, + "against": self.supplier, + "credit": flt(self.total_taxes_and_charges), + "credit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center + }, account_currency, item=self)) def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries @@ -1095,7 +1107,9 @@ class PurchaseInvoice(BuyingController): if self.docstatus == 2: status = "Cancelled" elif self.docstatus == 1: - if outstanding_amount > 0 and due_date < nowdate: + if self.is_internal_transfer(): + self.status = 'Internal Transfer' + elif outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" elif outstanding_amount > 0 and due_date >= nowdate: self.status = "Unpaid" diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index 86c2e408c0..8da7d6fe13 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -4,23 +4,25 @@ // render frappe.listview_settings['Purchase Invoice'] = { add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company", - "currency", "is_return", "release_date", "on_hold"], + "currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"], get_indicator: function(doc) { - if( (flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') { + if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') { return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"]; - } else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) { + } else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) { if(cint(doc.on_hold) && !doc.release_date) { return [__("On Hold"), "darkgrey"]; - } else if(cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) { + } else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) { return [__("Temporarily on Hold"), "darkgrey"]; - } else if(frappe.datetime.get_diff(doc.due_date) < 0) { + } else if (frappe.datetime.get_diff(doc.due_date) < 0) { return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"]; } else { return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"]; } - } else if(cint(doc.is_return)) { + } else if (cint(doc.is_return)) { return [__("Return"), "darkgrey", "is_return,=,Yes"]; - } else if(flt(doc.outstanding_amount)==0 && doc.docstatus==1) { + } else if (doc.company == doc.represents_company && doc.is_internal_supplier) { + return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"]; + } else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) { return [__("Paid"), "green", "outstanding_amount,=,0"]; } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 502e65ed8d..5efc32e11d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -580,6 +580,16 @@ frappe.ui.form.on('Sales Invoice', { }; }); + frm.set_query("unrealized_profit_loss_account", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + root_type: "Liability", + } + }; + }); + frm.custom_make_buttons = { 'Delivery Note': 'Delivery', 'Sales Invoice': 'Sales Return', diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 17fbe2def9..6799fb986a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:05", @@ -158,6 +157,7 @@ "more_information", "inter_company_invoice_reference", "is_internal_customer", + "represents_company", "customer_group", "campaign", "is_discounted", @@ -171,6 +171,7 @@ "c_form_applicable", "c_form_no", "column_break8", + "unrealized_profit_loss_account", "remarks", "sales_team_section_break", "sales_partner", @@ -1655,7 +1656,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled", + "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1, "read_only": 1 }, @@ -1950,13 +1951,31 @@ "fieldtype": "Data", "label": "Company Tax ID", "read_only": 1 + }, + { + "depends_on": "eval:doc.is_internal_customer", + "description": "Unrealized Profit / Loss account for intra-company transfers", + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.is_internal_customer", + "description": "Company which internal customer represents", + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:57:45.086303", + "modified": "2020-12-11 12:48:31.769958", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 81f425f868..ca6f22cc30 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -758,6 +758,7 @@ class SalesInvoice(SellingController): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_internal_transfer_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries) @@ -777,7 +778,7 @@ class SalesInvoice(SellingController): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total - if grand_total: + if grand_total and not self.is_internal_transfer(): # Didnot use base_grand_total to book rounding loss gle grand_total_in_company_currency = flt(grand_total * self.conversion_rate, self.precision("grand_total")) @@ -816,6 +817,18 @@ class SalesInvoice(SellingController): }, account_currency, item=tax) ) + def make_internal_transfer_gl_entries(self, gl_entries): + if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): + account_currency = get_account_currency(self.unrealized_profit_loss_account) + gl_entries.append( + self.get_gl_dict({ + "account": self.unrealized_profit_loss_account, + "against": self.customer, + "debit": flt(self.total_taxes_and_charges), + "debit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center + }, account_currency, item=self)) + def make_item_gl_entries(self, gl_entries): # income account gl entries for item in self.get("items"): @@ -838,22 +851,24 @@ class SalesInvoice(SellingController): asset.db_set("disposal_date", self.posting_date) asset.set_status("Sold" if self.docstatus==1 else None) else: - income_account = (item.income_account - if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) + # Do not book income for transfer within same company + if not self.is_internal_transfer(): + income_account = (item.income_account + if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) - account_currency = get_account_currency(income_account) - gl_entries.append( - self.get_gl_dict({ - "account": income_account, - "against": self.customer, - "credit": flt(item.base_net_amount, item.precision("base_net_amount")), - "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount")) - if account_currency==self.company_currency - else flt(item.net_amount, item.precision("net_amount"))), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) + account_currency = get_account_currency(income_account) + gl_entries.append( + self.get_gl_dict({ + "account": income_account, + "against": self.customer, + "credit": flt(item.base_net_amount, item.precision("base_net_amount")), + "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount")) + if account_currency==self.company_currency + else flt(item.net_amount, item.precision("net_amount"))), + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) # expense account gl entries if cint(self.update_stock) and \ @@ -1265,7 +1280,9 @@ class SalesInvoice(SellingController): if self.docstatus == 2: status = "Cancelled" elif self.docstatus == 1: - if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed': + if self.is_internal_transfer(): + self.status = 'Internal Transfer' + elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed': self.status = "Overdue and Discounted" elif outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" @@ -1530,9 +1547,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if doctype in ["Sales Invoice", "Sales Order"]: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" + source_document_warehouse_field = 'target_warehouse' + target_document_warehouse_field = 'from_warehouse' else: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order" + source_document_warehouse_field = 'from_warehouse' + target_document_warehouse_field = 'target_warehouse' validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -1559,6 +1580,26 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if currency: target_doc.currency = currency + item_field_map = { + "doctype": target_doctype + " Item", + "field_no_map": [ + "income_account", + "expense_account", + "cost_center", + "warehouse" + ] + } + + if source_doc.get('update_stock'): + item_field_map.update({ + 'field_map': { + source_document_warehouse_field: target_document_warehouse_field, + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' + } + }) + + doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, @@ -1567,15 +1608,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "taxes_and_charges" ] }, - doctype +" Item": { - "doctype": target_doctype + " Item", - "field_no_map": [ - "income_account", - "expense_account", - "cost_center", - "warehouse" - ] - } + doctype +" Item": item_field_map }, target_doc, set_missing_values) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index 05d49df711..41140d1938 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -14,8 +14,8 @@ frappe.listview_settings['Sales Invoice'] = { "Credit Note Issued": "darkgrey", "Unpaid and Discounted": "orange", "Overdue and Discounted": "red", - "Overdue": "red" - + "Overdue": "red", + "Internal Transfer": "darkgrey" }; return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; }, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 46e954d948..22a4f33654 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1573,7 +1573,7 @@ class TestSalesInvoice(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - + def test_sales_invoice_with_project_link(self): from erpnext.projects.doctype.project.test_project import make_project @@ -1607,9 +1607,9 @@ class TestSalesInvoice(unittest.TestCase): debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s order by account asc""", sales_invoice.name, as_dict=1) - + self.assertTrue(gl_entries) - + for gle in gl_entries: self.assertEqual(expected_values[gle.account]["project"], gle.project) @@ -1781,6 +1781,60 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") + def test_internal_transfer_gl_entry(self): + ## Create internal transfer account + account = create_account(account_name="Unrealized Profit", + parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") + + frappe.db.set_value('Company', '_Test Company with perpetual inventory', + 'unrealized_profit_loss_account', account) + + customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") + + create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") + + si = create_sales_invoice( + company = "_Test Company with perpetual inventory", + customer = customer, + debit_to = "Debtors - TCP1", + warehouse = "Stores - TCP1", + income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", + currency = "INR", + do_not_save = 1 + ) + + si.selling_price_list = "_Test Price List Rest of the World" + si.update_stock = 1 + si.items[0].target_warehouse = 'Work In Progress - TCP1' + add_taxes(si) + si.save() + si.submit() + + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + target_doc.company = '_Test Company with perpetual inventory' + target_doc.items[0].warehouse = 'Finished Goods - TCP1' + add_taxes(target_doc) + target_doc.save() + target_doc.submit() + + si_gl_entries = [ + ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()], + ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()] + ] + + check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + + pi_gl_entries = [ + ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], + ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] + ] + + check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) + def test_eway_bill_json(self): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): address = frappe.get_doc({ @@ -2039,4 +2093,57 @@ def get_taxes_and_charges(): "parentfield": "taxes", "rate": 2, "row_id": 1 - }] \ No newline at end of file + }] + +def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": represents_company + }) + + customer.append("companies", { + "company": allowed_to_interact_with + }) + + customer.insert() + customer_name = customer.name + else: + customer_name = frappe.db.get_value("Customer", customer_name) + + return customer_name + +def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.get_doc({ + "supplier_group": "_Test Supplier Group", + "supplier_name": supplier_name, + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": represents_company + }) + + supplier.append("companies", { + "company": allowed_to_interact_with + }) + + supplier.insert() + supplier_name = supplier.name + else: + supplier_name = frappe.db.exists("Supplier", supplier_name) + + return supplier_name + +def add_taxes(doc): + doc.append('taxes', { + 'account_head': '_Test Account Excise Duty - TCP1', + "charge_type": "On Net Total", + "cost_center": "Main - TCP1", + "description": "Excise Duty", + "rate": 12 + }) \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index df143eefa0..0ee9d180d9 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -49,6 +49,12 @@ class Supplier(TransactionBase): msgprint(_("Series is mandatory"), raise_exception=1) validate_party_accounts(self) + self.validate_internal_supplier() + + def validate_internal_supplier(self): + if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"): + frappe.throw(_("Internal Supplier for company {0} already exists").format( + frappe.bold(self.represents_company))) def on_trash(self): delete_contact_and_address('Supplier', self.name) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 93a79ec934..32c5d3a3b1 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -107,6 +107,8 @@ class AccountsController(TransactionBase): else: self.validate_deferred_start_and_end_date() + self.set_inter_company_account() + validate_regional(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) @@ -932,6 +934,38 @@ class AccountsController(TransactionBase): else: return frappe.db.get_single_value("Global Defaults", "disable_rounded_total") + def set_inter_company_account(self): + """ + Set intercompany account for inter warehouse transactions + This account will be used in case billing company and internal customer's + representation company is same + """ + + if self.is_internal_transfer() and not self.unrealized_profit_loss_account: + unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account') + + if not unrealized_profit_loss_account: + msg = _("Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}").format( + frappe.bold(self.company)) + frappe.throw(msg) + + self.unrealized_profit_loss_account = unrealized_profit_loss_account + + def is_internal_transfer(self): + """ + It will an internal transfer if its an internal customer and representation + company is same as billing company + """ + if self.doctype == 'Sales Invoice': + internal_party_field = 'is_internal_customer' + else: + internal_party_field = 'is_internal_supplier' + + if self.get(internal_party_field) and (self.represents_company == self.company): + return True + + return False + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 5fabf7017b..286c4f4451 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -42,6 +42,7 @@ class BuyingController(StockController): self.validate_items() self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() + self.update_tax_category_for_internal_transfer() self.validate_warehouse() self.validate_from_warehouse() self.set_supplier_address() @@ -94,13 +95,23 @@ class BuyingController(StockController): def validate_stock_or_nonstock_items(self): if self.meta.get_field("taxes") and not self.get_stock_items() and not self.get_asset_items(): - tax_for_valuation = [d for d in self.get("taxes") + msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items') + self.update_tax_category(msg) + + def update_tax_category_for_internal_transfer(self): + if self.doctype == 'Purchase Invoice' and self.is_internal_transfer(): + msg = _('Tax Category has been changed to "Total" as its an internal purchase.') + self.update_tax_category(msg) + + def update_tax_category(self, msg): + tax_for_valuation = [d for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"]] - if tax_for_valuation: - for d in tax_for_valuation: - d.category = 'Total' - msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items')) + if tax_for_valuation: + for d in tax_for_valuation: + d.category = 'Total' + + msgprint(msg) def validate_asset_return(self): if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2f7b361b39..683d7f77b5 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -77,7 +77,7 @@ class StockController(AccountsController): if sle_list: for sle in sle_list: if warehouse_account.get(sle.warehouse): - # from warehouse account/ target warehouse account + # from warehouse account self.check_expense_account(item_row) @@ -92,9 +92,16 @@ class StockController(AccountsController): sle = self.update_stock_ledger_entries(sle) + # expense account/ target_warehouse / source_warehouse + if item_row.get('target_warehouse'): + warehouse = item_row.get('target_warehouse') + expense_account = warehouse_account[warehouse]["account"] + else: + expense_account = item_row.expense_account + gl_list.append(self.get_gl_dict({ "account": warehouse_account[sle.warehouse]["account"], - "against": item_row.expense_account, + "against": expense_account, "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), "remarks": self.get("remarks") or "Accounting Entry for Stock", @@ -102,9 +109,8 @@ class StockController(AccountsController): "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) - # expense account gl_list.append(self.get_gl_dict({ - "account": item_row.expense_account, + "account": expense_account, "against": warehouse_account[sle.warehouse]["account"], "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index ad58f137ee..8dd2e5bacb 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -519,6 +519,17 @@ class calculate_taxes_and_totals(object): if self.doc.docstatus == 0: self.calculate_outstanding_amount() + def is_internal_invoice(self): + """ + Checks if its an internal transfer invoice + and decides if to calculate any out standing amount or not + """ + + if self.doc.doctype in ('Sales Invoice', 'Purchase Invoice') and self.doc.is_internal_transfer(): + return True + + return False + def calculate_outstanding_amount(self): # NOTE: # write_off_amount is only for POS Invoice @@ -526,7 +537,8 @@ class calculate_taxes_and_totals(object): if self.doc.doctype == "Sales Invoice": self.calculate_paid_amount() - if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return + if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos') or \ + self.is_internal_invoice(): return self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"]) self._set_in_company_currency(self.doc, ['write_off_amount']) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 99f3995a66..22e75780b8 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -609,6 +609,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.calculate_outstanding_amount(update_paid_amount); }, + is_internal_invoice: function() { + if (['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) { + if (this.frm.doc.company === this.frm.doc.represents_company) { + return true; + } + } + return false; + }, + calculate_outstanding_amount: function(update_paid_amount) { // NOTE: // paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice @@ -617,7 +626,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.calculate_paid_amount(); } - if(this.frm.doc.is_return || this.frm.doc.docstatus > 0) return; + if (this.frm.doc.is_return || (this.frm.doc.docstatus > 0) || this.is_internal_invoice()) return; frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]); diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 0172d9c128..29214ee06d 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -58,6 +58,7 @@ class Customer(TransactionBase): self.set_loyalty_program() self.check_customer_group_change() self.validate_default_bank_account() + self.validate_internal_customer() # set loyalty program tier if frappe.db.exists('Customer', self.name): @@ -82,6 +83,11 @@ class Customer(TransactionBase): if not is_company_account: frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) + def validate_internal_customer(self): + if self.is_internal_customer and frappe.db.get_value('Customer', {"represents_company": self.represents_company}, "name"): + frappe.throw(_("Internal Customer for company {0} already exists").format( + frappe.bold(self.represents_company))) + def on_update(self): self.validate_name_with_customer_group() self.create_primary_contact() @@ -398,7 +404,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, # form a list of emails and names to show to the user credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] if not credit_controller_users_formatted: - frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) + frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer)) message = """Please contact any of the following users to extend the credit limits for {0}:

""".format(customer, '
  • '.join(credit_controller_users_formatted)) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index cbf67b4cd6..36033d9dae 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -274,7 +274,8 @@ erpnext.company.setup_queries = function(frm) { ["default_employee_advance_account", {"root_type": "Asset"}], ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], - ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}] + ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], + ["unrealized_profit_loss_account", {"root_type": "Liability"}] ], function(i, v) { erpnext.company.set_custom_query(frm, v); }); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 40938ea0a5..d49ae7ce8a 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -46,10 +46,9 @@ "round_off_account", "round_off_cost_center", "write_off_account", - "discount_allowed_account", - "discount_received_account", "exchange_gain_loss_account", "unrealized_exchange_gain_loss_account", + "unrealized_profit_loss_account", "column_break0", "allow_account_creation_against_child_company", "default_payable_account", @@ -261,14 +260,14 @@ { "fieldname": "create_chart_of_accounts_based_on", "fieldtype": "Select", - "label": "Create Chart of Accounts Based on", + "label": "Create Chart Of Accounts Based On", "options": "\nStandard Template\nExisting Company" }, { "depends_on": "eval:doc.create_chart_of_accounts_based_on===\"Standard Template\"", "fieldname": "chart_of_accounts", "fieldtype": "Select", - "label": "Chart of Accounts Template", + "label": "Chart Of Accounts Template", "no_copy": 1 }, { @@ -345,18 +344,6 @@ "label": "Write Off Account", "options": "Account" }, - { - "fieldname": "discount_allowed_account", - "fieldtype": "Link", - "label": "Discount Allowed Account", - "options": "Account" - }, - { - "fieldname": "discount_received_account", - "fieldtype": "Link", - "label": "Discount Received Account", - "options": "Account" - }, { "fieldname": "exchange_gain_loss_account", "fieldtype": "Link", @@ -740,6 +727,12 @@ "fieldtype": "Link", "label": "Default In Transit Warehouse", "options": "Warehouse" + }, + { + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" } ], "icon": "fa fa-building", @@ -747,7 +740,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2020-08-06 00:38:08.311216", + "modified": "2020-12-03 12:27:27.085094", "modified_by": "Administrator", "module": "Setup", "name": "Company", @@ -808,4 +801,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} +} \ No newline at end of file