Merge pull request #50741 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-11-25 20:31:01 +05:30
committed by GitHub
57 changed files with 516 additions and 204 deletions

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
to: "{to_currency}", to: "{to_currency}",
}; };
add_param(frm, r.message, params, result); add_param(frm, r.message, params, result);
} else if (frm.doc.service_provider == "frankfurter.app") { } else if (frm.doc.service_provider == "frankfurter.dev") {
let result = ["rates", "{to_currency}"]; let result = ["rates", "{to_currency}"];
let params = { let params = {
base: "{from_currency}", base: "{from_currency}",

View File

@@ -78,7 +78,7 @@
"fieldname": "service_provider", "fieldname": "service_provider",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Service Provider", "label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom", "options": "frankfurter.dev\nexchangerate.host\nCustom",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -104,7 +104,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2024-03-18 08:32:26.895076", "modified": "2025-11-25 13:03:41.896424",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Currency Exchange Settings", "name": "Currency Exchange Settings",
@@ -141,7 +141,8 @@
"write": 1 "write": 1
} }
], ],
"sort_field": "modified", "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1

View File

@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
disabled: DF.Check disabled: DF.Check
req_params: DF.Table[CurrencyExchangeSettingsDetails] req_params: DF.Table[CurrencyExchangeSettingsDetails]
result_key: DF.Table[CurrencyExchangeSettingsResult] result_key: DF.Table[CurrencyExchangeSettingsResult]
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"] service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
url: DF.Data | None url: DF.Data | None
use_http: DF.Check use_http: DF.Check
# end: auto-generated types # end: auto-generated types
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "date", "value": "{transaction_date}"}) self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"}) self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"}) self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider == "frankfurter.app": elif self.service_provider == "frankfurter.dev":
self.set("result_key", []) self.set("result_key", [])
self.set("req_params", []) self.set("req_params", [])
@@ -105,11 +105,11 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist() @frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False): def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]: if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
if service_provider == "exchangerate.host": if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert" api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app": elif service_provider == "frankfurter.dev":
api = "api.frankfurter.app/{transaction_date}" api = "api.frankfurter.dev/v1/{transaction_date}"
protocol = "https://" protocol = "https://"
if use_http: if use_http:

View File

