mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-20 23:35:11 +00:00
Merge pull request #50741 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
to: "{to_currency}",
|
||||
};
|
||||
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 params = {
|
||||
base: "{from_currency}",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -104,7 +104,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-18 08:32:26.895076",
|
||||
"modified": "2025-11-25 13:03:41.896424",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -141,8 +141,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
||||
disabled: DF.Check
|
||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||
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
|
||||
use_http: DF.Check
|
||||
# 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": "from", "value": "{from_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("req_params", [])
|
||||
|
||||
@@ -105,11 +105,11 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
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":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args):
|
||||
if 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')
|
||||
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 [
|
||||
"Quotation",
|
||||
|
||||
@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
|
||||
|
||||
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
|
||||
setup(doc) {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
erpnext.selling.SellingController
|
||||
) {
|
||||
setup(doc) {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
this.frm.make_methods = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
@@ -994,11 +995,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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -69,12 +69,18 @@ class PartyLedgerSummaryReport:
|
||||
party_type = self.filters.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)
|
||||
query = (
|
||||
qb.from_(doctype)
|
||||
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
|
||||
.where(Criterion.all(conditions))
|
||||
)
|
||||
query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(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"
|
||||
|
||||
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 += [
|
||||
{
|
||||
"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
|
||||
|
||||
def get_data(self):
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -103,10 +104,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,
|
||||
@@ -270,18 +267,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"):
|
||||
|
||||
@@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", {
|
||||
callback: function (r) {
|
||||
if (!r.message) {
|
||||
$(".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.trigger("create_asset_capitalization");
|
||||
|
||||
@@ -316,7 +316,6 @@ class AssetRepair(AccountsController):
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Purchase Invoice",
|
||||
"against_voucher": self.purchase_invoice,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
"over_transfer_allowance",
|
||||
"validate_consumed_qty",
|
||||
"section_break_xcug",
|
||||
"auto_create_subcontracting_order",
|
||||
"column_break_izrr",
|
||||
@@ -270,6 +271,14 @@
|
||||
"label": "Fixed Outgoing Email Account",
|
||||
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
|
||||
"options": "Email Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
|
||||
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
|
||||
"fieldname": "validate_consumed_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Consumed Qty (as per BOM)"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -278,7 +287,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 22:13:38.506889",
|
||||
"modified": "2025-11-20 12:59:09.925862",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -44,6 +44,7 @@ class BuyingSettings(Document):
|
||||
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
|
||||
supplier_group: DF.Link | None
|
||||
use_transaction_date_exchange_rate: DF.Check
|
||||
validate_consumed_qty: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -303,6 +303,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
erpnext.buying.BuyingController
|
||||
) {
|
||||
setup() {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.frm.custom_make_buttons = {
|
||||
"Purchase Receipt": "Purchase Receipt",
|
||||
"Purchase Invoice": "Purchase Invoice",
|
||||
|
||||
@@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", {
|
||||
|
||||
frm.set_query("supplier_primary_contact", function (doc) {
|
||||
return {
|
||||
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact",
|
||||
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
|
||||
filters: {
|
||||
supplier: doc.name,
|
||||
type: "Contact",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("supplier_primary_address", function (doc) {
|
||||
return {
|
||||
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
|
||||
filters: {
|
||||
link_doctype: "Supplier",
|
||||
link_name: doc.name,
|
||||
supplier: doc.name,
|
||||
type: "Address",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -215,19 +215,25 @@ class Supplier(TransactionBase):
|
||||
|
||||
@frappe.whitelist()
|
||||
@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")
|
||||
contact = frappe.qb.DocType("Contact")
|
||||
type = filters.get("type")
|
||||
type_doctype = frappe.qb.DocType(type)
|
||||
dynamic_link = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(contact)
|
||||
query = (
|
||||
frappe.qb.from_(type_doctype)
|
||||
.join(dynamic_link)
|
||||
.on(contact.name == dynamic_link.parent)
|
||||
.select(contact.name, contact.email_id)
|
||||
.on(type_doctype.name == dynamic_link.parent)
|
||||
.select(type_doctype.name)
|
||||
.where(
|
||||
(dynamic_link.link_name == 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()
|
||||
|
||||
@@ -93,6 +93,7 @@ status_map = {
|
||||
["Draft", None],
|
||||
["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"],
|
||||
["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"],
|
||||
[
|
||||
"Completed",
|
||||
|
||||
@@ -505,7 +505,7 @@ class SubcontractingController(StockController):
|
||||
if item.get("serial_and_batch_bundle"):
|
||||
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
|
||||
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||
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 (
|
||||
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")
|
||||
):
|
||||
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
|
||||
|
||||
@@ -548,12 +548,14 @@
|
||||
{
|
||||
"fieldname": "process_loss_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "% Process Loss"
|
||||
"label": "% Process Loss",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Process Loss Qty",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -639,7 +641,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-29 17:43:12.966753",
|
||||
"modified": "2025-11-19 16:17:15.925156",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -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"""
|
||||
rate = 0
|
||||
if not self.rm_cost_as_per:
|
||||
@@ -491,7 +491,7 @@ class BOM(WebsiteGenerator):
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
elif notify:
|
||||
frappe.msgprint(
|
||||
_("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]),
|
||||
alert=True,
|
||||
@@ -796,11 +796,14 @@ class BOM(WebsiteGenerator):
|
||||
"stock_uom": d.stock_uom,
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"sourced_by_supplier": d.sourced_by_supplier,
|
||||
}
|
||||
},
|
||||
notify=False,
|
||||
)
|
||||
|
||||
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.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt(
|
||||
self.quantity, self.precision("quantity")
|
||||
@@ -823,7 +826,10 @@ class BOM(WebsiteGenerator):
|
||||
d.base_rate = flt(d.rate, d.precision("rate")) * flt(
|
||||
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(
|
||||
self.conversion_rate, self.precision("conversion_rate")
|
||||
)
|
||||
|
||||
@@ -38,6 +38,15 @@ frappe.ui.form.on("Job Card", {
|
||||
return doc.status === "Complete" ? "green" : "orange";
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query("employee", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
status: "Active",
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2024-10-21 14:13:38.777556",
|
||||
"modified": "2025-11-24 11:11:28.343568",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing",
|
||||
@@ -336,7 +336,7 @@
|
||||
"doc_view": "List",
|
||||
"label": "Learn Manufacturing",
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
|
||||
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter
|
||||
|
||||
@@ -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()
|
||||
@@ -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) {
|
||||
|
||||
@@ -79,18 +79,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)) {
|
||||
|
||||
@@ -55,17 +55,20 @@ frappe.ui.form.on("Customer", {
|
||||
|
||||
frm.set_query("customer_primary_contact", function (doc) {
|
||||
return {
|
||||
query: "erpnext.selling.doctype.customer.customer.get_customer_primary_contact",
|
||||
query: "erpnext.selling.doctype.customer.customer.get_customer_primary",
|
||||
filters: {
|
||||
customer: doc.name,
|
||||
type: "Contact",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("customer_primary_address", function (doc) {
|
||||
return {
|
||||
query: "erpnext.selling.doctype.customer.customer.get_customer_primary",
|
||||
filters: {
|
||||
link_doctype: "Customer",
|
||||
link_name: doc.name,
|
||||
customer: doc.name,
|
||||
type: "Address",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -610,7 +610,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2025-03-05 10:01:47.885574",
|
||||
"modified": "2025-11-25 09:35:56.772949",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
@@ -696,4 +696,4 @@
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ class Customer(TransactionBase):
|
||||
self.update_lead_status()
|
||||
|
||||
if self.flags.is_new_doc:
|
||||
self.link_lead_address_and_contact()
|
||||
self.link_address_and_contact()
|
||||
self.copy_communication()
|
||||
|
||||
self.update_customer_groups()
|
||||
@@ -272,15 +272,23 @@ class Customer(TransactionBase):
|
||||
if self.lead_name:
|
||||
frappe.db.set_value("Lead", self.lead_name, "status", "Converted")
|
||||
|
||||
def link_lead_address_and_contact(self):
|
||||
if self.lead_name:
|
||||
# assign lead address and contact to customer (if already not set)
|
||||
def link_address_and_contact(self):
|
||||
linked_documents = {
|
||||
"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(
|
||||
"Dynamic Link",
|
||||
filters=[
|
||||
["parenttype", "in", ["Contact", "Address"]],
|
||||
["link_doctype", "=", "Lead"],
|
||||
["link_name", "=", self.lead_name],
|
||||
["link_doctype", "=", doctype],
|
||||
["link_name", "=", docname],
|
||||
],
|
||||
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.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")
|
||||
|
||||
con = qb.DocType("Contact")
|
||||
type = filters.get("type")
|
||||
type_doctype = qb.DocType(type)
|
||||
dlink = qb.DocType("Dynamic Link")
|
||||
|
||||
return (
|
||||
qb.from_(con)
|
||||
query = (
|
||||
qb.from_(type_doctype)
|
||||
.join(dlink)
|
||||
.on(con.name == dlink.parent)
|
||||
.select(con.name, con.email_id)
|
||||
.where((dlink.link_name == customer) & (con.name.like(f"%{txt}%")))
|
||||
.run()
|
||||
.on(type_doctype.name == dlink.parent)
|
||||
.select(type_doctype.name)
|
||||
.where(
|
||||
(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]:
|
||||
"""Parse full name into first name, middle name and last name"""
|
||||
|
||||
@@ -574,6 +574,9 @@ frappe.ui.form.on("Sales Order Item", {
|
||||
});
|
||||
|
||||
erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController {
|
||||
setup() {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
}
|
||||
onload(doc, dt, dn) {
|
||||
super.onload(doc, dt, dn);
|
||||
}
|
||||
|
||||
@@ -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 test_exchange_values.get(kwargs["params"]["date"]):
|
||||
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"):
|
||||
date = args[0].replace("https://api.frankfurter.app/", "")
|
||||
date = args[0].replace("https://api.frankfurter.dev/v1/", "")
|
||||
if test_exchange_values.get(date):
|
||||
return PatchResponse(
|
||||
{"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)
|
||||
|
||||
settings = frappe.get_single("Currency Exchange Settings")
|
||||
settings.service_provider = "frankfurter.app"
|
||||
settings.service_provider = "frankfurter.dev"
|
||||
settings.save()
|
||||
|
||||
def test_exchange_rate_strict(self, mock_get):
|
||||
|
||||
@@ -93,7 +93,7 @@ def setup_currency_exchange():
|
||||
ces.set("result_key", [])
|
||||
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": "{to_currency}"})
|
||||
ces.append("req_params", {"key": "base", "value": "{from_currency}"})
|
||||
|
||||
@@ -140,6 +140,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
|
||||
erpnext.selling.SellingController
|
||||
) {
|
||||
setup(doc) {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
this.frm.make_methods = {
|
||||
|
||||
@@ -332,6 +332,9 @@ frappe.ui.form.on("Material Request", {
|
||||
label: __("For Warehouse"),
|
||||
options: "Warehouse",
|
||||
reqd: 1,
|
||||
get_query: function () {
|
||||
return { filters: { company: frm.doc.company } };
|
||||
},
|
||||
},
|
||||
{ fieldname: "qty", fieldtype: "Float", label: __("Quantity"), reqd: 1, default: 1 },
|
||||
{
|
||||
|
||||
@@ -108,7 +108,12 @@ def get_indexed_packed_items_table(doc):
|
||||
"""
|
||||
indexed_table = {}
|
||||
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
|
||||
|
||||
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, {}
|
||||
|
||||
# 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):
|
||||
pi_row, exists = packed_items_table.get(key), True
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from itertools import groupby
|
||||
|
||||
import frappe
|
||||
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.custom import GROUP_CONCAT
|
||||
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
|
||||
@@ -646,8 +646,8 @@ class PickList(TransactionBase):
|
||||
product_bundles = self._get_product_bundles()
|
||||
product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values())
|
||||
|
||||
for so_row, item_code in product_bundles.items():
|
||||
picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code])
|
||||
for so_row, value in product_bundles.items():
|
||||
picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[value.item_code])
|
||||
item_table = "Sales Order Item"
|
||||
already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True)
|
||||
frappe.db.set_value(
|
||||
@@ -770,19 +770,23 @@ class PickList(TransactionBase):
|
||||
if not item.product_bundle_item:
|
||||
continue
|
||||
|
||||
product_bundles[item.sales_order_item] = frappe.db.get_value(
|
||||
"Sales Order Item",
|
||||
item.sales_order_item,
|
||||
"item_code",
|
||||
product_bundles[item.sales_order_item] = frappe._dict(
|
||||
{
|
||||
"item_code": frappe.db.get_value(
|
||||
"Sales Order Item",
|
||||
item.sales_order_item,
|
||||
"item_code",
|
||||
),
|
||||
"pick_list_item": item.name,
|
||||
}
|
||||
)
|
||||
return product_bundles
|
||||
|
||||
def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]:
|
||||
# bundle_item_code: Dict[component, qty]
|
||||
def _get_product_bundle_qty_map(self, bundles) -> dict[str, dict[str, float]]:
|
||||
product_bundle_qty_map = {}
|
||||
for bundle_item_code in bundles:
|
||||
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0})
|
||||
product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
|
||||
for data in bundles:
|
||||
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": data.item_code, "disabled": 0})
|
||||
product_bundle_qty_map[data.item_code] = {item.item_code: item.qty for item in bundle.items}
|
||||
return product_bundle_qty_map
|
||||
|
||||
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_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)
|
||||
if sales_order and sales_order_item.parent != sales_order:
|
||||
continue
|
||||
|
||||
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
|
||||
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
|
||||
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
|
||||
|
||||
|
||||
@@ -195,6 +195,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
|
||||
erpnext.buying.BuyingController
|
||||
) {
|
||||
setup(doc) {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
}
|
||||
|
||||
@@ -893,7 +893,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "status",
|
||||
"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_width": "150px",
|
||||
"read_only": 1,
|
||||
@@ -1300,7 +1300,7 @@
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-06 16:41:02.690658",
|
||||
"modified": "2025-11-12 19:53:48.173096",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
||||
@@ -117,7 +117,15 @@ class PurchaseReceipt(BuyingController):
|
||||
shipping_address_display: DF.SmallText | None
|
||||
shipping_rule: DF.Link | None
|
||||
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
|
||||
supplied_items: DF.Table[PurchaseReceiptItemSupplied]
|
||||
|
||||
@@ -18,6 +18,9 @@ def get_data():
|
||||
"Purchase Order": ["items", "purchase_order"],
|
||||
"Project": ["items", "project"],
|
||||
},
|
||||
"internal_and_external_links": {
|
||||
"Purchase Invoice": ["items", "purchase_invoice"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Related"),
|
||||
|
||||
@@ -11,7 +11,7 @@ frappe.listview_settings["Purchase Receipt"] = {
|
||||
"currency",
|
||||
],
|
||||
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"];
|
||||
} else if (doc.status === "Closed") {
|
||||
return [__("Closed"), "green", "status,=,Closed"];
|
||||
|
||||
@@ -455,6 +455,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
# Check if Original PR updated
|
||||
self.assertEqual(pr.items[0].returned_qty, 2)
|
||||
self.assertEqual(pr.per_returned, 40)
|
||||
self.assertEqual(returned.status, "Return")
|
||||
|
||||
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.submit()
|
||||
|
||||
self.assertEqual(return_pr.status, "To Bill")
|
||||
self.assertEqual(return_pr.status, "Return")
|
||||
|
||||
pi = make_purchase_invoice(return_pr.name)
|
||||
pi.submit()
|
||||
|
||||
@@ -1347,7 +1347,36 @@ class SerialandBatchBundle(Document):
|
||||
if self.voucher_type == "POS Invoice":
|
||||
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)}
|
||||
is in submitted state, please cancel it first"""
|
||||
frappe.throw(_(msg))
|
||||
|
||||
@@ -951,12 +951,10 @@ frappe.ui.form.on("Stock Entry Detail", {
|
||||
no_batch_serial_number_value = true;
|
||||
}
|
||||
|
||||
if (
|
||||
no_batch_serial_number_value &&
|
||||
!frappe.flags.hide_serial_batch_dialog &&
|
||||
!frappe.flags.dialog_set
|
||||
) {
|
||||
frappe.flags.dialog_set = true;
|
||||
if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog) {
|
||||
if (!frappe.flags.dialog_set) {
|
||||
frappe.flags.dialog_set = true;
|
||||
}
|
||||
erpnext.stock.select_batch_and_serial_no(frm, d);
|
||||
} else {
|
||||
frappe.flags.dialog_set = false;
|
||||
|
||||
@@ -1136,7 +1136,9 @@ class StockEntry(StockController):
|
||||
"qty": row.transfer_qty * -1,
|
||||
}
|
||||
).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(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
|
||||
@@ -979,6 +979,7 @@ class StockReconciliation(StockController):
|
||||
is_customer_item = frappe.db.get_value("Item", d.item_code, "is_customer_provided_item")
|
||||
if is_customer_item and d.valuation_rate:
|
||||
d.valuation_rate = 0.0
|
||||
d.allow_zero_valuation_rate = 1
|
||||
changed_any_values = True
|
||||
|
||||
if changed_any_values:
|
||||
|
||||
@@ -185,12 +185,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "allow_zero_valuation_rate",
|
||||
"fieldname": "allow_zero_valuation_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Zero Valuation Rate",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "barcode",
|
||||
@@ -256,7 +254,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-12 16:34:51.326821",
|
||||
"modified": "2025-11-20 15:27:13.868179",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reconciliation Item",
|
||||
@@ -267,4 +265,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,9 @@ def get_child_warehouses(warehouse):
|
||||
|
||||
def get_warehouses_based_on_account(account, company=None):
|
||||
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:
|
||||
warehouses.extend(get_child_warehouses(d.name))
|
||||
else:
|
||||
|
||||
@@ -259,7 +259,7 @@ class SerialBatchBundle:
|
||||
and not self.sle.serial_and_batch_bundle
|
||||
and self.item_details.has_batch_no == 1
|
||||
and (
|
||||
self.item_details.create_new_batch
|
||||
(self.item_details.create_new_batch and self.sle.actual_qty > 0)
|
||||
or (
|
||||
frappe.db.get_single_value(
|
||||
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
|
||||
|
||||
@@ -187,7 +187,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
for item in self.get("items"):
|
||||
bom = frappe.get_doc("BOM", item.bom)
|
||||
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):
|
||||
total_qty = total = 0
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.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
|
||||
|
||||
|
||||
class BOMQuantityError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class SubcontractingReceipt(SubcontractingController):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -156,6 +162,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
def on_submit(self):
|
||||
self.validate_closed_subcontracting_order()
|
||||
self.validate_available_qty_for_consumption()
|
||||
self.validate_bom_required_qty()
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
self.set_subcontracting_order_status(update_bin=False)
|
||||
@@ -512,12 +519,60 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
item.available_qty_for_consumption
|
||||
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)}
|
||||
must be less than or equal to Available Qty For Consumption
|
||||
{flt(item.available_qty_for_consumption, precision)}
|
||||
in Consumed Items Table."""
|
||||
msg = _(
|
||||
"""Row {0}: Consumed Qty {1} {2} must be less than or equal to Available Qty For Consumption
|
||||
{3} {4} in Consumed Items Table."""
|
||||
).format(
|
||||
item.idx,
|
||||
flt(item.consumed_qty, precision),
|
||||
item.stock_uom,
|
||||
flt(item.available_qty_for_consumption, precision),
|
||||
item.stock_uom,
|
||||
)
|
||||
|
||||
frappe.throw(_(msg))
|
||||
frappe.throw(msg)
|
||||
|
||||
def validate_bom_required_qty(self):
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||
== "Material Transferred for Subcontract"
|
||||
) and not (frappe.db.get_single_value("Buying Settings", "validate_consumed_qty")):
|
||||
return
|
||||
|
||||
rm_consumed_dict = self.get_rm_wise_consumed_qty()
|
||||
|
||||
for row in self.items:
|
||||
precision = row.precision("qty")
|
||||
for bom_item in self._get_materials_from_bom(
|
||||
row.item_code, row.bom, row.get("include_exploded_items")
|
||||
):
|
||||
required_qty = flt(
|
||||
bom_item.qty_consumed_per_unit * row.qty * row.conversion_factor, precision
|
||||
)
|
||||
consumed_qty = rm_consumed_dict.get(bom_item.rm_item_code, 0)
|
||||
diff = flt(consumed_qty, precision) - flt(required_qty, precision)
|
||||
|
||||
if diff < 0:
|
||||
msg = _(
|
||||
"""Additional {0} {1} of item {2} required as per BOM to complete this transaction"""
|
||||
).format(
|
||||
frappe.bold(abs(diff)),
|
||||
frappe.bold(bom_item.stock_uom),
|
||||
frappe.bold(bom_item.rm_item_code),
|
||||
)
|
||||
|
||||
frappe.throw(
|
||||
msg,
|
||||
exc=BOMQuantityError,
|
||||
)
|
||||
|
||||
def get_rm_wise_consumed_qty(self):
|
||||
rm_dict = defaultdict(float)
|
||||
|
||||
for row in self.supplied_items:
|
||||
rm_dict[row.rm_item_code] += row.consumed_qty
|
||||
|
||||
return rm_dict
|
||||
|
||||
def update_status_updater_args(self):
|
||||
if cint(self.is_return):
|
||||
|
||||
@@ -38,6 +38,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
make_subcontracting_receipt,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
|
||||
BOMQuantityError,
|
||||
)
|
||||
|
||||
|
||||
class TestSubcontractingReceipt(FrappeTestCase):
|
||||
@@ -174,7 +177,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
def test_subcontracting_over_receipt(self):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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].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):
|
||||
args = frappe._dict(args)
|
||||
|
||||
Reference in New Issue
Block a user