From c865f229fbba691b46eb2d86edebf2d6d8e6c554 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 21 Sep 2015 09:18:43 +0530 Subject: [PATCH 1/6] Target Warehouse in Delivery Note and Sales Invoice and removed Serial No status --- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../sales_invoice/test_sales_invoice.py | 7 +- .../sales_invoice_item.json | 25 ++- erpnext/controllers/selling_controller.py | 6 +- erpnext/controllers/stock_controller.py | 67 +++++-- .../doctype/sales_order/sales_order.py | 2 +- .../sales_order_item/sales_order_item.json | 25 ++- .../doctype/delivery_note/delivery_note.py | 2 +- .../delivery_note/test_delivery_note.py | 12 +- .../delivery_note_item.json | 25 ++- .../doctype/packed_item/packed_item.json | 165 ++++++++++++------ .../stock/doctype/packed_item/packed_item.py | 56 +++--- .../purchase_receipt/test_purchase_receipt.py | 2 - .../stock/doctype/serial_no/serial_no.json | 30 +--- erpnext/stock/doctype/serial_no/serial_no.py | 37 +--- .../serial_no_status/serial_no_status.json | 6 +- erpnext/stock/stock_balance.py | 5 +- 17 files changed, 291 insertions(+), 183 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 33fa4e340b..940f754262 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -422,7 +422,7 @@ class SalesInvoice(SellingController): def update_packing_list(self): if cint(self.update_stock) == 1: from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - make_packing_list(self, 'items') + make_packing_list(self) else: self.set('packed_items', []) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 0eae7cbcfb..20c734ce5c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -688,7 +688,6 @@ class TestSalesInvoice(unittest.TestCase): si.insert() si.submit() - self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Delivered") self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name) @@ -702,20 +701,18 @@ class TestSalesInvoice(unittest.TestCase): serial_nos = get_serial_nos(si.get("items")[0].serial_no) - self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Available") self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC") self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no")) def test_serialize_status(self): - from erpnext.stock.doctype.serial_no.serial_no import SerialNoStatusError, get_serial_nos, SerialNoDuplicateError + from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError, get_serial_nos, SerialNoDuplicateError from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item se = make_serialized_item() serial_nos = get_serial_nos(se.get("items")[0].serial_no) sr = frappe.get_doc("Serial No", serial_nos[0]) - sr.status = "Not Available" sr.save() si = frappe.copy_doc(test_records[0]) @@ -725,7 +722,7 @@ class TestSalesInvoice(unittest.TestCase): si.get("items")[0].serial_no = serial_nos[0] si.insert() - self.assertRaises(SerialNoStatusError, si.submit) + self.assertRaises(SerialNoWarehouseError, si.submit) # hack! because stock ledger entires are already inserted and are not rolled back! self.assertRaises(SerialNoDuplicateError, si.cancel) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 2a14b8cb57..49b5b1052e 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -843,6 +843,29 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "target_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Target Warehouse", + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -1259,7 +1282,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-08-20 17:18:52.752064", + "modified": "2015-09-20 21:26:46.279615", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 4b44932a88..75be4d684c 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -173,7 +173,8 @@ class SellingController(StockController): 'uom': p.uom, 'batch_no': cstr(p.batch_no).strip(), 'serial_no': cstr(p.serial_no).strip(), - 'name': d.name + 'name': d.name, + 'target_warehouse': p.target_warehouse })) else: il.append(frappe._dict({ @@ -184,7 +185,8 @@ class SellingController(StockController): 'stock_uom': d.stock_uom, 'batch_no': cstr(d.get("batch_no")).strip(), 'serial_no': cstr(d.get("serial_no")).strip(), - 'name': d.name + 'name': d.name, + 'target_warehouse': p.target_warehouse })) return il diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index eccceb0ad3..4b58b3c046 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -7,6 +7,7 @@ from frappe.utils import cint, flt, cstr from frappe import msgprint, _ import frappe.defaults 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 @@ -184,7 +185,7 @@ class StockController(AccountsController): "voucher_no": self.name, "voucher_detail_no": d.name, "actual_qty": (self.docstatus==1 and 1 or -1)*flt(d.get("stock_qty")), - "stock_uom": d.get("stock_uom"), + "stock_uom": frappe.db.get_value("Item", args.get("item_code") or d.get("item_code"), "stock_uom"), "incoming_rate": 0, "company": self.company, "fiscal_year": self.fiscal_year, @@ -217,13 +218,14 @@ class StockController(AccountsController): return serialized_items - def get_incoming_rate_for_sales_return(self, item_code, against_document): + def get_incoming_rate_for_sales_return(self, item_code, warehouse, against_document): incoming_rate = 0.0 if against_document and item_code: incoming_rate = frappe.db.sql("""select abs(ifnull(stock_value_difference, 0) / actual_qty) from `tabStock Ledger Entry` - where voucher_type = %s and voucher_no = %s and item_code = %s limit 1""", - (self.doctype, against_document, item_code)) + where voucher_type = %s and voucher_no = %s + and item_code = %s and warehouse=%s limit 1""", + (self.doctype, against_document, item_code, warehouse)) incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 return incoming_rate @@ -252,19 +254,54 @@ class StockController(AccountsController): sl_entries = [] for d in self.get_item_list(): - if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 \ - and d.warehouse and flt(d['qty']): - - incoming_rate = 0 + 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: - incoming_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against) - - sl_entries.append(self.get_sl_entries(d, { - "actual_qty": -1*flt(d['qty']), - "stock_uom": frappe.db.get_value("Item", d.item_code, "stock_uom"), - "incoming_rate": incoming_rate - })) + return_rate = self.get_incoming_rate_for_sales_return(d.item_code, + d.warehouse, 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 update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 732867ff01..6cc12c53a9 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -33,7 +33,7 @@ class SalesOrder(SellingController): self.validate_warehouse() from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - make_packing_list(self,'items') + make_packing_list(self) self.validate_with_previous_doc() self.set_status() diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 8533ffaf75..a699a265b9 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -730,6 +730,29 @@ "unique": 0, "width": "150px" }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "target_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Target Warehouse", + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -1072,7 +1095,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-08-27 02:29:20.603580", + "modified": "2015-09-20 21:26:22.732109", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 51d4e403fc..6339752634 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -113,7 +113,7 @@ class DeliveryNote(SellingController): self.validate_with_previous_doc() from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - make_packing_list(self, 'items') + make_packing_list(self) self.update_current_stock() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index e41aab7324..d233d6b74a 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -15,7 +15,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.doctype.stock_entry.test_stock_entry \ import make_stock_entry, make_serialized_item, get_qty_after_transaction -from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoStatusError +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoWarehouseError from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation class TestDeliveryNote(unittest.TestCase): @@ -152,7 +152,6 @@ class TestDeliveryNote(unittest.TestCase): dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) self.check_serial_no_values(serial_no, { - "status": "Delivered", "warehouse": "", "delivery_document_no": dn.name }) @@ -160,7 +159,6 @@ class TestDeliveryNote(unittest.TestCase): dn.cancel() self.check_serial_no_values(serial_no, { - "status": "Available", "warehouse": "_Test Warehouse - _TC", "delivery_document_no": "" }) @@ -169,12 +167,10 @@ class TestDeliveryNote(unittest.TestCase): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] - frappe.db.set_value("Serial No", serial_no, "status", "Not Available") - dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no, do_not_submit=True) - self.assertRaises(SerialNoStatusError, dn.submit) + self.assertRaises(SerialNoWarehouseError, dn.submit) def check_serial_no_values(self, serial_no, field_values): serial_no = frappe.get_doc("Serial No", serial_no) @@ -295,7 +291,6 @@ class TestDeliveryNote(unittest.TestCase): dn = create_delivery_note(item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no) self.check_serial_no_values(serial_no, { - "status": "Delivered", "warehouse": "", "delivery_document_no": dn.name }) @@ -305,7 +300,6 @@ class TestDeliveryNote(unittest.TestCase): is_return=1, return_against=dn.name, qty=-1, rate=500, serial_no=serial_no) self.check_serial_no_values(serial_no, { - "status": "Sales Returned", "warehouse": "_Test Warehouse - _TC", "delivery_document_no": "" }) @@ -313,7 +307,6 @@ class TestDeliveryNote(unittest.TestCase): dn1.cancel() self.check_serial_no_values(serial_no, { - "status": "Delivered", "warehouse": "", "delivery_document_no": dn.name }) @@ -321,7 +314,6 @@ class TestDeliveryNote(unittest.TestCase): dn.cancel() self.check_serial_no_values(serial_no, { - "status": "Available", "warehouse": "_Test Warehouse - _TC", "delivery_document_no": "", "purchase_document_no": se.name diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index bdf771e54a..ea2e9a378d 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -751,6 +751,29 @@ "unique": 0, "width": "100px" }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "target_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Target Warehouse", + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -1137,7 +1160,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-08-26 08:33:03.676574", + "modified": "2015-09-20 21:26:33.043965", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 330e6cf24a..0250c30606 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -64,7 +64,7 @@ "hidden": 0, "ignore_user_permissions": 0, "in_filter": 1, - "in_list_view": 1, + "in_list_view": 0, "label": "Item Name", "no_copy": 0, "oldfieldname": "item_name", @@ -78,29 +78,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "parent_detail_docname", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Parent Detail docname", - "no_copy": 1, - "oldfieldname": "parent_detail_docname", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_on_submit": 0, "bold": 0, @@ -147,6 +124,27 @@ "unique": 0, "width": "300px" }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -156,7 +154,7 @@ "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, - "in_list_view": 1, + "in_list_view": 0, "label": "Warehouse", "no_copy": 0, "oldfieldname": "warehouse", @@ -171,6 +169,50 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "target_warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Target Warehouse", + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_9", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -324,20 +366,19 @@ "unique": 0 }, { - "allow_on_submit": 0, + "allow_on_submit": 1, "bold": 0, "collapsible": 0, - "fieldname": "uom", - "fieldtype": "Link", + "fieldname": "projected_qty", + "fieldtype": "Float", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "UOM", - "no_copy": 0, - "oldfieldname": "uom", - "oldfieldtype": "Link", - "options": "UOM", + "label": "Projected Qty", + "no_copy": 1, + "oldfieldname": "projected_qty", + "oldfieldtype": "Currency", "permlevel": 0, "print_hide": 0, "read_only": 1, @@ -369,19 +410,43 @@ "unique": 0 }, { - "allow_on_submit": 1, + "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "projected_qty", - "fieldtype": "Float", + "fieldname": "uom", + "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Projected Qty", - "no_copy": 1, - "oldfieldname": "projected_qty", - "oldfieldtype": "Currency", + "label": "UOM", + "no_copy": 0, + "oldfieldname": "uom", + "oldfieldtype": "Link", + "options": "UOM", + "permlevel": 0, + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "fieldname": "page_break", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Page Break", + "no_copy": 0, + "oldfieldname": "page_break", + "oldfieldtype": "Check", "permlevel": 0, "print_hide": 0, "read_only": 1, @@ -415,21 +480,21 @@ "unique": 0 }, { - "allow_on_submit": 1, + "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "page_break", - "fieldtype": "Check", - "hidden": 0, + "fieldname": "parent_detail_docname", + "fieldtype": "Data", + "hidden": 1, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Page Break", - "no_copy": 0, - "oldfieldname": "page_break", - "oldfieldtype": "Check", + "label": "Parent Detail docname", + "no_copy": 1, + "oldfieldname": "parent_detail_docname", + "oldfieldtype": "Data", "permlevel": 0, - "print_hide": 0, + "print_hide": 1, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -446,7 +511,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-08-19 12:45:53.583367", + "modified": "2015-09-21 08:29:43.704286", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index e7a75fe422..4d0d7c5955 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -25,67 +25,67 @@ def get_packing_item_details(item): def get_bin_qty(item, warehouse): det = frappe.db.sql("""select actual_qty, projected_qty from `tabBin` where item_code = %s and warehouse = %s""", (item, warehouse), as_dict = 1) - return det and det[0] or '' + return det and det[0] or frappe._dict() -def update_packing_list_item(obj, packing_item_code, qty, warehouse, line): - bin = get_bin_qty(packing_item_code, warehouse) +def update_packing_list_item(doc, packing_item_code, qty, main_item_row): + bin = get_bin_qty(packing_item_code, main_item_row.warehouse) item = get_packing_item_details(packing_item_code) # check if exists exists = 0 - for d in obj.get("packed_items"): - if d.parent_item == line.item_code and d.item_code == packing_item_code and d.parent_detail_docname == line.name: + for d in doc.get("packed_items"): + if d.parent_item == main_item_row.item_code and d.item_code == packing_item_code and d.parent_detail_docname == main_item_row.name: pi, exists = d, 1 break if not exists: - pi = obj.append('packed_items', {}) + pi = doc.append('packed_items', {}) - pi.parent_item = line.item_code + pi.parent_item = main_item_row.item_code pi.item_code = packing_item_code - pi.item_name = item['item_name'] - pi.parent_detail_docname = line.name - pi.description = item['description'] - pi.uom = item['stock_uom'] + pi.item_name = item.item_name + pi.parent_detail_docname = main_item_row.name + pi.description = item.description + pi.uom = item.stock_uom pi.qty = flt(qty) - pi.actual_qty = bin and flt(bin['actual_qty']) or 0 - pi.projected_qty = bin and flt(bin['projected_qty']) or 0 + pi.actual_qty = flt(bin.get("actual_qty")) + pi.projected_qty = flt(bin.get("projected_qty")) if not pi.warehouse: - pi.warehouse = warehouse + pi.warehouse = main_item_row.warehouse if not pi.batch_no: - pi.batch_no = cstr(line.get("batch_no")) + pi.batch_no = cstr(main_item_row.get("batch_no")) + if not pi.target_warehouse: + pi.target_warehouse = main_item_row.get("target_warehouse") - - -def make_packing_list(obj, item_table_fieldname): +def make_packing_list(doc): """make packing list for Product Bundle item""" - if obj.get("_action") and obj._action == "update_after_submit": return + if doc.get("_action") and doc._action == "update_after_submit": return parent_items = [] - for d in obj.get(item_table_fieldname): + for d in doc.get("items"): if frappe.db.get_value("Product Bundle", {"new_item_code": d.item_code}): for i in get_product_bundle_items(d.item_code): - update_packing_list_item(obj, i['item_code'], flt(i['qty'])*flt(d.qty), d.warehouse, d) + update_packing_list_item(doc, i.item_code, flt(i.qty)*flt(d.qty), d) if [d.item_code, d.name] not in parent_items: parent_items.append([d.item_code, d.name]) - cleanup_packing_list(obj, parent_items) + cleanup_packing_list(doc, parent_items) -def cleanup_packing_list(obj, parent_items): +def cleanup_packing_list(doc, parent_items): """Remove all those child items which are no longer present in main item table""" delete_list = [] - for d in obj.get("packed_items"): + for d in doc.get("packed_items"): if [d.parent_item, d.parent_detail_docname] not in parent_items: # mark for deletion from doclist delete_list.append(d) if not delete_list: - return obj + return doc - packed_items = obj.get("packed_items") - obj.set("packed_items", []) + packed_items = doc.get("packed_items") + doc.set("packed_items", []) for d in packed_items: if d not in delete_list: - obj.append("packed_items", d) + doc.append("packed_items", d) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 50179646cb..7ad6489295 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -163,7 +163,6 @@ class TestPurchaseReceipt(unittest.TestCase): serial_no = get_serial_nos(pr.get("items")[0].serial_no)[0] _check_serial_no_values(serial_no, { - "status": "Available", "warehouse": "_Test Warehouse - _TC", "purchase_document_no": pr.name }) @@ -172,7 +171,6 @@ class TestPurchaseReceipt(unittest.TestCase): is_return=1, return_against=pr.name, serial_no=serial_no) _check_serial_no_values(serial_no, { - "status": "Purchase Returned", "warehouse": "", "purchase_document_no": pr.name, "delivery_document_no": return_pr.name diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 2823e39a51..78f1fecb8e 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -99,32 +99,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "default": "Not Available", - "description": "Only Serial Nos with status \"Available\" can be delivered.", - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 1, - "in_list_view": 1, - "label": "Status", - "no_copy": 1, - "oldfieldname": "status", - "oldfieldtype": "Select", - "options": "Not Available\nAvailable\nDelivered\nPurchase Returned\nSales Returned", - "permlevel": 0, - "print_hide": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, { "allow_on_submit": 0, "bold": 0, @@ -917,7 +891,7 @@ "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-09-07 15:51:26", + "modified": "2015-09-20 20:39:40.751888", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", @@ -986,7 +960,7 @@ ], "read_only": 0, "read_only_onload": 0, - "search_fields": "item_code,status", + "search_fields": "item_code", "sort_field": "modified", "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 0df0ba2a52..0edd8e30a3 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -16,7 +16,6 @@ class SerialNoRequiredError(ValidationError): pass class SerialNoQtyError(ValidationError): pass class SerialNoItemError(ValidationError): pass class SerialNoWarehouseError(ValidationError): pass -class SerialNoStatusError(ValidationError): pass class SerialNoNotExistsError(ValidationError): pass class SerialNoDuplicateError(ValidationError): pass @@ -75,28 +74,6 @@ class SerialNo(StockController): self.brand = item.brand self.warranty_period = item.warranty_period - def set_status(self, last_sle): - if last_sle: - if last_sle.voucher_type == "Stock Entry": - document_type = frappe.db.get_value("Stock Entry", last_sle.voucher_no, "purpose") - else: - document_type = last_sle.voucher_type - - if last_sle.actual_qty > 0: - if document_type in ("Delivery Note", "Sales Invoice", "Sales Return"): - self.status = "Sales Returned" - else: - self.status = "Available" - else: - if document_type in ("Purchase Receipt", "Purchase Invoice", "Purchase Return"): - self.status = "Purchase Returned" - elif document_type in ("Delivery Note", "Sales Invoice"): - self.status = "Delivered" - else: - self.status = "Not Available" - else: - self.status = "Not Available" - def set_purchase_details(self, purchase_sle): if purchase_sle: self.purchase_document_type = purchase_sle.voucher_type @@ -162,10 +139,10 @@ class SerialNo(StockController): return sle_dict def on_trash(self): - if self.status == 'Delivered': - frappe.throw(_("Delivered Serial No {0} cannot be deleted").format(self.name)) if self.warehouse: frappe.throw(_("Cannot delete Serial No {0} in stock. First remove from stock, then delete.").format(self.name)) + elif self.delivery_document_no: + frappe.throw(_("Delivered Serial No {0} cannot be deleted").format(self.name)) def before_rename(self, old, new, merge=False): if merge: @@ -187,7 +164,6 @@ class SerialNo(StockController): def on_stock_ledger_entry(self): if self.via_stock_ledger and not self.get("__islocal"): last_sle = self.get_last_sle() - self.set_status(last_sle.get("last_sle")) self.set_purchase_details(last_sle.get("purchase_sle")) self.set_sales_details(last_sle.get("delivery_sle")) self.set_maintenance_status() @@ -232,10 +208,10 @@ def validate_serial_no(sle, item_det): frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse), SerialNoWarehouseError) - if sle.voucher_type in ("Delivery Note", "Sales Invoice") and sle.is_cancelled=="No" \ - and sr.status != "Available": - frappe.throw(_("Serial No {0} status must be 'Available' to Deliver").format(serial_no), - SerialNoStatusError) + if sle.voucher_type in ("Delivery Note", "Sales Invoice") \ + and sle.is_cancelled=="No" and not sr.warehouse: + frappe.throw(_("Serial No {0} does not belong to any Warehouse") + .format(serial_no), SerialNoWarehouseError) elif sle.actual_qty < 0: # transfer out @@ -287,7 +263,6 @@ def make_serial_no(serial_no, sle): sr.insert() sr.warehouse = sle.warehouse - sr.status = "Available" sr.save() frappe.msgprint(_("Serial No {0} created").format(sr.name)) return sr.name diff --git a/erpnext/stock/report/serial_no_status/serial_no_status.json b/erpnext/stock/report/serial_no_status/serial_no_status.json index ecfa3a0669..917d26f9e6 100644 --- a/erpnext/stock/report/serial_no_status/serial_no_status.json +++ b/erpnext/stock/report/serial_no_status/serial_no_status.json @@ -1,12 +1,14 @@ { + "add_total_row": 0, "apply_user_permissions": 1, "creation": "2013-01-14 10:52:58", + "disabled": 0, "docstatus": 0, "doctype": "Report", "idx": 1, "is_standard": "Yes", - "json": "{\"sort_by\": \"Serial No.name\", \"sort_order\": \"desc\", \"sort_by_next\": \"\", \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warehouse\", \"Serial No\"], [\"status\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"], [\"purchase_document_no\", \"Serial No\"], [\"purchase_date\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"purchase_rate\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"delivery_date\", \"Serial No\"], [\"supplier\", \"Serial No\"], [\"supplier_name\", \"Serial No\"]]}", - "modified": "2014-06-03 07:18:17.327622", + "json": "{\"filters\":[],\"columns\":[[\"name\",\"Serial No\"],[\"item_code\",\"Serial No\"],[\"warehouse\",\"Serial No\"],[\"status\",\"Serial No\"],[\"item_name\",\"Serial No\"],[\"description\",\"Serial No\"],[\"item_group\",\"Serial No\"],[\"brand\",\"Serial No\"],[\"purchase_document_no\",\"Serial No\"],[\"purchase_date\",\"Serial No\"],[\"customer\",\"Serial No\"],[\"customer_name\",\"Serial No\"],[\"purchase_rate\",\"Serial No\"],[\"delivery_document_no\",\"Serial No\"],[\"delivery_date\",\"Serial No\"],[\"supplier\",\"Serial No\"],[\"supplier_name\",\"Serial No\"]],\"sort_by\":\"Serial No.name\",\"sort_order\":\"desc\",\"sort_by_next\":null,\"sort_order_next\":\"desc\"}", + "modified": "2015-09-20 21:09:49.441973", "modified_by": "Administrator", "module": "Stock", "name": "Serial No Status", diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index cba2464efd..dd62e15c04 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -214,8 +214,7 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin def reset_serial_no_status_and_warehouse(serial_nos=None): if not serial_nos: - serial_nos = frappe.db.sql_list("""select name from `tabSerial No` where status != 'Not in Use' - and docstatus = 0""") + serial_nos = frappe.db.sql_list("""select name from `tabSerial No` where docstatus = 0""") for serial_no in serial_nos: try: sr = frappe.get_doc("Serial No", serial_no) @@ -228,8 +227,6 @@ def reset_serial_no_status_and_warehouse(serial_nos=None): except: pass - frappe.db.sql("""update `tabSerial No` set warehouse='' where status in ('Delivered', 'Purchase Returned')""") - def repost_all_stock_vouchers(): warehouses_with_account = frappe.db.sql_list("""select master_name from tabAccount where ifnull(account_type, '') = 'Warehouse'""") From b445be3552fc91ea5390771d44841717deccf7b8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 21 Sep 2015 10:56:02 +0530 Subject: [PATCH 2/6] Test case for delivery to target warehouse --- .../sales_invoice/test_sales_invoice.py | 21 +++---- erpnext/controllers/selling_controller.py | 2 +- .../delivery_note/test_delivery_note.py | 61 +++++++++++++++++-- .../test_landed_cost_voucher.py | 3 +- 4 files changed, 68 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 20c734ce5c..fc8ca632a6 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -8,6 +8,8 @@ from frappe.utils import nowdate, add_days, flt from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency +from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError +from frappe.model.naming import make_autoname class TestSalesInvoice(unittest.TestCase): def make(self): @@ -706,27 +708,22 @@ class TestSalesInvoice(unittest.TestCase): "delivery_document_no")) def test_serialize_status(self): - from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError, get_serial_nos, SerialNoDuplicateError - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - - se = make_serialized_item() - serial_nos = get_serial_nos(se.get("items")[0].serial_no) - - sr = frappe.get_doc("Serial No", serial_nos[0]) - sr.save() + serial_no = frappe.get_doc({ + "doctype": "Serial No", + "item_code": "_Test Serialized Item With Series", + "serial_no": make_autoname("SR", "Serial No") + }) + serial_no.save() si = frappe.copy_doc(test_records[0]) si.update_stock = 1 si.get("items")[0].item_code = "_Test Serialized Item With Series" si.get("items")[0].qty = 1 - si.get("items")[0].serial_no = serial_nos[0] + si.get("items")[0].serial_no = serial_no.name si.insert() self.assertRaises(SerialNoWarehouseError, si.submit) - # hack! because stock ledger entires are already inserted and are not rolled back! - self.assertRaises(SerialNoDuplicateError, si.cancel) - def test_invoice_due_date_against_customers_credit_days(self): # set customer's credit days frappe.db.set_value("Customer", "_Test Customer", "credit_days_based_on", "Fixed Days") diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 75be4d684c..b087b8a884 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -186,7 +186,7 @@ class SellingController(StockController): 'batch_no': cstr(d.get("batch_no")).strip(), 'serial_no': cstr(d.get("serial_no")).strip(), 'name': d.name, - 'target_warehouse': p.target_warehouse + 'target_warehouse': d.target_warehouse })) return il diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index d233d6b74a..af52c7a9ab 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -164,11 +164,16 @@ class TestDeliveryNote(unittest.TestCase): }) def test_serialize_status(self): - se = make_serialized_item() - serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + from frappe.model.naming import make_autoname + serial_no = frappe.get_doc({ + "doctype": "Serial No", + "item_code": "_Test Serialized Item With Series", + "serial_no": make_autoname("SR", "Serial No") + }) + serial_no.save() dn = create_delivery_note(item_code="_Test Serialized Item With Series", - serial_no=serial_no, do_not_submit=True) + serial_no=serial_no.name, do_not_submit=True) self.assertRaises(SerialNoWarehouseError, dn.submit) @@ -318,7 +323,54 @@ class TestDeliveryNote(unittest.TestCase): "delivery_document_no": "", "purchase_document_no": se.name }) + + def test_delivery_of_bundled_items_to_target_warehouse(self): + set_perpetual_inventory() + + create_stock_reconciliation(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, rate=100) + create_stock_reconciliation(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", + qty=50, rate=100) + + opening_qty_test_warehouse_1 = get_qty_after_transaction(warehouse="_Test Warehouse 1 - _TC") + + dn = create_delivery_note(item_code="_Test Product Bundle Item", + qty=5, rate=500, target_warehouse="_Test Warehouse 1 - _TC") + + # qty after delivery + actual_qty = get_qty_after_transaction(warehouse="_Test Warehouse - _TC") + self.assertEquals(actual_qty, 25) + + actual_qty = get_qty_after_transaction(warehouse="_Test Warehouse 1 - _TC") + self.assertEquals(actual_qty, opening_qty_test_warehouse_1 + 25) + + # stock value diff for source warehouse + stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn.name, "item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + "stock_value_difference") + + # stock value diff for target warehouse + stock_value_difference1 = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", + "voucher_no": dn.name, "item_code": "_Test Item", "warehouse": "_Test Warehouse 1 - _TC"}, + "stock_value_difference") + self.assertEquals(abs(stock_value_difference), stock_value_difference1) + # Check gl entries + gl_entries = get_gl_entries("Delivery Note", dn.name) + self.assertTrue(gl_entries) + + stock_value_difference = abs(frappe.db.sql("""select sum(stock_value_difference) + from `tabStock Ledger Entry` where voucher_type='Delivery Note' and voucher_no=%s + and warehouse='_Test Warehouse - _TC'""", dn.name)[0][0]) + + expected_values = { + "_Test Warehouse - _TC": [0.0, stock_value_difference], + "_Test Warehouse 1 - _TC": [stock_value_difference, 0.0] + } + for i, gle in enumerate(gl_entries): + self.assertEquals([gle.debit, gle.credit], expected_values.get(gle.account)) + + set_perpetual_inventory(0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) @@ -341,7 +393,8 @@ def create_delivery_note(**args): "conversion_factor": 1.0, "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", - "serial_no": args.serial_no + "serial_no": args.serial_no, + "target_warehouse": args.target_warehouse }) if not args.do_not_save: 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 af569f850c..89f3ad565e 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 @@ -65,9 +65,8 @@ class TestLandedCostVoucher(unittest.TestCase): self.submit_landed_cost_voucher(pr) serial_no = frappe.db.get_value("Serial No", "SN001", - ["status", "warehouse", "purchase_rate"], as_dict=1) + ["warehouse", "purchase_rate"], as_dict=1) - self.assertEquals(serial_no.status, "Available") self.assertEquals(serial_no.purchase_rate - serial_no_rate, 5.0) self.assertEquals(serial_no.warehouse, "_Test Warehouse - _TC") From e07958bbda9ca362a19c2c93f1f8d48dd1360883 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 21 Sep 2015 10:56:50 +0530 Subject: [PATCH 3/6] Change log and sopnsors for delivery to target warehouse --- .../change_log/current/delivery_to_target_warehouse.md | 1 + sponsors.md | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 erpnext/change_log/current/delivery_to_target_warehouse.md diff --git a/erpnext/change_log/current/delivery_to_target_warehouse.md b/erpnext/change_log/current/delivery_to_target_warehouse.md new file mode 100644 index 0000000000..a5159649c6 --- /dev/null +++ b/erpnext/change_log/current/delivery_to_target_warehouse.md @@ -0,0 +1 @@ +- Delivery to Target Warehouse: Now you can deliver items to customer's warehouse or rented warehouse. Sponsored by: **[Startrack](http://www.gps.gt/)** diff --git a/sponsors.md b/sponsors.md index 8e10081d47..d3fbc2113a 100644 --- a/sponsors.md +++ b/sponsors.md @@ -45,5 +45,13 @@ For Mandrill Integration #3546 + + + Startrack + + + For Delivery to Target Warehouse #3546 + + From d91382dbf32d7f05a5f07eab9c3416868df3d72e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 21 Sep 2015 11:02:26 +0530 Subject: [PATCH 4/6] [fix] serial no status --- erpnext/change_log/current/delivery_to_target_warehouse.md | 1 + erpnext/stock/doctype/serial_no/serial_no.js | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/erpnext/change_log/current/delivery_to_target_warehouse.md b/erpnext/change_log/current/delivery_to_target_warehouse.md index a5159649c6..f369efa10e 100644 --- a/erpnext/change_log/current/delivery_to_target_warehouse.md +++ b/erpnext/change_log/current/delivery_to_target_warehouse.md @@ -1 +1,2 @@ - Delivery to Target Warehouse: Now you can deliver items to customer's warehouse or rented warehouse. Sponsored by: **[Startrack](http://www.gps.gt/)** +- Serial No **Status** deprecated diff --git a/erpnext/stock/doctype/serial_no/serial_no.js b/erpnext/stock/doctype/serial_no/serial_no.js index eea652b871..9d5555ed63 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.js +++ b/erpnext/stock/doctype/serial_no/serial_no.js @@ -17,10 +17,4 @@ cur_frm.cscript.onload = function() { frappe.ui.form.on("Serial No", "refresh", function(frm) { frm.toggle_enable("item_code", frm.doc.__islocal); - - if(frm.doc.status == "Sales Returned" && frm.doc.warehouse) - cur_frm.add_custom_button(__('Set Status as Available'), function() { - cur_frm.set_value("status", "Available"); - cur_frm.save(); - }, "icon-ok", "btn-default"); }); From 03afb45e348e09c3ec5f62c87add28715294e893 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 22 Sep 2015 09:09:11 +0530 Subject: [PATCH 5/6] [fix] Validate deletion of serial no --- erpnext/stock/doctype/serial_no/serial_no.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 0edd8e30a3..2c6a5238c7 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -139,10 +139,19 @@ class SerialNo(StockController): return sle_dict def on_trash(self): - if self.warehouse: - frappe.throw(_("Cannot delete Serial No {0} in stock. First remove from stock, then delete.").format(self.name)) - elif self.delivery_document_no: - frappe.throw(_("Delivered Serial No {0} cannot be deleted").format(self.name)) + sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry` + where serial_no like %s and item_code=%s and ifnull(is_cancelled, 'No')='No'""", + ("%%%s%%" % self.name, self.item_code), as_dict=True) + + # Find the exact match + sle_exists = False + for d in sl_entries: + if self.name.upper() in get_serial_nos(d.serial_no): + sle_exists = True + break + + if sle_exists: + frappe.throw(_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name)) def before_rename(self, old, new, merge=False): if merge: From 38e4c6f2afe1b865e35320121b076f51c8acf07c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 13 Oct 2015 18:37:39 +0530 Subject: [PATCH 6/6] Allow same serial nos for raw materials and fg item in repack / manufacture entry --- erpnext/stock/doctype/serial_no/serial_no.py | 29 +++++++++++++++---- .../stock/doctype/stock_entry/stock_entry.py | 26 ++++++++++------- .../doctype/stock_entry/test_stock_entry.py | 24 ++++++++++++++- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 2c6a5238c7..07ac326446 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -53,7 +53,7 @@ class SerialNo(StockController): if not self.get("__islocal"): item_code, warehouse = frappe.db.get_value("Serial No", self.name, ["item_code", "warehouse"]) - if item_code != self.item_code: + if not self.via_stock_ledger and item_code != self.item_code: frappe.throw(_("Item Code cannot be changed for Serial No."), SerialNoCannotCannotChangeError) if not self.via_stock_ledger and warehouse != self.warehouse: @@ -205,9 +205,10 @@ def validate_serial_no(sle, item_det): sr = frappe.get_doc("Serial No", serial_no) if sr.item_code!=sle.item_code: - frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, - sle.item_code), SerialNoItemError) - + if not allow_serial_nos_with_different_item(serial_no, sle): + frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, + sle.item_code), SerialNoItemError) + if sr.warehouse and sle.actual_qty > 0: frappe.throw(_("Serial No {0} has already been received").format(sr.name), SerialNoDuplicateError) @@ -228,7 +229,24 @@ def validate_serial_no(sle, item_det): elif sle.actual_qty < 0 or not item_det.serial_no_series: frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError) - + +def allow_serial_nos_with_different_item(sle_serial_no, sle): + """ + Allows same serial nos for raw materials and finished goods + in Manufacture / Repack type Stock Entry + """ + allow_serial_nos = False + if sle.voucher_type=="Stock Entry" and sle.actual_qty > 0: + stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no) + if stock_entry.purpose in ("Repack", "Manufacture"): + for d in stock_entry.get("items"): + if d.serial_no and (d.s_warehouse if sle.is_cancelled=="No" else d.t_warehouse): + serial_nos = get_serial_nos(d.serial_no) + if sle_serial_no in serial_nos: + allow_serial_nos = True + + return allow_serial_nos + def update_serial_nos(sle, item_det): if sle.is_cancelled == "No" and not sle.serial_no and sle.actual_qty > 0 \ and item_det.has_serial_no == 1 and item_det.serial_no_series: @@ -245,6 +263,7 @@ def update_serial_nos(sle, item_det): if frappe.db.exists("Serial No", serial_no): sr = frappe.get_doc("Serial No", serial_no) sr.via_stock_ledger = True + sr.item_code = sle.item_code sr.warehouse = sle.warehouse if sle.actual_qty > 0 else None sr.save(ignore_permissions=True) elif sle.actual_qty > 0: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 18d600d4d8..868053e5a8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -359,14 +359,17 @@ class StockEntry(StockController): def update_stock_ledger(self): sl_entries = [] + + # make sl entries for source warehouse first, then do for target warehouse for d in self.get('items'): - if cstr(d.s_warehouse) and self.docstatus == 1: + if cstr(d.s_warehouse): sl_entries.append(self.get_sl_entries(d, { "warehouse": cstr(d.s_warehouse), "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0 })) - + + for d in self.get('items'): if cstr(d.t_warehouse): sl_entries.append(self.get_sl_entries(d, { "warehouse": cstr(d.t_warehouse), @@ -374,15 +377,18 @@ class StockEntry(StockController): "incoming_rate": flt(d.valuation_rate) })) - # On cancellation, make stock ledger entry for - # target warehouse first, to update serial no values properly + # On cancellation, make stock ledger entry for + # target warehouse first, to update serial no values properly - if cstr(d.s_warehouse) and self.docstatus == 2: - sl_entries.append(self.get_sl_entries(d, { - "warehouse": cstr(d.s_warehouse), - "actual_qty": -flt(d.transfer_qty), - "incoming_rate": 0 - })) + # if cstr(d.s_warehouse) and self.docstatus == 2: + # sl_entries.append(self.get_sl_entries(d, { + # "warehouse": cstr(d.s_warehouse), + # "actual_qty": -flt(d.transfer_qty), + # "incoming_rate": 0 + # })) + + if self.docstatus == 2: + sl_entries.reverse() self.make_sl_entries(sl_entries, self.amended_from and 'Yes' or 'No') diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 62c55fde50..252deaad36 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -566,6 +566,27 @@ class TestStockEntry(unittest.TestCase): stock_entry = frappe.get_doc(make_stock_entry(production_order.name, "Manufacture", 1)) stock_entry.insert() self.assertTrue("_Test Variant Item-S" in [d.item_code for d in stock_entry.items]) + + def test_same_serial_nos_in_repack_or_manufacture_entries(self): + s1 = make_serialized_item(target_warehouse="_Test Warehouse - _TC") + serial_nos = s1.get("items")[0].serial_no + + s2 = make_stock_entry(item_code="_Test Serialized Item With Series", source="_Test Warehouse - _TC", + qty=2, basic_rate=100, purpose="Repack", serial_no=serial_nos, do_not_save=True) + + s2.append("items", { + "item_code": "_Test Serialized Item", + "t_warehouse": "_Test Warehouse - _TC", + "qty": 2, + "basic_rate": 120, + "expense_account": "Stock Adjustment - _TC", + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "serial_no": serial_nos + }) + + s2.submit() + s2.cancel() def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se = frappe.copy_doc(test_records[0]) @@ -616,7 +637,8 @@ def make_stock_entry(**args): "basic_rate": args.basic_rate, "expense_account": args.expense_account or "Stock Adjustment - _TC", "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC" + "cost_center": "_Test Cost Center - _TC", + "serial_no": args.serial_no }) if not args.do_not_save: