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