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

chore: release v15
This commit is contained in:
ruthra kumar
2024-09-05 17:13:12 +05:30
committed by GitHub
28 changed files with 450 additions and 78 deletions

View File

@@ -2,6 +2,7 @@ import functools
import inspect import inspect
import frappe import frappe
from frappe.utils.user import is_website_user
__version__ = "15.34.0" __version__ = "15.34.0"
@@ -149,3 +150,13 @@ def allow_regional(fn):
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs) return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
return caller return caller
def check_app_permission():
if frappe.session.user == "Administrator":
return True
if is_website_user():
return False
return True

View File

@@ -38,6 +38,11 @@ frappe.ui.form.on("Bank Clearance", {
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries")); frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary"); frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
if (frm.doc.payment_entries.length) {
frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date"));
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
}
}, },
update_clearance_date: function (frm) { update_clearance_date: function (frm) {
@@ -45,13 +50,7 @@ frappe.ui.form.on("Bank Clearance", {
method: "update_clearance_date", method: "update_clearance_date",
doc: frm.doc, doc: frm.doc,
callback: function (r, rt) { callback: function (r, rt) {
frm.refresh_field("payment_entries"); frm.refresh();
frm.refresh_fields();
if (!frm.doc.payment_entries.length) {
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
frm.change_custom_button_type(__("Update Clearance Date"), null, "default");
}
}, },
}); });
}, },
@@ -60,17 +59,8 @@ frappe.ui.form.on("Bank Clearance", {
return frappe.call({ return frappe.call({
method: "get_payment_entries", method: "get_payment_entries",
doc: frm.doc, doc: frm.doc,
callback: function (r, rt) { callback: function () {
frm.refresh_field("payment_entries"); frm.refresh();
if (frm.doc.payment_entries.length) {
frm.add_custom_button(__("Update Clearance Date"), () =>
frm.trigger("update_clearance_date")
);
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
}
}, },
}); });
}, },

View File

