From ba6e1447eff415757b001931b1b5a4839ee07c65 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 22 Mar 2023 23:21:47 +0530 Subject: [PATCH] refactor: serial and batch bundle for Maintenance Schedule --- .../maintenance_schedule.js | 13 ++++ .../maintenance_schedule.py | 56 +++++++++++++---- .../maintenance_schedule_item.json | 25 ++++++-- .../serial_and_batch_bundle.json | 8 +-- .../serial_and_batch_bundle.py | 63 +++++++++---------- .../stock/doctype/serial_no/serial_no.json | 4 ++ erpnext/stock/serial_batch_bundle.py | 2 + 7 files changed, 118 insertions(+), 53 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js index 5252798ba5..4480ae5144 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js @@ -7,6 +7,19 @@ frappe.ui.form.on('Maintenance Schedule', { frm.set_query('contact_person', erpnext.queries.contact_query); frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer', erpnext.queries.customer); + + frm.set_query('serial_and_batch_bundle', 'items', (doc, cdt, cdn) => { + let item = locals[cdt][cdn]; + + return { + filters: { + 'item_code': item.item_code, + 'voucher_type': 'Maintenance Schedule', + 'type_of_transaction': 'Maintenance', + 'company': doc.company, + } + } + }); }, onload: function (frm) { if (!frm.doc.status) { diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 95e2d694a5..e5bb9e8c2e 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -7,7 +7,6 @@ from frappe.utils import add_days, cint, cstr, date_diff, formatdate, getdate from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos -from erpnext.stock.utils import get_valid_serial_nos from erpnext.utilities.transaction_base import TransactionBase, delete_events @@ -74,10 +73,14 @@ class MaintenanceSchedule(TransactionBase): email_map = {} for d in self.get("items"): - if d.serial_no: - serial_nos = get_valid_serial_nos(d.serial_no) - self.validate_serial_no(d.item_code, serial_nos, d.start_date) - self.update_amc_date(serial_nos, d.end_date) + if d.serial_and_batch_bundle: + serial_nos = frappe.get_doc( + "Serial and Batch Bundle", d.serial_and_batch_bundle + ).get_serial_nos() + + if serial_nos: + self.validate_serial_no(d.item_code, serial_nos, d.start_date) + self.update_amc_date(serial_nos, d.end_date) no_email_sp = [] if d.sales_person not in email_map: @@ -241,9 +244,27 @@ class MaintenanceSchedule(TransactionBase): self.validate_maintenance_detail() self.validate_dates_with_periodicity() self.validate_sales_order() + self.validate_serial_no_bundle() if not self.schedules or self.validate_items_table_change() or self.validate_no_of_visits(): self.generate_schedule() + def validate_serial_no_bundle(self): + ids = [d.serial_and_batch_bundle for d in self.items if d.serial_and_batch_bundle] + + if not ids: + return + + voucher_nos = frappe.get_all( + "Serial and Batch Bundle", fields=["name", "voucher_type"], filters={"name": ("in", ids)} + ) + + for row in voucher_nos: + if row.voucher_type != "Maintenance Schedule": + msg = f"""Serial and Batch Bundle {row.name} + should have voucher type as 'Maintenance Schedule'""" + + frappe.throw(_(msg)) + def on_update(self): self.db_set("status", "Draft") @@ -341,9 +362,14 @@ class MaintenanceSchedule(TransactionBase): def on_cancel(self): for d in self.get("items"): - if d.serial_no: - serial_nos = get_valid_serial_nos(d.serial_no) - self.update_amc_date(serial_nos) + if d.serial_and_batch_bundle: + serial_nos = frappe.get_doc( + "Serial and Batch Bundle", d.serial_and_batch_bundle + ).get_serial_nos() + + if serial_nos: + self.update_amc_date(serial_nos) + self.db_set("status", "Cancelled") delete_events(self.doctype, self.name) @@ -397,11 +423,15 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No target.maintenance_schedule_detail = s_id def update_serial(source, target, parent): - serial_nos = get_serial_nos(target.serial_no) - if len(serial_nos) == 1: - target.serial_no = serial_nos[0] - else: - target.serial_no = "" + if source.serial_and_batch_bundle: + serial_nos = frappe.get_doc( + "Serial and Batch Bundle", source.serial_and_batch_bundle + ).get_serial_nos() + + if len(serial_nos) == 1: + target.serial_no = serial_nos[0] + else: + target.serial_no = "" doclist = get_mapped_doc( "Maintenance Schedule", diff --git a/erpnext/maintenance/doctype/maintenance_schedule_item/maintenance_schedule_item.json b/erpnext/maintenance/doctype/maintenance_schedule_item/maintenance_schedule_item.json index 3dacdead62..d8e02cfadc 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule_item/maintenance_schedule_item.json +++ b/erpnext/maintenance/doctype/maintenance_schedule_item/maintenance_schedule_item.json @@ -20,7 +20,9 @@ "sales_person", "reference", "serial_no", - "sales_order" + "sales_order", + "column_break_ugqr", + "serial_and_batch_bundle" ], "fields": [ { @@ -121,7 +123,8 @@ "fieldtype": "Small Text", "label": "Serial No", "oldfieldname": "serial_no", - "oldfieldtype": "Small Text" + "oldfieldtype": "Small Text", + "read_only": 1 }, { "fieldname": "sales_order", @@ -144,17 +147,31 @@ { "fieldname": "column_break_10", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_ugqr", + "fieldtype": "Column Break" + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-04-15 16:09:47.311994", + "modified": "2023-03-22 18:44:36.816037", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Schedule Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 00d6b3f72b..337c6dda2e 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -177,14 +177,14 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Warehouse", - "options": "Warehouse", - "reqd": 1 + "mandatory_depends_on": "eval:doc.type_of_transaction != \"Maintenance\"", + "options": "Warehouse" }, { "fieldname": "type_of_transaction", "fieldtype": "Select", "label": "Type of Transaction", - "options": "\nInward\nOutward", + "options": "\nInward\nOutward\nMaintenance", "reqd": 1 }, { @@ -237,7 +237,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-03-21 10:52:25.105421", + "modified": "2023-03-22 18:56:37.035516", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 9013ef07d7..c06f63f203 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -8,7 +8,7 @@ import frappe from frappe import _, bold from frappe.model.document import Document from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, flt, get_link_to_form, today +from frappe.utils import add_days, cint, flt, get_link_to_form, today from pypika import Case from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation @@ -22,11 +22,14 @@ class SerialandBatchBundle(Document): def validate(self): self.validate_serial_and_batch_no() self.validate_duplicate_serial_and_batch_no() - # self.validate_voucher_no() - self.check_future_entries_exists() - self.validate_serial_nos_inventory() + self.validate_voucher_no() def before_save(self): + if self.type_of_transaction == "Maintenance": + return + + self.check_future_entries_exists() + self.validate_serial_nos_inventory() self.set_is_outward() self.calculate_qty_and_amount() self.set_warehouse() @@ -97,7 +100,7 @@ class SerialandBatchBundle(Document): d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0)) else: d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no)) - available_qty = sn_obj.batch_available_qty.get(d.batch_no) + d.qty + available_qty = flt(sn_obj.batch_available_qty.get(d.batch_no)) + flt(d.qty) self.validate_negative_batch(d.batch_no, available_qty) @@ -184,35 +187,37 @@ class SerialandBatchBundle(Document): self.set_incoming_rate(save=True, row=row) self.calculate_qty_and_amount(save=True) self.validate_quantity(row) + self.set_warranty_expiry_date(row) - def validate_voucher_no(self): - if self.is_new(): + def set_warranty_expiry_date(self): + if not (self.docstatus == 1 and self.voucher_type == "Delivery Note" and self.has_serial_no): return + warranty_period = frappe.get_cached_value("Item", self.item_code, "warranty_period") + + if not warranty_period: + return + + warranty_expiry_date = add_days(self.posting_date, cint(warranty_period)) + + serial_nos = self.get_serial_nos() + if not serial_nos: + return + + sn_table = frappe.qb.DocType("Serial No") + ( + frappe.qb.update(sn_table) + .set(sn_table.warranty_expiry_date, warranty_expiry_date) + .where(sn_table.name.isin(serial_nos)) + ).run() + + def validate_voucher_no(self): if not (self.voucher_type and self.voucher_no): return - if not frappe.db.exists(self.voucher_type, self.voucher_no): + if self.voucher_no and not frappe.db.exists(self.voucher_type, self.voucher_no): frappe.throw(_(f"The {self.voucher_type} # {self.voucher_no} does not exist")) - bundles = frappe.get_all( - "Serial and Batch Bundle", - filters={ - "voucher_no": self.voucher_no, - "is_cancelled": 0, - "name": ["!=", self.name], - "item_code": self.item_code, - "warehouse": self.warehouse, - }, - ) - - if bundles: - frappe.throw( - _( - f"The {self.voucher_type} # {self.voucher_no} already has a Serial and Batch Bundle {bundles[0].name}" - ) - ) - def check_future_entries_exists(self): if not self.has_serial_no: return @@ -413,12 +418,6 @@ class SerialandBatchBundle(Document): self.delink_reference_from_batch() self.clear_table() - def on_update(self): - self.validate_negative_stock() - - def validate_negative_stock(self): - pass - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 1750439c4d..8dba69832d 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -79,12 +79,15 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Data", "label": "Item Name", "read_only": 1 }, { + "fetch_from": "item_code.description", "fieldname": "description", "fieldtype": "Text", "label": "Description", @@ -188,6 +191,7 @@ "width": "150px" }, { + "fetch_from": "item_code.warranty_period", "fieldname": "warranty_period", "fieldtype": "Int", "label": "Warranty Period (Days)", diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index f82c309f94..f2de819a50 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -385,6 +385,7 @@ class SerialNoBundleValuation(DeprecatedSerialNoValuation): AND child.serial_no IN ({', '.join([frappe.db.escape(s) for s in serial_nos])}) AND child.is_outward = 0 AND parent.docstatus = 1 + AND parent.type_of_transaction != 'Maintenance' AND parent.is_cancelled = 0 AND child.warehouse = {frappe.db.escape(self.sle.warehouse)} AND parent.item_code = {frappe.db.escape(self.sle.item_code)} @@ -521,6 +522,7 @@ class BatchNoBundleValuation(DeprecatedBatchNoValuation): & (parent.item_code == self.sle.item_code) & (parent.docstatus == 1) & (parent.is_cancelled == 0) + & (parent.type_of_transaction != "Maintenance") ) .where(timestamp_condition) .groupby(child.batch_no)