From 05d3bcb63df47162bfe64412255dd5875b61dbe8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 28 Apr 2019 18:39:18 +0530 Subject: [PATCH 01/71] stock recon for serial no, batch no --- erpnext/stock/doctype/serial_no/serial_no.py | 2 +- .../stock_ledger_entry/stock_ledger_entry.py | 2 +- .../stock_reconciliation.js | 5 +- .../stock_reconciliation.py | 187 ++++++++++--- .../stock_reconciliation_item.json | 260 +++++++++++++++++- erpnext/stock/stock_ledger.py | 8 + erpnext/stock/utils.py | 4 + 7 files changed, 420 insertions(+), 48 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index c1aef95216..b91fddfc02 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -222,7 +222,7 @@ def validate_serial_no(sle, item_det): frappe.throw(_("Serial No {0} has already been received").format(serial_no), SerialNoDuplicateError) - if (sr.delivery_document_no and sle.voucher_type != 'Stock Entry' + if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation'] and sle.voucher_type == sr.delivery_document_type): return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, 'return_against') if return_against and return_against != sr.delivery_document_no: diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 79da70e313..5fe89d6e22 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -38,7 +38,7 @@ class StockLedgerEntry(Document): self.check_stock_frozen_date() self.actual_amt_check() - if not self.get("via_landed_cost_voucher") and self.voucher_type != 'Stock Reconciliation': + if not self.get("via_landed_cost_voucher"): from erpnext.stock.doctype.serial_no.serial_no import process_serial_no process_serial_no(self) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index ed9d77092a..818a671cab 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -12,8 +12,7 @@ frappe.ui.form.on("Stock Reconciliation", { return { query: "erpnext.controllers.queries.item_query", filters:{ - "is_stock_item": 1, - "has_serial_no": 0 + "is_stock_item": 1 } } }); @@ -93,7 +92,7 @@ frappe.ui.form.on("Stock Reconciliation", { frappe.model.set_value(cdt, cdn, "current_valuation_rate", r.message.rate); frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty); - + frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos); } }); } diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 205beed744..b1abe89275 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -9,7 +9,8 @@ from frappe.utils import cstr, flt, cint from erpnext.stock.stock_ledger import update_entries_after from erpnext.controllers.stock_controller import StockController from erpnext.accounts.utils import get_company_default -from erpnext.stock.utils import get_stock_balance +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos class OpeningEntryAccountError(frappe.ValidationError): pass class EmptyStockReconciliationItemsError(frappe.ValidationError): pass @@ -42,23 +43,28 @@ class StockReconciliation(StockController): """Remove items if qty or rate is not changed""" self.difference_amount = 0.0 def _changed(item): - qty, rate = get_stock_balance(item.item_code, item.warehouse, - self.posting_date, self.posting_time, with_valuation_rate=True) - if (item.qty==None or item.qty==qty) and (item.valuation_rate==None or item.valuation_rate==rate): + item_dict = get_stock_balance_for(item.item_code, item.warehouse, + self.posting_date, self.posting_time) + + if ((item.qty==None or item.qty==item_dict.get("qty")) + and (item.valuation_rate==None or item.valuation_rate==item_dict.get("rate"))): return False else: # set default as current rates if item.qty==None: - item.qty = qty + item.qty = item_dict.get("qty") if item.valuation_rate==None: - item.valuation_rate = rate + item.valuation_rate = item_dict.get("rate") - item.current_qty = qty - item.current_valuation_rate = rate + if item_dict.get("serial_nos"): + item.current_serial_no = item_dict.get("serial_nos") + + item.current_qty = item_dict.get("qty") + item.current_valuation_rate = item_dict.get("rate") self.difference_amount += (flt(item.qty, item.precision("qty")) * \ flt(item.valuation_rate or rate, item.precision("valuation_rate")) \ - - flt(qty, item.precision("qty")) * flt(rate, item.precision("valuation_rate"))) + - flt(item_dict.get("qty"), item.precision("qty")) * flt(item_dict.get("rate"), item.precision("valuation_rate"))) return True items = list(filter(lambda d: _changed(d), self.items)) @@ -89,7 +95,7 @@ class StockReconciliation(StockController): else: item_warehouse_combinations.append([row.item_code, row.warehouse]) - self.validate_item(row.item_code, row_num+1) + self.validate_item(row.item_code, row) # validate warehouse if not frappe.db.get_value("Warehouse", row.warehouse): @@ -131,7 +137,7 @@ class StockReconciliation(StockController): raise frappe.ValidationError(self.validation_messages) - def validate_item(self, item_code, row_num): + def validate_item(self, item_code, row): from erpnext.stock.doctype.item.item import validate_end_of_life, \ validate_is_stock_item, validate_cancelled_item @@ -145,51 +151,121 @@ class StockReconciliation(StockController): validate_is_stock_item(item_code, item.is_stock_item, verbose=0) # item should not be serialized - if item.has_serial_no == 1: - raise frappe.ValidationError(_("Serialized Item {0} cannot be updated using Stock Reconciliation, please use Stock Entry").format(item_code)) + if item.has_serial_no and not row.serial_no: + raise frappe.ValidationError(_("Serial nos are required for serialized item {0}").format(item_code)) # item managed batch-wise not allowed - if item.has_batch_no == 1: - raise frappe.ValidationError(_("Batched Item {0} cannot be updated using Stock Reconciliation, instead use Stock Entry").format(item_code)) + if item.has_batch_no and not row.batch: + raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code)) # docstatus should be < 2 validate_cancelled_item(item_code, item.docstatus, verbose=0) except Exception as e: - self.validation_messages.append(_("Row # ") + ("%d: " % (row_num)) + cstr(e)) + self.validation_messages.append(_("Row # ") + ("%d: " % (row.idx)) + cstr(e)) def update_stock_ledger(self): """ find difference between current and expected entries and create stock ledger entries based on the difference""" from erpnext.stock.stock_ledger import get_previous_sle + sl_entries = [] for row in self.items: + if row.serial_no: + self.get_sle_for_serialized_items(row, sl_entries) + else: + previous_sle = get_previous_sle({ + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time + }) + + if previous_sle: + if row.qty in ("", None): + row.qty = previous_sle.get("qty_after_transaction", 0) + + if row.valuation_rate in ("", None): + row.valuation_rate = previous_sle.get("valuation_rate", 0) + + if row.qty and not row.valuation_rate: + frappe.throw(_("Valuation Rate required for Item in row {0}").format(row.idx)) + + if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") + and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0)) + or (not previous_sle and not row.qty)): + continue + + sl_entries.append(self.get_sle_for_items(row)) + + if sl_entries: + self.make_sl_entries(sl_entries) + + def get_sle_for_serialized_items(self, row, sl_entries): + from erpnext.stock.stock_ledger import get_previous_sle + + # To issue existing serial nos + if row.current_serial_no: + args = self.get_sle_for_items(row) + args.update({ + 'actual_qty': -1 * row.current_qty, + 'serial_no': row.current_serial_no, + 'qty_after_transaction': 0, + 'valuation_rate': row.current_valuation_rate + }) + + sl_entries.append(args) + + for serial_no in get_serial_nos(row.serial_no): + args = self.get_sle_for_items(row, [serial_no]) + previous_sle = get_previous_sle({ "item_code": row.item_code, - "warehouse": row.warehouse, "posting_date": self.posting_date, - "posting_time": self.posting_time + "posting_time": self.posting_time, + "serial_no": serial_no }) - if previous_sle: - if row.qty in ("", None): - row.qty = previous_sle.get("qty_after_transaction", 0) - if row.valuation_rate in ("", None): - row.valuation_rate = previous_sle.get("valuation_rate", 0) + if previous_sle and row.warehouse != previous_sle.get("warehouse"): + # If serial no exists in different warehouse - if row.qty and not row.valuation_rate: - frappe.throw(_("Valuation Rate required for Item in row {0}").format(row.idx)) + new_args = args.copy() + new_args.update({ + 'actual_qty': -1, + 'qty_after_transaction': cint(previous_sle.get('qty_after_transaction')) - 1, + 'warehouse': previous_sle.get("warehouse", '') or row.warehouse, + 'valuation_rate': previous_sle.get("valuation_rate") + }) - if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") - and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0)) - or (not previous_sle and not row.qty)): - continue + sl_entries.append(new_args) - self.insert_entries(row) + if self.docstatus == 2: + args.update({ + 'actual_qty': 1, + 'incoming_rate': row.valuation_rate, + 'valuation_rate': row.valuation_rate + }) - def insert_entries(self, row): + sl_entries.append(args) + + if self.docstatus == 1: + args = self.get_sle_for_items(row) + + args.update({ + 'actual_qty': row.qty, + 'incoming_rate': row.valuation_rate, + 'valuation_rate': row.valuation_rate + }) + + sl_entries.append(args) + + def get_sle_for_items(self, row, serial_nos=None): """Insert Stock Ledger Entries""" - args = frappe._dict({ + + if not serial_nos and row.serial_no: + serial_nos = get_serial_nos(row.serial_no) + + return frappe._dict({ "doctype": "Stock Ledger Entry", "item_code": row.item_code, "warehouse": row.warehouse, @@ -199,11 +275,11 @@ class StockReconciliation(StockController): "voucher_no": self.name, "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), - "is_cancelled": "No", + "is_cancelled": "No" if self.docstatus != 2 else "Yes", "qty_after_transaction": flt(row.qty, row.precision("qty")), + "serial_no": '\n'.join(serial_nos) if serial_nos else '', "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")) }) - self.make_sl_entries([args]) def delete_and_repost_sle(self): """ Delete Stock Ledger Entries related to this voucher @@ -217,6 +293,15 @@ class StockReconciliation(StockController): frappe.db.sql("""delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)) + sl_entries = [] + for row in self.items: + if row.serial_no: + self.get_sle_for_serialized_items(row, sl_entries) + + if sl_entries: + sl_entries.reverse() + self.make_sl_entries(sl_entries) + # repost future entries for selected item_code, warehouse for entries in existing_entries: update_entries_after({ @@ -310,17 +395,43 @@ def get_items(warehouse, posting_date, posting_time, company): return res @frappe.whitelist() -def get_stock_balance_for(item_code, warehouse, posting_date, posting_time): +def get_stock_balance_for(item_code, warehouse, posting_date, posting_time, with_valuation_rate= True): frappe.has_permission("Stock Reconciliation", "write", throw = True) - qty, rate = get_stock_balance(item_code, warehouse, - posting_date, posting_time, with_valuation_rate=True) + has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") + + serial_nos = "" + if has_serial_no: + qty, rate, serial_nos = get_qty_rate_for_serial_nos(item_code, + warehouse, posting_date, posting_time) + else: + qty, rate = get_stock_balance(item_code, warehouse, + posting_date, posting_time, with_valuation_rate=with_valuation_rate) return { 'qty': qty, - 'rate': rate + 'rate': rate, + 'serial_nos': serial_nos } +def get_qty_rate_for_serial_nos(item_code, warehouse, posting_date, posting_time): + serial_nos_list = [serial_no.get("name") + for serial_no in get_available_serial_nos(item_code, warehouse)] + + qty = len(serial_nos_list) + serial_nos = '\n'.join(serial_nos_list) + + rate = get_incoming_rate({ + "item_code": item_code, + "warehouse": warehouse, + "posting_date": posting_date, + "posting_time": posting_time, + "qty": qty, + "serial_no":serial_nos, + }) + + return qty, rate, serial_nos + @frappe.whitelist() def get_difference_account(purpose, company): if purpose == 'Stock Reconciliation': diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 0fafe8306c..d64c218a6d 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, @@ -14,10 +15,12 @@ "fields": [ { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "barcode", "fieldtype": "Data", "hidden": 0, @@ -40,14 +43,17 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 3, + "fetch_if_empty": 0, "fieldname": "item_code", "fieldtype": "Link", "hidden": 0, @@ -71,14 +77,17 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "item_name", "fieldtype": "Data", "hidden": 0, @@ -101,14 +110,17 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 3, + "fetch_if_empty": 0, "fieldname": "warehouse", "fieldtype": "Link", "hidden": 0, @@ -132,14 +144,17 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "column_break_6", "fieldtype": "Column Break", "hidden": 0, @@ -161,15 +176,18 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 2, "description": "", + "fetch_if_empty": 0, "fieldname": "qty", "fieldtype": "Float", "hidden": 0, @@ -192,15 +210,18 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 2, "description": "", + "fetch_if_empty": 0, "fieldname": "valuation_rate", "fieldtype": "Currency", "hidden": 0, @@ -224,14 +245,17 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "amount", "fieldtype": "Currency", "hidden": 0, @@ -255,14 +279,149 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, + "fieldname": "serial_no_and_batch_section", + "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": "Serial No and Batch", + "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, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "serial_no", + "fieldtype": "Small Text", + "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": "Serial No", + "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, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "column_break_11", + "fieldtype": "Column 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, + "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, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "batch", + "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": "Batch", + "length": 0, + "no_copy": 0, + "options": "Batch", + "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, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, "fieldname": "section_break_3", "fieldtype": "Section Break", "hidden": 0, @@ -285,15 +444,18 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "description": "", + "fetch_if_empty": 0, "fieldname": "current_qty", "fieldtype": "Float", "hidden": 0, @@ -316,14 +478,85 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, + "fieldname": "current_batch", + "fieldtype": "Small Text", + "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": "Current Batch No", + "length": 0, + "no_copy": 1, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "current_serial_no", + "fieldtype": "Small Text", + "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": "Current Serial No", + "length": 0, + "no_copy": 1, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, "fieldname": "column_break_9", "fieldtype": "Column Break", "hidden": 0, @@ -345,15 +578,18 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "description": "", + "fetch_if_empty": 0, "fieldname": "current_valuation_rate", "fieldtype": "Currency", "hidden": 0, @@ -377,15 +613,18 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "description": "", + "fetch_if_empty": 0, "fieldname": "current_amount", "fieldtype": "Currency", "hidden": 0, @@ -409,14 +648,17 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "section_break_14", "fieldtype": "Section Break", "hidden": 0, @@ -438,14 +680,17 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "quantity_difference", "fieldtype": "Read Only", "hidden": 0, @@ -468,14 +713,17 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "column_break_16", "fieldtype": "Column Break", "hidden": 0, @@ -497,14 +745,17 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "amount_difference", "fieldtype": "Currency", "hidden": 0, @@ -528,21 +779,20 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], "has_web_view": 0, - "hide_heading": 0, "hide_toolbar": 0, "idx": 0, - "image_view": 0, "in_create": 0, "is_submittable": 0, "issingle": 0, "istable": 1, "max_attachments": 0, "menu_index": 0, - "modified": "2017-08-03 00:03:40.412071", + "modified": "2019-04-24 19:07:59.113660", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", @@ -551,10 +801,10 @@ "permissions": [], "quick_entry": 1, "read_only": 0, - "read_only_onload": 0, "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1, - "track_seen": 0 + "track_seen": 0, + "track_views": 0 } \ No newline at end of file diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c8706b291e..6a92ce8bd2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -157,7 +157,11 @@ class update_entries_after(object): if sle.serial_no: self.get_serialized_values(sle) self.qty_after_transaction += flt(sle.actual_qty) + if sle.voucher_type == "Stock Reconciliation": + self.qty_after_transaction = sle.qty_after_transaction + self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) + frappe.errprint([self.stock_value, self.qty_after_transaction, self.valuation_rate]) else: if sle.voucher_type=="Stock Reconciliation": # assert @@ -177,6 +181,7 @@ class update_entries_after(object): # rounding as per precision self.stock_value = flt(self.stock_value, self.precision) + frappe.errprint([self.stock_value, self.qty_after_transaction, self.valuation_rate, "wefjlk"]) if self.prev_stock_value < 0 and self.stock_value >= 0 and sle.voucher_type != 'Stock Reconciliation': stock_value_difference = sle.actual_qty * self.valuation_rate @@ -420,6 +425,9 @@ def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=No elif previous_sle.get("warehouse_condition"): conditions += " and " + previous_sle.get("warehouse_condition") + if previous_sle.get("serial_no"): + conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no")))) + if not previous_sle.get("posting_date"): previous_sle["posting_date"] = "1900-01-01" if not previous_sle.get("posting_time"): diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 76631fad64..ea8e8805a6 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -277,3 +277,7 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto new_row.append(None) result[row_idx] = new_row + +def get_available_serial_nos(item_code, warehouse): + return frappe.get_all("Serial No", filters = {'item_code': item_code, + 'warehouse': warehouse, 'delivery_document_no': ''}) or [] \ No newline at end of file From 66aa37f1e2dbba02007cfae0309c51f06a0f539b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 24 May 2019 16:53:51 +0530 Subject: [PATCH 02/71] provision to add batch number in the stock reconciliation --- erpnext/stock/doctype/batch/batch.py | 4 +- .../stock_reconciliation.js | 17 +- .../stock_reconciliation.py | 75 +- .../stock_reconciliation_item.json | 939 +++--------------- erpnext/stock/stock_ledger.py | 11 +- 5 files changed, 230 insertions(+), 816 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 4881983b8e..c540ac5227 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -257,10 +257,10 @@ def get_batches(item_code, warehouse, qty=1, throw=False): 'on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )' 'where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s ' 'and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL)' - 'group by batch_id ' + 'group by batch_id having qty > 0' 'order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC', (item_code, warehouse), - as_dict=True + as_dict=True, debug=1 ) return batches diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 818a671cab..a00e6e64bf 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -76,6 +76,7 @@ frappe.ui.form.on("Stock Reconciliation", { set_valuation_rate_and_qty: function(frm, cdt, cdn) { var d = frappe.model.get_doc(cdt, cdn); + if(d.item_code && d.warehouse) { frappe.call({ method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_stock_balance_for", @@ -83,7 +84,8 @@ frappe.ui.form.on("Stock Reconciliation", { item_code: d.item_code, warehouse: d.warehouse, posting_date: frm.doc.posting_date, - posting_time: frm.doc.posting_time + posting_time: frm.doc.posting_time, + batch_no: d.batch_no }, callback: function(r) { frappe.model.set_value(cdt, cdn, "qty", r.message.qty); @@ -152,9 +154,22 @@ frappe.ui.form.on("Stock Reconciliation Item", { frm.events.set_item_code(frm, cdt, cdn); }, warehouse: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if (child.batch_no) { + frappe.model.set_value(child.cdt, child.cdn, "batch_no", ""); + } + frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, item_code: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if (child.batch_no) { + frappe.model.set_value(child.cdt, child.cdn, "batch_no", ""); + } + + frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); + }, + batch_no: function(frm, cdt, cdn) { frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, qty: function(frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index b1abe89275..9aec76d7e6 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -11,6 +11,7 @@ from erpnext.controllers.stock_controller import StockController from erpnext.accounts.utils import get_company_default from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos +from erpnext.stock.doctype.batch.batch import get_batch_qty class OpeningEntryAccountError(frappe.ValidationError): pass class EmptyStockReconciliationItemsError(frappe.ValidationError): pass @@ -44,7 +45,7 @@ class StockReconciliation(StockController): self.difference_amount = 0.0 def _changed(item): item_dict = get_stock_balance_for(item.item_code, item.warehouse, - self.posting_date, self.posting_time) + self.posting_date, self.posting_time, batch_no=item.batch_no) if ((item.qty==None or item.qty==item_dict.get("qty")) and (item.valuation_rate==None or item.valuation_rate==item_dict.get("rate"))): @@ -90,10 +91,15 @@ class StockReconciliation(StockController): for row_num, row in enumerate(self.items): # find duplicates - if [row.item_code, row.warehouse] in item_warehouse_combinations: + key = [row.item_code, row.warehouse] + for field in ['serial_no', 'batch_no']: + if row.get(field): + key.append(row.get(field)) + + if key in item_warehouse_combinations: self.validation_messages.append(_get_msg(row_num, _("Duplicate entry"))) else: - item_warehouse_combinations.append([row.item_code, row.warehouse]) + item_warehouse_combinations.append(key) self.validate_item(row.item_code, row) @@ -155,9 +161,12 @@ class StockReconciliation(StockController): raise frappe.ValidationError(_("Serial nos are required for serialized item {0}").format(item_code)) # item managed batch-wise not allowed - if item.has_batch_no and not row.batch: + if item.has_batch_no and not row.batch_no and not item.create_new_batch: raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code)) + if self._action=="submit" and item.create_new_batch: + self.make_batches('warehouse') + # docstatus should be < 2 validate_cancelled_item(item_code, item.docstatus, verbose=0) @@ -171,7 +180,7 @@ class StockReconciliation(StockController): sl_entries = [] for row in self.items: - if row.serial_no: + if row.serial_no or row.batch_no: self.get_sle_for_serialized_items(row, sl_entries) else: previous_sle = get_previous_sle({ @@ -205,15 +214,20 @@ class StockReconciliation(StockController): from erpnext.stock.stock_ledger import get_previous_sle # To issue existing serial nos - if row.current_serial_no: + if row.current_qty and (row.current_serial_no or row.batch_no): args = self.get_sle_for_items(row) args.update({ 'actual_qty': -1 * row.current_qty, 'serial_no': row.current_serial_no, - 'qty_after_transaction': 0, + 'batch_no': row.batch_no, 'valuation_rate': row.current_valuation_rate }) + if row.current_serial_no: + args.update({ + 'qty_after_transaction': 0, + }) + sl_entries.append(args) for serial_no in get_serial_nos(row.serial_no): @@ -265,7 +279,7 @@ class StockReconciliation(StockController): if not serial_nos and row.serial_no: serial_nos = get_serial_nos(row.serial_no) - return frappe._dict({ + data = frappe._dict({ "doctype": "Stock Ledger Entry", "item_code": row.item_code, "warehouse": row.warehouse, @@ -276,11 +290,16 @@ class StockReconciliation(StockController): "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "is_cancelled": "No" if self.docstatus != 2 else "Yes", - "qty_after_transaction": flt(row.qty, row.precision("qty")), "serial_no": '\n'.join(serial_nos) if serial_nos else '', + "batch_no": row.batch_no, "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")) }) + if not row.batch_no: + data.qty_after_transaction = flt(row.qty, row.precision("qty")) + + return data + def delete_and_repost_sle(self): """ Delete Stock Ledger Entries related to this voucher and repost future Stock Ledger Entries""" @@ -295,7 +314,7 @@ class StockReconciliation(StockController): sl_entries = [] for row in self.items: - if row.serial_no: + if row.serial_no or row.batch_no: self.get_sle_for_serialized_items(row, sl_entries) if sl_entries: @@ -395,41 +414,51 @@ def get_items(warehouse, posting_date, posting_time, company): return res @frappe.whitelist() -def get_stock_balance_for(item_code, warehouse, posting_date, posting_time, with_valuation_rate= True): +def get_stock_balance_for(item_code, warehouse, + posting_date, posting_time, batch_no=None, with_valuation_rate= True): frappe.has_permission("Stock Reconciliation", "write", throw = True) - has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") + item_dict = frappe.db.get_value("Item", item_code, + ["has_serial_no", "has_batch_no"], as_dict=1) serial_nos = "" - if has_serial_no: + if item_dict.get("has_serial_no"): qty, rate, serial_nos = get_qty_rate_for_serial_nos(item_code, - warehouse, posting_date, posting_time) + warehouse, posting_date, posting_time, item_dict) else: qty, rate = get_stock_balance(item_code, warehouse, posting_date, posting_time, with_valuation_rate=with_valuation_rate) + if item_dict.get("has_batch_no"): + qty = get_batch_qty(batch_no, warehouse) or 0 + + print(qty, rate, batch_no, warehouse) return { 'qty': qty, 'rate': rate, 'serial_nos': serial_nos } -def get_qty_rate_for_serial_nos(item_code, warehouse, posting_date, posting_time): +def get_qty_rate_for_serial_nos(item_code, warehouse, posting_date, posting_time, item_dict): + args = { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": posting_date, + "posting_time": posting_time, + } + serial_nos_list = [serial_no.get("name") for serial_no in get_available_serial_nos(item_code, warehouse)] qty = len(serial_nos_list) serial_nos = '\n'.join(serial_nos_list) - - rate = get_incoming_rate({ - "item_code": item_code, - "warehouse": warehouse, - "posting_date": posting_date, - "posting_time": posting_time, - "qty": qty, - "serial_no":serial_nos, + args.update({ + 'qty': qty, + "serial_nos": serial_nos }) + rate = get_incoming_rate(args) + return qty, rate, serial_nos @frappe.whitelist() diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index d64c218a6d..dce87e5ae3 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -1,810 +1,181 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2015-02-17 01:06:05.072764", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Other", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2015-02-17 01:06:05.072764", + "doctype": "DocType", + "document_type": "Other", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "barcode", + "item_code", + "item_name", + "warehouse", + "column_break_6", + "qty", + "valuation_rate", + "amount", + "serial_no_and_batch_section", + "serial_no", + "column_break_11", + "batch_no", + "section_break_3", + "current_qty", + "current_serial_no", + "column_break_9", + "current_valuation_rate", + "current_amount", + "section_break_14", + "quantity_difference", + "column_break_16", + "amount_difference" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "barcode", - "fieldtype": "Data", - "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": "Barcode", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "barcode", + "fieldtype": "Data", + "label": "Barcode", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fetch_if_empty": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 3, + "fieldname": "item_code", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Item Name", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fetch_if_empty": 0, - "fieldname": "warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 3, + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_6", - "fieldtype": "Column 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, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "description": "", - "fetch_if_empty": 0, - "fieldname": "qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Quantity", - "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, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "description": "", - "fetch_if_empty": 0, - "fieldname": "valuation_rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Valuation Rate", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Valuation Rate", + "options": "Company:company:default_currency" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "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": "Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "serial_no_and_batch_section", - "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": "Serial No and Batch", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "serial_no_and_batch_section", + "fieldtype": "Section Break", + "label": "Serial No and Batch" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "serial_no", - "fieldtype": "Small Text", - "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": "Serial No", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_11", - "fieldtype": "Column 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, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "batch", - "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": "Batch", - "length": 0, - "no_copy": 0, - "options": "Batch", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Before reconciliation" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_3", - "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": "Before reconciliation", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "current_qty", + "fieldtype": "Float", + "label": "Current Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "current_qty", - "fieldtype": "Float", - "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": "Current Qty", - "length": 0, - "no_copy": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "current_serial_no", + "fieldtype": "Small Text", + "label": "Current Serial No", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "current_batch", - "fieldtype": "Small Text", - "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": "Current Batch No", - "length": 0, - "no_copy": 1, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "current_serial_no", - "fieldtype": "Small Text", - "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": "Current Serial No", - "length": 0, - "no_copy": 1, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "current_valuation_rate", + "fieldtype": "Currency", + "label": "Current Valuation Rate", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_9", - "fieldtype": "Column 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, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "current_amount", + "fieldtype": "Currency", + "label": "Current Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "current_valuation_rate", - "fieldtype": "Currency", - "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": "Current Valuation Rate", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_14", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "current_amount", - "fieldtype": "Currency", - "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": "Current Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "quantity_difference", + "fieldtype": "Read Only", + "label": "Quantity Difference" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_14", - "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, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "quantity_difference", - "fieldtype": "Read Only", - "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": "Quantity Difference", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "amount_difference", + "fieldtype": "Currency", + "label": "Amount Difference", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_16", - "fieldtype": "Column 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, - "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, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "amount_difference", - "fieldtype": "Currency", - "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": "Amount Difference", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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, - "translatable": 0, - "unique": 0 + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch" } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-04-24 19:07:59.113660", - "modified_by": "Administrator", - "module": "Stock", - "name": "Stock Reconciliation Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "modified": "2019-05-24 12:34:50.018491", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Reconciliation Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 6a92ce8bd2..e46823ea62 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -161,9 +161,8 @@ class update_entries_after(object): self.qty_after_transaction = sle.qty_after_transaction self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) - frappe.errprint([self.stock_value, self.qty_after_transaction, self.valuation_rate]) else: - if sle.voucher_type=="Stock Reconciliation": + if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no: # assert self.valuation_rate = sle.valuation_rate self.qty_after_transaction = sle.qty_after_transaction @@ -181,7 +180,6 @@ class update_entries_after(object): # rounding as per precision self.stock_value = flt(self.stock_value, self.precision) - frappe.errprint([self.stock_value, self.qty_after_transaction, self.valuation_rate, "wefjlk"]) if self.prev_stock_value < 0 and self.stock_value >= 0 and sle.voucher_type != 'Stock Reconciliation': stock_value_difference = sle.actual_qty * self.valuation_rate @@ -376,7 +374,7 @@ class update_entries_after(object): """get Stock Ledger Entries after a particular datetime, for reposting""" return get_stock_ledger_entries(self.previous_sle or frappe._dict({ "item_code": self.args.get("item_code"), "warehouse": self.args.get("warehouse") }), - ">", "asc", for_update=True) + ">", "asc", for_update=True, check_serial_no=False) def raise_exceptions(self): deficiency = min(e["diff"] for e in self.exceptions) @@ -417,7 +415,8 @@ def get_previous_sle(args, for_update=False): sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) return sle and sle[0] or {} -def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=None, for_update=False, debug=False): +def get_stock_ledger_entries(previous_sle, operator=None, + order="desc", limit=None, for_update=False, debug=False, check_serial_no=True): """get stock ledger entries filtered by specific posting datetime conditions""" conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator) if previous_sle.get("warehouse"): @@ -425,7 +424,7 @@ def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=No elif previous_sle.get("warehouse_condition"): conditions += " and " + previous_sle.get("warehouse_condition") - if previous_sle.get("serial_no"): + if check_serial_no and previous_sle.get("serial_no"): conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no")))) if not previous_sle.get("posting_date"): From 87c4b06437dcded371dd705b5e9b1c9a837443d0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 1 Jun 2019 14:22:46 +0530 Subject: [PATCH 03/71] Test cases for serial no --- erpnext/stock/doctype/serial_no/serial_no.py | 7 +- .../stock_reconciliation.py | 33 +++++-- .../test_stock_reconciliation.py | 86 ++++++++++++++++++- .../stock_reconciliation_item.json | 9 +- erpnext/stock/utils.py | 2 +- 5 files changed, 124 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index b91fddfc02..4ab95c7e14 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -299,7 +299,7 @@ def validate_so_serial_no(sr, sales_order,): be delivered""").format(sales_order, sr.item_code, sr.name)) def has_duplicate_serial_no(sn, sle): - if sn.warehouse: + if sn.warehouse and sle.voucher_type != 'Stock Reconciliation': return True if sn.company != sle.company: @@ -413,14 +413,17 @@ def update_serial_nos_after_submit(controller, parentfield): update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty) else False accepted_serial_nos_updated = False + if controller.doctype == "Stock Entry": warehouse = d.t_warehouse qty = d.transfer_qty else: warehouse = d.warehouse - qty = d.stock_qty + qty = (d.qty if controller.doctype == "Stock Reconciliation" + else d.stock_qty) for sle in stock_ledger_entries: + print(accepted_serial_nos_updated, qty, sle.actual_qty) if sle.voucher_detail_no==d.name: if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \ and sle.warehouse == warehouse and sle.serial_no != d.serial_no: diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 9aec76d7e6..cdf6068f11 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -36,6 +36,9 @@ class StockReconciliation(StockController): self.update_stock_ledger() self.make_gl_entries() + from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") + def on_cancel(self): self.delete_and_repost_sle() self.make_gl_entries_on_cancel() @@ -48,7 +51,8 @@ class StockReconciliation(StockController): self.posting_date, self.posting_time, batch_no=item.batch_no) if ((item.qty==None or item.qty==item_dict.get("qty")) - and (item.valuation_rate==None or item.valuation_rate==item_dict.get("rate"))): + and (item.valuation_rate==None or item.valuation_rate==item_dict.get("rate")) + and item.serial_no == item_dict.get("serial_nos")): return False else: # set default as current rates @@ -64,7 +68,7 @@ class StockReconciliation(StockController): item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") self.difference_amount += (flt(item.qty, item.precision("qty")) * \ - flt(item.valuation_rate or rate, item.precision("valuation_rate")) \ + flt(item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")) \ - flt(item_dict.get("qty"), item.precision("qty")) * flt(item_dict.get("rate"), item.precision("valuation_rate"))) return True @@ -157,7 +161,7 @@ class StockReconciliation(StockController): validate_is_stock_item(item_code, item.is_stock_item, verbose=0) # item should not be serialized - if item.has_serial_no and not row.serial_no: + if item.has_serial_no and not row.serial_no and not item.serial_no_series: raise frappe.ValidationError(_("Serial nos are required for serialized item {0}").format(item_code)) # item managed batch-wise not allowed @@ -180,7 +184,8 @@ class StockReconciliation(StockController): sl_entries = [] for row in self.items: - if row.serial_no or row.batch_no: + item = frappe.get_doc("Item", row.item_code) + if item.has_serial_no or item.has_batch_no: self.get_sle_for_serialized_items(row, sl_entries) else: previous_sle = get_previous_sle({ @@ -213,6 +218,9 @@ class StockReconciliation(StockController): def get_sle_for_serialized_items(self, row, sl_entries): from erpnext.stock.stock_ledger import get_previous_sle + serial_nos = get_serial_nos(row.serial_no) + + # To issue existing serial nos if row.current_qty and (row.current_serial_no or row.batch_no): args = self.get_sle_for_items(row) @@ -230,7 +238,7 @@ class StockReconciliation(StockController): sl_entries.append(args) - for serial_no in get_serial_nos(row.serial_no): + for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) previous_sle = get_previous_sle({ @@ -262,7 +270,7 @@ class StockReconciliation(StockController): sl_entries.append(args) - if self.docstatus == 1: + if self.docstatus == 1 and not row.remove_serial_no_from_stock: args = self.get_sle_for_items(row) args.update({ @@ -273,6 +281,15 @@ class StockReconciliation(StockController): sl_entries.append(args) + if serial_nos == get_serial_nos(row.current_serial_no): + # update valuation rate + self.update_valuation_rate_for_serial_nos(row, serial_nos) + + def update_valuation_rate_for_serial_nos(self, row, serial_nos): + valuation_rate = row.valuation_rate if self.docstatus == 1 else row.current_valuation_rate + for d in serial_nos: + frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate) + def get_sle_for_items(self, row, serial_nos=None): """Insert Stock Ledger Entries""" @@ -287,6 +304,7 @@ class StockReconciliation(StockController): "posting_time": self.posting_time, "voucher_type": self.doctype, "voucher_no": self.name, + "voucher_detail_no": row.name, "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "is_cancelled": "No" if self.docstatus != 2 else "Yes", @@ -432,7 +450,6 @@ def get_stock_balance_for(item_code, warehouse, if item_dict.get("has_batch_no"): qty = get_batch_qty(batch_no, warehouse) or 0 - print(qty, rate, batch_no, warehouse) return { 'qty': qty, 'rate': rate, @@ -457,7 +474,7 @@ def get_qty_rate_for_serial_nos(item_code, warehouse, posting_date, posting_time "serial_nos": serial_nos }) - rate = get_incoming_rate(args) + rate = get_incoming_rate(args, raise_error_if_no_rate=False) or 0 return qty, rate, serial_nos diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 2dc585b8d6..5ee8228edf 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -13,9 +13,12 @@ from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos class TestStockReconciliation(unittest.TestCase): def setUp(self): + create_batch_or_serial_no_items() frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) self.insert_existing_sle() @@ -106,6 +109,83 @@ class TestStockReconciliation(unittest.TestCase): make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item", target="_Test Warehouse - _TC", qty=15, basic_rate=1200) + def test_stock_reco_for_serialized_item(self): + set_perpetual_inventory() + + to_delete_records = [] + to_delete_serial_nos = [] + + # Add new serial nos + serial_item_code = "Stock-Reco-Serial-Item-1" + serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" + + sr = create_stock_reconciliation(item_code=serial_item_code, + warehouse = serial_warehouse, qty=5, rate=200) + + # print(sr.name) + serial_nos = get_serial_nos(sr.items[0].serial_no) + self.assertEqual(len(serial_nos), 5) + + args = { + "item_code": serial_item_code, + "warehouse": serial_warehouse, + "posting_date": nowdate(), + "posting_time": nowtime(), + "serial_no": sr.items[0].serial_no + } + + valuation_rate = get_incoming_rate(args) + self.assertEqual(valuation_rate, 200) + + to_delete_records.append(sr.name) + + sr = create_stock_reconciliation(item_code=serial_item_code, + warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos)) + + # print(sr.name) + serial_nos1 = get_serial_nos(sr.items[0].serial_no) + self.assertEqual(len(serial_nos1), 5) + + args = { + "item_code": serial_item_code, + "warehouse": serial_warehouse, + "posting_date": nowdate(), + "posting_time": nowtime(), + "serial_no": sr.items[0].serial_no + } + + valuation_rate = get_incoming_rate(args) + self.assertEqual(valuation_rate, 300) + + to_delete_records.append(sr.name) + to_delete_records.reverse() + + for d in to_delete_records: + stock_doc = frappe.get_doc("Stock Reconciliation", d) + stock_doc.cancel() + frappe.delete_doc("Stock Reconciliation", stock_doc.name) + + for d in serial_nos + serial_nos1: + if frappe.db.exists("Serial No", d): + frappe.delete_doc("Serial No", d) + +def create_batch_or_serial_no_items(): + create_warehouse("_Test Warehouse for Stock Reco1", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) + + serial_item_doc = create_item("Stock-Reco-Serial-Item-1", is_stock_item=1) + if not serial_item_doc.has_serial_no: + serial_item_doc.has_serial_no = 1 + serial_item_doc.serial_no_series = "SRSI.####" + serial_item_doc.save(ignore_permissions=True) + + batch_item_doc = create_item("Stock-Reco-batch-Item-1", is_stock_item=1) + if not batch_item_doc.has_batch_no: + batch_item_doc.has_batch_no = 1 + batch_item_doc.create_new_batch = 1 + serial_item_doc.batch_number_series = "BASR.#####" + batch_item_doc.save(ignore_permissions=True) + def create_stock_reconciliation(**args): args = frappe._dict(args) sr = frappe.new_doc("Stock Reconciliation") @@ -120,7 +200,10 @@ def create_stock_reconciliation(**args): "item_code": args.item_code or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": args.qty, - "valuation_rate": args.rate + "valuation_rate": args.rate, + "serial_no": args.serial_no, + "batch_no": args.batch_no, + "remove_serial_no_from_stock": args.remove_serial_no_from_stock or 0 }) try: @@ -140,3 +223,4 @@ def set_valuation_method(item_code, valuation_method): }, allow_negative_stock=1) test_dependencies = ["Item", "Warehouse"] + diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index dce87e5ae3..fa42c9c25c 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -15,6 +15,7 @@ "amount", "serial_no_and_batch_section", "serial_no", + "remove_serial_no_from_stock", "column_break_11", "batch_no", "section_break_3", @@ -165,10 +166,16 @@ "fieldtype": "Link", "label": "Batch No", "options": "Batch" + }, + { + "default": "0", + "fieldname": "remove_serial_no_from_stock", + "fieldtype": "Check", + "label": "Remove Serial No from Stock" } ], "istable": 1, - "modified": "2019-05-24 12:34:50.018491", + "modified": "2019-06-01 03:16:38.459307", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index ea8e8805a6..6ea322872e 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -173,7 +173,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'), args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'), currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), - raise_error_if_no_rate=True) + raise_error_if_no_rate=raise_error_if_no_rate) return in_rate From 059890ecefe2144cf49b0e2678265a84930afadb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 3 Jun 2019 01:27:58 +0530 Subject: [PATCH 04/71] Test cases for batch no --- erpnext/stock/doctype/batch/batch.py | 4 +- erpnext/stock/doctype/serial_no/serial_no.py | 1 - .../stock_reconciliation.js | 16 ++++- .../stock_reconciliation.py | 24 ++++---- .../test_stock_reconciliation.py | 60 +++++++++++++++++-- .../stock_reconciliation_item.json | 10 +--- 6 files changed, 87 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index c540ac5227..4881983b8e 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -257,10 +257,10 @@ def get_batches(item_code, warehouse, qty=1, throw=False): 'on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )' 'where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s ' 'and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL)' - 'group by batch_id having qty > 0' + 'group by batch_id ' 'order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC', (item_code, warehouse), - as_dict=True, debug=1 + as_dict=True ) return batches diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 4ab95c7e14..43bc5e2530 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -423,7 +423,6 @@ def update_serial_nos_after_submit(controller, parentfield): else d.stock_qty) for sle in stock_ledger_entries: - print(accepted_serial_nos_updated, qty, sle.actual_qty) if sle.voucher_detail_no==d.name: if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \ and sle.warehouse == warehouse and sle.serial_no != d.serial_no: diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index a00e6e64bf..5ac0b098fe 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -153,6 +153,7 @@ frappe.ui.form.on("Stock Reconciliation Item", { barcode: function(frm, cdt, cdn) { frm.events.set_item_code(frm, cdt, cdn); }, + warehouse: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; if (child.batch_no) { @@ -161,22 +162,35 @@ frappe.ui.form.on("Stock Reconciliation Item", { frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, + item_code: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; if (child.batch_no) { - frappe.model.set_value(child.cdt, child.cdn, "batch_no", ""); + frappe.model.set_value(cdt, cdn, "batch_no", ""); } frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, + batch_no: function(frm, cdt, cdn) { frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, + qty: function(frm, cdt, cdn) { frm.events.set_amount_quantity(frm, cdt, cdn); }, + valuation_rate: function(frm, cdt, cdn) { frm.events.set_amount_quantity(frm, cdt, cdn); + }, + + serial_no: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + + if (child.serial_no) { + const serial_nos = child.serial_no.trim().split('\n'); + frappe.model.set_value(cdt, cdn, "qty", serial_nos.length); + } } }); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index cdf6068f11..2be667c340 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -32,6 +32,9 @@ class StockReconciliation(StockController): self.validate_expense_account() self.set_total_qty_and_amount() + if self._action=="submit": + self.make_batches('warehouse') + def on_submit(self): self.update_stock_ledger() self.make_gl_entries() @@ -50,16 +53,16 @@ class StockReconciliation(StockController): item_dict = get_stock_balance_for(item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no) - if ((item.qty==None or item.qty==item_dict.get("qty")) - and (item.valuation_rate==None or item.valuation_rate==item_dict.get("rate")) - and item.serial_no == item_dict.get("serial_nos")): + if (((item.qty is None or item.qty==item_dict.get("qty")) and + (item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and not item.serial_no) + or (item.serial_no and item.serial_no == item_dict.get("serial_nos"))): return False else: # set default as current rates - if item.qty==None: + if item.qty is None: item.qty = item_dict.get("qty") - if item.valuation_rate==None: + if item.valuation_rate is None: item.valuation_rate = item_dict.get("rate") if item_dict.get("serial_nos"): @@ -162,15 +165,12 @@ class StockReconciliation(StockController): # item should not be serialized if item.has_serial_no and not row.serial_no and not item.serial_no_series: - raise frappe.ValidationError(_("Serial nos are required for serialized item {0}").format(item_code)) + raise frappe.ValidationError(_("Serial no(s) required for serialized item {0}").format(item_code)) # item managed batch-wise not allowed if item.has_batch_no and not row.batch_no and not item.create_new_batch: raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code)) - if self._action=="submit" and item.create_new_batch: - self.make_batches('warehouse') - # docstatus should be < 2 validate_cancelled_item(item_code, item.docstatus, verbose=0) @@ -203,7 +203,7 @@ class StockReconciliation(StockController): row.valuation_rate = previous_sle.get("valuation_rate", 0) if row.qty and not row.valuation_rate: - frappe.throw(_("Valuation Rate required for Item in row {0}").format(row.idx)) + frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx)) if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0)) @@ -270,7 +270,7 @@ class StockReconciliation(StockController): sl_entries.append(args) - if self.docstatus == 1 and not row.remove_serial_no_from_stock: + if self.docstatus == 1 and row.qty: args = self.get_sle_for_items(row) args.update({ @@ -332,7 +332,7 @@ class StockReconciliation(StockController): sl_entries = [] for row in self.items: - if row.serial_no or row.batch_no: + if row.serial_no or row.batch_no or row.current_serial_no: self.get_sle_for_serialized_items(row, sl_entries) if sl_entries: diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 5ee8228edf..f0c71cf39a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -13,7 +13,7 @@ from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.item.test_item import create_item -from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos +from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos, get_stock_value_on from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos class TestStockReconciliation(unittest.TestCase): @@ -169,10 +169,62 @@ class TestStockReconciliation(unittest.TestCase): if frappe.db.exists("Serial No", d): frappe.delete_doc("Serial No", d) + def test_stock_reco_for_batch_item(self): + set_perpetual_inventory() + + to_delete_records = [] + to_delete_serial_nos = [] + + # Add new serial nos + item_code = "Stock-Reco-batch-Item-1" + warehouse = "_Test Warehouse for Stock Reco2 - _TC" + + sr = create_stock_reconciliation(item_code=item_code, + warehouse = warehouse, qty=5, rate=200, do_not_submit=1) + sr.save(ignore_permissions=True) + sr.submit() + + self.assertTrue(sr.items[0].batch_no) + to_delete_records.append(sr.name) + + sr1 = create_stock_reconciliation(item_code=item_code, + warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no) + + args = { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": nowdate(), + "posting_time": nowtime(), + } + + valuation_rate = get_incoming_rate(args) + self.assertEqual(valuation_rate, 300) + to_delete_records.append(sr1.name) + + + sr2 = create_stock_reconciliation(item_code=item_code, + warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no) + + stock_value = get_stock_value_on(warehouse, nowdate(), item_code) + self.assertEqual(stock_value, 0) + to_delete_records.append(sr2.name) + + to_delete_records.reverse() + for d in to_delete_records: + stock_doc = frappe.get_doc("Stock Reconciliation", d) + stock_doc.cancel() + + frappe.delete_doc("Batch", sr.items[0].batch_no) + for d in to_delete_records: + frappe.delete_doc("Stock Reconciliation", d) + def create_batch_or_serial_no_items(): create_warehouse("_Test Warehouse for Stock Reco1", {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) + create_warehouse("_Test Warehouse for Stock Reco2", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) + serial_item_doc = create_item("Stock-Reco-Serial-Item-1", is_stock_item=1) if not serial_item_doc.has_serial_no: serial_item_doc.has_serial_no = 1 @@ -202,12 +254,12 @@ def create_stock_reconciliation(**args): "qty": args.qty, "valuation_rate": args.rate, "serial_no": args.serial_no, - "batch_no": args.batch_no, - "remove_serial_no_from_stock": args.remove_serial_no_from_stock or 0 + "batch_no": args.batch_no }) try: - sr.submit() + if not args.do_not_submit: + sr.submit() except EmptyStockReconciliationItemsError: pass return sr diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index fa42c9c25c..e53db0772b 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -15,7 +15,6 @@ "amount", "serial_no_and_batch_section", "serial_no", - "remove_serial_no_from_stock", "column_break_11", "batch_no", "section_break_3", @@ -110,6 +109,7 @@ "label": "Before reconciliation" }, { + "default": "0", "fieldname": "current_qty", "fieldtype": "Float", "label": "Current Qty", @@ -166,16 +166,10 @@ "fieldtype": "Link", "label": "Batch No", "options": "Batch" - }, - { - "default": "0", - "fieldname": "remove_serial_no_from_stock", - "fieldtype": "Check", - "label": "Remove Serial No from Stock" } ], "istable": 1, - "modified": "2019-06-01 03:16:38.459307", + "modified": "2019-06-14 17:10:53.188305", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", From c41403c8a987b632c776da9b8b5f07cf5d9e7db3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 27 Jun 2019 16:33:19 +0530 Subject: [PATCH 05/71] fix: remove move and add buttons from stock summary --- erpnext/stock/dashboard/item_dashboard.js | 99 +------------------ .../stock/dashboard/item_dashboard_list.html | 15 --- 2 files changed, 1 insertion(+), 113 deletions(-) diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 157dbfe174..f820b7aa86 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -16,18 +16,6 @@ erpnext.stock.ItemDashboard = Class.extend({ this.content = $(frappe.render_template('item_dashboard')).appendTo(this.parent); this.result = this.content.find('.result'); - // move - this.content.on('click', '.btn-move', function() { - erpnext.stock.move_item(unescape($(this).attr('data-item')), $(this).attr('data-warehouse'), - null, $(this).attr('data-actual_qty'), null, function() { me.refresh(); }); - }); - - this.content.on('click', '.btn-add', function() { - erpnext.stock.move_item(unescape($(this).attr('data-item')), null, $(this).attr('data-warehouse'), - $(this).attr('data-actual_qty'), $(this).attr('data-rate'), - function() { me.refresh(); }); - }); - // more this.content.find('.btn-more').on('click', function() { me.start += 20; @@ -111,89 +99,4 @@ erpnext.stock.ItemDashboard = Class.extend({ show_item: show_item || false } } -}) - -erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) { - var dialog = new frappe.ui.Dialog({ - title: target ? __('Add Item') : __('Move Item'), - fields: [ - {fieldname: 'item_code', label: __('Item'), - fieldtype: 'Link', options: 'Item', read_only: 1}, - {fieldname: 'source', label: __('Source Warehouse'), - fieldtype: 'Link', options: 'Warehouse', read_only: 1}, - {fieldname: 'target', label: __('Target Warehouse'), - fieldtype: 'Link', options: 'Warehouse', reqd: 1}, - {fieldname: 'qty', label: __('Quantity'), reqd: 1, - fieldtype: 'Float', description: __('Available {0}', [actual_qty]) }, - {fieldname: 'rate', label: __('Rate'), fieldtype: 'Currency', hidden: 1 }, - ], - }) - dialog.show(); - dialog.get_field('item_code').set_input(item); - - if(source) { - dialog.get_field('source').set_input(source); - } else { - dialog.get_field('source').df.hidden = 1; - dialog.get_field('source').refresh(); - } - - if(rate) { - dialog.get_field('rate').set_value(rate); - dialog.get_field('rate').df.hidden = 0; - dialog.get_field('rate').refresh(); - } - - if(target) { - dialog.get_field('target').df.read_only = 1; - dialog.get_field('target').value = target; - dialog.get_field('target').refresh(); - } - - dialog.set_primary_action(__('Submit'), function() { - var values = dialog.get_values(); - if(!values) { - return; - } - if(source && values.qty > actual_qty) { - frappe.msgprint(__('Quantity must be less than or equal to {0}', [actual_qty])); - return; - } - if(values.source === values.target) { - frappe.msgprint(__('Source and target warehouse must be different')); - } - - frappe.call({ - method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry', - args: values, - freeze: true, - callback: function(r) { - frappe.show_alert(__('Stock Entry {0} created', - ['' + r.message.name+ ''])); - dialog.hide(); - callback(r); - }, - }); - }); - - $('

' - + __("Add more items or open full form") + '

') - .appendTo(dialog.body) - .find('.link-open') - .on('click', function() { - frappe.model.with_doctype('Stock Entry', function() { - var doc = frappe.model.get_new_doc('Stock Entry'); - doc.from_warehouse = dialog.get_value('source'); - doc.to_warehouse = dialog.get_value('target'); - var row = frappe.model.add_child(doc, 'items'); - row.item_code = dialog.get_value('item_code'); - row.f_warehouse = dialog.get_value('target'); - row.t_warehouse = dialog.get_value('target'); - row.qty = dialog.get_value('qty'); - row.conversion_factor = 1; - row.transfer_qty = dialog.get_value('qty'); - row.basic_rate = dialog.get_value('rate'); - frappe.set_route('Form', doc.doctype, doc.name); - }) - }); -} \ No newline at end of file +}) \ No newline at end of file diff --git a/erpnext/stock/dashboard/item_dashboard_list.html b/erpnext/stock/dashboard/item_dashboard_list.html index 5a3fa2ed48..f0e87b1c53 100644 --- a/erpnext/stock/dashboard/item_dashboard_list.html +++ b/erpnext/stock/dashboard/item_dashboard_list.html @@ -39,21 +39,6 @@ - {% if can_write %} -
- {% if d.actual_qty %} -
- {% endif %} {% endfor %} From a596a7fd2bbf986c45d5d9df180dec6223351913 Mon Sep 17 00:00:00 2001 From: Govind S Menokee Date: Sat, 29 Jun 2019 04:08:26 +0530 Subject: [PATCH 06/71] feat: Add batch operation times in BOM Operation --- erpnext/manufacturing/doctype/bom/bom.py | 2 + .../doctype/bom_operation/bom_operation.json | 460 +++------- .../doctype/work_order/work_order.py | 9 +- .../work_order_operation.json | 847 ++++-------------- 4 files changed, 305 insertions(+), 1013 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 75eb794386..6925ed12aa 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -578,6 +578,8 @@ class BOM(WebsiteGenerator): for d in self.operations: if not d.description: d.description = frappe.db.get_value('Operation', d.operation, 'description') + if not d.is_batch_operation: + d.batch_size = 1 def get_list_context(context): context.title = _("Bill of Materials") diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 08c4f4fce6..298d097f33 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -1,361 +1,127 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:27:49", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2013-02-22 01:27:49", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "operation", + "workstation", + "description", + "col_break1", + "hour_rate", + "time_in_mins", + "is_batch_operation", + "batch_size", + "operating_cost", + "base_hour_rate", + "base_operating_cost", + "image" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "operation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Operation", - "length": 0, - "no_copy": 0, - "oldfieldname": "operation_no", - "oldfieldtype": "Data", - "options": "Operation", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "oldfieldname": "operation_no", + "oldfieldtype": "Data", + "options": "Operation", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "workstation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Workstation", - "length": 0, - "no_copy": 0, - "oldfieldname": "workstation", - "oldfieldtype": "Link", - "options": "Workstation", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "workstation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Workstation", + "oldfieldname": "workstation", + "oldfieldtype": "Link", + "options": "Workstation" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "opn_description", - "oldfieldtype": "Text", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "description", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "opn_description", + "oldfieldtype": "Text" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break1", - "fieldtype": "Column 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, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hour_rate", - "fieldtype": "Currency", - "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": "Hour Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate", - "oldfieldtype": "Currency", - "options": "currency", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "hour_rate", + "fieldtype": "Currency", + "label": "Hour Rate", + "oldfieldname": "hour_rate", + "oldfieldtype": "Currency", + "options": "currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "In minutes", - "fieldname": "time_in_mins", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Operation Time ", - "length": 0, - "no_copy": 0, - "oldfieldname": "time_in_mins", - "oldfieldtype": "Currency", - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "description": "In minutes", + "fieldname": "time_in_mins", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Operation Time ", + "oldfieldname": "time_in_mins", + "oldfieldtype": "Currency", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "operating_cost", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Operating Cost", - "length": 0, - "no_copy": 0, - "oldfieldname": "operating_cost", - "oldfieldtype": "Currency", - "options": "currency", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "operating_cost", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Operating Cost", + "oldfieldname": "operating_cost", + "oldfieldtype": "Currency", + "options": "currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_hour_rate", - "fieldtype": "Currency", - "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": "Base Hour Rate(Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "base_hour_rate", + "fieldtype": "Currency", + "label": "Base Hour Rate(Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "5", - "fieldname": "base_operating_cost", - "fieldtype": "Currency", - "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": "Operating Cost(Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "translatable": 0, - "unique": 0 - }, + "default": "5", + "fieldname": "base_operating_cost", + "fieldtype": "Currency", + "label": "Operating Cost(Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach", - "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": "Image", - "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, - "translatable": 0, - "unique": 0 + "fieldname": "image", + "fieldtype": "Attach", + "label": "Image" + }, + { + "default": "0", + "fieldname": "is_batch_operation", + "fieldtype": "Check", + "label": "Is Batch Operation" + }, + { + "default": "1", + "depends_on": "eval:doc.is_batch_operation==1;", + "fieldname": "batch_size", + "fieldtype": "Int", + "label": "Batch Size" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-03-26 09:55:28.107451", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Operation", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 0, - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "modified": "2019-06-29 03:35:32.213562", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Operation", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 0e8f69145b..24eb4e63ae 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe import json +import math from frappe import _ from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate from frappe.model.document import Document @@ -323,7 +324,8 @@ class WorkOrder(Document): select operation, description, workstation, idx, base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom + "Pending" as status, parent as bom, + is_batch_operation, batch_size from `tabBOM Operation` where @@ -348,7 +350,10 @@ class WorkOrder(Document): bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") for d in self.get("operations"): - d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * flt(self.qty) + if d.is_batch_operation: + d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * math.ceil(flt(self.qty)/flt(d.batch_size)) + else: + d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * flt(self.qty) self.calculate_operating_cost() diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 9c1c95383b..69735bfb28 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -1,690 +1,209 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2014-10-16 14:35:41.950175", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2014-10-16 14:35:41.950175", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "details", + "operation", + "bom", + "description", + "col_break1", + "completed_qty", + "status", + "workstation", + "estimated_time_and_cost", + "planned_start_time", + "planned_end_time", + "column_break_10", + "time_in_mins", + "hour_rate", + "is_batch_operation", + "batch_size", + "planned_operating_cost", + "section_break_9", + "actual_start_time", + "actual_end_time", + "column_break_11", + "actual_operation_time", + "actual_operating_cost" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "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": "", - "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 - }, + "fieldname": "details", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "operation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Operation", - "length": 0, - "no_copy": 0, - "oldfieldname": "operation_no", - "oldfieldtype": "Data", - "options": "Operation", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "oldfieldname": "operation_no", + "oldfieldtype": "Data", + "options": "Operation", + "read_only": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bom", - "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": "BOM", - "length": 0, - "no_copy": 1, - "options": "BOM", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "fieldname": "bom", + "fieldtype": "Link", + "label": "BOM", + "no_copy": 1, + "options": "BOM", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "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": "Operation Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "opn_description", - "oldfieldtype": "Text", - "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 - }, + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Operation Description", + "oldfieldname": "opn_description", + "oldfieldtype": "Text", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break1", - "fieldtype": "Column 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, - "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 - }, + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Operation completed for how many finished goods?", - "fieldname": "completed_qty", - "fieldtype": "Float", - "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": "Completed Qty", - "length": 0, - "no_copy": 1, - "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 - }, + "description": "Operation completed for how many finished goods?", + "fieldname": "completed_qty", + "fieldtype": "Float", + "label": "Completed Qty", + "no_copy": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Pending", - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Status", - "length": 0, - "no_copy": 1, - "options": "Pending\nWork in Progress\nCompleted", - "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 - }, + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "no_copy": 1, + "options": "Pending\nWork in Progress\nCompleted", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "workstation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Workstation", - "length": 0, - "no_copy": 0, - "oldfieldname": "workstation", - "oldfieldtype": "Link", - "options": "Workstation", - "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 - }, + "fieldname": "workstation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Workstation", + "oldfieldname": "workstation", + "oldfieldtype": "Link", + "options": "Workstation" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "estimated_time_and_cost", - "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": "Estimated Time and Cost", - "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 - }, + "fieldname": "estimated_time_and_cost", + "fieldtype": "Section Break", + "label": "Estimated Time and Cost" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "planned_start_time", - "fieldtype": "Datetime", - "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": "Planned Start Time", - "length": 0, - "no_copy": 1, - "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 - }, + "fieldname": "planned_start_time", + "fieldtype": "Datetime", + "label": "Planned Start Time", + "no_copy": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "planned_end_time", - "fieldtype": "Datetime", - "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": "Planned End Time", - "length": 0, - "no_copy": 1, - "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 - }, + "fieldname": "planned_end_time", + "fieldtype": "Datetime", + "label": "Planned End Time", + "no_copy": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_10", - "fieldtype": "Column 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, - "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 - }, + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "in Minutes", - "fieldname": "time_in_mins", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Operation Time", - "length": 0, - "no_copy": 0, - "oldfieldname": "time_in_mins", - "oldfieldtype": "Currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "in Minutes", + "fieldname": "time_in_mins", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Operation Time", + "oldfieldname": "time_in_mins", + "oldfieldtype": "Currency", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hour_rate", - "fieldtype": "Float", - "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": "Hour Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate", - "oldfieldtype": "Currency", - "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 - }, + "fieldname": "hour_rate", + "fieldtype": "Float", + "label": "Hour Rate", + "oldfieldname": "hour_rate", + "oldfieldtype": "Currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "planned_operating_cost", - "fieldtype": "Currency", - "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": "Planned Operating Cost", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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 - }, + "fieldname": "planned_operating_cost", + "fieldtype": "Currency", + "label": "Planned Operating Cost", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_9", - "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": "Actual Time and Cost", - "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 - }, + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Actual Time and Cost" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "actual_start_time", - "fieldtype": "Datetime", - "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": "Actual Start Time", - "length": 0, - "no_copy": 1, - "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 - }, + "fieldname": "actual_start_time", + "fieldtype": "Datetime", + "label": "Actual Start Time", + "no_copy": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Updated via 'Time Log'", - "fieldname": "actual_end_time", - "fieldtype": "Datetime", - "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": "Actual End Time", - "length": 0, - "no_copy": 1, - "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 - }, + "description": "Updated via 'Time Log'", + "fieldname": "actual_end_time", + "fieldtype": "Datetime", + "label": "Actual End Time", + "no_copy": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_11", - "fieldtype": "Column 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, - "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 - }, + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "in Minutes\nUpdated via 'Time Log'", - "fieldname": "actual_operation_time", - "fieldtype": "Float", - "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": "Actual Operation Time", - "length": 0, - "no_copy": 1, - "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 - }, + "description": "in Minutes\nUpdated via 'Time Log'", + "fieldname": "actual_operation_time", + "fieldtype": "Float", + "label": "Actual Operation Time", + "no_copy": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "(Hour Rate / 60) * Actual Operation Time", - "fieldname": "actual_operating_cost", - "fieldtype": "Currency", - "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": "Actual Operating Cost", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "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 + "description": "(Hour Rate / 60) * Actual Operation Time", + "fieldname": "actual_operating_cost", + "fieldtype": "Currency", + "label": "Actual Operating Cost", + "no_copy": 1, + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_batch_operation", + "fieldtype": "Check", + "label": "Is Batch Operation", + "read_only": 1 + }, + { + "depends_on": "eval:doc.is_batch_operation==1;", + "fieldname": "batch_size", + "fieldtype": "Int", + "label": "Batch Size", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-02-13 02:58:11.328693", - "modified_by": "Administrator", - "module": "Manufacturing", + ], + "istable": 1, + "modified": "2019-06-29 02:54:14.714995", + "modified_by": "Administrator", + "module": "Manufacturing", "name": "Work Order Operation", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file From fc03509e46e4a7a9a6ae2adce4ead3eabc3e3838 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 30 Jun 2019 16:24:29 +0530 Subject: [PATCH 07/71] created data structure for email automation --- .../campaign_email_schedule/__init__.py | 0 .../campaign_email_schedule.json | 38 +++++++ .../campaign_email_schedule.py | 10 ++ .../crm/doctype/email_campaign/__init__.py | 0 .../doctype/email_campaign/email_campaign.js | 8 ++ .../email_campaign/email_campaign.json | 100 ++++++++++++++++++ .../doctype/email_campaign/email_campaign.py | 10 ++ .../email_campaign/test_email_campaign.py | 10 ++ 8 files changed, 176 insertions(+) create mode 100644 erpnext/crm/doctype/campaign_email_schedule/__init__.py create mode 100644 erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json create mode 100644 erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.py create mode 100644 erpnext/crm/doctype/email_campaign/__init__.py create mode 100644 erpnext/crm/doctype/email_campaign/email_campaign.js create mode 100644 erpnext/crm/doctype/email_campaign/email_campaign.json create mode 100644 erpnext/crm/doctype/email_campaign/email_campaign.py create mode 100644 erpnext/crm/doctype/email_campaign/test_email_campaign.py diff --git a/erpnext/crm/doctype/campaign_email_schedule/__init__.py b/erpnext/crm/doctype/campaign_email_schedule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json new file mode 100644 index 0000000000..2d900940a3 --- /dev/null +++ b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.json @@ -0,0 +1,38 @@ +{ + "creation": "2019-06-30 15:56:20.306901", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "send_after_days", + "email_template" + ], + "fields": [ + { + "fieldname": "send_after_days", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Send After (days)", + "reqd": 1 + }, + { + "fieldname": "email_template", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Email Template", + "options": "Email Template", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-06-30 15:56:20.306901", + "modified_by": "Administrator", + "module": "CRM", + "name": "Campaign Email Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.py b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.py new file mode 100644 index 0000000000..8445b8a397 --- /dev/null +++ b/erpnext/crm/doctype/campaign_email_schedule/campaign_email_schedule.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class CampaignEmailSchedule(Document): + pass diff --git a/erpnext/crm/doctype/email_campaign/__init__.py b/erpnext/crm/doctype/email_campaign/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.js b/erpnext/crm/doctype/email_campaign/email_campaign.js new file mode 100644 index 0000000000..6020028a13 --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Email Campaign', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.json b/erpnext/crm/doctype/email_campaign/email_campaign.json new file mode 100644 index 0000000000..49b3c0643d --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign.json @@ -0,0 +1,100 @@ +{ + "autoname": "naming_series:", + "creation": "2019-06-30 16:05:30.015615", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign_section", + "campaign_name", + "lead", + "column_break_4", + "start_date", + "status", + "email_schedule_section", + "email_schedule", + "naming_series" + ], + "fields": [ + { + "fieldname": "campaign_section", + "fieldtype": "Section Break", + "label": "CAMPAIGN " + }, + { + "fieldname": "campaign_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Campaign Name", + "options": "Campaign", + "reqd": 1 + }, + { + "fieldname": "lead", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Lead", + "options": "Lead", + "reqd": 1 + }, + { + "default": "Started", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "\nStarted\nIn Progress\nCompleted" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "reqd": 1 + }, + { + "fieldname": "email_schedule_section", + "fieldtype": "Section Break", + "label": "EMAIL SCHEDULE" + }, + { + "fieldname": "email_schedule", + "fieldtype": "Table", + "label": "Email Schedule", + "options": "Campaign Email Schedule", + "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "MAIL-CAMP-.YYYY.-", + "reqd": 1 + } + ], + "modified": "2019-06-30 16:23:00.696185", + "modified_by": "Administrator", + "module": "CRM", + "name": "Email Campaign", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py new file mode 100644 index 0000000000..baa82e8969 --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class EmailCampaign(Document): + pass diff --git a/erpnext/crm/doctype/email_campaign/test_email_campaign.py b/erpnext/crm/doctype/email_campaign/test_email_campaign.py new file mode 100644 index 0000000000..f5eab48333 --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/test_email_campaign.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEmailCampaign(unittest.TestCase): + pass From dcf5fbd35dca426941342cae25a9b9e7d53eacb8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 30 Jun 2019 16:36:09 +0530 Subject: [PATCH 08/71] added some validations --- .../doctype/email_campaign/email_campaign.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index baa82e8969..c3e8f9f559 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -3,8 +3,26 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +from frappe import _ +from frappe.utils import getdate, add_days from frappe.model.document import Document class EmailCampaign(Document): - pass + def validate(self): + self.validate_dates() + + def validate_dates(self): + campaign = frappe.get_doc("Campaign", self.campaign_name) + + #email campaign cannot start before campaign + if campaign.from_date and getdate(self.start_date) < getdate(campaign.from_date): + frappe.throw(_("Email Campaign Start Date cannot be before Campaign Start Date")) + + #check if email_schedule is exceeding the campaign end date + no_of_days = 0 + for entry in self.get("email_schedule"): + no_of_days += entry.send_after_days + email_schedule_end_date = add_days(getdate(self.start_date), no_of_days) + if campaign.to_date and getdate(email_schedule_end_date) > getdate(campaign.to_date): + frappe.throw(_("Email Schedule cannot extend Campaign End Date")) From 162f7d1b50c175e85e118f15140b55e039084210 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 1 Jul 2019 01:09:58 +0530 Subject: [PATCH 09/71] auto email and new communication linked to email campaign setup --- .../email_campaign/email_campaign.json | 17 ++++++-- .../doctype/email_campaign/email_campaign.py | 42 ++++++++++++++++++- erpnext/hooks.py | 1 + 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.json b/erpnext/crm/doctype/email_campaign/email_campaign.json index 49b3c0643d..d7113f6b4e 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.json +++ b/erpnext/crm/doctype/email_campaign/email_campaign.json @@ -13,7 +13,8 @@ "status", "email_schedule_section", "email_schedule", - "naming_series" + "naming_series", + "amended_from" ], "fields": [ { @@ -42,7 +43,7 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "\nStarted\nIn Progress\nCompleted" + "options": "\nDraft\nSubmitted\nStarted\nIn Progress\nCompleted" }, { "fieldname": "column_break_4", @@ -72,9 +73,19 @@ "label": "Naming Series", "options": "MAIL-CAMP-.YYYY.-", "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Email Campaign", + "print_hide": 1, + "read_only": 1 } ], - "modified": "2019-06-30 16:23:00.696185", + "is_submittable": 1, + "modified": "2019-06-30 23:00:24.765312", "modified_by": "Administrator", "module": "CRM", "name": "Email Campaign", diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index c3e8f9f559..82ee6a6cb4 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -5,12 +5,14 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import getdate, add_days +from frappe.utils import getdate, add_days, nowdate from frappe.model.document import Document +from frappe.email.inbox import link_communication_to_document class EmailCampaign(Document): def validate(self): self.validate_dates() + self.validate_lead() def validate_dates(self): campaign = frappe.get_doc("Campaign", self.campaign_name) @@ -25,4 +27,40 @@ class EmailCampaign(Document): no_of_days += entry.send_after_days email_schedule_end_date = add_days(getdate(self.start_date), no_of_days) if campaign.to_date and getdate(email_schedule_end_date) > getdate(campaign.to_date): - frappe.throw(_("Email Schedule cannot extend Campaign End Date")) + frappe.throw(_("Email Schedule cannot extend Campaign End Date")) + + def validate_lead(self): + lead = frappe.get_doc("Lead", self.lead) + if not lead.get("email_id"): + frappe.throw(_("Please set email id for lead communication")) + + def send(self): + lead = frappe.get_doc("Lead", self.get("lead")) + email_schedule = frappe.get_doc("Campaign Email Schedule", self.get("email_schedule")) + email_template = frappe.get_doc("Email Template", email_schedule.name) + frappe.sendmail( + recipients = lead.get("email_id"), + sender = lead.get("lead_owner"), + subject = email_template.get("subject"), + message = email_template.get("response"), + reference_doctype = self.doctype, + reference_name = self.name + ) + + def on_submit(self): + """Create a new communication linked to the campaign if not created""" + if not frappe.db.sql("select subject from tabCommunication where reference_name = %s", self.name): + doc = frappe.new_doc("Communication") + doc.subject = "Email Campaign Communication: " + self.name + link_communication_to_document(doc, "Email Campaign", self.name, ignore_communication_links = False) + +@frappe.whitelist() +def send_email_to_leads(): + email_campaigns = frappe.get_all("Email Campaign", filters = { 'start_date': ("<=", nowdate()) }) + for campaign in email_campaigns: + email_campaign = frappe.get_doc("Email Campaign", campaign.name) + for entry in email_campaign.get("email_schedule"): + scheduled_date = add_days(email_campaign.get('start_date'), entry.get('send_after_days')) + if(scheduled_date == nowdate()): + email_campaign.send() +# send_email_to_leads() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6ce75bbac3..1466d243ad 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -266,6 +266,7 @@ scheduler_events = { "erpnext.projects.doctype.project.project.send_project_status_email_to_users", "erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", + "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads" ], "daily_long": [ "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" From 38b930b638f10888b629c02284813212dc36e5d7 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 5 Jul 2019 10:38:48 +0530 Subject: [PATCH 10/71] refactor: Move and Add buttons open new stock entry --- erpnext/stock/dashboard/item_dashboard.js | 25 +++++++++++++++++++ .../stock/dashboard/item_dashboard_list.html | 15 +++++++++++ 2 files changed, 40 insertions(+) diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index f820b7aa86..ed325a16b8 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -16,6 +16,31 @@ erpnext.stock.ItemDashboard = Class.extend({ this.content = $(frappe.render_template('item_dashboard')).appendTo(this.parent); this.result = this.content.find('.result'); + this.content.on('click', '.btn-move', function() { + let item = unescape($(this).attr('data-item')); + let warehouse = unescape($(this).attr('data-warehouse')); + open_stock_entry(item, warehouse, "Material Transfer"); + }); + + this.content.on('click', '.btn-add', function() { + let item = unescape($(this).attr('data-item')); + let warehouse = unescape($(this).attr('data-warehouse')); + open_stock_entry(item, warehouse); + }); + + function open_stock_entry(item, warehouse, entry_type) { + frappe.model.with_doctype('Stock Entry', function() { + var doc = frappe.model.get_new_doc('Stock Entry'); + if (entry_type) doc.stock_entry_type = entry_type; + + var row = frappe.model.add_child(doc, 'items'); + row.item_code = item; + row.s_warehouse = warehouse; + + frappe.set_route('Form', doc.doctype, doc.name); + }) + } + // more this.content.find('.btn-more').on('click', function() { me.start += 20; diff --git a/erpnext/stock/dashboard/item_dashboard_list.html b/erpnext/stock/dashboard/item_dashboard_list.html index f0e87b1c53..5a3fa2ed48 100644 --- a/erpnext/stock/dashboard/item_dashboard_list.html +++ b/erpnext/stock/dashboard/item_dashboard_list.html @@ -39,6 +39,21 @@ + {% if can_write %} +
+ {% if d.actual_qty %} +
+ {% endif %} {% endfor %} From 36963a8e0408af1171ad0ee170ac0224ebeebc5b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 9 Jul 2019 15:14:13 +0530 Subject: [PATCH 11/71] feat: Email Campaign --- .../email_campaign/email_campaign.json | 66 +++++---- .../doctype/email_campaign/email_campaign.py | 128 ++++++++++++++---- .../email_campaign/email_campaign_list.js | 11 ++ erpnext/hooks.py | 3 +- 4 files changed, 155 insertions(+), 53 deletions(-) create mode 100644 erpnext/crm/doctype/email_campaign/email_campaign_list.js diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.json b/erpnext/crm/doctype/email_campaign/email_campaign.json index d7113f6b4e..66b35467cf 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.json +++ b/erpnext/crm/doctype/email_campaign/email_campaign.json @@ -7,20 +7,23 @@ "field_order": [ "campaign_section", "campaign_name", - "lead", - "column_break_4", + "email_campaign_for", "start_date", + "column_break_4", + "sender", + "recipient", + "end_date", "status", "email_schedule_section", "email_schedule", - "naming_series", - "amended_from" + "unsubscribed", + "naming_series" ], "fields": [ { "fieldname": "campaign_section", "fieldtype": "Section Break", - "label": "CAMPAIGN " + "label": "Campaign" }, { "fieldname": "campaign_name", @@ -31,19 +34,10 @@ "reqd": 1 }, { - "fieldname": "lead", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Lead", - "options": "Lead", - "reqd": 1 - }, - { - "default": "Started", "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "\nDraft\nSubmitted\nStarted\nIn Progress\nCompleted" + "options": "\nScheduled\nIn Progress\nCompleted\nUnsubscribed" }, { "fieldname": "column_break_4", @@ -58,7 +52,7 @@ { "fieldname": "email_schedule_section", "fieldtype": "Section Break", - "label": "EMAIL SCHEDULE" + "label": "Email Schedule" }, { "fieldname": "email_schedule", @@ -75,17 +69,41 @@ "reqd": 1 }, { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Email Campaign", - "print_hide": 1, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", "read_only": 1 + }, + { + "default": "Lead", + "fieldname": "email_campaign_for", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Email Campaign For ", + "options": "\nLead\nContact" + }, + { + "fieldname": "recipient", + "fieldtype": "Dynamic Link", + "label": "Recipient", + "options": "email_campaign_for", + "reqd": 1 + }, + { + "default": "__user", + "fieldname": "sender", + "fieldtype": "Link", + "label": "Sender", + "options": "User" + }, + { + "default": "0", + "fieldname": "unsubscribed", + "fieldtype": "Check", + "label": "Unsubscribed" } ], - "is_submittable": 1, - "modified": "2019-06-30 23:00:24.765312", + "modified": "2019-07-09 15:07:03.328591", "modified_by": "Administrator", "module": "CRM", "name": "Email Campaign", diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 82ee6a6cb4..1132226b90 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -5,14 +5,18 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import getdate, add_days, nowdate +from frappe.utils import getdate, add_days, today, nowdate, cstr from frappe.model.document import Document -from frappe.email.inbox import link_communication_to_document +from frappe.core.doctype.communication.email import make class EmailCampaign(Document): def validate(self): self.validate_dates() - self.validate_lead() + #checking if email is set for lead. Not checking for contact as email is a mandatory field for contact. + if self.email_campaign_for == "Lead": + self.validate_lead() + self.set_end_date() + self.update_status() def validate_dates(self): campaign = frappe.get_doc("Campaign", self.campaign_name) @@ -30,37 +34,105 @@ class EmailCampaign(Document): frappe.throw(_("Email Schedule cannot extend Campaign End Date")) def validate_lead(self): - lead = frappe.get_doc("Lead", self.lead) + lead = frappe.get_doc("Lead", self.recipient) if not lead.get("email_id"): - frappe.throw(_("Please set email id for lead communication")) + frappe.throw(_("Please set an email id for lead communication")) - def send(self): - lead = frappe.get_doc("Lead", self.get("lead")) - email_schedule = frappe.get_doc("Campaign Email Schedule", self.get("email_schedule")) - email_template = frappe.get_doc("Email Template", email_schedule.name) - frappe.sendmail( - recipients = lead.get("email_id"), - sender = lead.get("lead_owner"), - subject = email_template.get("subject"), - message = email_template.get("response"), - reference_doctype = self.doctype, - reference_name = self.name - ) + def set_end_date(self): + #set the end date as start date + max(send after days) in email schedule + send_after_days = [] + for entry in self.get("email_schedule"): + send_after_days.append(entry.send_after_days) + self.end_date = add_days(getdate(self.start_date), max(send_after_days)) - def on_submit(self): - """Create a new communication linked to the campaign if not created""" - if not frappe.db.sql("select subject from tabCommunication where reference_name = %s", self.name): - doc = frappe.new_doc("Communication") - doc.subject = "Email Campaign Communication: " + self.name - link_communication_to_document(doc, "Email Campaign", self.name, ignore_communication_links = False) + def update_status(self): + start_date = getdate(self.start_date) + end_date = getdate(self.end_date) + today_date = getdate(today()) + if self.unsubscribed: + self.status = "Unsubscribed" + else: + if start_date > today_date: + self.status = "Scheduled" + elif end_date >= today_date: + self.status = "In Progress" + elif end_date < today_date: + self.status = "Completed" -@frappe.whitelist() +#called through hooks to send campaign mails to leads def send_email_to_leads(): - email_campaigns = frappe.get_all("Email Campaign", filters = { 'start_date': ("<=", nowdate()) }) + email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('not in', ['Unsubscribed', 'Completed', 'Scheduled']), 'unsubscribed': 0 }) for campaign in email_campaigns: email_campaign = frappe.get_doc("Email Campaign", campaign.name) for entry in email_campaign.get("email_schedule"): scheduled_date = add_days(email_campaign.get('start_date'), entry.get('send_after_days')) - if(scheduled_date == nowdate()): - email_campaign.send() -# send_email_to_leads() + if scheduled_date == getdate(today()): + send_mail(entry, email_campaign) + +def send_mail(entry, email_campaign): + if email_campaign.email_campaign_for == "Lead": + lead = frappe.get_doc("Lead", email_campaign.get("recipient")) + recipient_email = lead.email_id + elif email_campaign.email_campaign_for == "Contact": + recipient = frappe.get_doc("Contact", email_campaign.get("recipient")) + recipient_email = recipient.email_id + email_template = frappe.get_doc("Email Template", entry.get("email_template")) + sender = frappe.get_doc("User", email_campaign.get("sender")) + sender_email = sender.email + # send mail and link communication to document + comm = make( + doctype = "Email Campaign", + name = email_campaign.name, + subject = email_template.get("subject"), + content = email_template.get("response"), + sender = sender_email, + recipients = recipient_email, + communication_medium = "Email", + sent_or_received = "Sent", + send_email = False, + email_template = email_template.name + ) + frappe.sendmail( + recipients = recipient_email, + sender = sender_email, + subject = email_template.get("subject"), + content = email_template.get("response"), + reference_doctype = "Email Campaign", + reference_name = email_campaign.name, + unsubscribe_method = "/api/method/erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient", + unsubscribe_params = {"name": email_campaign.name, "email": recipient_email}, + unsubscribe_message = "Stop Getting Email Campaign Mails", + communication = comm.get("name") + ) + +@frappe.whitelist(allow_guest=True) +def unsubscribe_recipient(name, email): + # unsubsribe from comments and communications + try: + frappe.get_doc({ + "doctype": "Email Unsubscribe", + "email": email, + "reference_doctype": "Email Campaign", + "reference_name": name + }).insert(ignore_permissions=True) + + except frappe.DuplicateEntryError: + frappe.db.rollback() + + else: + frappe.db.commit() + frappe.db.set_value("Email Campaign", name, "unsubscribed", 1) + frappe.db.set_value("Email Campaign", name, "status", "Unsubscribed") + frappe.db.commit() + return_unsubscribed_page(email, name) + +def return_unsubscribed_page(email, name): + frappe.respond_as_web_page(_("Unsubscribed"), + _("{0} has left the Email Campaign {1}").format(email, name), + indicator_color='green') + +#called through hooks to update email campaign status daily +def set_email_campaign_status(): + email_campaigns = frappe.get_all("Email Campaign") + for email_campaign in email_campaigns: + email_campaign.update_status() diff --git a/erpnext/crm/doctype/email_campaign/email_campaign_list.js b/erpnext/crm/doctype/email_campaign/email_campaign_list.js new file mode 100644 index 0000000000..d1bfdd31ee --- /dev/null +++ b/erpnext/crm/doctype/email_campaign/email_campaign_list.js @@ -0,0 +1,11 @@ +frappe.listview_settings['Email Campaign'] = { + get_indicator: function(doc) { + var colors = { + "Unsubscribed": "red", + "Scheduled": "blue", + "In Progress": "orange", + "Completed": "green" + } + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + } +}; diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 1466d243ad..1b34a59f55 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -266,7 +266,8 @@ scheduler_events = { "erpnext.projects.doctype.project.project.send_project_status_email_to_users", "erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", - "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads" + "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads", + "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status" ], "daily_long": [ "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" From 444091b35013f9747a2fa4048f4388cf4d0154a5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 10 Jul 2019 10:22:08 +0530 Subject: [PATCH 12/71] refactor: disable quick entry for batched and serialized items --- erpnext/stock/dashboard/item_dashboard.js | 102 +++++++++++++++++- erpnext/stock/dashboard/item_dashboard.py | 4 +- .../stock/dashboard/item_dashboard_list.html | 2 + 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index ed325a16b8..c84acc54b3 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -19,13 +19,24 @@ erpnext.stock.ItemDashboard = Class.extend({ this.content.on('click', '.btn-move', function() { let item = unescape($(this).attr('data-item')); let warehouse = unescape($(this).attr('data-warehouse')); - open_stock_entry(item, warehouse, "Material Transfer"); + let actual_qty = unescape($(this).attr('data-actual_qty')); + let disable_quick_entry = Number(unescape($(this).attr('data-disable_quick_entry'))); + + if (disable_quick_entry) open_stock_entry(item, warehouse, "Material Transfer"); + + else erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function() { me.refresh(); }) }); this.content.on('click', '.btn-add', function() { let item = unescape($(this).attr('data-item')); let warehouse = unescape($(this).attr('data-warehouse')); - open_stock_entry(item, warehouse); + let actual_qty = unescape($(this).attr('data-actual_qty')); + let disable_quick_entry = Number(unescape($(this).attr('data-disable_quick_entry'))); + let rate = unescape($(this).attr('data-rate')); + + if (disable_quick_entry) open_stock_entry(item, warehouse); + + else erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function() { me.refresh(); }) }); function open_stock_entry(item, warehouse, entry_type) { @@ -124,4 +135,89 @@ erpnext.stock.ItemDashboard = Class.extend({ show_item: show_item || false } } -}) \ No newline at end of file +}) + +erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) { + var dialog = new frappe.ui.Dialog({ + title: target ? __('Add Item') : __('Move Item'), + fields: [ + {fieldname: 'item_code', label: __('Item'), + fieldtype: 'Link', options: 'Item', read_only: 1}, + {fieldname: 'source', label: __('Source Warehouse'), + fieldtype: 'Link', options: 'Warehouse', read_only: 1}, + {fieldname: 'target', label: __('Target Warehouse'), + fieldtype: 'Link', options: 'Warehouse', reqd: 1}, + {fieldname: 'qty', label: __('Quantity'), reqd: 1, + fieldtype: 'Float', description: __('Available {0}', [actual_qty]) }, + {fieldname: 'rate', label: __('Rate'), fieldtype: 'Currency', hidden: 1 }, + ], + }) + dialog.show(); + dialog.get_field('item_code').set_input(item); + + if(source) { + dialog.get_field('source').set_input(source); + } else { + dialog.get_field('source').df.hidden = 1; + dialog.get_field('source').refresh(); + } + + if(rate) { + dialog.get_field('rate').set_value(rate); + dialog.get_field('rate').df.hidden = 0; + dialog.get_field('rate').refresh(); + } + + if(target) { + dialog.get_field('target').df.read_only = 1; + dialog.get_field('target').value = target; + dialog.get_field('target').refresh(); + } + + dialog.set_primary_action(__('Submit'), function() { + var values = dialog.get_values(); + if(!values) { + return; + } + if(source && values.qty > actual_qty) { + frappe.msgprint(__('Quantity must be less than or equal to {0}', [actual_qty])); + return; + } + if(values.source === values.target) { + frappe.msgprint(__('Source and target warehouse must be different')); + } + + frappe.call({ + method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry', + args: values, + freeze: true, + callback: function(r) { + frappe.show_alert(__('Stock Entry {0} created', + ['' + r.message.name+ ''])); + dialog.hide(); + callback(r); + }, + }); + }); + + $('

' + + __("Add more items or open full form") + '

') + .appendTo(dialog.body) + .find('.link-open') + .on('click', function() { + frappe.model.with_doctype('Stock Entry', function() { + var doc = frappe.model.get_new_doc('Stock Entry'); + doc.from_warehouse = dialog.get_value('source'); + doc.to_warehouse = dialog.get_value('target'); + var row = frappe.model.add_child(doc, 'items'); + row.item_code = dialog.get_value('item_code'); + row.f_warehouse = dialog.get_value('target'); + row.t_warehouse = dialog.get_value('target'); + row.qty = dialog.get_value('qty'); + row.conversion_factor = 1; + row.transfer_qty = dialog.get_value('qty'); + row.basic_rate = dialog.get_value('rate'); + frappe.set_route('Form', doc.doctype, doc.name); + }) + }); +} \ No newline at end of file diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index 487c765659..7634ff0a28 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -44,7 +44,9 @@ def get_data(item_code=None, warehouse=None, item_group=None, for item in items: item.update({ - 'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name') + 'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'), + 'disable_quick_entry': frappe.get_cached_value("Item", item.item_code, 'has_batch_no') + or frappe.get_cached_value("Item", item.item_code, 'has_serial_no'), }) return items diff --git a/erpnext/stock/dashboard/item_dashboard_list.html b/erpnext/stock/dashboard/item_dashboard_list.html index 5a3fa2ed48..e1914ed76a 100644 --- a/erpnext/stock/dashboard/item_dashboard_list.html +++ b/erpnext/stock/dashboard/item_dashboard_list.html @@ -43,11 +43,13 @@
{% if d.actual_qty %}