@@ -305,7 +305,7 @@ frappe.ui.form.on("Payment Entry", {
set_dynamic_labels: function (frm) { set_dynamic_labels: function (frm) {
var company_currency = frm.doc.company var company_currency = frm.doc.company
? frappe.get_doc(":Company", frm.doc.company).default_currency ? frappe.get_doc(":Company", frm.doc.company)?.default_currency
: ""; : "";
frm.set_currency_labels( frm.set_currency_labels(
@@ -658,7 +658,7 @@ frappe.ui.form.on("Payment Entry", {
frm.set_value("source_exchange_rate", 1); frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from) { } else if (frm.doc.paid_from) {
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) { if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
frappe.call({ frappe.call({
method: "erpnext.setup.utils.get_exchange_rate", method: "erpnext.setup.utils.get_exchange_rate",
args: { args: {

View File

@@ -9,6 +9,7 @@
"transaction_date", "transaction_date",
"column_break_2", "column_break_2",
"naming_series", "naming_series",
"company",
"mode_of_payment", "mode_of_payment",
"party_details", "party_details",
"party_type", "party_type",
@@ -390,13 +391,20 @@
"options": "Payment Request", "options": "Payment Request",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-06-20 13:54:55.245774", "modified": "2024-08-07 16:39:54.288002",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Request", "name": "Payment Request",

View File

@@ -84,6 +84,7 @@ class PaymentRequest(Document):
subscription_plans: DF.Table[SubscriptionPlanDetail] subscription_plans: DF.Table[SubscriptionPlanDetail]
swift_number: DF.ReadOnly | None swift_number: DF.ReadOnly | None
transaction_date: DF.Date | None transaction_date: DF.Date | None
company: DF.Link | None
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
@@ -491,6 +492,7 @@ def make_payment_request(**args):
"message": gateway_account.get("message") or get_dummy_message(ref_doc), "message": gateway_account.get("message") or get_dummy_message(ref_doc),
"reference_doctype": args.dt, "reference_doctype": args.dt,
"reference_name": args.dn, "reference_name": args.dn,
"company": ref_doc.get("company"),
"party_type": args.get("party_type") or "Customer", "party_type": args.get("party_type") or "Customer",
"party": args.get("party") or ref_doc.get("customer"), "party": args.get("party") or ref_doc.get("customer"),
"bank_account": bank_account, "bank_account": bank_account,

View File

@@ -1262,7 +1262,11 @@ class PurchaseInvoice(BuyingController):
def update_gross_purchase_amount_for_linked_assets(self, item): def update_gross_purchase_amount_for_linked_assets(self, item):
assets = frappe.db.get_all( assets = frappe.db.get_all(
"Asset", "Asset",
filters={"purchase_invoice": self.name, "item_code": item.item_code}, filters={
"purchase_invoice": self.name,
"item_code": item.item_code,
"purchase_invoice_item": ("in", [item.name, ""]),
},
fields=["name", "asset_quantity"], fields=["name", "asset_quantity"],
) )
for asset in assets: for asset in assets:

View File

@@ -46,4 +46,20 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
fieldtype: "Check", fieldtype: "Check",
}, },
], ],
formatter: function (value, row, column, data, default_formatter, filter) {
if (column.fieldname == "payment_entry" && value == "Cheques and Deposits incorrectly cleared") {
column.link_onclick =
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
}
return default_formatter(value, row, column, data);
},
open_utility_report: function () {
frappe.route_options = {
company: frappe.query_report.get_filter_value("company"),
account: frappe.query_report.get_filter_value("account"),
report_date: frappe.query_report.get_filter_value("report_date"),
};
frappe.open_in_new_tab = true;
frappe.set_route("query-report", "Cheques and Deposits Incorrectly cleared");
},
}; };

View File

@@ -670,6 +670,11 @@ frappe.ui.form.on("Asset", {
if (item.asset_location) { if (item.asset_location) {
frm.set_value("location", item.asset_location); frm.set_value("location", item.asset_location);
} }
if (doctype === "Purchase Receipt") {
frm.set_value("purchase_receipt_item", item.name);
} else if (doctype === "Purchase Invoice") {
frm.set_value("purchase_invoice_item", item.name);
}
}); });
}, },

View File

@@ -33,14 +33,16 @@
"dimension_col_break", "dimension_col_break",
"purchase_details_section", "purchase_details_section",
"purchase_receipt", "purchase_receipt",
"purchase_receipt_item",
"purchase_invoice", "purchase_invoice",
"purchase_invoice_item",
"purchase_date",
"available_for_use_date", "available_for_use_date",
"total_asset_cost",
"additional_asset_cost",
"column_break_23", "column_break_23",
"gross_purchase_amount", "gross_purchase_amount",
"asset_quantity", "asset_quantity",
"purchase_date", "additional_asset_cost",
"total_asset_cost",
"section_break_23", "section_break_23",
"calculate_depreciation", "calculate_depreciation",
"column_break_33", "column_break_33",
@@ -536,6 +538,20 @@
"fieldname": "opening_number_of_booked_depreciations", "fieldname": "opening_number_of_booked_depreciations",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Opening Number of Booked Depreciations" "label": "Opening Number of Booked Depreciations"
},
{
"fieldname": "purchase_receipt_item",
"fieldtype": "Link",
"hidden": 1,
"label": "Purchase Receipt Item",
"options": "Purchase Receipt Item"
},
{
"fieldname": "purchase_invoice_item",
"fieldtype": "Link",
"hidden": 1,
"label": "Purchase Invoice Item",
"options": "Purchase Invoice Item"
} }
], ],
"idx": 72, "idx": 72,
@@ -579,7 +595,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2024-08-01 16:39:09.340973", "modified": "2024-08-26 23:28:29.095139",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@@ -94,7 +94,9 @@ class Asset(AccountsController):
purchase_amount: DF.Currency purchase_amount: DF.Currency
purchase_date: DF.Date | None purchase_date: DF.Date | None
purchase_invoice: DF.Link | None purchase_invoice: DF.Link | None
purchase_invoice_item: DF.Link | None
purchase_receipt: DF.Link | None purchase_receipt: DF.Link | None
purchase_receipt_item: DF.Link | None
split_from: DF.Link | None split_from: DF.Link | None
status: DF.Literal[ status: DF.Literal[
"Draft", "Draft",
@@ -691,12 +693,17 @@ class Asset(AccountsController):
return cwip_account return cwip_account
def make_gl_entries(self): def make_gl_entries(self):
if self.check_asset_capitalization_gl_entries():
return
gl_entries = [] gl_entries = []
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate(): if (self.is_composite_asset or (purchase_document and self.purchase_amount)) and getdate(
self.available_for_use_date
) <= getdate():
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
{ {
@@ -733,6 +740,24 @@ class Asset(AccountsController):
make_gl_entries(gl_entries) make_gl_entries(gl_entries)
self.db_set("booked_fixed_asset", 1) self.db_set("booked_fixed_asset", 1)
def check_asset_capitalization_gl_entries(self):
if self.is_composite_asset:
result = frappe.db.get_value(
"Asset Capitalization",
{"target_asset": self.name, "docstatus": 1},
["name", "target_fixed_asset_account"],
)
if result:
asset_capitalization, target_fixed_asset_account = result
# Check GL entries for the retrieved Asset Capitalization and target fixed asset account
return has_gl_entries(
"Asset Capitalization", asset_capitalization, target_fixed_asset_account
)
# return if there are no submitted capitalization for given asset
return True
return False
@frappe.whitelist() @frappe.whitelist()
def get_depreciation_rate(self, args, on_validate=False): def get_depreciation_rate(self, args, on_validate=False):
if isinstance(args, str): if isinstance(args, str):
@@ -779,6 +804,22 @@ class Asset(AccountsController):
return flt((100 * (1 - depreciation_rate)), float_precision) return flt((100 * (1 - depreciation_rate)), float_precision)
def has_gl_entries(doctype, docname, target_account):
gl_entry = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gl_entry)
.select(gl_entry.account)
.where(
(gl_entry.voucher_type == doctype)
& (gl_entry.voucher_no == docname)
& (gl_entry.debit != 0)
& (gl_entry.account == target_account)
)
.run(as_dict=True)
)
return len(gl_entries) > 0
def update_maintenance_status(): def update_maintenance_status():
assets = frappe.get_all( assets = frappe.get_all(
"Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")} "Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}

View File

@@ -317,7 +317,16 @@ class AssetCapitalization(StockController):
if not self.target_is_fixed_asset and not self.get("asset_items"): if not self.target_is_fixed_asset and not self.get("asset_items"):
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization")) frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")): if self.capitalization_method == "Create a new composite asset" and not (
self.get("stock_items") or self.get("asset_items")
):
frappe.throw(
_(
"Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset"
)
)
elif not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
frappe.throw( frappe.throw(
_( _(
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization" "Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
@@ -460,13 +469,24 @@ class AssetCapitalization(StockController):
self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision) self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision)
self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision) self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision)
self.get_gl_entries_for_target_item(gl_entries, target_against, precision) self.get_gl_entries_for_target_item(gl_entries, target_account, target_against, precision)
return gl_entries return gl_entries
def get_target_account(self): def get_target_account(self):
if self.target_is_fixed_asset: if self.target_is_fixed_asset:
return self.target_fixed_asset_account from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
if is_cwip_accounting_enabled(asset_category):
target_account = get_asset_category_account(
"capital_work_in_progress_account",
asset_category=asset_category,
company=self.company,
)
return target_account if target_account else self.target_fixed_asset_account
else:
return self.target_fixed_asset_account
else: else:
return self.warehouse_account[self.target_warehouse]["account"] return self.warehouse_account[self.target_warehouse]["account"]
@@ -554,13 +574,13 @@ class AssetCapitalization(StockController):
) )
) )
def get_gl_entries_for_target_item(self, gl_entries, target_against, precision): def get_gl_entries_for_target_item(self, gl_entries, target_account, target_against, precision):
if self.target_is_fixed_asset: if self.target_is_fixed_asset:
# Capitalization # Capitalization
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.target_fixed_asset_account, "account": target_account,
"against": ", ".join(target_against), "against": ", ".join(target_against),
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": flt(self.total_value, precision), "debit": flt(self.total_value, precision),

