feat: serial and batch bundle for Subcontracting
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 342b8e9..74ba2b8 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -714,8 +714,11 @@
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 1e9c4dc..f7bc5d5 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -11,6 +11,9 @@
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 @@
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 @@
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 @@
"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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
"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 bdfc2f0..73c3868 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 @@
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 1dbe915..33b8955 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -4,6 +4,8 @@
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 5e9b706..382e6a9 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 @@
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 @@
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 @@
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 @@
"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 @@
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 @@
)
)
+ 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 @@
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_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 @@
)
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 @@
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 6d652e4..e4e8e17 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -1120,6 +1120,8 @@
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 a6eb9bf..0691d63 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -779,7 +779,6 @@
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 @@
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 @@
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 @@
)
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 @@
"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 f3943eb..8e148f7 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 1e28988..7c4f062 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -6,7 +6,6 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
.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 @@
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 @@
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 4bf008a..78572a6 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -7,6 +7,7 @@
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 4e500a6..40dfd0d 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -105,7 +105,12 @@
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 78e94c0..90bcf4e 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",