diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 342b8e98c1..74ba2b846a 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -714,8 +714,11 @@ 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: + def set_serial_and_batch_bundle(self, table_name=None): + if not table_name: + table_name = "items" + + for row in self.get(table_name): if row.serial_and_batch_bundle: frappe.get_doc( "Serial and Batch Bundle", row.serial_and_batch_bundle diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 1e9c4dc847..f7bc5d5494 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -11,6 +11,9 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import cint, cstr, flt, get_link_to_form from erpnext.controllers.stock_controller import StockController +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_voucher_wise_serial_batch_from_bundle, +) from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_incoming_rate @@ -48,6 +51,7 @@ class SubcontractingController(StockController): if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]: self.validate_items() self.create_raw_materials_supplied() + self.set_serial_and_batch_bundle("supplied_items") else: super(SubcontractingController, self).validate() @@ -169,7 +173,11 @@ class SubcontractingController(StockController): self.qty_to_be_received[(row.item_code, row.parent)] += row.qty def __get_transferred_items(self): - fields = [f"`tabStock Entry`.`{self.subcontract_data.order_field}`"] + fields = [ + f"`tabStock Entry`.`{self.subcontract_data.order_field}`", + "`tabStock Entry`.`name` as voucher_no", + ] + alias_dict = { "item_code": "rm_item_code", "subcontracted_item": "main_item_code", @@ -234,9 +242,11 @@ class SubcontractingController(StockController): "serial_no", "rm_item_code", "reference_name", + "serial_and_batch_bundle", "batch_no", "consumed_qty", "main_item_code", + "parent as voucher_no", ], filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype}, ) @@ -253,6 +263,13 @@ class SubcontractingController(StockController): } consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys()) + voucher_nos = [d.voucher_no for d in consumed_materials if d.voucher_no] + voucher_bundle_data = get_voucher_wise_serial_batch_from_bundle( + voucher_no=voucher_nos, + is_outward=1, + get_subcontracted_item=("Subcontracting Receipt Supplied Item", "main_item_code"), + ) + if return_consumed_items: return (consumed_materials, receipt_items) @@ -262,11 +279,26 @@ class SubcontractingController(StockController): continue self.available_materials[key]["qty"] -= row.consumed_qty + + bundle_key = (row.rm_item_code, row.main_item_code, self.supplier_warehouse, row.voucher_no) + consumed_bundles = voucher_bundle_data.get(bundle_key, frappe._dict()) + + if consumed_bundles.serial_nos: + self.available_materials[key]["serial_no"] = list( + set(self.available_materials[key]["serial_no"]) - set(consumed_bundles.serial_nos) + ) + + if consumed_bundles.batch_nos: + for batch_no, qty in consumed_bundles.batch_nos.items(): + self.available_materials[key]["batch_no"][batch_no] -= abs(qty) + + # Will be deperecated in v16 if row.serial_no: self.available_materials[key]["serial_no"] = list( set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no)) ) + # Will be deperecated in v16 if row.batch_no: self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty @@ -281,7 +313,16 @@ class SubcontractingController(StockController): if not self.subcontract_orders: return - for row in self.__get_transferred_items(): + transferred_items = self.__get_transferred_items() + + voucher_nos = [row.voucher_no for row in transferred_items] + voucher_bundle_data = get_voucher_wise_serial_batch_from_bundle( + voucher_no=voucher_nos, + is_outward=0, + get_subcontracted_item=("Stock Entry Detail", "subcontracted_item"), + ) + + for row in transferred_items: key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field)) if key not in self.available_materials: @@ -310,6 +351,17 @@ class SubcontractingController(StockController): if row.batch_no: details.batch_no[row.batch_no] += row.qty + if voucher_bundle_data: + bundle_key = (row.rm_item_code, row.main_item_code, row.t_warehouse, row.voucher_no) + + bundle_data = voucher_bundle_data.get(bundle_key, frappe._dict()) + if bundle_data.serial_nos: + details.serial_no.extend(bundle_data.serial_nos) + + if bundle_data.batch_nos: + for batch_no, qty in bundle_data.batch_nos.items(): + details.batch_no[batch_no] += qty + self.__set_alternative_item_details(row) self.__transferred_items = copy.deepcopy(self.available_materials) @@ -327,6 +379,7 @@ class SubcontractingController(StockController): self.set(self.raw_material_table, []) for item in self._doc_before_save.supplied_items: if item.reference_name in self.__changed_name: + self.__remove_serial_and_batch_bundle(item) continue if item.reference_name not in self.__reference_name: @@ -337,6 +390,10 @@ class SubcontractingController(StockController): i += 1 + def __remove_serial_and_batch_bundle(self, item): + if item.serial_and_batch_bundle: + frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True) + def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] @@ -403,42 +460,88 @@ class SubcontractingController(StockController): rm_obj.required_qty = required_qty rm_obj.consumed_qty = consumed_qty - def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): + def __set_serial_and_batch_bundle(self, item_row, rm_obj, qty): key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field)) + if not self.available_materials.get(key): + return - if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: - new_rm_obj = None - for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): - if batch_qty >= qty or ( - rm_obj.consumed_qty == 0 - and self.backflush_based_on == "BOM" - and len(self.available_materials[key]["batch_no"]) == 1 - ): - if rm_obj.consumed_qty == 0: - self.__set_consumed_qty(rm_obj, qty) + if ( + not self.available_materials[key]["serial_no"] and not self.available_materials[key]["batch_no"] + ): + return - self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) - self.available_materials[key]["batch_no"][batch_no] -= qty - return + bundle = frappe.get_doc( + { + "doctype": "Serial and Batch Bundle", + "company": self.company, + "item_code": rm_obj.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_type": "Subcontracting Receipt", + "voucher_no": self.name, + "type_of_transaction": "Outward", + } + ) - elif qty > 0 and batch_qty > 0: - qty -= batch_qty - new_rm_obj = self.append(self.raw_material_table, bom_item) - new_rm_obj.reference_name = item_row.name - self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) - self.available_materials[key]["batch_no"][batch_no] = 0 + if self.available_materials.get(key) and self.available_materials[key]["serial_no"]: + self.__set_serial_nos_for_bundle(bundle, qty, key) - if abs(qty) > 0 and not new_rm_obj: - self.__set_consumed_qty(rm_obj, qty) - else: - self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) - self.__set_serial_nos(item_row, rm_obj) + elif self.available_materials.get(key) and self.available_materials[key]["batch_no"]: + self.__set_batch_nos_for_bundle(bundle, qty, key) + + bundle.flags.ignore_links = True + bundle.flags.ignore_mandatory = True + bundle.save(ignore_permissions=True) + return bundle.name + + def __set_batch_nos_for_bundle(self, bundle, qty, key): + bundle.has_batch_no = 1 + for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): + qty_to_consumed = 0 + if qty > 0: + if batch_qty >= qty: + qty_to_consumed = qty + else: + qty_to_consumed = batch_qty + + qty -= qty_to_consumed + if qty_to_consumed > 0: + bundle.append("ledgers", {"batch_no": batch_no, "qty": qty_to_consumed * -1}) + + def __set_serial_nos_for_bundle(self, bundle, qty, key): + bundle.has_serial_no = 1 + + used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(qty)] + + # Removed the used serial nos from the list + for sn in used_serial_nos: + batch_no = "" + if self.available_materials[key]["batch_no"]: + bundle.has_batch_no = 1 + batch_no = frappe.get_cached_value("Serial No", sn, "batch_no") + if batch_no: + self.available_materials[key]["batch_no"][batch_no] -= 1 + + bundle.append("ledgers", {"serial_no": sn, "batch_no": batch_no, "qty": -1}) + + self.available_materials[key]["serial_no"].remove(sn) def __add_supplied_item(self, item_row, bom_item, qty): bom_item.conversion_factor = item_row.conversion_factor rm_obj = self.append(self.raw_material_table, bom_item) rm_obj.reference_name = item_row.name + if self.doctype == self.subcontract_data.order_doctype: + rm_obj.required_qty = qty + rm_obj.amount = rm_obj.required_qty * rm_obj.rate + else: + rm_obj.consumed_qty = qty + rm_obj.required_qty = bom_item.required_qty or qty + setattr( + rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field) + ) + if self.doctype == "Subcontracting Receipt": args = frappe._dict( { @@ -447,25 +550,21 @@ class SubcontractingController(StockController): "posting_date": self.posting_date, "posting_time": self.posting_time, "qty": -1 * flt(rm_obj.consumed_qty), - "serial_no": rm_obj.serial_no, - "batch_no": rm_obj.batch_no, + "actual_qty": -1 * flt(rm_obj.consumed_qty), "voucher_type": self.doctype, "voucher_no": self.name, + "voucher_detail_no": item_row.name, "company": self.company, "allow_zero_valuation": 1, } ) - rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args) - if self.doctype == self.subcontract_data.order_doctype: - rm_obj.required_qty = qty - rm_obj.amount = rm_obj.required_qty * rm_obj.rate - else: - rm_obj.consumed_qty = 0 - setattr( - rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field) - ) - self.__set_batch_nos(bom_item, item_row, rm_obj, qty) + rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(item_row, rm_obj, qty) + + if rm_obj.serial_and_batch_bundle: + args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle + + rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args) def __get_qty_based_on_material_transfer(self, item_row, transfer_item): key = (item_row.item_code, item_row.get(self.subcontract_data.order_field)) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index bdfc2f0a91..73c3868efa 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -891,10 +891,11 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { doc: this.frm.doc, } }).then(r => { - debugger this.callback && this.callback(r.message); this.dialog.hide(); }) + } else { + frappe.msgprint(__('Please save the document first')); } } diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 1dbe9159c9..33b89553b8 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -4,6 +4,8 @@ from frappe.utils import flt class DeprecatedSerialNoValuation: + # Will be deperecated in v16 + 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()) 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 5e9b7061be..382e6a9f3d 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 @@ -19,12 +19,14 @@ class SerialandBatchBundle(Document): self.validate_serial_and_batch_no() self.validate_duplicate_serial_and_batch_no() self.validate_voucher_no() + self.validate_serial_nos() def before_save(self): self.set_total_qty() self.set_is_outward() self.set_warehouse() self.set_incoming_rate() + self.validate_qty_and_stock_value_difference() if self.ledgers: self.set_avg_rate() @@ -35,6 +37,17 @@ class SerialandBatchBundle(Document): else: self.set_incoming_rate_for_inward_transaction(row, save) + def validate_qty_and_stock_value_difference(self): + if self.type_of_transaction != "Outward": + return + + for d in self.ledgers: + if d.qty and d.qty > 0: + d.qty *= -1 + + if d.stock_value_difference and d.stock_value_difference > 0: + d.stock_value_difference *= -1 + 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: @@ -53,12 +66,12 @@ class SerialandBatchBundle(Document): 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) + d.incoming_rate = abs(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) + d.incoming_rate = abs(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 + d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) if save: d.db_set( @@ -73,7 +86,7 @@ class SerialandBatchBundle(Document): "item_code": self.item_code, "warehouse": self.warehouse, "serial_and_batch_bundle": self.name, - "actual_qty": self.total_qty * -1, + "actual_qty": self.total_qty, "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}, @@ -126,6 +139,9 @@ class SerialandBatchBundle(Document): self.set_incoming_rate(save=True, row=row) def validate_voucher_no(self): + if self.is_new(): + return + if not (self.voucher_type and self.voucher_no): return @@ -150,14 +166,22 @@ class SerialandBatchBundle(Document): ) ) + def validate_serial_nos(self): + if not self.has_serial_no: + return + 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: + qty_field = "qty" + if self.voucher_type in ["Subcontracting Receipt"]: + qty_field = "consumed_qty" + + if abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), 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}" + f"Total quantity {self.total_qty} in the Serial and Batch Bundle {self.name} does not match with the Item {self.item_code} in the {self.voucher_type} # {self.voucher_no}" ) ) @@ -368,7 +392,7 @@ def create_serial_batch_no_ledgers(ledgers, child_row, parent_doc) -> object: doc.append( "ledgers", { - "qty": row.qty or 1.0, + "qty": (row.qty or 1.0) * (1 if type_of_transaction == "Inward" else -1), "warehouse": warehouse, "batch_no": row.batch_no, "serial_no": row.serial_no, @@ -535,14 +559,24 @@ def get_available_batches(kwargs): def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> Dict[str, Dict]: data = get_ledgers_from_serial_batch_bundle(**kwargs) + if not data: + return {} group_by_voucher = {} for row in data: key = (row.item_code, row.warehouse, row.voucher_no) + if kwargs.get("get_subcontracted_item"): + # get_subcontracted_item = ("doctype", "field_name") + doctype, field_name = kwargs.get("get_subcontracted_item") + + subcontracted_item_code = frappe.get_cached_value(doctype, row.voucher_detail_no, field_name) + key = (row.item_code, subcontracted_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)} + key, + frappe._dict({"serial_nos": [], "batch_nos": collections.defaultdict(float), "item_row": row}), ) child_row = group_by_voucher[key] @@ -579,6 +613,9 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> List[frappe._dict]: ) for key, val in kwargs.items(): + if key in ["get_subcontracted_item"]: + continue + 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)) @@ -593,3 +630,56 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> List[frappe._dict]: query = query.where(serial_batch_table[key] == val) return query.run(as_dict=True) + + +def get_available_serial_nos(item_code, warehouse): + filters = { + "item_code": item_code, + "warehouse": ("is", "set"), + } + + fields = ["name", "warehouse", "batch_no"] + + if warehouse: + filters["warehouse"] = warehouse + + return frappe.get_all("Serial No", filters=filters, fields=fields) + + +def get_available_batch_nos(item_code, warehouse): + sl_entries = get_stock_ledger_entries(item_code, warehouse) + batchwise_qty = collections.defaultdict(float) + + precision = frappe.get_precision("Stock Ledger Entry", "qty") + for entry in sl_entries: + batchwise_qty[entry.batch_no] += flt(entry.qty, precision) + + +def get_stock_ledger_entries(item_code, warehouse): + stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") + batch_ledger = frappe.qb.DocType("Serial and Batch Ledger") + + return ( + frappe.qb.from_(stock_ledger_entry) + .left_join(batch_ledger) + .on(stock_ledger_entry.serial_and_batch_bundle == batch_ledger.parent) + .select( + stock_ledger_entry.warehouse, + stock_ledger_entry.item_code, + Sum( + Case() + .when(stock_ledger_entry.serial_and_batch_bundle, batch_ledger.qty) + .else_(stock_ledger_entry.actual_qty) + .as_("qty") + ), + Case() + .when(stock_ledger_entry.serial_and_batch_bundle, batch_ledger.batch_no) + .else_(stock_ledger_entry.batch_no) + .as_("batch_no"), + ) + .where( + (stock_ledger_entry.item_code == item_code) + & (stock_ledger_entry.warehouse == warehouse) + & (stock_ledger_entry.is_cancelled == 0) + ) + ).run(as_dict=True) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6d652e4094..e4e8e170d6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1120,6 +1120,8 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { frm.refresh_fields(); frappe.model.set_value(item.doctype, item.name, "serial_and_batch_bundle", r.name); + + frm.save(); } } ); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a6eb9bf454..0691d63946 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -779,7 +779,6 @@ 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 @@ -1223,6 +1222,14 @@ class StockEntry(StockController): if d.serial_and_batch_bundle and self.docstatus == 1: self.copy_serial_and_batch_bundle(sle, d) + if d.serial_and_batch_bundle and self.docstatus == 2: + bundle_id = frappe.get_cached_value( + "Serial and Batch Bundle", {"voucher_detail_no": d.name, "is_cancelled": 0}, "name" + ) + + if d.serial_and_batch_bundle != bundle_id: + sle.serial_and_batch_bundle = bundle_id + sl_entries.append(sle) def copy_serial_and_batch_bundle(self, sle, child): @@ -1240,9 +1247,17 @@ class StockEntry(StockController): bundle_doc.type_of_transaction = "Inward" for row in bundle_doc.ledgers: + if row.qty < 0: + row.qty = abs(row.qty) + + if row.stock_value_difference < 0: + row.stock_value_difference = abs(row.stock_value_difference) + row.warehouse = child.t_warehouse row.is_outward = 0 + bundle_doc.set_total_qty() + bundle_doc.set_avg_rate() bundle_doc.flags.ignore_permissions = True bundle_doc.submit() sle.serial_and_batch_bundle = bundle_doc.name @@ -2859,6 +2874,8 @@ def create_serial_and_batch_bundle(row, child): ) if row.serial_nos and row.batches_to_be_consume: + doc.has_serial_no = 1 + doc.has_batch_no = 1 batchwise_serial_nos = get_batchwise_serial_nos(child.item_code, row) for batch_no, qty in row.batches_to_be_consume.items(): @@ -2870,17 +2887,19 @@ def create_serial_and_batch_bundle(row, child): "batch_no": batch_no, "serial_no": batchwise_serial_nos.get(batch_no).pop(0), "warehouse": row.warehouse, - "qty": qty, + "qty": -1, }, ) elif row.serial_nos: + doc.has_serial_no = 1 for serial_no in row.serial_nos: - doc.append("ledgers", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": 1}) + doc.append("ledgers", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1}) elif row.batches_to_be_consume: + doc.has_batch_no = 1 for batch_no, qty in row.batches_to_be_consume.items(): - doc.append("ledgers", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty}) + doc.append("ledgers", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1}) return doc.insert(ignore_permissions=True).name 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 f3943ebf95..8e148f7dfc 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -20,13 +20,13 @@ "serial_and_batch_bundle", "batch_no", "column_break_11", + "current_serial_and_batch_bundle", "serial_no", "section_break_3", "current_qty", "current_amount", "column_break_9", "current_valuation_rate", - "current_serial_and_batch_bundle", "current_serial_no", "section_break_14", "quantity_difference", @@ -192,7 +192,7 @@ { "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", - "label": "Serial and Batch Bundle", + "label": "Serial / Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 1e28988817..7c4f062038 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -6,7 +6,6 @@ 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, @@ -209,13 +208,18 @@ 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, incoming_rate): + stock_value_difference = flt(self.sle.actual_qty) * flt(incoming_rate) + + if self.sle.actual_qty < 0: + stock_value_difference *= -1 + sn_doc.append( "ledgers", { "batch_no": batch_no, "qty": self.sle.actual_qty, "incoming_rate": incoming_rate, - "stock_value_difference": flt(self.sle.actual_qty) * flt(incoming_rate), + "stock_value_difference": stock_value_difference, }, ) @@ -286,7 +290,7 @@ class SerialBatchBundle: frappe.db.set_value( "Serial and Batch Bundle", - self.sle.serial_and_batch_bundle, + {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type}, {"is_cancelled": 1, "voucher_no": ""}, ) @@ -303,22 +307,24 @@ class SerialBatchBundle: 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: + 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() + if ( + self.sle.actual_qty > 0 + and self.item_details.has_serial_no == 1 + and self.item_details.has_batch_no == 1 + ): + self.set_batch_no_in_serial_nos() def set_warehouse_and_status_in_serial_nos(self): + serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle, check_outward=False) 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) + if not serial_nos: + return + sn_table = frappe.qb.DocType("Serial No") ( frappe.qb.update(sn_table) .set(sn_table.warehouse, warehouse) @@ -330,7 +336,7 @@ class SerialBatchBundle: ledgers = frappe.get_all( "Serial and Batch Ledger", fields=["serial_no", "batch_no"], - filters={"parent": self.serial_and_batch_bundle}, + filters={"parent": self.sle.serial_and_batch_bundle}, ) batch_serial_nos = {} @@ -391,7 +397,7 @@ class SerialNoBundleValuation(DeprecatedSerialNoValuation): TIMESTAMP( parent.posting_date, parent.posting_time ) - ), child.name + ), child.name, child.serial_no, child.warehouse FROM `tabSerial and Batch Bundle` as parent, `tabSerial and Batch Ledger` as child @@ -417,14 +423,18 @@ class SerialNoBundleValuation(DeprecatedSerialNoValuation): return frappe.db.sql( f""" SELECT - serial_no, incoming_rate + ledger.serial_no, ledger.incoming_rate, ledger.warehouse FROM `tabSerial and Batch Ledger` AS ledger, ({subquery}) AS SubQuery WHERE ledger.name = SubQuery.name + AND ledger.serial_no = SubQuery.serial_no + AND ledger.warehouse = SubQuery.warehouse GROUP BY ledger.serial_no + Order By + ledger.creation """, as_dict=1, ) @@ -468,7 +478,7 @@ class SerialNoBundleValuation(DeprecatedSerialNoValuation): return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse) def get_incoming_rate(self): - return flt(self.stock_value_change) / flt(self.sle.actual_qty) + return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty)) def is_rejected(voucher_type, voucher_detail_no, warehouse): @@ -517,7 +527,7 @@ class BatchNoBundleValuation(DeprecatedBatchNoValuation): .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"), + Sum(child.qty).as_("qty"), ) .where( (child.batch_no.isin(batch_nos)) @@ -544,7 +554,7 @@ class BatchNoBundleValuation(DeprecatedBatchNoValuation): 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 + stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty self.stock_value_change += stock_value_change frappe.db.set_value( "Serial and Batch Ledger", ledger.name, "stock_value_difference", stock_value_change @@ -564,9 +574,4 @@ class BatchNoBundleValuation(DeprecatedBatchNoValuation): self.wh_data.qty_after_transaction += self.sle.actual_qty def get_incoming_rate(self): - return flt(self.stock_value_change) / flt(self.sle.actual_qty) - - -class GetAvailableSerialBatchBundle: - def __init__(self) -> None: - pass + return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty)) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 4bf008ac40..78572a66bc 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -7,6 +7,7 @@ frappe.provide('erpnext.buying'); frappe.ui.form.on('Subcontracting Receipt', { setup: (frm) => { + frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle']; frm.get_field('supplied_items').grid.cannot_add_rows = true; frm.get_field('supplied_items').grid.only_sortable(); diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 4e500a6a16..40dfd0dab6 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -105,7 +105,12 @@ class SubcontractingReceipt(SubcontractingController): self.update_status() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Repost Item Valuation", + "Serial and Batch Bundle", + ) self.update_status_updater_args() self.update_prevdoc_status() self.update_stock_ledger() 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 78e94c0afe..90bcf4e544 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 @@ -33,6 +33,7 @@ ], "fields": [ { + "columns": 2, "fieldname": "main_item_code", "fieldtype": "Link", "in_list_view": 1, @@ -41,6 +42,7 @@ "read_only": 1 }, { + "columns": 2, "fieldname": "rm_item_code", "fieldtype": "Link", "in_list_view": 1, @@ -77,14 +79,16 @@ "fieldtype": "Column Break" }, { + "columns": 1, "fieldname": "required_qty", "fieldtype": "Float", + "in_list_view": 1, "label": "Required Qty", "print_hide": 1, "read_only": 1 }, { - "columns": 2, + "columns": 1, "fieldname": "consumed_qty", "fieldtype": "Float", "in_list_view": 1, @@ -102,6 +106,7 @@ { "fieldname": "rate", "fieldtype": "Currency", + "in_list_view": 1, "label": "Rate", "options": "Company:company:default_currency", "read_only": 1 @@ -124,7 +129,6 @@ { "fieldname": "current_stock", "fieldtype": "Float", - "in_list_view": 1, "label": "Current Stock", "read_only": 1 }, @@ -188,25 +192,25 @@ "default": "0", "fieldname": "available_qty_for_consumption", "fieldtype": "Float", - "in_list_view": 1, "label": "Available Qty For Consumption", "print_hide": 1, "read_only": 1 }, { + "columns": 2, "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", - "label": "Serial and Batch Bundle", + "in_list_view": 1, + "label": "Serial / Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-03-12 14:11:48.816699", + "modified": "2023-03-15 13:55:08.132626", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item",