refactor: serial and batch package creation for finished item and cleanup code
This commit is contained in:
parent
86da306cca
commit
16f26fb3d8
@ -328,8 +328,6 @@ class AssetCapitalization(StockController):
|
||||
{
|
||||
"item_code": self.target_item_code,
|
||||
"warehouse": self.target_warehouse,
|
||||
"batch_no": self.target_batch_no,
|
||||
"serial_no": self.target_serial_no,
|
||||
"actual_qty": flt(self.target_qty),
|
||||
"incoming_rate": flt(self.target_incoming_rate),
|
||||
},
|
||||
|
@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import ValidationError, _, msgprint
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.utils import cint, cstr, flt, getdate
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
@ -497,7 +497,6 @@ class BuyingController(SubcontractingController):
|
||||
d,
|
||||
{
|
||||
"actual_qty": flt(pr_qty),
|
||||
"serial_no": cstr(d.serial_no).strip(),
|
||||
"serial_and_batch_bundle": (
|
||||
d.serial_and_batch_bundle
|
||||
if not self.is_internal_transfer()
|
||||
@ -550,7 +549,6 @@ class BuyingController(SubcontractingController):
|
||||
{
|
||||
"warehouse": d.rejected_warehouse,
|
||||
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
|
||||
"serial_no": cstr(d.rejected_serial_no).strip(),
|
||||
"incoming_rate": 0.0,
|
||||
"serial_and_batch_bundle": d.rejected_serial_and_batch_bundle,
|
||||
},
|
||||
|
@ -407,7 +407,6 @@ class StockController(AccountsController):
|
||||
else:
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
print(bundle_doc.name)
|
||||
return bundle_doc.name
|
||||
|
||||
def get_sl_entries(self, d, args):
|
||||
@ -428,7 +427,6 @@ class StockController(AccountsController):
|
||||
),
|
||||
"incoming_rate": 0,
|
||||
"company": self.company,
|
||||
"serial_no": d.get("serial_no"),
|
||||
"project": d.get("project") or self.get("project"),
|
||||
"is_cancelled": 1 if self.docstatus == 2 else 0,
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ from collections import defaultdict
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
@ -768,9 +768,7 @@ class SubcontractingController(StockController):
|
||||
scr_qty = flt(item.qty) * flt(item.conversion_factor)
|
||||
|
||||
if scr_qty:
|
||||
sle = self.get_sl_entries(
|
||||
item, {"actual_qty": flt(scr_qty), "serial_no": cstr(item.serial_no).strip()}
|
||||
)
|
||||
sle = self.get_sl_entries(item, {"actual_qty": flt(scr_qty)})
|
||||
rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9
|
||||
incoming_rate = flt(item.rate, rate_db_precision)
|
||||
sle.update(
|
||||
@ -788,7 +786,6 @@ class SubcontractingController(StockController):
|
||||
{
|
||||
"warehouse": item.rejected_warehouse,
|
||||
"actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
|
||||
"serial_no": cstr(item.rejected_serial_no).strip(),
|
||||
"incoming_rate": 0.0,
|
||||
},
|
||||
)
|
||||
|
@ -42,7 +42,6 @@
|
||||
"has_serial_no",
|
||||
"has_batch_no",
|
||||
"column_break_18",
|
||||
"serial_no",
|
||||
"batch_size",
|
||||
"required_items_section",
|
||||
"materials_and_operations_tab",
|
||||
@ -532,14 +531,6 @@
|
||||
"label": "Has Batch No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "has_serial_no",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Serial Nos",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "has_batch_no",
|
||||
|
@ -17,6 +17,7 @@ from frappe.utils import (
|
||||
get_datetime,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
now,
|
||||
nowdate,
|
||||
time_diff_in_hours,
|
||||
)
|
||||
@ -32,11 +33,7 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings
|
||||
)
|
||||
from erpnext.stock.doctype.batch.batch import make_batch
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
|
||||
from erpnext.stock.doctype.serial_no.serial_no import (
|
||||
clean_serial_no_string,
|
||||
get_auto_serial_nos,
|
||||
get_serial_nos,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, get_serial_nos
|
||||
from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
|
||||
from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehouse_company
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
@ -447,24 +444,53 @@ class WorkOrder(Document):
|
||||
frappe.delete_doc("Batch", row.name)
|
||||
|
||||
def make_serial_nos(self, args):
|
||||
self.serial_no = clean_serial_no_string(self.serial_no)
|
||||
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
|
||||
if serial_no_series:
|
||||
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1
|
||||
)
|
||||
|
||||
if self.serial_no:
|
||||
args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
|
||||
# auto_make_serial_nos(args)
|
||||
serial_nos = []
|
||||
if item_details.serial_no_series:
|
||||
serial_nos = get_auto_serial_nos(item_details.serial_no_series, self.qty)
|
||||
|
||||
serial_nos_length = len(get_serial_nos(self.serial_no))
|
||||
if serial_nos_length != self.qty:
|
||||
frappe.throw(
|
||||
_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(
|
||||
self.qty, self.production_item, serial_nos_length
|
||||
),
|
||||
SerialNoQtyError,
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
fields = [
|
||||
"name",
|
||||
"serial_no",
|
||||
"creation",
|
||||
"modified",
|
||||
"owner",
|
||||
"modified_by",
|
||||
"company",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"description",
|
||||
"status",
|
||||
"work_order",
|
||||
]
|
||||
|
||||
serial_nos_details = []
|
||||
for serial_no in serial_nos:
|
||||
serial_nos_details.append(
|
||||
(
|
||||
serial_no,
|
||||
serial_no,
|
||||
now(),
|
||||
now(),
|
||||
frappe.session.user,
|
||||
frappe.session.user,
|
||||
self.company,
|
||||
self.production_item,
|
||||
item_details.item_name,
|
||||
item_details.description,
|
||||
"Inactive",
|
||||
self.name,
|
||||
)
|
||||
)
|
||||
|
||||
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
|
||||
|
||||
def create_job_card(self):
|
||||
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
|
||||
|
||||
@ -1041,24 +1067,6 @@ class WorkOrder(Document):
|
||||
bom.set_bom_material_details()
|
||||
return bom
|
||||
|
||||
def update_batch_produced_qty(self, stock_entry_doc):
|
||||
if not cint(
|
||||
frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")
|
||||
):
|
||||
return
|
||||
|
||||
for row in stock_entry_doc.items:
|
||||
if row.batch_no and (row.is_finished_item or row.is_scrap_item):
|
||||
qty = frappe.get_all(
|
||||
"Stock Entry Detail",
|
||||
filters={"batch_no": row.batch_no, "docstatus": 1},
|
||||
or_filters={"is_finished_item": 1, "is_scrap_item": 1},
|
||||
fields=["sum(qty)"],
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
|
||||
frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
|
@ -27,8 +27,8 @@ class SerialandBatchBundle(Document):
|
||||
self.validate_serial_nos_inventory()
|
||||
|
||||
def before_save(self):
|
||||
self.set_total_qty()
|
||||
self.set_is_outward()
|
||||
self.set_total_qty()
|
||||
self.set_warehouse()
|
||||
self.set_incoming_rate()
|
||||
self.validate_qty_and_stock_value_difference()
|
||||
@ -51,7 +51,9 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
for serial_no in serial_nos:
|
||||
if serial_no_warehouse.get(serial_no) != self.warehouse:
|
||||
if (
|
||||
not serial_no_warehouse.get(serial_no) or serial_no_warehouse.get(serial_no) != self.warehouse
|
||||
):
|
||||
frappe.throw(
|
||||
_(f"Serial No {bold(serial_no)} is not present in the warehouse {bold(self.warehouse)}.")
|
||||
)
|
||||
@ -73,6 +75,9 @@ class SerialandBatchBundle(Document):
|
||||
if d.stock_value_difference and d.stock_value_difference > 0:
|
||||
d.stock_value_difference *= -1
|
||||
|
||||
def get_serial_nos(self):
|
||||
return [d.serial_no for d in self.ledgers if d.serial_no]
|
||||
|
||||
def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
|
||||
sle = self.get_sle_for_outward_transaction(row)
|
||||
if self.has_serial_no:
|
||||
@ -271,6 +276,11 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
def set_is_outward(self):
|
||||
for row in self.ledgers:
|
||||
if self.type_of_transaction == "Outward" and row.qty > 0:
|
||||
row.qty *= -1
|
||||
elif self.type_of_transaction == "Inward" and row.qty < 0:
|
||||
row.qty *= -1
|
||||
|
||||
row.is_outward = 1 if self.type_of_transaction == "Outward" else 0
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -9,10 +9,9 @@ import frappe
|
||||
from frappe import ValidationError, _
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads
|
||||
from frappe.utils import cint, cstr, getdate, nowdate, safe_json_loads
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.get_item_details import get_reserved_qty_for_so
|
||||
|
||||
|
||||
class SerialNoCannotCreateDirectError(ValidationError):
|
||||
@ -108,384 +107,12 @@ class SerialNo(StockController):
|
||||
)
|
||||
|
||||
|
||||
def process_serial_no(sle):
|
||||
item_det = get_item_details(sle.item_code)
|
||||
validate_serial_no(sle, item_det)
|
||||
|
||||
|
||||
def validate_serial_no(sle, item_det):
|
||||
serial_nos = get_serial_nos(sle.serial_and_batch_bundle) if sle.serial_and_batch_bundle else []
|
||||
validate_material_transfer_entry(sle)
|
||||
|
||||
if item_det.has_serial_no == 0:
|
||||
if serial_nos:
|
||||
frappe.throw(
|
||||
_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code),
|
||||
SerialNoNotRequiredError,
|
||||
)
|
||||
elif not sle.is_cancelled:
|
||||
return
|
||||
if serial_nos:
|
||||
if cint(sle.actual_qty) != flt(sle.actual_qty):
|
||||
frappe.throw(
|
||||
_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty)
|
||||
)
|
||||
|
||||
if len(serial_nos) and len(serial_nos) != abs(cint(sle.actual_qty)):
|
||||
frappe.throw(
|
||||
_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(
|
||||
abs(sle.actual_qty), sle.item_code, len(serial_nos)
|
||||
),
|
||||
SerialNoQtyError,
|
||||
)
|
||||
|
||||
if len(serial_nos) != len(set(serial_nos)):
|
||||
frappe.throw(
|
||||
_("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError
|
||||
)
|
||||
|
||||
for serial_no in serial_nos:
|
||||
if frappe.db.exists("Serial No", serial_no):
|
||||
sr = frappe.db.get_value(
|
||||
"Serial No",
|
||||
serial_no,
|
||||
[
|
||||
"name",
|
||||
"item_code",
|
||||
"batch_no",
|
||||
"sales_order",
|
||||
"delivery_document_no",
|
||||
"delivery_document_type",
|
||||
"warehouse",
|
||||
"purchase_document_type",
|
||||
"purchase_document_no",
|
||||
"company",
|
||||
"status",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if sr.item_code != sle.item_code:
|
||||
if not allow_serial_nos_with_different_item(serial_no, sle):
|
||||
frappe.throw(
|
||||
_("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code),
|
||||
SerialNoItemError,
|
||||
)
|
||||
|
||||
if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle):
|
||||
doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no))
|
||||
frappe.throw(
|
||||
_("Serial No {0} has already been received in the {1} #{2}").format(
|
||||
frappe.bold(serial_no), sr.purchase_document_type, doc_name
|
||||
),
|
||||
SerialNoDuplicateError,
|
||||
)
|
||||
|
||||
if (
|
||||
sr.delivery_document_no
|
||||
and sle.voucher_type not in ["Stock Entry", "Stock Reconciliation"]
|
||||
and sle.voucher_type == sr.delivery_document_type
|
||||
):
|
||||
return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, "return_against")
|
||||
if return_against and return_against != sr.delivery_document_no:
|
||||
frappe.throw(_("Serial no {0} has been already returned").format(sr.name))
|
||||
|
||||
if cint(sle.actual_qty) < 0:
|
||||
if sr.warehouse != sle.warehouse:
|
||||
frappe.throw(
|
||||
_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse),
|
||||
SerialNoWarehouseError,
|
||||
)
|
||||
|
||||
if not sr.purchase_document_no:
|
||||
frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError)
|
||||
|
||||
if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
|
||||
|
||||
if sr.batch_no and sr.batch_no != sle.batch_no:
|
||||
frappe.throw(
|
||||
_("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no),
|
||||
SerialNoBatchError,
|
||||
)
|
||||
|
||||
if not sle.is_cancelled and not sr.warehouse:
|
||||
frappe.throw(
|
||||
_("Serial No {0} does not belong to any Warehouse").format(serial_no),
|
||||
SerialNoWarehouseError,
|
||||
)
|
||||
|
||||
# if Sales Order reference in Serial No validate the Delivery Note or Invoice is against the same
|
||||
if sr.sales_order:
|
||||
if sle.voucher_type == "Sales Invoice":
|
||||
if not frappe.db.exists(
|
||||
"Sales Invoice Item",
|
||||
{"parent": sle.voucher_no, "item_code": sle.item_code, "sales_order": sr.sales_order},
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}"
|
||||
).format(sr.name, sle.item_code, sr.sales_order)
|
||||
)
|
||||
elif sle.voucher_type == "Delivery Note":
|
||||
if not frappe.db.exists(
|
||||
"Delivery Note Item",
|
||||
{
|
||||
"parent": sle.voucher_no,
|
||||
"item_code": sle.item_code,
|
||||
"against_sales_order": sr.sales_order,
|
||||
},
|
||||
):
|
||||
invoice = frappe.db.get_value(
|
||||
"Delivery Note Item",
|
||||
{"parent": sle.voucher_no, "item_code": sle.item_code},
|
||||
"against_sales_invoice",
|
||||
)
|
||||
if not invoice or frappe.db.exists(
|
||||
"Sales Invoice Item",
|
||||
{"parent": invoice, "item_code": sle.item_code, "sales_order": sr.sales_order},
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}"
|
||||
).format(sr.name, sle.item_code, sr.sales_order)
|
||||
)
|
||||
# if Sales Order reference in Delivery Note or Invoice validate SO reservations for item
|
||||
if sle.voucher_type == "Sales Invoice":
|
||||
sales_order = frappe.db.get_value(
|
||||
"Sales Invoice Item",
|
||||
{"parent": sle.voucher_no, "item_code": sle.item_code},
|
||||
"sales_order",
|
||||
)
|
||||
if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
|
||||
validate_so_serial_no(sr, sales_order)
|
||||
elif sle.voucher_type == "Delivery Note":
|
||||
sales_order = frappe.get_value(
|
||||
"Delivery Note Item",
|
||||
{"parent": sle.voucher_no, "item_code": sle.item_code},
|
||||
"against_sales_order",
|
||||
)
|
||||
if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
|
||||
validate_so_serial_no(sr, sales_order)
|
||||
else:
|
||||
sales_invoice = frappe.get_value(
|
||||
"Delivery Note Item",
|
||||
{"parent": sle.voucher_no, "item_code": sle.item_code},
|
||||
"against_sales_invoice",
|
||||
)
|
||||
if sales_invoice:
|
||||
sales_order = frappe.db.get_value(
|
||||
"Sales Invoice Item",
|
||||
{"parent": sales_invoice, "item_code": sle.item_code},
|
||||
"sales_order",
|
||||
)
|
||||
if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
|
||||
validate_so_serial_no(sr, sales_order)
|
||||
elif cint(sle.actual_qty) < 0:
|
||||
# transfer out
|
||||
frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError)
|
||||
elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series:
|
||||
frappe.throw(
|
||||
_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError
|
||||
)
|
||||
elif serial_nos:
|
||||
return
|
||||
# SLE is being cancelled and has serial nos
|
||||
for serial_no in serial_nos:
|
||||
check_serial_no_validity_on_cancel(serial_no, sle)
|
||||
|
||||
|
||||
def check_serial_no_validity_on_cancel(serial_no, sle):
|
||||
sr = frappe.db.get_value(
|
||||
"Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1
|
||||
)
|
||||
sr_link = frappe.utils.get_link_to_form("Serial No", serial_no)
|
||||
doc_link = frappe.utils.get_link_to_form(sle.voucher_type, sle.voucher_no)
|
||||
actual_qty = cint(sle.actual_qty)
|
||||
is_stock_reco = sle.voucher_type == "Stock Reconciliation"
|
||||
msg = None
|
||||
|
||||
if sr and (actual_qty < 0 or is_stock_reco) and (sr.warehouse and sr.warehouse != sle.warehouse):
|
||||
# receipt(inward) is being cancelled
|
||||
msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format(
|
||||
sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse)
|
||||
)
|
||||
elif sr and actual_qty > 0 and not is_stock_reco:
|
||||
# delivery is being cancelled, check for warehouse.
|
||||
if sr.warehouse:
|
||||
# serial no is active in another warehouse/company.
|
||||
msg = _("Cannot cancel {0} {1} as Serial No {2} is active in warehouse {3}").format(
|
||||
sle.voucher_type, doc_link, sr_link, frappe.bold(sr.warehouse)
|
||||
)
|
||||
elif sr.company != sle.company and sr.status == "Delivered":
|
||||
# serial no is inactive (allowed) or delivered from another company (block).
|
||||
msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the company {3}").format(
|
||||
sle.voucher_type, doc_link, sr_link, frappe.bold(sle.company)
|
||||
)
|
||||
|
||||
if msg:
|
||||
frappe.throw(msg, title=_("Cannot cancel"))
|
||||
|
||||
|
||||
def validate_material_transfer_entry(sle_doc):
|
||||
sle_doc.update({"skip_update_serial_no": False, "skip_serial_no_validaiton": False})
|
||||
|
||||
if (
|
||||
sle_doc.voucher_type == "Stock Entry"
|
||||
and not sle_doc.is_cancelled
|
||||
and frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"
|
||||
):
|
||||
if sle_doc.actual_qty < 0:
|
||||
sle_doc.skip_update_serial_no = True
|
||||
else:
|
||||
sle_doc.skip_serial_no_validaiton = True
|
||||
|
||||
|
||||
def validate_so_serial_no(sr, sales_order):
|
||||
if not sr.sales_order or sr.sales_order != sales_order:
|
||||
msg = _(
|
||||
"Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}."
|
||||
).format(sales_order, sr.item_code)
|
||||
|
||||
frappe.throw(_("""{0} Serial No {1} cannot be delivered""").format(msg, sr.name))
|
||||
|
||||
|
||||
def has_serial_no_exists(sn, sle):
|
||||
if (
|
||||
sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation"
|
||||
):
|
||||
return True
|
||||
|
||||
if sn.company != sle.company:
|
||||
return False
|
||||
|
||||
|
||||
def allow_serial_nos_with_different_item(sle_serial_no, sle):
|
||||
"""
|
||||
Allows same serial nos for raw materials and finished goods
|
||||
in Manufacture / Repack type Stock Entry
|
||||
"""
|
||||
allow_serial_nos = False
|
||||
if sle.voucher_type == "Stock Entry" and cint(sle.actual_qty) > 0:
|
||||
stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no)
|
||||
if stock_entry.purpose in ("Repack", "Manufacture"):
|
||||
for d in stock_entry.get("items"):
|
||||
if d.serial_no and (d.s_warehouse if not sle.is_cancelled else d.t_warehouse):
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
if sle_serial_no in serial_nos:
|
||||
allow_serial_nos = True
|
||||
|
||||
return allow_serial_nos
|
||||
|
||||
|
||||
def update_warehouse_in_serial_no(sle, item_det):
|
||||
serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
|
||||
serial_no_data = get_serial_nos_warehouse(sle.item_code, serial_nos)
|
||||
|
||||
if not serial_no_data:
|
||||
for serial_no in serial_nos:
|
||||
frappe.db.set_value("Serial No", serial_no, "warehouse", None)
|
||||
|
||||
else:
|
||||
for row in serial_no_data:
|
||||
if not row.serial_no:
|
||||
continue
|
||||
|
||||
warehouse = row.warehouse if row.actual_qty > 0 else None
|
||||
frappe.db.set_value("Serial No", row.serial_no, "warehouse", warehouse)
|
||||
|
||||
|
||||
def get_serial_nos_warehouse(item_code, serial_nos):
|
||||
ledger_table = frappe.qb.DocType("Serial and Batch Ledger")
|
||||
sle_table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(ledger_table)
|
||||
.inner_join(sle_table)
|
||||
.on(ledger_table.parent == sle_table.serial_and_batch_bundle)
|
||||
.select(
|
||||
ledger_table.serial_no,
|
||||
sle_table.actual_qty,
|
||||
ledger_table.warehouse,
|
||||
)
|
||||
.where(
|
||||
(ledger_table.serial_no.isin(serial_nos))
|
||||
& (sle_table.is_cancelled == 0)
|
||||
& (sle_table.item_code == item_code)
|
||||
& (sle_table.serial_and_batch_bundle.isnotnull())
|
||||
)
|
||||
.orderby(sle_table.posting_date, order=frappe.qb.desc)
|
||||
.orderby(sle_table.posting_time, order=frappe.qb.desc)
|
||||
.orderby(sle_table.creation, order=frappe.qb.desc)
|
||||
.groupby(ledger_table.serial_no)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def create_batch_for_serial_no(sle):
|
||||
from erpnext.stock.doctype.batch.batch import make_batch
|
||||
|
||||
return make_batch(
|
||||
frappe._dict(
|
||||
{
|
||||
"item": sle.item_code,
|
||||
"reference_doctype": sle.voucher_type,
|
||||
"reference_name": sle.voucher_no,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def auto_create_serial_nos(sle, item_details) -> List[str]:
|
||||
sr_nos = []
|
||||
serial_nos_details = []
|
||||
current_series = frappe.db.sql(
|
||||
"select current from `tabSeries` where name = %s", item_details.serial_no_series
|
||||
)
|
||||
|
||||
for i in range(cint(sle.actual_qty)):
|
||||
serial_no = make_autoname(item_details.serial_no_series, "Serial No")
|
||||
sr_nos.append(serial_no)
|
||||
serial_nos_details.append(
|
||||
(
|
||||
serial_no,
|
||||
serial_no,
|
||||
now(),
|
||||
now(),
|
||||
frappe.session.user,
|
||||
frappe.session.user,
|
||||
sle.warehouse,
|
||||
sle.company,
|
||||
sle.item_code,
|
||||
item_details.item_name,
|
||||
item_details.description,
|
||||
)
|
||||
)
|
||||
|
||||
if serial_nos_details:
|
||||
fields = [
|
||||
"name",
|
||||
"serial_no",
|
||||
"creation",
|
||||
"modified",
|
||||
"owner",
|
||||
"modified_by",
|
||||
"warehouse",
|
||||
"company",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"description",
|
||||
]
|
||||
|
||||
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
|
||||
|
||||
return sr_nos
|
||||
|
||||
|
||||
def get_auto_serial_nos(serial_no_series, qty):
|
||||
def get_auto_serial_nos(serial_no_series, qty) -> List[str]:
|
||||
serial_nos = []
|
||||
for i in range(cint(qty)):
|
||||
serial_nos.append(get_new_serial_number(serial_no_series))
|
||||
|
||||
return "\n".join(serial_nos)
|
||||
return serial_nos
|
||||
|
||||
|
||||
def get_new_serial_number(series):
|
||||
@ -534,72 +161,6 @@ def clean_serial_no_string(serial_no: str) -> str:
|
||||
return "\n".join(serial_no_list)
|
||||
|
||||
|
||||
def update_serial_nos_after_submit(controller, parentfield):
|
||||
return
|
||||
stock_ledger_entries = frappe.db.sql(
|
||||
"""select voucher_detail_no, serial_no, actual_qty, warehouse
|
||||
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""",
|
||||
(controller.doctype, controller.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not stock_ledger_entries:
|
||||
return
|
||||
|
||||
for d in controller.get(parentfield):
|
||||
if d.serial_no:
|
||||
continue
|
||||
|
||||
update_rejected_serial_nos = (
|
||||
True
|
||||
if (
|
||||
controller.doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt")
|
||||
and d.rejected_qty
|
||||
)
|
||||
else False
|
||||
)
|
||||
accepted_serial_nos_updated = False
|
||||
|
||||
if controller.doctype == "Stock Entry":
|
||||
warehouse = d.t_warehouse
|
||||
qty = d.transfer_qty
|
||||
elif controller.doctype in ("Sales Invoice", "Delivery Note"):
|
||||
warehouse = d.warehouse
|
||||
qty = d.stock_qty
|
||||
else:
|
||||
warehouse = d.warehouse
|
||||
qty = (
|
||||
d.qty
|
||||
if controller.doctype in ["Stock Reconciliation", "Subcontracting Receipt"]
|
||||
else d.stock_qty
|
||||
)
|
||||
for sle in stock_ledger_entries:
|
||||
if sle.voucher_detail_no == d.name:
|
||||
if (
|
||||
not accepted_serial_nos_updated
|
||||
and qty
|
||||
and abs(sle.actual_qty) == abs(qty)
|
||||
and sle.warehouse == warehouse
|
||||
and sle.serial_no != d.serial_no
|
||||
):
|
||||
d.serial_no = sle.serial_no
|
||||
frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no)
|
||||
accepted_serial_nos_updated = True
|
||||
if not update_rejected_serial_nos:
|
||||
break
|
||||
elif (
|
||||
update_rejected_serial_nos
|
||||
and abs(sle.actual_qty) == d.rejected_qty
|
||||
and sle.warehouse == d.rejected_warehouse
|
||||
and sle.serial_no != d.rejected_serial_no
|
||||
):
|
||||
d.rejected_serial_no = sle.serial_no
|
||||
frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no)
|
||||
update_rejected_serial_nos = False
|
||||
if accepted_serial_nos_updated:
|
||||
break
|
||||
|
||||
|
||||
def update_maintenance_status():
|
||||
serial_nos = frappe.db.sql(
|
||||
"""select name from `tabSerial No` where (amc_expiry_date<%s or
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@ -37,8 +38,8 @@ from erpnext.stock.get_item_details import (
|
||||
get_bin_details,
|
||||
get_conversion_factor,
|
||||
get_default_cost_center,
|
||||
get_reserved_qty_for_so,
|
||||
)
|
||||
from erpnext.stock.serial_batch_bundle import get_empty_batches_based_work_order
|
||||
from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get_valuation_rate
|
||||
from erpnext.stock.utils import get_bin, get_incoming_rate
|
||||
|
||||
@ -203,13 +204,9 @@ class StockEntry(StockController):
|
||||
|
||||
self.repost_future_sle_and_gle()
|
||||
self.update_cost_in_project()
|
||||
self.validate_reserved_serial_no_consumption()
|
||||
self.update_transferred_qty()
|
||||
self.update_quality_inspection()
|
||||
|
||||
if self.work_order and self.purpose == "Manufacture":
|
||||
self.update_so_in_serial_number()
|
||||
|
||||
if self.purpose == "Material Transfer" and self.add_to_transit:
|
||||
self.set_material_request_transfer_status("In Transit")
|
||||
if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
|
||||
@ -359,7 +356,6 @@ class StockEntry(StockController):
|
||||
|
||||
def validate_item(self):
|
||||
stock_items = self.get_stock_items()
|
||||
serialized_items = self.get_serialized_items()
|
||||
for item in self.get("items"):
|
||||
if flt(item.qty) and flt(item.qty) < 0:
|
||||
frappe.throw(
|
||||
@ -401,16 +397,6 @@ class StockEntry(StockController):
|
||||
flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
|
||||
)
|
||||
|
||||
# if (
|
||||
# self.purpose in ("Material Transfer", "Material Transfer for Manufacture")
|
||||
# and not item.serial_and_batch_bundle
|
||||
# and item.item_code in serialized_items
|
||||
# ):
|
||||
# frappe.throw(
|
||||
# _("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
|
||||
# frappe.MandatoryError,
|
||||
# )
|
||||
|
||||
def validate_qty(self):
|
||||
manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"]
|
||||
|
||||
@ -1352,7 +1338,6 @@ class StockEntry(StockController):
|
||||
pro_doc.run_method("update_work_order_qty")
|
||||
if self.purpose == "Manufacture":
|
||||
pro_doc.run_method("update_planned_qty")
|
||||
pro_doc.update_batch_produced_qty(self)
|
||||
|
||||
pro_doc.run_method("update_status")
|
||||
if not pro_doc.operations:
|
||||
@ -1479,8 +1464,6 @@ class StockEntry(StockController):
|
||||
"ste_detail": d.name,
|
||||
"stock_uom": d.stock_uom,
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"serial_no": d.serial_no,
|
||||
"batch_no": d.batch_no,
|
||||
},
|
||||
)
|
||||
|
||||
@ -1651,6 +1634,7 @@ class StockEntry(StockController):
|
||||
if (
|
||||
self.work_order
|
||||
and self.pro_doc.has_batch_no
|
||||
and not self.pro_doc.has_serial_no
|
||||
and cint(
|
||||
frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "make_serial_no_batch_from_work_order", cache=True
|
||||
@ -1662,42 +1646,34 @@ class StockEntry(StockController):
|
||||
self.add_finished_goods(args, item)
|
||||
|
||||
def set_batchwise_finished_goods(self, args, item):
|
||||
filters = {
|
||||
"reference_name": self.pro_doc.name,
|
||||
"reference_doctype": self.pro_doc.doctype,
|
||||
"qty_to_produce": (">", 0),
|
||||
"batch_qty": ("=", 0),
|
||||
}
|
||||
batches = get_empty_batches_based_work_order(self.work_order, self.pro_doc.production_item)
|
||||
|
||||
fields = ["qty_to_produce as qty", "produced_qty", "name"]
|
||||
|
||||
data = frappe.get_all("Batch", filters=filters, fields=fields, order_by="creation asc")
|
||||
|
||||
if not data:
|
||||
if not batches:
|
||||
self.add_finished_goods(args, item)
|
||||
else:
|
||||
self.add_batchwise_finished_good(data, args, item)
|
||||
self.add_batchwise_finished_good(batches, args, item)
|
||||
|
||||
def add_batchwise_finished_good(self, data, args, item):
|
||||
def add_batchwise_finished_good(self, batches, args, item):
|
||||
qty = flt(self.fg_completed_qty)
|
||||
row = frappe._dict({"batches_to_be_consume": defaultdict(float)})
|
||||
|
||||
for row in data:
|
||||
batch_qty = flt(row.qty) - flt(row.produced_qty)
|
||||
if not batch_qty:
|
||||
continue
|
||||
self.update_batches_to_be_consume(batches, row, qty)
|
||||
|
||||
if qty <= 0:
|
||||
break
|
||||
if not row.batches_to_be_consume:
|
||||
return
|
||||
|
||||
fg_qty = batch_qty
|
||||
if batch_qty >= qty:
|
||||
fg_qty = qty
|
||||
id = create_serial_and_batch_bundle(
|
||||
row,
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": self.pro_doc.production_item,
|
||||
"warehouse": args.get("to_warehouse"),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
qty -= batch_qty
|
||||
args["qty"] = fg_qty
|
||||
args["batch_no"] = row.name
|
||||
|
||||
self.add_finished_goods(args, item)
|
||||
args["serial_and_batch_bundle"] = id
|
||||
self.add_finished_goods(args, item)
|
||||
|
||||
def add_finished_goods(self, args, item):
|
||||
self.add_to_stock_entry_detail({item.name: args}, bom_no=self.bom_no)
|
||||
@ -1902,27 +1878,8 @@ class StockEntry(StockController):
|
||||
|
||||
if row.batch_details:
|
||||
row.batches_to_be_consume = defaultdict(float)
|
||||
batches = sorted(row.batch_details.items(), key=lambda x: x[0])
|
||||
qty_to_be_consumed = qty
|
||||
for batch_no, batch_qty in batches:
|
||||
if qty_to_be_consumed <= 0 or batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if batch_qty > qty_to_be_consumed:
|
||||
batch_qty = qty_to_be_consumed
|
||||
|
||||
row.batches_to_be_consume[batch_no] += batch_qty
|
||||
|
||||
if batch_no and row.serial_nos:
|
||||
serial_nos = self.get_serial_nos_based_on_transferred_batch(batch_no, row.serial_nos)
|
||||
serial_nos = serial_nos[0 : cint(batch_qty)]
|
||||
|
||||
# remove consumed serial nos from list
|
||||
for sn in serial_nos:
|
||||
row.serial_nos.remove(sn)
|
||||
|
||||
row.batch_details[batch_no] -= batch_qty
|
||||
qty_to_be_consumed -= batch_qty
|
||||
batches = row.batch_details
|
||||
self.update_batches_to_be_consume(batches, row, qty)
|
||||
|
||||
elif row.serial_nos:
|
||||
serial_nos = row.serial_nos[0 : cint(qty)]
|
||||
@ -1930,6 +1887,32 @@ class StockEntry(StockController):
|
||||
|
||||
self.update_item_in_stock_entry_detail(row, item, qty)
|
||||
|
||||
def update_batches_to_be_consume(self, batches, row, qty):
|
||||
qty_to_be_consumed = qty
|
||||
batches = sorted(batches.items(), key=lambda x: x[0])
|
||||
|
||||
for batch_no, batch_qty in batches:
|
||||
if qty_to_be_consumed <= 0 or batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if batch_qty > qty_to_be_consumed:
|
||||
batch_qty = qty_to_be_consumed
|
||||
|
||||
row.batches_to_be_consume[batch_no] += batch_qty
|
||||
|
||||
if batch_no and row.serial_nos:
|
||||
serial_nos = self.get_serial_nos_based_on_transferred_batch(batch_no, row.serial_nos)
|
||||
serial_nos = serial_nos[0 : cint(batch_qty)]
|
||||
|
||||
# remove consumed serial nos from list
|
||||
for sn in serial_nos:
|
||||
row.serial_nos.remove(sn)
|
||||
|
||||
if "batch_details" in row:
|
||||
row.batch_details[batch_no] -= batch_qty
|
||||
|
||||
qty_to_be_consumed -= batch_qty
|
||||
|
||||
def update_item_in_stock_entry_detail(self, row, item, qty) -> None:
|
||||
if not qty:
|
||||
return
|
||||
@ -1939,7 +1922,7 @@ class StockEntry(StockController):
|
||||
"to_warehouse": "",
|
||||
"qty": qty,
|
||||
"item_name": item.item_name,
|
||||
"serial_and_batch_bundle": create_serial_and_batch_bundle(row, item),
|
||||
"serial_and_batch_bundle": create_serial_and_batch_bundle(row, item, "Outward"),
|
||||
"description": item.description,
|
||||
"stock_uom": item.stock_uom,
|
||||
"expense_account": item.expense_account,
|
||||
@ -2099,8 +2082,6 @@ class StockEntry(StockController):
|
||||
"expense_account",
|
||||
"description",
|
||||
"item_name",
|
||||
"serial_no",
|
||||
"batch_no",
|
||||
"serial_and_batch_bundle",
|
||||
"allow_zero_valuation_rate",
|
||||
]:
|
||||
@ -2210,42 +2191,6 @@ class StockEntry(StockController):
|
||||
stock_bin = get_bin(item_code, reserve_warehouse)
|
||||
stock_bin.update_reserved_qty_for_sub_contracting()
|
||||
|
||||
def update_so_in_serial_number(self):
|
||||
so_name, item_code = frappe.db.get_value(
|
||||
"Work Order", self.work_order, ["sales_order", "production_item"]
|
||||
)
|
||||
if so_name and item_code:
|
||||
qty_to_reserve = get_reserved_qty_for_so(so_name, item_code)
|
||||
if qty_to_reserve:
|
||||
reserved_qty = frappe.db.sql(
|
||||
"""select count(name) from `tabSerial No` where item_code=%s and
|
||||
sales_order=%s""",
|
||||
(item_code, so_name),
|
||||
)
|
||||
if reserved_qty and reserved_qty[0][0]:
|
||||
qty_to_reserve -= reserved_qty[0][0]
|
||||
if qty_to_reserve > 0:
|
||||
for item in self.items:
|
||||
has_serial_no = frappe.get_cached_value("Item", item.item_code, "has_serial_no")
|
||||
if item.item_code == item_code and has_serial_no:
|
||||
serial_nos = (item.serial_no).split("\n")
|
||||
for serial_no in serial_nos:
|
||||
if qty_to_reserve > 0:
|
||||
frappe.db.set_value("Serial No", serial_no, "sales_order", so_name)
|
||||
qty_to_reserve -= 1
|
||||
|
||||
def validate_reserved_serial_no_consumption(self):
|
||||
for item in self.items:
|
||||
if item.s_warehouse and not item.t_warehouse and item.serial_no:
|
||||
for sr in get_serial_nos(item.serial_no):
|
||||
sales_order = frappe.db.get_value("Serial No", sr, "sales_order")
|
||||
if sales_order:
|
||||
msg = _(
|
||||
"(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}."
|
||||
).format(sr, sales_order)
|
||||
|
||||
frappe.throw(_("Item {0} {1}").format(item.item_code, msg))
|
||||
|
||||
def update_transferred_qty(self):
|
||||
if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
|
||||
stock_entries = {}
|
||||
@ -2338,40 +2283,48 @@ class StockEntry(StockController):
|
||||
frappe.db.set_value("Material Request", material_request, "transfer_status", status)
|
||||
|
||||
def set_serial_no_batch_for_finished_good(self):
|
||||
serial_nos = []
|
||||
if self.pro_doc.serial_no:
|
||||
serial_nos = self.get_serial_nos_for_fg() or []
|
||||
if not (
|
||||
(self.pro_doc.has_serial_no or self.pro_doc.has_batch_no)
|
||||
and frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")
|
||||
):
|
||||
return
|
||||
|
||||
for row in self.items:
|
||||
if row.is_finished_item and row.item_code == self.pro_doc.production_item:
|
||||
for d in self.items:
|
||||
if d.is_finished_item and d.item_code == self.pro_doc.production_item:
|
||||
serial_nos = self.get_available_serial_nos()
|
||||
if serial_nos:
|
||||
row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)])
|
||||
row = frappe._dict({"serial_nos": serial_nos[0 : cint(d.qty)]})
|
||||
|
||||
def get_serial_nos_for_fg(self):
|
||||
fields = [
|
||||
"`tabStock Entry`.`name`",
|
||||
"`tabStock Entry Detail`.`qty`",
|
||||
"`tabStock Entry Detail`.`serial_no`",
|
||||
"`tabStock Entry Detail`.`batch_no`",
|
||||
]
|
||||
id = create_serial_and_batch_bundle(
|
||||
row,
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.t_warehouse,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
filters = [
|
||||
["Stock Entry", "work_order", "=", self.work_order],
|
||||
["Stock Entry", "purpose", "=", "Manufacture"],
|
||||
["Stock Entry", "docstatus", "<", 2],
|
||||
["Stock Entry Detail", "item_code", "=", self.pro_doc.production_item],
|
||||
]
|
||||
d.serial_and_batch_bundle = id
|
||||
|
||||
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
|
||||
return self.get_available_serial_nos(stock_entries)
|
||||
def get_available_serial_nos(self) -> List[str]:
|
||||
serial_nos = []
|
||||
data = frappe.get_all(
|
||||
"Serial No",
|
||||
filters={
|
||||
"item_code": self.pro_doc.production_item,
|
||||
"warehouse": ("is", "not set"),
|
||||
"status": "Inactive",
|
||||
"work_order": self.pro_doc.name,
|
||||
},
|
||||
fields=["name"],
|
||||
order_by="creation asc",
|
||||
)
|
||||
|
||||
def get_available_serial_nos(self, stock_entries):
|
||||
used_serial_nos = []
|
||||
for row in stock_entries:
|
||||
if row.serial_no:
|
||||
used_serial_nos.extend(get_serial_nos(row.serial_no))
|
||||
for row in data:
|
||||
serial_nos.append(row.name)
|
||||
|
||||
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
|
||||
return serial_nos
|
||||
|
||||
def update_subcontracting_order_status(self):
|
||||
if self.subcontracting_order and self.purpose in ["Send to Subcontractor", "Material Transfer"]:
|
||||
@ -2847,14 +2800,24 @@ def get_stock_entry_data(work_order):
|
||||
return data
|
||||
|
||||
|
||||
def create_serial_and_batch_bundle(row, child):
|
||||
def create_serial_and_batch_bundle(row, child, type_of_transaction=None):
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
||||
)
|
||||
|
||||
if not (item_details.has_serial_no or item_details.has_batch_no):
|
||||
return
|
||||
|
||||
if not type_of_transaction:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial and Batch Bundle",
|
||||
"voucher_type": "Stock Entry",
|
||||
"item_code": child.item_code,
|
||||
"warehouse": child.warehouse,
|
||||
"type_of_transaction": "Outward",
|
||||
"type_of_transaction": type_of_transaction,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -127,8 +127,6 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
||||
|
||||
out.update(data)
|
||||
|
||||
update_stock(args, out)
|
||||
|
||||
if args.transaction_date and item.lead_time_days:
|
||||
out.schedule_date = out.lead_time_date = add_days(args.transaction_date, item.lead_time_days)
|
||||
|
||||
@ -150,28 +148,6 @@ def remove_standard_fields(details):
|
||||
return details
|
||||
|
||||
|
||||
def update_stock(args, out):
|
||||
if (
|
||||
(
|
||||
args.get("doctype") == "Delivery Note"
|
||||
or (args.get("doctype") == "Sales Invoice" and args.get("update_stock"))
|
||||
)
|
||||
and out.warehouse
|
||||
and out.stock_qty > 0
|
||||
):
|
||||
if out.has_serial_no and args.get("batch_no"):
|
||||
reserved_so = get_so_reservation_for_item(args)
|
||||
out.batch_no = args.get("batch_no")
|
||||
out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so)
|
||||
|
||||
elif out.has_serial_no:
|
||||
reserved_so = get_so_reservation_for_item(args)
|
||||
out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so)
|
||||
|
||||
if not out.serial_no:
|
||||
out.pop("serial_no", None)
|
||||
|
||||
|
||||
def set_valuation_rate(out, args):
|
||||
if frappe.db.exists("Product Bundle", args.item_code, cache=True):
|
||||
valuation_rate = 0.0
|
||||
@ -1490,41 +1466,3 @@ def get_blanket_order_details(args):
|
||||
blanket_order_details = blanket_order_details[0] if blanket_order_details else ""
|
||||
|
||||
return blanket_order_details
|
||||
|
||||
|
||||
def get_so_reservation_for_item(args):
|
||||
reserved_so = None
|
||||
if args.get("against_sales_order"):
|
||||
if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")):
|
||||
reserved_so = args.get("against_sales_order")
|
||||
elif args.get("against_sales_invoice"):
|
||||
sales_order = frappe.db.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={
|
||||
"parent": args.get("against_sales_invoice"),
|
||||
"item_code": args.get("item_code"),
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields="sales_order",
|
||||
)
|
||||
if sales_order and sales_order[0]:
|
||||
if get_reserved_qty_for_so(sales_order[0].sales_order, args.get("item_code")):
|
||||
reserved_so = sales_order[0]
|
||||
elif args.get("sales_order"):
|
||||
if get_reserved_qty_for_so(args.get("sales_order"), args.get("item_code")):
|
||||
reserved_so = args.get("sales_order")
|
||||
return reserved_so
|
||||
|
||||
|
||||
def get_reserved_qty_for_so(sales_order, item_code):
|
||||
reserved_qty = frappe.db.get_value(
|
||||
"Sales Order Item",
|
||||
filters={
|
||||
"parent": sales_order,
|
||||
"item_code": item_code,
|
||||
"ensure_delivery_based_on_produced_serial_no": 1,
|
||||
},
|
||||
fieldname="sum(qty)",
|
||||
)
|
||||
|
||||
return reserved_qty or 0
|
||||
|
@ -586,3 +586,62 @@ class BatchNoBundleValuation(DeprecatedBatchNoValuation):
|
||||
|
||||
def get_incoming_rate(self):
|
||||
return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
|
||||
|
||||
|
||||
def get_empty_batches_based_work_order(work_order, item_code):
|
||||
batches = get_batches_from_work_order(work_order)
|
||||
if not batches:
|
||||
return batches
|
||||
|
||||
entries = get_batches_from_stock_entries(work_order)
|
||||
if not entries:
|
||||
return batches
|
||||
|
||||
ids = [d.serial_and_batch_bundle for d in entries if d.serial_and_batch_bundle]
|
||||
if ids:
|
||||
set_batch_details_from_package(ids, batches)
|
||||
|
||||
# Will be deprecated in v16
|
||||
for d in entries:
|
||||
if not d.batch_no:
|
||||
continue
|
||||
|
||||
batches[d.batch_no] -= d.qty
|
||||
|
||||
return batches
|
||||
|
||||
|
||||
def get_batches_from_work_order(work_order):
|
||||
return frappe._dict(
|
||||
frappe.get_all(
|
||||
"Batch", fields=["name", "qty_to_produce"], filters={"reference_name": work_order}, as_list=1
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_batches_from_stock_entries(work_order):
|
||||
entries = frappe.get_all(
|
||||
"Stock Entry",
|
||||
filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
|
||||
fields=["name"],
|
||||
)
|
||||
|
||||
return frappe.get_all(
|
||||
"Stock Entry Detail",
|
||||
fields=["batch_no", "qty", "serial_and_batch_bundle"],
|
||||
filters={
|
||||
"parent": ("in", [d.name for d in entries]),
|
||||
"is_finished_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def set_batch_details_from_package(ids, batches):
|
||||
entries = frappe.get_all(
|
||||
"Serial and Batch Ledger",
|
||||
filters={"parent": ("in", ids), "is_outward": 0},
|
||||
fields=["batch_no", "qty"],
|
||||
)
|
||||
|
||||
for d in entries:
|
||||
batches[d.batch_no] -= d.qty
|
||||
|
Loading…
x
Reference in New Issue
Block a user