fix: removed sales and purchase fields from serial nos

This commit is contained in:
Rohit Waghchaure 2022-12-05 14:48:18 +05:30
parent ba1aac1613
commit 6c9b212dd1
26 changed files with 873 additions and 642 deletions

View File

@ -1448,6 +1448,7 @@ class PurchaseInvoice(BuyingController):
"Repost Payment Ledger Items",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
"Serial and Batch Bundle",
)
self.update_advance_tax_references(cancel=1)

View File

@ -400,6 +400,7 @@ class SalesInvoice(SellingController):
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Payment Ledger Entry",
"Serial and Batch Bundle",
)
def update_status_updater_args(self):

View File

@ -5,7 +5,7 @@
import frappe
from frappe import _, bold, throw
from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime
from frappe.utils import cint, flt, get_link_to_form, nowtime
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
@ -299,8 +299,7 @@ class SellingController(StockController):
"item_code": p.item_code,
"qty": flt(p.qty),
"uom": p.uom,
"batch_no": cstr(p.batch_no).strip(),
"serial_no": cstr(p.serial_no).strip(),
"serial_and_batch_bundle": p.serial_and_batch_bundle,
"name": d.name,
"target_warehouse": p.target_warehouse,
"company": self.company,
@ -323,8 +322,7 @@ class SellingController(StockController):
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
"batch_no": cstr(d.get("batch_no")).strip(),
"serial_no": cstr(d.get("serial_no")).strip(),
"serial_and_batch_bundle": d.serial_and_batch_bundle,
"name": d.name,
"target_warehouse": d.target_warehouse,
"company": self.company,

View File

@ -354,6 +354,7 @@ class StockController(AccountsController):
"batch_no": batch_no,
"qty": d.qty,
"warehouse": d.get(warehouse_field),
"incoming_rate": d.rate,
}
],
}

View File

@ -33,7 +33,6 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings
from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
from erpnext.stock.doctype.serial_no.serial_no import (
auto_make_serial_nos,
clean_serial_no_string,
get_auto_serial_nos,
get_serial_nos,
@ -455,7 +454,7 @@ class WorkOrder(Document):
if self.serial_no:
args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
auto_make_serial_nos(args)
# auto_make_serial_nos(args)
serial_nos_length = len(get_serial_nos(self.serial_no))
if serial_nos_length != self.qty:

View File

@ -6,6 +6,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
setup() {
super.setup();
let me = this;
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frappe.flags.hide_serial_batch_dialog = true;
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
@ -124,7 +127,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let item_row = locals[cdt][cdn];
return {
filters: {
'item_code': item_row.item_code
'item_code': item_row.item_code,
'voucher_type': doc.doctype,
'voucher_no': ["in", [doc.name, ""]],
}
}
});
@ -2277,12 +2282,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
};
erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_dialog) {
erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) {
let warehouse, receiving_stock, existing_stock;
if (frm.doc.is_return) {
if (["Purchase Receipt", "Purchase Invoice"].includes(frm.doc.doctype)) {
existing_stock = true;
warehouse = d.warehouse;
warehouse = item_row.warehouse;
} else if (["Delivery Note", "Sales Invoice"].includes(frm.doc.doctype)) {
receiving_stock = true;
}
@ -2292,11 +2297,11 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_
receiving_stock = true;
} else {
existing_stock = true;
warehouse = d.s_warehouse;
warehouse = item_row.s_warehouse;
}
} else {
existing_stock = true;
warehouse = d.warehouse;
warehouse = item_row.warehouse;
}
}
@ -2309,16 +2314,13 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_
}
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
new erpnext.SerialNoBatchSelector({
frm: frm,
item: d,
warehouse_details: {
type: "Warehouse",
name: warehouse
},
callback: callback,
on_close: on_close
}, show_dialog);
new erpnext.SerialNoBatchBundleUpdate(frm, item_row, (r) => {
if (r) {
frm.refresh_fields();
frappe.model.set_value(item_row.doctype, item_row.name,
"serial_and_batch_bundle", r.name);
}
});
});
}

View File