View File

@@ -31,6 +31,12 @@ class TestAssetCapitalization(unittest.TestCase):
def test_capitalization_with_perpetual_inventory(self): def test_capitalization_with_perpetual_inventory(self):
company = "_Test Company with perpetual inventory" company = "_Test Company with perpetual inventory"
set_depreciation_settings_in_company(company=company) set_depreciation_settings_in_company(company=company)
name = frappe.db.get_value(
"Asset Category Account",
filters={"parent": "Computers", "company_name": company},
fieldname=["name"],
)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
# Variables # Variables
consumed_asset_value = 100000 consumed_asset_value = 100000
@@ -187,9 +193,10 @@ class TestAssetCapitalization(unittest.TestCase):
# Test General Ledger Entries # Test General Ledger Entries
default_expense_account = frappe.db.get_value("Company", company, "default_expense_account") default_expense_account = frappe.db.get_value("Company", company, "default_expense_account")
expected_gle = { expected_gle = {
"_Test Fixed Asset - _TC": 3000, "_Test Fixed Asset - _TC": -100000.0,
"Expenses Included In Asset Valuation - _TC": -1000, default_expense_account: -2000.0,
default_expense_account: -2000, "CWIP Account - _TC": 103000.0,
"Expenses Included In Asset Valuation - _TC": -1000.0,
} }
actual_gle = get_actual_gle_dict(asset_capitalization.name) actual_gle = get_actual_gle_dict(asset_capitalization.name)
@@ -214,6 +221,12 @@ class TestAssetCapitalization(unittest.TestCase):
def test_capitalization_with_wip_composite_asset(self): def test_capitalization_with_wip_composite_asset(self):
company = "_Test Company with perpetual inventory" company = "_Test Company with perpetual inventory"
set_depreciation_settings_in_company(company=company) set_depreciation_settings_in_company(company=company)
name = frappe.db.get_value(
"Asset Category Account",
filters={"parent": "Computers", "company_name": company},
fieldname=["name"],
)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
stock_rate = 1000 stock_rate = 1000
stock_qty = 2 stock_qty = 2
@@ -424,7 +437,7 @@ class TestAssetCapitalization(unittest.TestCase):
self.assertEqual(target_asset.purchase_amount, total_amount) self.assertEqual(target_asset.purchase_amount, total_amount)
expected_gle = { expected_gle = {
"_Test Fixed Asset - _TC": 1000.0, "CWIP Account - _TC": 1000.0,
"Expenses Included In Asset Valuation - _TC": -1000.0, "Expenses Included In Asset Valuation - _TC": -1000.0,
} }

