fix: serial and batch selector

This commit is contained in:
Rohit Waghchaure 2023-04-11 13:22:15 +05:30
parent 39da92929b
commit f4cfc589c6
6 changed files with 341 additions and 33 deletions

View File

@ -1527,7 +1527,6 @@ class TestWorkOrder(FrappeTestCase):
ste_doc.load_from_db()
# Create a stock entry to manufacture the item
print("remove 2 qty from each item")
ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 5))
for row in ste_doc.items:
if row.s_warehouse and not row.t_warehouse:

View File

@ -12,12 +12,12 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
}
make() {
let label = this.item?.has_serial_no ? __('Serial No') : __('Batch No');
let label = this.item?.has_serial_no ? __('Serial Nos') : __('Batch Nos');
let primary_label = this.bundle
? __('Update') : __('Add');
if (this.item?.has_serial_no && this.item?.batch_no) {
label = __('Serial No / Batch No');
label = __('Serial Nos / Batch Nos');
}
primary_label += ' ' + label;
@ -26,7 +26,9 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
title: this.item?.title || primary_label,
fields: this.get_dialog_fields(),
primary_action_label: primary_label,
primary_action: () => this.update_ledgers()
primary_action: () => this.update_ledgers(),
secondary_action_label: __('Edit Full Form'),
secondary_action: () => this.edit_full_form(),
});
this.dialog.set_value("qty", this.item.qty);
@ -48,7 +50,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
if (this.item.has_serial_no) {
fields.push({
fieldtype: 'Link',
fieldtype: 'Data',
fieldname: 'scan_serial_no',
label: __('Scan Serial No'),
options: 'Serial No',
@ -279,6 +281,37 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
})
}
edit_full_form() {
let bundle_id = this.item.serial_and_batch_bundle
if (!bundle_id) {
_new = frappe.model.get_new_doc(
"Serial and Batch Bundle", null, null, true
);
_new.item_code = this.item.item_code;
_new.warehouse = this.get_warehouse();
_new.has_serial_no = this.item.has_serial_no;
_new.has_batch_no = this.item.has_batch_no;
_new.type_of_transaction = this.get_type_of_transaction();
_new.company = this.frm.doc.company;
_new.voucher_type = this.frm.doc.doctype;
bundle_id = _new.name;
}
frappe.set_route("Form", "Serial and Batch Bundle", bundle_id);
this.dialog.hide();
}
get_warehouse() {
return (this.item?.outward ?
(this.item.warehouse || this.item.s_warehouse)
: (this.item.warehouse || this.item.t_warehouse));
}
get_type_of_transaction() {
return (this.item?.outward ? 'Outward' : 'Inward');
}
render_data() {
if (!this.frm.is_new() && this.bundle) {
frappe.call({

View File

@ -4,6 +4,7 @@ import frappe
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import flt
from frappe.utils.deprecations import deprecated
from pypika import Order
class DeprecatedSerialNoValuation:
@ -39,25 +40,25 @@ class DeprecatedSerialNoValuation:
# 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 serial_and_batch_bundle IS NULL
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
table = frappe.qb.DocType("Stock Ledger Entry")
incoming_rate = (
frappe.qb.from_(table)
.select(table.incoming_rate)
.where(
(
(table.serial_no == serial_no)
| (table.serial_no.like(serial_no + "\n%"))
| (table.serial_no.like("%\n" + serial_no))
| (table.serial_no.like("%\n" + serial_no + "\n%"))
)
order by posting_date desc
limit 1
""",
(self.sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
)
& (table.company == self.sle.company)
& (table.serial_and_batch_bundle.isnull())
& (table.actual_qty > 0)
& (table.is_cancelled == 0)
)
.orderby(table.posting_date, order=Order.desc)
.limit(1)
).run()
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]

View File

@ -8,6 +8,17 @@ frappe.ui.form.on('Serial and Batch Bundle', {
refresh(frm) {
frm.trigger('toggle_fields');
frm.trigger('prepare_serial_batch_prompt');
},
item_code(frm) {
frm.clear_custom_buttons();
frm.trigger('prepare_serial_batch_prompt');
},
type_of_transaction(frm) {
frm.clear_custom_buttons();
frm.trigger('prepare_serial_batch_prompt');
},
warehouse(frm) {
@ -30,6 +41,91 @@ frappe.ui.form.on('Serial and Batch Bundle', {
frm.trigger('toggle_fields');
},
prepare_serial_batch_prompt(frm) {
if (frm.doc.docstatus === 0 && frm.doc.item_code
&& frm.doc.type_of_transaction === "Inward") {
let label = frm.doc?.has_serial_no === 1
? __('Serial Nos') : __('Batch Nos');
if (frm.doc?.has_serial_no === 1 && frm.doc?.has_batch_no === 1) {
label = __('Serial and Batch Nos');
}
let fields = frm.events.get_prompt_fields(frm);
frm.add_custom_button(__("Make " + label), () => {
frappe.prompt(fields, (data) => {
frm.events.add_serial_batch(frm, data);
}, "Add " + label, "Make " + label);
});
}
},
get_prompt_fields(frm) {
let attach_field = {
"label": __("Attach CSV File"),
"fieldname": "csv_file",
"fieldtype": "Attach"
}
if (!frm.doc.has_batch_no) {
attach_field.depends_on = "eval:doc.using_csv_file === 1"
}
let fields = [
{
"label": __("Using CSV File"),
"fieldname": "using_csv_file",
"default": 1,
"fieldtype": "Check",
},
attach_field,
{
"fieldtype": "Section Break",
}
]
if (frm.doc.has_serial_no) {
fields.push({
"label": "Serial Nos",
"fieldname": "serial_nos",
"fieldtype": "Small Text",
"depends_on": "eval:doc.using_csv_file === 0"
})
}
if (frm.doc.has_batch_no) {
fields = attach_field
}
return fields;
},
add_serial_batch(frm, prompt_data) {
frm.events.validate_prompt_data(frm, prompt_data);
frm.call({
method: "add_serial_batch",
doc: frm.doc,
args: {
"data": prompt_data,
},
callback(r) {
refresh_field("entries");
}
});
},
validate_prompt_data(frm, prompt_data) {
if (prompt_data.using_csv_file && !prompt_data.csv_file) {
frappe.throw(__("Please attach CSV file"));
}
if (frm.doc.has_serial_no && !prompt_data.using_csv_file && !prompt_data.serial_nos) {
frappe.throw(__("Please enter serial nos"));
}
},
toggle_fields(frm) {
frm.fields_dict.entries.grid.update_docfield_property(
'serial_no', 'read_only', !frm.doc.has_serial_no

View File

@ -9,13 +9,13 @@
"item_details_tab",
"naming_series",
"company",
"warehouse",
"type_of_transaction",
"column_break_4",
"item_code",
"item_name",
"has_serial_no",
"has_batch_no",
"column_break_4",
"item_code",
"warehouse",
"type_of_transaction",
"serial_no_and_batch_no_tab",
"entries",
"quantity_and_rate_section",
@ -84,7 +84,8 @@
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name"
"label": "Item Name",
"read_only": 1
},
{
"default": "0",
@ -243,7 +244,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-04-06 02:35:38.404537",
"modified": "2023-04-10 20:02:42.964309",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",

View File

@ -2,6 +2,7 @@
# For license information, please see license.txt
import collections
import csv
from collections import defaultdict
from typing import Dict, List
@ -9,7 +10,17 @@ import frappe
from frappe import _, _dict, bold
from frappe.model.document import Document
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import add_days, cint, flt, get_link_to_form, nowtime, today
from frappe.utils import (
add_days,
cint,
cstr,
flt,
get_link_to_form,
now,
nowtime,
parse_json,
today,
)
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
@ -626,6 +637,173 @@ class SerialandBatchBundle(Document):
self.delink_reference_from_batch()
self.clear_table()
@frappe.whitelist()
def add_serial_batch(self, data):
serial_nos, batch_nos = [], []
if isinstance(data, str):
data = parse_json(data)
if data.get("csv_file"):
serial_nos, batch_nos = get_serial_batch_from_csv(self.item_code, data.get("csv_file"))
else:
serial_nos, batch_nos = get_serial_batch_from_data(self.item_code, data)
if not serial_nos and not batch_nos:
return
if serial_nos:
self.set("entries", serial_nos)
elif batch_nos:
self.set("entries", batch_nos)
def get_serial_batch_from_csv(item_code, file_path):
file_path = frappe.get_site_path() + file_path
serial_nos = []
batch_nos = []
with open(file_path, "r") as f:
reader = csv.reader(f)
serial_nos, batch_nos = parse_csv_file_to_get_serial_batch(reader)
if serial_nos:
make_serial_nos(item_code, serial_nos)
print(batch_nos)
if batch_nos:
make_batch_nos(item_code, batch_nos)
return serial_nos, batch_nos
def parse_csv_file_to_get_serial_batch(reader):
has_serial_no, has_batch_no = False, False
serial_nos = []
batch_nos = []
for index, row in enumerate(reader):
if index == 0:
has_serial_no = row[0] == "Serial No"
has_batch_no = row[0] == "Batch No"
continue
if not row[0]:
continue
if has_serial_no or (has_serial_no and has_batch_no):
_dict = {"serial_no": row[0], "qty": 1}
if has_batch_no:
_dict.update(
{
"batch_no": row[1],
"qty": row[2],
}
)
serial_nos.append(_dict)
elif has_batch_no:
batch_nos.append(
{
"batch_no": row[0],
"qty": row[1],
}
)
return serial_nos, batch_nos
def get_serial_batch_from_data(item_code, kwargs):
serial_nos = []
batch_nos = []
if kwargs.get("serial_nos"):
data = parse_serial_nos(kwargs.get("serial_nos"))
for serial_no in data:
if not serial_no:
continue
serial_nos.append({"serial_no": serial_no, "qty": 1})
make_serial_nos(item_code, serial_nos)
return serial_nos, batch_nos
def make_serial_nos(item_code, serial_nos):
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")]
serial_nos_details = []
user = frappe.session.user
for serial_no in serial_nos:
serial_nos_details.append(
(
serial_no,
serial_no,
now(),
now(),
user,
user,
item.item_code,
item.item_name,
item.description,
"Inactive",
)
)
fields = [
"name",
"serial_no",
"creation",
"modified",
"owner",
"modified_by",
"item_code",
"item_name",
"description",
"status",
]
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
frappe.msgprint(_("Serial Nos are created successfully"))
def make_batch_nos(item_code, batch_nos):
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
batch_nos = [d.get("batch_no") for d in batch_nos if d.get("batch_no")]
batch_nos_details = []
user = frappe.session.user
for batch_no in batch_nos:
batch_nos_details.append(
(batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description)
)
fields = [
"name",
"batch_id",
"creation",
"modified",
"owner",
"modified_by",
"item",
"item_name",
"description",
]
frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))
frappe.msgprint(_("Batch Nos are created successfully"))
def parse_serial_nos(data):
if isinstance(data, list):
return data
return [s.strip() for s in cstr(data).strip().upper().replace(",", "\n").split("\n") if s.strip()]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@ -690,13 +868,13 @@ def get_filters_for_bundle(item_code, docstatus=None, voucher_no=None, name=None
@frappe.whitelist()
def add_serial_batch_ledgers(entries, child_row, doc) -> object:
if isinstance(child_row, str):
child_row = frappe._dict(frappe.parse_json(child_row))
child_row = frappe._dict(parse_json(child_row))
if isinstance(entries, str):
entries = frappe.parse_json(entries)
entries = parse_json(entries)
if doc and isinstance(doc, str):
parent_doc = frappe.parse_json(doc)
parent_doc = parse_json(doc)
if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc)