feat: serial and batch bundle for Subcontracting

This commit is contained in:
Rohit Waghchaure 2023-03-16 12:58:48 +05:30
parent e6143abb8a
commit 5ddd55a8ae
12 changed files with 320 additions and 89 deletions

View File

@ -714,8 +714,11 @@ class StockController(AccountsController):
message = self.prepare_over_receipt_message(rule, values) message = self.prepare_over_receipt_message(rule, values)
frappe.throw(msg=message, title=_("Over Receipt")) frappe.throw(msg=message, title=_("Over Receipt"))
def set_serial_and_batch_bundle(self): def set_serial_and_batch_bundle(self, table_name=None):
for row in self.items: if not table_name:
table_name = "items"
for row in self.get(table_name):
if row.serial_and_batch_bundle: if row.serial_and_batch_bundle:
frappe.get_doc( frappe.get_doc(
"Serial and Batch Bundle", row.serial_and_batch_bundle "Serial and Batch Bundle", row.serial_and_batch_bundle

View File

@ -11,6 +11,9 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, flt, get_link_to_form from frappe.utils import cint, cstr, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController 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.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
@ -48,6 +51,7 @@ class SubcontractingController(StockController):
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]: if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
self.validate_items() self.validate_items()
self.create_raw_materials_supplied() self.create_raw_materials_supplied()
self.set_serial_and_batch_bundle("supplied_items")
else: else:
super(SubcontractingController, self).validate() super(SubcontractingController, self).validate()
@ -169,7 +173,11 @@ class SubcontractingController(StockController):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
def __get_transferred_items(self): 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 = { alias_dict = {
"item_code": "rm_item_code", "item_code": "rm_item_code",
"subcontracted_item": "main_item_code", "subcontracted_item": "main_item_code",
@ -234,9 +242,11 @@ class SubcontractingController(StockController):
"serial_no", "serial_no",
"rm_item_code", "rm_item_code",
"reference_name", "reference_name",
"serial_and_batch_bundle",
"batch_no", "batch_no",
"consumed_qty", "consumed_qty",
"main_item_code", "main_item_code",
"parent as voucher_no",
], ],
filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype}, 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()) 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: if return_consumed_items:
return (consumed_materials, receipt_items) return (consumed_materials, receipt_items)
@ -262,11 +279,26 @@ class SubcontractingController(StockController):
continue continue
self.available_materials[key]["qty"] -= row.consumed_qty 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: if row.serial_no:
self.available_materials[key]["serial_no"] = list( self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no)) set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
) )
# Will be deperecated in v16
if row.batch_no: if row.batch_no:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
@ -281,7 +313,16 @@ class SubcontractingController(StockController):
if not self.subcontract_orders: if not self.subcontract_orders:
return 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)) key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
if key not in self.available_materials: if key not in self.available_materials:
@ -310,6 +351,17 @@ class SubcontractingController(StockController):
if row.batch_no: if row.batch_no:
details.batch_no[row.batch_no] += row.qty 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.__set_alternative_item_details(row)
self.__transferred_items = copy.deepcopy(self.available_materials) self.__transferred_items = copy.deepcopy(self.available_materials)
@ -327,6 +379,7 @@ class SubcontractingController(StockController):
self.set(self.raw_material_table, []) self.set(self.raw_material_table, [])
for item in self._doc_before_save.supplied_items: for item in self._doc_before_save.supplied_items:
if item.reference_name in self.__changed_name: if item.reference_name in self.__changed_name:
self.__remove_serial_and_batch_bundle(item)
continue continue
if item.reference_name not in self.__reference_name: if item.reference_name not in self.__reference_name:
@ -337,6 +390,10 @@ class SubcontractingController(StockController):
i += 1 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): def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] 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.required_qty = required_qty
rm_obj.consumed_qty = consumed_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)) 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"]: if (
new_rm_obj = None not self.available_materials[key]["serial_no"] and not self.available_materials[key]["batch_no"]
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): ):
if batch_qty >= qty or ( return
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)
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) bundle = frappe.get_doc(
self.available_materials[key]["batch_no"][batch_no] -= qty {
return "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: if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
qty -= batch_qty self.__set_serial_nos_for_bundle(bundle, qty, key)
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 abs(qty) > 0 and not new_rm_obj: elif self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
self.__set_consumed_qty(rm_obj, qty) self.__set_batch_nos_for_bundle(bundle, qty, key)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) bundle.flags.ignore_links = True
self.__set_serial_nos(item_row, rm_obj) 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): def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item) rm_obj = self.append(self.raw_material_table, bom_item)
rm_obj.reference_name = item_row.name 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": if self.doctype == "Subcontracting Receipt":
args = frappe._dict( args = frappe._dict(
{ {
@ -447,25 +550,21 @@ class SubcontractingController(StockController):
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time, "posting_time": self.posting_time,
"qty": -1 * flt(rm_obj.consumed_qty), "qty": -1 * flt(rm_obj.consumed_qty),
"serial_no": rm_obj.serial_no, "actual_qty": -1 * flt(rm_obj.consumed_qty),
"batch_no": rm_obj.batch_no,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"voucher_detail_no": item_row.name,
"company": self.company, "company": self.company,
"allow_zero_valuation": 1, "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.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(item_row, rm_obj, qty)
rm_obj.required_qty = qty
rm_obj.amount = rm_obj.required_qty * rm_obj.rate if rm_obj.serial_and_batch_bundle:
else: args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
rm_obj.consumed_qty = 0
setattr( rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
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)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item): 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)) key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))

