feat: serial and batch bundle for Subcontracting
This commit is contained in:
parent
e6143abb8a
commit
5ddd55a8ae
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user