diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 007afe4b63..3fbde29bbf 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -323,7 +323,8 @@ cur_frm.cscript.hide_fields = function(doc) { } } - item_fields_stock = ['serial_no', 'batch_no', 'actual_qty', 'expense_account', 'warehouse', 'expense_account', 'warehouse'] + item_fields_stock = ['batch_no', 'actual_batch_qty', 'actual_qty', 'expense_account', + 'warehouse', 'expense_account', 'quality_inspection'] cur_frm.fields_dict['items'].grid.set_column_disp(item_fields_stock, (cint(doc.update_stock)==1 || cint(doc.is_return)==1 ? true : false)); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6e3990ac8d..f32cfcd232 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -88,6 +88,8 @@ class SalesInvoice(SellingController): self.validate_c_form() self.validate_time_sheets_are_submitted() self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items") + if not self.is_return: + self.validate_serial_numbers() self.update_packing_list() self.set_billing_hours_and_amount() self.update_timesheet_billing_for_project() @@ -125,6 +127,7 @@ class SalesInvoice(SellingController): if not self.is_return: self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.check_credit_limit() + self.update_serial_no() if not cint(self.is_pos) == 1 and not self.is_return: self.update_against_document_in_jv() @@ -155,6 +158,7 @@ class SalesInvoice(SellingController): if not self.is_return: self.update_billing_status_for_zero_amount_refdoc("Sales Order") + self.update_serial_no(in_cancel=True) self.validate_c_form_on_cancel() @@ -781,6 +785,61 @@ class SalesInvoice(SellingController): self.due_date = None + def update_serial_no(self, in_cancel=False): + """ update Sales Invoice refrence in Serial No """ + + for item in self.items: + if not item.serial_no: + continue + + serial_nos = ["'%s'"%serial_no for serial_no in item.serial_no.split("\n")] + + frappe.db.sql(""" update `tabSerial No` set sales_invoice='{invoice}' + where name in ({serial_nos})""".format( + invoice='' if in_cancel else self.name, + serial_nos=",".join(serial_nos) + ) + ) + + def validate_serial_numbers(self): + """ + validate serial number agains Delivery Note and Sales Invoice + """ + self.validate_serial_against_delivery_note() + self.validate_serial_against_sales_invoice() + + def validate_serial_against_delivery_note(self): + """ + validate if the serial numbers in Sales Invoice Items are same as in + Delivery Note Item + """ + + for item in self.items: + if not item.delivery_note or not item.dn_detail: + continue + + serial_nos = frappe.db.get_value("Delivery Note Item", item.dn_detail, "serial_no") or "" + dn_serial_nos = set(serial_nos.split("\n")) + + serial_nos = item.serial_no or "" + si_serial_nos = set(serial_nos.split("\n")) + + if si_serial_nos - dn_serial_nos: + frappe.throw(_("Serial Numbers in row {0} does not match with Delivery Note".format(item.idx))) + + def validate_serial_against_sales_invoice(self): + """ check if serial number is already used in other sales invoice """ + for item in self.items: + if not item.serial_no: + continue + + for serial_no in item.serial_no.split("\n"): + sales_invoice = frappe.db.get_value("Serial No", serial_no, "sales_invoice") + if sales_invoice and self.name != sales_invoice: + frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}".format( + serial_no, sales_invoice + ))) + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context list_context = get_list_context(context) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 0ada8478a1..8335879554 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -737,6 +737,12 @@ class TestSalesInvoice(unittest.TestCase): 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) + self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"), + si.name) + + # check if the serial number is already linked with any other Sales Invoice + _si = frappe.copy_doc(si.as_dict()) + self.assertRaises(frappe.ValidationError, _si.insert) return si @@ -750,6 +756,7 @@ class TestSalesInvoice(unittest.TestCase): 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")) + self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice")) def test_serialize_status(self): serial_no = frappe.get_doc({ @@ -768,6 +775,27 @@ class TestSalesInvoice(unittest.TestCase): self.assertRaises(SerialNoWarehouseError, si.submit) + def test_serial_numbers_against_delivery_note(self): + """ + check if the sales invoice item serial numbers and the delivery note items + serial numbers are same + """ + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + se = make_serialized_item() + serial_nos = get_serial_nos(se.get("items")[0].serial_no) + + dn = create_delivery_note(item=se.get("items")[0].item_code, serial_no=serial_nos[0]) + dn.submit() + + si = make_sales_invoice(dn.name) + si.save() + + self.assertEquals(si.get("items")[0].serial_no, dn.get("items")[0].serial_no) + 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/patches.txt b/erpnext/patches.txt index ac91f6038b..0faf98e688 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -396,3 +396,4 @@ erpnext.patches.v8_0.merge_student_batch_and_student_group erpnext.patches.v8_0.rename_total_margin_to_rate_with_margin # 11-05-2017 erpnext.patches.v8_0.fix_status_for_invoices_with_negative_outstanding erpnext.patches.v8_0.make_payments_table_blank_for_non_pos_invoice +erpnext.patches.v8_0.set_sales_invoice_serial_number_from_delivery_note \ No newline at end of file diff --git a/erpnext/patches/v8_0/set_sales_invoice_serial_number_from_delivery_note.py b/erpnext/patches/v8_0/set_sales_invoice_serial_number_from_delivery_note.py new file mode 100644 index 0000000000..2ae74cdbbc --- /dev/null +++ b/erpnext/patches/v8_0/set_sales_invoice_serial_number_from_delivery_note.py @@ -0,0 +1,42 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty + +def execute(): + """ Set the Serial Numbers in Sales Invoice Item from Delivery Note Item """ + + frappe.reload_doc("stock", "doctype", "serial_no") + + frappe.db.sql(""" update `tabSales Invoice Item` sii inner join + `tabDelivery Note Item` dni on sii.dn_detail=dni.name and sii.qty=dni.qty + set sii.serial_no=dni.serial_no where sii.parent IN (select si.name + from `tabSales Invoice` si where si.update_stock=0 and si.docstatus=1)""") + + items = frappe.db.sql(""" select sii.parent, sii.serial_no from `tabSales Invoice Item` sii + left join `tabSales Invoice` si on sii.parent=si.name + where si.docstatus=1 and si.update_stock=0""", as_dict=True) + + for item in items: + sales_invoice = item.get("parent", None) + serial_nos = item.get("serial_no", "") + + if not sales_invoice or not serial_nos: + continue + + serial_nos = ["'%s'"%no for no in serial_nos.split("\n")] + + frappe.db.sql(""" + UPDATE + `tabSerial No` + SET + sales_invoice='{sales_invoice}' + WHERE + name in ({serial_nos}) + """.format( + sales_invoice=sales_invoice, + serial_nos=",".join(serial_nos) + ) + ) \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 3b65ff8c26..b37713be96 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 1, "autoname": "field:serial_no", @@ -13,6 +14,7 @@ "editable_grid": 0, "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -42,6 +44,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -69,6 +72,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -99,6 +103,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -130,6 +135,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -162,6 +168,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -189,6 +196,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -217,6 +225,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -248,6 +257,7 @@ "width": "300px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -280,6 +290,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -311,6 +322,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -339,6 +351,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -367,6 +380,7 @@ "width": "50%" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -396,6 +410,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -425,6 +440,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -455,6 +471,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -483,6 +500,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -514,6 +532,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -542,6 +561,7 @@ "width": "50%" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -571,6 +591,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 1, "collapsible": 0, @@ -599,6 +620,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -628,6 +650,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -657,6 +680,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -686,6 +710,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -716,6 +741,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -744,6 +770,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -775,6 +802,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -803,6 +831,7 @@ "width": "50%" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -834,6 +863,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 1, "collapsible": 0, @@ -864,6 +894,68 @@ "unique": 0 }, { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "invoice_details", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Invoice Details", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sales_invoice", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sales Invoice", + "length": 0, + "no_copy": 0, + "options": "Sales Invoice", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -892,6 +984,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -920,6 +1013,7 @@ "width": "50%" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -952,6 +1046,7 @@ "width": "150px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -983,6 +1078,7 @@ "width": "150px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1011,6 +1107,7 @@ "width": "50%" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1042,6 +1139,7 @@ "width": "150px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1073,6 +1171,7 @@ "width": "150px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1101,6 +1200,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1129,6 +1229,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1158,18 +1259,18 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "icon": "fa fa-barcode", "idx": 1, "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-02-20 13:26:55.864960", + "modified": "2017-05-15 18:22:23.685286", "modified_by": "Administrator", "module": "Stock", "name": "Serial No",