From 573d9094bbf70e032ce8c25e09ebe2c7b7c93e0c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Dec 2020 18:43:37 +0530 Subject: [PATCH] fix: Item valuation for internal stocktransfers --- .../purchase_invoice/purchase_invoice.py | 18 ++-- .../purchase_invoice_item.json | 21 +++-- .../sales_invoice/test_sales_invoice.py | 86 +++++++++---------- erpnext/controllers/buying_controller.py | 41 ++++++--- erpnext/controllers/selling_controller.py | 10 ++- erpnext/controllers/stock_controller.py | 4 +- .../purchase_receipt_item.json | 9 +- 7 files changed, 116 insertions(+), 73 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3ca277050e5..17c6ee29651 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -471,7 +471,7 @@ class PurchaseInvoice(BuyingController): else: self.stock_received_but_not_billed = None self.expenses_included_in_valuation = None - + self.negative_expense_to_be_booked = 0.0 gl_entries = [] @@ -485,7 +485,7 @@ class PurchaseInvoice(BuyingController): self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) - + gl_entries = merge_similar_entries(gl_entries) self.make_payment_gl_entries(gl_entries) @@ -508,7 +508,7 @@ class PurchaseInvoice(BuyingController): grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total if grand_total and not self.is_internal_transfer(): - # Didnot use base_grand_total to book rounding loss gle + # Did not 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( @@ -539,8 +539,8 @@ class PurchaseInvoice(BuyingController): voucher_wise_stock_value = {} if self.update_stock: for d in frappe.get_all('Stock Ledger Entry', - fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}): - voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference) + fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}): + voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) valuation_tax_accounts = [d.account_head for d in self.get("taxes") if d.category in ('Valuation', 'Total and Valuation') @@ -823,10 +823,10 @@ class PurchaseInvoice(BuyingController): # Stock ledger value is not matching with the warehouse amount if (self.update_stock and voucher_wise_stock_value.get(item.name) and - warehouse_debit_amount != flt(voucher_wise_stock_value.get(item.name), net_amt_precision)): + warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)): cost_of_goods_sold_account = self.get_company_default("default_expense_account") - stock_amount = flt(voucher_wise_stock_value.get(item.name), net_amt_precision) + stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) stock_adjustment_amt = warehouse_debit_amount - stock_amount gl_entries.append( @@ -1027,10 +1027,10 @@ class PurchaseInvoice(BuyingController): self.delete_auto_created_batches() self.make_gl_entries_on_cancel() - + if self.update_stock == 1: self.repost_future_sle_and_gle() - + self.update_project() frappe.db.set(self, 'status', 'Cancelled') diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index f6d76e50502..b41b376cca8 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -46,6 +47,7 @@ "column_break_25", "base_net_rate", "base_net_amount", + "outgoing_rate", "valuation_rate", "item_tax_amount", "landed_cost_voucher_amount", @@ -553,8 +555,8 @@ "fieldtype": "Link", "hidden": 1, "label": "Brand", - "print_hide": 1, - "options": "Brand" + "options": "Brand", + "print_hide": 1 }, { "fetch_from": "item_code.item_group", @@ -562,9 +564,9 @@ "fieldname": "item_group", "fieldtype": "Link", "label": "Item Group", + "options": "Item Group", "print_hide": 1, - "read_only": 1, - "options": "Item Group" + "read_only": 1 }, { "description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges", @@ -779,11 +781,18 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "outgoing_rate", + "fieldtype": "Currency", + "label": "Outgoing Rate", + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2020-08-20 11:48:01.398356", + "links": [], + "modified": "2020-12-23 17:30:57.458876", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -791,4 +800,4 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3cb972c341e..3398fc30834 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1770,59 +1770,59 @@ 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") + 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) + 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") + 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") + 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 = 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() + 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() + 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()] - # ] + 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)) + 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()] - # ] + 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)) + check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) def test_eway_bill_json(self): si = make_sales_invoice_for_ewaybill() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6edc020701d..83352272131 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -44,7 +44,6 @@ 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() @@ -67,6 +66,8 @@ class BuyingController(StockController): if self.doctype in ("Purchase Receipt", "Purchase Invoice"): self.update_valuation_rate() + self.set_out_going_rate() + def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) @@ -100,11 +101,6 @@ class BuyingController(StockController): 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"]] @@ -224,6 +220,32 @@ class BuyingController(StockController): else: item.valuation_rate = 0.0 + def set_out_going_rate(self): + if self.doctype not in ("Purchase Receipt", "Purchase Invoice"): + return + + items = self.get("items") + for d in items: + if not cint(self.get("is_return")) and d.get("target_warehouse"): + # Get outgoing rate based on original item cost based on valuation method + d.outgoing_rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.target_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * flt(d.qty), + "serial_no": d.serial_no, + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) + + elif self.get("return_against"): + # Get incoming rate of return entry from reference document + # based on original item cost as per valuation method + d.outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d) + def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True): supplied_items_cost = 0.0 for d in self.get("supplied_items"): @@ -243,7 +265,7 @@ class BuyingController(StockController): d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount")) supplied_items_cost += flt(d.amount) - + return supplied_items_cost def validate_for_subcontracting(self): @@ -559,6 +581,7 @@ class BuyingController(StockController): from_warehouse_sle = self.get_sl_entries(d, { "actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, + "outgoing_rate": d.outgoing_rate, "dependant_sle_voucher_detail_no": d.name }) @@ -569,10 +592,8 @@ class BuyingController(StockController): "serial_no": cstr(d.serial_no).strip() }) if self.is_return: - outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d) - sle.update({ - "outgoing_rate": outgoing_rate, + "outgoing_rate": d.outgoing_rate, "recalculate_rate": 1 }) if d.from_warehouse: diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 85cfb951fcc..eb314de49ca 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -191,7 +191,7 @@ class SellingController(StockController): for it in self.get("items"): if not it.item_code: continue - + last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1) if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom): @@ -331,6 +331,12 @@ class SellingController(StockController): "voucher_no": self.name, "allow_zero_valuation": d.get("allow_zero_valuation") }, raise_error_if_no_rate=False) + + # For internal transfers use incoming rate as the valuation rate + if self.get('is_internal_customer') and d.get('target_warehouse'): + d.rate = d.incoming_rate + frappe.msgprint(_("Row {0}: Item rate updated as the valuation rate since its an internal transfer").format(d.idx)) + elif self.get("return_against"): # Get incoming rate of return entry from reference document # based on original item cost as per valuation method @@ -391,7 +397,7 @@ class SellingController(StockController): }) if item_row.warehouse: sle.dependant_sle_voucher_detail_no = item_row.name - + return sle def set_po_nos(self, for_validate=False): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 439997616c7..787f0b330db 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -72,7 +72,7 @@ class StockController(AccountsController): warehouse_with_no_account = [] precision = frappe.get_precision("GL Entry", "debit_in_account_currency") for item_row in voucher_details: - sle_list = sle_map.get(item_row.name) + sle_list = sle_map.get((item_row.name, item_row.warehouse)) if sle_list: for sle in sle_list: if warehouse_account.get(sle.warehouse): @@ -216,7 +216,7 @@ class StockController(AccountsController): """, (self.doctype, self.name), as_dict=True) for sle in stock_ledger_entries: - stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) + stock_ledger.setdefault((sle.voucher_detail_no, sle.warehouse), []).append(sle) return stock_ledger def make_batches(self, warehouse_field): diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 871b255b06a..5de52a40516 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -56,6 +56,7 @@ "column_break_32", "base_net_rate", "base_net_amount", + "outgoing_rate", "valuation_rate", "item_tax_amount", "rm_supp_cost", @@ -861,12 +862,18 @@ "fieldtype": "Float", "label": "Received Qty in Stock UOM", "print_hide": 1 + }, + { + "fieldname": "outgoing_rate", + "fieldtype": "Currency", + "label": "Outgoing Rate", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-07 10:00:38.204294", + "modified": "2020-12-23 17:33:19.479325", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item",