View File

@ -891,10 +891,11 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate {
doc: this.frm.doc, doc: this.frm.doc,
} }
}).then(r => { }).then(r => {
debugger
this.callback && this.callback(r.message); this.callback && this.callback(r.message);
this.dialog.hide(); this.dialog.hide();
}) })
} else {
frappe.msgprint(__('Please save the document first'));
} }
} }

View File

@ -4,6 +4,8 @@ from frappe.utils import flt
class DeprecatedSerialNoValuation: class DeprecatedSerialNoValuation:
# Will be deperecated in v16
def calculate_stock_value_from_deprecarated_ledgers(self): def calculate_stock_value_from_deprecarated_ledgers(self):
serial_nos = list( serial_nos = list(
filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos()) filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos())

View File

@ -19,12 +19,14 @@ class SerialandBatchBundle(Document):
self.validate_serial_and_batch_no() self.validate_serial_and_batch_no()
self.validate_duplicate_serial_and_batch_no() self.validate_duplicate_serial_and_batch_no()
self.validate_voucher_no() self.validate_voucher_no()
self.validate_serial_nos()
def before_save(self): def before_save(self):
self.set_total_qty() self.set_total_qty()
self.set_is_outward() self.set_is_outward()
self.set_warehouse() self.set_warehouse()
self.set_incoming_rate() self.set_incoming_rate()
self.validate_qty_and_stock_value_difference()
if self.ledgers: if self.ledgers:
self.set_avg_rate() self.set_avg_rate()
@ -35,6 +37,17 @@ class SerialandBatchBundle(Document):
else: else:
self.set_incoming_rate_for_inward_transaction(row, save) 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): def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
sle = self.get_sle_for_outward_transaction(row) sle = self.get_sle_for_outward_transaction(row)
if self.has_serial_no: if self.has_serial_no:
@ -53,12 +66,12 @@ class SerialandBatchBundle(Document):
for d in self.ledgers: for d in self.ledgers:
if self.has_serial_no: 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: 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: 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: if save:
d.db_set( d.db_set(
@ -73,7 +86,7 @@ class SerialandBatchBundle(Document):
"item_code": self.item_code, "item_code": self.item_code,
"warehouse": self.warehouse, "warehouse": self.warehouse,
"serial_and_batch_bundle": self.name, "serial_and_batch_bundle": self.name,
"actual_qty": self.total_qty * -1, "actual_qty": self.total_qty,
"company": self.company, "company": self.company,
"serial_nos": [row.serial_no for row in self.ledgers if row.serial_no], "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}, "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) self.set_incoming_rate(save=True, row=row)
def validate_voucher_no(self): def validate_voucher_no(self):
if self.is_new():
return
if not (self.voucher_type and self.voucher_no): if not (self.voucher_type and self.voucher_no):
return 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): def validate_quantity(self, row):
self.set_total_qty(save=True) self.set_total_qty(save=True)
precision = row.precision 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( 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( doc.append(
"ledgers", "ledgers",
{ {
"qty": row.qty or 1.0, "qty": (row.qty or 1.0) * (1 if type_of_transaction == "Inward" else -1),
"warehouse": warehouse, "warehouse": warehouse,
"batch_no": row.batch_no, "batch_no": row.batch_no,
"serial_no": row.serial_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]: def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> Dict[str, Dict]:
data = get_ledgers_from_serial_batch_bundle(**kwargs) data = get_ledgers_from_serial_batch_bundle(**kwargs)
if not data:
return {}
group_by_voucher = {} group_by_voucher = {}
for row in data: for row in data:
key = (row.item_code, row.warehouse, row.voucher_no) 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: if key not in group_by_voucher:
group_by_voucher.setdefault( 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] 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(): 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 key in ["name", "item_code", "warehouse", "voucher_no", "company", "voucher_detail_no"]:
if isinstance(val, list): if isinstance(val, list):
query = query.where(bundle_table[key].isin(val)) 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) query = query.where(serial_batch_table[key] == val)
return query.run(as_dict=True) 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)

View File

@ -1120,6 +1120,8 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
frm.refresh_fields(); frm.refresh_fields();
frappe.model.set_value(item.doctype, item.name, frappe.model.set_value(item.doctype, item.name,
"serial_and_batch_bundle", r.name); "serial_and_batch_bundle", r.name);
frm.save();
} }
} }
); );

