From ab7b7c2974b635d37f7b8d0f28cfa050566ec4e9 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 18 Mar 2013 17:37:31 +0530 Subject: [PATCH] completed sales purchase return as part of stock entry --- stock/doctype/delivery_note/delivery_note.py | 2 +- stock/doctype/stock_entry/stock_entry.js | 21 +- stock/doctype/stock_entry/stock_entry.py | 122 +++++++-- stock/doctype/stock_entry/test_stock_entry.py | 257 +++++++++++++++++- 4 files changed, 364 insertions(+), 38 deletions(-) diff --git a/stock/doctype/delivery_note/delivery_note.py b/stock/doctype/delivery_note/delivery_note.py index 229ec40a54..46c6ee4bd4 100644 --- a/stock/doctype/delivery_note/delivery_note.py +++ b/stock/doctype/delivery_note/delivery_note.py @@ -133,7 +133,7 @@ class DocType(SellingController): super(DocType, self).validate() import utilities - utilities.validate_status(self.doc.status, ["Draft", "submitted", "Cancelled"]) + utilities.validate_status(self.doc.status, ["Draft", "Submitted", "Cancelled"]) self.so_required() self.validate_fiscal_year() diff --git a/stock/doctype/stock_entry/stock_entry.js b/stock/doctype/stock_entry/stock_entry.js index 158184486f..0b27d99bad 100644 --- a/stock/doctype/stock_entry/stock_entry.js +++ b/stock/doctype/stock_entry/stock_entry.js @@ -60,6 +60,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ }, refresh: function() { + var me = this; erpnext.hide_naming_series(); this.toggle_related_fields(this.frm.doc); this.toggle_enable_bom(); @@ -67,12 +68,13 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ this.show_stock_ledger(); } - if(this.frm.doc.docstatus === 1 && wn.boot.profile.can_create("Journal Voucher")) { + if(this.frm.doc.docstatus === 1 && + wn.boot.profile.can_create.indexOf("Journal Voucher")!==-1) { if(this.frm.doc.purpose === "Sales Return") { - this.frm.add_custom_button("Make Credit Note", this.make_return_jv); + this.frm.add_custom_button("Make Credit Note", function() { me.make_return_jv(); }); this.add_excise_button(); } else if(this.frm.doc.purpose === "Purchase Return") { - this.frm.add_custom_button("Make Debit Note", this.make_return_jv); + this.frm.add_custom_button("Make Debit Note", function() { me.make_return_jv(); }); this.add_excise_button(); } } @@ -172,8 +174,17 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ stock_entry: this.frm.doc.name }, callback: function(r) { - console.log(r); - loaddoc("Journal Voucher", r.message); + if(!r.exc) { + var jv_name = wn.model.make_new_doc_and_get_name('Journal Voucher'); + var jv = locals["Journal Voucher"][jv_name]; + $.extend(jv, r.message[0]); + $.each(r.message.slice(1), function(i, jvd) { + var child = wn.model.add_child(jv, "Journal Voucher Detail", "entries"); + $.extend(child, jvd); + }); + loaddoc("Journal Voucher", jv_name); + } + } }); }, diff --git a/stock/doctype/stock_entry/stock_entry.py b/stock/doctype/stock_entry/stock_entry.py index dc67b9f441..131e9ffcd4 100644 --- a/stock/doctype/stock_entry/stock_entry.py +++ b/stock/doctype/stock_entry/stock_entry.py @@ -25,7 +25,6 @@ from webnotes import msgprint, _ from stock.utils import get_incoming_rate from stock.stock_ledger import get_previous_sle import json -from accounts.utils import get_balance_on sql = webnotes.conn.sql @@ -297,7 +296,7 @@ class DocType(AccountsController): # posting date check ref_posting_datetime = "%s %s" % (cstr(ref.doclist[0].posting_date), - cstr(ref.doclist[0].posting_time)) + cstr(ref.doclist[0].posting_time) or "00:00:00") this_posting_datetime = "%s %s" % (cstr(self.doc.posting_date), cstr(self.doc.posting_time)) if this_posting_datetime < ref_posting_datetime: @@ -779,13 +778,30 @@ def make_return_jv(stock_entry): result = make_return_jv_from_purchase_receipt(se, ref) # create jv doclist and fetch balance for each unique row item + jv_list = [{ + "__islocal": 1, + "doctype": "Journal Voucher", + "posting_date": se.doc.posting_date, + "voucher_type": se.doc.purpose == "Sales Return" and "Credit Note" or "Debit Note", + "fiscal_year": se.doc.fiscal_year, + "company": se.doc.company + }] - if not webnotes.response.get("docs"): - webnotes.response["docs"] = [] - - webnotes.response["docs"] = jv.doclist - - return jv.doc.name + from accounts.utils import get_balance_on + for r in result: + jv_list.append({ + "__islocal": 1, + "doctype": "Journal Voucher Detail", + "parentfield": "entries", + "account": r.get("account"), + "debit": r.get("debit"), + "credit": r.get("credit"), + "against_invoice": r.get("against_invoice"), + "against_voucher": r.get("against_voucher"), + "balance": get_balance_on(r.get("account"), se.doc.posting_date) + }) + + return jv_list def make_return_jv_from_sales_invoice(se, ref): # customer account entry @@ -833,12 +849,10 @@ def make_return_jv_from_delivery_note(se, ref): ref.doclist[0].name) if not invoices_against_delivery: - item_codes = [item.item_code for item in se.doclist.get({"parentfield": "mtn_details"})] - sales_orders_against_delivery = [d.prev_docname for d in - ref.doclist.get({"prev_doctype": "Sales Order"}) - if d.prev_docname and d.item_code in item_codes] - - invoices_against_delivery = get_invoice_list("Sales Order Item", "sales_order", + sales_orders_against_delivery = [d.prevdoc_docname for d in + ref.doclist.get({"prevdoc_doctype": "Sales Order"}) if d.prevdoc_docname] + + invoices_against_delivery = get_invoice_list("Sales Invoice Item", "sales_order", sales_orders_against_delivery) against_invoice = {} @@ -846,8 +860,10 @@ def make_return_jv_from_delivery_note(se, ref): for se_item in se.doclist.get({"parentfield": "mtn_details"}): pending = se_item.transfer_qty for sales_invoice in invoices_against_delivery: - si_doclist = webnotes.get_doclist("Sales Invoice", sales_invoice) - ref_item = si_doclist.get({"item_code": se_item.item_code}) + si = webnotes.bean("Sales Invoice", sales_invoice) + si.run_method("make_packing_list") + ref_item = si.doclist.get({"item_code": se_item.item_code}) + if not ref_item: continue @@ -860,12 +876,12 @@ def make_return_jv_from_delivery_note(se, ref): transfer_qty = pending pending = 0 - account, debit = get_sales_account_and_amount_from_item(si_doclist, ref_item, + account, debit = get_sales_account_and_amount_from_item(si.doclist, ref_item, transfer_qty) - if si_doclist[0].name not in against_invoice: + if si.doclist[0].name not in against_invoice: against_invoice[sales_invoice] = { - "parent": {"account": si_doclist[0].debit_to, "credit": 0}, + "parent": {"account": si.doclist[0].debit_to, "credit": 0}, "children": {} } @@ -883,17 +899,79 @@ def make_return_jv_from_delivery_note(se, ref): result = [] for sales_invoice, opts in against_invoice.items(): - result += [opts["parent"]] + [{"account": account, "debit": debit} + parent = opts["parent"] + parent.update({"against_invoice": sales_invoice}) + children = [{"account": account, "debit": debit} for account, debit in opts["children"].items()] + result += [parent] + children return result def get_invoice_list(doctype, link_field, value): if isinstance(value, basestring): value = [value] - + return webnotes.conn.sql_list("""select distinct parent from `tab%s` where docstatus = 1 and `%s` in (%s)""" % (doctype, link_field, ", ".join(["%s"]*len(value))), tuple(value)) def make_return_jv_from_purchase_receipt(se, ref): - pass + invoice_against_receipt = get_invoice_list("Purchase Invoice Item", "purchase_receipt", + ref.doclist[0].name) + + if not invoice_against_receipt: + purchase_orders_against_receipt = [d.prevdoc_docname for d in + ref.doclist.get({"prevdoc_doctype": "Purchase Order"}) if d.prevdoc_docname] + + invoice_against_receipt = get_invoice_list("Purchase Invoice Item", "purchase_order", + purchase_orders_against_receipt) + + against_voucher = {} + + for se_item in se.doclist.get({"parentfield": "mtn_details"}): + pending = se_item.transfer_qty + for purchase_invoice in invoice_against_receipt: + pi = webnotes.bean("Purchase Invoice", purchase_invoice) + ref_item = pi.doclist.get({"item_code": se_item.item_code}) + + if not ref_item: + continue + + ref_item = ref_item[0] + + if ref_item.qty < pending: + transfer_qty = ref_item.qty + pending -= ref_item.qty + else: + transfer_qty = pending + pending = 0 + + credit = ref_item.rate * transfer_qty + account = ref_item.expense_head + + if pi.doclist[0].name not in against_voucher: + against_voucher[purchase_invoice] = { + "parent": {"account": pi.doclist[0].credit_to, "debit": 0}, + "children": {} + } + + against_voucher[purchase_invoice]["parent"]["debit"] += credit + + if account not in against_voucher[purchase_invoice]["children"]: + against_voucher[purchase_invoice]["children"][account] = 0 + + against_voucher[purchase_invoice]["children"][account] += credit + + # find tax account and value and add corresponding rows + + if pending <= 0: + break + + result = [] + for purchase_invoice, opts in against_voucher.items(): + parent = opts["parent"] + parent.update({"against_voucher": purchase_invoice}) + children = [{"account": account, "credit": credit} + for account, credit in opts["children"].items()] + result += [parent] + children + return result + \ No newline at end of file diff --git a/stock/doctype/stock_entry/test_stock_entry.py b/stock/doctype/stock_entry/test_stock_entry.py index c0f8f28240..049b0e66c8 100644 --- a/stock/doctype/stock_entry/test_stock_entry.py +++ b/stock/doctype/stock_entry/test_stock_entry.py @@ -9,6 +9,9 @@ class TestStockEntry(unittest.TestCase): def test_auto_material_request(self): webnotes.conn.sql("""delete from `tabMaterial Request Item`""") webnotes.conn.sql("""delete from `tabMaterial Request`""") + self._clear_stock() + + webnotes.conn.set_value("Global Defaults", None, "auto_indent", True) st1 = webnotes.bean(copy=test_records[0]) st1.insert() @@ -166,9 +169,14 @@ class TestStockEntry(unittest.TestCase): def _insert_material_receipt(self): self._clear_stock() - material_receipt = webnotes.bean(copy=test_records[0]) - material_receipt.insert() - material_receipt.submit() + se1 = webnotes.bean(copy=test_records[0]) + se1.insert() + se1.submit() + + se2 = webnotes.bean(copy=test_records[0]) + se2.doclist[1].item_code = "_Test Item Home Desktop 100" + se2.insert() + se2.submit() def _get_actual_qty(self): return flt(webnotes.conn.get_value("Bin", {"item_code": "_Test Item", @@ -238,6 +246,8 @@ class TestStockEntry(unittest.TestCase): actual_qty_2 = self._get_actual_qty() self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) + + return se def test_sales_invoice_return_of_non_packing_item(self): self._test_sales_invoice_return("_Test Item", 5, 2) @@ -248,11 +258,12 @@ class TestStockEntry(unittest.TestCase): def _test_delivery_note_return(self, item_code, delivered_qty, returned_qty): self._insert_material_receipt() - actual_qty_0 = self._get_actual_qty() - - # insert and submit delivery note from stock.doctype.delivery_note.test_delivery_note \ import test_records as delivery_note_test_records + + actual_qty_0 = self._get_actual_qty() + + # make a delivery note based on this invoice dn = webnotes.bean(copy=delivery_note_test_records[0]) dn.doclist[1].item_code = item_code dn.insert() @@ -262,11 +273,26 @@ class TestStockEntry(unittest.TestCase): self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) + si_doclist = webnotes.map_doclist([ + ["Delivery Note", "Sales Invoice"], + ["Delivery Note Item", "Sales Invoice Item"], + ["Sales Taxes and Charges", "Sales Taxes and Charges"], + ["Sales Team", "Sales Team"]], dn.doc.name) + + si = webnotes.bean(si_doclist) + si.doc.posting_date = dn.doc.posting_date + si.doc.debit_to = "_Test Customer - _TC" + for d in si.doclist.get({"parentfield": "entries"}): + d.income_account = "Sales - _TC" + d.cost_center = "_Test Cost Center - _TC" + si.insert() + si.submit() + # insert and submit stock entry for sales return se = webnotes.bean(copy=test_records[0]) se.doc.purpose = "Sales Return" se.doc.delivery_note_no = dn.doc.name - se.doc.posting_date = "2013-03-01" + se.doc.posting_date = "2013-03-10" se.doclist[1].qty = se.doclist[1].transfer_qty = returned_qty se.insert() @@ -275,12 +301,115 @@ class TestStockEntry(unittest.TestCase): actual_qty_2 = self._get_actual_qty() self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) + return se + def test_delivery_note_return_of_non_packing_item(self): self._test_delivery_note_return("_Test Item", 5, 2) def test_delivery_note_return_of_packing_item(self): self._test_delivery_note_return("_Test Sales BOM Item", 25, 20) + def _test_sales_return_jv(self, se, returned_value): + from stock.doctype.stock_entry.stock_entry import make_return_jv + jv_list = make_return_jv(se.doc.name) + + self.assertEqual(len(jv_list), 3) + self.assertEqual(jv_list[0].get("voucher_type"), "Credit Note") + self.assertEqual(jv_list[0].get("posting_date"), se.doc.posting_date) + self.assertEqual(jv_list[1].get("account"), "_Test Customer - _TC") + self.assertEqual(jv_list[2].get("account"), "Sales - _TC") + self.assertTrue(jv_list[1].get("against_invoice")) + + # debit == credit + debit = sum([flt(d.get("debit")) for d in jv_list]) + credit = sum([flt(d.get("credit")) for d in jv_list]) + self.assertEqual(debit, credit) + + # validate value of debit + self.assertEqual(debit, returned_value) + + def test_make_return_jv_for_sales_invoice_non_packing_item(self): + se = self._test_sales_invoice_return("_Test Item", 5, 2) + self._test_sales_return_jv(se, 1000) + + def test_make_return_jv_for_sales_invoice_packing_item(self): + se = self._test_sales_invoice_return("_Test Sales BOM Item", 25, 20) + self._test_sales_return_jv(se, 2000) + + def test_make_return_jv_for_delivery_note_non_packing_item(self): + se = self._test_delivery_note_return("_Test Item", 5, 2) + self._test_sales_return_jv(se, 200) + + se = self._test_delivery_note_return_against_sales_order("_Test Item", 5, 2) + self._test_sales_return_jv(se, 200) + + def test_make_return_jv_for_delivery_note_packing_item(self): + se = self._test_delivery_note_return("_Test Sales BOM Item", 25, 20) + self._test_sales_return_jv(se, 400) + + se = self._test_delivery_note_return_against_sales_order("_Test Sales BOM Item", 25, 20) + self._test_sales_return_jv(se, 400) + + def _test_delivery_note_return_against_sales_order(self, item_code, delivered_qty, returned_qty): + self._insert_material_receipt() + + from selling.doctype.sales_order.test_sales_order \ + import test_records as sales_order_test_records + + actual_qty_0 = self._get_actual_qty() + + so = webnotes.bean(copy=sales_order_test_records[0]) + so.doclist[1].item_code = item_code + so.doclist[1].qty = 5.0 + so.insert() + so.submit() + + dn_doclist = webnotes.map_doclist([ + ["Sales Order", "Delivery Note"], + ["Sales Order Item", "Delivery Note Item"], + ["Sales Taxes and Charges", "Sales Taxes and Charges"], + ["Sales Team", "Sales Team"]], so.doc.name) + + dn = webnotes.bean(dn_doclist) + dn.doc.status = "Draft" + dn.doc.posting_date = so.doc.delivery_date + dn.insert() + dn.submit() + + actual_qty_1 = self._get_actual_qty() + + self.assertEquals(actual_qty_0 - delivered_qty, actual_qty_1) + + si_doclist = webnotes.map_doclist([ + ["Sales Order", "Sales Invoice"], + ["Sales Order Item", "Sales Invoice Item"], + ["Sales Taxes and Charges", "Sales Taxes and Charges"], + ["Sales Team", "Sales Team"]], so.doc.name) + + si = webnotes.bean(si_doclist) + si.doc.posting_date = dn.doc.posting_date + si.doc.debit_to = "_Test Customer - _TC" + for d in si.doclist.get({"parentfield": "entries"}): + d.income_account = "Sales - _TC" + d.cost_center = "_Test Cost Center - _TC" + si.insert() + si.submit() + + # insert and submit stock entry for sales return + se = webnotes.bean(copy=test_records[0]) + se.doc.purpose = "Sales Return" + se.doc.delivery_note_no = dn.doc.name + se.doc.posting_date = "2013-03-10" + se.doclist[1].qty = se.doclist[1].transfer_qty = returned_qty + + se.insert() + se.submit() + + actual_qty_2 = self._get_actual_qty() + self.assertEquals(actual_qty_1 + returned_qty, actual_qty_2) + + return se + def test_purchase_receipt_return(self): self._clear_stock() @@ -298,6 +427,24 @@ class TestStockEntry(unittest.TestCase): self.assertEquals(actual_qty_0 + 10, actual_qty_1) + pi_doclist = webnotes.map_doclist([ + ["Purchase Receipt", "Purchase Invoice"], + ["Purchase Receipt Item", "Purchase Invoice Item"], + ["Purchase Taxes and Charges", "Purchase Taxes and Charges"]], pr.doc.name) + + pi = webnotes.bean(pi_doclist) + pi.doc.posting_date = pr.doc.posting_date + pi.doc.credit_to = "_Test Supplier - _TC" + for d in pi.doclist.get({"parentfield": "entries"}): + d.expense_head = "_Test Account Cost for Goods Sold - _TC" + d.cost_center = "_Test Cost Center - _TC" + for d in pi.doclist.get({"parentfield": "purchase_tax_details"}): + d.cost_center = "_Test Cost Center - _TC" + + pi.run_method("calculate_taxes_and_totals") + pi.insert() + pi.submit() + # submit purchase return se = webnotes.bean(copy=test_records[0]) se.doc.purpose = "Purchase Return" @@ -312,13 +459,13 @@ class TestStockEntry(unittest.TestCase): self.assertEquals(actual_qty_1 - 5, actual_qty_2) - return pr.doc.name + return se, pr.doc.name def test_over_stock_return(self): from stock.doctype.stock_entry.stock_entry import StockOverReturnError # out of 10, 5 gets returned - pr_docname = self.test_purchase_receipt_return() + prev_se, pr_docname = self.test_purchase_receipt_return() # submit purchase return - return another 6 qtys so that exception is raised se = webnotes.bean(copy=test_records[0]) @@ -329,7 +476,97 @@ class TestStockEntry(unittest.TestCase): se.doclist[1].s_warehouse = "_Test Warehouse" self.assertRaises(StockOverReturnError, se.insert) - + + def _test_purchase_return_jv(self, se, returned_value): + from stock.doctype.stock_entry.stock_entry import make_return_jv + jv_list = make_return_jv(se.doc.name) + + self.assertEqual(len(jv_list), 3) + self.assertEqual(jv_list[0].get("voucher_type"), "Debit Note") + self.assertEqual(jv_list[0].get("posting_date"), se.doc.posting_date) + self.assertEqual(jv_list[1].get("account"), "_Test Supplier - _TC") + self.assertEqual(jv_list[2].get("account"), "_Test Account Cost for Goods Sold - _TC") + self.assertTrue(jv_list[1].get("against_voucher")) + + # debit == credit + debit = sum([flt(d.get("debit")) for d in jv_list]) + credit = sum([flt(d.get("credit")) for d in jv_list]) + self.assertEqual(debit, credit) + + # validate value of credit + self.assertEqual(credit, returned_value) + + def test_make_return_jv_for_purchase_receipt(self): + se, pr_name = self.test_purchase_receipt_return() + self._test_purchase_return_jv(se, 250) + + se, pr_name = self._test_purchase_return_return_against_purchase_order() + self._test_purchase_return_jv(se, 250) + + def _test_purchase_return_return_against_purchase_order(self): + self._clear_stock() + + actual_qty_0 = self._get_actual_qty() + + from buying.doctype.purchase_order.test_purchase_order \ + import test_records as purchase_order_test_records + + # submit purchase receipt + po = webnotes.bean(copy=purchase_order_test_records[0]) + po.doc.is_subcontracted = None + po.doclist[1].item_code = "_Test Item" + po.doclist[1].import_rate = 50 + po.insert() + po.submit() + + pr_doclist = webnotes.map_doclist([ + ["Purchase Order", "Purchase Receipt"], + ["Purchase Order Item", "Purchase Receipt Item"], + ["Purchase Taxes and Charges", "Purchase Taxes and Charges"]], po.doc.name) + + pr = webnotes.bean(pr_doclist) + pr.doc.posting_date = po.doc.transaction_date + pr.insert() + pr.submit() + + actual_qty_1 = self._get_actual_qty() + + self.assertEquals(actual_qty_0 + 10, actual_qty_1) + + pi_doclist = webnotes.map_doclist([ + ["Purchase Order", "Purchase Invoice"], + ["Purchase Order Item", "Purchase Invoice Item"], + ["Purchase Taxes and Charges", "Purchase Taxes and Charges"]], po.doc.name) + + pi = webnotes.bean(pi_doclist) + pi.doc.posting_date = pr.doc.posting_date + pi.doc.credit_to = "_Test Supplier - _TC" + for d in pi.doclist.get({"parentfield": "entries"}): + d.expense_head = "_Test Account Cost for Goods Sold - _TC" + d.cost_center = "_Test Cost Center - _TC" + for d in pi.doclist.get({"parentfield": "purchase_tax_details"}): + d.cost_center = "_Test Cost Center - _TC" + + pi.run_method("calculate_taxes_and_totals") + pi.insert() + pi.submit() + + # submit purchase return + se = webnotes.bean(copy=test_records[0]) + se.doc.purpose = "Purchase Return" + se.doc.purchase_receipt_no = pr.doc.name + se.doc.posting_date = "2013-03-01" + se.doclist[1].qty = se.doclist[1].transfer_qty = 5 + se.doclist[1].s_warehouse = "_Test Warehouse" + se.insert() + se.submit() + + actual_qty_2 = self._get_actual_qty() + + self.assertEquals(actual_qty_1 - 5, actual_qty_2) + + return se, pr.doc.name + test_records = [ [ {