View File

@@ -822,6 +822,8 @@ class BuyingController(SubcontractingController):
"asset_quantity": asset_quantity, "asset_quantity": asset_quantity,
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
"purchase_receipt_item": row.name if self.doctype == "Purchase Receipt" else None,
"purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None,
} }
) )

View File

@@ -366,7 +366,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
def get_empty_batches(filters, start, page_len, filtered_batches=None, txt=None): def get_empty_batches(filters, start, page_len, filtered_batches=None, txt=None):
query_filter = {"item": filters.get("item_code")} query_filter = {"item": filters.get("item_code"), "disabled": 0}
if txt: if txt:
query_filter["name"] = ("like", f"%{txt}%") query_filter["name"] = ("like", f"%{txt}%")

View File

@@ -16,11 +16,11 @@ add_to_apps_screen = [
"logo": "/assets/erpnext/images/erpnext-logo-blue.png", "logo": "/assets/erpnext/images/erpnext-logo-blue.png",
"title": "ERPNext", "title": "ERPNext",
"route": "/app/home", "route": "/app/home",
# "has_permission": "erpnext.api.permission.has_app_permission" "has_permission": "erpnext.check_app_permission",
} }
] ]
develop_version = "14.x.x-develop" develop_version = "15.x.x-develop"
app_include_js = "erpnext.bundle.js" app_include_js = "erpnext.bundle.js"
app_include_css = "erpnext.bundle.css" app_include_css = "erpnext.bundle.css"

View File

@@ -5,18 +5,17 @@
"document_type": "Document", "document_type": "Document",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"bom_and_work_order_tab",
"raw_materials_consumption_section", "raw_materials_consumption_section",
"material_consumption", "material_consumption",
"get_rm_cost_from_consumption_entry", "get_rm_cost_from_consumption_entry",
"column_break_3", "column_break_3",
"backflush_raw_materials_based_on", "backflush_raw_materials_based_on",
"capacity_planning", "validate_components_quantities_per_bom",
"disable_capacity_planning", "bom_section",
"allow_overtime", "update_bom_costs_automatically",
"allow_production_on_holidays", "column_break_lhyt",
"column_break_5", "manufacture_sub_assembly_in_operation",
"capacity_planning_for_days",
"mins_between_operations",
"section_break_6", "section_break_6",
"default_wip_warehouse", "default_wip_warehouse",
"default_fg_warehouse", "default_fg_warehouse",
@@ -30,8 +29,14 @@
"add_corrective_operation_cost_in_finished_good_valuation", "add_corrective_operation_cost_in_finished_good_valuation",
"column_break_24", "column_break_24",
"job_card_excess_transfer", "job_card_excess_transfer",
"capacity_planning",
"disable_capacity_planning",
"allow_overtime",
"allow_production_on_holidays",
"column_break_5",
"capacity_planning_for_days",
"mins_between_operations",
"other_settings_section", "other_settings_section",
"update_bom_costs_automatically",
"set_op_cost_and_scrape_from_sub_assemblies", "set_op_cost_and_scrape_from_sub_assemblies",
"column_break_23", "column_break_23",
"make_serial_no_batch_from_work_order" "make_serial_no_batch_from_work_order"
@@ -149,7 +154,7 @@
{ {
"fieldname": "raw_materials_consumption_section", "fieldname": "raw_materials_consumption_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Raw Materials Consumption" "label": "Raw Materials Consumption "
}, },
{ {
"fieldname": "column_break_16", "fieldname": "column_break_16",
@@ -183,8 +188,8 @@
}, },
{ {
"fieldname": "job_card_section", "fieldname": "job_card_section",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Job Card" "label": "Job Card and Capacity Planning"
}, },
{ {
"fieldname": "column_break_24", "fieldname": "column_break_24",
@@ -210,13 +215,41 @@
"fieldname": "get_rm_cost_from_consumption_entry", "fieldname": "get_rm_cost_from_consumption_entry",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Get Raw Materials Cost from Consumption Entry" "label": "Get Raw Materials Cost from Consumption Entry"
},
{
"fieldname": "bom_and_work_order_tab",
"fieldtype": "Tab Break",
"label": "BOM and Production"
},
{
"fieldname": "bom_section",
"fieldtype": "Section Break",
"label": "BOM"
},
{
"fieldname": "column_break_lhyt",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled then system will manufacture Sub-assembly against the Job Card (operation).",
"fieldname": "manufacture_sub_assembly_in_operation",
"fieldtype": "Check",
"label": "Manufacture Sub-assembly in Operation"
},
{
"default": "0",
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
"fieldname": "validate_components_quantities_per_bom",
"fieldtype": "Check",
"label": "Validate Components Quantities Per BOM"
} }
], ],
"icon": "icon-wrench", "icon": "icon-wrench",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2024-02-08 19:00:37.561244", "modified": "2024-09-02 12:12:03.132567",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing Settings", "name": "Manufacturing Settings",
@@ -234,4 +267,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -29,15 +29,22 @@ class ManufacturingSettings(Document):
get_rm_cost_from_consumption_entry: DF.Check get_rm_cost_from_consumption_entry: DF.Check
job_card_excess_transfer: DF.Check job_card_excess_transfer: DF.Check
make_serial_no_batch_from_work_order: DF.Check make_serial_no_batch_from_work_order: DF.Check
manufacture_sub_assembly_in_operation: DF.Check
material_consumption: DF.Check material_consumption: DF.Check
mins_between_operations: DF.Int mins_between_operations: DF.Int
overproduction_percentage_for_sales_order: DF.Percent overproduction_percentage_for_sales_order: DF.Percent
overproduction_percentage_for_work_order: DF.Percent overproduction_percentage_for_work_order: DF.Percent
set_op_cost_and_scrape_from_sub_assemblies: DF.Check set_op_cost_and_scrape_from_sub_assemblies: DF.Check
update_bom_costs_automatically: DF.Check update_bom_costs_automatically: DF.Check
validate_components_quantities_per_bom: DF.Check
# end: auto-generated types # end: auto-generated types
pass def before_save(self):
self.reset_values()
def reset_values(self):
if self.backflush_raw_materials_based_on != "BOM" and self.validate_components_quantities_per_bom:
self.validate_components_quantities_per_bom = 0
def get_mins_between_operations(): def get_mins_between_operations():

View File

