fix: removed sales and purchase fields from serial nos
This commit is contained in:
parent
ba1aac1613
commit
6c9b212dd1
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -354,6 +354,7 @@ class StockController(AccountsController):
|
||||
"batch_no": batch_no,
|
||||
"qty": d.qty,
|
||||
"warehouse": d.get(warehouse_field),
|
||||
"incoming_rate": d.rate,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
})
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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""",
|
||||
|
@ -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"];
|
||||
}
|
||||
}
|
||||
};
|
@ -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,
|
||||
{
|
||||
|
@ -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",
|
||||
|
@ -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.")
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user