diff --git a/.github/workflows/patch_faux.yml b/.github/workflows/patch_faux.yml index 7674631f41b..12491cdba2b 100644 --- a/.github/workflows/patch_faux.yml +++ b/.github/workflows/patch_faux.yml @@ -1,7 +1,6 @@ # Tests are skipped for these files but github doesn't allow "passing" hence this is required. name: Skipped Patch Test -permissions: none on: pull_request: @@ -12,6 +11,9 @@ on: - "**.html" - "**.csv" +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f0abc70c5b..66efc178b1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Entire Repository - uses: actions/checkout@v4 + uses: actions/checkout@v2 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/server-tests-mariadb-faux.yml b/.github/workflows/server-tests-mariadb-faux.yml index b448b115081..d48a13d9b06 100644 --- a/.github/workflows/server-tests-mariadb-faux.yml +++ b/.github/workflows/server-tests-mariadb-faux.yml @@ -1,7 +1,6 @@ # Tests are skipped for these files but github doesn't allow "passing" hence this is required. name: Skipped Tests -permissions: {} on: pull_request: @@ -11,6 +10,9 @@ on: - "**.md" - "**.html" +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest diff --git a/CODEOWNERS b/CODEOWNERS index 4a19fc871b5..50f5b524719 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,22 +3,21 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -erpnext/accounts/ @deepeshgarg007 @ruthra-kumar -erpnext/assets/ @khushi8112 @deepeshgarg007 -erpnext/regional @deepeshgarg007 @ruthra-kumar -erpnext/selling @deepeshgarg007 @ruthra-kumar -erpnext/support/ @deepeshgarg007 -pos* +erpnext/accounts/ @ruthra-kumar +erpnext/assets/ @khushi8112 +erpnext/regional @ruthra-kumar +erpnext/selling @ruthra-kumar +erpnext/support/ @ruthra-kumar -erpnext/buying/ @rohitwaghchaure +erpnext/buying/ @rohitwaghchaure @mihir-kandoi erpnext/maintenance/ @rohitwaghchaure -erpnext/manufacturing/ @rohitwaghchaure +erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi erpnext/quality_management/ @rohitwaghchaure -erpnext/stock/ @rohitwaghchaure -erpnext/subcontracting @rohitwaghchaure +erpnext/stock/ @rohitwaghchaure @mihir-kandoi +erpnext/subcontracting @mihir-kandoi -erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure -erpnext/patches/ @deepeshgarg007 +erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi +erpnext/patches/ @ruthra-kumar -.github/ @deepeshgarg007 +.github/ @ruthra-kumar pyproject.toml @akhilnarang diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 3e026e104ad..b9cde43653a 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -304,7 +304,9 @@ class Account(NestedSet): self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency") self.currency_explicitly_specified = False - gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency") + gl_currency = frappe.db.get_value( + "GL Entry", {"account": self.name, "is_cancelled": 0}, "account_currency" + ) if gl_currency and self.account_currency != gl_currency: if frappe.db.get_value("GL Entry", {"account": self.name}): diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 81937469f21..cdaaa6dd069 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -111,17 +111,15 @@ class AccountingDimension(Document): def make_dimension_in_accounting_doctypes(doc, doclist=None): if not doclist: doclist = get_doctypes_with_dimensions() - doc_count = len(get_accounting_dimensions()) count = 0 - repostable_doctypes = get_allowed_types_from_settings() + repostable_doctypes = get_allowed_types_from_settings(child_doc=True) for doctype in doclist: if (doc_count + 1) % 2 == 0: insert_after_field = "dimension_col_break" else: insert_after_field = "accounting_dimensions_section" - df = { "fieldname": doc.fieldname, "label": doc.label, diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index 4155f0edeef..5a728eb415b 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -109,6 +109,7 @@ class BankAccount(Document): "party_type": self.party_type, "party": self.party, "is_company_account": self.is_company_account, + "company": self.company, "is_default": 1, "disabled": 0, }, diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 7f97c3677bd..3ce867dc96e 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -9,7 +9,7 @@ from frappe import _ from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Sum -from frappe.utils import cint, flt +from frappe.utils import cint, create_batch, flt from erpnext import get_default_cost_center from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount @@ -377,16 +377,17 @@ def auto_reconcile_vouchers( bank_transactions = get_bank_transactions(bank_account) if len(bank_transactions) > 10: - frappe.enqueue( - method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile", - queue="long", - bank_transactions=bank_transactions, - from_date=from_date, - to_date=to_date, - filter_by_reference_date=filter_by_reference_date, - from_reference_date=from_reference_date, - to_reference_date=to_reference_date, - ) + for bank_transaction_batch in create_batch(bank_transactions, 1000): + frappe.enqueue( + method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile", + queue="long", + bank_transactions=bank_transaction_batch, + from_date=from_date, + to_date=to_date, + filter_by_reference_date=filter_by_reference_date, + from_reference_date=from_reference_date, + to_reference_date=to_reference_date, + ) frappe.msgprint(_("Auto Reconciliation has started in the background")) else: start_auto_reconcile( diff --git a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py index 4abc82d8bec..dc73242ac06 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py +++ b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py @@ -4,6 +4,8 @@ import unittest import frappe +from frappe.query_builder.functions import Sum +from frappe.tests.utils import change_settings from frappe.utils import add_days, today from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center @@ -190,6 +192,31 @@ class TestCostCenterAllocation(unittest.TestCase): coa2.cancel() jv.cancel() + @change_settings("System Settings", {"rounding_method": "Commercial Rounding"}) + def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + cca = create_cost_center_allocation( + "_Test Company", + "Main Cost Center 1 - _TC", + {"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50}, + ) + + si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC") + + gl_entry = frappe.qb.DocType("GL Entry") + gl_entries = ( + frappe.qb.from_(gl_entry) + .select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr")) + .where(gl_entry.voucher_type == "Sales Invoice") + .where(gl_entry.voucher_no == si.name) + ).run(as_dict=1) + + self.assertEqual(gl_entries[0].cr, gl_entries[0].dr) + + si.cancel() + cca.cancel() + def create_cost_center_allocation( company, diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js index f15c2bdab21..ab9a50f8285 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js +++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js @@ -8,4 +8,14 @@ frappe.ui.form.on("Payment Gateway Account", { frm.set_df_property("payment_gateway", "read_only", 1); } }, + + setup(frm) { + frm.set_query("payment_account", function () { + return { + filters: { + company: frm.doc.company, + }, + }; + }); + }, }); diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json index 12e6f5ef22d..fafd58c8a39 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json +++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json @@ -6,6 +6,7 @@ "field_order": [ "payment_gateway", "payment_channel", + "company", "is_default", "column_break_4", "payment_account", @@ -70,11 +71,21 @@ "fieldtype": "Select", "label": "Payment Channel", "options": "\nEmail\nPhone" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "print_hide": 1, + "remember_last_selected_value": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-20 13:30:27.722852", + "modified": "2025-07-14 16:49:55.210352", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Gateway Account", @@ -95,4 +106,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py index f1bf363eca0..7c58949be8b 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py +++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py @@ -15,6 +15,7 @@ class PaymentGatewayAccount(Document): if TYPE_CHECKING: from frappe.types import DF + company: DF.Link currency: DF.ReadOnly | None is_default: DF.Check message: DF.SmallText | None @@ -24,7 +25,8 @@ class PaymentGatewayAccount(Document): # end: auto-generated types def autoname(self): - self.name = self.payment_gateway + " - " + self.currency + abbr = frappe.db.get_value("Company", self.company, "abbr") + self.name = self.payment_gateway + " - " + self.currency + " - " + abbr def validate(self): self.currency = frappe.get_cached_value("Account", self.payment_account, "account_currency") @@ -34,13 +36,15 @@ class PaymentGatewayAccount(Document): def update_default_payment_gateway(self): if self.is_default: - frappe.db.sql( - """update `tabPayment Gateway Account` set is_default = 0 - where is_default = 1 """ + frappe.db.set_value( + "Payment Gateway Account", + {"is_default": 1, "name": ["!=", self.name], "company": self.company}, + "is_default", + 0, ) def set_as_default_if_not_set(self): - if not frappe.db.get_value( - "Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name" + if not frappe.db.exists( + "Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name), "company": self.company} ): self.is_default = 1 diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index 50f96a4e2b6..5cca11ae2fd 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -9,6 +9,14 @@ frappe.ui.form.on("Payment Request", { query: "erpnext.setup.doctype.party_type.party_type.get_party_type", }; }); + + frm.set_query("payment_gateway_account", function () { + return { + filters: { + company: frm.doc.company, + }, + }; + }); }, }); diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 8bb120f1ab5..1a400878ba5 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -539,7 +539,9 @@ def make_payment_request(**args): if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST: frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt))) - ref_doc = frappe.get_doc(args.dt, args.dn) + ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn) + if not args.get("company"): + args.company = ref_doc.company gateway_account = get_gateway_details(args) or frappe._dict() grand_total = get_amount(ref_doc, gateway_account.get("payment_account")) @@ -782,7 +784,7 @@ def get_gateway_details(args): # nosemgrep """ Return gateway and payment account of default payment gateway """ - gateway_account = args.get("payment_gateway_account", {"is_default": 1}) + gateway_account = args.get("payment_gateway_account", {"is_default": 1, "company": args.company}) if gateway_account: return get_payment_gateway_account(gateway_account) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index c3f73f0fc12..ae8ccffc639 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -28,12 +28,14 @@ payment_method = [ "payment_gateway": "_Test Gateway", "payment_account": "_Test Bank - _TC", "currency": "INR", + "company": "_Test Company", }, { "doctype": "Payment Gateway Account", "payment_gateway": "_Test Gateway", "payment_account": "_Test Bank USD - _TC", "currency": "USD", + "company": "_Test Company", }, ] @@ -46,7 +48,11 @@ class TestPaymentRequest(FrappeTestCase): for method in payment_method: if not frappe.db.get_value( "Payment Gateway Account", - {"payment_gateway": method["payment_gateway"], "currency": method["currency"]}, + { + "payment_gateway": method["payment_gateway"], + "currency": method["currency"], + "company": method["company"], + }, "name", ): frappe.get_doc(method).insert(ignore_permissions=True) @@ -60,7 +66,7 @@ class TestPaymentRequest(FrappeTestCase): dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", - payment_gateway_account="_Test Gateway - INR", + payment_gateway_account="_Test Gateway - INR - _TC", ) self.assertEqual(pr.reference_doctype, "Sales Order") @@ -74,7 +80,7 @@ class TestPaymentRequest(FrappeTestCase): dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - payment_gateway_account="_Test Gateway - USD", + payment_gateway_account="_Test Gateway - USD - _TC", ) self.assertEqual(pr.reference_doctype, "Sales Invoice") @@ -95,7 +101,7 @@ class TestPaymentRequest(FrappeTestCase): party="_Test Supplier USD", recipient_id="user@example.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD", + payment_gateway_account="_Test Gateway - USD - _TC", submit_doc=1, return_doc=1, ) @@ -119,7 +125,7 @@ class TestPaymentRequest(FrappeTestCase): dn=purchase_invoice.name, recipient_id="user@example.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD", + payment_gateway_account="_Test Gateway - USD - _TC", return_doc=1, ) @@ -138,7 +144,7 @@ class TestPaymentRequest(FrappeTestCase): dn=purchase_invoice.name, recipient_id="user@example.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD", + payment_gateway_account="_Test Gateway - USD - _TC", return_doc=1, ) @@ -162,7 +168,7 @@ class TestPaymentRequest(FrappeTestCase): dn=so_inr.name, recipient_id="saurabh@erpnext.com", mute_email=1, - payment_gateway_account="_Test Gateway - INR", + payment_gateway_account="_Test Gateway - INR - _TC", submit_doc=1, return_doc=1, ) @@ -184,7 +190,7 @@ class TestPaymentRequest(FrappeTestCase): dn=si_usd.name, recipient_id="saurabh@erpnext.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD", + payment_gateway_account="_Test Gateway - USD - _TC", submit_doc=1, return_doc=1, ) @@ -228,7 +234,7 @@ class TestPaymentRequest(FrappeTestCase): dn=si_usd.name, recipient_id="saurabh@erpnext.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD", + payment_gateway_account="_Test Gateway - USD - _TC", submit_doc=1, return_doc=1, ) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index aaf5142362f..b96e29979a8 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -301,6 +301,7 @@ "search_index": 1 }, { + "default": "Now", "fieldname": "posting_time", "fieldtype": "Time", "label": "Posting Time", @@ -1573,7 +1574,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2025-07-17 16:51:40.886083", + "modified": "2025-08-04 22:22:31.471752", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json index f5779cff84f..0b3095d84ab 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -78,18 +78,18 @@ "reqd": 1 }, { - "depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');", + "depends_on": "eval:(!doc.enable_auto_email && doc.report == 'General Ledger');", "fieldname": "from_date", "fieldtype": "Date", "label": "From Date", - "mandatory_depends_on": "eval:doc.frequency == '';" + "mandatory_depends_on": "eval:(!doc.enable_auto_email && doc.report == \"General Ledger\") " }, { - "depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');", + "depends_on": "eval:(!doc.enable_auto_email && doc.report == 'General Ledger');", "fieldname": "to_date", "fieldtype": "Date", "label": "To Date", - "mandatory_depends_on": "eval:doc.frequency == '';" + "mandatory_depends_on": "eval:(!doc.enable_auto_email && doc.report == \"General Ledger\") " }, { "fieldname": "cost_center", @@ -330,7 +330,8 @@ "depends_on": "eval:(doc.report == 'Accounts Receivable');", "fieldname": "posting_date", "fieldtype": "Date", - "label": "Posting Date" + "label": "Posting Date", + "mandatory_depends_on": "eval:(doc.report == 'Accounts Receivable');" }, { "depends_on": "eval: (doc.report == 'Accounts Receivable');", @@ -400,7 +401,7 @@ } ], "links": [], - "modified": "2025-07-08 16:52:12.602384", + "modified": "2025-08-04 18:21:12.603623", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts", diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.py b/erpnext/accounts/doctype/process_subscription/process_subscription.py index b4d18a3c0a8..0cc3fae58a8 100644 --- a/erpnext/accounts/doctype/process_subscription/process_subscription.py +++ b/erpnext/accounts/doctype/process_subscription/process_subscription.py @@ -3,7 +3,7 @@ import frappe from frappe.model.document import Document -from frappe.utils import getdate +from frappe.utils import create_batch, getdate from erpnext.accounts.doctype.subscription.subscription import DateTimeLikeObject, process_all @@ -23,7 +23,23 @@ class ProcessSubscription(Document): # end: auto-generated types def on_submit(self): - process_all(subscription=self.subscription, posting_date=self.posting_date) + self.process_all_subscription() + + def process_all_subscription(self): + filters = {"status": ("!=", "Cancelled")} + + if self.subscription: + filters["name"] = self.subscription + + subscriptions = frappe.get_all("Subscription", filters, pluck="name") + + for subscription in create_batch(subscriptions, 500): + frappe.enqueue( + method="erpnext.accounts.doctype.subscription.subscription.process_all", + queue="long", + subscription=subscription, + posting_date=self.posting_date, + ) def create_subscription_process( diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index dfdcea5357a..83e704734ce 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -319,6 +319,7 @@ "search_index": 1 }, { + "default": "Now", "fieldname": "posting_time", "fieldtype": "Time", "label": "Posting Time", @@ -1650,7 +1651,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2025-04-09 16:49:22.175081", + "modified": "2025-08-04 19:19:11.380664", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index aec26b280c4..37863036492 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -5,6 +5,7 @@ import inspect import frappe from frappe import _, qb +from frappe.desk.form.linked_with import get_child_tables_of_doctypes from frappe.model.document import Document from frappe.utils.data import comma_and @@ -208,13 +209,29 @@ def start_repost(account_repost_doc=str) -> None: doc.make_gl_entries() -def get_allowed_types_from_settings(): - return [ +def get_allowed_types_from_settings(child_doc: bool = False): + repost_docs = [ x.document_type for x in frappe.db.get_all( "Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"] ) ] + result = repost_docs + + if repost_docs and child_doc: + result.extend(get_child_docs(repost_docs)) + + return result + + +def get_child_docs(doc: list) -> list: + child_doc = [] + doc = get_child_tables_of_doctypes(doc) + for child_list in doc.values(): + for child in child_list: + if child.get("child_table"): + child_doc.append(child["child_table"]) + return child_doc def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py index 14a070dc464..637234fea1e 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py @@ -1,9 +1,14 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, +) +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs + class RepostAccountingLedgerSettings(Document): # begin: auto-generated types @@ -17,6 +22,24 @@ class RepostAccountingLedgerSettings(Document): from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes allowed_types: DF.Table[RepostAllowedTypes] - # end: auto-generated types - pass + # end: auto-generated types + def validate(self): + self.update_property_for_accounting_dimension() + + def update_property_for_accounting_dimension(self): + doctypes = [entry.document_type for entry in self.allowed_types if entry.allowed] + if not doctypes: + return + doctypes += get_child_docs(doctypes) + + set_allow_on_submit_for_dimension_fields(doctypes) + + +def set_allow_on_submit_for_dimension_fields(doctypes): + for dt in doctypes: + meta = frappe.get_meta(dt) + for dimension in get_accounting_dimensions(): + df = meta.get_field(dimension) + if df and not df.allow_on_submit: + frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 33df4882af9..07b97920ae2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -379,6 +379,7 @@ "search_index": 1 }, { + "default": "Now", "fieldname": "posting_time", "fieldtype": "Time", "hide_days": 1, @@ -2189,7 +2190,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2025-06-26 14:06:56.773552", + "modified": "2025-08-04 19:20:28.732039", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 829428eec86..97ded97dd2e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1370,8 +1370,9 @@ class SalesInvoice(SellingController): ) asset.db_set("disposal_date", None) add_asset_activity(asset.name, _("Asset returned")) + asset_status = asset.get_status() - if asset.calculate_depreciation: + if asset.calculate_depreciation and not asset_status == "Fully Depreciated": posting_date = ( frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") if self.is_return diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 7323c819d7e..8c4d21ac3fb 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -756,18 +756,14 @@ def get_prorata_factor( return diff / plan_days -def process_all(subscription: str | None = None, posting_date: DateTimeLikeObject | None = None) -> None: +def process_all(subscription: list, posting_date: DateTimeLikeObject | None = None) -> None: """ Task to updates the status of all `Subscription` apart from those that are cancelled """ - filters = {"status": ("!=", "Cancelled")} - if subscription: - filters["name"] = subscription - - for subscription in frappe.get_all("Subscription", filters, pluck="name"): + for subscription_name in subscription: try: - subscription = frappe.get_doc("Subscription", subscription) + subscription = frappe.get_doc("Subscription", subscription_name) subscription.process(posting_date) frappe.db.commit() except frappe.ValidationError: diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json index 153906ffe97..286b915b05e 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json @@ -75,7 +75,7 @@ }, { "default": "0", - "description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach", + "description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach", "fieldname": "consider_party_ledger_amount", "fieldtype": "Check", "label": "Consider Entire Party Ledger Amount", @@ -102,10 +102,11 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-07-27 21:47:34.396071", + "modified": "2025-07-30 07:13:51.785735", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withholding Category", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -148,4 +149,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index a0d08792d29..0e97d6969bd 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -186,6 +186,15 @@ def process_gl_map(gl_map, merge_entries=True, precision=None, from_repost=False def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_repost=False): + round_off_account, default_currency = frappe.get_cached_value( + "Company", gl_map[0].company, ["round_off_account", "default_currency"] + ) + if not precision: + precision = get_field_precision( + frappe.get_meta("GL Entry").get_field("debit"), + currency=default_currency, + ) + new_gl_map = [] for d in gl_map: cost_center = d.get("cost_center") @@ -203,6 +212,11 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r new_gl_map.append(d) continue + if d.account == round_off_account: + d.cost_center = cost_center_allocation[0][0] + new_gl_map.append(d) + continue + for sub_cost_center, percentage in cost_center_allocation: gle = copy.deepcopy(d) gle.cost_center = sub_cost_center diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index f27441bc3e2..cebdad3744f 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -197,6 +197,11 @@ frappe.query_reports["General Ledger"] = { label: __("Show Net Values in Party Account"), fieldtype: "Check", }, + { + fieldname: "show_amount_in_company_currency", + label: __("Show Credit / Debit in Company Currency"), + fieldtype: "Check", + }, { fieldname: "add_values_in_transaction_currency", label: __("Add Columns in Transaction Currency"), diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 91b244f94fa..2bec888729a 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -605,6 +605,18 @@ def get_columns(filters): company = filters.get("company") or get_default_company() filters["presentation_currency"] = currency = get_company_currency(company) + company_currency = get_company_currency(filters.get("company") or get_default_company()) + + if ( + filters.get("show_amount_in_company_currency") + and filters["presentation_currency"] != company_currency + ): + frappe.throw( + _( + f'Presentation Currency cannot be {frappe.bold(filters["presentation_currency"])} , When {frappe.bold("Show Credit / Debit in Company Currency")} is enabled.' + ) + ) + columns = [ { "label": _("GL Entry"), diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py index c0ca604cc6d..6a6f58c74bb 100644 --- a/erpnext/accounts/report/non_billed_report.py +++ b/erpnext/accounts/report/non_billed_report.py @@ -46,6 +46,7 @@ def get_ordered_to_be_billed_data(args, filters=None): child_doctype.item_name, child_doctype.description, project_field, + doctype.company, ) .where( (doctype.docstatus == 1) diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.py b/erpnext/accounts/report/payment_ledger/payment_ledger.py index 9dd5ae5c400..7a2fdc4c925 100644 --- a/erpnext/accounts/report/payment_ledger/payment_ledger.py +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.py @@ -46,6 +46,7 @@ class PaymentLedger: against_voucher_no=ple.against_voucher_no, amount=ple.amount, currency=ple.account_currency, + company=ple.company, ) if self.filters.include_account_currency: @@ -77,6 +78,7 @@ class PaymentLedger: against_voucher_no="Outstanding:", amount=total, currency=voucher_data[0].currency, + company=voucher_data[0].company, ) if self.filters.include_account_currency: @@ -85,7 +87,12 @@ class PaymentLedger: voucher_data.append(entry) # empty row - voucher_data.append(frappe._dict()) + voucher_data.append( + frappe._dict( + currency=voucher_data[0].currency, + company=voucher_data[0].company, + ) + ) self.data.extend(voucher_data) def build_conditions(self): @@ -130,7 +137,6 @@ class PaymentLedger: ) def get_columns(self): - company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency") options = None self.columns.append( dict( @@ -195,7 +201,7 @@ class PaymentLedger: label=_("Amount"), fieldname="amount", fieldtype="Currency", - options=company_currency, + options="Company:company:default_currency", width="100", ) ) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 4507fbdcb60..81dba55d609 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -45,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ gle_map = get_gle_map(tds_docs) out = [] + entries = {} for name, details in gle_map.items(): for entry in details: tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 @@ -119,8 +120,13 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ "supplier_invoice_date": bill_date, } ) - out.append(row) + key = entry.voucher_no + if key in entries: + entries[key]["tax_amount"] += tax_amount + else: + entries[key] = row + out = list(entries.values()) out.sort(key=lambda x: (x["section_code"], x["transaction_date"])) return out diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 136a0acbbb0..523fa0e0ee3 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -118,7 +118,7 @@ def convert_to_presentation_currency(gl_entries, currency_info, filters=None): len(account_currencies) == 1 and account_currency == presentation_currency and not exchange_gain_or_loss - ): + ) and not filters.get("show_amount_in_company_currency"): entry["debit"] = debit_in_account_currency entry["credit"] = credit_in_account_currency else: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 022dfb889a9..36fe32b0381 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt +from collections import defaultdict from json import loads from typing import TYPE_CHECKING, Optional @@ -1339,6 +1340,7 @@ def create_payment_gateway_account(gateway, payment_channel="Email"): "payment_account": bank_account.name, "currency": bank_account.account_currency, "payment_channel": payment_channel, + "company": company, } ).insert(ignore_permissions=True, ignore_if_duplicate=True) @@ -2419,25 +2421,37 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15): ).save() +def get_link_fields_grouped_by_option(doctype): + meta = frappe.get_meta(doctype) + link_fields_map = defaultdict(list) + + for df in meta.fields: + if df.fieldtype == "Link" and df.options and not df.ignore_user_permissions: + link_fields_map[df.options].append(df.fieldname) + + return link_fields_map + + def build_qb_match_conditions(doctype, user=None) -> list: match_filters = build_match_conditions(doctype, user, False) + link_fields_map = get_link_fields_grouped_by_option(doctype) criterion = [] apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions") if match_filters: - from frappe import qb - _dt = qb.DocType(doctype) for filter in match_filters: - for d, names in filter.items(): - fieldname = d.lower().replace(" ", "_") - field = _dt[fieldname] + for link_option, allowed_values in filter.items(): + fieldnames = link_fields_map.get(link_option, []) - cond = field.isin(names) - if not apply_strict_user_permissions: - cond = (Coalesce(field, "") == "") | field.isin(names) + for fieldname in fieldnames: + field = _dt[fieldname] + cond = field.isin(allowed_values) - criterion.append(cond) + if not apply_strict_user_permissions: + cond = (Coalesce(field, "") == "") | cond + + criterion.append(cond) return criterion diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 14d0d53ac69..79e266d14c2 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1097,7 +1097,7 @@ def make_journal_entry(asset_name): je.voucher_type = "Depreciation Entry" je.naming_series = depreciation_series je.company = asset.company - je.remark = f"Depreciation Entry against asset {asset_name}" + je.remark = _("Depreciation Entry against asset {0}").format(asset_name) je.append( "accounts", diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index d888e549cea..a0681cc2c56 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -277,7 +277,9 @@ def _make_journal_entry_for_depreciation( je.posting_date = depr_schedule.schedule_date je.company = asset.company je.finance_book = asset_depr_schedule_doc.finance_book - je.remark = f"Depreciation Entry against {asset.name} worth {depr_schedule.depreciation_amount}" + je.remark = _("Depreciation Entry against {0} worth {1}").format( + asset.name, depr_schedule.depreciation_amount + ) credit_entry = { "account": credit_account, diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 609a6b9677d..78a7929a4fe 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -346,6 +346,33 @@ class TestAsset(AssetSetup): si.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + def test_asset_status_after_sales_invoice_cancel(self): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + asset = create_asset( + calculate_depreciation=1, + available_for_use_date="2020-04-01", + purchase_date="2020-04-01", + expected_value_after_useful_life=0, + total_number_of_depreciations=5, + opening_number_of_booked_depreciations=2, + frequency_of_depreciation=12, + depreciation_start_date="2023-03-31", + opening_accumulated_depreciation=24000, + gross_purchase_amount=60000, + submit=1, + ) + + si = create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=40000, posting_date=getdate("2023-05-23") + ) + asset.load_from_db() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + + si.cancel() + asset.load_from_db() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + def test_gle_made_by_asset_sale_for_existing_asset(self): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index f9c4913b1a2..e1413096af3 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -98,20 +98,41 @@ class AssetDepreciationSchedule(Document): ) def on_submit(self): + self.validate_asset() self.db_set("status", "Active") - def before_cancel(self): + def validate_asset(self): + asset = frappe.get_doc("Asset", self.asset) + if not asset.calculate_depreciation: + frappe.throw( + _("Asset {0} is not set to calculate depreciation.").format( + get_link_to_form("Asset", self.asset) + ) + ) + if asset.docstatus != 1: + frappe.throw( + _("Asset {0} is not submitted. Please submit the asset before proceeding.").format( + get_link_to_form("Asset", self.asset) + ) + ) + + def on_cancel(self): + self.db_set("status", "Cancelled") if not self.flags.should_not_cancel_depreciation_entries: self.cancel_depreciation_entries() def cancel_depreciation_entries(self): for d in self.get("depreciation_schedule"): if d.journal_entry: + je_status = frappe.db.get_value("Journal Entry", d.journal_entry, "docstatus") + if je_status == 0: + frappe.throw( + _( + "Cannot cancel Asset Depreciation Schedule {0} as it has a draft journal entry {1}." + ).format(self.name, d.journal_entry) + ) frappe.get_doc("Journal Entry", d.journal_entry).cancel() - def on_cancel(self): - self.db_set("status", "Cancelled") - def update_shift_depr_schedule(self): if not self.shift_based or self.docstatus != 0: return diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js index 13f2444742d..20e98631de6 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js @@ -3,13 +3,13 @@ frappe.listview_settings["Asset Maintenance Log"] = { has_indicator_for_draft: 1, get_indicator: function (doc) { if (doc.maintenance_status == "Planned") { - return [__(doc.maintenance_status), "orange", "status,=," + doc.maintenance_status]; + return [__(doc.maintenance_status), "orange", "maintenance_status,=," + doc.maintenance_status]; } else if (doc.maintenance_status == "Completed") { - return [__(doc.maintenance_status), "green", "status,=," + doc.maintenance_status]; + return [__(doc.maintenance_status), "green", "maintenance_status,=," + doc.maintenance_status]; } else if (doc.maintenance_status == "Cancelled") { - return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; + return [__(doc.maintenance_status), "red", "maintenance_status,=," + doc.maintenance_status]; } else if (doc.maintenance_status == "Overdue") { - return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; + return [__(doc.maintenance_status), "red", "maintenance_status,=," + doc.maintenance_status]; } }, }; diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 6419a4ebac2..246690c3e39 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -720,6 +720,7 @@ def close_or_unclose_purchase_orders(names, status): def set_missing_values(source, target): target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") + target.run_method("set_use_serial_batch_fields") @frappe.whitelist() diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index bd0798236b3..6f610d2dc20 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -51,7 +51,7 @@ def get_columns(filters): }, { "label": _("Requestor"), - "options": "Employee", + "options": "User", "fieldname": "requestor", "fieldtype": "Link", "width": 140, diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 2e8ae7e340e..23ec7d8c537 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -153,7 +153,12 @@ class OpportunitySummaryBySalesStage: }[self.filters.get("based_on")] if self.filters.get("based_on") == "Opportunity Owner": - if d.get(based_on) == "[]" or d.get(based_on) is None or d.get(based_on) == "Not Assigned": + if ( + d.get(based_on) == "[]" + or d.get(based_on) is None + or d.get(based_on) == "Not Assigned" + or d.get(based_on) == "" + ): assignments = ["Not Assigned"] else: assignments = json.loads(d.get(based_on)) diff --git a/erpnext/edi/doctype/code_list/code_list_import.py b/erpnext/edi/doctype/code_list/code_list_import.py index 50df3be471e..ecabb256026 100644 --- a/erpnext/edi/doctype/code_list/code_list_import.py +++ b/erpnext/edi/doctype/code_list/code_list_import.py @@ -60,7 +60,7 @@ def import_genericode(): "doctype": "File", "attached_to_doctype": "Code List", "attached_to_name": code_list.name, - "folder": "Home/Attachments", + "folder": frappe.db.get_value("File", {"is_attachments_folder": 1}), "file_name": frappe.local.uploaded_filename, "file_url": frappe.local.uploaded_file_url, "is_private": 1, diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 037a3fe0e81..3bcc4e896bd 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -65,6 +65,7 @@ "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate", + "non_negative": 1, "oldfieldname": "hour_rate", "oldfieldtype": "Currency", "options": "currency", @@ -78,6 +79,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Operation Time", + "non_negative": 1, "oldfieldname": "time_in_mins", "oldfieldtype": "Currency", "reqd": 1 @@ -194,7 +196,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-04 17:17:16.986941", + "modified": "2025-07-31 16:17:47.287117", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json index b2ef19b20f0..27001227974 100644 --- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +++ b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json @@ -42,6 +42,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Qty", + "non_negative": 1, "reqd": 1 }, { @@ -49,6 +50,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Rate", + "non_negative": 1, "options": "currency" }, { @@ -92,7 +94,7 @@ ], "istable": 1, "links": [], - "modified": "2023-01-03 14:19:28.460965", + "modified": "2025-07-31 16:21:44.047007", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Scrap Item", @@ -103,4 +105,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 39e7aac38ae..7f4fbdaef06 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -160,6 +160,7 @@ "fieldname": "total_completed_qty", "fieldtype": "Float", "label": "Total Completed Qty", + "non_negative": 1, "read_only": 1 }, { @@ -510,7 +511,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2025-03-17 15:55:11.143456", + "modified": "2025-08-04 15:47:54.514290", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", @@ -568,4 +569,4 @@ "states": [], "title_field": "operation", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json index 5fb65698235..2eafd0ba81a 100644 --- a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json +++ b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json @@ -70,7 +70,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-14 01:20:48.588052", + "modified": "2025-07-29 13:09:57.323835", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Scrap Item", @@ -80,4 +80,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json index 6a86214986a..0565d677e9b 100644 --- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json +++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json @@ -42,7 +42,8 @@ "fieldname": "completed_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Completed Qty" + "label": "Completed Qty", + "non_negative": 1 }, { "fieldname": "employee", @@ -63,7 +64,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-05-21 12:40:55.765860", + "modified": "2025-08-04 15:47:11.748937", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Time Log", diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json index 753552ce54e..65daa03a76b 100644 --- a/erpnext/manufacturing/doctype/operation/operation.json +++ b/erpnext/manufacturing/doctype/operation/operation.json @@ -70,7 +70,8 @@ "fieldname": "batch_size", "fieldtype": "Int", "label": "Batch Size", - "mandatory_depends_on": "create_job_card_based_on_batch_size" + "mandatory_depends_on": "create_job_card_based_on_batch_size", + "non_negative": 1 }, { "default": "0", @@ -104,7 +105,7 @@ "icon": "fa fa-wrench", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-11-24 19:15:24.357187", + "modified": "2025-08-04 16:14:57.659318", "modified_by": "Administrator", "module": "Manufacturing", "name": "Operation", @@ -137,4 +138,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json index 10cee32398a..c7530e41aca 100644 --- a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json @@ -24,7 +24,8 @@ "fieldname": "time_in_mins", "fieldtype": "Float", "in_list_view": 1, - "label": "Operation Time" + "label": "Operation Time", + "non_negative": 1 }, { "fieldname": "column_break_5", @@ -39,7 +40,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-07-15 16:39:41.635362", + "modified": "2025-08-04 16:15:11.425349", "modified_by": "Administrator", "module": "Manufacturing", "name": "Sub Operation", @@ -49,4 +50,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d87f595a500..197596daa8d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -356,8 +356,8 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") erpnext.patches.v14_0.update_total_asset_cost_field +erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes #2025-06-19 erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool -erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22 erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request erpnext.patches.v14_0.update_pos_return_ledger_entries #2024-08-16 @@ -416,5 +416,6 @@ erpnext.patches.v15_0.update_payment_ledger_entries_against_advance_doctypes erpnext.patches.v15_0.rename_price_list_to_buying_price_list erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice -erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-07-31 +erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-08-04 execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1) +erpnext.patches.v15_0.add_company_payment_gateway_account diff --git a/erpnext/patches/v15_0/add_company_payment_gateway_account.py b/erpnext/patches/v15_0/add_company_payment_gateway_account.py new file mode 100644 index 00000000000..0c2f0c6f2d3 --- /dev/null +++ b/erpnext/patches/v15_0/add_company_payment_gateway_account.py @@ -0,0 +1,7 @@ +import frappe + + +def execute(): + for gateway_account in frappe.get_list("Payment Gateway Account", fields=["name", "payment_account"]): + company = frappe.db.get_value("Account", gateway_account.payment_account, "company") + frappe.db.set_value("Payment Gateway Account", gateway_account.name, "company", company) diff --git a/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py b/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py index e75610d0a53..dddeae328c7 100644 --- a/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py +++ b/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py @@ -9,6 +9,6 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger def execute(): - for dt in get_allowed_types_from_settings(): + for dt in get_allowed_types_from_settings(child_doc=True): for dimension in get_accounting_dimensions(): frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1) diff --git a/erpnext/patches/v15_0/repost_gl_entries_with_no_account_subcontracting.py b/erpnext/patches/v15_0/repost_gl_entries_with_no_account_subcontracting.py index e7394f1071c..1c7d1cc471a 100644 --- a/erpnext/patches/v15_0/repost_gl_entries_with_no_account_subcontracting.py +++ b/erpnext/patches/v15_0/repost_gl_entries_with_no_account_subcontracting.py @@ -2,24 +2,31 @@ import frappe def execute(): + def cancel_incorrect_gl_entries(gl_entries): + table = frappe.qb.DocType("GL Entry") + frappe.qb.update(table).set(table.is_cancelled, 1).where(table.name.isin(gl_entries)).run() + + def recreate_gl_entries(voucher_nos): + for doc in voucher_nos: + doc = frappe.get_doc("Subcontracting Receipt", doc) + for item in doc.supplied_items: + account, cost_center = frappe.db.get_values( + "Subcontracting Receipt Item", item.reference_name, ["expense_account", "cost_center"] + )[0] + + if not item.expense_account: + item.db_set("expense_account", account) + if not item.cost_center: + item.db_set("cost_center", cost_center) + + doc.make_gl_entries() + docs = frappe.get_all( "GL Entry", + fields=["name", "voucher_no"], filters={"voucher_type": "Subcontracting Receipt", "account": ["is", "not set"], "is_cancelled": 0}, - pluck="voucher_no", ) - for doc in docs: - doc = frappe.get_doc("Subcontracting Receipt", doc) - for item in doc.supplied_items: - account, cost_center = frappe.db.get_values( - "Subcontracting Receipt Item", item.reference_name, ["expense_account", "cost_center"] - )[0] - if not item.expense_account: - item.db_set("expense_account", account) - if not item.cost_center: - item.db_set("cost_center", cost_center) - - doc.docstatus = 2 - doc.make_gl_entries_on_cancel() - doc.docstatus = 1 - doc.make_gl_entries() + if docs: + cancel_incorrect_gl_entries([d.name for d in docs]) + recreate_gl_entries([d.voucher_no for d in docs]) \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project_dashboard.py b/erpnext/projects/doctype/project/project_dashboard.py index 5d17efbabc5..8acd02108cc 100644 --- a/erpnext/projects/doctype/project/project_dashboard.py +++ b/erpnext/projects/doctype/project/project_dashboard.py @@ -14,5 +14,6 @@ def get_data(): {"label": _("Material"), "items": ["Material Request", "BOM", "Stock Entry"]}, {"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, {"label": _("Purchase"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]}, + {"label": _("Manufacture"), "items": ["Work Order"]}, ], } diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index c50e6e53928..be503a45d56 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -928,15 +928,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.frm.refresh_fields(); } - async set_default_payment(total_amount_to_pay, update_paid_amount) { + set_default_payment(total_amount_to_pay, update_paid_amount) { var me = this; var payment_status = true; if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { - let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop"); - - if (r.message.disable_grand_total_to_default_mop) { - return; - } $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 6905292420b..5e3218a67a2 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -421,6 +421,7 @@ erpnext.PointOfSale.Controller = class { init_payments() { this.payment = new erpnext.PointOfSale.Payment({ wrapper: this.$components_wrapper, + settings: this.settings, events: { get_frm: () => this.frm || {}, diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index b47c25e20bb..59f293a96b0 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -1,8 +1,9 @@ /* eslint-disable no-unused-vars */ erpnext.PointOfSale.Payment = class { - constructor({ events, wrapper }) { + constructor({ events, settings, wrapper }) { this.wrapper = wrapper; this.events = events; + this.disable_grand_total_to_default_mop = settings.disable_grand_total_to_default_mop; this.init_component(); } @@ -341,10 +342,11 @@ erpnext.PointOfSale.Payment = class { } render_payment_section() { + this.remove_grand_total_from_default_mop(); this.render_payment_mode_dom(); this.make_invoice_fields_control(); this.update_totals_section(); - this.unset_grand_total_to_default_mop(); + this.focus_on_default_mop(); } after_render() { @@ -446,7 +448,19 @@ erpnext.PointOfSale.Payment = class { this.attach_cash_shortcuts(doc); } + remove_grand_total_from_default_mop() { + if (!this.disable_grand_total_to_default_mop) return; + const doc = this.events.get_frm().doc; + const payments = doc.payments; + payments.forEach((p) => { + if (p.default) { + frappe.model.set_value(p.doctype, p.name, "amount", 0); + } + }); + } + focus_on_default_mop() { + if (this.disable_grand_total_to_default_mop) return; const doc = this.events.get_frm().doc; const payments = doc.payments; payments.forEach((p) => { @@ -629,19 +643,6 @@ erpnext.PointOfSale.Payment = class { .toLowerCase(); } - async unset_grand_total_to_default_mop() { - const doc = this.events.get_frm().doc; - let r = await frappe.db.get_value( - "POS Profile", - doc.pos_profile, - "disable_grand_total_to_default_mop" - ); - - if (!r.message.disable_grand_total_to_default_mop) { - this.focus_on_default_mop(); - } - } - validate_reqd_invoice_fields() { const doc = this.events.get_frm().doc; let validation_flag = true; diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index e55b7f229dc..3a9cca1c418 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -258,6 +258,7 @@ "width": "100px" }, { + "default": "Now", "fieldname": "posting_time", "fieldtype": "Time", "label": "Posting Time", @@ -1395,7 +1396,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2024-11-26 12:44:28.258215", + "modified": "2025-08-04 19:20:47.724218", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", @@ -1486,6 +1487,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status,customer,customer_name, territory,base_grand_total", "show_name_in_global_search": 1, "sort_field": "modified", @@ -1495,4 +1497,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 299f36a6ab7..cbd7a1d7f57 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -96,7 +96,6 @@ class DeliveryNote(SellingController): per_billed: DF.Percent per_installed: DF.Percent per_returned: DF.Percent - pick_list: DF.Link | None plc_conversion_rate: DF.Float po_date: DF.Date | None po_no: DF.SmallText | None diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index c3933780203..f275779ecc2 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -243,6 +243,7 @@ "width": "100px" }, { + "default": "Now", "fieldname": "posting_time", "fieldtype": "Time", "label": "Posting Time", @@ -1290,7 +1291,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2025-04-09 16:52:19.323878", + "modified": "2025-08-04 19:18:47.754957", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", @@ -1360,4 +1361,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} 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 f6162ab558b..a453457f919 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 @@ -145,7 +145,7 @@ class SerialandBatchBundle(Document): ) elif not frappe.db.exists("Stock Ledger Entry", {"voucher_detail_no": self.voucher_detail_no}): - if self.voucher_type == "Delivery Note" and frappe.db.exists( + if self.voucher_type in ["Delivery Note", "Sales Invoice"] and frappe.db.exists( "Packed Item", self.voucher_detail_no ): return diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index f5fdfafe778..adec80dcab2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -218,6 +218,7 @@ "search_index": 1 }, { + "default": "Now", "fieldname": "posting_time", "fieldtype": "Time", "label": "Posting Time", @@ -697,7 +698,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-08-13 19:05:42.386955", + "modified": "2025-08-04 19:21:03.338958", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", @@ -763,6 +764,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "posting_date, from_warehouse, to_warehouse, purpose, remarks", "show_name_in_global_search": 1, "sort_field": "modified", @@ -770,4 +772,4 @@ "states": [], "title_field": "stock_entry_type", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index 9a854311100..4712b8aeb16 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -72,6 +72,7 @@ "reqd": 1 }, { + "default": "Now", "fieldname": "posting_time", "fieldtype": "Time", "in_list_view": 1, @@ -183,7 +184,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2022-05-11 09:10:26.327652", + "modified": "2025-08-04 19:21:20.179658", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", @@ -203,9 +204,10 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "posting_date", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 879b4836ac9..8d04141d78c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1287,12 +1287,12 @@ def get_conversion_factor(item_code, uom): if variant_of: filters["parent"] = ("in", (item_code, variant_of)) - conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor") + conversion_factor = frappe.get_all("UOM Conversion Detail", filters, pluck="conversion_factor") if not conversion_factor: stock_uom = frappe.db.get_value("Item", item_code, "stock_uom") - conversion_factor = get_uom_conv_factor(uom, stock_uom) + conversion_factor = [get_uom_conv_factor(uom, stock_uom) or 1] - return {"conversion_factor": conversion_factor or 1.0} + return {"conversion_factor": conversion_factor[-1]} @frappe.whitelist()