@ -629,20 +629,37 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate {
}
make() {
let label = this.item?.has_serial_no ? __('Serial No') : __('Batch No');
let primary_label = this.item?.serial_and_batch_bundle
? __('Update') : __('Add');
if (this.item?.has_serial_no && this.item?.batch_no) {
label = __('Serial No / Batch No');
}
primary_label += ' ' + label;
this.dialog = new frappe.ui.Dialog({
title: __('Update Serial No / Batch No'),
title: this.item?.title || primary_label,
fields: this.get_dialog_fields(),
primary_action_label: __('Update'),
primary_action_label: primary_label,
primary_action: () => this.update_ledgers()
});
if (this.item?.outward) {
this.prepare_for_auto_fetch();
}
this.dialog.show();
}
get_serial_no_filters() {
let warehouse = this.item?.outward ?
this.item.warehouse : "";
return {
'item_code': this.item.item_code,
'warehouse': ["=", ""],
'delivery_document_no': ["=", ""],
'warehouse': ["=", warehouse]
};
}
@ -681,12 +698,14 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate {
});
}
if (this.item.has_batch_no && this.item.has_serial_no) {
fields.push({
fieldtype: 'Section Break',
});
if (this.item?.outward) {
fields = [...fields, ...this.get_filter_fields()];
}
fields.push({
fieldtype: 'Section Break',
});
fields.push({
fieldname: 'ledgers',
fieldtype: 'Table',
@ -698,6 +717,41 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate {
return fields;
}
get_filter_fields() {
return [
{
fieldtype: 'Section Break',
label: __('Auto Fetch')
},
{
fieldtype: 'Float',
fieldname: 'qty',
default: this.item.qty || 0,
label: __('Qty to Fetch'),
},
{
fieldtype: 'Column Break',
},
{
fieldtype: 'Select',
options: ['FIFO', 'LIFO', 'Expiry'],
default: 'FIFO',
fieldname: 'based_on',
label: __('Fetch Based On')
},
{
fieldtype: 'Column Break',
},
{
fieldtype: 'Button',
fieldname: 'get_auto_data',
label: __('Fetch {0}',
[this.item?.has_serial_no ? 'Serial Nos' : 'Batch Nos']),
},
]
}
get_dialog_table_fields() {
let fields = []
@ -714,7 +768,9 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate {
}
}
})
} else if (this.item.has_batch_no) {
}
if (this.item.has_batch_no) {
fields = [
{
fieldtype: 'Link',
@ -742,6 +798,38 @@ erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate {
return fields;
}
prepare_for_auto_fetch() {
this.dialog.fields_dict.get_auto_data.$input.on('click', () => {
this.get_auto_data();
});
}
get_auto_data() {
const { qty, based_on } = this.dialog.get_values();
if (!qty) {
frappe.throw(__('Please enter Qty to Fetch'));
}
frappe.call({
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,
has_serial_no: this.item.has_serial_no,
has_batch_no: this.item.has_batch_no,
qty: qty,
based_on: based_on
},
callback: (r) => {
if (r.message) {
this.dialog.fields_dict.ledgers.df.data = r.message;
this.dialog.fields_dict.ledgers.grid.refresh();
}
}
});
}
update_serial_batch_no() {
const { scan_serial_no, scan_batch_no } = this.dialog.get_values();

View File

@ -420,6 +420,40 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
});
}
pick_serial_and_batch(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.outward = true;
item.title = item.has_serial_no ?
__("Select Serial No") : __("Select Batch No");
if (item.has_serial_no && item.has_batch_no) {
item.title = __("Select Serial and Batch");
}
frappe.require(path, function() {
new erpnext.SerialNoBatchBundleUpdate(
me.frm, item, (r) => {
if (r) {
me.frm.refresh_fields();
frappe.model.set_value(cdt, cdn,
"serial_and_batch_bundle", r.name);
}
}
);
});
}
});
}
update_auto_repeat_reference(doc) {
if (doc.auto_repeat) {
frappe.call({

View File

@ -12,7 +12,6 @@ from frappe.utils import cint, flt
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@ -142,11 +141,6 @@ class DeliveryNote(SellingController):
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
if self._action != "submit" and not self.is_return:
set_batch_nos(self, "warehouse", throw=True)
set_batch_nos(self, "warehouse", throw=True, child_table="packed_items")
self.update_current_stock()
if not self.installation_status:
@ -274,7 +268,12 @@ class DeliveryNote(SellingController):
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Serial and Batch Bundle",
)
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""

View File

@ -77,8 +77,8 @@
"dn_detail",
"pick_list_item",
"section_break_40",
"batch_no",
"serial_no",
"pick_serial_and_batch",
"serial_and_batch_bundle",
"actual_batch_qty",
"actual_qty",
"installed_qty",
@ -507,16 +507,6 @@
"fieldname": "section_break_40",
"fieldtype": "Section Break"
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Batch No",
"oldfieldname": "batch_no",
"oldfieldtype": "Link",
"options": "Batch",
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "actual_qty",
@ -542,15 +532,6 @@
"read_only": 1,
"width": "150px"
},
{
"fieldname": "serial_no",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Serial No",
"no_copy": 1,
"oldfieldname": "serial_no",
"oldfieldtype": "Text"
},
{
"fieldname": "item_group",
"fieldtype": "Link",
@ -861,6 +842,17 @@
"no_copy": 1,
"non_negative": 1,
"read_only": 1
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"options": "Serial and Batch Bundle"
},
{
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
}
],
"idx": 1,

View File

@ -19,6 +19,7 @@
"rate",
"uom",
"section_break_9",
"serial_and_batch_bundle",
"serial_no",
"column_break_11",
"batch_no",
@ -253,6 +254,12 @@
"no_copy": 1,
"non_negative": 1,
"read_only": 1
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"options": "Serial and Batch Bundle"
}
],
"idx": 1,

View File