@@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args):
if group_condition: if group_condition:
conditions += " and " + group_condition conditions += " and " + group_condition
if args.get("transaction_date"): date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date", ignore=True
)
if date:
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01') conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')""" and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date") values["transaction_date"] = date
if args.get("doctype") in [ if args.get("doctype") in [
"Quotation", "Quotation",

View File

@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController { erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
setup(doc) { setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
super.setup(doc); super.setup(doc);

View File

@@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.selling.SellingController erpnext.selling.SellingController
) { ) {
setup(doc) { setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
super.setup(doc); super.setup(doc);
this.frm.make_methods = { this.frm.make_methods = {

View File

@@ -26,16 +26,13 @@ frappe.query_reports["Accounts Payable"] = {
{ {
fieldname: "cost_center", fieldname: "cost_center",
label: __("Cost Center"), label: __("Cost Center"),
fieldtype: "Link", fieldtype: "MultiSelectList",
options: "Cost Center", get_data: function (txt) {
get_query: () => { return frappe.db.get_link_options("Cost Center", txt, {
var company = frappe.query_report.get_filter_value("company"); company: frappe.query_report.get_filter_value("company"),
return { });
filters: {
company: company,
},
};
}, },
options: "Cost Center",
}, },
{ {
fieldname: "party_account", fieldname: "party_account",

View File

@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Payable Summary"] = {
{ {
fieldname: "cost_center", fieldname: "cost_center",
label: __("Cost Center"), label: __("Cost Center"),
fieldtype: "Link", fieldtype: "MultiSelectList",
options: "Cost Center", get_data: function (txt) {
get_query: () => { return frappe.db.get_link_options("Cost Center", txt, {
var company = frappe.query_report.get_filter_value("company"); company: frappe.query_report.get_filter_value("company"),
return { });
filters: {
company: company,
},
};
}, },
options: "Cost Center",
}, },
{ {
fieldname: "party_type", fieldname: "party_type",

View File

@@ -28,16 +28,13 @@ frappe.query_reports["Accounts Receivable"] = {
{ {
fieldname: "cost_center", fieldname: "cost_center",
label: __("Cost Center"), label: __("Cost Center"),
fieldtype: "Link", fieldtype: "MultiSelectList",
options: "Cost Center", get_data: function (txt) {
get_query: () => { return frappe.db.get_link_options("Cost Center", txt, {
var company = frappe.query_report.get_filter_value("company"); company: frappe.query_report.get_filter_value("company"),
return { });
filters: {
company: company,
},
};
}, },
options: "Cost Center",
}, },
{ {
fieldname: "party_type", fieldname: "party_type",

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
get_dimension_with_children, get_dimension_with_children,
) )
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
from erpnext.accounts.utils import ( from erpnext.accounts.utils import (
build_qb_match_conditions, build_qb_match_conditions,
get_advance_payment_doctypes, get_advance_payment_doctypes,
@@ -994,11 +995,7 @@ class ReceivablePayableReport:
self.add_accounting_dimensions_filters() self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self): def get_cost_center_conditions(self):
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
cost_center_list = [
center.name
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
]
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list)) self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
def add_common_filters(self): def add_common_filters(self):

View File

@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Receivable Summary"] = {
{ {
fieldname: "cost_center", fieldname: "cost_center",
label: __("Cost Center"), label: __("Cost Center"),
fieldtype: "Link", fieldtype: "MultiSelectList",
options: "Cost Center", get_data: function (txt) {
get_query: () => { return frappe.db.get_link_options("Cost Center", txt, {
var company = frappe.query_report.get_filter_value("company"); company: frappe.query_report.get_filter_value("company"),
return { });
filters: {
company: company,
},
};
}, },
options: "Cost Center",
}, },
{ {
fieldname: "party_type", fieldname: "party_type",

View File

@@ -69,12 +69,18 @@ class PartyLedgerSummaryReport:
party_type = self.filters.party_type party_type = self.filters.party_type
doctype = qb.DocType(party_type) doctype = qb.DocType(party_type)
party_details_fields = [
doctype.name.as_("party"),
f"{scrub(party_type)}_name",
f"{scrub(party_type)}_group",
]
if party_type == "Customer":
party_details_fields.append(doctype.territory)
conditions = self.get_party_conditions(doctype) conditions = self.get_party_conditions(doctype)
query = ( query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions))
qb.from_(doctype)
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
.where(Criterion.all(conditions))
)
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
@@ -153,6 +159,31 @@ class PartyLedgerSummaryReport:
credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note" credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note"
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
},
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
}
]
columns += [ columns += [
{ {
"label": _("Opening Balance"), "label": _("Opening Balance"),
@@ -213,35 +244,6 @@ class PartyLedgerSummaryReport:
}, },
] ]
# Hidden columns for handling 'User Permissions'
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"hidden": 1,
},
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
"hidden": 1,
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
"hidden": 1,
}
]
return columns return columns
def get_data(self): def get_data(self):

View File

@@ -47,22 +47,23 @@ frappe.query_reports["Trial Balance"] = {
{ {
fieldname: "cost_center", fieldname: "cost_center",
label: __("Cost Center"), label: __("Cost Center"),
fieldtype: "Link", fieldtype: "MultiSelectList",
options: "Cost Center", get_data: function (txt) {
get_query: function () { return frappe.db.get_link_options("Cost Center", txt, {
var company = frappe.query_report.get_filter_value("company"); company: frappe.query_report.get_filter_value("company"),
return { });
doctype: "Cost Center",
filters: {
company: company,
},
};
}, },
options: "Cost Center",
}, },
{ {
fieldname: "project", fieldname: "project",
label: __("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", options: "Project",
}, },
{ {

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
from erpnext.accounts.report.financial_statements import ( from erpnext.accounts.report.financial_statements import (
filter_accounts, filter_accounts,
filter_out_zero_value_rows, filter_out_zero_value_rows,
get_cost_centers_with_children,
set_gl_entries_by_account, set_gl_entries_by_account,
) )
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
@@ -103,10 +104,6 @@ def get_data(filters):
opening_balances = get_opening_balances(filters, ignore_is_opening) 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( set_gl_entries_by_account(
filters.company, filters.company,
filters.from_date, filters.from_date,
@@ -270,18 +267,12 @@ def get_opening_balance(
opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher") opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher")
if filters.cost_center: 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( opening_balance = opening_balance.where(
closing_balance.cost_center.isin( closing_balance.cost_center.isin(get_cost_centers_with_children(filters.get("cost_center")))
frappe.qb.from_(cost_center)
.select("name")
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
)
) )
if filters.project: 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 frappe.db.count("Finance Book"):
if filters.get("include_default_book_entries"): if filters.get("include_default_book_entries"):

View File

@@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", {
callback: function (r) { callback: function (r) {
if (!r.message) { if (!r.message) {
$(".primary-action").prop("hidden", true); $(".primary-action").prop("hidden", true);
$(".form-message").text("Capitalize this asset to confirm"); $(".form-message").text(__("Capitalize this asset to confirm"));
frm.add_custom_button(__("Capitalize Asset"), function () { frm.add_custom_button(__("Capitalize Asset"), function () {
frm.trigger("create_asset_capitalization"); frm.trigger("create_asset_capitalization");

View File

@@ -316,7 +316,6 @@ class AssetRepair(AccountsController):
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": self.completion_date, "posting_date": self.completion_date,
"against_voucher_type": "Purchase Invoice", "against_voucher_type": "Purchase Invoice",
"against_voucher": self.purchase_invoice,
"company": self.company, "company": self.company,
}, },
item=self, item=self,

View File

@@ -36,6 +36,7 @@
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
"column_break_11", "column_break_11",
"over_transfer_allowance", "over_transfer_allowance",
"validate_consumed_qty",
"section_break_xcug", "section_break_xcug",
"auto_create_subcontracting_order", "auto_create_subcontracting_order",
"column_break_izrr", "column_break_izrr",
@@ -270,6 +271,14 @@
"label": "Fixed Outgoing Email Account", "label": "Fixed Outgoing Email Account",
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]", "link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
"options": "Email Account" "options": "Email Account"
},
{
"default": "0",
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
"fieldname": "validate_consumed_qty",
"fieldtype": "Check",
"label": "Validate Consumed Qty (as per BOM)"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -278,7 +287,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-08-20 22:13:38.506889", "modified": "2025-11-20 12:59:09.925862",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@@ -44,6 +44,7 @@ class BuyingSettings(Document):
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"] supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
supplier_group: DF.Link | None supplier_group: DF.Link | None
use_transaction_date_exchange_rate: DF.Check use_transaction_date_exchange_rate: DF.Check
validate_consumed_qty: DF.Check
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):

View File

@@ -303,6 +303,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
erpnext.buying.BuyingController erpnext.buying.BuyingController
) { ) {
setup() { setup() {
this.setup_accounting_dimension_triggers();
this.frm.custom_make_buttons = { this.frm.custom_make_buttons = {
"Purchase Receipt": "Purchase Receipt", "Purchase Receipt": "Purchase Receipt",
"Purchase Invoice": "Purchase Invoice", "Purchase Invoice": "Purchase Invoice",

View File

@@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", {
frm.set_query("supplier_primary_contact", function (doc) { frm.set_query("supplier_primary_contact", function (doc) {
return { return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact", query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: { filters: {
supplier: doc.name, supplier: doc.name,
type: "Contact",
}, },
}; };
}); });
frm.set_query("supplier_primary_address", function (doc) { frm.set_query("supplier_primary_address", function (doc) {
return { return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: { filters: {
link_doctype: "Supplier", supplier: doc.name,
link_name: doc.name, type: "Address",
}, },
}; };
}); });

View File

@@ -215,19 +215,25 @@ class Supplier(TransactionBase):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters):
supplier = filters.get("supplier") supplier = filters.get("supplier")
contact = frappe.qb.DocType("Contact") type = filters.get("type")
type_doctype = frappe.qb.DocType(type)
dynamic_link = frappe.qb.DocType("Dynamic Link") dynamic_link = frappe.qb.DocType("Dynamic Link")
return ( query = (
frappe.qb.from_(contact) frappe.qb.from_(type_doctype)
.join(dynamic_link) .join(dynamic_link)
.on(contact.name == dynamic_link.parent) .on(type_doctype.name == dynamic_link.parent)
.select(contact.name, contact.email_id) .select(type_doctype.name)
.where( .where(
(dynamic_link.link_name == supplier) (dynamic_link.link_name == supplier)
& (dynamic_link.link_doctype == "Supplier") & (dynamic_link.link_doctype == "Supplier")
& (contact.name.like(f"%{txt}%")) & (type_doctype.name.like(f"%{txt}%"))
) )
).run(as_dict=False) )
if type == "Contact":
query = query.select(type_doctype.email_id)
return query.run()

View File

@@ -93,6 +93,7 @@ status_map = {
["Draft", None], ["Draft", None],
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"], ["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"], ["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
[ [
"Completed", "Completed",

View File

@@ -505,7 +505,7 @@ class SubcontractingController(StockController):
if item.get("serial_and_batch_bundle"): if item.get("serial_and_batch_bundle"):
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True) frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
@@ -849,7 +849,7 @@ class SubcontractingController(StockController):
if self.doctype == self.subcontract_data.order_doctype or ( if self.doctype == self.subcontract_data.order_doctype or (
self.backflush_based_on == "BOM" or self.is_return self.backflush_based_on == "BOM" or self.is_return
): ):
for bom_item in self.__get_materials_from_bom( for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items") row.item_code, row.bom, row.get("include_exploded_items")
): ):
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor

View File

@@ -548,12 +548,14 @@
{ {
"fieldname": "process_loss_percentage", "fieldname": "process_loss_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "% Process Loss" "label": "% Process Loss",
"non_negative": 1
}, },
{ {
"fieldname": "process_loss_qty", "fieldname": "process_loss_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Process Loss Qty", "label": "Process Loss Qty",
"non_negative": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -639,7 +641,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-10-29 17:43:12.966753", "modified": "2025-11-19 16:17:15.925156",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@@ -465,7 +465,7 @@ class BOM(WebsiteGenerator):
) )
) )
def get_rm_rate(self, arg): def get_rm_rate(self, arg, notify=True):
"""Get raw material rate as per selected method, if bom exists takes bom cost""" """Get raw material rate as per selected method, if bom exists takes bom cost"""
rate = 0 rate = 0
if not self.rm_cost_as_per: if not self.rm_cost_as_per:
@@ -491,7 +491,7 @@ class BOM(WebsiteGenerator):
), ),
alert=True, alert=True,
) )
else: elif notify:
frappe.msgprint( frappe.msgprint(
_("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]), _("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]),
alert=True, alert=True,
@@ -796,11 +796,14 @@ class BOM(WebsiteGenerator):
"stock_uom": d.stock_uom, "stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor, "conversion_factor": d.conversion_factor,
"sourced_by_supplier": d.sourced_by_supplier, "sourced_by_supplier": d.sourced_by_supplier,
} },
notify=False,
) )
d.base_rate = flt(d.rate) * flt(self.conversion_rate) d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")) d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")), d.precision("amount")
)
d.base_amount = d.amount * flt(self.conversion_rate) d.base_amount = d.amount * flt(self.conversion_rate)
d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt( d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt(
self.quantity, self.precision("quantity") self.quantity, self.precision("quantity")
@@ -823,7 +826,10 @@ class BOM(WebsiteGenerator):
d.base_rate = flt(d.rate, d.precision("rate")) * flt( d.base_rate = flt(d.rate, d.precision("rate")) * flt(
self.conversion_rate, self.precision("conversion_rate") self.conversion_rate, self.precision("conversion_rate")
) )
d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")) d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")),
d.precision("amount"),
)
d.base_amount = flt(d.amount, d.precision("amount")) * flt( d.base_amount = flt(d.amount, d.precision("amount")) * flt(
self.conversion_rate, self.precision("conversion_rate") self.conversion_rate, self.precision("conversion_rate")
) )

View File

@@ -38,6 +38,15 @@ frappe.ui.form.on("Job Card", {
return doc.status === "Complete" ? "green" : "orange"; return doc.status === "Complete" ? "green" : "orange";
} }
}); });
frm.set_query("employee", () => {
return {
filters: {
company: frm.doc.company,
status: "Active",
},
};
});
}, },
refresh: function (frm) { refresh: function (frm) {

View File

@@ -318,7 +318,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2024-10-21 14:13:38.777556", "modified": "2025-11-24 11:11:28.343568",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing", "name": "Manufacturing",
@@ -336,7 +336,7 @@
"doc_view": "List", "doc_view": "List",
"label": "Learn Manufacturing", "label": "Learn Manufacturing",
"type": "URL", "type": "URL",
"url": "https://school.frappe.io/lms/courses/manufacturing?utm_source=in_app" "url": "https://school.frappe.io/lms/courses/production-planning-and-execution"
}, },
{ {
"color": "Grey", "color": "Grey",

View File

@@ -426,3 +426,4 @@ erpnext.patches.v15_0.set_asset_status_if_not_already_set
erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing
execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1) execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter

View File

@@ -0,0 +1,12 @@
import frappe
def execute():
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
return
settings.service_provider = "frankfurter.dev"
settings.set_parameters_and_result()
settings.flags.ignore_validate = True
settings.save()

View File

@@ -2749,6 +2749,23 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
]); ]);
} }
} }
setup_accounting_dimension_triggers() {
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
callback: function (r) {
if (r.message && r.message[0]) {
let dimensions = r.message[0].map((d) => d.fieldname);
dimensions.forEach((dim) => {
// nosemgrep: frappe-semgrep-rules.rules.frappe-cur-frm-usage
cur_frm.cscript[dim] = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", dim);
};
});
}
},
});
}
}; };
erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) { erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) {

View File

@@ -79,18 +79,35 @@ erpnext.financial_statements = {
}, },
open_general_ledger: function (data) { open_general_ledger: function (data) {
if (!data.account && !data.accounts) return; 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"; return e.df.fieldname == "project";
}); });
let cost_center = $.grep(filters, function (e) {
return e.df.fieldname == "cost_center";
});
frappe.route_options = { frappe.route_options = {
account: data.account || data.accounts, account: data.account || data.accounts,
company: frappe.query_report.get_filter_value("company"), company: frappe.query_report.get_filter_value("company"),
from_date: data.from_date || data.year_start_date, from_date: data.from_date || data.year_start_date,
to_date: data.to_date || data.year_end_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"; let report = "General Ledger";
if (["Payable", "Receivable"].includes(data.account_type)) { if (["Payable", "Receivable"].includes(data.account_type)) {

View File

@@ -55,17 +55,20 @@ frappe.ui.form.on("Customer", {
frm.set_query("customer_primary_contact", function (doc) { frm.set_query("customer_primary_contact", function (doc) {
return { return {
query: "erpnext.selling.doctype.customer.customer.get_customer_primary_contact", query: "erpnext.selling.doctype.customer.customer.get_customer_primary",
filters: { filters: {
customer: doc.name, customer: doc.name,
type: "Contact",
}, },
}; };
}); });
frm.set_query("customer_primary_address", function (doc) { frm.set_query("customer_primary_address", function (doc) {
return { return {
query: "erpnext.selling.doctype.customer.customer.get_customer_primary",
filters: { filters: {
link_doctype: "Customer", customer: doc.name,
link_name: doc.name, type: "Address",
}, },
}; };
}); });

View File

@@ -610,7 +610,7 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2025-03-05 10:01:47.885574", "modified": "2025-11-25 09:35:56.772949",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",

View File

@@ -232,7 +232,7 @@ class Customer(TransactionBase):
self.update_lead_status() self.update_lead_status()
if self.flags.is_new_doc: if self.flags.is_new_doc:
self.link_lead_address_and_contact() self.link_address_and_contact()
self.copy_communication() self.copy_communication()
self.update_customer_groups() self.update_customer_groups()
@@ -272,15 +272,23 @@ class Customer(TransactionBase):
if self.lead_name: if self.lead_name:
frappe.db.set_value("Lead", self.lead_name, "status", "Converted") frappe.db.set_value("Lead", self.lead_name, "status", "Converted")
def link_lead_address_and_contact(self): def link_address_and_contact(self):
if self.lead_name: linked_documents = {
# assign lead address and contact to customer (if already not set) "Lead": self.lead_name,
"Opportunity": self.opportunity_name,
"Prospect": self.prospect_name,
}
for doctype, docname in linked_documents.items():
# assign lead, opportunity and prospect address and contact to customer (if already not set)
if not docname:
continue
linked_contacts_and_addresses = frappe.get_all( linked_contacts_and_addresses = frappe.get_all(
"Dynamic Link", "Dynamic Link",
filters=[ filters=[
["parenttype", "in", ["Contact", "Address"]], ["parenttype", "in", ["Contact", "Address"]],
["link_doctype", "=", "Lead"], ["link_doctype", "=", doctype],
["link_name", "=", self.lead_name], ["link_name", "=", docname],
], ],
fields=["parent as name", "parenttype as doctype"], fields=["parent as name", "parenttype as doctype"],
) )
@@ -792,21 +800,29 @@ def make_address(args, is_primary_address=1, is_shipping_address=1):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): def get_customer_primary(doctype, txt, searchfield, start, page_len, filters):
customer = filters.get("customer") customer = filters.get("customer")
type = filters.get("type")
con = qb.DocType("Contact") type_doctype = qb.DocType(type)
dlink = qb.DocType("Dynamic Link") dlink = qb.DocType("Dynamic Link")
return ( query = (
qb.from_(con) qb.from_(type_doctype)
.join(dlink) .join(dlink)
.on(con.name == dlink.parent) .on(type_doctype.name == dlink.parent)
.select(con.name, con.email_id) .select(type_doctype.name)
.where((dlink.link_name == customer) & (con.name.like(f"%{txt}%"))) .where(
.run() (dlink.link_name == customer)
& (type_doctype.name.like(f"%{txt}%"))
& (dlink.link_doctype == "Customer")
)
) )
if type == "Contact":
query = query.select(type_doctype.email_id)
return query.run()
def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]: def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]:
"""Parse full name into first name, middle name and last name""" """Parse full name into first name, middle name and last name"""

View File

@@ -574,6 +574,9 @@ frappe.ui.form.on("Sales Order Item", {
}); });
erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController { erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController {
setup() {
this.setup_accounting_dimension_triggers();
}
onload(doc, dt, dn) { onload(doc, dt, dn) {
super.onload(doc, dt, dn); super.onload(doc, dt, dn);
} }

View File

@@ -68,9 +68,9 @@ def patched_requests_get(*args, **kwargs):
if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"): if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"):
if test_exchange_values.get(kwargs["params"]["date"]): if test_exchange_values.get(kwargs["params"]["date"]):
return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200) return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200)
elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"): elif args[0].startswith("https://api.frankfurter.dev") and kwargs.get("params"):
if kwargs["params"].get("base") and kwargs["params"].get("symbols"): if kwargs["params"].get("base") and kwargs["params"].get("symbols"):
date = args[0].replace("https://api.frankfurter.app/", "") date = args[0].replace("https://api.frankfurter.dev/v1/", "")
if test_exchange_values.get(date): if test_exchange_values.get(date):
return PatchResponse( return PatchResponse(
{"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200 {"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200
@@ -149,7 +149,7 @@ class TestCurrencyExchange(unittest.TestCase):
self.assertEqual(flt(exchange_rate, 3), 65.1) self.assertEqual(flt(exchange_rate, 3), 65.1)
settings = frappe.get_single("Currency Exchange Settings") settings = frappe.get_single("Currency Exchange Settings")
settings.service_provider = "frankfurter.app" settings.service_provider = "frankfurter.dev"
settings.save() settings.save()
def test_exchange_rate_strict(self, mock_get): def test_exchange_rate_strict(self, mock_get):

View File

@@ -93,7 +93,7 @@ def setup_currency_exchange():
ces.set("result_key", []) ces.set("result_key", [])
ces.set("req_params", []) ces.set("req_params", [])
ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}" ces.api_endpoint = "https://api.frankfurter.dev/v1/{transaction_date}"
ces.append("result_key", {"key": "rates"}) ces.append("result_key", {"key": "rates"})
ces.append("result_key", {"key": "{to_currency}"}) ces.append("result_key", {"key": "{to_currency}"})
ces.append("req_params", {"key": "base", "value": "{from_currency}"}) ces.append("req_params", {"key": "base", "value": "{from_currency}"})

View File

@@ -140,6 +140,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
erpnext.selling.SellingController erpnext.selling.SellingController
) { ) {
setup(doc) { setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
super.setup(doc); super.setup(doc);
this.frm.make_methods = { this.frm.make_methods = {

View File

@@ -332,6 +332,9 @@ frappe.ui.form.on("Material Request", {
label: __("For Warehouse"), label: __("For Warehouse"),
options: "Warehouse", options: "Warehouse",
reqd: 1, reqd: 1,
get_query: function () {
return { filters: { company: frm.doc.company } };
},
}, },
{ fieldname: "qty", fieldtype: "Float", label: __("Quantity"), reqd: 1, default: 1 }, { fieldname: "qty", fieldtype: "Float", label: __("Quantity"), reqd: 1, default: 1 },
{ {

View File

@@ -108,7 +108,12 @@ def get_indexed_packed_items_table(doc):
""" """
indexed_table = {} indexed_table = {}
for packed_item in doc.get("packed_items"): for packed_item in doc.get("packed_items"):
key = (packed_item.parent_item, packed_item.item_code, packed_item.parent_detail_docname) key = (
packed_item.parent_item,
packed_item.item_code,
packed_item.idx if doc.is_new() else packed_item.parent_detail_docname,
)
indexed_table[key] = packed_item indexed_table[key] = packed_item
return indexed_table return indexed_table
@@ -169,7 +174,11 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re
exists, pi_row = False, {} exists, pi_row = False, {}
# check if row already exists in packed items table # check if row already exists in packed items table
key = (main_item_row.item_code, packing_item.item_code, main_item_row.name) key = (
main_item_row.item_code,
packing_item.item_code,
main_item_row.idx if doc.is_new() else main_item_row.name,
)
if packed_items_table.get(key): if packed_items_table.get(key):
pi_row, exists = packed_items_table.get(key), True pi_row, exists = packed_items_table.get(key), True

View File

@@ -7,7 +7,7 @@ from itertools import groupby
import frappe import frappe
from frappe import _, bold from frappe import _, bold
from frappe.model.mapper import get_mapped_doc, map_child_doc from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
@@ -646,8 +646,8 @@ class PickList(TransactionBase):
product_bundles = self._get_product_bundles() product_bundles = self._get_product_bundles()
product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values()) product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values())
for so_row, item_code in product_bundles.items(): for so_row, value in product_bundles.items():
picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[value.item_code])
item_table = "Sales Order Item" item_table = "Sales Order Item"
already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True) already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True)
frappe.db.set_value( frappe.db.set_value(
@@ -770,19 +770,23 @@ class PickList(TransactionBase):
if not item.product_bundle_item: if not item.product_bundle_item:
continue continue
product_bundles[item.sales_order_item] = frappe.db.get_value( product_bundles[item.sales_order_item] = frappe._dict(
"Sales Order Item", {
item.sales_order_item, "item_code": frappe.db.get_value(
"item_code", "Sales Order Item",
item.sales_order_item,
"item_code",
),
"pick_list_item": item.name,
}
) )
return product_bundles return product_bundles
def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]: def _get_product_bundle_qty_map(self, bundles) -> dict[str, dict[str, float]]:
# bundle_item_code: Dict[component, qty]
product_bundle_qty_map = {} product_bundle_qty_map = {}
for bundle_item_code in bundles: for data in bundles:
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}) bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": data.item_code, "disabled": 0})
product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} product_bundle_qty_map[data.item_code] = {item.item_code: item.qty for item in bundle.items}
return product_bundle_qty_map return product_bundle_qty_map
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int: def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
@@ -1388,15 +1392,16 @@ def add_product_bundles_to_delivery_note(
product_bundles = pick_list._get_product_bundles() product_bundles = pick_list._get_product_bundles()
product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values())
for so_row, item_code in product_bundles.items(): for so_row, value in product_bundles.items():
sales_order_item = frappe.get_doc("Sales Order Item", so_row) sales_order_item = frappe.get_doc("Sales Order Item", so_row)
if sales_order and sales_order_item.parent != sales_order: if sales_order and sales_order_item.parent != sales_order:
continue continue
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper) dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
so_row, product_bundle_qty_map[item_code] so_row, product_bundle_qty_map[value.item_code]
) )
dn_bundle_item.pick_list_item = value.pick_list_item
dn_bundle_item.against_pick_list = pick_list.name dn_bundle_item.against_pick_list = pick_list.name
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)

View File

@@ -195,6 +195,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
erpnext.buying.BuyingController erpnext.buying.BuyingController
) { ) {
setup(doc) { setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
super.setup(doc); super.setup(doc);
} }

View File

@@ -893,7 +893,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", "options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
"print_hide": 1, "print_hide": 1,
"print_width": "150px", "print_width": "150px",
"read_only": 1, "read_only": 1,
@@ -1300,7 +1300,7 @@
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-08-06 16:41:02.690658", "modified": "2025-11-12 19:53:48.173096",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@@ -117,7 +117,15 @@ class PurchaseReceipt(BuyingController):
shipping_address_display: DF.SmallText | None shipping_address_display: DF.SmallText | None
shipping_rule: DF.Link | None shipping_rule: DF.Link | None
status: DF.Literal[ status: DF.Literal[
"", "Draft", "Partly Billed", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed" "",
"Draft",
"Partly Billed",
"To Bill",
"Completed",
"Return",
"Return Issued",
"Cancelled",
"Closed",
] ]
subcontracting_receipt: DF.Link | None subcontracting_receipt: DF.Link | None
supplied_items: DF.Table[PurchaseReceiptItemSupplied] supplied_items: DF.Table[PurchaseReceiptItemSupplied]

View File

@@ -18,6 +18,9 @@ def get_data():
"Purchase Order": ["items", "purchase_order"], "Purchase Order": ["items", "purchase_order"],
"Project": ["items", "project"], "Project": ["items", "project"],
}, },
"internal_and_external_links": {
"Purchase Invoice": ["items", "purchase_invoice"],
},
"transactions": [ "transactions": [
{ {
"label": _("Related"), "label": _("Related"),

View File

@@ -11,7 +11,7 @@ frappe.listview_settings["Purchase Receipt"] = {
"currency", "currency",
], ],
get_indicator: function (doc) { get_indicator: function (doc) {
if (cint(doc.is_return) == 1) { if (cint(doc.is_return) == 1 && doc.status == "Return") {
return [__("Return"), "gray", "is_return,=,Yes"]; return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.status === "Closed") { } else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"]; return [__("Closed"), "green", "status,=,Closed"];

View File

@@ -455,6 +455,7 @@ class TestPurchaseReceipt(FrappeTestCase):
# Check if Original PR updated # Check if Original PR updated
self.assertEqual(pr.items[0].returned_qty, 2) self.assertEqual(pr.items[0].returned_qty, 2)
self.assertEqual(pr.per_returned, 40) self.assertEqual(pr.per_returned, 40)
self.assertEqual(returned.status, "Return")
from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.controllers.sales_and_purchase_return import make_return_doc
@@ -2128,7 +2129,7 @@ class TestPurchaseReceipt(FrappeTestCase):
return_pr.items[0].stock_qty = 0.0 return_pr.items[0].stock_qty = 0.0
return_pr.submit() return_pr.submit()
self.assertEqual(return_pr.status, "To Bill") self.assertEqual(return_pr.status, "Return")
pi = make_purchase_invoice(return_pr.name) pi = make_purchase_invoice(return_pr.name)
pi.submit() pi.submit()

View File

@@ -1347,7 +1347,36 @@ class SerialandBatchBundle(Document):
if self.voucher_type == "POS Invoice": if self.voucher_type == "POS Invoice":
return return
if frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1: child_doctype = self.voucher_type + " Item"
mapper = {
"Asset Capitalization": "Asset Capitalization Stock Item",
"Asset Repair": "Asset Repair Consumed Item",
"Stock Entry": "Stock Entry Detail",
}.get(self.voucher_type)
if mapper:
child_doctype = mapper
if self.voucher_type == "Delivery Note" and not frappe.db.exists(
"Delivery Note Item", self.voucher_detail_no
):
child_doctype = "Packed Item"
elif self.voucher_type == "Sales Invoice" and not frappe.db.exists(
"Sales Invoice Item", self.voucher_detail_no
):
child_doctype = "Packed Item"
elif self.voucher_type == "Subcontracting Receipt" and not frappe.db.exists(
"Subcontracting Receipt Item", self.voucher_detail_no
):
child_doctype = "Subcontracting Receipt Supplied Item"
if (
frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1
and self.voucher_detail_no
and frappe.db.exists(child_doctype, self.voucher_detail_no)
):
msg = f"""The {self.voucher_type} {bold(self.voucher_no)} msg = f"""The {self.voucher_type} {bold(self.voucher_no)}
is in submitted state, please cancel it first""" is in submitted state, please cancel it first"""
frappe.throw(_(msg)) frappe.throw(_(msg))

View File

@@ -951,12 +951,10 @@ frappe.ui.form.on("Stock Entry Detail", {
no_batch_serial_number_value = true; no_batch_serial_number_value = true;
} }
if ( if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog) {
no_batch_serial_number_value && if (!frappe.flags.dialog_set) {
!frappe.flags.hide_serial_batch_dialog && frappe.flags.dialog_set = true;
!frappe.flags.dialog_set }
) {
frappe.flags.dialog_set = true;
erpnext.stock.select_batch_and_serial_no(frm, d); erpnext.stock.select_batch_and_serial_no(frm, d);
} else { } else {
frappe.flags.dialog_set = false; frappe.flags.dialog_set = false;

View File

@@ -1136,7 +1136,9 @@ class StockEntry(StockController):
"qty": row.transfer_qty * -1, "qty": row.transfer_qty * -1,
} }
).update_serial_and_batch_entries() ).update_serial_and_batch_entries()
elif not row.serial_and_batch_bundle: elif not row.serial_and_batch_bundle and frappe.get_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
):
bundle_doc = SerialBatchCreation( bundle_doc = SerialBatchCreation(
{ {
"item_code": row.item_code, "item_code": row.item_code,

View File

@@ -979,6 +979,7 @@ class StockReconciliation(StockController):
is_customer_item = frappe.db.get_value("Item", d.item_code, "is_customer_provided_item") is_customer_item = frappe.db.get_value("Item", d.item_code, "is_customer_provided_item")
if is_customer_item and d.valuation_rate: if is_customer_item and d.valuation_rate:
d.valuation_rate = 0.0 d.valuation_rate = 0.0
d.allow_zero_valuation_rate = 1
changed_any_values = True changed_any_values = True
if changed_any_values: if changed_any_values:

View File

@@ -185,12 +185,10 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "allow_zero_valuation_rate",
"fieldname": "allow_zero_valuation_rate", "fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Zero Valuation Rate", "label": "Allow Zero Valuation Rate",
"print_hide": 1, "print_hide": 1
"read_only": 1
}, },
{ {
"depends_on": "barcode", "depends_on": "barcode",
@@ -256,7 +254,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-03-12 16:34:51.326821", "modified": "2025-11-20 15:27:13.868179",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Reconciliation Item", "name": "Stock Reconciliation Item",

View File

@@ -221,7 +221,9 @@ def get_child_warehouses(warehouse):
def get_warehouses_based_on_account(account, company=None): def get_warehouses_based_on_account(account, company=None):
warehouses = [] warehouses = []
for d in frappe.get_all("Warehouse", fields=["name", "is_group"], filters={"account": account}): for d in frappe.get_all(
"Warehouse", fields=["name", "is_group"], filters={"account": account, "disabled": 0}
):
if d.is_group: if d.is_group:
warehouses.extend(get_child_warehouses(d.name)) warehouses.extend(get_child_warehouses(d.name))
else: else:

View File

@@ -259,7 +259,7 @@ class SerialBatchBundle:
and not self.sle.serial_and_batch_bundle and not self.sle.serial_and_batch_bundle
and self.item_details.has_batch_no == 1 and self.item_details.has_batch_no == 1
and ( and (
self.item_details.create_new_batch (self.item_details.create_new_batch and self.sle.actual_qty > 0)
or ( or (
frappe.db.get_single_value( frappe.db.get_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"

View File

@@ -187,7 +187,7 @@ class SubcontractingOrder(SubcontractingController):
for item in self.get("items"): for item in self.get("items"):
bom = frappe.get_doc("BOM", item.bom) bom = frappe.get_doc("BOM", item.bom)
rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items) rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items)
item.rm_cost_per_qty = rm_cost / flt(bom.quantity) item.rm_cost_per_qty = flt(rm_cost / flt(bom.quantity), item.precision("rm_cost_per_qty"))
def calculate_items_qty_and_amount(self): def calculate_items_qty_and_amount(self):
total_qty = total = 0 total_qty = total = 0

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
@@ -17,6 +19,10 @@ from erpnext.stock.get_item_details import get_default_cost_center, get_default_
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
class BOMQuantityError(frappe.ValidationError):
pass
class SubcontractingReceipt(SubcontractingController): class SubcontractingReceipt(SubcontractingController):
# begin: auto-generated types # begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block. # This code is auto-generated. Do not modify anything in this block.
@@ -156,6 +162,7 @@ class SubcontractingReceipt(SubcontractingController):
def on_submit(self): def on_submit(self):
self.validate_closed_subcontracting_order() self.validate_closed_subcontracting_order()
self.validate_available_qty_for_consumption() self.validate_available_qty_for_consumption()
self.validate_bom_required_qty()
self.update_status_updater_args() self.update_status_updater_args()
self.update_prevdoc_status() self.update_prevdoc_status()
self.set_subcontracting_order_status(update_bin=False) self.set_subcontracting_order_status(update_bin=False)
@@ -512,12 +519,60 @@ class SubcontractingReceipt(SubcontractingController):
item.available_qty_for_consumption item.available_qty_for_consumption
and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0 and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
): ):
msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)} msg = _(
must be less than or equal to Available Qty For Consumption """Row {0}: Consumed Qty {1} {2} must be less than or equal to Available Qty For Consumption
{flt(item.available_qty_for_consumption, precision)} {3} {4} in Consumed Items Table."""
in Consumed Items Table.""" ).format(
item.idx,
flt(item.consumed_qty, precision),
item.stock_uom,
flt(item.available_qty_for_consumption, precision),
item.stock_uom,
)
frappe.throw(_(msg)) frappe.throw(msg)
def validate_bom_required_qty(self):
if (
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
== "Material Transferred for Subcontract"
) and not (frappe.db.get_single_value("Buying Settings", "validate_consumed_qty")):
return
rm_consumed_dict = self.get_rm_wise_consumed_qty()
for row in self.items:
precision = row.precision("qty")
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
required_qty = flt(
bom_item.qty_consumed_per_unit * row.qty * row.conversion_factor, precision
)
consumed_qty = rm_consumed_dict.get(bom_item.rm_item_code, 0)
diff = flt(consumed_qty, precision) - flt(required_qty, precision)
if diff < 0:
msg = _(
"""Additional {0} {1} of item {2} required as per BOM to complete this transaction"""
).format(
frappe.bold(abs(diff)),
frappe.bold(bom_item.stock_uom),
frappe.bold(bom_item.rm_item_code),
)
frappe.throw(
msg,
exc=BOMQuantityError,
)
def get_rm_wise_consumed_qty(self):
rm_dict = defaultdict(float)
for row in self.supplied_items:
rm_dict[row.rm_item_code] += row.consumed_qty
return rm_dict
def update_status_updater_args(self): def update_status_updater_args(self):
if cint(self.is_return): if cint(self.is_return):