@@ -2102,6 +2102,59 @@ class TestWorkOrder(FrappeTestCase):
stock_entry.submit() stock_entry.submit()
def test_components_qty_for_bom_based_manufacture_entry(self):
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
fg_item = "Test FG Item For Component Validation"
source_warehouse = "Stores - _TC"
raw_materials = ["Test Component Validation RM Item 1", "Test Component Validation RM Item 2"]
make_item(fg_item, {"is_stock_item": 1})
for item in raw_materials:
make_item(item, {"is_stock_item": 1})
test_stock_entry.make_stock_entry(
item_code=item,
target=source_warehouse,
qty=10,
basic_rate=100,
)
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
wo = make_wo_order_test_record(
item=fg_item,
qty=10,
source_warehouse=source_warehouse,
)
transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
transfer_entry.save()
for row in transfer_entry.items:
row.qty = 5
self.assertRaises(frappe.ValidationError, transfer_entry.save)
transfer_entry.reload()
for row in transfer_entry.items:
self.assertEqual(row.qty, 10)
transfer_entry.submit()
manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
manufacture_entry.save()
for row in manufacture_entry.items:
if not row.s_warehouse:
continue
row.qty = 5
self.assertRaises(frappe.ValidationError, manufacture_entry.save)
manufacture_entry.reload()
manufacture_entry.submit()
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)
def make_operation(**kwargs): def make_operation(**kwargs):
kwargs = frappe._dict(kwargs) kwargs = frappe._dict(kwargs)

View File

@@ -374,3 +374,4 @@ erpnext.patches.v15_0.do_not_use_batchwise_valuation
erpnext.patches.v15_0.drop_index_posting_datetime_from_sle erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1 erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
erpnext.patches.v15_0.set_standard_stock_entry_type erpnext.patches.v15_0.set_standard_stock_entry_type
erpnext.patches.v15_0.link_purchase_item_to_asset_doc

View File

@@ -0,0 +1,74 @@
import frappe
def execute():
if frappe.db.has_column("Asset", "purchase_invoice_item") and frappe.db.has_column(
"Asset", "purchase_receipt_item"
):
# Get all assets with their related Purchase Invoice and Purchase Receipt
assets = frappe.get_all(
"Asset",
filters={"docstatus": 0},
fields=[
"name",
"item_code",
"purchase_invoice",
"purchase_receipt",
"gross_purchase_amount",
"asset_quantity",
"purchase_invoice_item",
"purchase_receipt_item",
],
)
for asset in assets:
# Get Purchase Invoice Items
if asset.purchase_invoice and not asset.purchase_invoice_item:
purchase_invoice_item = get_linked_item(
"Purchase Invoice Item",
asset.purchase_invoice,
asset.item_code,
asset.gross_purchase_amount,
asset.asset_quantity,
)
frappe.db.set_value("Asset", asset.name, "purchase_invoice_item", purchase_invoice_item)
# Get Purchase Receipt Items
if asset.purchase_receipt and not asset.purchase_receipt_item:
purchase_receipt_item = get_linked_item(
"Purchase Receipt Item",
asset.purchase_receipt,
asset.item_code,
asset.gross_purchase_amount,
asset.asset_quantity,
)
frappe.db.set_value("Asset", asset.name, "purchase_receipt_item", purchase_receipt_item)
def get_linked_item(doctype, parent, item_code, amount, quantity):
items = frappe.get_all(
doctype,
filters={
"parenttype": doctype.replace(" Item", ""),
"parent": parent,
"item_code": item_code,
},
fields=["name", "rate", "amount", "qty", "landed_cost_voucher_amount"],
)
if len(items) == 1:
# If only one item exists, return it directly
return items[0].name
for item in items:
landed_cost = item.get("landed_cost_voucher_amount", 0)
# Check if the asset is grouped
if quantity > 1:
if item.amount + landed_cost == amount and item.qty == quantity:
return item.name
elif item.qty == quantity:
return item.name
else:
if item.rate + (landed_cost / item.qty) == amount:
return item.name
return items[0].name if items else None

View File

