mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-05 05:09:11 +00:00
fix: stock reco test case for serial and batch bundle
This commit is contained in:
@@ -88,7 +88,6 @@ class TestAssetRepair(unittest.TestCase):
|
|||||||
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
|
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
|
||||||
|
|
||||||
def test_serialized_item_consumption(self):
|
def test_serialized_item_consumption(self):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError
|
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||||
|
|
||||||
stock_entry = make_serialized_item()
|
stock_entry = make_serialized_item()
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
fieldtype: 'Data',
|
fieldtype: 'Data',
|
||||||
fieldname: 'scan_serial_no',
|
fieldname: 'scan_serial_no',
|
||||||
label: __('Scan Serial No'),
|
label: __('Scan Serial No'),
|
||||||
options: 'Serial No',
|
|
||||||
get_query: () => {
|
get_query: () => {
|
||||||
return {
|
return {
|
||||||
filters: this.get_serial_no_filters()
|
filters: this.get_serial_no_filters()
|
||||||
@@ -71,10 +70,9 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
|
|
||||||
if (this.item.has_batch_no) {
|
if (this.item.has_batch_no) {
|
||||||
fields.push({
|
fields.push({
|
||||||
fieldtype: 'Link',
|
fieldtype: 'Data',
|
||||||
fieldname: 'scan_batch_no',
|
fieldname: 'scan_batch_no',
|
||||||
label: __('Scan Batch No'),
|
label: __('Scan Batch No'),
|
||||||
options: 'Batch',
|
|
||||||
get_query: () => {
|
get_query: () => {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -104,6 +102,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
|
|
||||||
if (this.item?.outward) {
|
if (this.item?.outward) {
|
||||||
fields = [...this.get_filter_fields(), ...fields];
|
fields = [...this.get_filter_fields(), ...fields];
|
||||||
|
} else {
|
||||||
|
fields = [...fields, ...this.get_attach_field()];
|
||||||
}
|
}
|
||||||
|
|
||||||
fields.push({
|
fields.push({
|
||||||
@@ -121,6 +121,73 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_attach_field() {
|
||||||
|
let label = this.item?.has_serial_no ? __('Serial Nos') : __('Batch Nos');
|
||||||
|
let primary_label = this.bundle
|
||||||
|
? __('Update') : __('Add');
|
||||||
|
|
||||||
|
if (this.item?.has_serial_no && this.item?.has_batch_no) {
|
||||||
|
label = __('Serial Nos / Batch Nos');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldtype: 'Section Break',
|
||||||
|
label: __('{0} {1} via CSV File', [primary_label, label])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: 'Button',
|
||||||
|
fieldname: 'download_csv',
|
||||||
|
label: __('Download CSV Template'),
|
||||||
|
click: () => this.download_csv_file()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: 'Column Break',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: 'Attach',
|
||||||
|
fieldname: 'attach_serial_batch_csv',
|
||||||
|
label: __('Attach CSV File'),
|
||||||
|
onchange: () => this.upload_csv_file()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
download_csv_file() {
|
||||||
|
let csvFileData = ['Serial No'];
|
||||||
|
|
||||||
|
if (this.item.has_serial_no && this.item.has_batch_no) {
|
||||||
|
csvFileData = ['Serial No', 'Batch No', 'Quantity'];
|
||||||
|
} else if (this.item.has_batch_no) {
|
||||||
|
csvFileData = ['Batch No', 'Quantity'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = `/api/method/erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.download_blank_csv_template?content=${encodeURIComponent(JSON.stringify(csvFileData))}`;
|
||||||
|
const w = window.open(frappe.urllib.get_full_url(method));
|
||||||
|
if (!w) {
|
||||||
|
frappe.msgprint(__("Please enable pop-ups"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_csv_file() {
|
||||||
|
const file_path = this.dialog.get_value("attach_serial_batch_csv")
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.upload_csv_file',
|
||||||
|
args: {
|
||||||
|
item_code: this.item.item_code,
|
||||||
|
file_path: file_path
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
if (r.message.serial_nos && r.message.serial_nos.length) {
|
||||||
|
this.set_data(r.message.serial_nos);
|
||||||
|
} else if (r.message.batch_nos && r.message.batch_nos.length) {
|
||||||
|
this.set_data(r.message.batch_nos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get_filter_fields() {
|
get_filter_fields() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -213,10 +280,6 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
get_auto_data() {
|
get_auto_data() {
|
||||||
const { qty, based_on } = this.dialog.get_values();
|
const { qty, based_on } = this.dialog.get_values();
|
||||||
|
|
||||||
if (!qty) {
|
|
||||||
frappe.throw(__('Please enter Qty to Fetch'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!based_on) {
|
if (!based_on) {
|
||||||
based_on = 'FIFO';
|
based_on = 'FIFO';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,12 @@ class Batch(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_batch_qty(
|
def get_batch_qty(
|
||||||
batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None
|
batch_no=None,
|
||||||
|
warehouse=None,
|
||||||
|
item_code=None,
|
||||||
|
posting_date=None,
|
||||||
|
posting_time=None,
|
||||||
|
ignore_voucher_nos=None,
|
||||||
):
|
):
|
||||||
"""Returns batch actual qty if warehouse is passed,
|
"""Returns batch actual qty if warehouse is passed,
|
||||||
or returns dict of qty by warehouse if warehouse is None
|
or returns dict of qty by warehouse if warehouse is None
|
||||||
@@ -191,6 +196,7 @@ def get_batch_qty(
|
|||||||
"posting_date": posting_date,
|
"posting_date": posting_date,
|
||||||
"posting_time": posting_time,
|
"posting_time": posting_time,
|
||||||
"batch_no": batch_no,
|
"batch_no": batch_no,
|
||||||
|
"ignore_voucher_nos": ignore_voucher_nos,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from frappe.utils import (
|
|||||||
parse_json,
|
parse_json,
|
||||||
today,
|
today,
|
||||||
)
|
)
|
||||||
|
from frappe.utils.csvutils import build_csv_response
|
||||||
|
|
||||||
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
|
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
|
||||||
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
||||||
@@ -152,15 +153,15 @@ class SerialandBatchBundle(Document):
|
|||||||
if self.has_serial_no:
|
if self.has_serial_no:
|
||||||
sn_obj = SerialNoValuation(
|
sn_obj = SerialNoValuation(
|
||||||
sle=sle,
|
sle=sle,
|
||||||
warehouse=self.item_code,
|
item_code=self.item_code,
|
||||||
item_code=self.warehouse,
|
warehouse=self.warehouse,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
sn_obj = BatchNoValuation(
|
sn_obj = BatchNoValuation(
|
||||||
sle=sle,
|
sle=sle,
|
||||||
warehouse=self.item_code,
|
item_code=self.item_code,
|
||||||
item_code=self.warehouse,
|
warehouse=self.warehouse,
|
||||||
)
|
)
|
||||||
|
|
||||||
for d in self.entries:
|
for d in self.entries:
|
||||||
@@ -657,6 +658,31 @@ class SerialandBatchBundle(Document):
|
|||||||
self.set("entries", batch_nos)
|
self.set("entries", batch_nos)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def download_blank_csv_template(content):
|
||||||
|
csv_data = []
|
||||||
|
if isinstance(content, str):
|
||||||
|
content = parse_json(content)
|
||||||
|
|
||||||
|
csv_data.append(content)
|
||||||
|
csv_data.append([])
|
||||||
|
csv_data.append([])
|
||||||
|
|
||||||
|
filename = "serial_and_batch_bundle"
|
||||||
|
build_csv_response(csv_data, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def upload_csv_file(item_code, file_path):
|
||||||
|
serial_nos, batch_nos = [], []
|
||||||
|
serial_nos, batch_nos = get_serial_batch_from_csv(item_code, file_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"serial_nos": serial_nos,
|
||||||
|
"batch_nos": batch_nos,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_serial_batch_from_csv(item_code, file_path):
|
def get_serial_batch_from_csv(item_code, file_path):
|
||||||
file_path = frappe.get_site_path() + file_path
|
file_path = frappe.get_site_path() + file_path
|
||||||
serial_nos = []
|
serial_nos = []
|
||||||
@@ -669,7 +695,6 @@ def get_serial_batch_from_csv(item_code, file_path):
|
|||||||
if serial_nos:
|
if serial_nos:
|
||||||
make_serial_nos(item_code, serial_nos)
|
make_serial_nos(item_code, serial_nos)
|
||||||
|
|
||||||
print(batch_nos)
|
|
||||||
if batch_nos:
|
if batch_nos:
|
||||||
make_batch_nos(item_code, batch_nos)
|
make_batch_nos(item_code, batch_nos)
|
||||||
|
|
||||||
@@ -938,7 +963,7 @@ def update_serial_batch_no_ledgers(entries, child_row, parent_doc) -> object:
|
|||||||
doc.append(
|
doc.append(
|
||||||
"entries",
|
"entries",
|
||||||
{
|
{
|
||||||
"qty": 1 if doc.type_of_transaction == "Inward" else -1,
|
"qty": d.get("qty") * (1 if doc.type_of_transaction == "Inward" else -1),
|
||||||
"warehouse": d.get("warehouse"),
|
"warehouse": d.get("warehouse"),
|
||||||
"batch_no": d.get("batch_no"),
|
"batch_no": d.get("batch_no"),
|
||||||
"serial_no": d.get("serial_no"),
|
"serial_no": d.get("serial_no"),
|
||||||
@@ -1272,6 +1297,9 @@ def get_available_batches(kwargs):
|
|||||||
else:
|
else:
|
||||||
query = query.orderby(batch_table.creation)
|
query = query.orderby(batch_table.creation)
|
||||||
|
|
||||||
|
if kwargs.get("ignore_voucher_nos"):
|
||||||
|
query = query.where(stock_ledger_entry.voucher_no.notin(kwargs.get("ignore_voucher_nos")))
|
||||||
|
|
||||||
data = query.run(as_dict=True)
|
data = query.run(as_dict=True)
|
||||||
data = list(filter(lambda x: x.qty > 0, data))
|
data = list(filter(lambda x: x.qty > 0, data))
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,41 @@ from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
|
|||||||
|
|
||||||
|
|
||||||
class TestSerialandBatchBundle(FrappeTestCase):
|
class TestSerialandBatchBundle(FrappeTestCase):
|
||||||
pass
|
def test_inward_serial_batch_bundle(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_outward_serial_batch_bundle(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_old_batch_valuation(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_old_batch_batchwise_valuation(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_old_serial_no_valuation(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_batch_not_belong_to_serial_no(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_serial_no_not_exists(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_serial_no_item(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_serial_no_not_required(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_serial_no_required(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_batch_no_not_required(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_batch_no_required(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_batch_from_bundle(bundle):
|
def get_batch_from_bundle(bundle):
|
||||||
|
|||||||
@@ -22,38 +22,10 @@ class SerialNoCannotCannotChangeError(ValidationError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SerialNoNotRequiredError(ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SerialNoRequiredError(ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SerialNoQtyError(ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SerialNoItemError(ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SerialNoWarehouseError(ValidationError):
|
class SerialNoWarehouseError(ValidationError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SerialNoBatchError(ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SerialNoNotExistsError(ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SerialNoDuplicateError(ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SerialNo(StockController):
|
class SerialNo(StockController):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SerialNo, self).__init__(*args, **kwargs)
|
super(SerialNo, self).__init__(*args, **kwargs)
|
||||||
@@ -69,6 +41,15 @@ class SerialNo(StockController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.set_maintenance_status()
|
self.set_maintenance_status()
|
||||||
|
self.validate_warehouse()
|
||||||
|
|
||||||
|
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 set_maintenance_status(self):
|
def set_maintenance_status(self):
|
||||||
if not self.warranty_expiry_date and not self.amc_expiry_date:
|
if not self.warranty_expiry_date and not self.amc_expiry_date:
|
||||||
|
|||||||
@@ -744,8 +744,11 @@ frappe.ui.form.on('Stock Entry Detail', {
|
|||||||
no_batch_serial_number_value = !d.batch_no;
|
no_batch_serial_number_value = !d.batch_no;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog) {
|
if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) {
|
||||||
|
frappe.flags.dialog_set = true;
|
||||||
erpnext.stock.select_batch_and_serial_no(frm, d);
|
erpnext.stock.select_batch_and_serial_no(frm, d);
|
||||||
|
} else {
|
||||||
|
frappe.flags.dialog_set = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,21 +181,25 @@ class StockReconciliation(StockController):
|
|||||||
bundle_doc.flags.ignore_permissions = True
|
bundle_doc.flags.ignore_permissions = True
|
||||||
bundle_doc.save()
|
bundle_doc.save()
|
||||||
item.serial_and_batch_bundle = bundle_doc.name
|
item.serial_and_batch_bundle = bundle_doc.name
|
||||||
elif item.serial_and_batch_bundle:
|
elif item.serial_and_batch_bundle and not item.qty and not item.valuation_rate:
|
||||||
pass
|
bundle_doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||||
|
|
||||||
|
item.qty = bundle_doc.total_qty
|
||||||
|
item.valuation_rate = bundle_doc.avg_rate
|
||||||
|
|
||||||
def remove_items_with_no_change(self):
|
def remove_items_with_no_change(self):
|
||||||
"""Remove items if qty or rate is not changed"""
|
"""Remove items if qty or rate is not changed"""
|
||||||
self.difference_amount = 0.0
|
self.difference_amount = 0.0
|
||||||
|
|
||||||
def _changed(item):
|
def _changed(item):
|
||||||
|
if item.current_serial_and_batch_bundle:
|
||||||
|
self.calculate_difference_amount(item, frappe._dict({}))
|
||||||
|
return True
|
||||||
|
|
||||||
item_dict = get_stock_balance_for(
|
item_dict = get_stock_balance_for(
|
||||||
item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no
|
item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no
|
||||||
)
|
)
|
||||||
|
|
||||||
if item.current_serial_and_batch_bundle:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if (item.qty is None or item.qty == item_dict.get("qty")) and (
|
if (item.qty is None or item.qty == item_dict.get("qty")) and (
|
||||||
item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")
|
item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")
|
||||||
):
|
):
|
||||||
@@ -210,11 +214,7 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
item.current_qty = item_dict.get("qty")
|
item.current_qty = item_dict.get("qty")
|
||||||
item.current_valuation_rate = item_dict.get("rate")
|
item.current_valuation_rate = item_dict.get("rate")
|
||||||
self.difference_amount += flt(item.qty, item.precision("qty")) * flt(
|
self.calculate_difference_amount(item, item_dict)
|
||||||
item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")
|
|
||||||
) - flt(item_dict.get("qty"), item.precision("qty")) * flt(
|
|
||||||
item_dict.get("rate"), item.precision("valuation_rate")
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
items = list(filter(lambda d: _changed(d), self.items))
|
items = list(filter(lambda d: _changed(d), self.items))
|
||||||
@@ -231,6 +231,13 @@ class StockReconciliation(StockController):
|
|||||||
item.idx = i + 1
|
item.idx = i + 1
|
||||||
frappe.msgprint(_("Removed items with no change in quantity or value."))
|
frappe.msgprint(_("Removed items with no change in quantity or value."))
|
||||||
|
|
||||||
|
def calculate_difference_amount(self, item, item_dict):
|
||||||
|
self.difference_amount += flt(item.qty, item.precision("qty")) * flt(
|
||||||
|
item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")
|
||||||
|
) - flt(item_dict.get("qty"), item.precision("qty")) * flt(
|
||||||
|
item_dict.get("rate"), item.precision("valuation_rate")
|
||||||
|
)
|
||||||
|
|
||||||
def validate_data(self):
|
def validate_data(self):
|
||||||
def _get_msg(row_num, msg):
|
def _get_msg(row_num, msg):
|
||||||
return _("Row # {0}:").format(row_num + 1) + " " + msg
|
return _("Row # {0}:").format(row_num + 1) + " " + msg
|
||||||
@@ -643,7 +650,14 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
if not (row.item_code == item_code and row.batch_no == batch_no):
|
if (
|
||||||
|
not (row.item_code == item_code and row.batch_no == batch_no)
|
||||||
|
and not row.serial_and_batch_bundle
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if row.current_serial_and_batch_bundle:
|
||||||
|
self.recalculate_qty_for_serial_and_batch_bundle(row)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
current_qty = get_batch_qty_for_stock_reco(
|
current_qty = get_batch_qty_for_stock_reco(
|
||||||
@@ -677,6 +691,27 @@ class StockReconciliation(StockController):
|
|||||||
if sl_entries:
|
if sl_entries:
|
||||||
self.make_sl_entries(sl_entries)
|
self.make_sl_entries(sl_entries)
|
||||||
|
|
||||||
|
def recalculate_qty_for_serial_and_batch_bundle(self, row):
|
||||||
|
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
|
||||||
|
precision = doc.entries[0].precision("qty")
|
||||||
|
|
||||||
|
for d in doc.entries:
|
||||||
|
qty = (
|
||||||
|
get_batch_qty(
|
||||||
|
d.batch_no,
|
||||||
|
doc.warehouse,
|
||||||
|
posting_date=doc.posting_date,
|
||||||
|
posting_time=doc.posting_time,
|
||||||
|
ignore_voucher_nos=[doc.voucher_no],
|
||||||
|
)
|
||||||
|
or 0
|
||||||
|
) * -1
|
||||||
|
|
||||||
|
if flt(d.qty, precision) == flt(qty, precision):
|
||||||
|
continue
|
||||||
|
|
||||||
|
d.db_set("qty", qty)
|
||||||
|
|
||||||
|
|
||||||
def get_batch_qty_for_stock_reco(
|
def get_batch_qty_for_stock_reco(
|
||||||
item_code, warehouse, batch_no, posting_date, posting_time, voucher_no
|
item_code, warehouse, batch_no, posting_date, posting_time, voucher_no
|
||||||
|
|||||||
@@ -694,10 +694,12 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
item_code=item_code, posting_time="09:00:00", target=warehouse, qty=100, basic_rate=700
|
item_code=item_code, posting_time="09:00:00", target=warehouse, qty=100, basic_rate=700
|
||||||
)
|
)
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
# Removed 50 Qty, Balace Qty 50
|
# Removed 50 Qty, Balace Qty 50
|
||||||
se2 = make_stock_entry(
|
se2 = make_stock_entry(
|
||||||
item_code=item_code,
|
item_code=item_code,
|
||||||
batch_no=se1.items[0].batch_no,
|
batch_no=batch_no,
|
||||||
posting_time="10:00:00",
|
posting_time="10:00:00",
|
||||||
source=warehouse,
|
source=warehouse,
|
||||||
qty=50,
|
qty=50,
|
||||||
@@ -709,15 +711,23 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
item_code=item_code,
|
item_code=item_code,
|
||||||
posting_time="11:00:00",
|
posting_time="11:00:00",
|
||||||
warehouse=warehouse,
|
warehouse=warehouse,
|
||||||
batch_no=se1.items[0].batch_no,
|
batch_no=batch_no,
|
||||||
qty=100,
|
qty=100,
|
||||||
rate=100,
|
rate=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sle = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
filters={"is_cancelled": 0, "voucher_no": stock_reco.name, "actual_qty": ("<", 0)},
|
||||||
|
fields=["actual_qty"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(flt(sle[0].actual_qty), flt(-50.0))
|
||||||
|
|
||||||
# Removed 50 Qty, Balace Qty 50
|
# Removed 50 Qty, Balace Qty 50
|
||||||
make_stock_entry(
|
make_stock_entry(
|
||||||
item_code=item_code,
|
item_code=item_code,
|
||||||
batch_no=se1.items[0].batch_no,
|
batch_no=batch_no,
|
||||||
posting_time="12:00:00",
|
posting_time="12:00:00",
|
||||||
source=warehouse,
|
source=warehouse,
|
||||||
qty=50,
|
qty=50,
|
||||||
@@ -741,12 +751,20 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
sle = frappe.get_all(
|
sle = frappe.get_all(
|
||||||
"Stock Ledger Entry",
|
"Stock Ledger Entry",
|
||||||
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
|
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
|
||||||
fields=["qty_after_transaction"],
|
fields=["qty_after_transaction", "actual_qty", "voucher_type", "voucher_no"],
|
||||||
order_by="posting_time desc, creation desc",
|
order_by="posting_time desc, creation desc",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0))
|
self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0))
|
||||||
|
|
||||||
|
sle = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
filters={"is_cancelled": 0, "voucher_no": stock_reco.name, "actual_qty": ("<", 0)},
|
||||||
|
fields=["actual_qty"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
|
||||||
|
|
||||||
def test_update_stock_reconciliation_while_reposting(self):
|
def test_update_stock_reconciliation_while_reposting(self):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
@@ -914,7 +932,7 @@ def create_stock_reconciliation(**args):
|
|||||||
"do_not_submit": True,
|
"do_not_submit": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
).name
|
||||||
|
|
||||||
sr.append(
|
sr.append(
|
||||||
"items",
|
"items",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"amount",
|
"amount",
|
||||||
"allow_zero_valuation_rate",
|
"allow_zero_valuation_rate",
|
||||||
"serial_no_and_batch_section",
|
"serial_no_and_batch_section",
|
||||||
|
"add_serial_batch_bundle",
|
||||||
"serial_and_batch_bundle",
|
"serial_and_batch_bundle",
|
||||||
"batch_no",
|
"batch_no",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
@@ -203,11 +204,16 @@
|
|||||||
"label": "Current Serial / Batch Bundle",
|
"label": "Current Serial / Batch Bundle",
|
||||||
"options": "Serial and Batch Bundle",
|
"options": "Serial and Batch Bundle",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "add_serial_batch_bundle",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Add Serial / Batch No"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-05-09 18:42:19.224916",
|
"modified": "2023-05-27 17:35:31.026852",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reconciliation Item",
|
"name": "Stock Reconciliation Item",
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ class SerialBatchBundle:
|
|||||||
|
|
||||||
self.set_serial_and_batch_bundle(sn_doc)
|
self.set_serial_and_batch_bundle(sn_doc)
|
||||||
|
|
||||||
|
def validate_actual_qty(self, sn_doc):
|
||||||
|
precision = sn_doc.precision("total_qty")
|
||||||
|
if flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision):
|
||||||
|
msg = f"Total qty {flt(sn_doc.total_qty, precision)} of Serial and Batch Bundle {sn_doc.name} is not equal to Actual Qty {flt(self.sle.actual_qty, precision)} in the {self.sle.voucher_type} {self.sle.voucher_no}"
|
||||||
|
frappe.throw(_(msg))
|
||||||
|
|
||||||
def validate_item(self):
|
def validate_item(self):
|
||||||
msg = ""
|
msg = ""
|
||||||
if self.sle.actual_qty > 0:
|
if self.sle.actual_qty > 0:
|
||||||
@@ -214,6 +220,8 @@ class SerialBatchBundle:
|
|||||||
|
|
||||||
def submit_serial_and_batch_bundle(self):
|
def submit_serial_and_batch_bundle(self):
|
||||||
doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
|
doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
|
||||||
|
self.validate_actual_qty(doc)
|
||||||
|
|
||||||
doc.flags.ignore_voucher_validation = True
|
doc.flags.ignore_voucher_validation = True
|
||||||
doc.submit()
|
doc.submit()
|
||||||
|
|
||||||
@@ -426,9 +434,6 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
entries = self.get_batch_no_ledgers()
|
entries = self.get_batch_no_ledgers()
|
||||||
if frappe.flags.add_breakpoint:
|
|
||||||
breakpoint()
|
|
||||||
|
|
||||||
self.batch_avg_rate = defaultdict(float)
|
self.batch_avg_rate = defaultdict(float)
|
||||||
self.available_qty = defaultdict(float)
|
self.available_qty = defaultdict(float)
|
||||||
self.stock_value_differece = defaultdict(float)
|
self.stock_value_differece = defaultdict(float)
|
||||||
|
|||||||
@@ -676,7 +676,7 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
sle.voucher_type == "Stock Reconciliation"
|
sle.voucher_type == "Stock Reconciliation"
|
||||||
and sle.batch_no
|
and (sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle))
|
||||||
and sle.voucher_detail_no
|
and sle.voucher_detail_no
|
||||||
and sle.actual_qty < 0
|
and sle.actual_qty < 0
|
||||||
):
|
):
|
||||||
@@ -734,9 +734,17 @@ class update_entries_after(object):
|
|||||||
self.update_outgoing_rate_on_transaction(sle)
|
self.update_outgoing_rate_on_transaction(sle)
|
||||||
|
|
||||||
def reset_actual_qty_for_stock_reco(self, sle):
|
def reset_actual_qty_for_stock_reco(self, sle):
|
||||||
current_qty = frappe.get_cached_value(
|
if sle.serial_and_batch_bundle:
|
||||||
"Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
|
current_qty = frappe.get_cached_value(
|
||||||
)
|
"Serial and Batch Bundle", sle.serial_and_batch_bundle, "total_qty"
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_qty is not None:
|
||||||
|
current_qty = abs(current_qty)
|
||||||
|
else:
|
||||||
|
current_qty = frappe.get_cached_value(
|
||||||
|
"Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
|
||||||
|
)
|
||||||
|
|
||||||
if current_qty:
|
if current_qty:
|
||||||
sle.actual_qty = current_qty * -1
|
sle.actual_qty = current_qty * -1
|
||||||
@@ -1524,7 +1532,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
next_stock_reco_detail = get_next_stock_reco(args)
|
next_stock_reco_detail = get_next_stock_reco(args)
|
||||||
if next_stock_reco_detail:
|
if next_stock_reco_detail:
|
||||||
detail = next_stock_reco_detail[0]
|
detail = next_stock_reco_detail[0]
|
||||||
if detail.batch_no:
|
if detail.batch_no or (detail.serial_and_batch_bundle and detail.has_batch_no):
|
||||||
regenerate_sle_for_batch_stock_reco(detail)
|
regenerate_sle_for_batch_stock_reco(detail)
|
||||||
|
|
||||||
# add condition to update SLEs before this date & time
|
# add condition to update SLEs before this date & time
|
||||||
@@ -1602,7 +1610,9 @@ def get_next_stock_reco(kwargs):
|
|||||||
sle.voucher_no,
|
sle.voucher_no,
|
||||||
sle.item_code,
|
sle.item_code,
|
||||||
sle.batch_no,
|
sle.batch_no,
|
||||||
|
sle.serial_and_batch_bundle,
|
||||||
sle.actual_qty,
|
sle.actual_qty,
|
||||||
|
sle.has_batch_no,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(sle.item_code == kwargs.get("item_code"))
|
(sle.item_code == kwargs.get("item_code"))
|
||||||
|
|||||||
Reference in New Issue
Block a user