View File

@@ -38,6 +38,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
make_subcontracting_receipt, make_subcontracting_receipt,
) )
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
BOMQuantityError,
)
class TestSubcontractingReceipt(FrappeTestCase): class TestSubcontractingReceipt(FrappeTestCase):
@@ -174,7 +177,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
def test_subcontracting_over_receipt(self): def test_subcontracting_over_receipt(self):
""" """
Behaviour: Raise multiple SCRs against one SCO that in total Behaviour: Raise multiple SCRs against one SCO that in total
receive more than the required qty in the SCO. receive more than the required qty in the SCO.
Expected Result: Error Raised for Over Receipt against SCO. Expected Result: Error Raised for Over Receipt against SCO.
""" """
from erpnext.controllers.subcontracting_controller import ( from erpnext.controllers.subcontracting_controller import (
@@ -1785,6 +1788,109 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertEqual(scr.items[0].rm_cost_per_qty, 300) self.assertEqual(scr.items[0].rm_cost_per_qty, 300)
self.assertEqual(scr.items[0].service_cost_per_qty, 100) self.assertEqual(scr.items[0].service_cost_per_qty, 100)
def test_bom_required_qty_validation_based_on_bom(self):
set_backflush_based_on("BOM")
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
rm_item1 = make_item(
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BRQV-.####",
}
).name
make_bom(item=fg_item, raw_materials=[rm_item1], rm_qty=2)
se = make_stock_entry(
item_code=rm_item1,
qty=1,
target="_Test Warehouse 1 - _TC",
rate=300,
)
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 1,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 1,
},
]
sco = get_subcontracting_order(service_items=service_items)
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.reload()
self.assertEqual(scr.supplied_items[0].batch_no, batch_no)
self.assertEqual(scr.supplied_items[0].consumed_qty, 1)
self.assertEqual(scr.supplied_items[0].required_qty, 2)
self.assertRaises(BOMQuantityError, scr.submit)
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
def test_bom_required_qty_validation_based_on_transfer(self):
from erpnext.controllers.subcontracting_controller import (
make_rm_stock_entry as make_subcontract_transfer_entry,
)
set_backflush_based_on("Material Transferred for Subcontract")
frappe.db.set_single_value("Buying Settings", "validate_consumed_qty", 1)
item_code = "_Test Subcontracted Validation FG Item 1"
rm_item1 = make_item(
properties={
"is_stock_item": 1,
}
).name
make_subcontracted_item(item_code=item_code, raw_materials=[rm_item1])
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 100,
"fg_item": item_code,
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(
service_items=service_items,
include_exploded_items=0,
)
# inward raw material stock
make_stock_entry(target="_Test Warehouse - _TC", item_code=rm_item1, qty=10, basic_rate=100)
rm_items = [
{
"item_code": item_code,
"rm_item_code": sco.supplied_items[0].rm_item_code,
"qty": sco.supplied_items[0].required_qty - 5,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
]
# transfer partial raw materials
ste = frappe.get_doc(make_subcontract_transfer_entry(sco.name, rm_items))
ste.to_warehouse = "_Test Warehouse 1 - _TC"
ste.save()
ste.submit()
scr = make_subcontracting_receipt(sco.name)
scr.save()
self.assertRaises(BOMQuantityError, scr.submit)
def make_return_subcontracting_receipt(**args): def make_return_subcontracting_receipt(**args):
args = frappe._dict(args) args = frappe._dict(args)