mirror of
https://github.com/frappe/erpnext.git
synced 2025-12-03 18:35:36 +00:00
fix: add validation for FG Items as per BOM qty (#50579)
Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit d01c4b68fe)
# Conflicts:
# erpnext/buying/doctype/buying_settings/buying_settings.json
# erpnext/buying/doctype/buying_settings/buying_settings.py
# erpnext/controllers/subcontracting_controller.py
# erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
# erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
This commit is contained in:
@@ -29,7 +29,18 @@
|
||||
"subcontract",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
<<<<<<< HEAD
|
||||
"over_transfer_allowance"
|
||||
=======
|
||||
"over_transfer_allowance",
|
||||
"validate_consumed_qty",
|
||||
"section_break_xcug",
|
||||
"auto_create_subcontracting_order",
|
||||
"column_break_izrr",
|
||||
"auto_create_purchase_receipt",
|
||||
"request_for_quotation_tab",
|
||||
"fixed_email"
|
||||
>>>>>>> d01c4b68fe (fix: add validation for FG Items as per BOM qty (#50579))
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -181,6 +192,71 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Update frequency of Project",
|
||||
"options": "Each Transaction\nManual"
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Allows users to submit Purchase Orders with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
|
||||
"fieldname": "allow_zero_qty_in_purchase_order",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Purchase Order with Zero Quantity"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Allows users to submit Request for Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
|
||||
"fieldname": "allow_zero_qty_in_request_for_quotation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Request for Quotation with Zero Quantity"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Allows users to submit Supplier Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
|
||||
"fieldname": "allow_zero_qty_in_supplier_quotation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Supplier Quotation with Zero Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xmlt",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sbwq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_fcyl",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"description": "If enabled, the system will generate an accounting entry for materials rejected in the Purchase Receipt.",
|
||||
"fieldname": "set_valuation_rate_for_rejected_materials",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Valuation Rate for Rejected Materials"
|
||||
},
|
||||
{
|
||||
"fieldname": "request_for_quotation_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Request for Quotation"
|
||||
},
|
||||
{
|
||||
"description": "If set, the system does not use the user's Email or the standard outgoing Email account for sending request for quotations.",
|
||||
"fieldname": "fixed_email",
|
||||
"fieldtype": "Link",
|
||||
"label": "Fixed Outgoing Email Account",
|
||||
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
|
||||
"options": "Email Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
|
||||
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
|
||||
"fieldname": "validate_consumed_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Consumed Qty (as per BOM)"
|
||||
>>>>>>> d01c4b68fe (fix: add validation for FG Items as per BOM qty (#50579))
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@@ -188,7 +264,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
<<<<<<< HEAD
|
||||
"modified": "2024-01-12 16:42:01.894346",
|
||||
=======
|
||||
"modified": "2025-11-20 12:59:09.925862",
|
||||
>>>>>>> d01c4b68fe (fix: add validation for FG Items as per BOM qty (#50579))
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -9,6 +9,47 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class BuyingSettings(Document):
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
allow_multiple_items: DF.Check
|
||||
allow_zero_qty_in_purchase_order: DF.Check
|
||||
allow_zero_qty_in_request_for_quotation: DF.Check
|
||||
allow_zero_qty_in_supplier_quotation: DF.Check
|
||||
auto_create_purchase_receipt: DF.Check
|
||||
auto_create_subcontracting_order: DF.Check
|
||||
backflush_raw_materials_of_subcontract_based_on: DF.Literal[
|
||||
"BOM", "Material Transferred for Subcontract"
|
||||
]
|
||||
bill_for_rejected_quantity_in_purchase_invoice: DF.Check
|
||||
blanket_order_allowance: DF.Float
|
||||
buying_price_list: DF.Link | None
|
||||
disable_last_purchase_rate: DF.Check
|
||||
fixed_email: DF.Link | None
|
||||
maintain_same_rate: DF.Check
|
||||
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
||||
over_transfer_allowance: DF.Float
|
||||
po_required: DF.Literal["No", "Yes"]
|
||||
pr_required: DF.Literal["No", "Yes"]
|
||||
project_update_frequency: DF.Literal["Each Transaction", "Manual"]
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
set_landed_cost_based_on_purchase_invoice_rate: DF.Check
|
||||
set_valuation_rate_for_rejected_materials: DF.Check
|
||||
show_pay_button: DF.Check
|
||||
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
|
||||
supplier_group: DF.Link | None
|
||||
use_transaction_date_exchange_rate: DF.Check
|
||||
validate_consumed_qty: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
>>>>>>> d01c4b68fe (fix: add validation for FG Items as per BOM qty (#50579))
|
||||
def validate(self):
|
||||
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
|
||||
frappe.db.set_default(key, self.get(key, ""))
|
||||
|
||||
@@ -343,9 +343,23 @@ class SubcontractingController(StockController):
|
||||
|
||||
i += 1
|
||||
|
||||
<<<<<<< HEAD
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
doctype = "BOM Explosion Item" if exploded_item else "BOM Item"
|
||||
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
|
||||
=======
|
||||
def __remove_serial_and_batch_bundle(self, item):
|
||||
if item.get("serial_and_batch_bundle"):
|
||||
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
|
||||
|
||||
def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
data = []
|
||||
|
||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||
fields = [
|
||||
{"DIV": [f"`tab{doctype}`.`stock_qty`", "`tabBOM`.`quantity`"], "as": "qty_consumed_per_unit"}
|
||||
]
|
||||
>>>>>>> d01c4b68fe (fix: add validation for FG Items as per BOM qty (#50579))
|
||||
|
||||
alias_dict = {
|
||||
"item_code": "rm_item_code",
|
||||
@@ -371,7 +385,23 @@ class SubcontractingController(StockController):
|
||||
[doctype, "sourced_by_supplier", "=", 0],
|
||||
]
|
||||
|
||||
<<<<<<< HEAD
|
||||
return frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
|
||||
=======
|
||||
data = frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
|
||||
to_remove = []
|
||||
for item in data:
|
||||
if item.is_phantom_item:
|
||||
data += self._get_materials_from_bom(
|
||||
item.rm_item_code, item.bom_no, exploded_item=exploded_item
|
||||
)
|
||||
to_remove.append(item)
|
||||
|
||||
for item in to_remove:
|
||||
data.remove(item)
|
||||
|
||||
return data
|
||||
>>>>>>> d01c4b68fe (fix: add validation for FG Items as per BOM qty (#50579))
|
||||
|
||||
def __update_reserve_warehouse(self, row, item):
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
@@ -502,8 +532,15 @@ class SubcontractingController(StockController):
|
||||
):
|
||||
continue
|
||||
|
||||
<<<<<<< HEAD
|
||||
if self.doctype == self.subcontract_data.order_doctype or self.backflush_based_on == "BOM":
|
||||
for bom_item in self.__get_materials_from_bom(
|
||||
=======
|
||||
if self.doctype == self.subcontract_data.order_doctype or (
|
||||
self.backflush_based_on == "BOM" or self.is_return
|
||||
):
|
||||
for bom_item in self._get_materials_from_bom(
|
||||
>>>>>>> d01c4b68fe (fix: add validation for FG Items as per BOM qty (#50579))
|
||||
row.item_code, row.bom, row.get("include_exploded_items")
|
||||
):
|
||||
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, getdate, nowdate
|
||||
@@ -11,6 +13,10 @@ from erpnext.controllers.subcontracting_controller import SubcontractingControll
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
class BOMQuantityError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class SubcontractingReceipt(SubcontractingController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -94,6 +100,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_available_qty_for_consumption()
|
||||
self.validate_bom_required_qty()
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
self.set_subcontracting_order_status()
|
||||
@@ -298,9 +305,328 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
if self.company:
|
||||
expense_account = self.get_company_default("default_expense_account", ignore_validation=True)
|
||||
|
||||
<<<<<<< HEAD
|
||||
for item in self.items:
|
||||
if not item.expense_account:
|
||||
item.expense_account = expense_account
|
||||
=======
|
||||
def set_supplied_items_expense_account(self):
|
||||
for item in self.supplied_items:
|
||||
if not item.expense_account:
|
||||
item.expense_account = get_default_expense_account(
|
||||
frappe._dict(
|
||||
{
|
||||
"expense_account": self.get_company_default(
|
||||
"default_expense_account", ignore_validation=True
|
||||
)
|
||||
}
|
||||
),
|
||||
get_item_defaults(item.rm_item_code, self.company),
|
||||
get_item_group_defaults(item.rm_item_code, self.company),
|
||||
get_brand_defaults(item.rm_item_code, self.company),
|
||||
)
|
||||
|
||||
def reset_supplied_items(self):
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||
== "BOM"
|
||||
and self.supplied_items
|
||||
):
|
||||
if not any(
|
||||
item.serial_and_batch_bundle or item.batch_no or item.serial_no
|
||||
for item in self.supplied_items
|
||||
):
|
||||
self.supplied_items = []
|
||||
else:
|
||||
self.update_rate_for_supplied_items()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_scrap_items(self, recalculate_rate=False):
|
||||
self.remove_scrap_items()
|
||||
|
||||
for item in list(self.items):
|
||||
if item.bom:
|
||||
bom = frappe.get_doc("BOM", item.bom)
|
||||
for scrap_item in bom.scrap_items:
|
||||
qty = flt(item.qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity))
|
||||
rate = (
|
||||
get_valuation_rate(
|
||||
scrap_item.item_code,
|
||||
self.set_warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
currency=erpnext.get_company_currency(self.company),
|
||||
company=self.company,
|
||||
)
|
||||
or scrap_item.rate
|
||||
)
|
||||
self.append(
|
||||
"items",
|
||||
{
|
||||
"is_scrap_item": 1,
|
||||
"reference_name": item.name,
|
||||
"item_code": scrap_item.item_code,
|
||||
"item_name": scrap_item.item_name,
|
||||
"qty": qty,
|
||||
"stock_uom": scrap_item.stock_uom,
|
||||
"rate": rate,
|
||||
"rm_cost_per_qty": 0,
|
||||
"service_cost_per_qty": 0,
|
||||
"additional_cost_per_qty": 0,
|
||||
"scrap_cost_per_qty": 0,
|
||||
"amount": qty * rate,
|
||||
"warehouse": self.set_warehouse,
|
||||
"rejected_warehouse": self.rejected_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
if recalculate_rate:
|
||||
self.calculate_additional_costs()
|
||||
self.calculate_items_qty_and_amount()
|
||||
|
||||
def remove_scrap_items(self, recalculate_rate=False):
|
||||
for item in list(self.items):
|
||||
if item.is_scrap_item:
|
||||
self.remove(item)
|
||||
else:
|
||||
item.scrap_cost_per_qty = 0
|
||||
|
||||
if recalculate_rate:
|
||||
self.calculate_items_qty_and_amount()
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_missing_values(self):
|
||||
self.set_available_qty_for_consumption()
|
||||
self.calculate_additional_costs()
|
||||
self.calculate_items_qty_and_amount()
|
||||
|
||||
def set_available_qty_for_consumption(self):
|
||||
supplied_items_details = {}
|
||||
|
||||
sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item")
|
||||
for item in self.get("items"):
|
||||
supplied_items = (
|
||||
frappe.qb.from_(sco_supplied_item)
|
||||
.select(
|
||||
sco_supplied_item.rm_item_code,
|
||||
sco_supplied_item.reference_name,
|
||||
(sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_(
|
||||
"available_qty"
|
||||
),
|
||||
)
|
||||
.where(
|
||||
(sco_supplied_item.parent == item.subcontracting_order)
|
||||
& (sco_supplied_item.main_item_code == item.item_code)
|
||||
& (sco_supplied_item.reference_name == item.subcontracting_order_item)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
if supplied_items:
|
||||
supplied_items_details[item.name] = {}
|
||||
|
||||
for supplied_item in supplied_items:
|
||||
if supplied_item.rm_item_code not in supplied_items_details[item.name]:
|
||||
supplied_items_details[item.name][supplied_item.rm_item_code] = 0.0
|
||||
|
||||
supplied_items_details[item.name][
|
||||
supplied_item.rm_item_code
|
||||
] += supplied_item.available_qty
|
||||
else:
|
||||
for item in self.get("supplied_items"):
|
||||
item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get(
|
||||
item.rm_item_code, 0
|
||||
)
|
||||
|
||||
def calculate_items_qty_and_amount(self):
|
||||
rm_cost_map = {}
|
||||
for item in self.get("supplied_items") or []:
|
||||
item.amount = flt(item.consumed_qty) * flt(item.rate)
|
||||
|
||||
if item.reference_name in rm_cost_map:
|
||||
rm_cost_map[item.reference_name] += item.amount
|
||||
else:
|
||||
rm_cost_map[item.reference_name] = item.amount
|
||||
|
||||
scrap_cost_map = {}
|
||||
for item in self.get("items") or []:
|
||||
if item.is_scrap_item:
|
||||
item.amount = flt(item.qty) * flt(item.rate)
|
||||
|
||||
if item.reference_name in scrap_cost_map:
|
||||
scrap_cost_map[item.reference_name] += item.amount
|
||||
else:
|
||||
scrap_cost_map[item.reference_name] = item.amount
|
||||
|
||||
total_qty = total_amount = 0
|
||||
for item in self.get("items") or []:
|
||||
if not item.is_scrap_item:
|
||||
if item.qty:
|
||||
if item.name in rm_cost_map:
|
||||
item.rm_supp_cost = rm_cost_map[item.name]
|
||||
item.rm_cost_per_qty = item.rm_supp_cost / item.qty
|
||||
rm_cost_map.pop(item.name)
|
||||
|
||||
if item.name in scrap_cost_map:
|
||||
item.scrap_cost_per_qty = scrap_cost_map[item.name] / item.qty
|
||||
scrap_cost_map.pop(item.name)
|
||||
else:
|
||||
item.scrap_cost_per_qty = 0
|
||||
|
||||
lcv_cost_per_qty = 0.0
|
||||
if item.landed_cost_voucher_amount:
|
||||
lcv_cost_per_qty = item.landed_cost_voucher_amount / item.qty
|
||||
|
||||
item.rate = (
|
||||
flt(item.rm_cost_per_qty)
|
||||
+ flt(item.service_cost_per_qty)
|
||||
+ flt(item.additional_cost_per_qty)
|
||||
+ flt(lcv_cost_per_qty)
|
||||
- flt(item.scrap_cost_per_qty)
|
||||
)
|
||||
|
||||
item.received_qty = flt(item.qty) + flt(item.rejected_qty)
|
||||
item.amount = flt(item.qty) * flt(item.rate)
|
||||
|
||||
total_qty += flt(item.qty)
|
||||
total_amount += item.amount
|
||||
else:
|
||||
self.total_qty = total_qty
|
||||
self.total = total_amount
|
||||
|
||||
def validate_scrap_items(self):
|
||||
for item in self.items:
|
||||
if item.is_scrap_item:
|
||||
if not item.qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Scrap Item Qty cannot be zero").format(item.idx),
|
||||
)
|
||||
|
||||
if item.rejected_qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Rejected Qty cannot be set for Scrap Item {1}.").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
),
|
||||
)
|
||||
|
||||
if not item.reference_name:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Finished Good reference is mandatory for Scrap Item {1}.").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
),
|
||||
)
|
||||
|
||||
def validate_accepted_warehouse(self):
|
||||
for item in self.get("items"):
|
||||
if flt(item.qty) and not item.warehouse:
|
||||
if self.set_warehouse:
|
||||
item.warehouse = self.set_warehouse
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}").format(
|
||||
item.idx, item.item_code
|
||||
)
|
||||
)
|
||||
|
||||
if item.get("warehouse") and (item.get("warehouse") == item.get("rejected_warehouse")):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx)
|
||||
)
|
||||
|
||||
def validate_available_qty_for_consumption(self):
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||
== "BOM"
|
||||
):
|
||||
return
|
||||
|
||||
for item in self.get("supplied_items"):
|
||||
precision = item.precision("consumed_qty")
|
||||
if (
|
||||
item.available_qty_for_consumption
|
||||
and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
|
||||
):
|
||||
msg = _(
|
||||
"""Row {0}: Consumed Qty {1} {2} must be less than or equal to Available Qty For Consumption
|
||||
{3} {4} in Consumed Items Table."""
|
||||
).format(
|
||||
item.idx,
|
||||
flt(item.consumed_qty, precision),
|
||||
item.stock_uom,
|
||||
flt(item.available_qty_for_consumption, precision),
|
||||
item.stock_uom,
|
||||
)
|
||||
|
||||
frappe.throw(msg)
|
||||
|
||||
def validate_bom_required_qty(self):
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||
== "Material Transferred for Subcontract"
|
||||
) and not (frappe.db.get_single_value("Buying Settings", "validate_consumed_qty")):
|
||||
return
|
||||
|
||||
rm_consumed_dict = self.get_rm_wise_consumed_qty()
|
||||
|
||||
for row in self.items:
|
||||
precision = row.precision("qty")
|
||||
for bom_item in self._get_materials_from_bom(
|
||||
row.item_code, row.bom, row.get("include_exploded_items")
|
||||
):
|
||||
required_qty = flt(
|
||||
bom_item.qty_consumed_per_unit * row.qty * row.conversion_factor, precision
|
||||
)
|
||||
consumed_qty = rm_consumed_dict.get(bom_item.rm_item_code, 0)
|
||||
diff = flt(consumed_qty, precision) - flt(required_qty, precision)
|
||||
|
||||
if diff < 0:
|
||||
msg = _(
|
||||
"""Additional {0} {1} of item {2} required as per BOM to complete this transaction"""
|
||||
).format(
|
||||
frappe.bold(abs(diff)),
|
||||
frappe.bold(bom_item.stock_uom),
|
||||
frappe.bold(bom_item.rm_item_code),
|
||||
)
|
||||
|
||||
frappe.throw(
|
||||
msg,
|
||||
exc=BOMQuantityError,
|
||||
)
|
||||
|
||||
def get_rm_wise_consumed_qty(self):
|
||||
rm_dict = defaultdict(float)
|
||||
|
||||
for row in self.supplied_items:
|
||||
rm_dict[row.rm_item_code] += row.consumed_qty
|
||||
|
||||
return rm_dict
|
||||
|
||||
def update_status_updater_args(self):
|
||||
if cint(self.is_return):
|
||||
self.status_updater.extend(
|
||||
[
|
||||
{
|
||||
"source_dt": "Subcontracting Receipt Item",
|
||||
"target_dt": "Subcontracting Order Item",
|
||||
"join_field": "subcontracting_order_item",
|
||||
"target_field": "returned_qty",
|
||||
"source_field": "-1 * qty",
|
||||
"extra_cond": """ and exists (select name from `tabSubcontracting Receipt`
|
||||
where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""",
|
||||
},
|
||||
{
|
||||
"source_dt": "Subcontracting Receipt Item",
|
||||
"target_dt": "Subcontracting Receipt Item",
|
||||
"join_field": "subcontracting_receipt_item",
|
||||
"target_field": "returned_qty",
|
||||
"target_parent_dt": "Subcontracting Receipt",
|
||||
"target_parent_field": "per_returned",
|
||||
"target_ref_field": "received_qty",
|
||||
"source_field": "-1 * received_qty",
|
||||
"percent_join_field_parent": "return_against",
|
||||
},
|
||||
]
|
||||
)
|
||||
>>>>>>> d01c4b68fe (fix: add validation for FG Items as per BOM qty (#50579))
|
||||
|
||||
def update_status(self, status=None, update_modified=False):
|
||||
if not status:
|
||||
|
||||
@@ -32,6 +32,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
make_subcontracting_receipt,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
|
||||
BOMQuantityError,
|
||||
)
|
||||
|
||||
|
||||
class TestSubcontractingReceipt(FrappeTestCase):
|
||||
@@ -769,6 +772,701 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
scr.cancel()
|
||||
self.assertTrue(scr.docstatus == 2)
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
def test_subcontract_return_from_rejected_warehouse(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
|
||||
make_subcontract_return_against_rejected_warehouse,
|
||||
)
|
||||
|
||||
# Create subcontracted item
|
||||
fg_item = make_item(
|
||||
"_Test Subcontract Item Return from Rejected Warehouse",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"is_sub_contracted_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
# Create service item
|
||||
service_item = make_item(
|
||||
"_Test Service Item Return from Rejected Warehouse", properties={"is_stock_item": 0}
|
||||
).name
|
||||
|
||||
# Create BOM for the subcontracted item with required raw materials
|
||||
rm_item1 = make_item(
|
||||
"_Test RM Item 1 Return from Rejected Warehouse", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
rm_item2 = make_item(
|
||||
"_Test RM Item 2 Return from Rejected Warehouse", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
|
||||
|
||||
# Create warehouses
|
||||
rejected_warehouse = create_warehouse("_Test Subcontract Rejected Warehouse Return Qty Warehouse")
|
||||
|
||||
# Create service items for subcontracting order
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": service_item,
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
|
||||
# Create Subcontracting Order
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
|
||||
# Stock raw materials
|
||||
make_stock_entry(item_code=rm_item1, qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
|
||||
make_stock_entry(item_code=rm_item2, qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
|
||||
|
||||
# Transfer raw materials
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
# Step 1: Create Subcontracting Receipt with rejected quantity
|
||||
sr = make_subcontracting_receipt(sco.name)
|
||||
sr.items[0].qty = 8 # Accepted quantity
|
||||
sr.items[0].rejected_qty = 2
|
||||
sr.items[0].rejected_warehouse = rejected_warehouse
|
||||
sr.save()
|
||||
sr.submit()
|
||||
|
||||
# Verify initial state
|
||||
sr.reload()
|
||||
self.assertEqual(sr.items[0].qty, 8)
|
||||
self.assertEqual(sr.items[0].rejected_qty, 2)
|
||||
self.assertEqual(sr.items[0].rejected_warehouse, rejected_warehouse)
|
||||
|
||||
# Step 2: Create Subcontract Return from Rejected Warehouse
|
||||
sr_return = make_subcontract_return_against_rejected_warehouse(sr.name)
|
||||
|
||||
# Verify the return document properties
|
||||
self.assertEqual(sr_return.doctype, "Subcontracting Receipt")
|
||||
self.assertEqual(sr_return.is_return, 1)
|
||||
self.assertEqual(sr_return.return_against, sr.name)
|
||||
|
||||
# Verify item details in return document
|
||||
self.assertEqual(len(sr_return.items), 1)
|
||||
self.assertEqual(sr_return.items[0].item_code, fg_item)
|
||||
self.assertEqual(sr_return.items[0].warehouse, rejected_warehouse)
|
||||
self.assertEqual(sr_return.items[0].qty, -2.0) # Negative for return
|
||||
self.assertEqual(sr_return.items[0].rejected_qty, 0.0)
|
||||
self.assertEqual(sr_return.items[0].rejected_warehouse, "")
|
||||
|
||||
# Check specific fields that should be set for subcontracting returns
|
||||
self.assertEqual(sr_return.items[0].subcontracting_order, sco.name)
|
||||
self.assertEqual(sr_return.items[0].subcontracting_order_item, sr.items[0].subcontracting_order_item)
|
||||
self.assertEqual(sr_return.items[0].return_qty_from_rejected_warehouse, 1)
|
||||
|
||||
# For returns from rejected warehouse, supplied_items might be empty initially
|
||||
# They might get populated when the document is saved/submitted
|
||||
# Or they might not be needed since we're returning finished goods
|
||||
|
||||
# Save and submit the return
|
||||
sr_return.save()
|
||||
sr_return.submit()
|
||||
|
||||
# Verify final state
|
||||
sr_return.reload()
|
||||
self.assertEqual(sr_return.docstatus, 1)
|
||||
self.assertEqual(sr_return.status, "Return")
|
||||
|
||||
# Verify stock ledger entries for the return
|
||||
sle = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={
|
||||
"voucher_type": "Subcontracting Receipt",
|
||||
"voucher_no": sr_return.name,
|
||||
"warehouse": rejected_warehouse,
|
||||
},
|
||||
fields=["item_code", "actual_qty", "warehouse"],
|
||||
)
|
||||
|
||||
self.assertEqual(len(sle), 1)
|
||||
self.assertEqual(sle[0].item_code, fg_item)
|
||||
self.assertEqual(sle[0].actual_qty, -2.0) # Outward entry from rejected warehouse
|
||||
self.assertEqual(sle[0].warehouse, rejected_warehouse)
|
||||
|
||||
# Verify that the original document's rejected quantity is not affected
|
||||
sr.reload()
|
||||
self.assertEqual(sr.items[0].rejected_qty, 2) # Should remain the same
|
||||
|
||||
@IntegrationTestCase.change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
|
||||
def test_auto_create_purchase_receipt(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
fg_item = "Subcontracted Item SA1"
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 5,
|
||||
},
|
||||
]
|
||||
|
||||
po = create_purchase_order(
|
||||
rm_items=service_items,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
do_not_submit=True,
|
||||
)
|
||||
po.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account Excise Duty - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Excise Duty",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
sco = get_subcontracting_order(po_name=po.name)
|
||||
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].qty = 3
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
pr_details = frappe.get_all(
|
||||
"Purchase Receipt",
|
||||
filters={"subcontracting_receipt": scr.name},
|
||||
fields=["name", "total_taxes_and_charges"],
|
||||
)
|
||||
|
||||
self.assertTrue(pr_details)
|
||||
|
||||
pr_qty = frappe.db.get_value("Purchase Receipt Item", {"parent": pr_details[0]["name"]}, "qty")
|
||||
self.assertEqual(pr_qty, 6)
|
||||
|
||||
self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60)
|
||||
|
||||
@IntegrationTestCase.change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
|
||||
def test_auto_create_purchase_receipt_with_no_reference_of_po_item(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
fg_item = "Subcontracted Item SA1"
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 5,
|
||||
},
|
||||
]
|
||||
|
||||
po = create_purchase_order(
|
||||
rm_items=service_items,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
do_not_submit=True,
|
||||
)
|
||||
po.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account Excise Duty - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Excise Duty",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
sco = get_subcontracting_order(po_name=po.name)
|
||||
for row in sco.items:
|
||||
row.db_set("purchase_order_item", None)
|
||||
|
||||
sco.reload()
|
||||
|
||||
for row in sco.items:
|
||||
self.assertFalse(row.purchase_order_item)
|
||||
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
for row in scr.items:
|
||||
self.assertFalse(row.purchase_order_item)
|
||||
|
||||
scr.items[0].qty = 3
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
pr_details = frappe.get_all(
|
||||
"Purchase Receipt",
|
||||
filters={"subcontracting_receipt": scr.name},
|
||||
fields=["name", "total_taxes_and_charges"],
|
||||
)
|
||||
|
||||
self.assertTrue(pr_details)
|
||||
|
||||
pr_qty = frappe.db.get_value("Purchase Receipt Item", {"parent": pr_details[0]["name"]}, "qty")
|
||||
self.assertEqual(pr_qty, 6)
|
||||
|
||||
self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60)
|
||||
|
||||
def test_use_serial_batch_fields_for_subcontracting_receipt(self):
|
||||
fg_item = make_item(
|
||||
"Test Subcontracted Item With Batch No",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-BNGS-.####",
|
||||
"is_sub_contracted_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_item(
|
||||
"Test Subcontracted Item With Batch No Service Item 1",
|
||||
properties={"is_stock_item": 0},
|
||||
)
|
||||
|
||||
make_bom(
|
||||
item=fg_item,
|
||||
raw_materials=[
|
||||
make_item(
|
||||
"Test Subcontracted Item With Batch No RM Item 1",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-RM-BNGS-.####",
|
||||
},
|
||||
).name
|
||||
],
|
||||
)
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Test Subcontracted Item With Batch No Service Item 1",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 1,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
batch_no = "BATCH-BNGS-0001"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": fg_item,
|
||||
}
|
||||
).insert()
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
self.assertFalse(scr.items[0].serial_and_batch_bundle)
|
||||
scr.items[0].use_serial_batch_fields = 1
|
||||
scr.items[0].batch_no = batch_no
|
||||
|
||||
scr.save()
|
||||
scr.submit()
|
||||
scr.reload()
|
||||
self.assertTrue(scr.items[0].serial_and_batch_bundle)
|
||||
|
||||
def test_use_serial_batch_fields_for_subcontracting_receipt_with_rejected_qty(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
fg_item = make_item(
|
||||
"Test Subcontracted Item With Batch No for Rejected Qty",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-REJ-BNGS-.####",
|
||||
"is_sub_contracted_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_item(
|
||||
"Test Subcontracted Item With Batch No Service Item 2",
|
||||
properties={"is_stock_item": 0},
|
||||
)
|
||||
|
||||
make_bom(
|
||||
item=fg_item,
|
||||
raw_materials=[
|
||||
make_item(
|
||||
"Test Subcontracted Item With Batch No RM Item 2",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-REJ-RM-BNGS-.####",
|
||||
},
|
||||
).name
|
||||
],
|
||||
)
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Test Subcontracted Item With Batch No Service Item 2",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
batch_no = "BATCH-REJ-BNGS-0001"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": fg_item,
|
||||
}
|
||||
).insert()
|
||||
|
||||
rej_warehouse = create_warehouse("_Test Subcontract Warehouse For Rejected Qty")
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
self.assertFalse(scr.items[0].serial_and_batch_bundle)
|
||||
scr.items[0].use_serial_batch_fields = 1
|
||||
scr.items[0].batch_no = batch_no
|
||||
scr.items[0].received_qty = 10
|
||||
scr.items[0].rejected_qty = 2
|
||||
scr.items[0].qty = 8
|
||||
scr.items[0].rejected_warehouse = rej_warehouse
|
||||
|
||||
scr.save()
|
||||
scr.submit()
|
||||
scr.reload()
|
||||
self.assertTrue(scr.items[0].serial_and_batch_bundle)
|
||||
self.assertTrue(scr.items[0].rejected_serial_and_batch_bundle)
|
||||
|
||||
def test_subcontracting_receipt_for_batch_materials_without_use_serial_batch_fields(self):
|
||||
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
|
||||
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
fg_item = make_item(
|
||||
"Test Subcontracted FG Item With Batch No and Without Use Serial Batch Fields",
|
||||
properties={"is_stock_item": 1, "is_sub_contracted_item": 1},
|
||||
).name
|
||||
|
||||
rm_item1 = make_item(
|
||||
"Test Subcontracted RM Item With Batch No and Without Use Serial Batch Fields",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-RM-BNGS-.####",
|
||||
},
|
||||
).name
|
||||
|
||||
make_item(
|
||||
"Subcontracted Service Item 21",
|
||||
properties={
|
||||
"is_stock_item": 0,
|
||||
},
|
||||
)
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item1])
|
||||
|
||||
rm_batch_no = None
|
||||
for row in bom.items:
|
||||
se = make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
qty=10,
|
||||
target="_Test Warehouse - _TC",
|
||||
rate=300,
|
||||
)
|
||||
|
||||
se.reload()
|
||||
rm_batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 21",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
self.assertTrue(sco.docstatus)
|
||||
rm_items = [
|
||||
{
|
||||
"name": sco.supplied_items[0].name,
|
||||
"item_code": rm_item1,
|
||||
"rm_item_code": rm_item1,
|
||||
"item_name": rm_item1,
|
||||
"qty": 10,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"rate": 100,
|
||||
"stock_uom": frappe.get_cached_value("Item", rm_item1, "stock_uom"),
|
||||
"use_serial_batch_fields": 1,
|
||||
},
|
||||
]
|
||||
se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
|
||||
se.items[0].subcontracted_item = fg_item
|
||||
se.items[0].s_warehouse = "_Test Warehouse - _TC"
|
||||
se.items[0].t_warehouse = "_Test Warehouse 1 - _TC"
|
||||
se.items[0].use_serial_batch_fields = 1
|
||||
se.items[0].batch_no = rm_batch_no
|
||||
se.submit()
|
||||
|
||||
self.assertEqual(se.items[0].batch_no, rm_batch_no)
|
||||
self.assertEqual(se.items[0].use_serial_batch_fields, 1)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].qty = 2
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 2)
|
||||
self.assertEqual(scr.supplied_items[0].batch_no, rm_batch_no)
|
||||
self.assertEqual(get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle), rm_batch_no)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].qty = 2
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 2)
|
||||
self.assertEqual(scr.supplied_items[0].batch_no, rm_batch_no)
|
||||
self.assertEqual(get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle), rm_batch_no)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].qty = 6
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 6)
|
||||
self.assertEqual(scr.supplied_items[0].batch_no, rm_batch_no)
|
||||
self.assertEqual(get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle), rm_batch_no)
|
||||
|
||||
sco.reload()
|
||||
self.assertEqual(sco.status, "Completed")
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
def test_change_batch_for_raw_materials(self):
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
|
||||
rm_item1 = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BNGS-.####",
|
||||
}
|
||||
).name
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item1])
|
||||
second_batch_no = None
|
||||
for row in bom.items:
|
||||
se = make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
qty=1,
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
rate=300,
|
||||
)
|
||||
|
||||
se.reload()
|
||||
se1 = make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
qty=1,
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
rate=300,
|
||||
)
|
||||
|
||||
se1.reload()
|
||||
|
||||
second_batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 1,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
scr.reload()
|
||||
|
||||
scr.supplied_items[0].batch_no = second_batch_no
|
||||
scr.supplied_items[0].use_serial_batch_fields = 1
|
||||
scr.submit()
|
||||
scr.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(batch_no, second_batch_no)
|
||||
self.assertEqual(scr.items[0].rm_cost_per_qty, 300)
|
||||
self.assertEqual(scr.items[0].service_cost_per_qty, 100)
|
||||
|
||||
def test_bom_required_qty_validation_based_on_bom(self):
|
||||
set_backflush_based_on("BOM")
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
|
||||
rm_item1 = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BRQV-.####",
|
||||
}
|
||||
).name
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[rm_item1], rm_qty=2)
|
||||
se = make_stock_entry(
|
||||
item_code=rm_item1,
|
||||
qty=1,
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
rate=300,
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 1,
|
||||
},
|
||||
]
|
||||
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
scr.reload()
|
||||
|
||||
self.assertEqual(scr.supplied_items[0].batch_no, batch_no)
|
||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 1)
|
||||
self.assertEqual(scr.supplied_items[0].required_qty, 2)
|
||||
|
||||
self.assertRaises(BOMQuantityError, scr.submit)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||
|
||||
def test_bom_required_qty_validation_based_on_transfer(self):
|
||||
from erpnext.controllers.subcontracting_controller import (
|
||||
make_rm_stock_entry as make_subcontract_transfer_entry,
|
||||
)
|
||||
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
frappe.db.set_single_value("Buying Settings", "validate_consumed_qty", 1)
|
||||
|
||||
item_code = "_Test Subcontracted Validation FG Item 1"
|
||||
rm_item1 = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
}
|
||||
).name
|
||||
|
||||
make_subcontracted_item(item_code=item_code, raw_materials=[rm_item1])
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": item_code,
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(
|
||||
service_items=service_items,
|
||||
include_exploded_items=0,
|
||||
)
|
||||
|
||||
# inward raw material stock
|
||||
make_stock_entry(target="_Test Warehouse - _TC", item_code=rm_item1, qty=10, basic_rate=100)
|
||||
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": sco.supplied_items[0].rm_item_code,
|
||||
"qty": sco.supplied_items[0].required_qty - 5,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
]
|
||||
|
||||
# transfer partial raw materials
|
||||
ste = frappe.get_doc(make_subcontract_transfer_entry(sco.name, rm_items))
|
||||
ste.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||
ste.save()
|
||||
ste.submit()
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
|
||||
self.assertRaises(BOMQuantityError, scr.submit)
|
||||
|
||||
>>>>>>> d01c4b68fe (fix: add validation for FG Items as per BOM qty (#50579))
|
||||
|
||||
def make_return_subcontracting_receipt(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
Reference in New Issue
Block a user