View File

@ -779,7 +779,6 @@ class StockEntry(StockController):
if reset_outgoing_rate: if reset_outgoing_rate:
args = self.get_args_for_incoming_rate(d) args = self.get_args_for_incoming_rate(d)
rate = get_incoming_rate(args, raise_error_if_no_rate) rate = get_incoming_rate(args, raise_error_if_no_rate)
print(rate, "set rate for outgoing items")
if rate > 0: if rate > 0:
d.basic_rate = rate d.basic_rate = rate
@ -1223,6 +1222,14 @@ class StockEntry(StockController):
if d.serial_and_batch_bundle and self.docstatus == 1: if d.serial_and_batch_bundle and self.docstatus == 1:
self.copy_serial_and_batch_bundle(sle, d) 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) sl_entries.append(sle)
def copy_serial_and_batch_bundle(self, sle, child): def copy_serial_and_batch_bundle(self, sle, child):
@ -1240,9 +1247,17 @@ class StockEntry(StockController):
bundle_doc.type_of_transaction = "Inward" bundle_doc.type_of_transaction = "Inward"
for row in bundle_doc.ledgers: 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.warehouse = child.t_warehouse
row.is_outward = 0 row.is_outward = 0
bundle_doc.set_total_qty()
bundle_doc.set_avg_rate()
bundle_doc.flags.ignore_permissions = True bundle_doc.flags.ignore_permissions = True
bundle_doc.submit() bundle_doc.submit()
sle.serial_and_batch_bundle = bundle_doc.name 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: 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) batchwise_serial_nos = get_batchwise_serial_nos(child.item_code, row)
for batch_no, qty in row.batches_to_be_consume.items(): 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, "batch_no": batch_no,
"serial_no": batchwise_serial_nos.get(batch_no).pop(0), "serial_no": batchwise_serial_nos.get(batch_no).pop(0),
"warehouse": row.warehouse, "warehouse": row.warehouse,
"qty": qty, "qty": -1,
}, },
) )
elif row.serial_nos: elif row.serial_nos:
doc.has_serial_no = 1
for serial_no in row.serial_nos: 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: elif row.batches_to_be_consume:
doc.has_batch_no = 1
for batch_no, qty in row.batches_to_be_consume.items(): 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 return doc.insert(ignore_permissions=True).name

View File

