Merge branch 'develop' of https://github.com/frappe/erpnext into support-52064

This commit is contained in:
Pugazhendhi Velu
2025-11-04 11:41:03 +00:00
38 changed files with 538 additions and 206 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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