diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js index 060c4b5edaa..c34e0f9099c 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js @@ -9,13 +9,6 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number"); frappe.ui.form.on("Bank Guarantee", { setup: function (frm) { - frm.set_query("bank", function () { - return { - filters: { - company: frm.doc.company, - }, - }; - }); frm.set_query("bank_account", function () { return { filters: { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 3898636a051..0cda42a6ca2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -71,14 +71,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. if (this.frm.doc.supplier && this.frm.doc.__islocal) { this.frm.trigger("supplier"); } - - this.frm.set_query("supplier", function () { - return { - filters: { - is_transporter: 0, - }, - }; - }); } refresh(doc) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5f6fe525fa8..4a137da1230 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -847,9 +847,10 @@ class SalesInvoice(SellingController): timesheet.db_update_all() def update_billed_qty_in_scio(self): - table = frappe.qb.DocType("Subcontracting Inward Order Received Item") - fieldname = table.returned_qty if self.is_return else table.billed_qty + if self.is_return: + return + table = frappe.qb.DocType("Subcontracting Inward Order Received Item") data = frappe._dict( { item.scio_detail: item.stock_qty if self._action == "submit" else -item.stock_qty @@ -861,8 +862,8 @@ class SalesInvoice(SellingController): if data: case_expr = Case() for name, qty in data.items(): - case_expr = case_expr.when(table.name == name, fieldname + qty) - frappe.qb.update(table).set(fieldname, case_expr).where( + case_expr = case_expr.when(table.name == name, table.billed_qty + qty) + frappe.qb.update(table).set(table.billed_qty, case_expr).where( (table.name.isin(list(data.keys()))) & (table.docstatus == 1) ).run() @@ -1281,16 +1282,14 @@ class SalesInvoice(SellingController): table = frappe.qb.DocType("Subcontracting Inward Order Received Item") query = ( frappe.qb.from_(table) - .select( - table.required_qty, table.consumed_qty, table.billed_qty, table.returned_qty, table.name - ) + .select(table.required_qty, table.consumed_qty, table.billed_qty, table.name) .where((table.docstatus == 1) & (table.name.isin([item.scio_detail for item in self_rms]))) ) result = query.run(as_dict=True) data = {item.name: item for item in result} for item in self_rms: row = data.get(item.scio_detail) - max_qty = max(row.required_qty, row.consumed_qty) - row.billed_qty - row.returned_qty + max_qty = max(row.required_qty, row.consumed_qty) - row.billed_qty if item.stock_qty > max_qty: frappe.throw( _("Row #{0}: Stock quantity {1} ({2}) for item {3} cannot exceed {4}").format( diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 1f974a16f95..0562b0da86f 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -26,16 +26,13 @@ frappe.query_reports["Accounts Payable"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_account", diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index ac9d5bfbd01..18a85af95be 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -45,16 +45,13 @@ frappe.query_reports["Accounts Payable Summary"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_type", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index fa2467b526d..ddee9c6b6b0 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -28,16 +28,13 @@ frappe.query_reports["Accounts Receivable"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_type", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 9e1112ba117..2e507aef592 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, get_dimension_with_children, ) +from erpnext.accounts.report.financial_statements import get_cost_centers_with_children from erpnext.accounts.utils import ( build_qb_match_conditions, get_advance_payment_doctypes, @@ -666,7 +667,16 @@ class ReceivablePayableReport: invoiced = d.base_payment_amount paid_amount = d.base_paid_amount - if company_currency == d.party_account_currency or self.filters.get("in_party_currency"): + in_party_currency = self.filters.get("in_party_currency") + # company, billing, and party account currencies are the same + if company_currency == d.currency and company_currency == d.party_account_currency: + in_party_currency = False + + # When filtered by party currency and the billing currency not matches the party account currency + if in_party_currency and d.currency != d.party_account_currency: + in_party_currency = False + + if in_party_currency: invoiced = d.payment_amount paid_amount = d.paid_amount @@ -987,11 +997,7 @@ class ReceivablePayableReport: self.add_accounting_dimensions_filters() def get_cost_center_conditions(self): - lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) - cost_center_list = [ - center.name - for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)}) - ] + cost_center_list = get_cost_centers_with_children(self.filters.cost_center) self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list)) def add_common_filters(self): diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index e65c4d94146..afaba697fb1 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -199,6 +199,81 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase): row = report[1] self.assertTrue(len(row) == 0) + @IntegrationTestCase.change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1}, + ) + def test_allow_multi_currency_invoices_against_single_party_account(self): + filters = { + "company": self.company, + "based_on_payment_terms": 1, + "report_date": today(), + "range": "30, 60, 90, 120", + "show_remarks": True, + "in_party_currency": 1, + } + + # CASE 1: Company currency and party account currency are the same + si = self.create_sales_invoice(qty=1, no_payment_schedule=True, do_not_submit=True) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + filters.update( + { + "party_type": "Customer", + "party": [self.customer], + } + ) + report = execute(filters) + row = report[1][0] + + expected_data = [8000, 8000, "No Remarks"] # Data in company currency + + self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) + + # CASE 2: Transaction currency and party account currency are the same + self.create_customer( + "USD Customer", currency="USD", default_account=self.debtors_usd, company=self.company + ) + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + currency="USD", + conversion_rate=80, + price_list_rate=100, + do_not_save=1, + ) + si.save().submit() + + filters.update( + { + "party_type": "Customer", + "party": [self.customer], + } + ) + report = execute(filters) + row = report[1][0] + + expected_data = [100, 100, "No Remarks"] # Data in Part Account Currency + + self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) + + # View in Company currency + filters.pop("in_party_currency") + report = execute(filters) + row = report[1][0] + + expected_data = [8000, 8000, "No Remarks"] # Data in Company Currency + + self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks]) + def test_accounts_receivable_with_partial_payment(self): filters = { "company": self.company, diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index ae0bddaa766..1ac2b27ca71 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -45,16 +45,13 @@ frappe.query_reports["Accounts Receivable Summary"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: () => { - var company = frappe.query_report.get_filter_value("company"); - return { - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "party_type", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 0ddb95fff2f..af20179d743 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -85,6 +85,12 @@ frappe.query_reports["Gross Profit"] = { }); }, }, + { + fieldname: "include_returned_invoices", + label: __("Include Returned Invoices (Stand-alone)"), + fieldtype: "Check", + default: 1, + }, ], tree: true, name_field: "parent", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index baf2da6ceea..d2fe570fa3b 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -859,7 +859,10 @@ class GrossProfitGenerator: if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" - conditions += " and (is_return = 0 or (is_return=1 and return_against is null))" + if self.filters.include_returned_invoices: + conditions += " and (is_return = 0 or (is_return=1 and return_against is null))" + else: + conditions += " and is_return = 0" if self.filters.item_group: conditions += f" and {get_item_group_condition(self.filters.item_group)}" diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index ab1d44ddbc6..116cdca4d6c 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -447,7 +447,11 @@ class TestGrossProfit(IntegrationTestCase): sinv = sinv.save().submit() filters = frappe._dict( - company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + company=self.company, + from_date=nowdate(), + to_date=nowdate(), + group_by="Invoice", + include_returned_invoices=1, ) columns, data = execute(filters=filters) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index e91073a962c..b86f6ab75e0 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -47,22 +47,23 @@ frappe.query_reports["Trial Balance"] = { { fieldname: "cost_center", label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center", - get_query: function () { - var company = frappe.query_report.get_filter_value("company"); - return { - doctype: "Cost Center", - filters: { - company: company, - }, - }; + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Cost Center", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, + options: "Cost Center", }, { fieldname: "project", label: __("Project"), - fieldtype: "Link", + fieldtype: "MultiSelectList", + get_data: function (txt) { + return frappe.db.get_link_options("Project", txt, { + company: frappe.query_report.get_filter_value("company"), + }); + }, options: "Project", }, { diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index e5f8044aee2..933e9cd30d9 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.report.financial_statements import ( filter_accounts, filter_out_zero_value_rows, + get_cost_centers_with_children, set_gl_entries_by_account, ) from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency @@ -101,10 +102,6 @@ def get_data(filters): opening_balances = get_opening_balances(filters, ignore_is_opening) - # add filter inside list so that the query in financial_statements.py doesn't break - if filters.project: - filters.project = [filters.project] - set_gl_entries_by_account( filters.company, filters.from_date, @@ -297,18 +294,12 @@ def get_opening_balance( opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher") if filters.cost_center: - lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) - cost_center = frappe.qb.DocType("Cost Center") opening_balance = opening_balance.where( - closing_balance.cost_center.isin( - frappe.qb.from_(cost_center) - .select("name") - .where((cost_center.lft >= lft) & (cost_center.rgt <= rgt)) - ) + closing_balance.cost_center.isin(get_cost_centers_with_children(filters.get("cost_center"))) ) if filters.project: - opening_balance = opening_balance.where(closing_balance.project == filters.project) + opening_balance = opening_balance.where(closing_balance.project.isin(filters.project)) if frappe.db.count("Finance Book"): if filters.get("include_default_book_entries"): diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index d4de7626272..5b1c9e6aa57 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -5,7 +5,9 @@ from erpnext.stock.doctype.item.test_item import create_item class AccountsTestMixin: - def create_customer(self, customer_name="_Test Customer", currency=None): + def create_customer( + self, customer_name="_Test Customer", currency=None, default_account=None, company=None + ): if not frappe.db.exists("Customer", customer_name): customer = frappe.new_doc("Customer") customer.customer_name = customer_name @@ -13,9 +15,28 @@ class AccountsTestMixin: if currency: customer.default_currency = currency + if company and default_account: + customer.append( + "accounts", + { + "company": company, + "account": default_account, + }, + ) customer.save() self.customer = customer.name else: + if company and default_account: + customer = frappe.get_doc("Customer", customer_name) + customer.accounts = [] + customer.append( + "accounts", + { + "company": company, + "account": default_account, + }, + ) + customer.save() self.customer = customer_name def create_supplier(self, supplier_name="_Test Supplier", currency=None): diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 89f21bdd443..96f49a0e1cb 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -946,7 +946,7 @@ def make_post_gl_entry(): assets = frappe.db.sql_list( """ select name from `tabAsset` where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0 - and available_for_use_date = %s""", + and available_for_use_date = %s and docstatus = 1""", (asset_category.name, nowdate()), ) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index f339f503a6b..a1b50202f5b 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -502,17 +502,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( } } - onload() { - super.onload(); - this.frm.set_query("supplier", function () { - return { - filters: { - is_transporter: 0, - }, - }; - }); - } - get_items_from_open_material_requests() { erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order_based_on_supplier", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index dd5aedbeff7..7272202ee33 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2005,7 +2005,7 @@ class AccountsController(TransactionBase): discount_amount * self.get("conversion_rate"), item.precision("discount_amount"), ), - "debit_in_account_currency": flt( + "debit_in_transaction_currency": flt( discount_amount, item.precision("discount_amount") ), "cost_center": item.cost_center, @@ -2026,7 +2026,7 @@ class AccountsController(TransactionBase): discount_amount * self.get("conversion_rate"), item.precision("discount_amount"), ), - "credit_in_account_currency": flt( + "credit_in_transaction_currency": flt( discount_amount, item.precision("discount_amount") ), "cost_center": item.cost_center, @@ -3680,8 +3680,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item" return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row) + def is_allowed_zero_qty(): + if parent_doctype == "Sales Order": + return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False + elif parent_doctype == "Purchase Order": + return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False + return False + def validate_quantity(child_item, new_data): - if not flt(new_data.get("qty")): + if not flt(new_data.get("qty")) and not is_allowed_zero_qty(): frappe.throw( _("Row #{0}: Quantity for Item {1} cannot be zero.").format( new_data.get("idx"), frappe.bold(new_data.get("item_code")) @@ -3817,6 +3824,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil conv_fac_precision = child_item.precision("conversion_factor") or 2 qty_precision = child_item.precision("qty") or 2 + prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) + rate_unchanged = prev_rate == new_rate + if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty(): + frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price")))) + # Amount cannot be lesser than billed amount, except for negative amounts row_rate = flt(d.get("rate"), rate_precision) amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt( diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 16b86eeb525..4ad3d5a656e 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -484,6 +484,13 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai target_doc.subcontracting_order_item = source_doc.subcontracting_order_item target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.subcontracting_receipt_item = source_doc.name + if return_against_rejected_qty: + target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0)) + target_doc.rejected_qty = 0.0 + 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 else: target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order_item = source_doc.purchase_order_item diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index f604fef5720..eaa30a97cd4 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -510,7 +510,6 @@ class SubcontractingInwardController: ) .else_(table.qty) - table.delivered_qty - - table.returned_qty ).as_("max_allowed_qty") ) .where((table.name == item.scio_detail) & (table.docstatus == 1)) diff --git a/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.py b/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.py index 9e7563543f6..cecfcfe4af2 100644 --- a/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.py +++ b/erpnext/manufacturing/doctype/master_production_schedule/master_production_schedule.py @@ -284,9 +284,6 @@ class MasterProductionSchedule(Document): row = data[key] row.cumulative_lead_time = math.ceil(row.cumulative_lead_time) row.order_release_date = add_days(row.delivery_date, -row.cumulative_lead_time) - if getdate(row.order_release_date) < getdate(today()): - continue - row.planned_qty = row.qty row.uom = row.stock_uom row.warehouse = row.warehouse or self.parent_warehouse diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index fe3c294e471..8a7133490c9 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1928,7 +1928,12 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.quantity * child.conversion_factor)) + .select( + Sum( + Case().when(child.quantity == 0, child.required_bom_qty).else_(child.quantity) + * child.conversion_factor + ) + ) .where( (table.docstatus == 1) & (child.item_code == item_code) diff --git a/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py b/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py index 8bd06abc46c..86d9274052f 100644 --- a/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py +++ b/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py @@ -84,7 +84,7 @@ class SalesForecast(Document): ) for index in range(self.demand_number): - if self.horizon_type == "Monthly": + if self.frequency == "Monthly": delivery_date = add_to_date(self.from_date, months=index + 1) else: delivery_date = add_to_date(self.from_date, weeks=index + 1) @@ -95,15 +95,13 @@ class SalesForecast(Document): "delivery_date": delivery_date, "item_name": item_details.item_name, "uom": item_details.uom, - "demand_qty": 0.0, + "demand_qty": 1.0, } ) for demand in forecast_demand: self.append("items", demand) - self.save() - @frappe.whitelist() def generate_demand(self): from statsmodels.tsa.holtwinters import ExponentialSmoothing @@ -124,7 +122,7 @@ class SalesForecast(Document): seasonal_periods = self.get_seasonal_periods(data) pd_sales_data = pd.DataFrame({"item": data.item, "date": data.date, "qty": data.qty}) - resample_val = "M" if self.horizon_type == "Monthly" else "W" + resample_val = "M" if self.frequency == "Monthly" else "W" _sales_data = pd_sales_data.set_index("date").resample(resample_val).sum()["qty"] model = ExponentialSmoothing( @@ -164,7 +162,7 @@ class SalesForecast(Document): def get_seasonal_periods(self, data): days = date_diff(data["end_date"], data["start_date"]) - if self.horizon_type == "Monthly": + if self.frequency == "Monthly": months = (days / 365) * 12 seasonal_periods = cint(months / 2) if seasonal_periods > 12: diff --git a/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.js b/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.js index bab665c8277..5687e8e9483 100644 --- a/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.js +++ b/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.js @@ -77,13 +77,6 @@ frappe.query_reports["Material Requirements Planning Report"] = { default: "All", options: "\nFinished Goods\nRaw Materials\nAll", }, - { - fieldname: "safety_stock_check_frequency", - label: __("Safety Stock Check Frequency"), - fieldtype: "Select", - default: "Weekly", - options: "\nDaily\nWeekly\nMonthly", - }, { fieldname: "add_safety_stock", label: __("Add Safety Stock"), diff --git a/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py b/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py index a04ee0100e7..ee93cdc27e5 100644 --- a/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py +++ b/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py @@ -41,8 +41,9 @@ class MaterialRequirementsPlanningReport: self.rm_items = [] self.dates = self.get_dates() self.mps_data = self.get_mps_data() - items = self.get_items_from_mps(self.mps_data) + self.update_sales_forecast_data() + self.item_rm_details = self.get_raw_materials_data(items) self._bin_details = self.get_item_wise_bin_details() @@ -52,7 +53,6 @@ class MaterialRequirementsPlanningReport: self._po_details = self.get_purchase_order_data() self._so_details = self.get_sales_order_data() - self.update_sales_forecast_data() data, chart = self.get_mrp_data() return data, chart @@ -850,6 +850,23 @@ class MaterialRequirementsPlanningReport: if row.item_code not in items: items.append(row.item_code) + if self.filters.mps: + sales_forecasts = frappe.get_all( + "Master Production Schedule", + filters={"name": self.filters.mps}, + pluck="sales_forecast", + ) + + if sales_forecasts: + sales_forecast_items = frappe.get_all( + "Sales Forecast Item", + filters={"parent": ("in", sales_forecasts)}, + pluck="item_code", + ) + + if sales_forecast_items: + items.extend(sales_forecast_items) + return items def get_raw_materials_data(self, items): @@ -1139,7 +1156,12 @@ class MaterialRequirementsPlanningReport: query = query.where(doctype.delivery_date <= self.filters.to_date) if self.filters.warehouse: - query = query.where(doctype.warehouse == self.filters.warehouse) + warehouses = [self.filters.get("warehouse")] + if frappe.db.get_value("Warehouse", self.filters.get("warehouse"), "is_group"): + warehouses = get_descendants_of("Warehouse", self.filters.get("warehouse")) + warehouses.append(self.filters.get("warehouse")) + + query = query.where(forecast_doc.parent_warehouse.isin(warehouses)) if self.filters.item_code: query = query.where(doctype.item_code == self.filters.item_code) diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 75f6870ff22..3d16dac7acc 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -347,7 +347,7 @@ "hidden": 0, "is_query_report": 0, "label": "Production", - "link_count": 7, + "link_count": 8, "link_type": "DocType", "onboard": 0, "type": "Card Break" @@ -385,16 +385,6 @@ "onboard": 1, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Master Production Schedule", - "link_count": 0, - "link_to": "Master Production Schedule", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "dependencies": "", "hidden": 0, @@ -406,6 +396,26 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Item Lead Time", + "link_count": 0, + "link_to": "Item Lead Time", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Master Production Schedule", + "link_count": 0, + "link_to": "Master Production Schedule", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -428,7 +438,7 @@ "type": "Link" } ], - "modified": "2025-10-30 11:49:35.589944", + "modified": "2025-11-03 17:02:26.882516", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 159daa972d7..2783055613a 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -75,18 +75,35 @@ erpnext.financial_statements = { }, open_general_ledger: function (data) { if (!data.account && !data.accounts) return; - let project = $.grep(frappe.query_report.filters, function (e) { + let filters = frappe.query_report.filters; + + let project = $.grep(filters, function (e) { return e.df.fieldname == "project"; }); + let cost_center = $.grep(filters, function (e) { + return e.df.fieldname == "cost_center"; + }); + frappe.route_options = { account: data.account || data.accounts, company: frappe.query_report.get_filter_value("company"), from_date: data.from_date || data.year_start_date, to_date: data.to_date || data.year_end_date, - project: project && project.length > 0 ? project[0].$input.val() : "", + project: project && project.length > 0 ? project[0].get_value() : "", + cost_center: cost_center && cost_center.length > 0 ? cost_center[0].get_value() : "", }; + filters.forEach((f) => { + if (f.df.fieldtype == "MultiSelectList") { + if (f.df.fieldname in frappe.route_options) return; + let value = f.get_value(); + if (value && value.length > 0) { + frappe.route_options[f.df.fieldname] = value; + } + } + }); + let report = "General Ledger"; if (["Payable", "Receivable"].includes(data.account_type)) { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 95eaa788cab..f7c4fef5ebb 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1361,7 +1361,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a .select( child.required_qty, child.consumed_qty, - (child.billed_qty - child.returned_qty).as_("qty"), + child.billed_qty, child.rm_item_code, child.stock_uom, child.name, @@ -1377,7 +1377,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a if result: idx = len(doclist.items) + 1 for item in result: - if (qty := max(item.required_qty, item.consumed_qty) - item.qty) > 0: + if (qty := max(item.required_qty, item.consumed_qty) - item.billed_qty) > 0: doclist.append( "items", { diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index cc89b85b823..2d6fb2c3a58 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -119,12 +119,6 @@ "default_item_manufacturer", "default_manufacturer_part_no", "total_projected_qty", - "lead_time_in_days_section", - "procurement_time", - "manufacturing_time", - "column_break_whvr", - "planning_buffer", - "cumulative_time", "capacity_in_days_section", "production_capacity" ], @@ -894,36 +888,6 @@ "fieldtype": "Section Break", "label": "Deferred Accounting" }, - { - "fieldname": "lead_time_in_days_section", - "fieldtype": "Section Break", - "label": "Lead Time (In Days)" - }, - { - "fieldname": "procurement_time", - "fieldtype": "Int", - "label": "Procurement Time" - }, - { - "fieldname": "planning_buffer", - "fieldtype": "Int", - "label": "Planning Buffer" - }, - { - "fieldname": "column_break_whvr", - "fieldtype": "Column Break" - }, - { - "fieldname": "manufacturing_time", - "fieldtype": "Int", - "label": "Manufacturing Time" - }, - { - "fieldname": "cumulative_time", - "fieldtype": "Int", - "label": "Cumulative Time", - "read_only": 1 - }, { "fieldname": "capacity_in_days_section", "fieldtype": "Section Break", @@ -953,7 +917,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2025-10-13 16:58:40.946604", + "modified": "2025-11-03 17:01:24.555003", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index f91beb8b488..51531faf990 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -81,7 +81,6 @@ class Item(Document): brand: DF.Link | None country_of_origin: DF.Link | None create_new_batch: DF.Check - cumulative_time: DF.Int customer: DF.Link | None customer_code: DF.SmallText | None customer_items: DF.Table[ItemCustomerDetail] @@ -120,7 +119,6 @@ class Item(Document): item_name: DF.Data | None last_purchase_rate: DF.Float lead_time_days: DF.Int - manufacturing_time: DF.Int max_discount: DF.Float min_order_qty: DF.Float naming_series: DF.Literal["STO-ITEM-.YYYY.-"] @@ -129,8 +127,6 @@ class Item(Document): opening_stock: DF.Float over_billing_allowance: DF.Float over_delivery_receipt_allowance: DF.Float - planning_buffer: DF.Int - procurement_time: DF.Int production_capacity: DF.Int purchase_uom: DF.Link | None quality_inspection_template: DF.Link | None @@ -221,16 +217,10 @@ class Item(Document): self.validate_auto_reorder_enabled_in_stock_settings() self.cant_change() self.validate_item_tax_net_rate_range() - self.set_cumulative_time() if not self.is_new(): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") - def set_cumulative_time(self): - self.cumulative_time = ( - cint(self.procurement_time) + cint(self.manufacturing_time) + cint(self.planning_buffer) - ) - def on_update(self): self.update_variants() self.update_item_price() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 9c7035feafe..db065a80c92 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -199,17 +199,6 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend super.setup(doc); } - onload() { - super.onload(); - this.frm.set_query("supplier", function () { - return { - filters: { - is_transporter: 0, - }, - }; - }); - } - refresh() { var me = this; super.refresh(); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index bd003212745..932f8a4844e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -262,6 +262,8 @@ class StockEntry(StockController, SubcontractingInwardController): self.reset_default_field_value("from_warehouse", "items", "s_warehouse") self.reset_default_field_value("to_warehouse", "items", "t_warehouse") + self.validate_same_source_target_warehouse_during_material_transfer() + self.validate_closed_subcontracting_order() self.validate_subcontract_order() @@ -864,6 +866,53 @@ class StockEntry(StockController, SubcontractingInwardController): title=_("Missing Item"), ) + def validate_same_source_target_warehouse_during_material_transfer(self): + """ + Validate Material Transfer entries where source and target warehouses are identical. + + For Material Transfer purpose, if an item has the same source and target warehouse, + require that at least one inventory dimension (if configured) differs between source + and target to ensure a meaningful transfer is occurring. + + Raises: + frappe.ValidationError: If warehouses are same and no inventory dimensions differ + """ + from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions + + inventory_dimensions = get_inventory_dimensions() + if self.purpose == "Material Transfer": + for item in self.items: + if cstr(item.s_warehouse) == cstr(item.t_warehouse): + if not inventory_dimensions: + frappe.throw( + _( + "Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer" + ).format(item.idx), + title=_("Invalid Source and Target Warehouse"), + ) + else: + difference_found = False + for dimension in inventory_dimensions: + fieldname = ( + dimension.source_fieldname + if dimension.source_fieldname.startswith("to_") + else f"to_{dimension.source_fieldname}" + ) + if ( + item.get(dimension.source_fieldname) + and item.get(fieldname) + and item.get(dimension.source_fieldname) != item.get(fieldname) + ): + difference_found = True + break + if not difference_found: + frappe.throw( + _( + "Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer" + ).format(item.idx), + title=_("Invalid Source and Target Warehouse"), + ) + def get_matched_items(self, item_code): for row in self.items: if row.item_code == item_code or row.original_item == item_code: diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 018d0b20dc0..d6ec32aeff8 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -109,6 +109,37 @@ class StockReservationEntry(Document): self.update_status() self.update_reserved_stock_in_bin() + def before_cancel(self) -> None: + self.validate_reserved_entries() + + def validate_reserved_entries(self): + entries = frappe.get_all( + "Stock Reservation Entry", + fields=["voucher_no as name"], + filters={ + "status": "Closed", + "docstatus": 1, + "from_voucher_type": "Purchase Receipt", + "from_voucher_no": self.from_voucher_no, + }, + ) + + if entries: + work_orders = frappe.get_all( + "Work Order", + fields=["name"], + filters={"production_plan": ("in", [entry.name for entry in entries])}, + ) + + frappe.throw( + _( + "Cannot cancel Stock Reservation Entry {0}, as it has used in the work order {1}. Please cancel the work order first or unreserved the stock" + ).format( + ", ".join([frappe.bold(entry.name) for entry in entries]), + ", ".join([frappe.bold(wo.name) for wo in work_orders]), + ) + ) + def update_unreserved_qty_in_sre(self): if self.voucher_type == "Delivery Note": return diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py index ca3abb8bde9..f79ee9f8b37 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py @@ -451,7 +451,7 @@ class SubcontractingInwardOrder(SubcontractingController): qty = ( fg_item.produced_qty if allow_over - else min(fg_item.qty, fg_item.produced_qty) - fg_item.delivered_qty - fg_item.returned_qty + else min(fg_item.qty, fg_item.produced_qty) - fg_item.delivered_qty ) if qty < 0: continue diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json b/erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json index 02e138e89d5..82ff107e17e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order_received_item/subcontracting_inward_order_received_item.json @@ -120,6 +120,7 @@ }, { "default": "0", + "depends_on": "eval:doc.is_customer_provided_item", "fieldname": "returned_qty", "fieldtype": "Float", "label": "Returned Qty", @@ -196,7 +197,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-21 23:44:18.302327", + "modified": "2025-11-03 17:46:49.905804", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Inward Order Received Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index fee1cac2542..4e502793068 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -82,10 +82,53 @@ frappe.ui.form.on("Subcontracting Receipt", { frm.add_custom_button( __("Subcontract Return"), () => { - frappe.model.open_mapped_doc({ - method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return", - frm: frm, + const make_standard_return = () => { + frappe.model.open_mapped_doc({ + method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return", + frm: frm, + }); + }; + + let has_rejected_items = frm.doc.items.filter((item) => { + if (item.rejected_qty > 0) { + return true; + } }); + + if (has_rejected_items && has_rejected_items.length > 0) { + frappe.prompt( + [ + { + label: __("Return Qty from Rejected Warehouse"), + fieldtype: "Check", + fieldname: "return_for_rejected_warehouse", + default: 1, + }, + ], + function (values) { + if (values.return_for_rejected_warehouse) { + frappe.call({ + method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return_against_rejected_warehouse", + args: { + source_name: frm.doc.name, + }, + callback: function (r) { + if (r.message) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + }, + }); + } else { + make_standard_return(); + } + }, + __("Return Qty"), + __("Make Return Entry") + ); + } else { + make_standard_return(); + } }, __("Create") ); diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 815c4002cf1..51520d401ba 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -854,6 +854,13 @@ class SubcontractingReceipt(SubcontractingController): make_purchase_receipt(self, save=True, notify=True) +@frappe.whitelist() +def make_subcontract_return_against_rejected_warehouse(source_name): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return make_return_doc("Subcontracting Receipt", source_name, return_against_rejected_qty=True) + + @frappe.whitelist() def make_subcontract_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 46b11dc29fa..ac9d956fa08 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -1195,6 +1195,136 @@ class TestSubcontractingReceipt(IntegrationTestCase): scr.cancel() self.assertTrue(scr.docstatus == 2) + 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