@ -7,8 +7,6 @@ frappe.provide("erpnext.stock");
frappe.ui.form.on("Purchase Receipt", {
setup: (frm) => {
frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frm.make_methods = {
'Landed Cost Voucher': () => {
let lcv = frappe.model.get_new_doc('Landed Cost Voucher');

View File

@ -10,6 +10,36 @@ frappe.ui.form.on('Serial and Batch Bundle', {
frm.trigger('toggle_fields');
},
warehouse(frm) {
if (frm.doc.warehouse) {
frm.call({
method: "set_warehouse",
doc: frm.doc,
callback(r) {
refresh_field("ledgers");
}
})
}
},
has_serial_no(frm) {
frm.trigger('toggle_fields');
},
has_batch_no(frm) {
frm.trigger('toggle_fields');
},
toggle_fields(frm) {
frm.fields_dict.ledgers.grid.update_docfield_property(
'serial_no', 'read_only', !frm.doc.has_serial_no
);
frm.fields_dict.ledgers.grid.update_docfield_property(
'batch_no', 'read_only', !frm.doc.has_batch_no
);
},
set_queries(frm) {
frm.set_query('item_code', () => {
return {
@ -35,6 +65,15 @@ frappe.ui.form.on('Serial and Batch Bundle', {
};
});
frm.set_query('warehouse', () => {
return {
filters: {
'is_group': 0,
'company': frm.doc.company,
}
};
});
frm.set_query('serial_no', 'ledgers', () => {
return {
filters: {
@ -58,23 +97,14 @@ frappe.ui.form.on('Serial and Batch Bundle', {
}
};
});
},
has_serial_no(frm) {
frm.trigger('toggle_fields');
},
has_batch_no(frm) {
frm.trigger('toggle_fields');
},
toggle_fields(frm) {
frm.fields_dict.ledgers.grid.update_docfield_property(
'serial_no', 'read_only', !frm.doc.has_serial_no
);
frm.fields_dict.ledgers.grid.update_docfield_property(
'batch_no', 'read_only', !frm.doc.has_batch_no
);
}
});
frappe.ui.form.on("Serial and Batch Ledger", {
ledgers_add(frm, cdt, cdn) {
if (frm.doc.warehouse) {
locals[cdt][cdn].warehouse = frm.doc.warehouse;
}
},
})

View File

@ -8,17 +8,23 @@
"item_details_tab",
"company",
"item_group",
"has_serial_no",
"warehouse",
"column_break_4",
"item_code",
"item_name",
"has_serial_no",
"has_batch_no",
"serial_no_and_batch_no_tab",
"ledgers",
"qty",
"quantity_and_rate_section",
"total_qty",
"column_break_13",
"avg_rate",
"total_amount",
"tab_break_12",
"voucher_type",
"voucher_no",
"column_break_aouy",
"is_cancelled",
"amended_from"
],
@ -90,12 +96,6 @@
"options": "Serial and Batch Ledger",
"reqd": 1
},
{
"fieldname": "qty",
"fieldtype": "Float",
"label": "Total Qty",
"read_only": 1
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
@ -129,12 +129,54 @@
"fieldname": "tab_break_12",
"fieldtype": "Tab Break",
"label": "Reference"
},
{
"fieldname": "quantity_and_rate_section",
"fieldtype": "Section Break",
"label": "Quantity and Rate"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "avg_rate",
"fieldtype": "Float",
"label": "Avg Rate",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "total_amount",
"fieldtype": "Float",
"label": "Total Amount",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "total_qty",
"fieldtype": "Float",
"label": "Total Qty",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_aouy",
"fieldtype": "Column Break"
},
{
"depends_on": "company",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-11-24 13:05:11.623968",
"modified": "2023-01-10 11:32:09.018760",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",

View File

@ -1,20 +1,114 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import collections
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, today
from pypika import Case
class SerialandBatchBundle(Document):
def validate(self):
self.validate_serial_and_batch_no()
self.validate_duplicate_serial_and_batch_no()
def before_save(self):
self.set_outgoing_rate()
if self.ledgers:
self.set_total_qty()
self.set_avg_rate()
@frappe.whitelist()
def set_warehouse(self):
for row in self.ledgers:
row.warehouse = self.warehouse
def set_total_qty(self):
self.total_qty = sum([row.qty for row in self.ledgers])
def set_avg_rate(self):
self.total_amount = 0.0
for row in self.ledgers:
rate = flt(row.incoming_rate) or flt(row.outgoing_rate)
self.total_amount += flt(row.qty) * rate
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
if not (self.voucher_type and self.voucher_no):
return False
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
return frappe.get_cached_value(self.voucher_type, self.voucher_no, "is_return")
elif self.voucher_type in ["Sales Invoice", "Delivery Note"]:
return not frappe.get_cached_value(self.voucher_type, self.voucher_no, "is_return")
elif self.voucher_type == "Stock Entry":
return frappe.get_cached_value(self.voucher_type, self.voucher_no, "purpose") in [
"Material Receipt"
]
def validate_serial_and_batch_no(self):
if self.item_code and not self.has_serial_no and not self.has_batch_no:
msg = f"The Item {self.item_code} does not have Serial No or Batch No"
frappe.throw(_(msg))
def validate_duplicate_serial_and_batch_no(self):
serial_nos = []
batch_nos = []
for row in self.ledgers:
if row.serial_no:
serial_nos.append(row.serial_no)
if row.batch_no:
batch_nos.append(row.batch_no)
if serial_nos:
for key, value in collections.Counter(serial_nos).items():
if value > 1:
frappe.throw(_(f"Duplicate Serial No {key} found"))
if batch_nos:
for key, value in collections.Counter(batch_nos).items():
if value > 1:
frappe.throw(_(f"Duplicate Batch No {key} found"))
def before_cancel(self):
self.delink_serial_and_batch_bundle()
self.clear_table()
@ -30,6 +124,35 @@ class SerialandBatchBundle(Document):
def clear_table(self):
self.set("ledgers", [])
def delink_refernce_from_voucher(self):
child_table = f"{self.voucher_type} Item"
if self.voucher_type == "Stock Entry":
child_table = f"{self.voucher_type} Detail"
vouchers = frappe.get_all(
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)
def delink_reference_from_batch(self):
batches = frappe.get_all(
"Batch",
fields=["name"],
filters={"reference_name": self.name, "reference_doctype": "Serial and Batch Bundle"},
)
for batch in batches:
frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
def on_trash(self):
self.delink_refernce_from_voucher()
self.delink_reference_from_batch()
self.clear_table()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@ -125,3 +248,144 @@ def update_serial_batch_no_ledgers(ledgers, child_row) -> object:
frappe.msgprint(_("Serial and Batch Bundle updated"), alert=True)
return doc
def get_serial_and_batch_ledger(**kwargs):
kwargs = frappe._dict(kwargs)
sle_table = frappe.qb.DocType("Stock Ledger Entry")
serial_batch_table = frappe.qb.DocType("Serial and Batch Ledger")
query = (
frappe.qb.from_(sle_table)
.inner_join(serial_batch_table)
.on(sle_table.serial_and_batch_bundle == serial_batch_table.parent)
.select(
serial_batch_table.serial_no,
serial_batch_table.warehouse,
serial_batch_table.batch_no,
serial_batch_table.qty,
serial_batch_table.incoming_rate,
)
.where((sle_table.item_code == kwargs.item_code) & (sle_table.warehouse == kwargs.warehouse))
)
if kwargs.serial_nos:
query = query.where(serial_batch_table.serial_no.isin(kwargs.serial_nos))
if kwargs.batch_nos:
query = query.where(serial_batch_table.batch_no.isin(kwargs.batch_nos))
if kwargs.fetch_incoming_rate:
query = query.where(sle_table.actual_qty > 0)
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)
elif cint(kwargs.has_batch_no):
return get_auto_batch_nos(kwargs)
def get_auto_serial_nos(kwargs):
fields = ["name as serial_no"]
if kwargs.has_batch_no:
fields.append("batch_no")
order_by = "creation"
if kwargs.based_on == "LIFO":
order_by = "creation desc"
elif kwargs.based_on == "Expiry":
order_by = "amc_expiry_date asc"
return frappe.get_all(
"Serial No",
fields=fields,
filters={"item_code": kwargs.item_code, "warehouse": kwargs.warehouse},
limit=cint(kwargs.qty),
order_by=order_by,
)
def get_auto_batch_nos(kwargs):
available_batches = get_available_batches(kwargs)
qty = flt(kwargs.qty)
batches = []
for batch in available_batches:
if qty > 0:
batch_qty = flt(batch.qty)
if qty > batch_qty:
batches.append(
{
"batch_no": batch.batch_no,
"qty": batch_qty,
}
)
qty -= batch_qty
else:
batches.append(
{
"batch_no": batch.batch_no,
"qty": qty,
}
)
qty = 0
return batches
def get_available_batches(kwargs):
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
batch_ledger = frappe.qb.DocType("Serial and Batch Ledger")
batch_table = frappe.qb.DocType("Batch")
query = (
frappe.qb.from_(stock_ledger_entry)
.inner_join(batch_ledger)
.on(stock_ledger_entry.serial_and_batch_bundle == batch_ledger.parent)
.inner_join(batch_table)
.on(batch_ledger.batch_no == batch_table.name)
.select(
batch_ledger.batch_no,
Sum(
Case().when(stock_ledger_entry.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)
).as_("qty"),
)
.where(
(stock_ledger_entry.item_code == kwargs.item_code)
& (stock_ledger_entry.warehouse == kwargs.warehouse)
& ((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
)
.groupby(batch_ledger.batch_no)
)
if kwargs.based_on == "LIFO":
query = query.orderby(batch_table.creation, order=frappe.qb.desc)
elif kwargs.based_on == "Expiry":
query = query.orderby(batch_table.expiry_date)
else:
query = query.orderby(batch_table.creation)
data = query.run(as_dict=True)
data = list(filter(lambda x: x.qty > 0, data))
return data

View File

@ -5,12 +5,17 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"serial_no",
"batch_no",
"column_break_2",
"qty",
"warehouse",
"is_rejected"
"section_break_6",
"incoming_rate",
"column_break_8",
"outgoing_rate",
"stock_value_difference"
],
"fields": [
{
@ -34,6 +39,7 @@
"options": "Batch"
},
{
"default": "1",
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
@ -46,22 +52,53 @@
"label": "Warehouse",
"options": "Warehouse"
},
{
"default": "0",
"depends_on": "eval:parent.voucher_type == 'Purchase Receipt'",
"fieldname": "is_rejected",
"fieldtype": "Check",
"label": "Is Rejected"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"label": "Rate Section"
},
{
"fieldname": "incoming_rate",
"fieldtype": "Float",
"label": "Incoming Rate",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "outgoing_rate",
"fieldtype": "Float",
"label": "Outgoing Rate",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item Code",
"options": "Item",
"read_only": 1
},
{
"fieldname": "stock_value_difference",
"fieldtype": "Float",
"label": "Change in Stock Value",
"no_copy": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-11-24 13:00:23.598351",
"modified": "2023-01-10 12:55:57.368650",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Ledger",

View File

@ -12,24 +12,13 @@
"column_break0",
"serial_no",
"item_code",
"warehouse",
"batch_no",
"warehouse",
"column_break1",
"item_name",
"description",
"item_group",
"brand",
"sales_order",
"purchase_details",
"column_break2",
"purchase_document_type",
"purchase_document_no",
"purchase_date",
"purchase_time",
"purchase_rate",
"column_break3",
"supplier",
"supplier_name",
"asset_details",
"asset",
"asset_status",
@ -38,14 +27,6 @@
"employee",
"delivery_details",
"delivery_document_type",
"delivery_document_no",
"delivery_date",
"delivery_time",
"column_break5",
"customer",
"customer_name",
"invoice_details",
"sales_invoice",
"warranty_amc_details",
"column_break6",
"warranty_expiry_date",
@ -56,7 +37,6 @@
"more_info",
"serial_no_details",
"company",
"status",
"work_order"
],
"fields": [
@ -90,29 +70,6 @@
"options": "Item",
"reqd": 1
},
{
"description": "Warehouse can only be changed via Stock Entry / Delivery Note / Purchase Receipt",
"fieldname": "warehouse",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Warehouse",
"no_copy": 1,
"oldfieldname": "warehouse",
"oldfieldtype": "Link",
"options": "Warehouse",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Batch No",
"options": "Batch",
"read_only": 1
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break"
@ -150,84 +107,6 @@
"options": "Brand",
"read_only": 1
},
{
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"options": "Sales Order"
},
{
"fieldname": "purchase_details",
"fieldtype": "Section Break",
"label": "Purchase / Manufacture Details"
},
{
"fieldname": "column_break2",
"fieldtype": "Column Break",
"width": "50%"
},
{
"fieldname": "purchase_document_type",
"fieldtype": "Link",
"label": "Creation Document Type",
"no_copy": 1,
"options": "DocType",
"read_only": 1
},
{
"fieldname": "purchase_document_no",
"fieldtype": "Dynamic Link",
"label": "Creation Document No",
"no_copy": 1,
"options": "purchase_document_type",
"read_only": 1
},
{
"fieldname": "purchase_date",
"fieldtype": "Date",
"label": "Creation Date",
"no_copy": 1,
"oldfieldname": "purchase_date",
"oldfieldtype": "Date",
"read_only": 1
},
{
"fieldname": "purchase_time",
"fieldtype": "Time",
"label": "Creation Time",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "purchase_rate",
"fieldtype": "Currency",
"label": "Incoming Rate",
"no_copy": 1,
"oldfieldname": "purchase_rate",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break3",
"fieldtype": "Column Break",
"width": "50%"
},
{
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
"no_copy": 1,
"options": "Supplier"
},
{
"bold": 1,
"fieldname": "supplier_name",
"fieldtype": "Data",
"label": "Supplier Name",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "asset_details",
"fieldtype": "Section Break",
@ -283,67 +162,6 @@
"options": "DocType",
"read_only": 1
},
{
"fieldname": "delivery_document_no",
"fieldtype": "Dynamic Link",
"label": "Delivery Document No",
"no_copy": 1,
"options": "delivery_document_type",
"read_only": 1
},
{
"fieldname": "delivery_date",
"fieldtype": "Date",
"label": "Delivery Date",
"no_copy": 1,
"oldfieldname": "delivery_date",
"oldfieldtype": "Date",
"read_only": 1
},
{
"fieldname": "delivery_time",
"fieldtype": "Time",
"label": "Delivery Time",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break5",
"fieldtype": "Column Break",
"width": "50%"
},
{
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"no_copy": 1,
"oldfieldname": "customer",
"oldfieldtype": "Link",
"options": "Customer",
"print_hide": 1
},
{
"bold": 1,
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"no_copy": 1,
"oldfieldname": "customer_name",
"oldfieldtype": "Data",
"read_only": 1
},
{
"fieldname": "invoice_details",
"fieldtype": "Section Break",
"label": "Invoice Details"
},
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"label": "Sales Invoice",
"options": "Sales Invoice",
"read_only": 1
},
{
"fieldname": "warranty_amc_details",
"fieldtype": "Section Break",
@ -408,6 +226,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"remember_last_selected_value": 1,
@ -415,25 +234,30 @@
"search_index": 1,
"set_only_once": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"options": "\nActive\nInactive\nDelivered\nExpired",
"read_only": 1
},
{
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
"options": "Work Order"
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
"read_only": 1
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
}
],
"icon": "fa fa-barcode",
"idx": 1,
"links": [],
"modified": "2023-04-14 15:58:46.139887",
"modified": "2023-04-15 15:58:46.139887",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No",

View File

@ -9,17 +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 (
add_days,
cint,
cstr,
flt,
get_link_to_form,
getdate,
now,
nowdate,
safe_json_loads,
)
from frappe.utils import cint, 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
@ -80,19 +70,6 @@ class SerialNo(StockController):
)
self.set_maintenance_status()
self.validate_warehouse()
self.validate_item()
self.set_status()
def set_status(self):
if self.delivery_document_type:
self.status = "Delivered"
elif self.warranty_expiry_date and getdate(self.warranty_expiry_date) <= getdate(nowdate()):
self.status = "Expired"
elif not self.warehouse:
self.status = "Inactive"
else:
self.status = "Active"
def set_maintenance_status(self):
if not self.warranty_expiry_date and not self.amc_expiry_date:
@ -110,127 +87,6 @@ class SerialNo(StockController):
if self.warranty_expiry_date and getdate(self.warranty_expiry_date) >= getdate(nowdate()):
self.maintenance_status = "Under Warranty"
def validate_warehouse(self):
if not self.get("__islocal"):
item_code, warehouse = frappe.db.get_value("Serial No", self.name, ["item_code", "warehouse"])
if not self.via_stock_ledger and item_code != self.item_code:
frappe.throw(_("Item Code cannot be changed for Serial No."), SerialNoCannotCannotChangeError)
if not self.via_stock_ledger and warehouse != self.warehouse:
frappe.throw(_("Warehouse cannot be changed for Serial No."), SerialNoCannotCannotChangeError)
def validate_item(self):
"""
Validate whether serial no is required for this item
"""
item = frappe.get_cached_doc("Item", self.item_code)
if item.has_serial_no != 1:
frappe.throw(
_("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code)
)
self.item_group = item.item_group
self.description = item.description
self.item_name = item.item_name
self.brand = item.brand
self.warranty_period = item.warranty_period
def set_purchase_details(self, purchase_sle):
if purchase_sle:
self.purchase_document_type = purchase_sle.voucher_type
self.purchase_document_no = purchase_sle.voucher_no
self.purchase_date = purchase_sle.posting_date
self.purchase_time = purchase_sle.posting_time
self.purchase_rate = purchase_sle.incoming_rate
if purchase_sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
self.supplier, self.supplier_name = frappe.db.get_value(
purchase_sle.voucher_type, purchase_sle.voucher_no, ["supplier", "supplier_name"]
)
# If sales return entry
if self.purchase_document_type == "Delivery Note":
self.sales_invoice = None
else:
for fieldname in (
"purchase_document_type",
"purchase_document_no",
"purchase_date",
"purchase_time",
"purchase_rate",
"supplier",
"supplier_name",
):
self.set(fieldname, None)
def set_sales_details(self, delivery_sle):
if delivery_sle:
self.delivery_document_type = delivery_sle.voucher_type
self.delivery_document_no = delivery_sle.voucher_no
self.delivery_date = delivery_sle.posting_date
self.delivery_time = delivery_sle.posting_time
if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"):
self.customer, self.customer_name = frappe.db.get_value(
delivery_sle.voucher_type, delivery_sle.voucher_no, ["customer", "customer_name"]
)
if self.warranty_period:
self.warranty_expiry_date = add_days(
cstr(delivery_sle.posting_date), cint(self.warranty_period)
)
else:
for fieldname in (
"delivery_document_type",
"delivery_document_no",
"delivery_date",
"delivery_time",
"customer",
"customer_name",
"warranty_expiry_date",
):
self.set(fieldname, None)
def get_last_sle(self, serial_no=None):
entries = {}
sle_dict = self.get_stock_ledger_entries(serial_no)
if sle_dict:
if sle_dict.get("incoming", []):
entries["purchase_sle"] = sle_dict["incoming"][0]
if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0:
entries["last_sle"] = sle_dict["incoming"][0]
else:
entries["last_sle"] = sle_dict["outgoing"][0]
entries["delivery_sle"] = sle_dict["outgoing"][0]
return entries
def get_stock_ledger_entries(self, serial_no=None):
sle_dict = {}
if not serial_no:
serial_no = self.name
print("serial_no", serial_no)
for sle in frappe.db.sql(
"""
SELECT sle.voucher_type, sle.voucher_no, serial_and_batch_bundle,
sle.posting_date, sle.posting_time, sle.incoming_rate, sle.actual_qty, snb.serial_no
FROM
`tabStock Ledger Entry` sle, `tabSerial and Batch Ledger` snb
WHERE
sle.item_code=%s AND sle.company = %s
AND sle.is_cancelled = 0
AND snb.serial_no = %s and snb.parent = sle.serial_and_batch_bundle
ORDER BY
sle.posting_date desc, sle.posting_time desc, sle.creation desc""",
(self.item_code, self.company, serial_no),
as_dict=1,
):
if serial_no.upper() in get_serial_nos(sle.serial_and_batch_bundle):
if cint(sle.actual_qty) > 0:
sle_dict.setdefault("incoming", []).append(sle)
else:
sle_dict.setdefault("outgoing", []).append(sle)
return sle_dict
def on_trash(self):
sl_entries = frappe.db.sql(
"""select serial_no from `tabStock Ledger Entry`
@ -251,19 +107,11 @@ class SerialNo(StockController):
_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name)
)
def update_serial_no_reference(self, serial_no=None):
last_sle = self.get_last_sle(serial_no)
print(last_sle)
self.set_purchase_details(last_sle.get("purchase_sle"))
self.set_sales_details(last_sle.get("delivery_sle"))
self.set_maintenance_status()
self.set_status()
def process_serial_no(sle):
item_det = get_item_details(sle.item_code)
validate_serial_no(sle, item_det)
update_serial_nos(sle, item_det)
create_serial_nos(sle, item_det)
def validate_serial_no(sle, item_det):
@ -277,6 +125,7 @@ def validate_serial_no(sle, item_det):
SerialNoNotRequiredError,
)
elif not sle.is_cancelled:
return
if serial_nos:
if cint(sle.actual_qty) != flt(sle.actual_qty):
frappe.throw(
@ -440,6 +289,7 @@ def validate_serial_no(sle, item_det):
_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError
)
elif serial_nos:
return
# SLE is being cancelled and has serial nos
for serial_no in serial_nos:
check_serial_no_validity_on_cancel(serial_no, sle)
@ -528,7 +378,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle):
return allow_serial_nos
def update_serial_nos(sle, item_det):
def create_serial_nos(sle, item_det):
if sle.skip_update_serial_no:
return
if (
@ -538,7 +388,7 @@ def update_serial_nos(sle, item_det):
and item_det.has_serial_no == 1
and item_det.serial_no_series
):
bundle = make_serial_bundle(sle, item_det)
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"
@ -552,64 +402,127 @@ def update_serial_nos(sle, item_det):
)
elif sle.serial_and_batch_bundle:
auto_make_serial_nos(sle)
def make_serial_bundle(sle, item_details):
sr_nos = auto_create_serial_nos(sle, item_details)
if 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.qty = sle.actual_qty
sn_doc.insert()
batch_no = ""
if item_details.has_batch_no:
batch_no = create_batch_for_serial_no(sle)
ledgers = []
fields = [
"name",
"serial_no",
"batch_no",
"warehouse",
"qty",
"parent",
"parenttype",
"parentfield",
]
for serial_no in sr_nos:
ledgers.append(
(
frappe.generate_hash("", 10),
serial_no,
batch_no,
sle.warehouse,
1,
sn_doc.name,
sn_doc.doctype,
"ledgers",
)
if sle.is_cancelled:
frappe.db.set_value(
"Serial and Batch Bundle",
sle.serial_and_batch_bundle,
"is_cancelled",
1,
)
frappe.db.bulk_insert(
"Serial and Batch Ledger",
fields=fields,
values=set(ledgers),
ignore_duplicates=True,
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)
if not serial_no_data:
for serial_no in serial_nos:
frappe.db.set_value("Serial No", serial_no, "warehouse", None)
else:
for row in serial_no_data:
if not row.serial_no:
continue
warehouse = row.warehouse if row.actual_qty > 0 else None
frappe.db.set_value("Serial No", row.serial_no, "warehouse", warehouse)
def get_serial_nos_warehouse(item_code, serial_nos):
ledger_table = frappe.qb.DocType("Serial and Batch Ledger")
sle_table = frappe.qb.DocType("Stock Ledger Entry")
return (
frappe.qb.from_(ledger_table)
.inner_join(sle_table)
.on(ledger_table.parent == sle_table.serial_and_batch_bundle)
.select(
ledger_table.serial_no,
sle_table.actual_qty,
ledger_table.warehouse,
)
.where(
(ledger_table.serial_no.isin(serial_nos))
& (sle_table.is_cancelled == 0)
& (sle_table.item_code == item_code)
& (sle_table.serial_and_batch_bundle.isnotnull())
)
.orderby(sle_table.posting_date, order=frappe.qb.desc)
.orderby(sle_table.posting_time, order=frappe.qb.desc)
.orderby(sle_table.creation, order=frappe.qb.desc)
.groupby(ledger_table.serial_no)
).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",
)
)
sn_doc.load_from_db()
return sn_doc.submit()
frappe.db.bulk_insert("Serial and Batch Ledger", fields=fields, values=set(ledgers))
def create_batch_for_serial_no(sle):
@ -629,6 +542,10 @@ def create_batch_for_serial_no(sle):
def auto_create_serial_nos(sle, item_details) -> List[str]:
sr_nos = []
serial_nos_details = []
current_series = frappe.db.sql(
"select current from `tabSeries` where name = %s", item_details.serial_no_series
)
for i in range(cint(sle.actual_qty)):
serial_no = make_autoname(item_details.serial_no_series, "Serial No")
sr_nos.append(serial_no)
@ -640,13 +557,8 @@ def auto_create_serial_nos(sle, item_details) -> List[str]:
now(),
frappe.session.user,
frappe.session.user,
sle.voucher_type,
sle.voucher_no,
sle.warehouse,
sle.company,
sle.posting_date,
sle.posting_time,
sle.incoming_rate,
sle.item_code,
item_details.item_name,
item_details.description,
@ -661,24 +573,14 @@ def auto_create_serial_nos(sle, item_details) -> List[str]:
"modified",
"owner",
"modified_by",
"purchase_document_type",
"purchase_document_no",
"warehouse",
"company",
"purchase_date",
"purchase_time",
"purchase_rate",
"item_code",
"item_name",
"description",
]
frappe.db.bulk_insert(
"Serial No",
fields=fields,
values=set(serial_nos_details),
ignore_duplicates=True,
)
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
return sr_nos
@ -698,41 +600,6 @@ def get_new_serial_number(series):
return sr_no
def auto_make_serial_nos(args):
serial_nos = get_serial_nos(args.get("serial_and_batch_bundle"))
created_numbers = []
voucher_type = args.get("voucher_type")
item_code = args.get("item_code")
for serial_no in serial_nos:
is_new = False
if frappe.db.exists("Serial No", serial_no):
sr = frappe.get_cached_doc("Serial No", serial_no)
elif args.get("actual_qty", 0) > 0:
sr = frappe.new_doc("Serial No")
is_new = True
sr = update_args_for_serial_no(sr, serial_no, args, is_new=is_new)
if is_new:
created_numbers.append(sr.name)
form_links = list(map(lambda d: get_link_to_form("Serial No", d), created_numbers))
# Setting up tranlated title field for all cases
singular_title = _("Serial Number Created")
multiple_title = _("Serial Numbers Created")
if voucher_type:
multiple_title = singular_title = _("{0} Created").format(voucher_type)
if len(form_links) == 1:
frappe.msgprint(_("Serial No {0} Created").format(form_links[0]), singular_title)
elif len(form_links) > 0:
message = _("The following serial numbers were created: <br><br> {0}").format(
get_items_html(form_links, item_code)
)
frappe.msgprint(message, multiple_title)
def get_items_html(serial_nos, item_code):
body = ", ".join(serial_nos)
return """<details><summary>
@ -773,36 +640,8 @@ def clean_serial_no_string(serial_no: str) -> str:
return "\n".join(serial_no_list)
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
if args.get(field):
serial_no_doc.set(field, args.get(field))
serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True
serial_no_doc.warehouse = args.get("warehouse") if args.get("actual_qty", 0) > 0 else None
if is_new:
serial_no_doc.serial_no = serial_no
if (
serial_no_doc.sales_order
and args.get("voucher_type") == "Stock Entry"
and not args.get("actual_qty", 0) > 0
):
serial_no_doc.sales_order = None
serial_no_doc.validate_item()
serial_no_doc.update_serial_no_reference(serial_no)
if is_new:
serial_no_doc.db_insert()
else:
serial_no_doc.db_update()
return serial_no_doc
def update_serial_nos_after_submit(controller, parentfield):
return
stock_ledger_entries = frappe.db.sql(
"""select voucher_detail_no, serial_no, actual_qty, warehouse
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""",

View File

@ -1,14 +0,0 @@
frappe.listview_settings['Serial No'] = {
add_fields: ["item_code", "warehouse", "warranty_expiry_date", "delivery_document_type"],
get_indicator: (doc) => {
if (doc.delivery_document_type) {
return [__("Delivered"), "green", "delivery_document_type,is,set"];
} else if (doc.warranty_expiry_date && frappe.datetime.get_diff(doc.warranty_expiry_date, frappe.datetime.nowdate()) <= 0) {
return [__("Expired"), "red", "warranty_expiry_date,not in,|warranty_expiry_date,<=,Today|delivery_document_type,is,not set"];
} else if (!doc.warehouse) {
return [__("Inactive"), "grey", "warehouse,is,not set"];
} else {
return [__("Active"), "green", "delivery_document_type,is,not set"];
}
}
};

View File

@ -29,6 +29,9 @@ 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,
@ -232,7 +235,12 @@ class StockEntry(StockController):
self.update_work_order()
self.update_stock_ledger()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Serial and Batch Bundle",
)
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
@ -1208,6 +1216,11 @@ 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,
{

View File

@ -32,9 +32,11 @@
"stock_uom",
"project",
"serial_and_batch_bundle",
"has_batch_no",
"batch_no",
"column_break_26",
"fiscal_year",
"has_serial_no",
"serial_no",
"is_cancelled",
"to_rename"
@ -317,6 +319,20 @@
"label": "Serial and Batch Bundle",
"options": "Serial and Batch Bundle",
"search_index": 1
},
{
"default": "0",
"fetch_from": "item_code.has_batch_no",
"fieldname": "has_batch_no",
"fieldtype": "Check",
"label": "Has Batch No"
},
{
"default": "0",
"fetch_from": "item_code.has_serial_no",
"fieldname": "has_serial_no",
"fieldtype": "Check",
"label": "Has Serial No"
}
],
"hide_toolbar": 1,
@ -325,7 +341,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-24 13:14:31.974743",
"modified": "2022-12-28 14:50:56.359348",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",

View File

@ -8,7 +8,7 @@ import frappe
from frappe import _
from frappe.core.doctype.role.role import get_users
from frappe.model.document import Document
from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
from frappe.utils import add_days, cint, formatdate, get_datetime, get_link_to_form, getdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@ -47,6 +47,7 @@ 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()
@ -103,15 +104,20 @@ 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}"))
elif self.item_code != frappe.get_cached_value(
"Serial and Batch Bundle", self.serial_and_batch_bundle, "item_code"
):
frappe.throw(
_(
f"Serial No and Batch No Bundle {self.serial_and_batch_bundle} is not 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"))
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}"))
@ -211,6 +217,36 @@ 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.")

View File

@ -60,8 +60,13 @@ class StockReconciliation(StockController):
update_serial_nos_after_submit(self, "items")
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.validate_reserved_stock()
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Serial and Batch Bundle",
)
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()

View File

@ -19,7 +19,6 @@ from erpnext.accounts.doctype.pricing_rule.pricing_rule import (
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.setup.utils import get_exchange_rate
from erpnext.stock.doctype.batch.batch import get_batch_no
from erpnext.stock.doctype.item.item import get_item_defaults, get_uom_conv_factor
from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_manufacturer_part_no
from erpnext.stock.doctype.price_list.price_list import get_price_list_details
@ -160,13 +159,6 @@ def update_stock(args, out):
and out.warehouse
and out.stock_qty > 0
):
if out.has_batch_no and not args.get("batch_no"):
out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty)
actual_batch_qty = get_batch_qty(out.batch_no, out.warehouse, out.item_code)
if actual_batch_qty:
out.update(actual_batch_qty)
if out.has_serial_no and args.get("batch_no"):
reserved_so = get_so_reservation_for_item(args)
out.batch_no = args.get("batch_no")

View File

@ -295,19 +295,3 @@ def set_stock_balance_as_per_serial_no(
"posting_time": posting_time,
}
)
def reset_serial_no_status_and_warehouse(serial_nos=None):
if not serial_nos:
serial_nos = frappe.db.sql_list("""select name from `tabSerial No` where docstatus = 0""")
for serial_no in serial_nos:
try:
sr = frappe.get_doc("Serial No", serial_no)
last_sle = sr.get_last_sle()
if flt(last_sle.actual_qty) > 0:
sr.warehouse = last_sle.warehouse
sr.via_stock_ledger = True
sr.save()
except Exception:
pass

View File

@ -69,6 +69,9 @@ 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"))
@ -104,6 +107,18 @@ 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"):
@ -659,8 +674,6 @@ class update_entries_after(object):
self.new_items_found = True
def process_sle(self, sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
# previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse]
self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
@ -692,7 +705,7 @@ class update_entries_after(object):
):
sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
if get_serial_nos(sle.serial_no):
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":
@ -701,9 +714,7 @@ class update_entries_after(object):
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
self.wh_data.valuation_rate
)
elif sle.batch_no and frappe.db.get_value(
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
):
elif sle.serial_and_batch_bundle and sle.has_batch_no:
self.update_batched_values(sle)
else:
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
@ -963,9 +974,22 @@ class update_entries_after(object):
item.db_update()
def get_serialized_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
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)
serial_nos = cstr(sle.serial_no).split("\n")
incoming_rate = flt(ledger.avg_rate)
if incoming_rate < 0:
# wrong incoming rate
@ -977,11 +1001,11 @@ class update_entries_after(object):
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:
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
stock_value_change = outgoing_value
new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
@ -1138,7 +1162,7 @@ class update_entries_after(object):
outgoing_rate = get_batch_incoming_rate(
item_code=sle.item_code,
warehouse=sle.warehouse,
batch_no=sle.batch_no,
serial_and_batch_bundle=sle.serial_and_batch_bundle,
posting_date=sle.posting_date,
posting_time=sle.posting_time,
creation=sle.creation,
@ -1402,10 +1426,11 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
def get_batch_incoming_rate(
item_code, warehouse, batch_no, posting_date, posting_time, creation=None
item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
):
sle = frappe.qb.DocType("Stock Ledger Entry")
batch_ledger = frappe.qb.DocType("Serial and Batch Ledger")
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
posting_date, posting_time
@ -1416,18 +1441,36 @@ def get_batch_incoming_rate(
== CombineDatetime(posting_date, posting_time)
) & (sle.creation < creation)
batches = frappe.get_all(
"Serial and Batch Ledger", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
)
batch_details = (
frappe.qb.from_(sle)
.select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
.inner_join(batch_ledger)
.on(sle.serial_and_batch_bundle == batch_ledger.parent)
.select(
Sum(
Case()
.when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
.else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
).as_("batch_value"),
Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
"batch_qty"
),
)
.where(
(sle.item_code == item_code)
& (sle.warehouse == warehouse)
& (sle.batch_no == batch_no)
& (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
& (sle.is_cancelled == 0)
)
.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