mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-18 22:42:12 +00:00
* fix: multiple issues related to serial and batch bundle (#41662)
(cherry picked from commit ce834f5dba)
# Conflicts:
# erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
# erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
* chore: fix conflicts
* chore: fix conflicts
---------
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
This commit is contained in:
@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.submit_serial_batch_bundle()
|
||||
|
||||
if self.coupon_code:
|
||||
|
||||
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
pos.reload()
|
||||
|
||||
pos_return1 = make_sales_return(pos.name)
|
||||
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
|
||||
bundle_id = frappe.get_doc(
|
||||
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
|
||||
)
|
||||
|
||||
bundle_id.remove(bundle_id.entries[1])
|
||||
bundle_id.save()
|
||||
|
||||
bundle_id.load_from_db()
|
||||
|
||||
serial_no = bundle_id.entries[0].serial_no
|
||||
self.assertEqual(serial_no, serial_nos[0])
|
||||
|
||||
pos_return1.insert()
|
||||
pos_return1.submit()
|
||||
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.submit()
|
||||
|
||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
|
||||
self.assertEqual(serial_no, serial_nos[1])
|
||||
|
||||
@@ -707,6 +707,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
|
||||
@@ -450,6 +450,7 @@ class SalesInvoice(SellingController):
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.update_stock_ledger()
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import flt, format_datetime, get_datetime
|
||||
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
target_doc.rejected_warehouse = ""
|
||||
target_doc.warehouse = source_doc.rejected_warehouse
|
||||
target_doc.received_qty = target_doc.qty
|
||||
target_doc.return_qty_from_rejected_warehouse = 1
|
||||
|
||||
elif doctype == "Purchase Invoice":
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
|
||||
if source_doc.item_code:
|
||||
if (
|
||||
(source_doc.serial_no or source_doc.batch_no)
|
||||
and not source_doc.serial_and_batch_bundle
|
||||
and not source_doc.use_serial_batch_fields
|
||||
):
|
||||
target_doc.set("use_serial_batch_fields", 1)
|
||||
|
||||
if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||
)
|
||||
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||
return
|
||||
|
||||
if not target_doc.get("use_serial_batch_fields"):
|
||||
for qty_field in ["stock_qty", "rejected_qty"]:
|
||||
if not target_doc.get(qty_field):
|
||||
continue
|
||||
|
||||
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
|
||||
elif target_doc.get("use_serial_batch_fields"):
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
|
||||
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -839,3 +841,229 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
|
||||
batches.update(get_batches_from_bundle(ids))
|
||||
|
||||
return batches
|
||||
|
||||
|
||||
def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
|
||||
available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not available_dict:
|
||||
frappe.throw(_("No Serial / Batches are available for return"))
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
|
||||
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not _bundle_ids:
|
||||
return frappe._dict({})
|
||||
|
||||
return get_serial_batches_based_on_bundle(field, _bundle_ids)
|
||||
|
||||
|
||||
def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
available_dict = frappe._dict({})
|
||||
batch_serial_nos = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
fields=[
|
||||
"`tabSerial and Batch Entry`.`serial_no`",
|
||||
"`tabSerial and Batch Entry`.`batch_no`",
|
||||
"`tabSerial and Batch Entry`.`qty`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
],
|
||||
filters=[
|
||||
["Serial and Batch Bundle", "name", "in", _bundle_ids],
|
||||
["Serial and Batch Entry", "docstatus", "=", 1],
|
||||
],
|
||||
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
|
||||
)
|
||||
|
||||
for row in batch_serial_nos:
|
||||
key = row.voucher_detail_no
|
||||
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
|
||||
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||
|
||||
if key not in available_dict:
|
||||
available_dict[key] = frappe._dict(
|
||||
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
||||
)
|
||||
|
||||
available_dict[key]["qty"] += row.qty
|
||||
|
||||
if row.serial_no:
|
||||
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
||||
elif row.batch_no:
|
||||
available_dict[key]["batches"][row.batch_no] += row.qty
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
|
||||
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||
|
||||
pluck_field = "serial_and_batch_bundle"
|
||||
if is_rejected:
|
||||
del filters["serial_and_batch_bundle"]
|
||||
filters["rejected_serial_and_batch_bundle"] = ("is", "set")
|
||||
pluck_field = "rejected_serial_and_batch_bundle"
|
||||
|
||||
_bundle_ids = frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck=pluck_field,
|
||||
)
|
||||
|
||||
if not _bundle_ids:
|
||||
return {}
|
||||
|
||||
del filters["name"]
|
||||
|
||||
filters[field] = ("in", reference_ids)
|
||||
|
||||
if not is_rejected:
|
||||
_bundle_ids.extend(
|
||||
frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck="serial_and_batch_bundle",
|
||||
)
|
||||
)
|
||||
else:
|
||||
fields = [
|
||||
"serial_and_batch_bundle",
|
||||
]
|
||||
|
||||
if is_rejected:
|
||||
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
||||
|
||||
data = frappe.get_all(
|
||||
doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if is_rejected:
|
||||
if d.get("return_qty_from_rejected_warehouse"):
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
|
||||
return _bundle_ids
|
||||
|
||||
|
||||
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
warehouse = row.get(warehouse_field)
|
||||
qty = abs(row.get(qty_field))
|
||||
|
||||
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
|
||||
|
||||
if data.serial_nos:
|
||||
available_serial_nos = []
|
||||
for serial_no, sn_qty in data.serial_nos.items():
|
||||
if sn_qty != 0:
|
||||
available_serial_nos.append(serial_no)
|
||||
|
||||
if available_serial_nos:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos)
|
||||
|
||||
if len(available_serial_nos) > qty:
|
||||
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
||||
else:
|
||||
filterd_serial_batch["serial_nos"] = available_serial_nos
|
||||
|
||||
elif data.batches:
|
||||
for batch_no, batch_qty in data.batches.items():
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
batch_qty = batch_qty * -1
|
||||
|
||||
if batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
batch_qty = get_available_batch_qty(
|
||||
parent_doc,
|
||||
batch_no,
|
||||
warehouse,
|
||||
)
|
||||
|
||||
if batch_qty <= 0:
|
||||
frappe.throw(
|
||||
_("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
|
||||
title=_("Batch Not Available for Return"),
|
||||
)
|
||||
|
||||
if qty <= 0:
|
||||
break
|
||||
|
||||
if batch_qty > qty:
|
||||
filterd_serial_batch["batches"][batch_no] = qty
|
||||
qty = 0
|
||||
else:
|
||||
filterd_serial_batch["batches"][batch_no] += batch_qty
|
||||
qty -= batch_qty
|
||||
|
||||
return filterd_serial_batch
|
||||
|
||||
|
||||
def get_available_batch_qty(parent_doc, batch_no, warehouse):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
return get_batch_qty(
|
||||
batch_no,
|
||||
warehouse,
|
||||
posting_date=parent_doc.posting_date,
|
||||
posting_time=parent_doc.posting_time,
|
||||
for_stock_levels=True,
|
||||
)
|
||||
|
||||
|
||||
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
type_of_transaction = "Outward"
|
||||
if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
warehouse = child_doc.get(warehouse_field)
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
warehouse = child_doc.get("target_warehouse")
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"item_code": child_doc.item_code,
|
||||
"warehouse": warehouse,
|
||||
"serial_nos": data.get("serial_nos"),
|
||||
"batches": data.get("batches"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"voucher_type": parent_doc.doctype,
|
||||
"voucher_no": parent_doc.name,
|
||||
"voucher_detail_no": child_doc.name,
|
||||
"qty": child_doc.qty,
|
||||
"company": parent_doc.company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
return cls_obj.name
|
||||
|
||||
|
||||
def get_available_serial_nos(serial_nos, warehouse):
|
||||
return frappe.get_all(
|
||||
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
|
||||
)
|
||||
|
||||
@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
|
||||
)
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.controllers.sales_and_purchase_return import (
|
||||
available_serial_batch_for_return,
|
||||
filter_serial_batches,
|
||||
make_serial_batch_bundle_for_return,
|
||||
)
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
@@ -217,6 +222,125 @@ class StockController(AccountsController):
|
||||
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
||||
self.create_serial_batch_bundle(bundle_details, row)
|
||||
|
||||
def make_bundle_for_sales_purchase_return(self, table_name=None):
|
||||
if not self.get("is_return"):
|
||||
return
|
||||
|
||||
if not table_name:
|
||||
table_name = "items"
|
||||
|
||||
self.make_bundle_for_non_rejected_qty(table_name)
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
self.make_bundle_for_rejected_qty(table_name)
|
||||
|
||||
def make_bundle_for_rejected_qty(self, table_name=None):
|
||||
field, reference_ids = self.get_reference_ids(
|
||||
table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
|
||||
)
|
||||
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(
|
||||
field, child_doctype, reference_ids, is_rejected=True
|
||||
)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
qty_field = "rejected_qty"
|
||||
warehouse_field = "rejected_warehouse"
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
qty_field = "qty"
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
data = filter_serial_batches(
|
||||
self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
|
||||
)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field)
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
else:
|
||||
row.db_set(
|
||||
{
|
||||
"rejected_serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"rejected_serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def make_bundle_for_non_rejected_qty(self, table_name):
|
||||
field, reference_ids = self.get_reference_ids(table_name)
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
data = filter_serial_batches(self, data, row)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self)
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||
field = {
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
"Delivery Note": "dn_detail",
|
||||
"Purchase Receipt": "purchase_receipt_item",
|
||||
"Purchase Invoice": "purchase_invoice_item",
|
||||
"POS Invoice": "pos_invoice_item",
|
||||
}.get(self.doctype)
|
||||
|
||||
if not bundle_field:
|
||||
bundle_field = "serial_and_batch_bundle"
|
||||
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
reference_ids = []
|
||||
|
||||
for row in self.get(table_name):
|
||||
if not self.is_serial_batch_item(row.item_code):
|
||||
continue
|
||||
|
||||
if (
|
||||
row.get(field)
|
||||
and (
|
||||
qty_field == "qty"
|
||||
and not row.get("return_qty_from_rejected_warehouse")
|
||||
or qty_field == "rejected_qty"
|
||||
and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
|
||||
)
|
||||
and not row.get("use_serial_batch_fields")
|
||||
and not row.get(bundle_field)
|
||||
):
|
||||
reference_ids.append(row.get(field))
|
||||
|
||||
return field, reference_ids
|
||||
|
||||
@frappe.request_cache
|
||||
def is_serial_batch_item(self, item_code) -> bool:
|
||||
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
||||
|
||||
if item_details.has_serial_no or item_details.has_batch_no:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
@@ -611,35 +735,16 @@ class StockController(AccountsController):
|
||||
def make_package_for_transfer(
|
||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
|
||||
):
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
|
||||
|
||||
if not type_of_transaction:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = warehouse
|
||||
bundle_doc.type_of_transaction = type_of_transaction
|
||||
bundle_doc.voucher_type = self.doctype
|
||||
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
return make_bundle_for_material_transfer(
|
||||
is_new=self.is_new(),
|
||||
docstatus=self.docstatus,
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.name,
|
||||
serial_and_batch_bundle=serial_and_batch_bundle,
|
||||
warehouse=warehouse,
|
||||
type_of_transaction=type_of_transaction,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
|
||||
def get_sl_entries(self, d, args):
|
||||
sl_dict = frappe._dict(
|
||||
@@ -1557,3 +1662,38 @@ def create_item_wise_repost_entries(
|
||||
repost_entries.append(repost_entry)
|
||||
|
||||
return repost_entries
|
||||
|
||||
|
||||
def make_bundle_for_material_transfer(**kwargs):
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
|
||||
|
||||
if not kwargs.type_of_transaction:
|
||||
kwargs.type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = kwargs.warehouse
|
||||
bundle_doc.type_of_transaction = kwargs.type_of_transaction
|
||||
bundle_doc.voucher_type = kwargs.voucher_type
|
||||
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if kwargs.type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = kwargs.warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
|
||||
@@ -8,8 +8,11 @@ from pypika import Order
|
||||
class DeprecatedSerialNoValuation:
|
||||
@deprecated
|
||||
def calculate_stock_value_from_deprecarated_ledgers(self):
|
||||
if not frappe.db.get_value(
|
||||
"Stock Ledger Entry", {"serial_no": ("is", "set"), "is_cancelled": 0}, "name"
|
||||
if not frappe.db.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["name"],
|
||||
filters={"serial_no": ("is", "set"), "is_cancelled": 0, "item_code": self.sle.item_code},
|
||||
limit=1,
|
||||
):
|
||||
return
|
||||
|
||||
@@ -41,6 +44,12 @@ class DeprecatedSerialNoValuation:
|
||||
# get rate from serial nos within same company
|
||||
incoming_values = 0.0
|
||||
for serial_no in serial_nos:
|
||||
sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1)
|
||||
if sn_details and sn_details.purchase_rate and sn_details.company == self.sle.company:
|
||||
self.serial_no_incoming_rate[serial_no] += flt(sn_details.purchase_rate)
|
||||
incoming_values += self.serial_no_incoming_rate[serial_no]
|
||||
continue
|
||||
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
stock_ledgers = (
|
||||
frappe.qb.from_(table)
|
||||
|
||||
@@ -208,7 +208,8 @@ def get_batch_qty(
|
||||
|
||||
:param batch_no: Optional - give qty for this batch no
|
||||
:param warehouse: Optional - give qty for this warehouse
|
||||
:param item_code: Optional - give qty for this item"""
|
||||
:param item_code: Optional - give qty for this item
|
||||
:param for_stock_levels: True consider expired batches"""
|
||||
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
|
||||
@@ -467,6 +467,7 @@ class DeliveryNote(SellingController):
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
@@ -1371,6 +1372,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
if source_parent.doctype == "Delivery Note" and source.received_qty:
|
||||
target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty)
|
||||
|
||||
if source.get("use_serial_batch_fields"):
|
||||
target.set("use_serial_batch_fields", 1)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
doctype,
|
||||
source_name,
|
||||
|
||||
@@ -249,18 +249,15 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertTrue(dn.items[0].serial_no)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
# return entry
|
||||
dn1 = make_sales_return(dn.name)
|
||||
|
||||
dn1.items[0].qty = -2
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle)
|
||||
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||
bundle_doc.save()
|
||||
|
||||
dn1.save()
|
||||
dn1.items[0].serial_no = "\n".join(get_serial_nos(serial_nos)[0:2])
|
||||
dn1.submit()
|
||||
dn1.reload()
|
||||
|
||||
returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle)
|
||||
for serial_no in returned_serial_nos1:
|
||||
@@ -269,21 +266,15 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn2 = make_sales_return(dn.name)
|
||||
|
||||
dn2.items[0].qty = -2
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn2.items[0].serial_and_batch_bundle)
|
||||
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||
bundle_doc.save()
|
||||
|
||||
dn2.save()
|
||||
dn2.items[0].serial_no = "\n".join(get_serial_nos(serial_nos)[2:4])
|
||||
dn2.submit()
|
||||
dn2.reload()
|
||||
|
||||
returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle)
|
||||
for serial_no in returned_serial_nos2:
|
||||
self.assertTrue(serial_no in serial_nos)
|
||||
self.assertFalse(serial_no in returned_serial_nos1)
|
||||
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
def test_sales_return_for_non_bundled_items_partial(self):
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
@@ -428,7 +419,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(dn.per_returned, 100)
|
||||
self.assertEqual(dn.status, "Return Issued")
|
||||
|
||||
def test_delivery_note_return_valuation_on_different_warehuose(self):
|
||||
def test_delivery_note_return_valuation_on_different_warehouse(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
@@ -370,6 +370,7 @@ class PurchaseReceipt(BuyingController):
|
||||
else:
|
||||
self.db_set("status", "Completed")
|
||||
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty, reserved_qty_for_subcontract in bin
|
||||
|
||||
@@ -2645,7 +2645,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
for row in inter_transfer_dn_return.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
def test_internal_transfer_with_serial_batch_items_without_user_serial_batch_fields(self):
|
||||
def test_internal_transfer_with_serial_batch_items_without_use_serial_batch_fields(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"purchase_invoice",
|
||||
"column_break_40",
|
||||
"allow_zero_valuation_rate",
|
||||
"return_qty_from_rejected_warehouse",
|
||||
"is_fixed_asset",
|
||||
"asset_location",
|
||||
"asset_category",
|
||||
@@ -1116,12 +1117,19 @@
|
||||
"hidden": 1,
|
||||
"label": "Apply TDS",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "return_qty_from_rejected_warehouse",
|
||||
"fieldtype": "Check",
|
||||
"label": "Return Qty from Rejected Warehouse",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-08 20:00:16.278292",
|
||||
"modified": "2024-05-28 09:48:24.448815",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
@@ -1132,4 +1140,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ class PurchaseReceiptItem(Document):
|
||||
rejected_serial_no: DF.Text | None
|
||||
rejected_warehouse: DF.Link | None
|
||||
retain_sample: DF.Check
|
||||
return_qty_from_rejected_warehouse: DF.Check
|
||||
returned_qty: DF.Float
|
||||
rm_supp_cost: DF.Currency
|
||||
sales_order: DF.Link | None
|
||||
|
||||
@@ -377,7 +377,7 @@ class TestPutawayRule(FrappeTestCase):
|
||||
apply_putaway_rule=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
stock_entry.save()
|
||||
stock_entry.submit()
|
||||
stock_entry.load_from_db()
|
||||
|
||||
self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1)
|
||||
@@ -398,11 +398,17 @@ class TestPutawayRule(FrappeTestCase):
|
||||
|
||||
self.assertUnchangedItemsOnResave(stock_entry)
|
||||
|
||||
for row in stock_entry.items:
|
||||
if row.serial_and_batch_bundle:
|
||||
frappe.delete_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||
|
||||
stock_entry.load_from_db()
|
||||
stock_entry.cancel()
|
||||
|
||||
rivs = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name})
|
||||
for row in rivs:
|
||||
riv_doc = frappe.get_doc("Repost Item Valuation", row.name)
|
||||
riv_doc.cancel()
|
||||
riv_doc.delete()
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1)
|
||||
|
||||
stock_entry.delete()
|
||||
pr.cancel()
|
||||
rule_1.delete()
|
||||
|
||||
@@ -156,6 +156,8 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
def validate_serial_nos_duplicate(self):
|
||||
# Don't inward same serial number multiple times
|
||||
if self.voucher_type in ["POS Invoice", "Pick List"]:
|
||||
return
|
||||
|
||||
if not self.warehouse:
|
||||
return
|
||||
|
||||
@@ -111,6 +111,8 @@ frappe.ui.form.on("Stock Entry", {
|
||||
// or a pre-existing batch
|
||||
if (frm.doc.purpose != "Material Receipt") {
|
||||
filters["warehouse"] = item.s_warehouse || item.t_warehouse;
|
||||
} else {
|
||||
filters["is_inward"] = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1110,6 +1112,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
|
||||
on_submit() {
|
||||
this.clean_up();
|
||||
this.refresh_serial_batch_bundle_field();
|
||||
}
|
||||
|
||||
refresh_serial_batch_bundle_field() {
|
||||
frappe.route_hooks.after_submit = (frm_obj) => {
|
||||
frm_obj.reload_doc();
|
||||
};
|
||||
}
|
||||
|
||||
after_cancel() {
|
||||
|
||||
@@ -226,6 +226,7 @@ class StockEntry(StockController):
|
||||
if not self.from_bom:
|
||||
self.fg_completed_qty = 0.0
|
||||
|
||||
self.make_serial_and_batch_bundle_for_outward()
|
||||
self.validate_serialized_batch()
|
||||
self.calculate_rate_and_amount()
|
||||
self.validate_putaway_capacity()
|
||||
@@ -289,9 +290,6 @@ class StockEntry(StockController):
|
||||
if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
|
||||
self.set_material_request_transfer_status("In Transit")
|
||||
|
||||
def before_save(self):
|
||||
self.make_serial_and_batch_bundle_for_outward()
|
||||
|
||||
def on_update(self):
|
||||
self.set_serial_and_batch_bundle()
|
||||
|
||||
@@ -992,7 +990,7 @@ class StockEntry(StockController):
|
||||
self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose")
|
||||
|
||||
def make_serial_and_batch_bundle_for_outward(self):
|
||||
if self.docstatus == 1:
|
||||
if self.docstatus == 0:
|
||||
return
|
||||
|
||||
serial_or_batch_items = get_serial_or_batch_items(self.items)
|
||||
@@ -1045,12 +1043,11 @@ class StockEntry(StockController):
|
||||
if not bundle_doc:
|
||||
continue
|
||||
|
||||
if self.docstatus == 0:
|
||||
for entry in bundle_doc.entries:
|
||||
if not entry.serial_no:
|
||||
continue
|
||||
for entry in bundle_doc.entries:
|
||||
if not entry.serial_no:
|
||||
continue
|
||||
|
||||
already_picked_serial_nos.append(entry.serial_no)
|
||||
already_picked_serial_nos.append(entry.serial_no)
|
||||
|
||||
row.serial_and_batch_bundle = bundle_doc.name
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, get_table_name, getdate
|
||||
from frappe.utils import add_to_date, cint, flt, get_datetime, get_table_name, getdate
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from pypika import functions as fn
|
||||
|
||||
@@ -107,6 +107,8 @@ def get_stock_ledger_entries_for_batch_no(filters):
|
||||
if not filters.get("to_date"):
|
||||
frappe.throw(_("'To Date' is required"))
|
||||
|
||||
posting_datetime = get_datetime(add_to_date(filters["to_date"], days=1))
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
@@ -121,7 +123,7 @@ def get_stock_ledger_entries_for_batch_no(filters):
|
||||
(sle.docstatus < 2)
|
||||
& (sle.is_cancelled == 0)
|
||||
& (sle.batch_no != "")
|
||||
& (sle.posting_date <= filters["to_date"])
|
||||
& (sle.posting_datetime < posting_datetime)
|
||||
)
|
||||
.groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
|
||||
.orderby(sle.item_code, sle.warehouse)
|
||||
|
||||
@@ -55,8 +55,45 @@ class SerialBatchBundle:
|
||||
elif not self.sle.is_cancelled:
|
||||
self.validate_item_and_warehouse()
|
||||
|
||||
def is_material_transfer(self):
|
||||
allowed_types = [
|
||||
"Material Transfer",
|
||||
"Send to Subcontractor",
|
||||
"Material Transfer for Manufacture",
|
||||
]
|
||||
|
||||
if (
|
||||
self.sle.voucher_type == "Stock Entry"
|
||||
and not self.sle.is_cancelled
|
||||
and frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose") in allowed_types
|
||||
):
|
||||
return True
|
||||
|
||||
def make_serial_batch_no_bundle_for_material_transfer(self):
|
||||
from erpnext.controllers.stock_controller import make_bundle_for_material_transfer
|
||||
|
||||
bundle = frappe.db.get_value(
|
||||
"Stock Entry Detail", self.sle.voucher_detail_no, "serial_and_batch_bundle"
|
||||
)
|
||||
|
||||
if bundle:
|
||||
new_bundle_id = make_bundle_for_material_transfer(
|
||||
is_new=False,
|
||||
docstatus=1,
|
||||
voucher_type=self.sle.voucher_type,
|
||||
voucher_no=self.sle.voucher_no,
|
||||
serial_and_batch_bundle=bundle,
|
||||
warehouse=self.sle.warehouse,
|
||||
type_of_transaction="Inward" if self.sle.actual_qty > 0 else "Outward",
|
||||
do_not_submit=0,
|
||||
)
|
||||
self.sle.db_set({"serial_and_batch_bundle": new_bundle_id})
|
||||
|
||||
def make_serial_batch_no_bundle(self):
|
||||
self.validate_item()
|
||||
if self.sle.actual_qty > 0 and self.is_material_transfer():
|
||||
self.make_serial_batch_no_bundle_for_material_transfer()
|
||||
return
|
||||
|
||||
sn_doc = SerialBatchCreation(
|
||||
{
|
||||
@@ -143,6 +180,9 @@ class SerialBatchBundle:
|
||||
"serial_and_batch_bundle": sn_doc.name,
|
||||
}
|
||||
|
||||
if self.sle.actual_qty < 0 and self.is_material_transfer():
|
||||
values_to_update["valuation_rate"] = sn_doc.avg_rate
|
||||
|
||||
if not frappe.db.get_single_value(
|
||||
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle"
|
||||
):
|
||||
@@ -341,11 +381,9 @@ def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
|
||||
if serial_nos:
|
||||
filters["serial_no"] = ("in", serial_nos)
|
||||
|
||||
entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx")
|
||||
if not entries:
|
||||
return []
|
||||
serial_nos = frappe.get_all("Serial and Batch Entry", filters=filters, order_by="idx", pluck="serial_no")
|
||||
|
||||
return [d.serial_no for d in entries if d.serial_no]
|
||||
return serial_nos
|
||||
|
||||
|
||||
def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
|
||||
|
||||
@@ -312,7 +312,11 @@ def get_reposting_data(file_path) -> dict:
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
|
||||
data = gzip.decompress(content)
|
||||
try:
|
||||
data = gzip.decompress(content)
|
||||
except Exception:
|
||||
return frappe._dict()
|
||||
|
||||
if data := json.loads(data.decode("utf-8")):
|
||||
data = data
|
||||
|
||||
|
||||
@@ -302,6 +302,21 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
reset_raw_materials_table: (frm) => {
|
||||
frm.clear_table("supplied_items");
|
||||
|
||||
frm.call({
|
||||
method: "reset_raw_materials",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frm.save();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Landed Cost Taxes and Charges", {
|
||||
|
||||
@@ -47,8 +47,11 @@
|
||||
"total_qty",
|
||||
"column_break_27",
|
||||
"total",
|
||||
"raw_material_details",
|
||||
"raw_materials_consumed_section",
|
||||
"reset_raw_materials_table",
|
||||
"column_break_uinr",
|
||||
"get_current_stock",
|
||||
"raw_material_details",
|
||||
"supplied_items",
|
||||
"additional_costs_section",
|
||||
"distribute_additional_costs_based_on",
|
||||
@@ -300,6 +303,7 @@
|
||||
"depends_on": "supplied_items",
|
||||
"fieldname": "raw_material_details",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Raw Materials Consumed",
|
||||
"options": "fa fa-table",
|
||||
"print_hide": 1,
|
||||
@@ -640,12 +644,26 @@
|
||||
"fieldname": "supplier_delivery_note",
|
||||
"fieldtype": "Data",
|
||||
"label": "Supplier Delivery Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "raw_materials_consumed_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Raw Materials Actions"
|
||||
},
|
||||
{
|
||||
"fieldname": "reset_raw_materials_table",
|
||||
"fieldtype": "Button",
|
||||
"label": "Reset Raw Materials Table"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uinr",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-16 13:04:00.710534",
|
||||
"modified": "2024-05-28 15:02:13.517969",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt",
|
||||
@@ -714,4 +732,4 @@
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,11 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.update_status()
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset_raw_materials(self):
|
||||
self.supplied_items = []
|
||||
self.create_raw_materials_supplied()
|
||||
|
||||
def validate_closed_subcontracting_order(self):
|
||||
for item in self.items:
|
||||
if item.subcontracting_order:
|
||||
|
||||
Reference in New Issue
Block a user