diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 8b13b00c042..5db9c30135a 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -60,7 +60,7 @@ body:
description: Share exact version number of Frappe and ERPNext you are using.
placeholder: |
Frappe version -
- ERPNext Verion -
+ ERPNext version -
validations:
required: true
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
index fb974765db0..939e8219421 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
@@ -33,6 +33,17 @@
},
"account_number": "1151.000"
},
+ "Pajak Dibayar di Muka": {
+ "PPN Masukan": {
+ "account_number": "1152.001",
+ "account_type": "Tax"
+ },
+ "PPh 23 Dibayar di Muka": {
+ "account_number": "1152.002",
+ "account_type": "Tax"
+ },
+ "account_number": "1152.000"
+ },
"account_number": "1150.000"
},
"Kas": {
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.js b/erpnext/accounts/doctype/bank_account/bank_account.js
index 202f750fb50..5173e0539f4 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.js
+++ b/erpnext/accounts/doctype/bank_account/bank_account.js
@@ -42,8 +42,4 @@ frappe.ui.form.on("Bank Account", {
});
}
},
-
- is_company_account: function (frm) {
- frm.set_df_property("account", "reqd", frm.doc.is_company_account);
- },
});
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json
index 4946dc168ba..ce239365f30 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.json
+++ b/erpnext/accounts/doctype/bank_account/bank_account.json
@@ -52,6 +52,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company Account",
+ "mandatory_depends_on": "is_company_account",
"options": "Account"
},
{
@@ -98,6 +99,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
+ "mandatory_depends_on": "is_company_account",
"options": "Company"
},
{
@@ -252,7 +254,7 @@
"link_fieldname": "default_bank_account"
}
],
- "modified": "2025-08-29 12:32:01.081687",
+ "modified": "2026-01-20 00:46:16.633364",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py
index 5a874701a39..1aa11df0c9a 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.py
+++ b/erpnext/accounts/doctype/bank_account/bank_account.py
@@ -52,31 +52,35 @@ class BankAccount(Document):
delete_contact_and_address("Bank Account", self.name)
def validate(self):
- self.validate_company()
- self.validate_account()
+ self.validate_is_company_account()
self.update_default_bank_account()
- def validate_account(self):
- if self.account:
- if accounts := frappe.db.get_all(
- "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
- ):
- frappe.throw(
- _("'{0}' account is already used by {1}. Use another account.").format(
- frappe.bold(self.account),
- frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
- )
- )
+ def validate_is_company_account(self):
+ if self.is_company_account:
+ if not self.company:
+ frappe.throw(_("Company is mandatory for company account"))
- def validate_company(self):
- if self.is_company_account and not self.company:
- frappe.throw(_("Company is manadatory for company account"))
+ if not self.account:
+ frappe.throw(_("Company Account is mandatory"))
+
+ self.validate_account()
@deprecated
def validate_iban(self):
"""Kept for backward compatibility, will be removed in v16."""
validate_iban(self.iban, throw=True)
+ def validate_account(self):
+ if accounts := frappe.db.get_all(
+ "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
+ ):
+ frappe.throw(
+ _("'{0}' account is already used by {1}. Use another account.").format(
+ frappe.bold(self.account),
+ frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
+ )
+ )
+
def update_default_bank_account(self):
if self.is_default and not self.disabled:
frappe.db.set_value(
diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js
index 25d437f154f..ec53f1fa78e 100644
--- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js
+++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js
@@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", {
let d = locals[cdt][cdn];
return {
filters: [
- ["Account", "account_type", "in", "Bank, Cash, Receivable"],
+ ["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]],
["Account", "is_group", "=", 0],
["Account", "company", "=", d.company],
],
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 97b3e87770f..c1be1d2eae0 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -838,6 +838,53 @@ class TestPOSInvoice(unittest.TestCase):
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
self.assertEqual(batch.qty, 5)
+ def test_pos_batch_reservation_with_return_qty(self):
+ """
+ Test POS Invoice reserved qty for batch without bundle with return invoices.
+ """
+ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_auto_batch_nos,
+ )
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_batch_item_with_batch,
+ )
+
+ create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01")
+ se = make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code="_Batch Item Reserve Return",
+ qty=30,
+ basic_rate=100,
+ )
+
+ se.reload()
+
+ batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
+
+ # POS Invoice for the batch without bundle
+ pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1)
+ pos_inv.append(
+ "payments",
+ {"mode_of_payment": "Cash", "amount": 4500},
+ )
+ pos_inv.items[0].batch_no = batch_no
+ pos_inv.save()
+ pos_inv.submit()
+
+ # POS Invoice return
+ pos_return = make_sales_return(pos_inv.name)
+
+ pos_return.insert()
+ pos_return.submit()
+
+ batches = get_auto_batch_nos(
+ frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"})
+ )
+
+ for batch in batches:
+ if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
+ self.assertEqual(batch.qty, 30)
+
def test_pos_batch_item_qty_validation(self):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
BatchNegativeStockError,
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 913ab7e6c47..43091e9bda9 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -697,6 +697,7 @@ def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
& (SalesInvoice.is_return == 0)
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
+ & (SalesInvoice.docstatus == 1)
)
)
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
index c4c75913d65..f650806966f 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
@@ -412,8 +412,9 @@ def reconcile(doc: None | str = None) -> None:
for x in allocations:
pr.append("allocation", x)
+ skip_ref_details_update_for_pe = check_multi_currency(pr)
# reconcile
- pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
+ pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe)
# If Payment Entry, update details only for newly linked references
# This is for performance
@@ -503,6 +504,37 @@ def reconcile(doc: None | str = None) -> None:
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
+def check_multi_currency(pr_doc):
+ GL = frappe.qb.DocType("GL Entry")
+ Account = frappe.qb.DocType("Account")
+
+ def get_account_currency(voucher_type, voucher_no):
+ currency = (
+ frappe.qb.from_(GL)
+ .join(Account)
+ .on(GL.account == Account.name)
+ .select(Account.account_currency)
+ .where(
+ (GL.voucher_type == voucher_type)
+ & (GL.voucher_no == voucher_no)
+ & (Account.account_type.isin(["Payable", "Receivable"]))
+ )
+ .limit(1)
+ ).run(as_dict=True)
+
+ return currency[0].account_currency if currency else None
+
+ for allocation in pr_doc.allocation:
+ reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name)
+
+ invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number)
+
+ if reference_currency != invoice_currency:
+ return True
+
+ return False
+
+
@frappe.whitelist()
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 2c96286d3bb..aeb993e2399 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -43,6 +43,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
"Unreconcile Payment Entries",
"Serial and Batch Bundle",
"Bank Transaction",
+ "Packing Slip",
];
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
diff --git a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html
index 8a6968ee373..ad8c908dbcf 100644
--- a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html
+++ b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html
@@ -17,7 +17,7 @@
- | Date: | {{ frappe.utils.format_date(doc.creation) }} |
+ | Date: | {{ frappe.utils.format_date(doc.posting_date) }} |
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
index ccb4d26f77b..00ab15dba12 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
@@ -159,11 +159,11 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
- labels = [d.get("label") for d in columns[2:]]
+ labels = [d.get("label") for d in columns[4:]]
income_data, expense_data, net_profit = [], [], []
- for p in columns[2:]:
+ for p in columns[4:]:
if income:
income_data.append(income[-2].get(p.get("fieldname")))
if expense:
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js
index 8c08c35b3e1..d1047f3276f 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.js
+++ b/erpnext/accounts/report/trial_balance/trial_balance.js
@@ -112,6 +112,12 @@ frappe.query_reports["Trial Balance"] = {
fieldtype: "Check",
default: 1,
},
+ {
+ fieldname: "show_group_accounts",
+ label: __("Show Group Accounts"),
+ fieldtype: "Check",
+ default: 1,
+ },
],
formatter: erpnext.financial_statements.formatter,
tree: true,
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index 13bf6f531d1..5f78aef582b 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -83,7 +83,7 @@ def validate_filters(filters):
def get_data(filters):
accounts = frappe.db.sql(
- """select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
+ """select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
from `tabAccount` where company=%s order by lft""",
filters.company,
@@ -393,6 +393,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
"from_date": filters.from_date,
"to_date": filters.to_date,
"currency": company_currency,
+ "is_group_account": d.is_group,
"account_name": (
f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name
),
@@ -409,6 +410,10 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
data.append(row)
total_row = calculate_total_row(accounts, company_currency)
+
+ if not filters.get("show_group_accounts"):
+ data = hide_group_accounts(data)
+
data.extend([{}, total_row])
return data
@@ -488,3 +493,12 @@ def prepare_opening_closing(row):
row[valid_col] = 0.0
else:
row[reverse_col] = 0.0
+
+
+def hide_group_accounts(data):
+ non_group_accounts_data = []
+ for d in data:
+ if not d.get("is_group_account"):
+ d.update(indent=0)
+ non_group_accounts_data.append(d)
+ return non_group_accounts_data
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index b66cb09e082..cd22aec6960 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -304,12 +304,17 @@ class RequestforQuotation(BuyingController):
else:
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
+ rendered_message = frappe.render_template(self.message_for_supplier, doc_args)
+ subject_source = (
+ self.subject
+ or frappe.get_value("Email Template", self.email_template, "subject")
+ or _("Request for Quotation")
+ )
+ rendered_subject = frappe.render_template(subject_source, doc_args)
if preview:
return {
- "message": self.message_for_supplier,
- "subject": self.subject
- or frappe.get_value("Email Template", self.email_template, "subject")
- or _("Request for Quotation"),
+ "message": rendered_message,
+ "subject": rendered_subject,
}
attachments = []
@@ -333,10 +338,8 @@ class RequestforQuotation(BuyingController):
self.send_email(
data,
sender,
- self.subject
- or frappe.get_value("Email Template", self.email_template, "subject")
- or _("Request for Quotation"),
- self.message_for_supplier,
+ rendered_subject,
+ rendered_message,
attachments,
)
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index d3d5cb808f3..a53eb2130a8 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -770,12 +770,34 @@ def get_filters(
if reference_voucher_detail_no:
filters["voucher_detail_no"] = reference_voucher_detail_no
- if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row and item_row.get("warehouse"):
- filters["warehouse"] = item_row.get("warehouse")
+ warehouses = []
+ if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row:
+ if reference_voucher_detail_no:
+ warehouses = get_warehouses_for_return(voucher_type, reference_voucher_detail_no)
+
+ if item_row.get("warehouse") and item_row.get("warehouse") in warehouses:
+ filters["warehouse"] = item_row.get("warehouse")
return filters
+def get_warehouses_for_return(voucher_type, name):
+ warehouses = []
+ warehouse_details = frappe.get_all(
+ voucher_type + " Item",
+ filters={"name": name, "docstatus": 1},
+ fields=["warehouse", "rejected_warehouse"],
+ )
+
+ for d in warehouse_details:
+ if d.warehouse:
+ warehouses.append(d.warehouse)
+ if d.rejected_warehouse:
+ warehouses.append(d.rejected_warehouse)
+
+ return warehouses
+
+
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None):
from erpnext.stock.doctype.serial_no.serial_no import (
get_serial_nos as get_serial_nos_from_serial_no,
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 5c2dc7491c2..c2a9afadcf0 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -279,7 +279,7 @@ class SellingController(StockController):
_(
"""Row #{0}: Selling rate for item {1} is lower than its {2}.
Selling {3} should be atleast {4}.
Alternatively,
- you can disable selling price validation in {5} to bypass
+ you can disable '{5}' in {6} to bypass
this validation."""
).format(
idx,
@@ -287,6 +287,7 @@ class SellingController(StockController):
bold(ref_rate_field),
bold("net rate"),
bold(rate),
+ bold(frappe.get_meta("Selling Settings").get_label("validate_selling_price")),
get_link_to_form("Selling Settings", "Selling Settings"),
),
title=_("Invalid Selling Price"),
@@ -298,7 +299,6 @@ class SellingController(StockController):
return
is_internal_customer = self.get("is_internal_customer")
- valuation_rate_map = {}
for item in self.items:
if not item.item_code or item.is_free_item:
@@ -308,7 +308,9 @@ class SellingController(StockController):
"Item", item.item_code, ("last_purchase_rate", "is_stock_item")
)
- last_purchase_rate_in_sales_uom = last_purchase_rate * (item.conversion_factor or 1)
+ last_purchase_rate_in_sales_uom = flt(
+ last_purchase_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
+ )
if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
throw_message(item.idx, item.item_name, last_purchase_rate_in_sales_uom, "last purchase rate")
@@ -316,50 +318,16 @@ class SellingController(StockController):
if is_internal_customer or not is_stock_item:
continue
- valuation_rate_map[(item.item_code, item.warehouse)] = None
-
- if not valuation_rate_map:
- return
-
- or_conditions = (
- f"""(item_code = {frappe.db.escape(valuation_rate[0])}
- and warehouse = {frappe.db.escape(valuation_rate[1])})"""
- for valuation_rate in valuation_rate_map
- )
-
- valuation_rates = frappe.db.sql(
- f"""
- select
- item_code, warehouse, valuation_rate
- from
- `tabBin`
- where
- ({" or ".join(or_conditions)})
- and valuation_rate > 0
- """,
- as_dict=True,
- )
-
- for rate in valuation_rates:
- valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
-
- for item in self.items:
- if not item.item_code or item.is_free_item:
- continue
-
- last_valuation_rate = valuation_rate_map.get((item.item_code, item.warehouse))
-
- if not last_valuation_rate:
- continue
-
- last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
-
- if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
+ if item.get("incoming_rate") and item.base_net_rate < (
+ valuation_rate := flt(
+ item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
+ )
+ ):
throw_message(
item.idx,
item.item_name,
- last_valuation_rate_in_sales_uom,
- "valuation rate (Moving Average)",
+ valuation_rate,
+ "valuation rate",
)
def get_item_list(self):
@@ -518,6 +486,8 @@ class SellingController(StockController):
if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
+ from erpnext.stock.serial_batch_bundle import get_batch_nos
+
allow_at_arms_length_price = frappe.get_cached_value(
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
)
@@ -525,6 +495,7 @@ class SellingController(StockController):
"Selling Settings", "set_zero_rate_for_expired_batch"
)
+ old_doc = self.get_doc_before_save()
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
@@ -554,6 +525,29 @@ class SellingController(StockController):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
+ if old_doc:
+ old_item = next(
+ (
+ item
+ for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
+ if item.name == d.name
+ ),
+ None,
+ )
+ if old_item:
+ old_qty = flt(
+ old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty")
+ )
+ if (
+ old_item.item_code != d.item_code
+ or old_item.warehouse != d.warehouse
+ or old_qty != qty
+ or old_item.batch_no != d.batch_no
+ or get_batch_nos(old_item.serial_and_batch_bundle)
+ != get_batch_nos(d.serial_and_batch_bundle)
+ ):
+ d.incoming_rate = 0
+
if (
not d.incoming_rate
or self.is_internal_transfer()
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index a7924288058..dcecd995a48 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -83,7 +83,8 @@ status_map = {
],
"Delivery Note": [
["Draft", None],
- ["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
+ ["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
+ ["Partially Billed", "eval:self.per_billed < 100 and self.per_billed > 0 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
@@ -341,14 +342,17 @@ class StatusUpdater(Document):
):
return
- if qty_or_amount == "qty":
- action_msg = _(
- 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
- )
+ if args["target_dt"] != "Quotation Item":
+ if qty_or_amount == "qty":
+ action_msg = _(
+ 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
+ )
+ else:
+ action_msg = _(
+ 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
+ )
else:
- action_msg = _(
- 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
- )
+ action_msg = None
frappe.throw(
_(
@@ -360,8 +364,7 @@ class StatusUpdater(Document):
frappe.bold(_(self.doctype)),
frappe.bold(item.get("item_code")),
)
- + "
"
- + action_msg,
+ + ("
" + action_msg if action_msg else ""),
OverAllowanceError,
title=_("Limit Crossed"),
)
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index bf91f9490a4..a20d59333b6 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -465,7 +465,10 @@ class StockController(AccountsController):
if is_rejected:
serial_nos = row.get("rejected_serial_no")
type_of_transaction = "Inward" if not self.is_return else "Outward"
- qty = row.get("rejected_qty") * row.get("conversion_factor", 1.0)
+ qty = flt(
+ row.get("rejected_qty") * row.get("conversion_factor", 1.0),
+ frappe.get_precision("Serial and Batch Entry", "qty"),
+ )
warehouse = row.get("rejected_warehouse")
if (
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 07d4fd31387..246693d2d01 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -254,10 +254,10 @@ class SubcontractingController(StockController):
):
for row in frappe.get_all(
f"{self.subcontract_data.order_doctype} Item",
- fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
+ fields=["item_code", "(qty - received_qty) as qty", "parent", "bom"],
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
):
- self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
+ self.qty_to_be_received[(row.item_code, row.parent, row.bom)] += row.qty
def __get_transferred_items(self):
se = frappe.qb.DocType("Stock Entry")
@@ -829,13 +829,17 @@ class SubcontractingController(StockController):
self.__set_serial_nos(item_row, rm_obj)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
- key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
+ key = (
+ item_row.item_code,
+ item_row.get(self.subcontract_data.order_field),
+ item_row.get("bom"),
+ )
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
- if self.qty_to_be_received:
- qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
+ if self.qty_to_be_received.get(key):
+ qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key))
transfer_item.item_details.required_qty = transfer_item.qty
if transfer_item.serial_no or frappe.get_cached_value(
@@ -880,7 +884,11 @@ class SubcontractingController(StockController):
if self.qty_to_be_received:
self.qty_to_be_received[
- (row.item_code, row.get(self.subcontract_data.order_field))
+ (
+ row.item_code,
+ row.get(self.subcontract_data.order_field),
+ row.get("bom"),
+ )
] -= row.qty
def __set_rate_for_serial_and_batch_bundle(self):
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 74ccdfd02aa..6173f40fb10 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -701,19 +701,21 @@ class ProductionPlan(Document):
"project": self.project,
}
- key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse)
+ key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse, d.planned_start_date)
if self.combine_items:
- key = (d.item_code, d.sales_order, d.warehouse)
+ key = (d.item_code, d.sales_order, d.warehouse, d.planned_start_date)
if not d.sales_order:
- key = (d.name, d.item_code, d.warehouse)
+ key = (d.name, d.item_code, d.warehouse, d.planned_start_date)
if not item_details["project"] and d.sales_order:
item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
if self.get_items_from == "Material Request":
item_details.update({"qty": d.planned_qty})
- item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details
+ item_dict[
+ (d.item_code, d.material_request_item, d.warehouse, d.planned_start_date)
+ ] = item_details
else:
item_details.update(
{
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index b4b0468a0dd..25fc4f26247 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -867,7 +867,7 @@ class TestProductionPlan(FrappeTestCase):
items_data = pln.get_production_items()
# Update qty
- items_data[(pln.po_items[0].name, item, None)]["qty"] = qty
+ items_data[(pln.po_items[0].name, item, None, pln.po_items[0].planned_start_date)]["qty"] = qty
# Create and Submit Work Order for each item in items_data
for _key, item in items_data.items():
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 9e0679a7cd1..971b811fbcc 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -664,7 +664,7 @@ erpnext.work_order = {
set_custom_buttons: function (frm) {
var doc = frm.doc;
- if (doc.docstatus === 1 && doc.status !== "Closed") {
+ if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
frm.add_custom_button(
__("Close"),
function () {
@@ -674,9 +674,6 @@ erpnext.work_order = {
},
__("Status")
);
- }
-
- if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
if (doc.status != "Stopped" && doc.status != "Completed") {
frm.add_custom_button(
__("Stop"),
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 0fdfbef279b..7ded266c62a 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -428,3 +428,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
+erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
diff --git a/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py b/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py
new file mode 100644
index 00000000000..faa99fcd2ca
--- /dev/null
+++ b/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py
@@ -0,0 +1,16 @@
+import frappe
+
+
+def execute():
+ data = frappe.get_all(
+ "Sales Order Item",
+ filters={"quotation_item": ["is", "set"], "docstatus": 1},
+ fields=["quotation_item", "sum(stock_qty) as ordered_qty"],
+ group_by="quotation_item",
+ )
+ if data:
+ frappe.db.auto_commit_on_many_writes = 1
+ frappe.db.bulk_update(
+ "Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
+ )
+ frappe.db.auto_commit_on_many_writes = 0
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 1f434a485b5..bc4e4d869c0 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -308,6 +308,8 @@ class Project(Document):
self.gross_margin = flt(self.total_billed_amount) - expense_amount
if self.total_billed_amount:
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
+ else:
+ self.per_gross_margin = 0
def update_purchase_costing(self):
total_purchase_cost = calculate_total_purchase_cost(self.name)
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index 0d93fd21382..77246d82bc6 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -138,14 +138,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.run_serially([
() => this.set_selector_trigger_flag(data),
- () => this.set_serial_no(row, serial_no),
- () => this.set_batch_no(row, batch_no),
() => this.set_barcode(row, barcode),
() => this.set_warehouse(row),
() =>
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
this.show_scan_message(row.idx, !is_new_row, qty);
}),
+ () => this.set_serial_no(row, serial_no),
+ () => this.set_batch_no(row, batch_no),
() => this.clean_up(),
() => this.set_barcode_uom(row, uom),
() => this.revert_selector_flag(),
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 7fb15943c9f..1ce480fbd2e 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -497,6 +497,9 @@ def _set_missing_values(source, target):
if contact:
target.contact_person = contact[0].parent
+ target.contact_display, target.contact_email, target.contact_mobile = frappe.get_value(
+ "Contact", contact[0].parent, ["full_name", "email_id", "mobile_no"]
+ )
@frappe.whitelist()
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index d6b2fe73cac..3f30664e39b 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -446,7 +446,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
"Quotation",
source_name,
{
- "Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}},
+ "Quotation": {
+ "doctype": "Sales Order",
+ "validation": {"docstatus": ["=", 1]},
+ },
"Quotation Item": {
"doctype": "Sales Order Item",
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
@@ -549,6 +552,8 @@ def _make_customer(source_name, ignore_permissions=False):
if quotation.quotation_to == "Customer":
return frappe.get_doc("Customer", quotation.party_name)
+ elif quotation.quotation_to == "CRM Deal":
+ return frappe.get_doc("Customer", {"crm_deal": quotation.party_name})
# Check if a Customer already exists for the Lead or Prospect.
existing_customer = None
@@ -610,25 +615,8 @@ def handle_mandatory_error(e, customer, lead_name):
def get_ordered_items(quotation: str):
- """
- Returns a dict of ordered items with their total qty based on quotation row name.
-
- In `Sales Order Item`, `quotation_item` is the row name of `Quotation Item`.
-
- Example:
- ```
- {
- "refsdjhd2": 10,
- "ygdhdshrt": 5,
- }
- ```
- """
return frappe._dict(
frappe.get_all(
- "Sales Order Item",
- filters={"prevdoc_docname": quotation, "docstatus": 1},
- fields=["quotation_item", "sum(qty)"],
- group_by="quotation_item",
- as_list=1,
+ "Quotation Item", {"docstatus": 1, "parent": quotation}, ["name", "ordered_qty"], as_list=True
)
)
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 210f4715815..ae756f34288 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -828,7 +828,7 @@ class TestQuotation(FrappeTestCase):
# item code same but description different
make_item("_Test Item 2", {"is_stock_item": 1})
- quotation = make_quotation(qty=1, rate=100, do_not_submit=1)
+ quotation = make_quotation(qty=10, rate=100, do_not_submit=1)
# duplicate items
for qty in [1, 1, 2, 3]:
@@ -842,7 +842,7 @@ class TestQuotation(FrappeTestCase):
sales_order.delivery_date = nowdate()
self.assertEqual(len(sales_order.items), 6)
- self.assertEqual(sales_order.items[0].qty, 1)
+ self.assertEqual(sales_order.items[0].qty, 10)
self.assertEqual(sales_order.items[-1].qty, 5)
# Row 1: 10, Row 4: 1, Row 5: 1
@@ -885,6 +885,18 @@ class TestQuotation(FrappeTestCase):
f"Expected conversion rate {expected_rate}, got {quotation.conversion_rate}",
)
+ def test_over_order_limit(self):
+ from erpnext.selling.doctype.quotation.quotation import make_sales_order
+
+ quotation = make_quotation(qty=5)
+ so1 = make_sales_order(quotation.name)
+ so2 = make_sales_order(quotation.name)
+ so1.delivery_date = nowdate()
+ so2.delivery_date = nowdate()
+
+ so1.submit()
+ self.assertRaises(frappe.ValidationError, so2.submit)
+
test_records = frappe.get_test_records("Quotation")
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index 74c4670063e..27d318de4b0 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -24,6 +24,7 @@
"uom",
"conversion_factor",
"stock_qty",
+ "ordered_qty",
"available_quantity_section",
"actual_qty",
"column_break_ylrv",
@@ -694,12 +695,23 @@
"print_hide": 1,
"read_only": 1,
"report_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "ordered_qty",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Ordered Qty",
+ "no_copy": 1,
+ "non_negative": 1,
+ "read_only": 1,
+ "reqd": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2025-08-26 20:31:47.775890",
+ "modified": "2026-01-30 12:56:08.320190",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.py b/erpnext/selling/doctype/quotation_item/quotation_item.py
index bbdd8643593..9ab265c885c 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.py
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.py
@@ -48,6 +48,7 @@ class QuotationItem(Document):
margin_type: DF.Literal["", "Percentage", "Amount"]
net_amount: DF.Currency
net_rate: DF.Currency
+ ordered_qty: DF.Float
page_break: DF.Check
parent: DF.Data
parentfield: DF.Data
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index a757efffa4e..a1047c11a96 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -185,6 +185,16 @@ class SalesOrder(SellingController):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ self.status_updater = [
+ {
+ "source_dt": "Sales Order Item",
+ "target_dt": "Quotation Item",
+ "join_field": "quotation_item",
+ "target_field": "ordered_qty",
+ "target_ref_field": "stock_qty",
+ "source_field": "stock_qty",
+ }
+ ]
def onload(self) -> None:
super().onload()
@@ -419,6 +429,7 @@ class SalesOrder(SellingController):
frappe.throw(_("Row #{0}: Set Supplier for item {1}").format(d.idx, d.item_code))
def on_submit(self):
+ super().update_prevdoc_status()
self.check_credit_limit()
self.update_reserved_qty()
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index db4d60b56f6..13759d0f7f7 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -57,6 +57,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.set_user("Administrator")
+ @change_settings("Selling Settings", {"allow_negative_rates_for_items": 1})
def test_sales_order_with_negative_rate(self):
"""
Test if negative rate is allowed in Sales Order via doc submission and update items
diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json
index 3616425771c..3259a2323b7 100644
--- a/erpnext/setup/setup_wizard/data/country_wise_tax.json
+++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json
@@ -4844,17 +4844,17 @@
"Switzerland": {
"Switzerland normal VAT": {
- "account_name": "VAT 7.7%",
- "tax_rate": 7.70,
+ "account_name": "VAT 8.1%",
+ "tax_rate": 8.10,
"default": 1
},
"Switzerland reduced VAT": {
- "account_name": "VAT 2.5%",
- "tax_rate": 2.50
+ "account_name": "VAT 2.6%",
+ "tax_rate": 2.60
},
"Switzerland lodging VAT": {
- "account_name": "VAT 3.7%",
- "tax_rate": 3.70
+ "account_name": "VAT 3.8%",
+ "tax_rate": 3.80
}
},
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 97873234dcf..06623192797 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -1091,7 +1091,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "\nDraft\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
+ "options": "\nDraft\nTo Bill\nPartially Billed\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
@@ -1404,7 +1404,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2025-12-02 23:55:25.415443",
+ "modified": "2026-02-03 12:27:19.055918",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index f2c6be169be..b2dd23f80bd 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -127,7 +127,15 @@ class DeliveryNote(SellingController):
shipping_rule: DF.Link | None
source: DF.Link | None
status: DF.Literal[
- "", "Draft", "To Bill", "Completed", "Return", "Return Issued", "Cancelled", "Closed"
+ "",
+ "Draft",
+ "To Bill",
+ "Partially Billed",
+ "Completed",
+ "Return",
+ "Return Issued",
+ "Cancelled",
+ "Closed",
]
tax_category: DF.Link | None
tax_id: DF.Data | None
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
index 0f045bf405d..914063e1041 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
@@ -18,8 +18,10 @@ frappe.listview_settings["Delivery Note"] = {
return [__("Closed"), "green", "status,=,Closed"];
} else if (doc.status === "Return Issued") {
return [__("Return Issued"), "grey", "status,=,Return Issued"];
- } else if (flt(doc.per_billed, 2) < 100) {
- return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
+ } else if (flt(doc.per_billed) == 0) {
+ return [__("To Bill"), "orange", "per_billed,=,0|docstatus,=,1"];
+ } else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
+ return [__("Partially Billed"), "yellow", "per_billed,<,100|docstatus,=,1"];
} else if (flt(doc.per_billed, 2) === 100) {
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
}
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 29eabe2670e..90bae7f68f5 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1093,7 +1093,8 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(dn2.get("items")[0].billed_amt, 400)
self.assertEqual(dn2.per_billed, 80)
- self.assertEqual(dn2.status, "To Bill")
+ # Since 20% of DN2 is yet to be billed, it should be classified as partially billed.
+ self.assertEqual(dn2.status, "Partially Billed")
def test_dn_billing_status_case4(self):
# SO -> SI -> DN
@@ -2807,6 +2808,23 @@ class TestDeliveryNote(FrappeTestCase):
frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision)
+ @change_settings("Selling Settings", {"validate_selling_price": 1})
+ def test_validate_selling_price(self):
+ item_code = make_item("VSP Item", properties={"is_stock_item": 1}).name
+ make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=10)
+ make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=1)
+
+ dn = create_delivery_note(
+ item_code=item_code,
+ qty=1,
+ rate=9,
+ do_not_save=True,
+ )
+ self.assertRaises(frappe.ValidationError, dn.save)
+ dn.items[0].incoming_rate = 0
+ dn.items[0].stock_qty = 2
+ dn.save()
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 341c38bdcf1..c91b9e22632 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -4794,6 +4794,128 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(stk_ledger.incoming_rate, 120)
self.assertEqual(stk_ledger.stock_value_difference, 600)
+ def test_negative_stock_error_for_purchase_return_when_stock_exists_in_future_date(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+ from erpnext.stock.stock_ledger import NegativeStockError
+
+ item_code = make_item(
+ "Test Negative Stock for Purchase Return with Future Stock Item",
+ {
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TNSPFPRI.#####",
+ },
+ ).name
+
+ make_purchase_receipt(
+ item_code=item_code,
+ posting_date=add_days(today(), -4),
+ qty=100,
+ rate=100,
+ warehouse="_Test Warehouse - _TC",
+ )
+
+ pr1 = make_purchase_receipt(
+ item_code=item_code,
+ posting_date=add_days(today(), -3),
+ qty=100,
+ rate=100,
+ warehouse="_Test Warehouse - _TC",
+ )
+
+ batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
+
+ pr2 = make_purchase_receipt(
+ item_code=item_code,
+ posting_date=add_days(today(), -2),
+ qty=100,
+ rate=100,
+ warehouse="_Test Warehouse - _TC",
+ )
+
+ batch2 = get_batch_from_bundle(pr2.items[0].serial_and_batch_bundle)
+
+ make_stock_entry(
+ item_code=item_code,
+ qty=100,
+ posting_date=add_days(today(), -1),
+ source="_Test Warehouse - _TC",
+ target="_Test Warehouse 1 - _TC",
+ batch_no=batch1,
+ use_serial_batch_fields=1,
+ )
+
+ make_stock_entry(
+ item_code=item_code,
+ qty=100,
+ posting_date=add_days(today(), -1),
+ source="_Test Warehouse - _TC",
+ target="_Test Warehouse 1 - _TC",
+ batch_no=batch2,
+ use_serial_batch_fields=1,
+ )
+
+ make_stock_entry(
+ item_code=item_code,
+ qty=100,
+ posting_date=today(),
+ source="_Test Warehouse 1 - _TC",
+ target="_Test Warehouse - _TC",
+ batch_no=batch1,
+ use_serial_batch_fields=1,
+ )
+
+ make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name)
+ make_purchase_entry.set_posting_time = 1
+ make_purchase_entry.posting_date = pr1.posting_date
+ self.assertRaises(NegativeStockError, make_purchase_entry.submit)
+
+ def test_purchase_return_from_different_warehouse(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+ item_code = make_item(
+ "Test Purchase Return From Different Warehouse Item",
+ {
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TPRFDWU.#####",
+ },
+ ).name
+
+ pr1 = make_purchase_receipt(
+ item_code=item_code,
+ posting_date=add_days(today(), -4),
+ qty=100,
+ rate=100,
+ warehouse="_Test Warehouse - _TC",
+ )
+
+ batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
+
+ make_stock_entry(
+ item_code=item_code,
+ qty=100,
+ posting_date=add_days(today(), -1),
+ source="_Test Warehouse - _TC",
+ target="_Test Warehouse 1 - _TC",
+ batch_no=batch1,
+ use_serial_batch_fields=1,
+ )
+
+ make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name)
+ make_purchase_entry.items[0].warehouse = "_Test Warehouse 1 - _TC"
+ make_purchase_entry.submit()
+ make_purchase_entry.reload()
+
+ sabb = frappe.get_doc("Serial and Batch Bundle", make_purchase_entry.items[0].serial_and_batch_bundle)
+ for row in sabb.entries:
+ self.assertEqual(row.warehouse, "_Test Warehouse 1 - _TC")
+ self.assertEqual(row.incoming_rate, 100)
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index fe10c6aeeb9..f51254784d9 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -15,6 +15,7 @@ from frappe.utils import (
cint,
cstr,
flt,
+ get_datetime,
get_link_to_form,
getdate,
now,
@@ -439,6 +440,8 @@ class SerialandBatchBundle(Document):
)
def get_valuation_rate_for_return_entry(self, return_against):
+ from erpnext.controllers.sales_and_purchase_return import get_warehouses_for_return
+
if not self.voucher_detail_no:
return {}
@@ -468,9 +471,11 @@ class SerialandBatchBundle(Document):
["Serial and Batch Bundle", "voucher_detail_no", "=", return_against_voucher_detail_no],
]
+ # Added to handle rejected warehouse case
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
- # Added to handle rejected warehouse case
- filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse])
+ warehouses = get_warehouses_for_return(self.voucher_type, return_against_voucher_detail_no)
+ if self.warehouse in warehouses:
+ filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse])
bundle_data = frappe.get_all(
"Serial and Batch Bundle",
@@ -1419,31 +1424,44 @@ class SerialandBatchBundle(Document):
for d in self.entries:
available_qty = batch_wise_available_qty.get(d.batch_no, 0)
if flt(available_qty, precision) < 0:
- frappe.throw(
- _(
- """
- The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
- ).format(
- bold(d.batch_no),
- bold(self.item_code),
- bold(self.warehouse),
- bold(abs(flt(available_qty, precision))),
- ),
- title=_("Negative Stock Error"),
- )
+ self.throw_negative_batch(d.batch_no, available_qty, precision)
+
+ def throw_negative_batch(self, batch_no, available_qty, precision):
+ from erpnext.stock.stock_ledger import NegativeStockError
+
+ frappe.throw(
+ _(
+ """
+ The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
+ ).format(
+ bold(batch_no),
+ bold(self.item_code),
+ bold(self.warehouse),
+ bold(abs(flt(available_qty, precision))),
+ ),
+ title=_("Negative Stock Error"),
+ exc=NegativeStockError,
+ )
def get_batchwise_available_qty(self):
- available_qty = self.get_available_qty_from_sabb()
- available_qty_from_ledger = self.get_available_qty_from_stock_ledger()
+ batchwise_entries = self.get_available_qty_from_sabb()
+ batchwise_entries.extend(self.get_available_qty_from_stock_ledger())
- if not available_qty_from_ledger:
- return available_qty
+ available_qty = frappe._dict({})
+ batchwise_entries = sorted(
+ batchwise_entries,
+ key=lambda x: (get_datetime(x.get("posting_datetime")), get_datetime(x.get("creation"))),
+ )
- for batch_no, qty in available_qty_from_ledger.items():
- if batch_no in available_qty:
- available_qty[batch_no] += qty
+ precision = frappe.get_precision("Serial and Batch Entry", "qty")
+ for row in batchwise_entries:
+ if row.batch_no in available_qty:
+ available_qty[row.batch_no] += flt(row.qty)
else:
- available_qty[batch_no] = qty
+ available_qty[row.batch_no] = flt(row.qty)
+
+ if flt(available_qty[row.batch_no], precision) < 0:
+ self.throw_negative_batch(row.batch_no, available_qty[row.batch_no], precision)
return available_qty
@@ -1456,7 +1474,9 @@ class SerialandBatchBundle(Document):
frappe.qb.from_(sle)
.select(
sle.batch_no,
- Sum(sle.actual_qty).as_("available_qty"),
+ sle.actual_qty.as_("qty"),
+ sle.posting_datetime,
+ sle.creation,
)
.where(
(sle.item_code == self.item_code)
@@ -1468,12 +1488,9 @@ class SerialandBatchBundle(Document):
& (sle.batch_no.isnotnull())
)
.for_update()
- .groupby(sle.batch_no)
)
- res = query.run(as_list=True)
-
- return frappe._dict(res) if res else frappe._dict()
+ return query.run(as_dict=True)
def get_available_qty_from_sabb(self):
batches = [d.batch_no for d in self.entries if d.batch_no]
@@ -1487,7 +1504,9 @@ class SerialandBatchBundle(Document):
.on(parent.name == child.parent)
.select(
child.batch_no,
- Sum(child.qty).as_("total_qty"),
+ child.qty,
+ CombineDatetime(parent.posting_date, parent.posting_time).as_("posting_datetime"),
+ parent.creation,
)
.where(
(parent.warehouse == self.warehouse)
@@ -1498,14 +1517,11 @@ class SerialandBatchBundle(Document):
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
)
.for_update()
- .groupby(child.batch_no)
)
query = query.where(parent.voucher_type != "Pick List")
- res = query.run(as_list=True)
-
- return frappe._dict(res) if res else frappe._dict()
+ return query.run(as_dict=True)
def validate_voucher_no_docstatus(self):
if self.voucher_type == "POS Invoice":
@@ -2465,11 +2481,11 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
key = (row.batch_no, row.warehouse)
if key in pos_batches:
- pos_batches[key]["qty"] -= row.qty * -1 if row.is_return else row.qty
+ pos_batches[key]["qty"] += row.qty * -1
else:
pos_batches[key] = frappe._dict(
{
- "qty": (row.qty * -1 if not row.is_return else row.qty),
+ "qty": row.qty * -1,
"warehouse": row.warehouse,
}
)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index d313c037e02..d7158bf1680 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -226,6 +226,7 @@ class StockEntry(StockController):
self.validate_job_card_item()
self.set_purpose_for_stock_entry()
self.clean_serial_nos()
+ self.validate_repack_entry()
if not self.from_bom:
self.fg_completed_qty = 0.0
@@ -245,6 +246,20 @@ class StockEntry(StockController):
self.validate_same_source_target_warehouse_during_material_transfer()
self.validate_raw_materials_exists()
+ def validate_repack_entry(self):
+ if self.purpose != "Repack":
+ return
+
+ fg_items = {row.item_code: row for row in self.items if row.is_finished_item}
+
+ if len(fg_items) > 1 and not all(row.set_basic_rate_manually for row in fg_items.values()):
+ frappe.throw(
+ _(
+ "When there are multiple finished goods ({0}) in a Repack stock entry, the basic rate for all finished goods must be set manually. To set rate manually, enable the checkbox 'Set Basic Rate Manually' in the respective finished good row."
+ ).format(", ".join(fg_items)),
+ title=_("Set Basic Rate Manually"),
+ )
+
def validate_raw_materials_exists(self):
if self.purpose not in ["Manufacture", "Repack", "Disassemble"]:
return
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 2383fabaf89..f11854f52a9 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -413,6 +413,10 @@ class TestStockEntry(FrappeTestCase):
},
)
repack.set_stock_entry_type()
+ for row in repack.items:
+ if row.t_warehouse:
+ row.set_basic_rate_manually = 1
+
repack.insert()
self.assertEqual(repack.items[1].is_finished_item, 1)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 5359995ea9c..8ab9cf210ce 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -1266,15 +1266,11 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
for d in items:
if (d.item_code, d.warehouse) in itemwise_batch_data:
- valuation_rate = get_stock_balance(
- d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
- )[1]
-
for row in itemwise_batch_data.get((d.item_code, d.warehouse)):
if ignore_empty_stock and not row.qty:
continue
- args = get_item_data(row, row.qty, valuation_rate)
+ args = get_item_data(row, row.qty, row.valuation_rate)
res.append(args)
else:
stock_bal = get_stock_balance(
@@ -1408,6 +1404,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
"item_code": row[0],
"warehouse": row[3],
"qty": row[8],
+ "valuation_rate": row[9],
"item_name": row[1],
"batch_no": row[4],
}
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index 2535fc4096f..913a31df1a7 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -198,7 +198,11 @@ class StockBalanceReport:
for field in self.inventory_dimensions:
qty_dict[field] = entry.get(field)
- if entry.voucher_type == "Stock Reconciliation" and (not entry.batch_no or entry.serial_no):
+ if (
+ entry.voucher_type == "Stock Reconciliation"
+ and frappe.get_cached_value(entry.voucher_type, entry.voucher_no, "purpose") != "Opening Stock"
+ and (not entry.batch_no or entry.serial_no)
+ ):
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
else:
qty_diff = flt(entry.actual_qty)
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index 6a2f818f7ae..c3f5086fbc5 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -3,6 +3,7 @@
import frappe
+from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import cstr, flt, now, nowdate, nowtime
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
@@ -182,18 +183,67 @@ def get_indented_qty(item_code, warehouse):
def get_ordered_qty(item_code, warehouse):
- ordered_qty = frappe.db.sql(
- """
- select sum((po_item.qty - po_item.received_qty)*po_item.conversion_factor)
- from `tabPurchase Order Item` po_item, `tabPurchase Order` po
- where po_item.item_code=%s and po_item.warehouse=%s
- and po_item.qty > po_item.received_qty and po_item.parent=po.name
- and po.status not in ('Closed', 'Delivered') and po.docstatus=1
- and po_item.delivered_by_supplier = 0""",
- (item_code, warehouse),
+ """Return total pending ordered quantity for an item in a warehouse.
+ Includes outstanding quantities from Purchase Orders and Subcontracting Orders"""
+
+ purchase_order_qty = get_purchase_order_qty(item_code, warehouse)
+ subcontracting_order_qty = get_subcontracting_order_qty(item_code, warehouse)
+
+ return flt(purchase_order_qty) + flt(subcontracting_order_qty)
+
+
+def get_purchase_order_qty(item_code, warehouse):
+ PurchaseOrder = frappe.qb.DocType("Purchase Order")
+ PurchaseOrderItem = frappe.qb.DocType("Purchase Order Item")
+
+ purchase_order_qty = (
+ frappe.qb.from_(PurchaseOrderItem)
+ .join(PurchaseOrder)
+ .on(PurchaseOrderItem.parent == PurchaseOrder.name)
+ .select(
+ Sum(
+ (PurchaseOrderItem.qty - PurchaseOrderItem.received_qty) * PurchaseOrderItem.conversion_factor
+ )
+ )
+ .where(
+ (PurchaseOrderItem.item_code == item_code)
+ & (PurchaseOrderItem.warehouse == warehouse)
+ & (PurchaseOrderItem.qty > PurchaseOrderItem.received_qty)
+ & (PurchaseOrder.status.notin(["Closed", "Delivered"]))
+ & (PurchaseOrder.docstatus == 1)
+ & (Coalesce(PurchaseOrderItem.delivered_by_supplier, 0) == 0)
+ )
+ .run()
)
- return flt(ordered_qty[0][0]) if ordered_qty else 0
+ return purchase_order_qty[0][0] if purchase_order_qty else 0
+
+
+def get_subcontracting_order_qty(item_code, warehouse):
+ SubcontractingOrder = frappe.qb.DocType("Subcontracting Order")
+ SubcontractingOrderItem = frappe.qb.DocType("Subcontracting Order Item")
+
+ subcontracting_order_qty = (
+ frappe.qb.from_(SubcontractingOrderItem)
+ .join(SubcontractingOrder)
+ .on(SubcontractingOrderItem.parent == SubcontractingOrder.name)
+ .select(
+ Sum(
+ (SubcontractingOrderItem.qty - SubcontractingOrderItem.received_qty)
+ * SubcontractingOrderItem.conversion_factor
+ )
+ )
+ .where(
+ (SubcontractingOrderItem.item_code == item_code)
+ & (SubcontractingOrderItem.warehouse == warehouse)
+ & (SubcontractingOrderItem.qty > SubcontractingOrderItem.received_qty)
+ & (SubcontractingOrder.status.notin(["Closed", "Completed"]))
+ & (SubcontractingOrder.docstatus == 1)
+ )
+ .run()
+ )
+
+ return subcontracting_order_qty[0][0] if subcontracting_order_qty else 0
def get_planned_qty(item_code, warehouse):
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index 4f87e695dfc..f764329e338 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -8,7 +8,7 @@ from frappe.utils import flt
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController
-from erpnext.stock.stock_balance import update_bin_qty
+from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
from erpnext.stock.utils import get_bin
@@ -211,30 +211,7 @@ class SubcontractingOrder(SubcontractingController):
):
item_wh_list.append([item.item_code, item.warehouse])
for item_code, warehouse in item_wh_list:
- update_bin_qty(item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)})
-
- @staticmethod
- def get_ordered_qty(item_code, warehouse):
- table = frappe.qb.DocType("Subcontracting Order")
- child = frappe.qb.DocType("Subcontracting Order Item")
-
- query = (
- frappe.qb.from_(table)
- .inner_join(child)
- .on(table.name == child.parent)
- .select((child.qty - child.received_qty) * child.conversion_factor)
- .where(
- (table.docstatus == 1)
- & (child.item_code == item_code)
- & (child.warehouse == warehouse)
- & (child.qty > child.received_qty)
- & (table.status != "Completed")
- )
- )
-
- query = query.run()
-
- return flt(query[0][0]) if query else 0
+ update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)})
def update_reserved_qty_for_subcontracting(self, sco_item_rows=None):
for item in self.supplied_items:
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index 443ca8cb569..26025169979 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -618,6 +618,117 @@ class TestSubcontractingReceipt(FrappeTestCase):
for item in scr.supplied_items:
self.assertFalse(item.available_qty_for_consumption)
+ def test_supplied_items_consumed_qty_for_similar_finished_goods(self):
+ """
+ Test that supplied raw material consumption is calculated correctly
+ when multiple subcontracted service items use the same finished good
+ but different BOMs.
+ """
+
+ from erpnext.controllers.subcontracting_controller import (
+ make_rm_stock_entry as make_subcontract_transfer_entry,
+ )
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+ # Configuration: Backflush based on subcontract material transfer
+ set_backflush_based_on("Material Transferred for Subcontract")
+
+ # Create Raw Materials
+ raw_material_1 = make_item("_RM Item 1", properties={"is_stock_item": 1}).name
+
+ raw_material_2 = make_item("_RM Item 2", properties={"is_stock_item": 1}).name
+
+ # Create Subcontracted Finished Good
+ finished_good = make_item("_Finished Good Item", properties={"is_stock_item": 1})
+ finished_good.is_sub_contracted_item = 1
+ finished_good.save()
+
+ # Receive Raw Materials into Warehouse
+ for raw_material in (raw_material_1, raw_material_2):
+ make_stock_entry(
+ item_code=raw_material,
+ qty=10,
+ target="_Test Warehouse - _TC",
+ basic_rate=100,
+ )
+
+ # Create BOMs for the same Finished Good with different RMs
+ bom_rm_1 = make_bom(
+ item=finished_good.name,
+ quantity=1,
+ raw_materials=[raw_material_1],
+ ).name
+
+ _bom_rm_2 = make_bom(
+ item=finished_good.name,
+ quantity=1,
+ raw_materials=[raw_material_2],
+ ).name
+
+ # Define Subcontracted Service Items
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 1,
+ "rate": 100,
+ "fg_item": finished_good.name,
+ "fg_item_qty": 10,
+ },
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 1,
+ "rate": 150,
+ "fg_item": finished_good.name,
+ "fg_item_qty": 10,
+ },
+ ]
+
+ # Create Subcontracting Order
+ subcontracting_order = get_subcontracting_order(
+ service_items=service_items,
+ do_not_save=True,
+ )
+
+ # Assign BOM only to the first service item
+ subcontracting_order.items[0].bom = bom_rm_1
+ subcontracting_order.save()
+ subcontracting_order.submit()
+
+ # Prepare Raw Material Transfer Items
+ raw_material_transfer_items = []
+ for supplied_item in subcontracting_order.supplied_items:
+ raw_material_transfer_items.append(
+ {
+ "item_code": supplied_item.main_item_code,
+ "rm_item_code": supplied_item.rm_item_code,
+ "qty": supplied_item.required_qty,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ }
+ )
+
+ # Transfer Raw Materials to Subcontractor Warehouse
+ stock_entry = frappe.get_doc(
+ make_subcontract_transfer_entry(
+ subcontracting_order.name,
+ raw_material_transfer_items,
+ )
+ )
+ stock_entry.to_warehouse = "_Test Warehouse 1 - _TC"
+ stock_entry.save()
+ stock_entry.submit()
+
+ # Create Subcontracting Receipt
+ subcontracting_receipt = make_subcontracting_receipt(subcontracting_order.name)
+ subcontracting_receipt.save()
+
+ # Check consumed_qty for each supplied item
+ self.assertEqual(len(subcontracting_receipt.supplied_items), 2)
+ self.assertEqual(subcontracting_receipt.supplied_items[0].consumed_qty, 10)
+ self.assertEqual(subcontracting_receipt.supplied_items[1].consumed_qty, 10)
+
def test_supplied_items_cost_after_reposting(self):
# Set Backflush Based On as "BOM"
set_backflush_based_on("BOM")