@ -20,13 +20,13 @@
"serial_and_batch_bundle", "serial_and_batch_bundle",
"batch_no", "batch_no",
"column_break_11", "column_break_11",
"current_serial_and_batch_bundle",
"serial_no", "serial_no",
"section_break_3", "section_break_3",
"current_qty", "current_qty",
"current_amount", "current_amount",
"column_break_9", "column_break_9",
"current_valuation_rate", "current_valuation_rate",
"current_serial_and_batch_bundle",
"current_serial_no", "current_serial_no",
"section_break_14", "section_break_14",
"quantity_difference", "quantity_difference",
@ -192,7 +192,7 @@
{ {
"fieldname": "serial_and_batch_bundle", "fieldname": "serial_and_batch_bundle",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Serial and Batch Bundle", "label": "Serial / Batch Bundle",
"no_copy": 1, "no_copy": 1,
"options": "Serial and Batch Bundle", "options": "Serial and Batch Bundle",
"print_hide": 1 "print_hide": 1

View File

@ -6,7 +6,6 @@ from frappe import _, bold
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, now from frappe.utils import cint, flt, now
from pypika import Case
from erpnext.stock.deprecated_serial_batch import ( from erpnext.stock.deprecated_serial_batch import (
DeprecatedBatchNoValuation, DeprecatedBatchNoValuation,
@ -209,13 +208,18 @@ class SerialBatchBundle:
frappe.db.bulk_insert("Serial and Batch Ledger", fields=fields, values=set(ledgers)) 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): 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( sn_doc.append(
"ledgers", "ledgers",
{ {
"batch_no": batch_no, "batch_no": batch_no,
"qty": self.sle.actual_qty, "qty": self.sle.actual_qty,
"incoming_rate": incoming_rate, "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( frappe.db.set_value(
"Serial and Batch Bundle", "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": ""}, {"is_cancelled": 1, "voucher_no": ""},
) )
@ -303,22 +307,24 @@ class SerialBatchBundle:
return False return False
def post_process(self): def post_process(self):
if not self.sle.is_cancelled: if self.item_details.has_serial_no == 1:
if self.item_details.has_serial_no == 1: self.set_warehouse_and_status_in_serial_nos()
self.set_warehouse_and_status_in_serial_nos()
if self.item_details.has_serial_no == 1 and self.item_details.has_batch_no == 1: if (
self.set_batch_no_in_serial_nos() self.sle.actual_qty > 0
else: and self.item_details.has_serial_no == 1
pass and self.item_details.has_batch_no == 1
# self.set_data_based_on_last_sle() ):
self.set_batch_no_in_serial_nos()
def set_warehouse_and_status_in_serial_nos(self): 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 warehouse = self.warehouse if self.sle.actual_qty > 0 else None
sn_table = frappe.qb.DocType("Serial No") if not serial_nos:
serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle, check_outward=False) return
sn_table = frappe.qb.DocType("Serial No")
( (
frappe.qb.update(sn_table) frappe.qb.update(sn_table)
.set(sn_table.warehouse, warehouse) .set(sn_table.warehouse, warehouse)
@ -330,7 +336,7 @@ class SerialBatchBundle:
ledgers = frappe.get_all( ledgers = frappe.get_all(
"Serial and Batch Ledger", "Serial and Batch Ledger",
fields=["serial_no", "batch_no"], fields=["serial_no", "batch_no"],
filters={"parent": self.serial_and_batch_bundle}, filters={"parent": self.sle.serial_and_batch_bundle},
) )
batch_serial_nos = {} batch_serial_nos = {}
@ -391,7 +397,7 @@ class SerialNoBundleValuation(DeprecatedSerialNoValuation):
TIMESTAMP( TIMESTAMP(
parent.posting_date, parent.posting_time parent.posting_date, parent.posting_time
) )
), child.name ), child.name, child.serial_no, child.warehouse
FROM FROM
`tabSerial and Batch Bundle` as parent, `tabSerial and Batch Bundle` as parent,
`tabSerial and Batch Ledger` as child `tabSerial and Batch Ledger` as child
@ -417,14 +423,18 @@ class SerialNoBundleValuation(DeprecatedSerialNoValuation):
return frappe.db.sql( return frappe.db.sql(
f""" f"""
SELECT SELECT
serial_no, incoming_rate ledger.serial_no, ledger.incoming_rate, ledger.warehouse
FROM FROM
`tabSerial and Batch Ledger` AS ledger, `tabSerial and Batch Ledger` AS ledger,
({subquery}) AS SubQuery ({subquery}) AS SubQuery
WHERE WHERE
ledger.name = SubQuery.name ledger.name = SubQuery.name
AND ledger.serial_no = SubQuery.serial_no
AND ledger.warehouse = SubQuery.warehouse
GROUP BY GROUP BY
ledger.serial_no ledger.serial_no
Order By
ledger.creation
""", """,
as_dict=1, 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) return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
def get_incoming_rate(self): 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): def is_rejected(voucher_type, voucher_detail_no, warehouse):
@ -517,7 +527,7 @@ class BatchNoBundleValuation(DeprecatedBatchNoValuation):
.select( .select(
child.batch_no, child.batch_no,
Sum(child.stock_value_difference).as_("incoming_rate"), 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( .where(
(child.batch_no.isin(batch_nos)) (child.batch_no.isin(batch_nos))
@ -544,7 +554,7 @@ class BatchNoBundleValuation(DeprecatedBatchNoValuation):
def set_stock_value_difference(self): def set_stock_value_difference(self):
self.stock_value_change = 0 self.stock_value_change = 0
for batch_no, ledger in self.batch_nos.items(): 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 self.stock_value_change += stock_value_change
frappe.db.set_value( frappe.db.set_value(
"Serial and Batch Ledger", ledger.name, "stock_value_difference", stock_value_change "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 self.wh_data.qty_after_transaction += self.sle.actual_qty
def get_incoming_rate(self): 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))
class GetAvailableSerialBatchBundle:
def __init__(self) -> None:
pass

View File

@ -7,6 +7,7 @@ frappe.provide('erpnext.buying');
frappe.ui.form.on('Subcontracting Receipt', { frappe.ui.form.on('Subcontracting Receipt', {
setup: (frm) => { 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.cannot_add_rows = true;
frm.get_field('supplied_items').grid.only_sortable(); frm.get_field('supplied_items').grid.only_sortable();

View File

@ -105,7 +105,12 @@ class SubcontractingReceipt(SubcontractingController):
self.update_status() self.update_status()
def on_cancel(self): 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_status_updater_args()
self.update_prevdoc_status() self.update_prevdoc_status()
self.update_stock_ledger() self.update_stock_ledger()

View File

@ -33,6 +33,7 @@
], ],
"fields": [ "fields": [
{ {
"columns": 2,
"fieldname": "main_item_code", "fieldname": "main_item_code",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -41,6 +42,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 2,
"fieldname": "rm_item_code", "fieldname": "rm_item_code",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -77,14 +79,16 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"columns": 1,
"fieldname": "required_qty", "fieldname": "required_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1,
"label": "Required Qty", "label": "Required Qty",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 2, "columns": 1,
"fieldname": "consumed_qty", "fieldname": "consumed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
@ -102,6 +106,7 @@
{ {
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate", "label": "Rate",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
@ -124,7 +129,6 @@
{ {
"fieldname": "current_stock", "fieldname": "current_stock",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1,
"label": "Current Stock", "label": "Current Stock",
"read_only": 1 "read_only": 1
}, },
@ -188,25 +192,25 @@
"default": "0", "default": "0",
"fieldname": "available_qty_for_consumption", "fieldname": "available_qty_for_consumption",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1,
"label": "Available Qty For Consumption", "label": "Available Qty For Consumption",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 2,
"fieldname": "serial_and_batch_bundle", "fieldname": "serial_and_batch_bundle",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Serial and Batch Bundle", "in_list_view": 1,
"label": "Serial / Batch Bundle",
"no_copy": 1, "no_copy": 1,
"options": "Serial and Batch Bundle", "options": "Serial and Batch Bundle",
"print_hide": 1, "print_hide": 1
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-03-12 14:11:48.816699", "modified": "2023-03-15 13:55:08.132626",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Receipt Supplied Item", "name": "Subcontracting Receipt Supplied Item",