@@ -116,14 +116,17 @@ erpnext.financial_statements = {
erpnext.financial_statements.filters = get_filters(); erpnext.financial_statements.filters = get_filters();
let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
var filters = report.get_values();
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { if (!filters.period_start_date || !filters.period_end_date) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
frappe.query_report.set_filter_value({ var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
period_start_date: fy.year_start_date, frappe.query_report.set_filter_value({
period_end_date: fy.year_end_date, period_start_date: fy.year_start_date,
period_end_date: fy.year_end_date,
});
}); });
}); }
if (report.page) { if (report.page) {
const views_menu = report.page.add_custom_button_group(__("Financial Statements")); const views_menu = report.page.add_custom_button_group(__("Financial Statements"));

View File

@@ -368,8 +368,28 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
]; ];
} }
get_batch_qty(batch_no, callback) {
let warehouse = this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse;
frappe.call({
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
args: {
batch_no: batch_no,
warehouse: warehouse,
item_code: this.item.item_code,
posting_date: this.frm.doc.posting_date,
posting_time: this.frm.doc.posting_time,
},
callback: (r) => {
if (r.message) {
callback(flt(r.message));
}
},
});
}
get_dialog_table_fields() { get_dialog_table_fields() {
let fields = []; let fields = [];
let me = this;
if (this.item.has_serial_no) { if (this.item.has_serial_no) {
fields.push({ fields.push({
@@ -395,6 +415,15 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
fieldname: "batch_no", fieldname: "batch_no",
label: __("Batch No"), label: __("Batch No"),
in_list_view: 1, in_list_view: 1,
change() {
let doc = this.doc;
if (!doc.qty && me.item.type_of_transaction === "Outward") {
me.get_batch_qty(doc.batch_no, (qty) => {
doc.qty = qty;
this.grid.set_value("qty", qty, doc);
});
}
},
get_query: () => { get_query: () => {
let is_inward = false; let is_inward = false;
if ( if (

View File

@@ -20,7 +20,6 @@ frappe.ui.form.on("Company", {
}, },
setup: function (frm) { setup: function (frm) {
frm.__rename_queue = "long"; frm.__rename_queue = "long";
erpnext.company.setup_queries(frm);
frm.set_query("parent_company", function () { frm.set_query("parent_company", function () {
return { return {
@@ -81,6 +80,8 @@ frappe.ui.form.on("Company", {
}, },
refresh: function (frm) { refresh: function (frm) {
erpnext.company.setup_queries(frm);
frm.toggle_display("address_html", !frm.is_new()); frm.toggle_display("address_html", !frm.is_new());
if (!frm.is_new()) { if (!frm.is_new()) {

View File

@@ -837,7 +837,11 @@ class PurchaseReceipt(BuyingController):
def update_assets(self, item, valuation_rate): def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all( assets = frappe.db.get_all(
"Asset", "Asset",
filters={"purchase_receipt": self.name, "item_code": item.item_code}, filters={
"purchase_receipt": self.name,
"item_code": item.item_code,
"purchase_receipt_item": ("in", [item.name, ""]),
},
fields=["name", "asset_quantity"], fields=["name", "asset_quantity"],
) )

View File

@@ -232,6 +232,7 @@ class StockEntry(StockController):
self.validate_serialized_batch() self.validate_serialized_batch()
self.calculate_rate_and_amount() self.calculate_rate_and_amount()
self.validate_putaway_capacity() self.validate_putaway_capacity()
self.validate_component_quantities()
if not self.get("purpose") == "Manufacture": if not self.get("purpose") == "Manufacture":
# ignore scrap item wh difference and empty source/target wh # ignore scrap item wh difference and empty source/target wh
@@ -747,6 +748,34 @@ class StockEntry(StockController):
title=_("Insufficient Stock"), title=_("Insufficient Stock"),
) )
def validate_component_quantities(self):
if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]:
return
if not frappe.db.get_single_value("Manufacturing Settings", "validate_components_quantities_per_bom"):
return
if not self.fg_completed_qty:
return
raw_materials = self.get_bom_raw_materials(self.fg_completed_qty)
precision = frappe.get_precision("Stock Entry Detail", "qty")
for row in self.items:
if not row.s_warehouse:
continue
if details := raw_materials.get(row.item_code):
if flt(details.get("qty"), precision) != flt(row.qty, precision):
frappe.throw(
_("For the item {0}, the quantity should be {1} according to the BOM {2}.").format(
frappe.bold(row.item_code),
flt(details.get("qty"), precision),
get_link_to_form("BOM", self.bom_no),
),
title=_("Incorrect Component Quantity"),
)
@frappe.whitelist() @frappe.whitelist()
def get_stock_and_rate(self): def get_stock_and_rate(self):
""" """

View File

@@ -807,14 +807,10 @@ def get_price_list_rate(args, item_doc, out=None):
if price_list_rate is None or frappe.db.get_single_value( if price_list_rate is None or frappe.db.get_single_value(
"Stock Settings", "update_existing_price_list_rate" "Stock Settings", "update_existing_price_list_rate"
): ):
if args.get("is_internal_supplier") or args.get("is_internal_customer"): insert_item_price(args)
return out
if args.price_list and args.rate: if price_list_rate is None:
insert_item_price(args) return out
if not price_list_rate:
return out
out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate)
@@ -835,6 +831,14 @@ def get_price_list_rate(args, item_doc, out=None):
def insert_item_price(args): def insert_item_price(args):
"""Insert Item Price if Price List and Price List Rate are specified and currency is the same""" """Insert Item Price if Price List and Price List Rate are specified and currency is the same"""
if (
not args.price_list
or not args.rate
or args.get("is_internal_supplier")
or args.get("is_internal_customer")
):
return
if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint( if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint(
frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing") frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")
): ):

View File

@@ -354,9 +354,14 @@ def get_email_list(company):
def get_comapny_wise_users(company): def get_comapny_wise_users(company):
companies = [company]
if parent_company := frappe.db.get_value("Company", company, "parent_company"):
companies.append(parent_company)
users = frappe.get_all( users = frappe.get_all(
"User Permission", "User Permission",
filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1}, filters={"allow": "Company", "for_value": ("in", companies), "apply_to_all_doctypes": 1},
fields=["user"], fields=["user"],
) )

View File

@@ -1532,7 +1532,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
operator = "<=" operator = "<="
voucher_condition = f"and creation < '{creation}'" voucher_condition = f"and creation < '{creation}'"
sle = frappe.db.sql( sle = frappe.db.sql( # nosemgrep
f""" f"""
select *, posting_datetime as "timestamp" select *, posting_datetime as "timestamp"
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
@@ -1629,6 +1629,7 @@ def get_stock_ledger_entries(
if extra_cond: if extra_cond:
conditions += f"{extra_cond}" conditions += f"{extra_cond}"
# nosemgrep
return frappe.db.sql( return frappe.db.sql(
""" """
select *, posting_datetime as "timestamp" select *, posting_datetime as "timestamp"
@@ -1744,7 +1745,7 @@ def get_valuation_rate(
return batch_obj.get_incoming_rate() return batch_obj.get_incoming_rate()
# Get valuation rate from last sle for the same item and warehouse # Get valuation rate from last sle for the same item and warehouse
if last_valuation_rate := frappe.db.sql( if last_valuation_rate := frappe.db.sql( # nosemgrep
"""select valuation_rate """select valuation_rate
from `tabStock Ledger Entry` force index (item_warehouse) from `tabStock Ledger Entry` force index (item_warehouse)
where where
@@ -1824,7 +1825,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
detail = next_stock_reco_detail[0] detail = next_stock_reco_detail[0]
datetime_limit_condition = get_datetime_limit_condition(detail) datetime_limit_condition = get_datetime_limit_condition(detail)
frappe.db.sql( frappe.db.sql( # nosemgrep
f""" f"""
update `tabStock Ledger Entry` update `tabStock Ledger Entry`
set qty_after_transaction = qty_after_transaction + {qty_shift} set qty_after_transaction = qty_after_transaction + {qty_shift}
@@ -1990,8 +1991,8 @@ def is_negative_with_precision(neg_sle, is_batch=False):
return qty_deficit < 0 and abs(qty_deficit) > 0.0001 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
def get_future_sle_with_negative_qty(args): def get_future_sle_with_negative_qty(sle_args):
return frappe.db.sql( return frappe.db.sql( # nosemgrep
""" """
select select
qty_after_transaction, posting_date, posting_time, qty_after_transaction, posting_date, posting_time,
@@ -2007,13 +2008,13 @@ def get_future_sle_with_negative_qty(args):
order by posting_date asc, posting_time asc order by posting_date asc, posting_time asc
limit 1 limit 1
""", """,
args, sle_args,
as_dict=1, as_dict=1,
) )
def get_future_sle_with_negative_batch_qty(args): def get_future_sle_with_negative_batch_qty(sle_args):
return frappe.db.sql( return frappe.db.sql( # nosemgrep
""" """
with batch_ledger as ( with batch_ledger as (
select select
@@ -2033,7 +2034,7 @@ def get_future_sle_with_negative_batch_qty(args):
and posting_datetime >= %(posting_datetime)s and posting_datetime >= %(posting_datetime)s
limit 1 limit 1
""", """,
args, sle_args,
as_dict=1, as_dict=1,
) )