From e6143abb8a89082508534aaacdca54fc1c2cc669 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 13 Mar 2023 14:51:43 +0530 Subject: [PATCH] refactor: added new file serial batch bundle --- .../pos_invoice_item/pos_invoice_item.json | 20 +- .../purchase_invoice/purchase_invoice.py | 3 - .../purchase_invoice_item.json | 20 +- .../sales_invoice_item.json | 20 +- erpnext/controllers/buying_controller.py | 21 +- .../controllers/sales_and_purchase_return.py | 3 +- erpnext/controllers/selling_controller.py | 1 + erpnext/controllers/stock_controller.py | 54 +- .../doctype/job_card/job_card.json | 17 +- .../doctype/work_order/work_order.json | 3 +- .../add_missing_fg_item_for_stock_entry.py | 1 - erpnext/public/js/controllers/buying.js | 32 +- erpnext/public/js/controllers/transaction.js | 4 + .../js/utils/serial_no_batch_selector.js | 55 +- .../installation_note_item.json | 354 ++++--------- erpnext/selling/sales_common.js | 2 +- erpnext/stock/deprecated_serial_batch.py | 101 ++++ erpnext/stock/doctype/batch/batch.json | 2 +- erpnext/stock/doctype/batch/batch.py | 4 +- .../delivery_note_item.json | 2 + .../doctype/packed_item/packed_item.json | 16 +- .../pick_list_item/pick_list_item.json | 25 +- .../purchase_receipt/purchase_receipt.py | 4 +- .../purchase_receipt_item.json | 36 +- .../serial_and_batch_bundle.json | 59 ++- .../serial_and_batch_bundle.py | 306 +++++++++-- .../serial_and_batch_ledger.json | 2 +- .../serial_and_batch_no_bundle/__init__.py | 0 .../serial_and_batch_no_bundle.js | 8 - .../serial_and_batch_no_bundle.json | 176 ------ .../serial_and_batch_no_bundle.py | 9 - .../test_serial_and_batch_no_bundle.py | 9 - .../stock/doctype/serial_no/serial_no.json | 38 +- erpnext/stock/doctype/serial_no/serial_no.py | 120 +---- .../stock/doctype/stock_entry/stock_entry.js | 78 +-- .../stock/doctype/stock_entry/stock_entry.py | 210 ++++++-- .../stock_entry_detail.json | 23 +- .../stock_ledger_entry/stock_ledger_entry.py | 51 +- .../stock_reconciliation.py | 1 - .../stock_reconciliation_item.json | 20 +- erpnext/stock/serial_batch_bundle.py | 501 ++++++++++++------ erpnext/stock/stock_ledger.py | 99 +--- erpnext/stock/utils.py | 40 +- .../subcontracting_receipt.py | 3 - .../subcontracting_receipt_item.json | 28 +- .../subcontracting_receipt_supplied_item.json | 18 +- 46 files changed, 1468 insertions(+), 1131 deletions(-) create mode 100644 erpnext/stock/deprecated_serial_batch.py delete mode 100644 erpnext/stock/doctype/serial_and_batch_no_bundle/__init__.py delete mode 100644 erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.js delete mode 100644 erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.json delete mode 100644 erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.py delete mode 100644 erpnext/stock/doctype/serial_and_batch_no_bundle/test_serial_and_batch_no_bundle.py diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 4bb18655b4..cb0ed3d6aa 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -79,6 +79,7 @@ "warehouse", "target_warehouse", "quality_inspection", + "serial_and_batch_bundle", "batch_no", "col_break5", "allow_zero_valuation_rate", @@ -628,10 +629,11 @@ { "fieldname": "batch_no", "fieldtype": "Link", - "in_list_view": 1, + "hidden": 1, "label": "Batch No", "options": "Batch", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "col_break5", @@ -648,10 +650,12 @@ { "fieldname": "serial_no", "fieldtype": "Small Text", + "hidden": 1, "in_list_view": 1, "label": "Serial No", "oldfieldname": "serial_no", - "oldfieldtype": "Small Text" + "oldfieldtype": "Small Text", + "read_only": 1 }, { "fieldname": "item_tax_rate", @@ -817,11 +821,19 @@ "fieldtype": "Check", "label": "Has Item Scanned", "read_only": 1 + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 } ], "istable": 1, "links": [], - "modified": "2022-11-02 12:52:39.125295", + "modified": "2023-03-12 13:36:40.160468", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 8ed11a4299..f46cec6fa4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -102,9 +102,6 @@ class PurchaseInvoice(BuyingController): # validate service stop date to lie in between start and end date validate_service_stop_date(self) - if self._action == "submit" and self.update_stock: - self.make_batches("warehouse") - self.validate_release_date() self.check_conversion_rate() self.validate_credit_to_acc() diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 1fa7e7f3fc..b58871ba7f 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -64,6 +64,7 @@ "warehouse", "from_warehouse", "quality_inspection", + "serial_and_batch_bundle", "serial_no", "col_br_wh", "rejected_warehouse", @@ -436,9 +437,10 @@ "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "batch_no", "fieldtype": "Link", + "hidden": 1, "label": "Batch No", - "no_copy": 1, - "options": "Batch" + "options": "Batch", + "read_only": 1 }, { "fieldname": "col_br_wh", @@ -448,8 +450,9 @@ "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "serial_no", "fieldtype": "Text", + "hidden": 1, "label": "Serial No", - "no_copy": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.is_fixed_asset", @@ -875,12 +878,21 @@ "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply TDS" + }, + { + "depends_on": "eval:!doc.is_fixed_asset", + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-11-29 13:01:20.438217", + "modified": "2023-03-12 13:40:39.044607", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 35d19ed843..f3e21858c4 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -81,6 +81,7 @@ "warehouse", "target_warehouse", "quality_inspection", + "serial_and_batch_bundle", "batch_no", "incoming_rate", "col_break5", @@ -600,10 +601,10 @@ { "fieldname": "batch_no", "fieldtype": "Link", - "in_list_view": 1, + "hidden": 1, "label": "Batch No", "options": "Batch", - "print_hide": 1 + "read_only": 1 }, { "fieldname": "col_break5", @@ -620,10 +621,11 @@ { "fieldname": "serial_no", "fieldtype": "Small Text", - "in_list_view": 1, + "hidden": 1, "label": "Serial No", "oldfieldname": "serial_no", - "oldfieldtype": "Small Text" + "oldfieldtype": "Small Text", + "read_only": 1 }, { "fieldname": "item_group", @@ -885,12 +887,20 @@ "fieldtype": "Check", "label": "Has Item Scanned", "read_only": 1 + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-12-28 16:17:33.484531", + "modified": "2023-03-12 13:42:24.303113", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index f87f38ea53..85624d5afb 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -58,6 +58,7 @@ class BuyingController(SubcontractingController): if self.doctype in ("Purchase Receipt", "Purchase Invoice"): self.update_valuation_rate() + self.set_serial_and_batch_bundle() def onload(self): super(BuyingController, self).onload() @@ -305,8 +306,7 @@ class BuyingController(SubcontractingController): "posting_date": self.get("posting_date") or self.get("transation_date"), "posting_time": posting_time, "qty": -1 * flt(d.get("stock_qty")), - "serial_no": d.get("serial_no"), - "batch_no": d.get("batch_no"), + "serial_and_batch_bundle": d.get("serial_and_batch_bundle"), "company": self.company, "voucher_type": self.doctype, "voucher_no": self.name, @@ -463,7 +463,12 @@ class BuyingController(SubcontractingController): sl_entries.append(from_warehouse_sle) sle = self.get_sl_entries( - d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()} + d, + { + "actual_qty": flt(pr_qty), + "serial_no": cstr(d.serial_no).strip(), + "serial_and_batch_bundle": d.serial_and_batch_bundle, + }, ) if self.is_return: @@ -471,7 +476,13 @@ class BuyingController(SubcontractingController): self.doctype, self.name, d.item_code, self.return_against, item_row=d ) - sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1}) + sle.update( + { + "outgoing_rate": outgoing_rate, + "recalculate_rate": 1, + "serial_and_batch_bundle": d.serial_and_batch_bundle, + } + ) if d.from_warehouse: sle.dependant_sle_voucher_detail_no = d.name else: @@ -483,6 +494,7 @@ class BuyingController(SubcontractingController): "recalculate_rate": 1 if (self.is_subcontracted and (d.bom or d.fg_item)) or d.from_warehouse else 0, + "serial_and_batch_bundle": d.serial_and_batch_bundle, } ) sl_entries.append(sle) @@ -506,6 +518,7 @@ class BuyingController(SubcontractingController): "actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor), "serial_no": cstr(d.rejected_serial_no).strip(), "incoming_rate": 0.0, + "serial_and_batch_bundle": d.rejected_serial_and_batch_bundle, }, ) ) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 15c270e58a..80275de8e6 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -573,8 +573,7 @@ def get_rate_for_return( "posting_date": sle.get("posting_date"), "posting_time": sle.get("posting_time"), "qty": sle.actual_qty, - "serial_no": sle.get("serial_no"), - "batch_no": sle.get("batch_no"), + "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"), "company": sle.company, "voucher_type": sle.voucher_type, "voucher_no": sle.voucher_no, diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index bd4bc18fb8..f6e1e05fe3 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -38,6 +38,7 @@ class SellingController(StockController): self.validate_for_duplicate_items() self.validate_target_warehouse() self.validate_auto_repeat_subscription_dates() + self.set_serial_and_batch_bundle() def set_missing_values(self, for_validate=False): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 6e71004374..342b8e98c1 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -325,53 +325,6 @@ class StockController(AccountsController): stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) return stock_ledger - def make_batches(self, warehouse_field): - """Create batches if required. Called before submit""" - for d in self.items: - if d.get(warehouse_field) and not d.serial_and_batch_bundle: - has_batch_no, create_new_batch = frappe.get_cached_value( - "Item", d.item_code, ["has_batch_no", "create_new_batch"] - ) - - if has_batch_no and create_new_batch: - batch_no = ( - frappe.get_doc( - dict(doctype="Batch", item=d.item_code, supplier=getattr(self, "supplier", None)) - ) - .insert() - .name - ) - - d.serial_and_batch_bundle = ( - frappe.get_doc( - { - "doctype": "Serial and Batch Bundle", - "item_code": d.item_code, - "voucher_type": self.doctype, - "voucher_no": self.name, - "ledgers": [ - { - "batch_no": batch_no, - "qty": d.qty, - "warehouse": d.get(warehouse_field), - "incoming_rate": d.rate, - } - ], - } - ) - .submit() - .name - ) - - frappe.db.set_value( - "Batch", - batch_no, - { - "reference_doctype": "Serial and Batch Bundle", - "reference_name": d.serial_and_batch_bundle, - }, - ) - def check_expense_account(self, item): if not item.get("expense_account"): msg = _("Please set an Expense Account in the Items table") @@ -761,6 +714,13 @@ class StockController(AccountsController): message = self.prepare_over_receipt_message(rule, values) frappe.throw(msg=message, title=_("Over Receipt")) + def set_serial_and_batch_bundle(self): + for row in self.items: + if row.serial_and_batch_bundle: + frappe.get_doc( + "Serial and Batch Bundle", row.serial_and_batch_bundle + ).set_serial_and_batch_values(self, row) + def prepare_over_receipt_message(self, rule, values): message = _( "{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}." diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 316e586b7a..f49f018d20 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -16,6 +16,7 @@ "production_item", "item_name", "for_quantity", + "serial_and_batch_bundle", "serial_no", "column_break_12", "wip_warehouse", @@ -391,13 +392,17 @@ { "fieldname": "serial_no", "fieldtype": "Small Text", - "label": "Serial No" + "hidden": 1, + "label": "Serial No", + "read_only": 1 }, { "fieldname": "batch_no", "fieldtype": "Link", + "hidden": 1, "label": "Batch No", - "options": "Batch" + "options": "Batch", + "read_only": 1 }, { "collapsible": 1, @@ -435,6 +440,14 @@ "fieldname": "expected_end_date", "fieldtype": "Datetime", "label": "Expected End Date" + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 } ], "is_submittable": 1, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index aa9049801c..d83bd1dfd1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -537,7 +537,8 @@ "fieldname": "serial_no", "fieldtype": "Small Text", "label": "Serial Nos", - "no_copy": 1 + "no_copy": 1, + "read_only": 1 }, { "default": "0", diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py index ddbb7fd0f1..ed764f4ef3 100644 --- a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -61,7 +61,6 @@ def execute(): doc.load_items_from_bom() doc.calculate_rate_and_amount() set_expense_account(doc) - doc.make_batches("t_warehouse") if doc.docstatus == 0: doc.save() diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index e37a9b735b..2a81651440 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -346,7 +346,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac } } - update_serial_batch_bundle(doc, cdt, cdn) { + add_serial_batch_bundle(doc, cdt, cdn) { let item = locals[cdt][cdn]; let me = this; let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; @@ -356,6 +356,8 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { item.has_serial_no = r.message.has_serial_no; item.has_batch_no = r.message.has_batch_no; + item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward"; + item.is_rejected = false; frappe.require(path, function() { new erpnext.SerialNoBatchBundleUpdate( @@ -371,6 +373,34 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac } }); } + + add_serial_batch_for_rejected_qty(doc, cdt, cdn) { + let item = locals[cdt][cdn]; + let me = this; + let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; + + frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) + .then((r) => { + if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward"; + item.is_rejected = true; + + frappe.require(path, function() { + new erpnext.SerialNoBatchBundleUpdate( + me.frm, item, (r) => { + if (r) { + me.frm.refresh_fields(); + frappe.model.set_value(cdt, cdn, + "rejected_serial_and_batch_bundle", r.name); + } + } + ); + }); + } + }); + } }; cur_frm.add_fetch('project', 'cost_center', 'cost_center'); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 52abbc0a3d..e706ab9783 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -682,6 +682,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + on_submit() { + refresh_field("items"); + } + update_qty(cdt, cdn) { var valid_serial_nos = []; var serialnos = []; diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index fcaaaf0953..bdfc2f0a91 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -624,13 +624,16 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { this.item = item; this.qty = item.qty; this.callback = callback; + this.bundle = this.item?.is_rejected ? + this.item.rejected_serial_and_batch_bundle : this.item.serial_and_batch_bundle; + this.make(); this.render_data(); } make() { let label = this.item?.has_serial_no ? __('Serial No') : __('Batch No'); - let primary_label = this.item?.serial_and_batch_bundle + let primary_label = this.bundle ? __('Update') : __('Add'); if (this.item?.has_serial_no && this.item?.batch_no) { @@ -655,7 +658,7 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { get_serial_no_filters() { let warehouse = this.item?.outward ? - this.item.warehouse : ""; + (this.item.warehouse || this.item.s_warehouse) : ""; return { 'item_code': this.item.item_code, @@ -684,7 +687,6 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { if (this.item.has_batch_no && this.item.has_serial_no) { fields.push({ fieldtype: 'Column Break', - label: __('Batch No') }); } @@ -698,6 +700,22 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { }); } + if (this.frm.doc.doctype === 'Stock Entry' + && this.frm.doc.purpose === 'Manufacture') { + fields.push({ + fieldtype: 'Column Break', + }); + + fields.push({ + fieldtype: 'Link', + fieldname: 'work_order', + label: __('For Work Order'), + options: 'Work Order', + read_only: 1, + default: this.frm.doc.work_order, + }); + } + if (this.item?.outward) { fields = [...fields, ...this.get_filter_fields()]; } @@ -770,30 +788,36 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { }) } + let batch_fields = [] if (this.item.has_batch_no) { - fields = [ + batch_fields = [ { fieldtype: 'Link', options: 'Batch', fieldname: 'batch_no', label: __('Batch No'), in_list_view: 1, - }, - { + } + ] + + if (!this.item.has_serial_no) { + batch_fields.push({ fieldtype: 'Float', fieldname: 'qty', label: __('Quantity'), in_list_view: 1, - } - ] + }) + } } + fields = [...fields, ...batch_fields]; + fields.push({ fieldtype: 'Data', fieldname: 'name', label: __('Name'), hidden: 1, - }) + }); return fields; } @@ -815,13 +839,14 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_auto_data', args: { item_code: this.item.item_code, - warehouse: this.item.warehouse, + warehouse: this.item.warehouse || this.item.s_warehouse, has_serial_no: this.item.has_serial_no, has_batch_no: this.item.has_batch_no, qty: qty, based_on: based_on }, callback: (r) => { + debugger if (r.message) { this.dialog.fields_dict.ledgers.df.data = r.message; this.dialog.fields_dict.ledgers.grid.refresh(); @@ -854,7 +879,7 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { if (!this.frm.is_new()) { let ledgers = this.dialog.get_values().ledgers; - if (ledgers && !ledgers.length) { + if (ledgers && !ledgers.length || !ledgers) { frappe.throw(__('Please add atleast one Serial No / Batch No')); } @@ -862,9 +887,11 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers', args: { ledgers: ledgers, - child_row: this.item + child_row: this.item, + doc: this.frm.doc, } }).then(r => { + debugger this.callback && this.callback(r.message); this.dialog.hide(); }) @@ -872,12 +899,12 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { } render_data() { - if (!this.frm.is_new() && this.item.serial_and_batch_bundle) { + if (!this.frm.is_new() && this.bundle) { frappe.call({ method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers', args: { item_code: this.item.item_code, - name: this.item.serial_and_batch_bundle, + name: this.bundle, voucher_no: this.item.parent, } }).then(r => { diff --git a/erpnext/selling/doctype/installation_note_item/installation_note_item.json b/erpnext/selling/doctype/installation_note_item/installation_note_item.json index 79bcf105af..3e49fc92cf 100644 --- a/erpnext/selling/doctype/installation_note_item/installation_note_item.json +++ b/erpnext/selling/doctype/installation_note_item/installation_note_item.json @@ -1,260 +1,126 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-02-22 01:27:51", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:27:51", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "serial_and_batch_bundle", + "serial_no", + "qty", + "description", + "prevdoc_detail_docname", + "prevdoc_docname", + "prevdoc_doctype" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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, - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Item", - "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, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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": 1, - "in_standard_filter": 0, - "label": "Serial No", - "length": 0, - "no_copy": 0, - "oldfieldname": "serial_no", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "180px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No", + "no_copy": 1, + "oldfieldname": "serial_no", + "oldfieldtype": "Small Text", + "print_hide": 1, + "print_width": "180px", "width": "180px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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": "Installed Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "qty", - "oldfieldtype": "Currency", - "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, - "unique": 0 - }, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Installed Qty", + "oldfieldname": "qty", + "oldfieldtype": "Currency", + "reqd": 1 + }, { - "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": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "300px", - "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", + "in_global_search": 1, + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Data", + "print_width": "300px", + "read_only": 1, "width": "300px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "prevdoc_detail_docname", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Against Document Detail No", - "length": 0, - "no_copy": 1, - "oldfieldname": "prevdoc_detail_docname", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "prevdoc_detail_docname", + "fieldtype": "Data", + "hidden": 1, + "label": "Against Document Detail No", + "no_copy": 1, + "oldfieldname": "prevdoc_detail_docname", + "oldfieldtype": "Data", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "prevdoc_docname", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Against Document No", - "length": 0, - "no_copy": 1, - "oldfieldname": "prevdoc_docname", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "unique": 0, + "fieldname": "prevdoc_docname", + "fieldtype": "Data", + "hidden": 1, + "label": "Against Document No", + "no_copy": 1, + "oldfieldname": "prevdoc_docname", + "oldfieldtype": "Data", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "search_index": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "prevdoc_doctype", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Document Type", - "length": 0, - "no_copy": 1, - "oldfieldname": "prevdoc_doctype", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "unique": 0, + "fieldname": "prevdoc_doctype", + "fieldtype": "Data", + "hidden": 1, + "label": "Document Type", + "no_copy": 1, + "oldfieldname": "prevdoc_doctype", + "oldfieldtype": "Data", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "search_index": 1, "width": "150px" + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "menu_index": 0, - "modified": "2017-02-20 13:24:18.142419", - "modified_by": "Administrator", - "module": "Selling", - "name": "Installation Note Item", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2023-03-12 13:47:08.257955", + "modified_by": "Administrator", + "module": "Selling", + "name": "Installation Note Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index f5268d6e5e..4d17f4ed8f 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -430,7 +430,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { item.has_serial_no = r.message.has_serial_no; item.has_batch_no = r.message.has_batch_no; - item.outward = true; + item.type_of_transaction = item.qty > 0 ? "Outward":"Inward"; item.title = item.has_serial_no ? __("Select Serial No") : __("Select Batch No"); diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py new file mode 100644 index 0000000000..1dbe9159c9 --- /dev/null +++ b/erpnext/stock/deprecated_serial_batch.py @@ -0,0 +1,101 @@ +import frappe +from frappe.query_builder.functions import CombineDatetime, Sum +from frappe.utils import flt + + +class DeprecatedSerialNoValuation: + def calculate_stock_value_from_deprecarated_ledgers(self): + serial_nos = list( + filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos()) + ) + + actual_qty = flt(self.sle.actual_qty) + + stock_value_change = 0 + if actual_qty < 0: + # In case of delivery/stock issue, get average purchase rate + # of serial nos of current entry + if not self.sle.is_cancelled: + outgoing_value = self.get_incoming_value_for_serial_nos(serial_nos) + stock_value_change = -1 * outgoing_value + else: + stock_value_change = actual_qty * self.sle.outgoing_rate + + self.stock_value_change += stock_value_change + + def get_incoming_value_for_serial_nos(self, serial_nos): + # get rate from serial nos within same company + all_serial_nos = frappe.get_all( + "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)} + ) + + incoming_values = 0.0 + for d in all_serial_nos: + if d.company == self.sle.company: + self.serial_no_incoming_rate[d.name] = flt(d.purchase_rate) + incoming_values += flt(d.purchase_rate) + + # Get rate for serial nos which has been transferred to other company + invalid_serial_nos = [d.name for d in all_serial_nos if d.company != self.sle.company] + for serial_no in invalid_serial_nos: + incoming_rate = frappe.db.sql( + """ + select incoming_rate + from `tabStock Ledger Entry` + where + company = %s + and actual_qty > 0 + and is_cancelled = 0 + and (serial_no = %s + or serial_no like %s + or serial_no like %s + or serial_no like %s + ) + order by posting_date desc + limit 1 + """, + (self.sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"), + ) + + self.serial_no_incoming_rate[serial_no] = flt(incoming_rate[0][0]) if incoming_rate else 0 + incoming_values += self.serial_no_incoming_rate[serial_no] + + return incoming_values + + +class DeprecatedBatchNoValuation: + def calculate_avg_rate_from_deprecarated_ledgers(self): + ledgers = self.get_sle_for_batches() + for ledger in ledgers: + self.batch_avg_rate[ledger.batch_no] += flt(ledger.incoming_rate) / flt(ledger.qty) + + def get_sle_for_batches(self): + batch_nos = list(self.batch_nos.keys()) + sle = frappe.qb.DocType("Stock Ledger Entry") + + timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( + self.sle.posting_date, self.sle.posting_time + ) + if self.sle.creation: + timestamp_condition |= ( + CombineDatetime(sle.posting_date, sle.posting_time) + == CombineDatetime(self.sle.posting_date, self.sle.posting_time) + ) & (sle.creation < self.sle.creation) + + return ( + frappe.qb.from_(sle) + .select( + sle.batch_no, + Sum(sle.stock_value_difference).as_("batch_value"), + Sum(sle.actual_qty).as_("batch_qty"), + ) + .where( + (sle.item_code == self.sle.item_code) + & (sle.name != self.sle.name) + & (sle.warehouse == self.sle.warehouse) + & (sle.batch_no.isin(batch_nos)) + & (sle.is_cancelled == 0) + ) + .where(timestamp_condition) + .groupby(sle.batch_no) + ).run(as_dict=True) diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index 967c5729bf..e6cb3516a3 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -207,7 +207,7 @@ "image_field": "image", "links": [], "max_attachments": 5, - "modified": "2022-02-21 08:08:23.999236", + "modified": "2023-03-12 15:56:09.516586", "modified_by": "Administrator", "module": "Stock", "name": "Batch", diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 1843c6e797..35d862b571 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -264,7 +264,7 @@ def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): warehouse = d.get(warehouse_field, None) if warehouse and qty > 0 and frappe.db.get_value("Item", d.item_code, "has_batch_no"): if not d.batch_no: - d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no) + pass else: batch_qty = get_batch_qty(batch_no=d.batch_no, warehouse=warehouse) if flt(batch_qty, d.precision("qty")) < flt(qty, d.precision("qty")): @@ -365,7 +365,7 @@ def validate_serial_no_with_batch(serial_nos, item_code): def make_batch(args): if frappe.db.get_value("Item", args.item, "has_batch_no"): args.doctype = "Batch" - frappe.get_doc(args).insert().name + return frappe.get_doc(args).insert().name @frappe.whitelist() diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index c75d57f69e..ba0f28a13c 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -874,12 +874,14 @@ { "fieldname": "serial_no", "fieldtype": "Text", + "hidden": 1, "label": "Serial No", "read_only": 1 }, { "fieldname": "batch_no", "fieldtype": "Link", + "hidden": 1, "label": "Batch No", "options": "Batch", "read_only": 1 diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 244c905ca3..5dd8934d43 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -19,6 +19,7 @@ "rate", "uom", "section_break_9", + "pick_serial_and_batch", "serial_and_batch_bundle", "serial_no", "column_break_11", @@ -119,7 +120,8 @@ { "fieldname": "serial_no", "fieldtype": "Text", - "label": "Serial No" + "label": "Serial No", + "read_only": 1 }, { "fieldname": "column_break_11", @@ -129,7 +131,8 @@ "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch" + "options": "Batch", + "read_only": 1 }, { "fieldname": "section_break_13", @@ -259,7 +262,14 @@ "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", - "options": "Serial and Batch Bundle" + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 + }, + { + "fieldname": "pick_serial_and_batch", + "fieldtype": "Button", + "label": "Pick Serial / Batch No" } ], "idx": 1, diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index a6f8c0db45..e6653a804a 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -21,6 +21,8 @@ "conversion_factor", "stock_uom", "serial_no_and_batch_section", + "pick_serial_and_batch", + "serial_and_batch_bundle", "serial_no", "column_break_20", "batch_no", @@ -72,14 +74,16 @@ "depends_on": "serial_no", "fieldname": "serial_no", "fieldtype": "Small Text", - "label": "Serial No" + "label": "Serial No", + "read_only": 1 }, { "depends_on": "batch_no", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch" + "options": "Batch", + "read_only": 1 }, { "fieldname": "column_break_2", @@ -187,11 +191,24 @@ "hidden": 1, "label": "Product Bundle Item", "read_only": 1 + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 + }, + { + "fieldname": "pick_serial_and_batch", + "fieldtype": "Button", + "label": "Pick Serial / Batch No" } ], "istable": 1, "links": [], - "modified": "2022-04-22 05:27:38.497997", + "modified": "2023-03-12 13:50:22.258100", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", @@ -202,4 +219,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 660504d2bf..284d003cf9 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -118,9 +118,7 @@ class PurchaseReceipt(BuyingController): self.validate_posting_time() super(PurchaseReceipt, self).validate() - if self._action == "submit": - self.make_batches("warehouse") - else: + if self._action != "submit": self.set_status() self.po_required() diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index f7798936ab..e576ab789a 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -92,12 +92,15 @@ "delivery_note_item", "putaway_rule", "section_break_45", - "update_serial_batch_bundle", + "add_serial_batch_bundle", "serial_and_batch_bundle", - "rejected_serial_and_batch_bundle", "col_break5", + "add_serial_batch_for_rejected_qty", + "rejected_serial_and_batch_bundle", + "section_break_3vxt", "serial_no", "rejected_serial_no", + "column_break_tolu", "batch_no", "subcontract_bom_section", "include_exploded_items", @@ -997,12 +1000,8 @@ "fieldtype": "Link", "label": "Serial and Batch Bundle", "no_copy": 1, - "options": "Serial and Batch Bundle" - }, - { - "fieldname": "update_serial_batch_bundle", - "fieldtype": "Button", - "label": "Add Serial / Batch No" + "options": "Serial and Batch Bundle", + "print_hide": 1 }, { "depends_on": "eval:parent.is_old_subcontracting_flow", @@ -1033,13 +1032,32 @@ "fieldname": "rejected_serial_and_batch_bundle", "fieldtype": "Link", "label": "Rejected Serial and Batch Bundle", + "no_copy": 1, "options": "Serial and Batch Bundle" + }, + { + "fieldname": "add_serial_batch_for_rejected_qty", + "fieldtype": "Button", + "label": "Add Serial / Batch No (Rejected Qty)" + }, + { + "fieldname": "section_break_3vxt", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_tolu", + "fieldtype": "Column Break" + }, + { + "fieldname": "add_serial_batch_bundle", + "fieldtype": "Button", + "label": "Add Serial / Batch No" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-03-03 12:45:03.087766", + "modified": "2023-03-12 13:37:47.778021", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 4148946e34..7493c79c77 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -1,11 +1,13 @@ { "actions": [], + "autoname": "naming_series:", "creation": "2022-09-29 14:56:38.338267", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "item_details_tab", + "naming_series", "company", "warehouse", "type_of_transaction", @@ -25,15 +27,20 @@ "tab_break_12", "voucher_type", "voucher_no", + "voucher_detail_no", "column_break_aouy", + "posting_date", + "posting_time", + "section_break_wzou", "is_cancelled", + "is_rejected", "amended_from" ], "fields": [ { "fieldname": "item_details_tab", "fieldtype": "Tab Break", - "label": "Item Details" + "label": "Serial and Batch" }, { "fieldname": "company", @@ -94,13 +101,14 @@ "allow_bulk_edit": 1, "fieldname": "ledgers", "fieldtype": "Table", - "label": "Serial / Batch Ledgers", + "label": "Ledgers", "options": "Serial and Batch Ledger", "reqd": 1 }, { "fieldname": "voucher_type", "fieldtype": "Link", + "in_list_view": 1, "label": "Voucher Type", "options": "DocType", "reqd": 1 @@ -109,6 +117,7 @@ "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "label": "Voucher No", + "no_copy": 1, "options": "voucher_type" }, { @@ -116,6 +125,7 @@ "fieldname": "is_cancelled", "fieldtype": "Check", "label": "Is Cancelled", + "no_copy": 1, "read_only": 1 }, { @@ -133,6 +143,7 @@ "label": "Reference" }, { + "collapsible": 1, "fieldname": "quantity_and_rate_section", "fieldtype": "Section Break", "label": "Quantity and Rate" @@ -170,6 +181,8 @@ "depends_on": "company", "fieldname": "warehouse", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Warehouse", "options": "Warehouse", "reqd": 1 @@ -180,15 +193,55 @@ "label": "Type of Transaction", "options": "\nInward\nOutward", "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "SBB-.####" + }, + { + "default": "0", + "depends_on": "eval:doc.voucher_type == \"Purchase Receipt\"", + "fieldname": "is_rejected", + "fieldtype": "Check", + "label": "Is Rejected", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_wzou", + "fieldtype": "Section Break" + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "no_copy": 1 + }, + { + "default": "today", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1 + }, + { + "fieldname": "voucher_detail_no", + "fieldtype": "Data", + "label": "Voucher Detail No", + "no_copy": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-03-03 16:18:53.709069", + "modified": "2023-03-12 16:05:18.141958", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 0f8f6d2586..5e9b7061be 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import collections +from typing import Dict, List import frappe from frappe import _ @@ -10,26 +11,170 @@ from frappe.query_builder.functions import Sum from frappe.utils import cint, flt, today from pypika import Case +from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation + class SerialandBatchBundle(Document): def validate(self): self.validate_serial_and_batch_no() self.validate_duplicate_serial_and_batch_no() + self.validate_voucher_no() def before_save(self): - self.set_outgoing_rate() + self.set_total_qty() + self.set_is_outward() + self.set_warehouse() + self.set_incoming_rate() if self.ledgers: - self.set_total_qty() self.set_avg_rate() + def set_incoming_rate(self, row=None, save=False): + if self.type_of_transaction == "Outward": + self.set_incoming_rate_for_outward_transaction(row, save) + else: + self.set_incoming_rate_for_inward_transaction(row, save) + + def set_incoming_rate_for_outward_transaction(self, row=None, save=False): + sle = self.get_sle_for_outward_transaction(row) + if self.has_serial_no: + sn_obj = SerialNoBundleValuation( + sle=sle, + warehouse=self.item_code, + item_code=self.warehouse, + ) + + else: + sn_obj = BatchNoBundleValuation( + sle=sle, + warehouse=self.item_code, + item_code=self.warehouse, + ) + + for d in self.ledgers: + if self.has_serial_no: + d.incoming_rate = sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0) + else: + d.incoming_rate = sn_obj.batch_avg_rate.get(d.batch_no) + + if self.has_batch_no: + d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) * -1 + + if save: + d.db_set( + {"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference} + ) + + def get_sle_for_outward_transaction(self, row): + return frappe._dict( + { + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "item_code": self.item_code, + "warehouse": self.warehouse, + "serial_and_batch_bundle": self.name, + "actual_qty": self.total_qty * -1, + "company": self.company, + "serial_nos": [row.serial_no for row in self.ledgers if row.serial_no], + "batch_nos": {row.batch_no: row for row in self.ledgers if row.batch_no}, + } + ) + + def set_incoming_rate_for_inward_transaction(self, row=None, save=False): + rate = row.valuation_rate if row else 0.0 + precision = frappe.get_precision(self.child_table, "valuation_rate") or 2 + + if not rate and self.voucher_detail_no and self.voucher_no: + rate = frappe.db.get_value(self.child_table, self.voucher_detail_no, "valuation_rate") + + for d in self.ledgers: + if not rate or flt(rate, precision) == flt(d.incoming_rate, precision): + continue + + d.incoming_rate = flt(rate, precision) + if self.has_batch_no: + d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) + + if save: + d.db_set( + {"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference} + ) + + def set_serial_and_batch_values(self, parent, row): + values_to_set = {} + if not self.voucher_no or self.voucher_no != row.parent: + values_to_set["voucher_no"] = row.parent + + if not self.voucher_detail_no or self.voucher_detail_no != row.name: + values_to_set["voucher_detail_no"] = row.name + + if parent.get("posting_date") and ( + not self.posting_date or self.posting_date != parent.posting_date + ): + values_to_set["posting_date"] = parent.posting_date + + if parent.get("posting_time") and ( + not self.posting_time or self.posting_time != parent.posting_time + ): + values_to_set["posting_time"] = parent.posting_time + + if values_to_set: + self.db_set(values_to_set) + + self.validate_voucher_no() + self.validate_quantity(row) + self.set_incoming_rate(save=True, row=row) + + def validate_voucher_no(self): + if not (self.voucher_type and self.voucher_no): + return + + if not frappe.db.exists(self.voucher_type, self.voucher_no): + frappe.throw(_(f"The {self.voucher_type} # {self.voucher_no} does not exist")) + + bundles = frappe.get_all( + "Serial and Batch Bundle", + filters={ + "voucher_no": self.voucher_no, + "is_cancelled": 0, + "name": ["!=", self.name], + "item_code": self.item_code, + "warehouse": self.warehouse, + }, + ) + + if bundles: + frappe.throw( + _( + f"The {self.voucher_type} # {self.voucher_no} already has a Serial and Batch Bundle {bundles[0].name}" + ) + ) + + def validate_quantity(self, row): + self.set_total_qty(save=True) + + precision = row.precision + if abs(flt(self.total_qty, precision) - flt(row.qty, precision)) > 0.01: + frappe.throw( + _( + f"Total quantity {self.total_qty} in the Serial and Batch Bundle {self.name} does not match with the Item {row.item_code} in the {self.voucher_type} # {self.voucher_no}" + ) + ) + + def set_is_outward(self): + for row in self.ledgers: + row.is_outward = 1 if self.type_of_transaction == "Outward" else 0 + @frappe.whitelist() def set_warehouse(self): for row in self.ledgers: - row.warehouse = self.warehouse + if row.warehouse != self.warehouse: + row.warehouse = self.warehouse - def set_total_qty(self): + def set_total_qty(self, save=False): self.total_qty = sum([row.qty for row in self.ledgers]) + if save: + self.db_set("total_qty", self.total_qty) def set_avg_rate(self): self.total_amount = 0.0 @@ -41,32 +186,6 @@ class SerialandBatchBundle(Document): if self.total_qty: self.avg_rate = flt(self.total_amount) / flt(self.total_qty) - def set_outgoing_rate(self, update_rate=False): - if not self.calculate_outgoing_rate(): - return - - serial_nos = [row.serial_no for row in self.ledgers] - data = get_serial_and_batch_ledger( - item_code=self.item_code, - warehouse=self.ledgers[0].warehouse, - serial_nos=serial_nos, - fetch_incoming_rate=True, - ) - - if not data: - return - - serial_no_details = {row.serial_no: row for row in data} - - for ledger in self.ledgers: - if sn_details := serial_no_details.get(ledger.serial_no): - if ledger.outgoing_rate and ledger.outgoing_rate == sn_details.incoming_rate: - continue - - ledger.outgoing_rate = sn_details.incoming_rate or 0.0 - if update_rate: - ledger.db_set("outgoing_rate", ledger.outgoing_rate) - def calculate_outgoing_rate(self): if not (self.has_serial_no and self.ledgers): return @@ -96,7 +215,7 @@ class SerialandBatchBundle(Document): if row.serial_no: serial_nos.append(row.serial_no) - if row.batch_no: + if row.batch_no and not row.serial_no: batch_nos.append(row.batch_no) if serial_nos: @@ -124,19 +243,23 @@ class SerialandBatchBundle(Document): def clear_table(self): self.set("ledgers", []) - def delink_refernce_from_voucher(self): - child_table = f"{self.voucher_type} Item" + @property + def child_table(self): + table = f"{self.voucher_type} Item" if self.voucher_type == "Stock Entry": - child_table = f"{self.voucher_type} Detail" + table = f"{self.voucher_type} Detail" + return table + + def delink_refernce_from_voucher(self): vouchers = frappe.get_all( - child_table, + self.child_table, fields=["name"], filters={"serial_and_batch_bundle": self.name, "docstatus": 0}, ) for voucher in vouchers: - frappe.db.set_value(child_table, voucher.name, "serial_and_batch_bundle", None) + frappe.db.set_value(self.child_table, voucher.name, "serial_and_batch_bundle", None) def delink_reference_from_batch(self): batches = frappe.get_all( @@ -153,6 +276,12 @@ class SerialandBatchBundle(Document): self.delink_reference_from_batch() self.clear_table() + def on_update(self): + self.validate_negative_stock() + + def validate_negative_stock(self): + pass + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs @@ -191,29 +320,46 @@ def get_serial_batch_ledgers(item_code, voucher_no, name=None): @frappe.whitelist() -def add_serial_batch_ledgers(ledgers, child_row) -> object: +def add_serial_batch_ledgers(ledgers, child_row, doc) -> object: if isinstance(child_row, str): child_row = frappe._dict(frappe.parse_json(child_row)) if isinstance(ledgers, str): ledgers = frappe.parse_json(ledgers) + if doc and isinstance(doc, str): + d = frappe.parse_json(doc) + parent_doc = frappe.get_doc(d.doctype, d.name) + if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle): - doc = update_serial_batch_no_ledgers(ledgers, child_row) + doc = update_serial_batch_no_ledgers(ledgers, child_row, parent_doc) else: - doc = create_serial_batch_no_ledgers(ledgers, child_row) + doc = create_serial_batch_no_ledgers(ledgers, child_row, parent_doc) return doc -def create_serial_batch_no_ledgers(ledgers, child_row) -> object: +def create_serial_batch_no_ledgers(ledgers, child_row, parent_doc) -> object: + + warehouse = child_row.rejected_warhouse if child_row.is_rejected else child_row.warehouse + + type_of_transaction = child_row.type_of_transaction + if parent_doc.doctype == "Stock Entry": + type_of_transaction = "Outward" if child_row.s_warehouse else "Inward" + warehouse = child_row.s_warehouse or child_row.t_warehouse + doc = frappe.get_doc( { "doctype": "Serial and Batch Bundle", "voucher_type": child_row.parenttype, "voucher_no": child_row.parent, "item_code": child_row.item_code, + "warehouse": warehouse, "voucher_detail_no": child_row.name, + "is_rejected": child_row.is_rejected, + "type_of_transaction": type_of_transaction, + "posting_date": parent_doc.posting_date, + "posting_time": parent_doc.posting_time, } ) @@ -223,7 +369,7 @@ def create_serial_batch_no_ledgers(ledgers, child_row) -> object: "ledgers", { "qty": row.qty or 1.0, - "warehouse": child_row.warehouse, + "warehouse": warehouse, "batch_no": row.batch_no, "serial_no": row.serial_no, }, @@ -238,9 +384,11 @@ def create_serial_batch_no_ledgers(ledgers, child_row) -> object: return doc -def update_serial_batch_no_ledgers(ledgers, child_row) -> object: +def update_serial_batch_no_ledgers(ledgers, child_row, parent_doc) -> object: doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle) doc.voucher_detail_no = child_row.name + doc.posting_date = parent_doc.posting_date + doc.posting_time = parent_doc.posting_time doc.set("ledgers", []) doc.set("ledgers", ledgers) doc.save() @@ -266,6 +414,7 @@ def get_serial_and_batch_ledger(**kwargs): serial_batch_table.batch_no, serial_batch_table.qty, serial_batch_table.incoming_rate, + serial_batch_table.voucher_detail_no, ) .where( (sle_table.item_code == kwargs.item_code) @@ -286,20 +435,9 @@ def get_serial_and_batch_ledger(**kwargs): return query.run(as_dict=True) -def get_copy_of_serial_and_batch_bundle(serial_and_batch_bundle, warehouse): - bundle_doc = frappe.copy_doc(serial_and_batch_bundle) - for row in bundle_doc.ledgers: - row.warehouse = warehouse - row.incoming_rate = row.outgoing_rate - row.outgoing_rate = 0.0 - - return bundle_doc.submit(ignore_permissions=True) - - @frappe.whitelist() def get_auto_data(**kwargs): kwargs = frappe._dict(kwargs) - if cint(kwargs.has_serial_no): return get_auto_serial_nos(kwargs) @@ -393,3 +531,65 @@ def get_available_batches(kwargs): data = list(filter(lambda x: x.qty > 0, data)) return data + + +def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> Dict[str, Dict]: + data = get_ledgers_from_serial_batch_bundle(**kwargs) + + group_by_voucher = {} + + for row in data: + key = (row.item_code, row.warehouse, row.voucher_no) + if key not in group_by_voucher: + group_by_voucher.setdefault( + key, {"serial_nos": [], "batch_nos": collections.defaultdict(float)} + ) + + child_row = group_by_voucher[key] + if row.serial_no: + child_row["serial_nos"].append(row.serial_no) + + if row.batch_no: + child_row["batch_nos"][row.batch_no] += row.qty + + return group_by_voucher + + +def get_ledgers_from_serial_batch_bundle(**kwargs) -> List[frappe._dict]: + bundle_table = frappe.qb.DocType("Serial and Batch Bundle") + serial_batch_table = frappe.qb.DocType("Serial and Batch Ledger") + + query = ( + frappe.qb.from_(bundle_table) + .inner_join(serial_batch_table) + .on(bundle_table.name == serial_batch_table.parent) + .select( + serial_batch_table.serial_no, + bundle_table.warehouse, + bundle_table.item_code, + serial_batch_table.batch_no, + serial_batch_table.qty, + serial_batch_table.incoming_rate, + bundle_table.voucher_detail_no, + bundle_table.voucher_no, + bundle_table.posting_date, + bundle_table.posting_time, + ) + .where((bundle_table.docstatus == 1) & (bundle_table.is_cancelled == 0)) + ) + + for key, val in kwargs.items(): + if key in ["name", "item_code", "warehouse", "voucher_no", "company", "voucher_detail_no"]: + if isinstance(val, list): + query = query.where(bundle_table[key].isin(val)) + else: + query = query.where(bundle_table[key] == val) + elif key in ["posting_date", "posting_time"]: + query = query.where(bundle_table[key] >= val) + else: + if isinstance(val, list): + query = query.where(serial_batch_table[key].isin(val)) + else: + query = query.where(serial_batch_table[key] == val) + + return query.run(as_dict=True) diff --git a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json index d99322504f..7e83c70b5d 100644 --- a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json +++ b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json @@ -106,7 +106,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-03-03 16:52:26.039613", + "modified": "2023-03-10 12:02:49.560343", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Ledger", diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/__init__.py b/erpnext/stock/doctype/serial_and_batch_no_bundle/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.js b/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.js deleted file mode 100644 index c36abd652e..0000000000 --- a/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("Serial and Batch No Bundle", { -// refresh(frm) { - -// }, -// }); diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.json b/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.json deleted file mode 100644 index ec3315678c..0000000000 --- a/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.json +++ /dev/null @@ -1,176 +0,0 @@ -{ - "actions": [], - "creation": "2022-09-29 14:56:38.338267", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "item_details_tab", - "company", - "item_group", - "has_serial_no", - "column_break_4", - "item_code", - "item_name", - "has_batch_no", - "serial_no_and_batch_no_tab", - "ledgers", - "qty", - "reference_tab", - "voucher_type", - "voucher_no", - "posting_date", - "posting_time", - "is_cancelled", - "amended_from" - ], - "fields": [ - { - "fieldname": "item_details_tab", - "fieldtype": "Tab Break", - "label": "Item Details" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "options": "Company", - "reqd": 1 - }, - { - "fetch_from": "item_code.item_group", - "fieldname": "item_group", - "fieldtype": "Link", - "label": "Item Group", - "options": "Item Group" - }, - { - "default": "0", - "fetch_from": "item_code.has_serial_no", - "fieldname": "has_serial_no", - "fieldtype": "Check", - "label": "Has Serial No", - "read_only": 1 - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "item_code", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Item Code", - "options": "Item", - "reqd": 1 - }, - { - "fetch_from": "item_code.item_name", - "fieldname": "item_name", - "fieldtype": "Data", - "label": "Item Name" - }, - { - "default": "0", - "fetch_from": "item_code.has_batch_no", - "fieldname": "has_batch_no", - "fieldtype": "Check", - "label": "Has Batch No", - "read_only": 1 - }, - { - "fieldname": "serial_no_and_batch_no_tab", - "fieldtype": "Section Break" - }, - { - "allow_bulk_edit": 1, - "fieldname": "ledgers", - "fieldtype": "Table", - "label": "Serial No and Batch No Transaction", - "options": "Serial and Batch No Ledger", - "reqd": 1 - }, - { - "fieldname": "qty", - "fieldtype": "Float", - "label": "Total Qty", - "read_only": 1 - }, - { - "fieldname": "reference_tab", - "fieldtype": "Tab Break", - "label": "Reference" - }, - { - "fieldname": "voucher_type", - "fieldtype": "Link", - "label": "Voucher Type", - "options": "DocType", - "reqd": 1 - }, - { - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "label": "Voucher No", - "options": "voucher_type" - }, - { - "fieldname": "posting_date", - "fieldtype": "Date", - "label": "Posting Date", - "read_only": 1 - }, - { - "default": "0", - "fieldname": "is_cancelled", - "fieldtype": "Check", - "label": "Is Cancelled", - "read_only": 1 - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Serial and Batch No Bundle", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "posting_time", - "fieldtype": "Time", - "label": "Posting Time", - "read_only": 1 - } - ], - "index_web_pages_for_search": 1, - "is_submittable": 1, - "links": [], - "modified": "2023-03-05 17:38:51.871723", - "modified_by": "Administrator", - "module": "Stock", - "name": "Serial and Batch No Bundle", - "owner": "Administrator", - "permissions": [ - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "submit": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "title_field": "item_code" -} \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.py b/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.py deleted file mode 100644 index 46c0e5ae02..0000000000 --- a/erpnext/stock/doctype/serial_and_batch_no_bundle/serial_and_batch_no_bundle.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class SerialandBatchNoBundle(Document): - pass diff --git a/erpnext/stock/doctype/serial_and_batch_no_bundle/test_serial_and_batch_no_bundle.py b/erpnext/stock/doctype/serial_and_batch_no_bundle/test_serial_and_batch_no_bundle.py deleted file mode 100644 index 2d5b9d3d06..0000000000 --- a/erpnext/stock/doctype/serial_and_batch_no_bundle/test_serial_and_batch_no_bundle.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests.utils import FrappeTestCase - - -class TestSerialandBatchNoBundle(FrappeTestCase): - pass diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 7f22af16a1..1750439c4d 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -14,7 +14,9 @@ "item_code", "batch_no", "warehouse", + "purchase_rate", "column_break1", + "status", "item_name", "description", "item_group", @@ -35,9 +37,11 @@ "maintenance_status", "warranty_period", "more_info", - "serial_no_details", "company", - "work_order" + "column_break_2cmm", + "work_order", + "section_break_fgyk", + "serial_no_details" ], "fields": [ { @@ -227,6 +231,7 @@ "fieldname": "company", "fieldtype": "Link", "in_list_view": 1, + "in_standard_filter": 1, "label": "Company", "options": "Company", "remember_last_selected_value": 1, @@ -243,6 +248,7 @@ { "fieldname": "warehouse", "fieldtype": "Link", + "in_list_view": 1, "label": "Warehouse", "options": "Warehouse", "read_only": 1 @@ -251,13 +257,37 @@ "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch" + "options": "Batch", + "read_only": 1 + }, + { + "fieldname": "purchase_rate", + "fieldtype": "Float", + "label": "Incoming Rate", + "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "\nActive\nInactive\nExpired", + "read_only": 1 + }, + { + "fieldname": "column_break_2cmm", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_fgyk", + "fieldtype": "Section Break" } ], "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2023-04-15 15:58:46.139887", + "modified": "2023-04-16 15:58:46.139887", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 6d92cc3a76..4c5156c066 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -9,7 +9,7 @@ import frappe from frappe import ValidationError, _ from frappe.model.naming import make_autoname from frappe.query_builder.functions import Coalesce -from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads from erpnext.controllers.stock_controller import StockController from erpnext.stock.get_item_details import get_reserved_qty_for_so @@ -111,7 +111,6 @@ class SerialNo(StockController): def process_serial_no(sle): item_det = get_item_details(sle.item_code) validate_serial_no(sle, item_det) - create_serial_nos(sle, item_det) def validate_serial_no(sle, item_det): @@ -378,42 +377,6 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): return allow_serial_nos -def create_serial_nos(sle, item_det): - if sle.skip_update_serial_no: - return - if ( - not sle.is_cancelled - and not sle.serial_and_batch_bundle - and cint(sle.actual_qty) > 0 - and item_det.has_serial_no == 1 - and item_det.serial_no_series - ): - bundle = make_serial_no_bundle(sle, item_det) - if bundle: - sle.db_set("serial_and_batch_bundle", bundle.name) - child_doctype = sle.voucher_type + " Item" - if sle.voucher_type == "Stock Entry": - child_doctype = "Stock Entry Detail" - elif sle.voucher_type == "Stock Reconciliation": - child_doctype = "Stock Reconciliation Item" - - frappe.db.set_value( - child_doctype, sle.voucher_detail_no, "serial_and_batch_bundle", bundle.name - ) - - elif sle.serial_and_batch_bundle: - if sle.is_cancelled: - frappe.db.set_value( - "Serial and Batch Bundle", - sle.serial_and_batch_bundle, - "is_cancelled", - 1, - ) - - if item_det.has_serial_no: - update_warehouse_in_serial_no(sle, item_det) - - def update_warehouse_in_serial_no(sle, item_det): serial_nos = get_serial_nos(sle.serial_and_batch_bundle) serial_no_data = get_serial_nos_warehouse(sle.item_code, serial_nos) @@ -457,74 +420,6 @@ def get_serial_nos_warehouse(item_code, serial_nos): ).run(as_dict=True) -def make_serial_no_bundle(sle, item_details): - sr_nos = auto_create_serial_nos(sle, item_details) - if sr_nos: - return make_serial_batch_bundle(sle, item_details, sr_nos) - - -def make_serial_batch_bundle(sle, item_details, sr_nos): - sn_doc = frappe.new_doc("Serial and Batch Bundle") - sn_doc.item_code = item_details.name - sn_doc.item_name = item_details.item_name - sn_doc.item_group = item_details.item_group - sn_doc.has_serial_no = item_details.has_serial_no - sn_doc.has_batch_no = item_details.has_batch_no - sn_doc.voucher_type = sle.voucher_type - sn_doc.voucher_no = sle.voucher_no - sn_doc.flags.ignore_mandatory = True - sn_doc.flags.ignore_validate = True - sn_doc.total_qty = sle.actual_qty - sn_doc.avg_rate = sle.incoming_rate - sn_doc.total_amount = flt(sle.actual_qty) * flt(sle.incoming_rate) - sn_doc.insert() - - batch_no = "" - if item_details.has_batch_no: - batch_no = create_batch_for_serial_no(sle) - - add_serial_no_to_bundle(sn_doc, sle, sr_nos, batch_no, item_details) - - sn_doc.load_from_db() - sn_doc.flags.ignore_validate = True - return sn_doc.submit() - - -def add_serial_no_to_bundle(sn_doc, sle, sr_nos, batch_no, item_details): - ledgers = [] - - fields = [ - "name", - "serial_no", - "batch_no", - "warehouse", - "item_code", - "qty", - "incoming_rate", - "parent", - "parenttype", - "parentfield", - ] - - for serial_no in sr_nos: - ledgers.append( - ( - frappe.generate_hash("Serial and Batch Ledger", 10), - serial_no, - batch_no, - sle.warehouse, - item_details.item_code, - 1, - sle.incoming_rate, - sn_doc.name, - sn_doc.doctype, - "ledgers", - ) - ) - - frappe.db.bulk_insert("Serial and Batch Ledger", fields=fields, values=set(ledgers)) - - def create_batch_for_serial_no(sle): from erpnext.stock.doctype.batch.batch import make_batch @@ -622,14 +517,13 @@ def get_item_details(item_code): )[0] -def get_serial_nos(serial_and_batch_bundle): - serial_nos = frappe.get_all( - "Serial and Batch Ledger", - filters={"parent": serial_and_batch_bundle, "serial_no": ("is", "set")}, - fields=["serial_no"], - ) +def get_serial_nos(serial_no): + if isinstance(serial_no, list): + return serial_no - return [d.serial_no for d in serial_nos] + return [ + s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip() + ] def clean_serial_no_string(serial_no: str) -> str: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index fb1f77ad3b..6d652e4094 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -7,6 +7,8 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Stock Entry', { setup: function(frm) { + frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle']; + frm.set_indicator_formatter('item_code', function(doc) { if (!doc.s_warehouse) { return 'blue'; @@ -680,17 +682,17 @@ frappe.ui.form.on('Stock Entry', { }); frappe.ui.form.on('Stock Entry Detail', { - qty: function(frm, cdt, cdn) { + qty(frm, cdt, cdn) { frm.events.set_serial_no(frm, cdt, cdn, () => { frm.events.set_basic_rate(frm, cdt, cdn); }); }, - conversion_factor: function(frm, cdt, cdn) { + conversion_factor(frm, cdt, cdn) { frm.events.set_basic_rate(frm, cdt, cdn); }, - s_warehouse: function(frm, cdt, cdn) { + s_warehouse(frm, cdt, cdn) { frm.events.set_serial_no(frm, cdt, cdn, () => { frm.events.get_warehouse_details(frm, cdt, cdn); }); @@ -702,16 +704,16 @@ frappe.ui.form.on('Stock Entry Detail', { } }, - t_warehouse: function(frm, cdt, cdn) { + t_warehouse(frm, cdt, cdn) { frm.events.get_warehouse_details(frm, cdt, cdn); }, - basic_rate: function(frm, cdt, cdn) { + basic_rate(frm, cdt, cdn) { var item = locals[cdt][cdn]; frm.events.calculate_basic_amount(frm, item); }, - uom: function(doc, cdt, cdn) { + uom(doc, cdt, cdn) { var d = locals[cdt][cdn]; if(d.uom && d.item_code){ return frappe.call({ @@ -730,7 +732,7 @@ frappe.ui.form.on('Stock Entry Detail', { } }, - item_code: function(frm, cdt, cdn) { + item_code(frm, cdt, cdn) { var d = locals[cdt][cdn]; if(d.item_code) { var args = { @@ -777,18 +779,27 @@ frappe.ui.form.on('Stock Entry Detail', { }); } }, - expense_account: function(frm, cdt, cdn) { + + expense_account(frm, cdt, cdn) { erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "items", "expense_account"); }, - cost_center: function(frm, cdt, cdn) { + + cost_center(frm, cdt, cdn) { erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "items", "cost_center"); }, - sample_quantity: function(frm, cdt, cdn) { + + sample_quantity(frm, cdt, cdn) { validate_sample_quantity(frm, cdt, cdn); }, - batch_no: function(frm, cdt, cdn) { + + batch_no(frm, cdt, cdn) { validate_sample_quantity(frm, cdt, cdn); }, + + add_serial_batch_bundle(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + erpnext.stock.select_batch_and_serial_no(frm, child); + } }); var validate_sample_quantity = function(frm, cdt, cdn) { @@ -1093,35 +1104,28 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle }; erpnext.stock.select_batch_and_serial_no = (frm, item) => { - let get_warehouse_type_and_name = (item) => { - let value = ''; - if(frm.fields_dict.from_warehouse.disp_status === "Write") { - value = cstr(item.s_warehouse) || ''; - return { - type: 'Source Warehouse', - name: value - }; - } else { - value = cstr(item.t_warehouse) || ''; - return { - type: 'Target Warehouse', - name: value - }; - } - } + let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; - if(item && !item.has_serial_no && !item.has_batch_no) return; - if (frm.doc.purpose === 'Material Receipt') return; + frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) + .then((r) => { + if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + item.outward = item.s_warehouse ? 1 : 0; - frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() { - if (frm.batch_selector?.dialog?.display) return; - frm.batch_selector = new erpnext.SerialNoBatchSelector({ - frm: frm, - item: item, - warehouse_details: get_warehouse_type_and_name(item), + frappe.require(path, function() { + new erpnext.SerialNoBatchBundleUpdate( + frm, item, (r) => { + if (r) { + frm.refresh_fields(); + frappe.model.set_value(item.doctype, item.name, + "serial_and_batch_bundle", r.name); + } + } + ); + }); + } }); - }); - } function attach_bom_items(bom_no) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3263ed43ff..a6eb9bf454 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -29,13 +29,7 @@ from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.batch.batch import get_batch_no, get_batch_qty, set_batch_nos from erpnext.stock.doctype.item.item import get_item_defaults -from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( - get_copy_of_serial_and_batch_bundle, -) -from erpnext.stock.doctype.serial_no.serial_no import ( - get_serial_nos, - update_serial_nos_after_submit, -) +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( OpeningEntryAccountError, ) @@ -148,9 +142,7 @@ class StockEntry(StockController): if not self.from_bom: self.fg_completed_qty = 0.0 - if self._action == "submit": - self.make_batches("t_warehouse") - else: + if self._action != "submit": set_batch_nos(self, "s_warehouse") self.validate_serialized_batch() @@ -201,8 +193,6 @@ class StockEntry(StockController): def on_submit(self): self.update_stock_ledger() - - update_serial_nos_after_submit(self, "items") self.update_work_order() self.validate_subcontract_order() self.update_subcontract_order_supplied_items() @@ -411,15 +401,15 @@ class StockEntry(StockController): flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item) ) - if ( - self.purpose in ("Material Transfer", "Material Transfer for Manufacture") - and not item.serial_no - and item.item_code in serialized_items - ): - frappe.throw( - _("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), - frappe.MandatoryError, - ) + # if ( + # self.purpose in ("Material Transfer", "Material Transfer for Manufacture") + # and not item.serial_and_batch_bundle + # and item.item_code in serialized_items + # ): + # frappe.throw( + # _("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), + # frappe.MandatoryError, + # ) def validate_qty(self): manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"] @@ -749,6 +739,9 @@ class StockEntry(StockController): d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost) if not d.basic_rate and not d.allow_zero_valuation_rate: + if self.is_new(): + raise_error_if_no_rate = False + d.basic_rate = get_valuation_rate( d.item_code, d.t_warehouse, @@ -786,6 +779,7 @@ class StockEntry(StockController): if reset_outgoing_rate: args = self.get_args_for_incoming_rate(d) rate = get_incoming_rate(args, raise_error_if_no_rate) + print(rate, "set rate for outgoing items") if rate > 0: d.basic_rate = rate @@ -803,12 +797,11 @@ class StockEntry(StockController): "posting_date": self.posting_date, "posting_time": self.posting_time, "qty": item.s_warehouse and -1 * flt(item.transfer_qty) or flt(item.transfer_qty), - "serial_no": item.serial_no, - "batch_no": item.batch_no, "voucher_type": self.doctype, "voucher_no": self.name, "company": self.company, "allow_zero_valuation": item.allow_zero_valuation_rate, + "serial_and_batch_bundle": item.serial_and_batch_bundle, } ) @@ -1216,11 +1209,6 @@ class StockEntry(StockController): def get_sle_for_target_warehouse(self, sl_entries, finished_item_row): for d in self.get("items"): if cstr(d.t_warehouse): - if d.s_warehouse and d.serial_and_batch_bundle: - d.serial_and_batch_bundle = get_copy_of_serial_and_batch_bundle( - d.serial_and_batch_bundle, d.t_warehouse - ) - sle = self.get_sl_entries( d, { @@ -1232,8 +1220,33 @@ class StockEntry(StockController): if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name): sle.recalculate_rate = 1 + if d.serial_and_batch_bundle and self.docstatus == 1: + self.copy_serial_and_batch_bundle(sle, d) + sl_entries.append(sle) + def copy_serial_and_batch_bundle(self, sle, child): + allowed_types = [ + "Material Transfer", + "Send to Subcontractor", + "Material Transfer for Manufacture", + ] + + if self.purpose in allowed_types: + bundle_doc = frappe.get_doc("Serial and Batch Bundle", child.serial_and_batch_bundle) + + bundle_doc = frappe.copy_doc(bundle_doc) + bundle_doc.warehouse = child.t_warehouse + bundle_doc.type_of_transaction = "Inward" + + for row in bundle_doc.ledgers: + row.warehouse = child.t_warehouse + row.is_outward = 0 + + bundle_doc.flags.ignore_permissions = True + bundle_doc.submit() + sle.serial_and_batch_bundle = bundle_doc.name + def get_gl_entries(self, warehouse_account): gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) @@ -1888,21 +1901,34 @@ class StockEntry(StockController): qty = frappe.utils.ceil(qty) if row.batch_details: + row.batches_to_be_consume = defaultdict(float) batches = sorted(row.batch_details.items(), key=lambda x: x[0]) + qty_to_be_consumed = qty for batch_no, batch_qty in batches: - if qty <= 0 or batch_qty <= 0: + if qty_to_be_consumed <= 0 or batch_qty <= 0: continue - if batch_qty > qty: - batch_qty = qty + if batch_qty > qty_to_be_consumed: + batch_qty = qty_to_be_consumed - item.batch_no = batch_no - self.update_item_in_stock_entry_detail(row, item, batch_qty) + row.batches_to_be_consume[batch_no] += batch_qty + + if batch_no and row.serial_nos: + serial_nos = self.get_serial_nos_based_on_transferred_batch(batch_no, row.serial_nos) + serial_nos = serial_nos[0 : cint(batch_qty)] + + # remove consumed serial nos from list + for sn in serial_nos: + row.serial_nos.remove(sn) row.batch_details[batch_no] -= batch_qty - qty -= batch_qty - else: - self.update_item_in_stock_entry_detail(row, item, qty) + qty_to_be_consumed -= batch_qty + + elif row.serial_nos: + serial_nos = row.serial_nos[0 : cint(qty)] + row.serial_nos = serial_nos + + self.update_item_in_stock_entry_detail(row, item, qty) def update_item_in_stock_entry_detail(self, row, item, qty) -> None: if not qty: @@ -1913,7 +1939,7 @@ class StockEntry(StockController): "to_warehouse": "", "qty": qty, "item_name": item.item_name, - "batch_no": item.batch_no, + "serial_and_batch_bundle": create_serial_and_batch_bundle(row, item), "description": item.description, "stock_uom": item.stock_uom, "expense_account": item.expense_account, @@ -1924,24 +1950,14 @@ class StockEntry(StockController): if self.is_return: ste_item_details["to_warehouse"] = item.s_warehouse - if row.serial_nos: - serial_nos = row.serial_nos - if item.batch_no: - serial_nos = self.get_serial_nos_based_on_transferred_batch(item.batch_no, row.serial_nos) - - serial_nos = serial_nos[0 : cint(qty)] - ste_item_details["serial_no"] = "\n".join(serial_nos) - - # remove consumed serial nos from list - for sn in serial_nos: - row.serial_nos.remove(sn) - self.add_to_stock_entry_detail({item.item_code: ste_item_details}) @staticmethod def get_serial_nos_based_on_transferred_batch(batch_no, serial_nos) -> list: serial_nos = frappe.get_all( - "Serial No", filters={"batch_no": batch_no, "name": ("in", serial_nos)}, order_by="creation" + "Serial No", + filters={"batch_no": batch_no, "name": ("in", serial_nos), "warehouse": ("is", "not set")}, + order_by="creation", ) return [d.name for d in serial_nos] @@ -2085,6 +2101,7 @@ class StockEntry(StockController): "item_name", "serial_no", "batch_no", + "serial_and_batch_bundle", "allow_zero_valuation_rate", ]: if item_row.get(field): @@ -2738,9 +2755,17 @@ def get_available_materials(work_order) -> dict: if row.batch_no: item_data.batch_details[row.batch_no] += row.qty + if row.batch_nos: + for batch_no, qty in row.batch_nos.items(): + item_data.batch_details[batch_no] += qty + if row.serial_no: item_data.serial_nos.extend(get_serial_nos(row.serial_no)) item_data.serial_nos.sort() + + if row.serial_nos: + item_data.serial_nos.extend(get_serial_nos(row.serial_nos)) + item_data.serial_nos.sort() else: # Consume raw material qty in case of 'Manufacture' or 'Material Consumption for Manufacture' @@ -2748,18 +2773,30 @@ def get_available_materials(work_order) -> dict: if row.batch_no: item_data.batch_details[row.batch_no] -= row.qty + if row.batch_nos: + for batch_no, qty in row.batch_nos.items(): + item_data.batch_details[batch_no] -= qty + if row.serial_no: for serial_no in get_serial_nos(row.serial_no): item_data.serial_nos.remove(serial_no) + if row.serial_nos: + for serial_no in get_serial_nos(row.serial_nos): + item_data.serial_nos.remove(serial_no) + return available_materials def get_stock_entry_data(work_order): + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_voucher_wise_serial_batch_from_bundle, + ) + stock_entry = frappe.qb.DocType("Stock Entry") stock_entry_detail = frappe.qb.DocType("Stock Entry Detail") - return ( + data = ( frappe.qb.from_(stock_entry) .from_(stock_entry_detail) .select( @@ -2773,9 +2810,11 @@ def get_stock_entry_data(work_order): stock_entry_detail.stock_uom, stock_entry_detail.expense_account, stock_entry_detail.cost_center, + stock_entry_detail.serial_and_batch_bundle, stock_entry_detail.batch_no, stock_entry_detail.serial_no, stock_entry.purpose, + stock_entry.name, ) .where( (stock_entry.name == stock_entry_detail.parent) @@ -2790,3 +2829,72 @@ def get_stock_entry_data(work_order): ) .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) ).run(as_dict=1) + + if not data: + return [] + + voucher_nos = [row.get("name") for row in data if row.get("name")] + if voucher_nos: + bundle_data = get_voucher_wise_serial_batch_from_bundle(voucher_no=voucher_nos) + for row in data: + key = (row.item_code, row.warehouse, row.name) + if row.purpose != "Material Transfer for Manufacture": + key = (row.item_code, row.s_warehouse, row.name) + + if bundle_data.get(key): + row.update(bundle_data.get(key)) + + return data + + +def create_serial_and_batch_bundle(row, child): + doc = frappe.get_doc( + { + "doctype": "Serial and Batch Bundle", + "voucher_type": "Stock Entry", + "item_code": child.item_code, + "warehouse": child.warehouse, + "type_of_transaction": "Outward", + } + ) + + if row.serial_nos and row.batches_to_be_consume: + batchwise_serial_nos = get_batchwise_serial_nos(child.item_code, row) + for batch_no, qty in row.batches_to_be_consume.items(): + + while qty > 0: + qty -= 1 + doc.append( + "ledgers", + { + "batch_no": batch_no, + "serial_no": batchwise_serial_nos.get(batch_no).pop(0), + "warehouse": row.warehouse, + "qty": qty, + }, + ) + + elif row.serial_nos: + for serial_no in row.serial_nos: + doc.append("ledgers", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": 1}) + + elif row.batches_to_be_consume: + for batch_no, qty in row.batches_to_be_consume.items(): + doc.append("ledgers", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty}) + + return doc.insert(ignore_permissions=True).name + + +def get_batchwise_serial_nos(item_code, row): + batchwise_serial_nos = {} + + for batch_no in row.batches_to_be_consume: + serial_nos = frappe.get_all( + "Serial No", + filters={"item_code": item_code, "batch_no": batch_no, "name": ("in", row.serial_nos)}, + ) + + if serial_nos: + batchwise_serial_nos[batch_no] = sorted([serial_no.name for serial_no in serial_nos]) + + return batchwise_serial_nos diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 6b1a8efc99..0c08fb2ed3 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -46,8 +46,10 @@ "basic_amount", "amount", "serial_no_batch", - "serial_no", + "add_serial_batch_bundle", + "serial_and_batch_bundle", "col_break4", + "serial_no", "batch_no", "accounting", "expense_account", @@ -292,7 +294,8 @@ "label": "Serial No", "no_copy": 1, "oldfieldname": "serial_no", - "oldfieldtype": "Text" + "oldfieldtype": "Text", + "read_only": 1 }, { "fieldname": "col_break4", @@ -305,7 +308,8 @@ "no_copy": 1, "oldfieldname": "batch_no", "oldfieldtype": "Link", - "options": "Batch" + "options": "Batch", + "read_only": 1 }, { "depends_on": "eval:parent.inspection_required && doc.t_warehouse", @@ -566,6 +570,19 @@ "fieldtype": "Check", "label": "Has Item Scanned", "read_only": 1 + }, + { + "fieldname": "add_serial_batch_bundle", + "fieldtype": "Button", + "label": "Add Serial / Batch No" + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 } ], "idx": 1, 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 c95d821cf4..a902655952 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -12,6 +12,7 @@ from frappe.utils import add_days, cint, formatdate, get_datetime, get_link_to_f from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock +from erpnext.stock.serial_batch_bundle import SerialBatchBundle class StockFreezeError(frappe.ValidationError): @@ -47,16 +48,18 @@ class StockLedgerEntry(Document): self.validate_and_set_fiscal_year() self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() - self.process_serial_and_batch_bundle() def on_submit(self): self.check_stock_frozen_date() self.calculate_batch_qty() if not self.get("via_landed_cost_voucher"): - from erpnext.stock.doctype.serial_no.serial_no import process_serial_no - - process_serial_no(self) + SerialBatchBundle( + sle=self, + item_code=self.item_code, + warehouse=self.warehouse, + company=self.company, + ) self.validate_serial_batch_no_bundle() @@ -103,17 +106,12 @@ class StockLedgerEntry(Document): if item_detail.has_serial_no or item_detail.has_batch_no: if not self.serial_and_batch_bundle: - frappe.throw(_(f"Serial No and Batch No are mandatory for Item {self.item_code}")) + frappe.throw(_(f"Serial No / Batch No are mandatory for Item {self.item_code}")) else: bundle_data = frappe.get_cached_value( "Serial and Batch Bundle", self.serial_and_batch_bundle, ["item_code", "docstatus"], as_dict=1 ) - if self.item_code != bundle_data.item_code: - frappe.throw( - _(f"Serial and Batch Bundle {self.serial_and_batch_bundle} is not for Item {self.item_code}") - ) - if bundle_data.docstatus != 1: link = get_link_to_form("Serial and Batch Bundle", self.serial_and_batch_bundle) frappe.throw(_(f"Serial and Batch Bundle {link} should be submitted first")) @@ -121,9 +119,6 @@ class StockLedgerEntry(Document): if self.serial_and_batch_bundle and not (item_detail.has_serial_no or item_detail.has_batch_no): frappe.throw(_(f"Serial No and Batch No are not allowed for Item {self.item_code}")) - if self.stock_uom != item_detail.stock_uom: - self.stock_uom = item_detail.stock_uom - def check_stock_frozen_date(self): stock_settings = frappe.get_cached_doc("Stock Settings") @@ -217,36 +212,6 @@ class StockLedgerEntry(Document): msg += "
" + "
".join(authorized_users) frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry")) - def process_serial_and_batch_bundle(self): - if self.serial_and_batch_bundle: - self.update_warehouse_and_voucher_no() - self.set_outgoing_rate() - - def update_warehouse_and_voucher_no(self): - voucher_no = self.name if not self.is_cancelled else None - frappe.db.set_value( - "Serial and Batch Bundle", self.serial_and_batch_bundle, "voucher_no", voucher_no - ) - - if not self.is_cancelled: - frappe.db.sql( - f""" - UPDATE `tabSerial and Batch Ledger` - SET warehouse = {frappe.db.escape(self.warehouse)} - WHERE parent = {frappe.db.escape(self.serial_and_batch_bundle)} - AND ( - warehouse is NULL or warehouse = '' or - warehouse != {frappe.db.escape(self.warehouse)} - )""" - ) - - def set_outgoing_rate(self): - if self.is_cancelled: - return - - doc = frappe.get_cached_doc("Serial and Batch Bundle", self.serial_and_batch_bundle) - doc.set_outgoing_rate() - def on_cancel(self): msg = _("Individual Stock Ledger Entry cannot be cancelled.") msg += "
" + _("Please cancel related transaction.") diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 525a0b02c2..da53644439 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -48,7 +48,6 @@ class StockReconciliation(StockController): if self._action == "submit": self.validate_reserved_stock() - self.make_batches("warehouse") def on_submit(self): self.update_stock_ledger() 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 2f65eaa358..f3943ebf95 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -17,6 +17,7 @@ "amount", "allow_zero_valuation_rate", "serial_no_and_batch_section", + "serial_and_batch_bundle", "batch_no", "column_break_11", "serial_no", @@ -25,6 +26,7 @@ "current_amount", "column_break_9", "current_valuation_rate", + "current_serial_and_batch_bundle", "current_serial_no", "section_break_14", "quantity_difference", @@ -168,7 +170,8 @@ "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch" + "options": "Batch", + "read_only": 1 }, { "default": "0", @@ -185,6 +188,21 @@ "fieldtype": "Data", "label": "Has Item Scanned", "read_only": 1 + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 + }, + { + "fieldname": "current_serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Current Serial / Batch Bundle", + "options": "Serial and Batch Bundle", + "read_only": 1 } ], "istable": 1, diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index f32b79db67..1e28988817 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1,23 +1,37 @@ -import frappe -from frappe.model.naming import make_autoname -from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, cstr, flt, now +from collections import defaultdict +from typing import List +import frappe +from frappe import _, bold +from frappe.model.naming import make_autoname +from frappe.query_builder.functions import Sum +from frappe.utils import cint, flt, now +from pypika import Case + +from erpnext.stock.deprecated_serial_batch import ( + DeprecatedBatchNoValuation, + DeprecatedSerialNoValuation, +) from erpnext.stock.valuation import round_off_if_near_zero class SerialBatchBundle: def __init__(self, **kwargs): - for key, value in kwargs.iteritems(): + for key, value in kwargs.items(): setattr(self, key, value) self.set_item_details() + self.process_serial_and_batch_bundle() + if self.sle.is_cancelled: + self.delink_serial_and_batch_bundle() + + self.post_process() def process_serial_and_batch_bundle(self): if self.item_details.has_serial_no: - self.process_serial_no + self.process_serial_no() elif self.item_details.has_batch_no: - self.process_batch_no + self.process_batch_no() def set_item_details(self): fields = [ @@ -39,11 +53,13 @@ class SerialBatchBundle: and self.sle.actual_qty > 0 and self.item_details.has_serial_no == 1 and self.item_details.serial_no_series + and self.allow_to_make_auto_bundle() ): - sr_nos = self.auto_create_serial_nos() - self.make_serial_no_bundle(sr_nos) + self.make_serial_batch_no_bundle() + elif not self.sle.is_cancelled: + self.validate_item_and_warehouse() - def auto_create_serial_nos(self): + def auto_create_serial_nos(self, batch_no=None): sr_nos = [] serial_nos_details = [] @@ -63,6 +79,8 @@ class SerialBatchBundle: self.item_code, self.item_details.item_name, self.item_details.description, + "Active", + batch_no, ) ) @@ -79,36 +97,51 @@ class SerialBatchBundle: "item_code", "item_name", "description", + "status", + "batch_no", ] frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details)) return sr_nos - def make_serial_no_bundle(self, serial_nos=None): + def make_serial_batch_no_bundle(self): sn_doc = frappe.new_doc("Serial and Batch Bundle") sn_doc.item_code = self.item_code + sn_doc.warehouse = self.warehouse sn_doc.item_name = self.item_details.item_name sn_doc.item_group = self.item_details.item_group sn_doc.has_serial_no = self.item_details.has_serial_no sn_doc.has_batch_no = self.item_details.has_batch_no sn_doc.voucher_type = self.sle.voucher_type sn_doc.voucher_no = self.sle.voucher_no - sn_doc.flags.ignore_mandatory = True - sn_doc.flags.ignore_validate = True + sn_doc.voucher_detail_no = self.sle.voucher_detail_no sn_doc.total_qty = self.sle.actual_qty sn_doc.avg_rate = self.sle.incoming_rate sn_doc.total_amount = flt(self.sle.actual_qty) * flt(self.sle.incoming_rate) + sn_doc.type_of_transaction = "Inward" + sn_doc.posting_date = self.sle.posting_date + sn_doc.posting_time = self.sle.posting_time + sn_doc.is_rejected = self.is_rejected_entry() + + sn_doc.flags.ignore_mandatory = True sn_doc.insert() batch_no = "" if self.item_details.has_batch_no: batch_no = self.create_batch() - if serial_nos: - self.add_serial_no_to_bundle(sn_doc, serial_nos, batch_no) + incoming_rate = self.sle.incoming_rate + if not incoming_rate: + incoming_rate = frappe.get_cached_value( + self.child_doctype, self.sle.voucher_detail_no, "valuation_rate" + ) + + if self.item_details.has_serial_no: + sr_nos = self.auto_create_serial_nos(batch_no) + self.add_serial_no_to_bundle(sn_doc, sr_nos, incoming_rate, batch_no) elif self.item_details.has_batch_no: - self.add_batch_no_to_bundle(sn_doc, batch_no) + self.add_batch_no_to_bundle(sn_doc, batch_no, incoming_rate) sn_doc.save() sn_doc.load_from_db() @@ -116,10 +149,32 @@ class SerialBatchBundle: sn_doc.flags.ignore_mandatory = True sn_doc.submit() + self.set_serial_and_batch_bundle(sn_doc) - self.sle.serial_and_batch_bundle = sn_doc.name + def set_serial_and_batch_bundle(self, sn_doc): + self.sle.db_set("serial_and_batch_bundle", sn_doc.name) - def add_serial_no_to_bundle(self, sn_doc, serial_nos, batch_no=None): + if sn_doc.is_rejected: + frappe.db.set_value( + self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name + ) + else: + frappe.db.set_value( + self.child_doctype, self.sle.voucher_detail_no, "serial_and_batch_bundle", sn_doc.name + ) + + @property + def child_doctype(self): + child_doctype = self.sle.voucher_type + " Item" + if self.sle.voucher_type == "Stock Entry": + child_doctype = "Stock Entry Detail" + + return child_doctype + + def is_rejected_entry(self): + return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse) + + def add_serial_no_to_bundle(self, sn_doc, serial_nos, incoming_rate, batch_no=None): ledgers = [] fields = [ @@ -144,7 +199,7 @@ class SerialBatchBundle: self.warehouse, self.item_details.item_code, 1, - self.sle.incoming_rate, + incoming_rate, sn_doc.name, sn_doc.doctype, "ledgers", @@ -153,13 +208,14 @@ class SerialBatchBundle: frappe.db.bulk_insert("Serial and Batch Ledger", fields=fields, values=set(ledgers)) - def add_batch_no_to_bundle(self, sn_doc, batch_no): + def add_batch_no_to_bundle(self, sn_doc, batch_no, incoming_rate): sn_doc.append( "ledgers", { "batch_no": batch_no, "qty": self.sle.actual_qty, - "incoming_rate": self.sle.incoming_rate, + "incoming_rate": incoming_rate, + "stock_value_difference": flt(self.sle.actual_qty) * flt(incoming_rate), }, ) @@ -184,46 +240,182 @@ class SerialBatchBundle: and self.item_details.has_batch_no == 1 and self.item_details.create_new_batch and self.item_details.batch_number_series + and self.allow_to_make_auto_bundle() ): - self.make_serial_no_bundle() + self.make_serial_batch_no_bundle() + elif not self.sle.is_cancelled: + self.validate_item_and_warehouse() + + def validate_item_and_warehouse(self): + + data = frappe.db.get_value( + "Serial and Batch Bundle", + self.sle.serial_and_batch_bundle, + ["item_code", "warehouse", "voucher_no"], + as_dict=1, + ) + + if self.sle.serial_and_batch_bundle and not frappe.db.exists( + "Serial and Batch Bundle", + { + "name": self.sle.serial_and_batch_bundle, + "item_code": self.item_code, + "warehouse": self.warehouse, + "voucher_no": self.sle.voucher_no, + }, + ): + msg = f""" + The Serial and Batch Bundle + {bold(self.sle.serial_and_batch_bundle)} + does not belong to Item {bold(self.item_code)} + or Warehouse {bold(self.warehouse)} + or {self.sle.voucher_type} no {bold(self.sle.voucher_no)} + """ + + frappe.throw(_(msg)) + + def delink_serial_and_batch_bundle(self): + update_values = { + "serial_and_batch_bundle": "", + } + + if is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse): + update_values["rejected_serial_and_batch_bundle"] = "" + + frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values) + + frappe.db.set_value( + "Serial and Batch Bundle", + self.sle.serial_and_batch_bundle, + {"is_cancelled": 1, "voucher_no": ""}, + ) + + def allow_to_make_auto_bundle(self): + if self.sle.voucher_type in ["Stock Entry", "Purchase Receipt", "Purchase Invoice"]: + if self.sle.voucher_type == "Stock Entry": + stock_entry_type = frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose") + + if stock_entry_type in ["Material Receipt", "Manufacture", "Repack"]: + return True + + return True + + return False + + def post_process(self): + if not self.sle.is_cancelled: + if self.item_details.has_serial_no == 1: + self.set_warehouse_and_status_in_serial_nos() + + if self.item_details.has_serial_no == 1 and self.item_details.has_batch_no == 1: + self.set_batch_no_in_serial_nos() + else: + pass + # self.set_data_based_on_last_sle() + + def set_warehouse_and_status_in_serial_nos(self): + warehouse = self.warehouse if self.sle.actual_qty > 0 else None + + sn_table = frappe.qb.DocType("Serial No") + serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle, check_outward=False) + + ( + frappe.qb.update(sn_table) + .set(sn_table.warehouse, warehouse) + .set(sn_table.status, "Active" if warehouse else "Inactive") + .where(sn_table.name.isin(serial_nos)) + ).run() + + def set_batch_no_in_serial_nos(self): + ledgers = frappe.get_all( + "Serial and Batch Ledger", + fields=["serial_no", "batch_no"], + filters={"parent": self.serial_and_batch_bundle}, + ) + + batch_serial_nos = {} + for ledger in ledgers: + batch_serial_nos.setdefault(ledger.batch_no, []).append(ledger.serial_no) + + for batch_no, serial_nos in batch_serial_nos.items(): + sn_table = frappe.qb.DocType("Serial No") + ( + frappe.qb.update(sn_table) + .set(sn_table.batch_no, batch_no) + .where(sn_table.name.isin(serial_nos)) + ).run() -class RepostSerialBatchBundle: +def get_serial_nos(serial_and_batch_bundle, check_outward=True): + filters = {"parent": serial_and_batch_bundle} + if check_outward: + filters["is_outward"] = 1 + + ledgers = frappe.get_all("Serial and Batch Ledger", fields=["serial_no"], filters=filters) + + return [d.serial_no for d in ledgers] + + +class SerialNoBundleValuation(DeprecatedSerialNoValuation): def __init__(self, **kwargs): - for key, value in kwargs.iteritems(): + for key, value in kwargs.items(): setattr(self, key, value) - def get_valuation_rate(self): + self.calculate_stock_value_change() + self.calculate_valuation_rate() + + def calculate_stock_value_change(self): if self.sle.actual_qty > 0: - self.sle.incoming_rate = self.sle.valuation_rate + self.stock_value_change = frappe.get_cached_value( + "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount" + ) - if self.sle.actual_qty < 0: - self.sle.outgoing_rate = self.sle.valuation_rate + else: + ledgers = self.get_serial_no_ledgers() - def get_valuation_rate_for_serial_nos(self): + self.serial_no_incoming_rate = defaultdict(float) + self.stock_value_change = 0.0 + + for ledger in ledgers: + self.stock_value_change += ledger.incoming_rate * -1 + self.serial_no_incoming_rate[ledger.serial_no] = ledger.incoming_rate + + self.calculate_stock_value_from_deprecarated_ledgers() + + def get_serial_no_ledgers(self): serial_nos = self.get_serial_nos() subquery = f""" SELECT - MAX(ledger.posting_date), name + MAX( + TIMESTAMP( + parent.posting_date, parent.posting_time + ) + ), child.name FROM - ledger + `tabSerial and Batch Bundle` as parent, + `tabSerial and Batch Ledger` as child WHERE - ledger.serial_no IN {tuple(serial_nos)} - AND ledger.is_outward = 0 - AND ledger.warehouse = {frappe.db.escape(self.sle.warehouse)} - AND ledger.item_code = {frappe.db.escape(self.sle.item_code)} + parent.name = child.parent + AND child.serial_no IN ({', '.join([frappe.db.escape(s) for s in serial_nos])}) + AND child.is_outward = 0 + AND parent.docstatus < 2 + AND parent.is_cancelled = 0 + AND child.warehouse = {frappe.db.escape(self.sle.warehouse)} + AND parent.item_code = {frappe.db.escape(self.sle.item_code)} AND ( - ledger.posting_date < '{self.sle.posting_date}' + parent.posting_date < '{self.sle.posting_date}' OR ( - ledger.posting_date = '{self.sle.posting_date}' - AND ledger.posting_time <= '{self.sle.posting_time}' + parent.posting_date = '{self.sle.posting_date}' + AND parent.posting_time <= '{self.sle.posting_time}' ) ) + GROUP BY + child.serial_no """ - frappe.db.sql( - """ + return frappe.db.sql( + f""" SELECT serial_no, incoming_rate FROM @@ -233,153 +425,148 @@ class RepostSerialBatchBundle: ledger.name = SubQuery.name GROUP BY ledger.serial_no - """ + """, + as_dict=1, ) def get_serial_nos(self): - ledgers = frappe.get_all( - "Serial and Batch Ledger", - fields=["serial_no"], - filters={"parent": self.sle.serial_and_batch_bundle, "is_outward": 1}, - ) + if self.sle.get("serial_nos"): + return self.sle.serial_nos - return [d.serial_no for d in ledgers] + return get_serial_nos(self.sle.serial_and_batch_bundle) + def calculate_valuation_rate(self): + if not hasattr(self, "wh_data"): + return -class DeprecatedRepostSerialBatchBundle(RepostSerialBatchBundle): - def get_serialized_values(self, sle): - incoming_rate = flt(sle.incoming_rate) - actual_qty = flt(sle.actual_qty) - serial_nos = cstr(sle.serial_no).split("\n") - - if incoming_rate < 0: - # wrong incoming rate - incoming_rate = self.wh_data.valuation_rate - - stock_value_change = 0 - if actual_qty > 0: - stock_value_change = actual_qty * incoming_rate - else: - # In case of delivery/stock issue, get average purchase rate - # of serial nos of current entry - if not sle.is_cancelled: - outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos) - stock_value_change = -1 * outgoing_value - else: - stock_value_change = actual_qty * sle.outgoing_rate - - new_stock_qty = self.wh_data.qty_after_transaction + actual_qty + new_stock_qty = self.wh_data.qty_after_transaction + self.sle.actual_qty if new_stock_qty > 0: new_stock_value = ( self.wh_data.qty_after_transaction * self.wh_data.valuation_rate - ) + stock_value_change + ) + self.stock_value_change if new_stock_value >= 0: # calculate new valuation rate only if stock value is positive # else it remains the same as that of previous entry self.wh_data.valuation_rate = new_stock_value / new_stock_qty - if not self.wh_data.valuation_rate and sle.voucher_detail_no: - allow_zero_rate = self.check_if_allow_zero_valuation_rate( - sle.voucher_type, sle.voucher_detail_no + if ( + not self.wh_data.valuation_rate and self.sle.voucher_detail_no and not self.is_rejected_entry() + ): + allow_zero_rate = self.sle_self.check_if_allow_zero_valuation_rate( + self.sle.voucher_type, self.sle.voucher_detail_no ) if not allow_zero_rate: - self.wh_data.valuation_rate = self.get_fallback_rate(sle) + self.wh_data.valuation_rate = self.sle_self.get_fallback_rate(self.sle) - def get_incoming_value_for_serial_nos(self, sle, serial_nos): - # get rate from serial nos within same company - all_serial_nos = frappe.get_all( - "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)} + self.wh_data.qty_after_transaction += self.sle.actual_qty + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate ) - incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company == sle.company) + def is_rejected_entry(self): + return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse) - # Get rate for serial nos which has been transferred to other company - invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company] - for serial_no in invalid_serial_nos: - incoming_rate = frappe.db.sql( - """ - select incoming_rate - from `tabStock Ledger Entry` - where - company = %s - and actual_qty > 0 - and is_cancelled = 0 - and (serial_no = %s - or serial_no like %s - or serial_no like %s - or serial_no like %s - ) - order by posting_date desc - limit 1 - """, - (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"), + def get_incoming_rate(self): + return flt(self.stock_value_change) / flt(self.sle.actual_qty) + + +def is_rejected(voucher_type, voucher_detail_no, warehouse): + if voucher_type in ["Purchase Receipt", "Purchase Invoice"]: + return warehouse == frappe.get_cached_value( + voucher_type + " Item", voucher_detail_no, "rejected_warehouse" + ) + + return False + + +class BatchNoBundleValuation(DeprecatedBatchNoValuation): + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + self.batch_nos = self.get_batch_nos() + self.calculate_avg_rate() + self.calculate_valuation_rate() + + def calculate_avg_rate(self): + if self.sle.actual_qty > 0: + self.stock_value_change = frappe.get_cached_value( + "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount" ) - - incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0 - - return incoming_values - - def update_batched_values(self, sle): - incoming_rate = flt(sle.incoming_rate) - actual_qty = flt(sle.actual_qty) - - self.wh_data.qty_after_transaction = round_off_if_near_zero( - self.wh_data.qty_after_transaction + actual_qty - ) - - if actual_qty > 0: - stock_value_difference = incoming_rate * actual_qty else: - outgoing_rate = get_batch_incoming_rate( - item_code=sle.item_code, - warehouse=sle.warehouse, - batch_no=sle.batch_no, - posting_date=sle.posting_date, - posting_time=sle.posting_time, - creation=sle.creation, + ledgers = self.get_batch_no_ledgers() + + self.batch_avg_rate = defaultdict(float) + for ledger in ledgers: + self.batch_avg_rate[ledger.batch_no] += flt(ledger.incoming_rate) / flt(ledger.qty) + + self.calculate_avg_rate_from_deprecarated_ledgers() + self.set_stock_value_difference() + + def get_batch_no_ledgers(self) -> List[dict]: + parent = frappe.qb.DocType("Serial and Batch Bundle") + child = frappe.qb.DocType("Serial and Batch Ledger") + + batch_nos = list(self.batch_nos.keys()) + + return ( + frappe.qb.from_(parent) + .inner_join(child) + .on(parent.name == child.parent) + .select( + child.batch_no, + Sum(child.stock_value_difference).as_("incoming_rate"), + Sum(Case().when(child.is_outward == 1, child.qty * -1).else_(child.qty)).as_("qty"), ) - if outgoing_rate is None: - # This can *only* happen if qty available for the batch is zero. - # in such case fall back various other rates. - # future entries will correct the overall accounting as each - # batch individually uses moving average rates. - outgoing_rate = self.get_fallback_rate(sle) - stock_value_difference = outgoing_rate * actual_qty + .where( + (child.batch_no.isin(batch_nos)) + & (child.parent != self.sle.serial_and_batch_bundle) + & (parent.warehouse == self.sle.warehouse) + & (parent.item_code == self.sle.item_code) + & (parent.is_cancelled == 0) + ) + .groupby(child.batch_no) + ).run(as_dict=True) + + def get_batch_nos(self) -> list: + if self.sle.get("batch_nos"): + return self.sle.batch_nos + + ledgers = frappe.get_all( + "Serial and Batch Ledger", + fields=["batch_no", "qty", "name"], + filters={"parent": self.sle.serial_and_batch_bundle, "is_outward": 1}, + ) + + return {d.batch_no: d for d in ledgers} + + def set_stock_value_difference(self): + self.stock_value_change = 0 + for batch_no, ledger in self.batch_nos.items(): + stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty * -1 + self.stock_value_change += stock_value_change + frappe.db.set_value( + "Serial and Batch Ledger", ledger.name, "stock_value_difference", stock_value_change + ) + + def calculate_valuation_rate(self): + if not hasattr(self, "wh_data"): + return self.wh_data.stock_value = round_off_if_near_zero( - self.wh_data.stock_value + stock_value_difference + self.wh_data.stock_value + self.stock_value_change ) + if self.wh_data.qty_after_transaction: self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction + self.wh_data.qty_after_transaction += self.sle.actual_qty -def get_batch_incoming_rate( - item_code, warehouse, batch_no, posting_date, posting_time, creation=None -): + def get_incoming_rate(self): + return flt(self.stock_value_change) / flt(self.sle.actual_qty) - sle = frappe.qb.DocType("Stock Ledger Entry") - timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( - posting_date, posting_time - ) - if creation: - timestamp_condition |= ( - CombineDatetime(sle.posting_date, sle.posting_time) - == CombineDatetime(posting_date, posting_time) - ) & (sle.creation < creation) - - batch_details = ( - frappe.qb.from_(sle) - .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty")) - .where( - (sle.item_code == item_code) - & (sle.warehouse == warehouse) - & (sle.batch_no == batch_no) - & (sle.is_cancelled == 0) - ) - .where(timestamp_condition) - ).run(as_dict=True) - - if batch_details and batch_details[0].batch_qty: - return batch_details[0].batch_value / batch_details[0].batch_qty +class GetAvailableSerialBatchBundle: + def __init__(self) -> None: + pass diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e70e7f11aa..416355a47f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -27,6 +27,7 @@ from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock, ) +from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, get_or_make_bin, @@ -69,9 +70,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc if sle.serial_no and not via_landed_cost_voucher: validate_serial_no(sle) - if not cancel and sle["actual_qty"] > 0 and sle.get("serial_and_batch_bundle"): - set_incoming_rate_for_serial_and_batch(sle) - if cancel: sle["actual_qty"] = -flt(sle.get("actual_qty")) @@ -107,18 +105,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc ) -def set_incoming_rate_for_serial_and_batch(row): - frappe.db.sql( - """ - UPDATE `tabSerial and Batch Ledger` - SET incoming_rate = %s - WHERE - parent = %s - """, - (row.get("incoming_rate"), row.get("serial_and_batch_bundle")), - ) - - def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False): if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": if not args.get("posting_date"): @@ -705,17 +691,23 @@ class update_entries_after(object): ): sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle) - if sle.serial_and_batch_bundle and sle.has_serial_no: - self.get_serialized_values(sle) - self.wh_data.qty_after_transaction += flt(sle.actual_qty) - if sle.voucher_type == "Stock Reconciliation": - self.wh_data.qty_after_transaction = sle.qty_after_transaction - - self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( - self.wh_data.valuation_rate - ) - elif sle.serial_and_batch_bundle and sle.has_batch_no: - self.update_batched_values(sle) + if sle.serial_and_batch_bundle: + if frappe.get_cached_value("Item", sle.item_code, "has_serial_no"): + SerialNoBundleValuation( + sle=sle, + sle_self=self, + wh_data=self.wh_data, + warehouse=sle.warehouse, + item_code=sle.item_code, + ) + else: + BatchNoBundleValuation( + sle=sle, + sle_self=self, + wh_data=self.wh_data, + warehouse=sle.warehouse, + item_code=sle.item_code, + ) else: if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: # assert @@ -973,58 +965,6 @@ class update_entries_after(object): for item in sr.items: item.db_update() - def get_serialized_values(self, sle): - ledger = frappe.db.get_value( - "Serial and Batch Bundle", - sle.serial_and_batch_bundle, - ["avg_rate", "total_amount", "total_qty"], - as_dict=True, - ) - - if flt(abs(ledger.total_qty)) - flt(abs(sle.actual_qty)) > 0.001: - msg = f"""Actual Qty in Serial and Batch Bundle - {sle.serial_and_batch_bundle} does not match with - Stock Ledger Entry {sle.name}""" - - frappe.throw(_(msg)) - - actual_qty = flt(sle.actual_qty) - incoming_rate = flt(ledger.avg_rate) - - if incoming_rate < 0: - # wrong incoming rate - incoming_rate = self.wh_data.valuation_rate - - stock_value_change = 0 - if actual_qty > 0: - stock_value_change = actual_qty * incoming_rate - else: - # In case of delivery/stock issue, get average purchase rate - # of serial nos of current entry - outgoing_value = flt(ledger.total_amount) - if not sle.is_cancelled: - stock_value_change = -1 * outgoing_value - else: - stock_value_change = outgoing_value - - new_stock_qty = self.wh_data.qty_after_transaction + actual_qty - - if new_stock_qty > 0: - new_stock_value = ( - self.wh_data.qty_after_transaction * self.wh_data.valuation_rate - ) + stock_value_change - if new_stock_value >= 0: - # calculate new valuation rate only if stock value is positive - # else it remains the same as that of previous entry - self.wh_data.valuation_rate = new_stock_value / new_stock_qty - - if not self.wh_data.valuation_rate and sle.voucher_detail_no: - allow_zero_rate = self.check_if_allow_zero_valuation_rate( - sle.voucher_type, sle.voucher_detail_no - ) - if not allow_zero_rate: - self.wh_data.valuation_rate = self.get_fallback_rate(sle) - def get_incoming_value_for_serial_nos(self, sle, serial_nos): # get rate from serial nos within same company all_serial_nos = frappe.get_all( @@ -1468,9 +1408,6 @@ def get_batch_incoming_rate( .where(timestamp_condition) ).run(as_dict=True) - print(batch_details) - - print(batch_details[0].batch_value / batch_details[0].batch_qty) if batch_details and batch_details[0].batch_qty: return batch_details[0].batch_value / batch_details[0].batch_qty diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index ba36983150..c8fffdfee1 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -12,6 +12,7 @@ from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime import erpnext from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses +from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation from erpnext.stock.valuation import FIFOValuation, LIFOValuation BarcodeScanResult = Dict[str, Optional[str]] @@ -247,28 +248,37 @@ def _create_bin(item_code, warehouse): @frappe.whitelist() def get_incoming_rate(args, raise_error_if_no_rate=True): """Get Incoming Rate based on valuation method""" - from erpnext.stock.stock_ledger import ( - get_batch_incoming_rate, - get_previous_sle, - get_valuation_rate, - ) + from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate if isinstance(args, str): args = json.loads(args) in_rate = None - if (args.get("serial_no") or "").strip(): - in_rate = get_avg_purchase_rate(args.get("serial_no")) - elif args.get("batch_no") and frappe.db.get_value( - "Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True - ): - in_rate = get_batch_incoming_rate( - item_code=args.get("item_code"), + + item_details = frappe.get_cached_value( + "Item", args.get("item_code"), ["has_serial_no", "has_batch_no"], as_dict=1 + ) + + if item_details.has_serial_no and args.get("serial_and_batch_bundle"): + args["actual_qty"] = args["qty"] + sn_obj = SerialNoBundleValuation( + sle=args, warehouse=args.get("warehouse"), - batch_no=args.get("batch_no"), - posting_date=args.get("posting_date"), - posting_time=args.get("posting_time"), + item_code=args.get("item_code"), ) + + in_rate = sn_obj.get_incoming_rate() + + elif item_details.has_batch_no and args.get("serial_and_batch_bundle"): + args["actual_qty"] = args["qty"] + batch_obj = BatchNoBundleValuation( + sle=args, + warehouse=args.get("warehouse"), + item_code=args.get("item_code"), + ) + + in_rate = batch_obj.get_incoming_rate() + else: valuation_method = get_valuation_method(args.get("item_code")) previous_sle = get_previous_sle(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 416f4f80a2..4e500a6a16 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -81,9 +81,6 @@ class SubcontractingReceipt(SubcontractingController): self.validate_posting_time() self.validate_rejected_warehouse() - if self._action == "submit": - self.make_batches("warehouse") - if getdate(self.posting_date) > getdate(nowdate()): frappe.throw(_("Posting Date cannot be future date")) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 4b64e4bafe..d550b75839 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -46,8 +46,10 @@ "subcontracting_receipt_item", "section_break_45", "bom", + "serial_and_batch_bundle", "serial_no", "col_break5", + "rejected_serial_and_batch_bundle", "batch_no", "rejected_serial_no", "manufacture_details", @@ -298,19 +300,19 @@ "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "serial_no", "fieldtype": "Small Text", - "in_list_view": 1, "label": "Serial No", - "no_copy": 1 + "no_copy": 1, + "read_only": 1 }, { "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "batch_no", "fieldtype": "Link", - "in_list_view": 1, "label": "Batch No", "no_copy": 1, "options": "Batch", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "depends_on": "eval: !parent.is_return", @@ -471,12 +473,28 @@ "fieldname": "recalculate_rate", "fieldtype": "Check", "label": "Recalculate Rate" + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 + }, + { + "fieldname": "rejected_serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Rejected Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-11-16 14:21:26.125815", + "modified": "2023-03-12 14:00:41.418681", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index d21bc22ad7..78e94c0afe 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -25,6 +25,7 @@ "consumed_qty", "current_stock", "secbreak_3", + "serial_and_batch_bundle", "batch_no", "col_break4", "serial_no", @@ -61,13 +62,15 @@ "fieldtype": "Link", "label": "Batch No", "no_copy": 1, - "options": "Batch" + "options": "Batch", + "read_only": 1 }, { "fieldname": "serial_no", "fieldtype": "Text", "label": "Serial No", - "no_copy": 1 + "no_copy": 1, + "read_only": 1 }, { "fieldname": "col_break1", @@ -189,12 +192,21 @@ "label": "Available Qty For Consumption", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-11-07 17:17:21.670761", + "modified": "2023-03-12 14:11:48.816699", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item",