diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index 411b90f84ee..ff44723afee 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -22,8 +22,7 @@ frappe.ui.form.on("Account", { // hide fields if group frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0); - // disable fields - frm.toggle_enable(["is_group", "company"], false); + frm.toggle_enable(["is_group", "company", "account_number"], frm.is_new()); if (cint(frm.doc.is_group) == 0) { frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account); diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json index 78f73efff11..0c9232015d9 100644 --- a/erpnext/accounts/doctype/account/account.json +++ b/erpnext/accounts/doctype/account/account.json @@ -55,8 +55,7 @@ "fieldtype": "Data", "in_list_view": 1, "in_standard_filter": 1, - "label": "Account Number", - "read_only": 1 + "label": "Account Number" }, { "default": "0", @@ -72,7 +71,6 @@ "oldfieldname": "company", "oldfieldtype": "Link", "options": "Company", - "read_only": 1, "remember_last_selected_value": 1, "reqd": 1 }, diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json index b6673795bea..d599ea65ea6 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json @@ -1525,7 +1525,8 @@ "41-Clients et comptes rattach\u00e9s (PASSIF)": { "Clients cr\u00e9diteurs": { "Clients - Avances et acomptes re\u00e7us sur commandes": { - "account_number": "4191" + "account_number": "4191", + "account_type": "Income Account" }, "Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": { "account_number": "4196" @@ -3141,4 +3142,4 @@ "account_number": "7" } } -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py index 75f02e659f4..a1271b9c32a 100644 --- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -68,6 +68,9 @@ class AutoMatchbyAccountIBAN: party, or_filters=or_filters, pluck="name", limit_page_length=1 ) + if "bank_ac_no" in or_filters: + or_filters["bank_account_no"] = or_filters.pop("bank_ac_no") + if party_result: result = ( party, diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py index 784ad27dd47..34b2ec68743 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py +++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py @@ -4,7 +4,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_days, format_date, getdate +from frappe.utils import add_days, flt, format_date, getdate class MainCostCenterCantBeChild(frappe.ValidationError): @@ -60,7 +60,7 @@ class CostCenterAllocation(Document): self.validate_child_cost_centers() def validate_total_allocation_percentage(self): - total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])]) + total_percentage = sum([flt(d.percentage) for d in self.get("allocation_percentages", [])]) if total_percentage != 100: frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json index fb997847448..66db37fe13b 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json @@ -82,7 +82,7 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2024-01-30 12:35:38.645968", + "modified": "2024-05-27 17:29:55.560840", "modified_by": "Administrator", "module": "Accounts", "name": "Fiscal Year", @@ -127,6 +127,10 @@ { "read": 1, "role": "Stock Manager" + }, + { + "read": 1, + "role": "Auditor" } ], "show_name_in_global_search": 1, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 402f19ee968..3ec47ff022a 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1031,6 +1031,17 @@ class JournalEntry(AccountsController): def build_gl_map(self): gl_map = [] + + company_currency = erpnext.get_company_currency(self.company) + if self.multi_currency: + for row in self.get("accounts"): + if row.account_currency != company_currency: + self.currency = row.account_currency + self.conversion_rate = row.exchange_rate + break + else: + self.currency = company_currency + for d in self.get("accounts"): if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"): r = [d.user_remark, self.remark] diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index b047898b771..7f9c55ff24f 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -929,6 +929,30 @@ class TestPricingRule(unittest.TestCase): for doc in [si, si1]: doc.delete() + def test_pricing_rule_for_transaction_with_condition(self): + make_item("PR Transaction Condition") + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + make_pricing_rule( + selling=1, + min_qty=0, + price_or_product_discount="Product", + apply_on="Transaction", + free_item="PR Transaction Condition", + free_qty=1, + free_item_rate=10, + condition="customer=='_Test Customer 1'", + ) + + si = create_sales_invoice(qty=5, customer="_Test Customer 1", do_not_submit=True) + self.assertEqual(len(si.items), 2) + self.assertEqual(si.items[1].rate, 10) + + si1 = create_sales_invoice(qty=5, customer="_Test Customer 2", do_not_submit=True) + self.assertEqual(len(si1.items), 1) + + for doc in [si, si1]: + doc.delete() + def test_remove_pricing_rule(self): item = make_item("Water Flask") make_item_price("Water Flask", "_Test Price List", 100) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 733c9bdfd95..60c9e26aabe 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -564,6 +564,7 @@ def apply_pricing_rule_on_transaction(doc): if pricing_rules: pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules) + pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc) if not pricing_rules: remove_free_item(doc) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 49ba9f459f2..afccc21c313 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -448,7 +448,7 @@ class PurchaseInvoice(BuyingController): stock_not_billed_account = self.get_company_default("stock_received_but_not_billed") stock_items = self.get_stock_items() - asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") + self.asset_received_but_not_billed = None if self.update_stock: self.validate_item_code() @@ -534,18 +534,23 @@ class PurchaseInvoice(BuyingController): elif item.is_fixed_asset: account = None if item.pr_detail: + if not self.asset_received_but_not_billed: + self.asset_received_but_not_billed = self.get_company_default( + "asset_received_but_not_billed" + ) + # check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not arbnb_booked_in_pr = frappe.db.get_value( "GL Entry", { "voucher_type": "Purchase Receipt", "voucher_no": item.purchase_receipt, - "account": asset_received_but_not_billed, + "account": self.asset_received_but_not_billed, }, "name", ) if arbnb_booked_in_pr: - account = asset_received_but_not_billed + account = self.asset_received_but_not_billed if not account: account_type = ( diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json index f9344ce4f3a..0d74b2150f7 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json @@ -53,13 +53,15 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-05-23 17:00:42.984798", + "modified": "2024-06-03 17:30:37.012593", "modified_by": "Administrator", "module": "Accounts", "name": "Repost Accounting Ledger", "owner": "Administrator", "permissions": [ { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -68,7 +70,9 @@ "read": 1, "report": 1, "role": "System Manager", + "select": 1, "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json index 4ecff8cac3b..2b4efead7f3 100644 --- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json @@ -98,13 +98,15 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-05-23 17:00:31.540640", + "modified": "2024-06-03 17:31:04.472279", "modified_by": "Administrator", "module": "Accounts", "name": "Repost Payment Ledger", "owner": "Administrator", "permissions": [ { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -113,7 +115,9 @@ "read": 1, "report": 1, "role": "System Manager", + "select": 1, "share": 1, + "submit": 1, "write": 1 }, { diff --git a/erpnext/accounts/report/account_balance/account_balance.py b/erpnext/accounts/report/account_balance/account_balance.py index 6f9f7ebcb8d..f385c71a9f4 100644 --- a/erpnext/accounts/report/account_balance/account_balance.py +++ b/erpnext/accounts/report/account_balance/account_balance.py @@ -49,7 +49,6 @@ def get_conditions(filters): if filters.account_type: conditions["account_type"] = filters.account_type - return conditions if filters.company: conditions["company"] = filters.company diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index d285f28d8e3..e1545bdcd87 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder import DocType from frappe.utils import cstr, flt @@ -75,11 +76,24 @@ def get_data(filters): asset_data = assets_details.get(d.against_voucher) if asset_data: if not asset_data.get("accumulated_depreciation_amount"): - asset_data.accumulated_depreciation_amount = d.debit + asset_data.get( - "opening_accumulated_depreciation" - ) + AssetDepreciationSchedule = DocType("Asset Depreciation Schedule") + DepreciationSchedule = DocType("Depreciation Schedule") + query = ( + frappe.qb.from_(DepreciationSchedule) + .join(AssetDepreciationSchedule) + .on(DepreciationSchedule.parent == AssetDepreciationSchedule.name) + .select(DepreciationSchedule.accumulated_depreciation_amount) + .where( + (AssetDepreciationSchedule.asset == d.against_voucher) + & (DepreciationSchedule.parenttype == "Asset Depreciation Schedule") + & (DepreciationSchedule.schedule_date == d.posting_date) + ) + ).run(as_dict=True) + asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"] + else: asset_data.accumulated_depreciation_amount += d.debit + asset_data.opening_accumulated_depreciation = asset_data.accumulated_depreciation_amount - d.debit row = frappe._dict(asset_data) row.update( diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 4edfcd11c89..063a5447ab5 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1140,6 +1140,8 @@ def create_new_asset_after_split(asset, split_qty): for row in new_asset.get("finance_books"): current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book) + if not current_asset_depr_schedule_doc: + continue new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 4ff04c80434..5d4ef4e3845 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -159,8 +159,9 @@ def prepare_chart_data(data, filters): if filters.filter_based_on not in ("Date Range", "Fiscal Year"): filters_filter_based_on = "Date Range" date_field = "purchase_date" - filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field) - filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field) + filtered_data = [d for d in data if d.get(date_field)] + filters_from_date = min(filtered_data, key=lambda a: a.get(date_field)).get(date_field) + filters_to_date = max(filtered_data, key=lambda a: a.get(date_field)).get(date_field) else: filters_filter_based_on = filters.filter_based_on date_field = frappe.scrub(filters.date_based_on) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5351ff6d791..d1f19841ac5 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -902,12 +902,12 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): ) target_doc.populate_items_table() + source_doc = frappe.get_doc("Purchase Order", source_name) if target_doc.set_warehouse: for item in target_doc.items: item.warehouse = target_doc.set_warehouse else: - source_doc = frappe.get_doc("Purchase Order", source_name) if source_doc.set_warehouse: for item in target_doc.items: item.warehouse = source_doc.set_warehouse diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index aeaebafabe9..db9d31b53bb 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -118,6 +118,8 @@ class Opportunity(TransactionBase, CRMNote): self.title = self.customer_name self.calculate_totals() + + def on_update(self): self.update_prospect() def map_fields(self): diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 4ce8342af6e..58ce7c50583 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -591,13 +591,13 @@ "default": "0", "fieldname": "fg_based_operating_cost", "fieldtype": "Check", - "label": "FG based Operating Cost" + "label": "Finished Goods based Operating Cost" }, { "depends_on": "fg_based_operating_cost", "fieldname": "fg_based_section_section", "fieldtype": "Section Break", - "label": "FG Based Operating Cost Section" + "label": "Finished Goods Based Operating Cost" }, { "depends_on": "fg_based_operating_cost", @@ -637,7 +637,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2024-04-02 16:22:47.518411", + "modified": "2024-06-03 16:24:47.518411", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json index 56acd8a1a67..1726f898751 100644 --- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -70,7 +70,7 @@ "fieldname": "fg_item", "fieldtype": "Link", "in_list_view": 1, - "label": "FG Item", + "label": "Finished Goods Item", "options": "Item", "reqd": 1 }, @@ -203,7 +203,7 @@ { "fieldname": "fg_reference_id", "fieldtype": "Data", - "label": "FG Reference", + "label": "Finished Goods Reference", "no_copy": 1, "read_only": 1 }, @@ -230,7 +230,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-16 13:34:06.321061", + "modified": "2024-06-03 18:45:24.339532", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator Item", diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json index 78a389760a7..089d7396b60 100644 --- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json +++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -85,7 +85,7 @@ "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, - "label": "FG Warehouse", + "label": "Finished Goods Warehouse", "options": "Warehouse" }, { @@ -220,7 +220,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-02-27 13:24:43.571844", + "modified": "2024-06-03 13:10:20.252166", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Item", diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py index 51efc6e655f..73560dd939b 100644 --- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py +++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py @@ -102,7 +102,12 @@ def get_columns() -> Columns: "fieldtype": "Float", "width": "150", }, - {"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"}, + { + "label": _("Finished Goods Value"), + "fieldname": "total_fg_value", + "fieldtype": "Float", + "width": "150", + }, { "label": _("Raw Material Value"), "fieldname": "total_rm_value", diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 3c3d90c6d4f..4928f2dc1a5 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -613,7 +613,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { } render_data() { - if (this.bundle) { + if (this.bundle || this.frm.doc.is_return) { frappe .call({ method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers", @@ -621,6 +621,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { item_code: this.item.item_code, name: this.bundle, voucher_no: !this.frm.is_new() ? this.item.parent : "", + child_row: this.frm.doc.is_return ? this.item : "", }, }) .then((r) => { diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index d95bf9811a2..d903f7eedf5 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -867,6 +867,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex fields: fields, primary_action: function () { var data = { items: d.fields_dict.items.grid.get_selected_children() }; + if (!data) { + frappe.throw(__("Please select items")); + } me.frm.call({ method: "make_work_orders", args: { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index bc8991486db..d6c08d957fe 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1224,11 +1224,19 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t target.discount_amount = 0.0 target.inter_company_order_reference = "" target.shipping_rule = "" + target.tc_name = "" + target.terms = "" + target.payment_terms_template = "" + target.payment_schedule = [] default_price_list = frappe.get_value("Supplier", supplier, "default_price_list") if default_price_list: target.buying_price_list = default_price_list + default_payment_terms = frappe.get_value("Supplier", supplier, "payment_terms") + if default_payment_terms: + target.payment_terms_template = default_payment_terms + if any(item.delivered_by_supplier == 1 for item in source.items): if source.shipping_address_name: target.shipping_address = source.shipping_address_name @@ -1280,7 +1288,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "contact_person", "taxes_and_charges", "shipping_address", - "terms", ], "validation": {"docstatus": ["=", 1]}, }, @@ -1348,6 +1355,10 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.discount_amount = 0.0 target.inter_company_order_reference = "" target.shipping_rule = "" + target.tc_name = "" + target.terms = "" + target.payment_terms_template = "" + target.payment_schedule = [] if is_drop_ship_order(target): target.customer = source.customer @@ -1383,7 +1394,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "contact_person", "taxes_and_charges", "shipping_address", - "terms", ], "validation": {"docstatus": ["=", 1]}, }, diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 670d898b8df..4712a10cc0a 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -12,10 +12,11 @@ frappe.ui.form.on("Company", { } }); } - - frm.call("check_if_transactions_exist").then((r) => { - frm.toggle_enable("default_currency", !r.message); - }); + if (!frm.doc.__islocal) { + frm.call("check_if_transactions_exist").then((r) => { + frm.toggle_enable("default_currency", !r.message); + }); + } }, setup: function (frm) { frm.__rename_queue = "long"; @@ -251,7 +252,10 @@ erpnext.company.setup_queries = function (frm) { ["discount_allowed_account", { root_type: "Expense" }], ["discount_received_account", { root_type: "Income" }], ["exchange_gain_loss_account", { root_type: ["in", ["Expense", "Income"]] }], - ["unrealized_exchange_gain_loss_account", { root_type: ["in", ["Expense", "Income"]] }], + [ + "unrealized_exchange_gain_loss_account", + { root_type: ["in", ["Expense", "Income", "Equity", "Liability"]] }, + ], [ "accumulated_depreciation_account", { root_type: "Asset", account_type: "Accumulated Depreciation" }, diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 674805980f5..b2d6af03a10 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -795,7 +795,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2024-05-16 12:39:54.694232", + "modified": "2024-05-27 17:32:49.057386", "modified_by": "Administrator", "module": "Setup", "name": "Company", @@ -851,6 +851,10 @@ "role": "Accounts Manager", "share": 1, "write": 1 + }, + { + "role": "Auditor", + "select": 1 } ], "show_name_in_global_search": 1, diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 65fe853ec8d..116a0bd833d 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -167,6 +167,7 @@ class TestPickList(FrappeTestCase): "item_code": "_Test Serialized Item", "warehouse": "_Test Warehouse - _TC", "valuation_rate": 100, + "reconcile_all_serial_batch": 1, "qty": 5, "serial_and_batch_bundle": make_serial_batch_bundle( frappe._dict( diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index ad757351a9b..87bf2df5f61 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1209,31 +1209,44 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals @frappe.whitelist() -def get_serial_batch_ledgers(item_code=None, docstatus=None, voucher_no=None, name=None): +def get_serial_batch_ledgers(item_code=None, docstatus=None, voucher_no=None, name=None, child_row=None): filters = get_filters_for_bundle( - item_code=item_code, docstatus=docstatus, voucher_no=voucher_no, name=name + item_code=item_code, docstatus=docstatus, voucher_no=voucher_no, name=name, child_row=child_row ) + fields = [ + "`tabSerial and Batch Bundle`.`item_code`", + "`tabSerial and Batch Entry`.`qty`", + "`tabSerial and Batch Entry`.`warehouse`", + "`tabSerial and Batch Entry`.`batch_no`", + "`tabSerial and Batch Entry`.`serial_no`", + ] + + if not child_row: + fields.append("`tabSerial and Batch Bundle`.`name`") + return frappe.get_all( "Serial and Batch Bundle", - fields=[ - "`tabSerial and Batch Bundle`.`name`", - "`tabSerial and Batch Bundle`.`item_code`", - "`tabSerial and Batch Entry`.`qty`", - "`tabSerial and Batch Entry`.`warehouse`", - "`tabSerial and Batch Entry`.`batch_no`", - "`tabSerial and Batch Entry`.`serial_no`", - ], + fields=fields, filters=filters, order_by="`tabSerial and Batch Entry`.`idx`", ) -def get_filters_for_bundle(item_code=None, docstatus=None, voucher_no=None, name=None): +def get_filters_for_bundle(item_code=None, docstatus=None, voucher_no=None, name=None, child_row=None): filters = [ ["Serial and Batch Bundle", "is_cancelled", "=", 0], ] + if child_row and isinstance(child_row, str): + child_row = parse_json(child_row) + + if not name and child_row and child_row.get("qty") < 0: + bundle = get_reference_serial_and_batch_bundle(child_row) + if bundle: + voucher_no = None + filters.append(["Serial and Batch Bundle", "name", "=", bundle]) + if item_code: filters.append(["Serial and Batch Bundle", "item_code", "=", item_code]) @@ -1257,6 +1270,19 @@ def get_filters_for_bundle(item_code=None, docstatus=None, voucher_no=None, name return filters +def get_reference_serial_and_batch_bundle(child_row): + field = { + "Sales Invoice Item": "sales_invoice_item", + "Delivery Note Item": "dn_detail", + "Purchase Receipt Item": "purchase_receipt_item", + "Purchase Invoice Item": "purchase_invoice_item", + "POS Invoice Item": "pos_invoice_item", + }.get(child_row.doctype) + + if field: + return frappe.get_cached_value(child_row.doctype, child_row.get(field), "serial_and_batch_bundle") + + @frappe.whitelist() def add_serial_batch_ledgers(entries, child_row, doc, warehouse, do_not_save=False) -> object: if isinstance(child_row, str): @@ -1334,9 +1360,6 @@ def get_type_of_transaction(parent_doc, child_row): if parent_doc.get("doctype") in ["Purchase Receipt", "Purchase Invoice"]: type_of_transaction = "Inward" - if parent_doc.get("is_return"): - type_of_transaction = "Inward" if type_of_transaction == "Outward" else "Outward" - if parent_doc.get("doctype") == "Subcontracting Receipt": type_of_transaction = "Outward" if child_row.get("doctype") == "Subcontracting Receipt Item": @@ -1344,6 +1367,14 @@ def get_type_of_transaction(parent_doc, child_row): elif parent_doc.get("doctype") == "Stock Reconciliation": type_of_transaction = "Inward" + if parent_doc.get("is_return"): + type_of_transaction = "Inward" + if ( + parent_doc.get("doctype") in ["Purchase Receipt", "Purchase Invoice"] + or child_row.get("doctype") == "Subcontracting Receipt Item" + ): + type_of_transaction = "Outward" + return type_of_transaction diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 8532b60d59c..31985678009 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -206,6 +206,7 @@ frappe.ui.form.on("Stock Reconciliation", { posting_date: frm.doc.posting_date, posting_time: frm.doc.posting_time, batch_no: d.batch_no, + row: d, }, callback: function (r) { const row = frappe.model.get_doc(cdt, cdn); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index f92d7361f41..89e69c153d4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -3,7 +3,7 @@ import frappe -from frappe import _, bold, msgprint +from frappe import _, bold, json, msgprint from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import add_to_date, cint, cstr, flt @@ -162,6 +162,11 @@ class StockReconciliation(StockController): def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None: """Set Serial and Batch Bundle for each item""" for item in self.items: + if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle: + bundle = self.get_bundle_for_specific_serial_batch(item) + item.current_serial_and_batch_bundle = bundle + continue + if not save and item.use_serial_batch_fields: continue @@ -273,6 +278,75 @@ class StockReconciliation(StockController): } ) + def get_bundle_for_specific_serial_batch(self, row) -> str: + from erpnext.stock.serial_batch_bundle import SerialBatchCreation + + if row.current_serial_and_batch_bundle and not self.has_change_in_serial_batch(row): + return row.current_serial_and_batch_bundle + + cls_obj = SerialBatchCreation( + { + "type_of_transaction": "Outward", + "serial_and_batch_bundle": row.serial_and_batch_bundle, + "item_code": row.get("item_code"), + "warehouse": row.get("warehouse"), + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "do_not_save": True, + } + ) + + reco_obj = cls_obj.duplicate_package() + + total_current_qty = 0.0 + for entry in reco_obj.entries: + if not entry.batch_no or entry.serial_no: + total_current_qty += entry.qty + entry.qty *= -1 + continue + + current_qty = get_batch_qty( + entry.batch_no, + row.warehouse, + row.item_code, + posting_date=self.posting_date, + posting_time=self.posting_time, + ) + + total_current_qty += current_qty + entry.qty = current_qty * -1 + + reco_obj.flags.ignore_validate = True + reco_obj.save() + + row.current_qty = total_current_qty + + return reco_obj.name + + def has_change_in_serial_batch(self, row) -> bool: + bundles = {row.serial_and_batch_bundle: [], row.current_serial_and_batch_bundle: []} + + data = frappe.get_all( + "Serial and Batch Entry", + fields=["serial_no", "batch_no", "parent"], + filters={"parent": ("in", [row.serial_and_batch_bundle, row.current_serial_and_batch_bundle])}, + order_by="idx", + ) + + for d in data: + bundles[d.parent].append(d.serial_no or d.batch_no) + + diff = set(bundles[row.serial_and_batch_bundle]) - set(bundles[row.current_serial_and_batch_bundle]) + + if diff: + bundle = row.current_serial_and_batch_bundle + row.current_serial_and_batch_bundle = None + frappe.delete_doc("Serial and Batch Bundle", bundle) + + return True + + return False + def set_new_serial_and_batch_bundle(self): for item in self.items: if item.use_serial_batch_fields: @@ -340,6 +414,7 @@ class StockReconciliation(StockController): self.posting_time, batch_no=item.batch_no, inventory_dimensions_dict=inventory_dimensions_dict, + row=item, ) if ( @@ -840,6 +915,7 @@ class StockReconciliation(StockController): row.warehouse, self.posting_date, self.posting_time, + row=row, ) current_qty = item_dict.get("qty") @@ -1166,11 +1242,18 @@ def get_stock_balance_for( batch_no: str | None = None, with_valuation_rate: bool = True, inventory_dimensions_dict=None, + row=None, ): frappe.has_permission("Stock Reconciliation", "write", throw=True) item_dict = frappe.get_cached_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1) + if isinstance(row, str): + row = json.loads(row) + + if isinstance(row, dict): + row = frappe._dict(row) + if not item_dict: # In cases of data upload to Items table msg = _("Item {} does not exist.").format(item_code) @@ -1188,7 +1271,7 @@ def get_stock_balance_for( "qty": 0, "rate": 0, "serial_nos": None, - "use_serial_batch_fields": use_serial_batch_fields, + "use_serial_batch_fields": row.use_serial_batch_fields if row else use_serial_batch_fields, } # TODO: fetch only selected batch's values @@ -1214,7 +1297,7 @@ def get_stock_balance_for( "qty": qty, "rate": rate, "serial_nos": serial_nos, - "use_serial_batch_fields": use_serial_batch_fields, + "use_serial_batch_fields": row.use_serial_batch_fields if row else use_serial_batch_fields, } diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 92a931036e9..4397616e30c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1070,6 +1070,103 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertTrue(sr.items[0].serial_and_batch_bundle) self.assertFalse(sr.items[0].current_serial_and_batch_bundle) + def test_not_reconcile_all_batch(self): + from erpnext.stock.doctype.batch.batch import get_batch_qty + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item = self.make_item( + "Test Batch Item Not Reconcile All Serial Batch", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-NRALL-SRCOSRWFEE-.###", + }, + ) + + warehouse = "_Test Warehouse - _TC" + + batches = [] + for qty in [10, 20, 30]: + se = make_stock_entry( + item_code=item.name, + target=warehouse, + qty=qty, + basic_rate=100 + qty, + posting_date=nowdate(), + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + batches.append(frappe._dict({"batch_no": batch_no, "qty": qty})) + + sr = create_stock_reconciliation( + item_code=item.name, + warehouse=warehouse, + qty=100, + rate=1000, + reconcile_all_serial_batch=0, + batch_no=batches[0].batch_no, + ) + + sr.reload() + current_sabb = sr.items[0].current_serial_and_batch_bundle + doc = frappe.get_doc("Serial and Batch Bundle", current_sabb) + for row in doc.entries: + self.assertEqual(row.batch_no, batches[0].batch_no) + self.assertEqual(row.qty, batches[0].qty * -1) + + batch_qty = get_batch_qty(batches[0].batch_no, warehouse, item.name) + self.assertEqual(batch_qty, 100) + + def test_not_reconcile_all_serial_nos(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.stock.utils import get_incoming_rate + + item = self.make_item( + "Test Serial NO Item Not Reconcile All Serial Batch", + { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SNN-TEST-BATCH-NRALL-S-.###", + }, + ) + + warehouse = "_Test Warehouse - _TC" + + serial_nos = [] + for qty in [5, 5, 5]: + se = make_stock_entry( + item_code=item.name, + target=warehouse, + qty=qty, + basic_rate=100 + qty, + posting_date=nowdate(), + ) + + serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + sr = create_stock_reconciliation( + item_code=item.name, + warehouse=warehouse, + qty=5, + rate=1000, + reconcile_all_serial_batch=0, + serial_no=serial_nos[0:5], + ) + + sr.reload() + current_sabb = sr.items[0].current_serial_and_batch_bundle + doc = frappe.get_doc("Serial and Batch Bundle", current_sabb) + for row in doc.entries: + self.assertEqual(row.serial_no, serial_nos[row.idx - 1]) + + sabb = sr.items[0].serial_and_batch_bundle + doc = frappe.get_doc("Serial and Batch Bundle", sabb) + for row in doc.entries: + self.assertEqual(row.qty, 1) + self.assertAlmostEqual(row.incoming_rate, 1000.00) + self.assertEqual(row.serial_no, serial_nos[row.idx - 1]) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) @@ -1193,12 +1290,16 @@ def create_stock_reconciliation(**args): ) ).name + if args.reconcile_all_serial_batch is None: + args.reconcile_all_serial_batch = 1 + sr.append( "items", { "item_code": args.item_code or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": args.qty, + "reconcile_all_serial_batch": args.reconcile_all_serial_batch, "valuation_rate": args.rate, "serial_no": args.serial_no if args.use_serial_batch_fields else None, "batch_no": args.batch_no if args.use_serial_batch_fields else None, diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 734225972c7..bce819fe231 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -20,6 +20,7 @@ "serial_no_and_batch_section", "add_serial_batch_bundle", "use_serial_batch_fields", + "reconcile_all_serial_batch", "column_break_11", "serial_and_batch_bundle", "current_serial_and_batch_bundle", @@ -243,11 +244,18 @@ { "fieldname": "column_break_eefq", "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:!doc.use_serial_batch_fields", + "fieldname": "reconcile_all_serial_batch", + "fieldtype": "Check", + "label": "Reconcile All Serial Nos / Batches" } ], "istable": 1, "links": [], - "modified": "2024-02-04 16:19:44.576022", + "modified": "2024-05-30 23:20:00.947243", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", @@ -258,4 +266,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py index 1938fec32b0..f2a9aeba8f4 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py @@ -33,6 +33,7 @@ class StockReconciliationItem(Document): parenttype: DF.Data qty: DF.Float quantity_difference: DF.ReadOnly | None + reconcile_all_serial_batch: DF.Check serial_and_batch_bundle: DF.Link | None serial_no: DF.LongText | None use_serial_batch_fields: DF.Check diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index b5df094eee7..24e8dccd17c 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -84,14 +84,6 @@ class StockSettings(Document): make_mandatory=0, ) - stock_frozen_limit = 356 - submitted_stock_frozen = self.stock_frozen_upto_days or 0 - if submitted_stock_frozen > stock_frozen_limit: - self.stock_frozen_upto_days = stock_frozen_limit - frappe.msgprint( - _("`Freeze Stocks Older Than` should be smaller than %d days.") % stock_frozen_limit - ) - # show/hide barcode field for name in ["barcode", "barcodes", "scan_barcode"]: frappe.make_property_setter( diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 1a50e3dea89..e56c499d767 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -7,7 +7,7 @@ from collections import defaultdict import frappe from frappe import _ -from frappe.query_builder.functions import CombineDatetime +from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import cint, flt from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions @@ -67,9 +67,14 @@ def execute(filters=None): if filters.get("batch_no") or inventory_dimension_filters_applied: actual_qty += flt(sle.actual_qty, precision) stock_value += sle.stock_value_difference - batch_balance_dict[sle.batch_no] += sle.actual_qty + if sle.batch_no: + if not batch_balance_dict.get(sle.batch_no): + batch_balance_dict[sle.batch_no] = [0, 0] + + batch_balance_dict[sle.batch_no][0] += sle.actual_qty + if filters.get("segregate_serial_batch_bundle"): - actual_qty = batch_balance_dict[sle.batch_no] + actual_qty = batch_balance_dict[sle.batch_no][0] if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty: actual_qty = sle.qty_after_transaction @@ -525,7 +530,9 @@ def get_opening_balance_from_batch(filters, columns, sl_entries): query_filters = { "batch_no": filters.batch_no, "docstatus": 1, + "is_cancelled": 0, "posting_date": ("<", filters.from_date), + "company": filters.company, } for fields in ["item_code", "warehouse"]: @@ -542,25 +549,30 @@ def get_opening_balance_from_batch(filters, columns, sl_entries): if opening_data.get(field) is None: opening_data[field] = 0.0 - query_filters = [ - ["Serial and Batch Entry", "batch_no", "=", filters.batch_no], - ["Serial and Batch Bundle", "docstatus", "=", 1], - ["Serial and Batch Bundle", "posting_date", "<", filters.from_date], - ] - - for fields in ["item_code", "warehouse"]: - if filters.get(fields): - query_filters.append(["Serial and Batch Bundle", fields, "=", filters.get(fields)]) - - bundle_data = frappe.get_all( - "Serial and Batch Bundle", - fields=[ - "sum(`tabSerial and Batch Entry`.`qty`) as qty", - "sum(`tabSerial and Batch Entry`.`stock_value_difference`) as stock_value", - ], - filters=query_filters, + table = frappe.qb.DocType("Stock Ledger Entry") + sabb_table = frappe.qb.DocType("Serial and Batch Entry") + query = ( + frappe.qb.from_(table) + .inner_join(sabb_table) + .on(table.serial_and_batch_bundle == sabb_table.parent) + .select( + Sum(sabb_table.qty).as_("qty"), + Sum(sabb_table.stock_value_difference).as_("stock_value"), + ) + .where( + (sabb_table.batch_no == filters.batch_no) + & (sabb_table.docstatus == 1) + & (table.posting_date < filters.from_date) + & (table.is_cancelled == 0) + ) ) + for field in ["item_code", "warehouse", "company"]: + if filters.get(field): + query = query.where(table[field] == filters.get(field)) + + bundle_data = query.run(as_dict=True) + if bundle_data: opening_data.qty_after_transaction += flt(bundle_data[0].qty) opening_data.stock_value += flt(bundle_data[0].stock_value) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index fcebf0491ac..1fa5665c141 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -848,10 +848,14 @@ class SerialBatchCreation: new_package.docstatus = 0 new_package.warehouse = self.warehouse new_package.voucher_no = "" - new_package.posting_date = today() - new_package.posting_time = nowtime() + new_package.posting_date = self.posting_date if hasattr(self, "posting_date") else today() + new_package.posting_time = self.posting_time if hasattr(self, "posting_time") else nowtime() new_package.type_of_transaction = self.type_of_transaction new_package.returned_against = self.get("returned_against") + + if self.get("do_not_save"): + return new_package + new_package.save() self.serial_and_batch_bundle = new_package.name diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index 9d788f0809d..4ed73805314 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -235,9 +235,11 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll } has_unsupplied_items() { - return this.frm.doc["supplied_items"].some( - (item) => item.required_qty > item.supplied_qty - item.returned_qty - ); + let over_transfer_allowance = this.frm.doc.__onload.over_transfer_allowance; + return this.frm.doc["supplied_items"].some((item) => { + let required_qty = item.required_qty + (item.required_qty * over_transfer_allowance) / 100; + return required_qty > item.supplied_qty - item.returned_qty; + }); } make_subcontracting_receipt() { diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 8e146c9d49c..8419c8e9093 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -99,6 +99,12 @@ class SubcontractingOrder(SubcontractingController): } ] + def onload(self): + self.set_onload( + "over_transfer_allowance", + frappe.db.get_single_value("Buying Settings", "over_transfer_allowance"), + ) + def before_validate(self): super().before_validate()