fix: multiple issues related to serial and batch bundle (backport #41662) (#41668)

* 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:
mergify[bot]
2024-05-28 21:57:13 +05:30
committed by GitHub
parent 2e8ae4dc30
commit dc0bb220ed
24 changed files with 571 additions and 90 deletions

View File

@@ -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:

View File

@@ -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])

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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": []
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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() {

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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", {

View File

@@ -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
}
}

View File

@@ -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: