From 14aa9c5320d46b8c1868eab3d527f27cafb8d32f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 18 Apr 2016 15:54:01 +0530 Subject: [PATCH] [fix] Cleanup and fixes in update stock feature in Purchase Invoice --- .../purchase_invoice/purchase_invoice.js | 42 +++++- .../purchase_invoice/purchase_invoice.py | 137 ++++++++++++------ .../purchase_invoice/test_purchase_invoice.py | 39 ++++- .../purchase_invoice/test_records.json | 6 +- .../doctype/sales_invoice/sales_invoice.py | 27 ++-- .../purchase_order/test_purchase_order.py | 26 ++++ erpnext/controllers/accounts_controller.py | 18 +-- erpnext/controllers/buying_controller.py | 35 ++++- erpnext/controllers/selling_controller.py | 74 ++++++++++ erpnext/controllers/stock_controller.py | 75 ---------- erpnext/patches.txt | 2 +- .../doctype/sales_order/test_sales_order.py | 28 ++++ .../doctype/delivery_note/delivery_note.py | 4 + .../test_landed_cost_voucher.py | 92 ++++++++---- .../purchase_receipt/purchase_receipt.js | 14 ++ .../purchase_receipt/purchase_receipt.py | 45 ++---- 16 files changed, 429 insertions(+), 235 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 6d137a5e12..5af3e82efc 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -15,30 +15,30 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ this.frm.set_df_property("credit_to", "print_hide", 0); } } - - hide_fields(this.frm.doc); }, refresh: function(doc) { this._super(); + + hide_fields(this.frm.doc); // Show / Hide button this.show_general_ledger(); + + if(doc.update_stock==1 && doc.docstatus==1) { + this.show_stock_ledger(); + } if(!doc.is_return && doc.docstatus==1) { if(doc.outstanding_amount > 0) { this.frm.add_custom_button(__('Payment'), this.make_bank_entry, __("Make")); cur_frm.page.set_inner_btn_group_as_primary(__("Make")); } - + if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) { cur_frm.add_custom_button(doc.update_stock ? __('Purchase Return') : __('Debit Note'), this.make_debit_note, __("Make")); } - - if(doc.update_stock==1) { - this.show_stock_ledger(); - } } if(doc.docstatus===0) { @@ -69,6 +69,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }) }, __("Get items from")); } + + this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes"); }, supplier: function() { @@ -326,3 +328,29 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){ else cur_frm.pformat.print_heading = __("Purchase Invoice"); } + +frappe.ui.form.on("Purchase Invoice", { + onload: function(frm) { + $.each(["warehouse", "rejected_warehouse"], function(i, field) { + frm.set_query(field, "items", function() { + return { + filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]] + } + }) + }) + + frm.set_query("supplier_warehouse", function() { + return { + filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]] + } + }) + }, + + is_subcontracted: function(frm) { + if (frm.doc.is_subcontracted === "Yes") { + erpnext.buying.get_default_bom(frm); + } + frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted==="Yes"); + } +}) + \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 81d386717e..43d14edbb3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import cint, formatdate, flt, getdate -from frappe import msgprint, _, throw +from frappe import _, throw from erpnext.setup.utils import get_company_currency import frappe.defaults @@ -13,7 +13,8 @@ from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.utils import get_account_currency, get_fiscal_year from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billed_amount_based_on_po from erpnext.controllers.stock_controller import get_warehouse_account - +from erpnext.accounts.general_ledger import make_gl_entries, merge_similar_entries, delete_gl_entries +from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -51,7 +52,6 @@ class PurchaseInvoice(BuyingController): if (self.is_paid == 1): self.validate_cash() - self.check_active_purchase_items() self.check_conversion_rate() self.validate_credit_to_acc() self.clear_unallocated_advances("Purchase Invoice Advance", "advances") @@ -61,7 +61,6 @@ class PurchaseInvoice(BuyingController): self.set_expense_account() self.set_against_expense_account() self.validate_write_off_account() - self.update_valuation_rate("items") self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") self.validate_fixed_asset_account() self.create_remarks() @@ -177,7 +176,6 @@ class PurchaseInvoice(BuyingController): else: item.expense_account = stock_not_billed_account - item.cost_center = None elif not item.expense_account: throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) @@ -281,35 +279,38 @@ class PurchaseInvoice(BuyingController): } ]) - def validate_purchase_receipt(self): - for item in self.get("items"): - if item.purchase_receipt: - frappe.throw(_("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt)) + def validate_purchase_receipt_if_update_stock(self): + if self.update_stock: + for item in self.get("items"): + if item.purchase_receipt: + frappe.throw(_("Stock cannot be updated against Purchase Receipt {0}") + .format(item.purchase_receipt)) def on_submit(self): self.check_prev_docstatus() + self.update_status_updater_args() + self.validate_asset() frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total) - - if (self.update_stock == 1): - # from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_stock_ledger - self.update_stock_ledger() - from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit - update_serial_nos_after_submit(self, "items") - self.update_status_updater_args() - self.update_prevdoc_status() - - # this sequence because outstanding may get -negative - self.make_gl_entries() - + if not self.is_return: self.update_against_document_in_jv() self.update_prevdoc_status() self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.update_billing_status_in_pr() + # Updating stock ledger should always be called after updating prevdoc status, + # because updating ordered qty in bin depends upon updated ordered qty in PO + if self.update_stock == 1: + self.update_stock_ledger() + from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") + + # this sequence because outstanding may get -negative + self.make_gl_entries() + self.update_project() def validate_asset(self): @@ -353,38 +354,32 @@ class PurchaseInvoice(BuyingController): self.make_item_gl_entries(gl_entries) self.make_tax_gl_entries(gl_entries) - from erpnext.accounts.general_ledger import merge_similar_entries gl_entries = merge_similar_entries(gl_entries) self.make_payment_gl_entries(gl_entries) self.make_write_off_gl_entry(gl_entries) + if gl_entries: - from erpnext.accounts.general_ledger import make_gl_entries update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding=update_outstanding, merge_entries=False) if update_outstanding == "No": - from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt update_outstanding_amt(self.credit_to, "Supplier", self.supplier, self.doctype, self.return_against if cint(self.is_return) else self.name) - if repost_future_gle and cint(self.update_stock) \ - and cint(frappe.defaults.get_global_default("auto_accounting_for_stock")): - from erpnext.controllers.stock_controller import update_gl_entries_after - items, warehouses = self.get_items_and_warehouses() - update_gl_entries_after(self.posting_date, self.posting_time, warehouses, items) + if repost_future_gle and cint(self.update_stock) and self.auto_accounting_for_stock: + from erpnext.controllers.stock_controller import update_gl_entries_after + items, warehouses = self.get_items_and_warehouses() + update_gl_entries_after(self.posting_date, self.posting_time, warehouses, items) - elif self.docstatus == 2 and cint(self.update_stock) \ - and cint(frappe.defaults.get_global_default("auto_accounting_for_stock")): - from erpnext.accounts.general_ledger import delete_gl_entries - delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: + delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name) def make_supplier_gl_entry(self, gl_entries): - # parent's gl entry if self.grand_total: # Didnot use base_grand_total to book rounding loss gle grand_total_in_company_currency = flt(self.grand_total * self.conversion_rate, @@ -406,21 +401,62 @@ class PurchaseInvoice(BuyingController): 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") + warehouse_account = get_warehouse_account() for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) - gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": item.base_net_amount, - "debit_in_account_currency": item.base_net_amount \ - if account_currency==self.company_currency else item.net_amount, - "cost_center": item.cost_center - }, account_currency) - ) + if self.update_stock and self.auto_accounting_for_stock: + val_rate_db_precision = 6 if cint(item.precision("valuation_rate")) <= 6 else 9 + + # warehouse account + warehouse_debit_amount = flt(flt(item.valuation_rate, val_rate_db_precision) + * flt(item.qty) * flt(item.conversion_factor), item.precision("base_net_amount")) + + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "against": self.supplier, + "debit": warehouse_debit_amount, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center + }, account_currency) + ) + + # Amount added through landed-cost-voucher + if flt(item.landed_cost_voucher_amount): + gl_entries.append(self.get_gl_dict({ + "account": expenses_included_in_valuation, + "against": item.expense_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.landed_cost_voucher_amount) + })) + + # sub-contracting warehouse + if flt(item.rm_supp_cost): + supplier_warehouse_account = warehouse_account[self.supplier_warehouse]["name"] + gl_entries.append(self.get_gl_dict({ + "account": supplier_warehouse_account, + "against": item.expense_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.rm_supp_cost) + }, warehouse_account[self.supplier_warehouse]["account_currency"])) + else: + 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")), + "debit_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 + }, account_currency) + ) if self.auto_accounting_for_stock and self.is_opening == "No" and \ item.item_code in stock_items and item.item_tax_amount: @@ -435,13 +471,13 @@ class PurchaseInvoice(BuyingController): self.get_gl_dict({ "account": self.stock_received_but_not_billed, "against": self.supplier, - "debit": flt(item.item_tax_amount, self.precision("item_tax_amount", item)), + "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), "remarks": self.remarks or "Accounting Entry for Stock" }) ) self.negative_expense_to_be_booked += flt(item.item_tax_amount, \ - self.precision("item_tax_amount", item)) + item.precision("item_tax_amount")) def make_tax_gl_entries(self, gl_entries): # tax table gl entries @@ -558,7 +594,9 @@ class PurchaseInvoice(BuyingController): def on_cancel(self): self.check_for_closed_status() - + + self.update_status_updater_args() + if not self.is_return: from erpnext.accounts.utils import remove_against_link_from_jv remove_against_link_from_jv(self.doctype, self.name) @@ -567,6 +605,11 @@ class PurchaseInvoice(BuyingController): self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.update_billing_status_in_pr() + # Updating stock ledger should always be called after updating prevdoc status, + # because updating ordered qty in bin depends upon updated ordered qty in PO + if self.update_stock == 1: + self.update_stock_ledger() + self.make_gl_entries_on_cancel() self.update_project() self.validate_asset() diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index c0691744a6..9afcd21b90 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import unittest import frappe import frappe.model -from frappe.utils import cint +from frappe.utils import cint, flt import frappe.defaults from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \ test_records as pr_test_records @@ -120,20 +120,20 @@ class TestPurchaseInvoice(unittest.TestCase): set_perpetual_inventory(0) def test_purchase_invoice_calculation(self): - wrapper = frappe.copy_doc(test_records[0]) - wrapper.insert() - wrapper.load_from_db() + pi = frappe.copy_doc(test_records[0]) + pi.insert() + pi.load_from_db() expected_values = [ ["_Test Item Home Desktop 100", 90, 59], ["_Test Item Home Desktop 200", 135, 177] ] - for i, item in enumerate(wrapper.get("items")): + for i, item in enumerate(pi.get("items")): self.assertEqual(item.item_code, expected_values[i][0]) self.assertEqual(item.item_tax_amount, expected_values[i][1]) self.assertEqual(item.valuation_rate, expected_values[i][2]) - self.assertEqual(wrapper.base_net_total, 1250) + self.assertEqual(pi.base_net_total, 1250) # tax amounts expected_values = [ @@ -147,7 +147,7 @@ class TestPurchaseInvoice(unittest.TestCase): ["_Test Account Discount - _TC", 168.03, 1512.30], ] - for i, tax in enumerate(wrapper.get("taxes")): + for i, tax in enumerate(pi.get("taxes")): self.assertEqual(tax.account_head, expected_values[i][0]) self.assertEqual(tax.tax_amount, expected_values[i][1]) self.assertEqual(tax.total, expected_values[i][2]) @@ -375,8 +375,28 @@ class TestPurchaseInvoice(unittest.TestCase): pi1 = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, rate=50, update_stock=1) actual_qty_2 = get_qty_after_transaction() - self.assertEquals(actual_qty_1 - 2, actual_qty_2) + + pi1.cancel() + self.assertEquals(actual_qty_1, get_qty_after_transaction()) + + pi.cancel() + self.assertEquals(actual_qty_0, get_qty_after_transaction()) + + def test_subcontracting_via_purchase_invoice(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) + make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC", + qty=100, basic_rate=100) + + pi = make_purchase_invoice(item_code="_Test FG Item", qty=10, rate=500, + update_stock=1, is_subcontracted="Yes") + + self.assertEquals(len(pi.get("supplied_items")), 2) + + rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")]) + self.assertEquals(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) def make_purchase_invoice(**args): pi = frappe.new_doc("Purchase Invoice") @@ -389,6 +409,7 @@ def make_purchase_invoice(**args): pi.update_stock = 1 if args.is_paid: pi.is_paid = 1 + if args.cash_bank_account: pi.cash_bank_account=args.cash_bank_account @@ -398,6 +419,8 @@ def make_purchase_invoice(**args): pi.conversion_rate = args.conversion_rate or 1 pi.is_return = args.is_return pi.return_against = args.return_against + pi.is_subcontracted = args.is_subcontracted + pi.supplier_warehouse = "_Test Warehouse 1 - _TC" pi.append("items", { "item_code": args.item or args.item_code or "_Test Item", diff --git a/erpnext/accounts/doctype/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json index 4218828470..7feca2380f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_records.json +++ b/erpnext/accounts/doctype/purchase_invoice/test_records.json @@ -22,7 +22,8 @@ "parentfield": "items", "qty": 10, "rate": 50, - "uom": "_Test UOM" + "uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC" }, { "amount": 750, @@ -37,7 +38,8 @@ "parentfield": "items", "qty": 5, "rate": 150, - "uom": "_Test UOM" + "uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC" } ], "grand_total": 0, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 265c38b91a..53747bfc82 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -81,14 +81,10 @@ class SalesInvoice(SellingController): self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items") self.update_packing_list() - def on_submit(self): - if cint(self.update_stock) == 1: - self.update_stock_ledger() - else: - # Check for Approving Authority - if not self.recurring_id: - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total, self) + def on_submit(self): + if not self.recurring_id: + frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, + self.company, self.base_grand_total, self) self.check_prev_docstatus() @@ -99,7 +95,12 @@ class SalesInvoice(SellingController): self.update_status_updater_args() self.update_prevdoc_status() self.update_billing_status_in_dn() - + + # Updating stock ledger should always be called after updating prevdoc status, + # because updating reserved qty in bin depends upon updated delivered qty in SO + if self.update_stock == 1: + self.update_stock_ledger() + # this sequence because outstanding may get -ve self.make_gl_entries() @@ -117,9 +118,6 @@ class SalesInvoice(SellingController): self.update_time_log_batch(None) def on_cancel(self): - if cint(self.update_stock) == 1: - self.update_stock_ledger() - self.check_close_sales_order("sales_order") from erpnext.accounts.utils import remove_against_link_from_jv @@ -137,6 +135,11 @@ class SalesInvoice(SellingController): self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.validate_c_form_on_cancel() + + # Updating stock ledger should always be called after updating prevdoc status, + # because updating reserved qty in bin depends upon updated delivered qty in SO + if self.update_stock == 1: + self.update_stock_ledger() self.make_gl_entries_on_cancel() diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 94dd07067d..c24bcdcd92 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -45,6 +45,32 @@ class TestPurchaseOrder(unittest.TestCase): po.load_from_db() self.assertEquals(po.get("items")[0].received_qty, 4) + + def test_ordered_qty_against_pi_with_update_stock(self): + existing_ordered_qty = get_ordered_qty() + + po = create_purchase_order() + + self.assertEqual(get_ordered_qty(), existing_ordered_qty + 10) + + frappe.db.set_value('Item', '_Test Item', 'tolerance', 50) + + pi = make_purchase_invoice(po.name) + pi.update_stock = 1 + pi.items[0].qty = 12 + pi.insert() + pi.submit() + + self.assertEqual(get_ordered_qty(), existing_ordered_qty) + + po.load_from_db() + self.assertEquals(po.get("items")[0].received_qty, 12) + + pi.cancel() + self.assertEqual(get_ordered_qty(), existing_ordered_qty + 10) + + po.load_from_db() + self.assertEquals(po.get("items")[0].received_qty, 0) def test_make_purchase_invoice(self): po = create_purchase_order(do_not_submit=True) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7691ff5b5f..594a576a79 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -146,14 +146,6 @@ class AccountsController(TransactionBase): """set missing item values""" from erpnext.stock.get_item_details import get_item_details - if self.doctype == "Purchase Invoice": - auto_accounting_for_stock = cint(frappe.defaults.get_global_default("auto_accounting_for_stock")) - - if auto_accounting_for_stock: - stock_not_billed_account = self.get_company_default("stock_received_but_not_billed") - - stock_items = self.get_stock_items() - if hasattr(self, "items"): parent_dict = {} for fieldname in self.meta.get_valid_columns(): @@ -200,14 +192,8 @@ class AccountsController(TransactionBase): item.rate = flt(item.price_list_rate * (1.0 - (flt(item.discount_percentage) / 100.0)), item.precision("rate")) - if self.doctype == "Purchase Invoice": - if auto_accounting_for_stock and item.item_code in stock_items \ - and self.is_opening == 'No' \ - and (not item.po_detail or not frappe.db.get_value("Purchase Order Item", - item.po_detail, "delivered_by_supplier")): - - item.expense_account = stock_not_billed_account - item.cost_center = None + if self.doctype == "Purchase Invoice": + self.set_expense_account() def set_taxes(self): if not self.meta.get_field("taxes"): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 9c9f1c3ac8..74a3d696e0 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -33,10 +33,10 @@ class BuyingController(StockController): self.validate_stock_or_nonstock_items() self.validate_warehouse() - if self.doctype=="Purchase Invoice" and getattr(self, "update_stock"): - self.validate_purchase_receipt() + if self.doctype=="Purchase Invoice": + self.validate_purchase_receipt_if_update_stock() - if self.doctype=="Purchase Receipt" or (self.doctype=="Purchase Invoice" and getattr(self, "update_stock")): + if self.doctype=="Purchase Receipt" or (self.doctype=="Purchase Invoice" and self.update_stock): self.validate_purchase_return() self.validate_rejected_warehouse() self.validate_accepted_rejected_qty() @@ -48,11 +48,13 @@ class BuyingController(StockController): self.validate_for_subcontracting() self.create_raw_materials_supplied("supplied_items") self.set_landed_cost_voucher_amount() + + if self.doctype in ("Purchase Receipt", "Purchase Invoice"): self.update_valuation_rate("items") def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) - + self.set_supplier_from_item_default() self.set_price_list_currency("Buying") @@ -118,7 +120,6 @@ class BuyingController(StockController): if item.item_code and item.qty and item.item_code in stock_items: item_proportion = flt(item.base_net_amount) / stock_items_amount if stock_items_amount \ else flt(item.qty) / stock_items_qty - if i == (last_stock_item_idx - 1): item.item_tax_amount = flt(valuation_amount_adjustment, self.precision("item_tax_amount", item)) @@ -229,7 +230,7 @@ class BuyingController(StockController): rm.amount = required_qty * flt(rm.rate) raw_materials_cost += flt(rm.amount) - if self.doctype == "Purchase Receipt": + if self.doctype in ("Purchase Receipt", "Purchase Invoice"): item.rm_supp_cost = raw_materials_cost def cleanup_raw_materials_supplied(self, parent_items, raw_material_table): @@ -319,6 +320,8 @@ class BuyingController(StockController): frappe.throw(_("Accepted + Rejected Qty must be equal to Received quantity for Item {0}").format(d.item_code)) def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False): + self.update_ordered_qty() + sl_entries = [] stock_items = self.get_stock_items() @@ -354,6 +357,26 @@ class BuyingController(StockController): self.make_sl_entries_for_supplier_warehouse(sl_entries) self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + + def update_ordered_qty(self): + po_map = {} + for d in self.get("items"): + if self.doctype=="Purchase Receipt" \ + and d.prevdoc_doctype=="Purchase Order" and d.prevdoc_detail_docname: + po_map.setdefault(d.prevdoc_docname, []).append(d.prevdoc_detail_docname) + + elif self.doctype=="Purchase Invoice" and d.purchase_order and d.po_detail: + po_map.setdefault(d.purchase_order, []).append(d.po_detail) + + for po, po_item_rows in po_map.items(): + if po and po_item_rows: + po_obj = frappe.get_doc("Purchase Order", po) + + if po_obj.status in ["Closed", "Cancelled"]: + frappe.throw(_("{0} {1} is cancelled or closed").format(_("Purchase Order"), po), + frappe.InvalidStatusError) + + po_obj.update_ordered_qty(po_item_rows) def make_sl_entries_for_supplier_warehouse(self, sl_entries): if hasattr(self, 'supplied_items'): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 3c9efa10d7..b9b94f512a 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -7,6 +7,7 @@ from frappe.utils import cint, flt, cstr, comma_or from erpnext.setup.utils import get_company_currency from frappe import _, throw from erpnext.stock.get_item_details import get_bin_details +from erpnext.stock.utils import get_incoming_rate from erpnext.controllers.stock_controller import StockController @@ -228,6 +229,79 @@ class SellingController(StockController): status = frappe.db.get_value("Sales Order", d.get(ref_fieldname), "status") if status == "Closed": frappe.throw(_("Sales Order {0} is {1}").format(d.get(ref_fieldname), status)) + + def update_reserved_qty(self): + so_map = {} + for d in self.get("items"): + if d.so_detail: + if self.doctype == "Delivery Note" and d.against_sales_order: + so_map.setdefault(d.against_sales_order, []).append(d.so_detail) + elif self.doctype == "Sales Invoice" and d.sales_order and self.update_stock: + so_map.setdefault(d.sales_order, []).append(d.so_detail) + + for so, so_item_rows in so_map.items(): + if so and so_item_rows: + sales_order = frappe.get_doc("Sales Order", so) + + if sales_order.status in ["Closed", "Cancelled"]: + frappe.throw(_("{0} {1} is cancelled or closed").format(_("Sales Order"), so), + frappe.InvalidStatusError) + + sales_order.update_reserved_qty(so_item_rows) + + def update_stock_ledger(self): + self.update_reserved_qty() + + sl_entries = [] + for d in self.get_item_list(): + if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty): + return_rate = 0 + if cint(self.is_return) and self.return_against and self.docstatus==1: + return_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against) + + # On cancellation or if return entry submission, make stock ledger entry for + # target warehouse first, to update serial no values properly + + if d.warehouse and ((not cint(self.is_return) and self.docstatus==1) + or (cint(self.is_return) and self.docstatus==2)): + sl_entries.append(self.get_sl_entries(d, { + "actual_qty": -1*flt(d.qty), + "incoming_rate": return_rate + })) + + if d.target_warehouse: + target_warehouse_sle = self.get_sl_entries(d, { + "actual_qty": flt(d.qty), + "warehouse": d.target_warehouse + }) + + if self.docstatus == 1: + if not cint(self.is_return): + args = frappe._dict({ + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1*flt(d.qty), + "serial_no": d.serial_no + }) + target_warehouse_sle.update({ + "incoming_rate": get_incoming_rate(args) + }) + else: + target_warehouse_sle.update({ + "outgoing_rate": return_rate + }) + sl_entries.append(target_warehouse_sle) + + if d.warehouse and ((not cint(self.is_return) and self.docstatus==2) + or (cint(self.is_return) and self.docstatus==1)): + sl_entries.append(self.get_sl_entries(d, { + "actual_qty": -1*flt(d.qty), + "incoming_rate": return_rate + })) + + self.make_sl_entries(sl_entries) def check_active_sales_items(obj): for d in obj.get("items"): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index bd1f258a8d..845a4d0744 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -8,8 +8,6 @@ from frappe import msgprint, _ import frappe.defaults from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.general_ledger import make_gl_entries, delete_gl_entries, process_gl_map -from erpnext.stock.utils import get_incoming_rate - from erpnext.controllers.accounts_controller import AccountsController class StockController(AccountsController): @@ -230,79 +228,6 @@ class StockController(AccountsController): incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 return incoming_rate - - def update_reserved_qty(self): - so_map = {} - for d in self.get("items"): - if d.so_detail: - if self.doctype == "Delivery Note" and d.against_sales_order: - so_map.setdefault(d.against_sales_order, []).append(d.so_detail) - elif self.doctype == "Sales Invoice" and d.sales_order and self.update_stock: - so_map.setdefault(d.sales_order, []).append(d.so_detail) - - for so, so_item_rows in so_map.items(): - if so and so_item_rows: - sales_order = frappe.get_doc("Sales Order", so) - - if sales_order.status in ["Closed", "Cancelled"]: - frappe.throw(_("{0} {1} is cancelled or closed").format(_("Sales Order"), so), - frappe.InvalidStatusError) - - sales_order.update_reserved_qty(so_item_rows) - - def update_stock_ledger(self): - self.update_reserved_qty() - - sl_entries = [] - for d in self.get_item_list(): - if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty): - return_rate = 0 - if cint(self.is_return) and self.return_against and self.docstatus==1: - return_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against) - - # On cancellation or if return entry submission, make stock ledger entry for - # target warehouse first, to update serial no values properly - - if d.warehouse and ((not cint(self.is_return) and self.docstatus==1) - or (cint(self.is_return) and self.docstatus==2)): - sl_entries.append(self.get_sl_entries(d, { - "actual_qty": -1*flt(d.qty), - "incoming_rate": return_rate - })) - - if d.target_warehouse: - target_warehouse_sle = self.get_sl_entries(d, { - "actual_qty": flt(d.qty), - "warehouse": d.target_warehouse - }) - - if self.docstatus == 1: - if not cint(self.is_return): - args = frappe._dict({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1*flt(d.qty), - "serial_no": d.serial_no - }) - target_warehouse_sle.update({ - "incoming_rate": get_incoming_rate(args) - }) - else: - target_warehouse_sle.update({ - "outgoing_rate": return_rate - }) - sl_entries.append(target_warehouse_sle) - - if d.warehouse and ((not cint(self.is_return) and self.docstatus==2) - or (cint(self.is_return) and self.docstatus==1)): - sl_entries.append(self.get_sl_entries(d, { - "actual_qty": -1*flt(d.qty), - "incoming_rate": return_rate - })) - - self.make_sl_entries(sl_entries) def validate_warehouse(self): from erpnext.stock.utils import validate_warehouse_company diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 469e327245..ccc4629448 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -196,7 +196,7 @@ execute:frappe.db.sql("update `tabProduction Order` pro set description = (selec erpnext.patches.v5_7.item_template_attributes execute:frappe.delete_doc_if_exists("DocType", "Manage Variants") execute:frappe.delete_doc_if_exists("DocType", "Manage Variants Item") -erpnext.patches.v4_2.repost_reserved_qty #2015-08-20 +erpnext.patches.v4_2.repost_reserved_qty #2016-04-15 erpnext.patches.v5_4.update_purchase_cost_against_project erpnext.patches.v5_8.update_order_reference_in_return_entries erpnext.patches.v5_8.add_credit_note_print_heading diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 7ac8941808..5fa2d7e2ba 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -126,6 +126,34 @@ class TestSalesOrder(unittest.TestCase): dn.cancel() self.assertEqual(get_reserved_qty(), existing_reserved_qty + 10) + + def test_reserved_qty_for_over_delivery_via_sales_invoice(self): + # set over-delivery tolerance + frappe.db.set_value('Item', "_Test Item", 'tolerance', 50) + + existing_reserved_qty = get_reserved_qty() + + so = make_sales_order() + self.assertEqual(get_reserved_qty(), existing_reserved_qty + 10) + + si = make_sales_invoice(so.name) + si.update_stock = 1 + si.get("items")[0].qty = 12 + si.insert() + si.submit() + + self.assertEqual(get_reserved_qty(), existing_reserved_qty) + + so.load_from_db() + self.assertEqual(so.get("items")[0].delivered_qty, 12) + self.assertEqual(so.per_delivered, 100) + + si.cancel() + self.assertEqual(get_reserved_qty(), existing_reserved_qty + 10) + + so.load_from_db() + self.assertEqual(so.get("items")[0].delivered_qty, 0) + self.assertEqual(so.per_delivered, 0) def test_reserved_qty_for_partial_delivery_with_packing_list(self): existing_reserved_qty_item1 = get_reserved_qty("_Test Item") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 63abaa5a34..6712ee85af 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -191,6 +191,8 @@ class DeliveryNote(SellingController): if not self.is_return: self.check_credit_limit() + # Updating stock ledger should always be called after updating prevdoc status, + # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() self.make_gl_entries() @@ -201,6 +203,8 @@ class DeliveryNote(SellingController): self.update_prevdoc_status() self.update_billing_status() + # Updating stock ledger should always be called after updating prevdoc status, + # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() self.cancel_packing_slips() diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 81708ce83d..c58607fe05 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -14,9 +14,6 @@ class TestLandedCostVoucher(unittest.TestCase): set_perpetual_inventory(1) pr = frappe.copy_doc(pr_test_records[0]) pr.submit() - - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - _TC", is_paid=1) last_sle = frappe.db.get_value("Stock Ledger Entry", { "voucher_type": pr.doctype, @@ -24,16 +21,12 @@ class TestLandedCostVoucher(unittest.TestCase): "item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC" }, - fieldname=["qty_after_transaction", "stock_value"], - as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], as_dict=1) - self.submit_landed_cost_voucher(pr, pi) + self.submit_landed_cost_voucher("Purchase Receipt", pr.name) pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount") self.assertEquals(pr_lc_value, 25.0) - - pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name}, "landed_cost_voucher_amount") - self.assertEquals(pi_lc_value, 25.0) last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", { "voucher_type": pr.doctype, @@ -41,8 +34,7 @@ class TestLandedCostVoucher(unittest.TestCase): "item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC" }, - fieldname=["qty_after_transaction", "stock_value"], - as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], as_dict=1) self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) @@ -68,7 +60,56 @@ class TestLandedCostVoucher(unittest.TestCase): self.assertEquals(expected_values[gle.account][1], gle.credit) set_perpetual_inventory(0) + + def test_landed_cost_voucher_against_purchase_invoice(self): + set_perpetual_inventory(1) + + pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime()) + last_sle = frappe.db.get_value("Stock Ledger Entry", { + "voucher_type": pi.doctype, + "voucher_no": pi.name, + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC" + }, + fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + + self.submit_landed_cost_voucher("Purchase Invoice", pi.name) + + pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name}, + "landed_cost_voucher_amount") + + self.assertEquals(pi_lc_value, 50.0) + + last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", { + "voucher_type": pi.doctype, + "voucher_no": pi.name, + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC" + }, + fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + + self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) + + self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) + + gl_entries = get_gl_entries("Purchase Invoice", pi.name) + + self.assertTrue(gl_entries) + + expected_values = { + pi.get("items")[0].warehouse: [300.0, 0.0], + "Creditors - _TC": [0.0, 250.0], + "Expenses Included In Valuation - _TC": [0.0, 50.0] + } + + for gle in gl_entries: + self.assertEquals(expected_values[gle.account][0], gle.debit) + self.assertEquals(expected_values[gle.account][1], gle.credit) + + set_perpetual_inventory(0) + def test_landed_cost_voucher_for_serialized_item(self): set_perpetual_inventory(1) frappe.db.sql("delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')") @@ -80,40 +121,33 @@ class TestLandedCostVoucher(unittest.TestCase): serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate") - self.submit_landed_cost_voucher(pr) + self.submit_landed_cost_voucher("Purchase Receipt", pr.name) serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1) - self.assertEquals(serial_no.purchase_rate - serial_no_rate, 7.5) + self.assertEquals(serial_no.purchase_rate - serial_no_rate, 5.0) self.assertEquals(serial_no.warehouse, "_Test Warehouse - _TC") set_perpetual_inventory(0) - def submit_landed_cost_voucher(self, pr, pi=None): + def submit_landed_cost_voucher(self, receipt_document_type, receipt_document): + ref_doc = frappe.get_doc(receipt_document_type, receipt_document) + lcv = frappe.new_doc("Landed Cost Voucher") lcv.company = "_Test Company" lcv.set("purchase_receipts", [{ - "receipt_document_type": "Purchase Receipt", - "receipt_document": pr.name, - "supplier": pr.supplier, - "posting_date": pr.posting_date, - "grand_total": pr.base_grand_total + "receipt_document_type": receipt_document_type, + "receipt_document": receipt_document, + "supplier": ref_doc.supplier, + "posting_date": ref_doc.posting_date, + "grand_total": ref_doc.base_grand_total }]) - if pi: - lcv.append("purchase_receipts", { - "receipt_document_type": "Purchase Invoice", - "receipt_document": pi.name, - "supplier": pi.supplier, - "posting_date": pi.posting_date, - "grand_total": pi.base_grand_total - }) - lcv.set("taxes", [{ "description": "Insurance Charges", "account": "_Test Account Insurance Charges - _TC", - "amount": 75.0 + "amount": 50 }]) lcv.insert() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 332dc63c68..9b9581b073 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -21,6 +21,20 @@ frappe.ui.form.on("Purchase Receipt", { "batch_no": doc.batch_no } } + + $.each(["warehouse", "rejected_warehouse"], function(i, field) { + frm.set_query(field, "items", function() { + return { + filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]] + } + }) + }) + + frm.set_query("supplier_warehouse", function() { + return { + filters: [["Warehouse", "company", "in", ["", cstr(frm.doc.company)]]] + } + }) } }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index bad1228dd2..1babf2ea43 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cstr, flt, cint +from frappe.utils import flt, cint from frappe import _ import frappe.defaults @@ -81,22 +81,6 @@ class PurchaseReceipt(BuyingController): if not d.prevdoc_docname: frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code)) - def update_ordered_qty(self): - po_map = {} - for d in self.get("items"): - if d.prevdoc_doctype and d.prevdoc_doctype == "Purchase Order" and d.prevdoc_detail_docname: - po_map.setdefault(d.prevdoc_docname, []).append(d.prevdoc_detail_docname) - - for po, po_item_rows in po_map.items(): - if po and po_item_rows: - po_obj = frappe.get_doc("Purchase Order", po) - - if po_obj.status in ["Closed", "Cancelled"]: - frappe.throw(_("{0} {1} is cancelled or closed").format(_("Purchase Order"), po), - frappe.InvalidStatusError) - - po_obj.update_ordered_qty(po_item_rows) - def get_already_received_qty(self, po, po_detail): qty = frappe.db.sql("""select sum(qty) from `tabPurchase Receipt Item` where prevdoc_detail_docname = %s and docstatus = 1 @@ -129,19 +113,20 @@ class PurchaseReceipt(BuyingController): purchase_controller = frappe.get_doc("Purchase Common") # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total) + frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, + self.company, self.base_grand_total) # Set status as Submitted frappe.db.set(self, 'status', 'Submitted') self.update_prevdoc_status() - self.update_ordered_qty() - self.update_billing_status() if not self.is_return: purchase_controller.update_last_purchase_rate(self, 1) + # Updating stock ledger should always be called after updating prevdoc status, + # because updating ordered qty in bin depends upon updated ordered qty in PO self.update_stock_ledger() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit @@ -171,17 +156,15 @@ class PurchaseReceipt(BuyingController): frappe.db.set(self,'status','Cancelled') - self.update_stock_ledger() - - self.update_prevdoc_status() - # Must be called after updating received qty in PO - self.update_ordered_qty() - + self.update_prevdoc_status() self.update_billing_status() if not self.is_return: pc_obj.update_last_purchase_rate(self, 0) - + + # Updating stock ledger should always be called after updating prevdoc status, + # because updating ordered qty in bin depends upon updated ordered qty in PO + self.update_stock_ledger() self.make_gl_entries_on_cancel() def get_current_stock(self): @@ -256,13 +239,11 @@ class PurchaseReceipt(BuyingController): }, warehouse_account[self.supplier_warehouse]["account_currency"])) # divisional loss adjustment - sle_valuation_amount = flt(flt(d.valuation_rate, val_rate_db_precision) * flt(d.qty) * flt(d.conversion_factor), - self.precision("base_net_amount", d)) - - distributed_amount = flt(flt(d.base_net_amount, self.precision("base_net_amount", d))) + \ + distributed_amount = flt(flt(d.base_net_amount, d.precision("base_net_amount"))) + \ flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount) - divisional_loss = flt(distributed_amount - sle_valuation_amount, self.precision("base_net_amount", d)) + divisional_loss = flt(distributed_amount - stock_value_diff, + d.precision("base_net_amount")) if divisional_loss: gl_entries.append(self.get_gl_dict({ "account": stock_rbnb,