diff --git a/.github/workflows/label-base-on-title.yml b/.github/workflows/label-base-on-title.yml new file mode 100644 index 00000000000..4e811edf99a --- /dev/null +++ b/.github/workflows/label-base-on-title.yml @@ -0,0 +1,30 @@ +name: "Auto-label PRs based on title" + +on: + pull_request_target: + types: [opened, reopened] + +jobs: + add-label-if-prefix-matches: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Check PR title and add label if it matches prefixes + uses: actions/github-script@v7 + continue-on-error: true + with: + script: | + const title = context.payload.pull_request.title.toLowerCase(); + const prefixes = ['chore', 'ci', 'style', 'test', 'refactor']; + + // Check if the PR title starts with any of the prefixes + if (prefixes.some(prefix => title.startsWith(prefix))) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['skip-release-notes'] + }); + } diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 2768f321ff6..1c81f07cee3 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -116,6 +116,7 @@ def identify_is_group(child): return is_group +@frappe.whitelist() def get_chart(chart_template, existing_company=None): chart = {} if existing_company: diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 7f08653b15b..55ddfa698b7 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -159,9 +159,6 @@ def get_payment_entries_for_bank_clearance( as_dict=1, ) - if bank_account: - condition += "and bank_account = %(bank_account)s" - payment_entries = frappe.db.sql( f""" select @@ -183,7 +180,6 @@ def get_payment_entries_for_bank_clearance( "account": account, "from": from_date, "to": to_date, - "bank_account": bank_account, }, as_dict=1, ) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json index b438dbbe4ec..769fbbc427a 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.json +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json @@ -105,7 +105,8 @@ "label": "Cost Center", "oldfieldname": "cost_center", "oldfieldtype": "Link", - "options": "Cost Center" + "options": "Cost Center", + "search_index": 1 }, { "fieldname": "debit", @@ -358,7 +359,7 @@ "idx": 1, "in_create": 1, "links": [], - "modified": "2025-02-21 14:36:49.431166", + "modified": "2025-03-21 15:29:11.221890", "modified_by": "Administrator", "module": "Accounts", "name": "GL Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 9e1eaf25442..e8ac493d659 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -576,8 +576,22 @@ class JournalEntry(AccountsController): if customers: from erpnext.selling.doctype.customer.customer import check_credit_limit + customer_details = frappe._dict( + frappe.db.get_all( + "Customer Credit Limit", + filters={ + "parent": ["in", customers], + "parenttype": ["=", "Customer"], + "company": ["=", self.company], + }, + fields=["parent", "bypass_credit_limit_check"], + as_list=True, + ) + ) + for customer in customers: - check_credit_limit(customer, self.company) + ignore_outstanding_sales_order = bool(customer_details.get(customer)) + check_credit_limit(customer, self.company, ignore_outstanding_sales_order) def validate_cheque_info(self): if self.voucher_type in ["Bank Entry"]: diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index fe9e6e5f107..6c7b1ad5f5a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -7,6 +7,7 @@ from functools import reduce import frappe from frappe import ValidationError, _, qb, scrub, throw +from frappe.model.meta import get_field_precision from frappe.query_builder import Tuple from frappe.query_builder.functions import Count from frappe.utils import cint, comma_or, flt, getdate, nowdate @@ -37,7 +38,7 @@ from erpnext.accounts.general_ledger import ( make_reverse_gl_entries, process_gl_map, ) -from erpnext.accounts.party import get_party_account +from erpnext.accounts.party import complete_contact_details, get_party_account, set_contact_details from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, get_account_currency, @@ -439,6 +440,12 @@ class PaymentEntry(AccountsController): self.party_name = frappe.db.get_value(self.party_type, self.party, "name") if self.party: + if not self.contact_person: + set_contact_details( + self, party=frappe._dict({"name": self.party}), party_type=self.party_type + ) + else: + complete_contact_details(self) if not self.party_balance: self.party_balance = get_balance_on( party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company @@ -736,16 +743,39 @@ class PaymentEntry(AccountsController): outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding")) discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt")) + conversion_rate = frappe.db.get_value(key[2], {"name": key[1]}, "conversion_rate") + base_paid_amount_precision = get_field_precision( + frappe.get_meta("Payment Schedule").get_field("base_paid_amount") + ) + base_outstanding_precision = get_field_precision( + frappe.get_meta("Payment Schedule").get_field("base_outstanding") + ) + + base_paid_amount = flt( + (allocated_amount - discounted_amt) * conversion_rate, base_paid_amount_precision + ) + base_outstanding = flt(allocated_amount * conversion_rate, base_outstanding_precision) + if cancel: frappe.db.sql( """ UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s, + base_paid_amount = `base_paid_amount` - %s, discounted_amount = `discounted_amount` - %s, - outstanding = `outstanding` + %s + outstanding = `outstanding` + %s, + base_outstanding = `base_outstanding` - %s WHERE parent = %s and payment_term = %s""", - (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), + ( + allocated_amount - discounted_amt, + base_paid_amount, + discounted_amt, + allocated_amount, + base_outstanding, + key[1], + key[0], + ), ) else: if allocated_amount > outstanding: @@ -761,10 +791,20 @@ class PaymentEntry(AccountsController): UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s, + base_paid_amount = `base_paid_amount` + %s, discounted_amount = `discounted_amount` + %s, - outstanding = `outstanding` - %s + outstanding = `outstanding` - %s, + base_outstanding = `base_outstanding` - %s WHERE parent = %s and payment_term = %s""", - (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), + ( + allocated_amount - discounted_amt, + base_paid_amount, + discounted_amt, + allocated_amount, + base_outstanding, + key[1], + key[0], + ), ) def get_allocated_amount_in_transaction_currency( @@ -2879,7 +2919,7 @@ def get_payment_entry( pe.party_type = party_type pe.party = doc.get(scrub(party_type)) pe.contact_person = doc.get("contact_person") - pe.contact_email = doc.get("contact_email") + complete_contact_details(pe) pe.ensure_supplier_is_not_blocked() pe.paid_from = party_account if payment_type == "Receive" else bank.account diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index dde9980ce53..b72281b6314 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -24,7 +24,9 @@ "paid_amount", "discounted_amount", "column_break_3", - "base_payment_amount" + "base_payment_amount", + "base_outstanding", + "base_paid_amount" ], "fields": [ { @@ -155,19 +157,35 @@ "fieldtype": "Currency", "label": "Payment Amount (Company Currency)", "options": "Company:company:default_currency" + }, + { + "fieldname": "base_outstanding", + "fieldtype": "Currency", + "label": "Outstanding (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "depends_on": "base_paid_amount", + "fieldname": "base_paid_amount", + "fieldtype": "Currency", + "label": "Paid Amount (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-09-16 13:57:06.382859", + "modified": "2025-03-11 11:06:51.792982", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Schedule", "owner": "Administrator", "permissions": [], "quick_entry": 1, - "sort_field": "modified", + "row_format": "Dynamic", + "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.py b/erpnext/accounts/doctype/payment_schedule/payment_schedule.py index 8a292fd3aba..a3d1dbe5564 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.py +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.py @@ -14,6 +14,8 @@ class PaymentSchedule(Document): if TYPE_CHECKING: from frappe.types import DF + base_outstanding: DF.Currency + base_paid_amount: DF.Currency base_payment_amount: DF.Currency description: DF.SmallText | None discount: DF.Float 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 73977f5d560..800f1647471 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -8,6 +8,8 @@ from frappe import _, qb from frappe.model.document import Document from frappe.utils.data import comma_and +from erpnext.stock import get_warehouse_account_map + class RepostAccountingLedger(Document): # begin: auto-generated types @@ -97,6 +99,9 @@ class RepostAccountingLedger(Document): doc = frappe.get_doc(x.voucher_type, x.voucher_no) if doc.doctype in ["Payment Entry", "Journal Entry"]: gle_map = doc.build_gl_map() + elif doc.doctype == "Purchase Receipt": + warehouse_account_map = get_warehouse_account_map(doc.company) + gle_map = doc.get_gl_entries(warehouse_account_map) else: gle_map = doc.get_gl_entries() @@ -177,6 +182,14 @@ def start_repost(account_repost_doc=str) -> None: doc.force_set_against_expense_account() doc.make_gl_entries() + elif doc.doctype == "Purchase Receipt": + if not repost_doc.delete_cancelled_entries: + doc.docstatus = 2 + doc.make_gl_entries_on_cancel() + + doc.docstatus = 1 + doc.make_gl_entries(from_repost=True) + elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]: if not repost_doc.delete_cancelled_entries: doc.make_gl_entries(1) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py index 9f906bb7647..7ed999831cd 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -12,6 +12,8 @@ from erpnext.accounts.doctype.payment_request.payment_request import make_paymen from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.utils import get_fiscal_year +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): @@ -204,9 +206,81 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) + def test_06_repost_purchase_receipt(self): + from erpnext.accounts.doctype.account.test_account import create_account + + provisional_account = create_account( + account_name="Provision Account", + parent_account="Current Liabilities - _TC", + company=self.company, + ) + + another_provisional_account = create_account( + account_name="Another Provision Account", + parent_account="Current Liabilities - _TC", + company=self.company, + ) + + company = frappe.get_doc("Company", self.company) + company.enable_provisional_accounting_for_non_stock_items = 1 + company.default_provisional_account = provisional_account + company.save() + + test_cc = company.cost_center + default_expense_account = company.default_expense_account + + item = make_item(properties={"is_stock_item": 0}) + + pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0) + pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) + expected_pr_gles = [ + {"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, + {"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc}, + ] + self.assertEqual(expected_pr_gles, pr_gl_entries) + + # change the provisional account + frappe.db.set_value( + "Purchase Receipt Item", + pr.items[0].name, + "provisional_expense_account", + another_provisional_account, + ) + + repost_doc = frappe.new_doc("Repost Accounting Ledger") + repost_doc.company = self.company + repost_doc.delete_cancelled_entries = True + repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name}) + repost_doc.save().submit() + + pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) + expected_pr_gles_after_repost = [ + {"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc}, + {"account": another_provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, + ] + self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost)) + self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost) + + # teardown + repost_doc.cancel() + repost_doc.delete() + + pr.reload() + pr.cancel() + + company.enable_provisional_accounting_for_non_stock_items = 0 + company.default_provisional_account = None + company.save() + def update_repost_settings(): - allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"] + allowed_types = [ + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Journal Entry", + "Purchase Receipt", + ] repost_settings = frappe.get_doc("Repost Accounting Ledger Settings") for x in allowed_types: repost_settings.append("allowed_types", {"document_type": x, "allowed": True}) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f428eb50a3c..fe8342d78bd 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1819,17 +1819,6 @@ class TestSalesInvoice(FrappeTestCase): for field in expected_gle: self.assertEqual(expected_gle[field], gle[field]) - def test_invoice_exchange_rate(self): - si = create_sales_invoice( - customer="_Test Customer USD", - debit_to="_Test Receivable USD - _TC", - currency="USD", - conversion_rate=1, - do_not_save=1, - ) - - self.assertRaises(frappe.ValidationError, si.save) - def test_invalid_currency(self): # Customer currency = USD diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 959e3429095..37b5b884234 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -279,9 +279,7 @@ def get_regional_address_details(party_details, doctype, company): pass -def set_contact_details(party_details, party, party_type): - party_details.contact_person = get_default_contact(party_type, party.name) - +def complete_contact_details(party_details): if not party_details.contact_person: party_details.update( { @@ -310,6 +308,11 @@ def set_contact_details(party_details, party, party_type): party_details.update(contact_details) +def set_contact_details(party_details, party, party_type): + party_details.contact_person = get_default_contact(party_type, party.name) + complete_contact_details(party_details) + + def set_other_values(party_details, party, party_type): # copy if party_type == "Customer": diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 5a140ec3f2d..9625a86f05f 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -517,7 +517,7 @@ class ReceivablePayableReport: select si.name, si.party_account_currency, si.currency, si.conversion_rate, si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount, - ps.description, ps.paid_amount, ps.discounted_amount + ps.description, ps.paid_amount, ps.base_paid_amount, ps.discounted_amount from `tab{row.voucher_type}` si, `tabPayment Schedule` ps where si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and @@ -540,20 +540,24 @@ class ReceivablePayableReport: # Deduct that from paid amount pre allocation row.paid -= flt(payment_terms_details[0].total_advance) + company_currency = frappe.get_value("Company", self.filters.get("company"), "default_currency") + # If single payment terms, no need to split the row if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term: - self.append_payment_term(row, payment_terms_details[0], original_row) + self.append_payment_term(row, payment_terms_details[0], original_row, company_currency) return for d in payment_terms_details: term = frappe._dict(original_row) - self.append_payment_term(row, d, term) + self.append_payment_term(row, d, term, company_currency) - def append_payment_term(self, row, d, term): - if d.currency == d.party_account_currency: + def append_payment_term(self, row, d, term, company_currency): + invoiced = d.base_payment_amount + paid_amount = d.base_paid_amount + + if company_currency == d.party_account_currency or self.filters.get("in_party_currency"): invoiced = d.payment_amount - else: - invoiced = d.base_payment_amount + paid_amount = d.paid_amount row.payment_terms.append( term.update( @@ -562,15 +566,15 @@ class ReceivablePayableReport: "invoiced": invoiced, "invoice_grand_total": row.invoiced, "payment_term": d.description or d.payment_term, - "paid": d.paid_amount + d.discounted_amount, + "paid": paid_amount + d.discounted_amount, "credit_note": 0.0, - "outstanding": invoiced - d.paid_amount - d.discounted_amount, + "outstanding": invoiced - paid_amount - d.discounted_amount, } ) ) - if d.paid_amount: - row["paid"] -= d.paid_amount + d.discounted_amount + if paid_amount: + row["paid"] -= paid_amount + d.discounted_amount def allocate_closing_to_term(self, row, term, key): if row[key]: diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index 5229839bec6..cdeddf3d38b 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -145,6 +145,130 @@ def get_asset_categories_for_grouped_by_category(filters): ) +def get_assets_for_grouped_by_category(filters): + condition = "" + if filters.get("asset_category"): + condition = f" and a.asset_category = '{filters.get('asset_category')}'" + finance_book_filter = "" + if filters.get("finance_book"): + finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + + # nosemgrep + return frappe.db.sql( + f""" + SELECT results.asset_category, + sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, + sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal, + sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, + sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period + from (SELECT a.asset_category, + ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then + gle.debit + else + 0 + end), 0) as accumulated_depreciation_as_on_from_date, + ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then + gle.credit + else + 0 + end), 0) as depreciation_eliminated_via_reversal, + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s + and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then + gle.debit + else + 0 + end), 0) as depreciation_eliminated_during_the_period, + ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s + and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then + gle.debit + else + 0 + end), 0) as depreciation_amount_during_the_period + from `tabGL Entry` gle + join `tabAsset` a on + gle.against_voucher = a.name + join `tabAsset Category Account` aca on + aca.parent = a.asset_category and aca.company_name = %(company)s + join `tabCompany` company on + company.name = %(company)s + where + a.docstatus=1 + and a.company=%(company)s + and a.purchase_date <= %(to_date)s + and gle.is_cancelled = 0 + and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + {condition} {finance_book_filter} + group by a.asset_category + union + SELECT a.asset_category, + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then + 0 + else + a.opening_accumulated_depreciation + end), 0) as accumulated_depreciation_as_on_from_date, + 0 as depreciation_eliminated_via_reversal, + ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then + a.opening_accumulated_depreciation + else + 0 + end), 0) as depreciation_eliminated_during_the_period, + 0 as depreciation_amount_during_the_period + from `tabAsset` a + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} + group by a.asset_category) as results + group by results.asset_category + """, + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "finance_book": filters.get("finance_book", ""), + }, + as_dict=1, + ) + + +def get_group_by_asset_data(filters): + data = [] + + asset_details = get_asset_details_for_grouped_by_category(filters) + assets = get_assets_for_grouped_by_asset(filters) + + for asset_detail in asset_details: + row = frappe._dict() + row.update(asset_detail) + + row.value_as_on_to_date = ( + flt(row.value_as_on_from_date) + + flt(row.value_of_new_purchase) + - flt(row.value_of_sold_asset) + - flt(row.value_of_scrapped_asset) + - flt(row.value_of_capitalized_asset) + ) + + row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", ""))) + + row.accumulated_depreciation_as_on_to_date = ( + flt(row.accumulated_depreciation_as_on_from_date) + + flt(row.depreciation_amount_during_the_period) + - flt(row.depreciation_eliminated_during_the_period) + - flt(row.depreciation_eliminated_via_reversal) + ) + + row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt( + row.accumulated_depreciation_as_on_from_date + ) + + row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt( + row.accumulated_depreciation_as_on_to_date + ) + + data.append(row) + + return data + + def get_asset_details_for_grouped_by_category(filters): condition = "" if filters.get("asset"): @@ -224,130 +348,6 @@ def get_asset_details_for_grouped_by_category(filters): ) -def get_group_by_asset_data(filters): - data = [] - - asset_details = get_asset_details_for_grouped_by_category(filters) - assets = get_assets_for_grouped_by_asset(filters) - - for asset_detail in asset_details: - row = frappe._dict() - row.update(asset_detail) - - row.value_as_on_to_date = ( - flt(row.value_as_on_from_date) - + flt(row.value_of_new_purchase) - - flt(row.value_of_sold_asset) - - flt(row.value_of_scrapped_asset) - - flt(row.value_of_capitalized_asset) - ) - - row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", ""))) - - row.accumulated_depreciation_as_on_to_date = ( - flt(row.accumulated_depreciation_as_on_from_date) - + flt(row.depreciation_amount_during_the_period) - - flt(row.depreciation_eliminated_during_the_period) - - flt(row.depreciation_eliminated_via_reversal) - ) - - row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt( - row.accumulated_depreciation_as_on_from_date - ) - - row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt( - row.accumulated_depreciation_as_on_to_date - ) - - data.append(row) - - return data - - -def get_assets_for_grouped_by_category(filters): - condition = "" - if filters.get("asset_category"): - condition = f" and a.asset_category = '{filters.get('asset_category')}'" - finance_book_filter = "" - if filters.get("finance_book"): - finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" - condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" - - # nosemgrep - return frappe.db.sql( - f""" - SELECT results.asset_category, - sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, - sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal, - sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, - sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period - from (SELECT a.asset_category, - ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then - gle.debit - else - 0 - end), 0) as accumulated_depreciation_as_on_from_date, - ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then - gle.credit - else - 0 - end), 0) as depreciation_eliminated_via_reversal, - ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s - and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then - gle.debit - else - 0 - end), 0) as depreciation_eliminated_during_the_period, - ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s - and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then - gle.debit - else - 0 - end), 0) as depreciation_amount_during_the_period - from `tabGL Entry` gle - join `tabAsset` a on - gle.against_voucher = a.name - join `tabAsset Category Account` aca on - aca.parent = a.asset_category and aca.company_name = %(company)s - join `tabCompany` company on - company.name = %(company)s - where - a.docstatus=1 - and a.company=%(company)s - and a.purchase_date <= %(to_date)s - and gle.is_cancelled = 0 - and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) - {condition} {finance_book_filter} - group by a.asset_category - union - SELECT a.asset_category, - ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then - 0 - else - a.opening_accumulated_depreciation - end), 0) as accumulated_depreciation_as_on_from_date, - 0 as depreciation_eliminated_via_reversal, - ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then - a.opening_accumulated_depreciation - else - 0 - end), 0) as depreciation_eliminated_during_the_period, - 0 as depreciation_amount_during_the_period - from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} - group by a.asset_category) as results - group by results.asset_category - """, - { - "to_date": filters.to_date, - "from_date": filters.from_date, - "company": filters.company, - "finance_book": filters.get("finance_book", ""), - }, - as_dict=1, - ) - - def get_assets_for_grouped_by_asset(filters): condition = "" if filters.get("asset"): @@ -405,7 +405,7 @@ def get_assets_for_grouped_by_asset(filters): group by a.name union SELECT a.name as name, - ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then 0 else a.opening_accumulated_depreciation diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2758ff0e26f..ac37775f45f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -9,8 +9,8 @@ import frappe import frappe.defaults from frappe import _, qb, throw from frappe.model.meta import get_field_precision -from frappe.query_builder import AliasedQuery, Criterion, Table -from frappe.query_builder.functions import Count, Sum +from frappe.query_builder import AliasedQuery, Case, Criterion, Table +from frappe.query_builder.functions import Count, Max, Sum from frappe.query_builder.utils import DocType from frappe.utils import ( add_days, @@ -1974,6 +1974,15 @@ class QueryPaymentLedger: .select( ple.against_voucher_no.as_("voucher_no"), Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"), + Max( + Case().when( + ( + (ple.voucher_no == ple.against_voucher_no) + & (ple.voucher_type == ple.against_voucher_type) + ), + (ple.posting_date), + ) + ).as_("invoice_date"), ) .where(ple.delinked == 0) .where(Criterion.all(filter_on_against_voucher_no)) @@ -1981,7 +1990,7 @@ class QueryPaymentLedger: .where(Criterion.all(self.dimensions_filter)) .where(Criterion.all(self.voucher_posting_date)) .groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party) - .orderby(ple.posting_date, ple.voucher_no) + .orderby(ple.invoice_date, ple.voucher_no) .having(qb.Field("amount_in_account_currency") > 0) .limit(self.limit) .run() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index eb483bdb868..e0d7e2a36df 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2363,6 +2363,9 @@ class AccountsController(TransactionBase): base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount") ) d.outstanding = d.payment_amount + d.base_outstanding = flt( + d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding") + ) elif not d.invoice_portion: d.base_payment_amount = flt( d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount") @@ -2689,12 +2692,17 @@ class AccountsController(TransactionBase): default_currency = erpnext.get_company_currency(self.company) if not default_currency: throw(_("Please enter default currency in Company Master")) - if ( - (self.currency == default_currency and flt(self.conversion_rate) != 1.00) - or not self.conversion_rate - or (self.currency != default_currency and flt(self.conversion_rate) == 1.00) - ): - throw(_("Conversion rate cannot be 0 or 1")) + + if not self.conversion_rate: + throw(_("Conversion rate cannot be 0")) + + if self.currency == default_currency and flt(self.conversion_rate) != 1.00: + throw(_("Conversion rate must be 1.00 if document currency is same as company currency")) + + if self.currency != default_currency and flt(self.conversion_rate) == 1.00: + frappe.msgprint( + _("Conversion rate is 1.00, but document currency is different from company currency") + ) def check_finance_books(self, item, asset): if ( diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index c5708d0e4b4..4d141d346eb 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -69,7 +69,7 @@ def get_transaction_list( filters=None, limit_start=0, limit_page_length=20, - order_by="modified", + order_by="creation desc", custom=False, ): user = frappe.session.user @@ -115,7 +115,7 @@ def get_transaction_list( limit_page_length, fields="name", ignore_permissions=ignore_permissions, - order_by="modified desc", + order_by=order_by, ) if custom: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 835ee5b6133..300ae9b8fec 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -397,7 +397,8 @@ erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment erpnext.patches.v14_0.update_posting_datetime erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes erpnext.patches.v15_0.update_query_report -erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference #2025-03-18 -erpnext.patches.v15_0.recalculate_amount_difference_field +erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference +erpnext.patches.v15_0.recalculate_amount_difference_field #2025-03-18 erpnext.patches.v15_0.rename_sla_fields #2025-03-12 erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item +erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices diff --git a/erpnext/patches/v15_0/update_payment_schedule_fields_in_invoices.py b/erpnext/patches/v15_0/update_payment_schedule_fields_in_invoices.py new file mode 100644 index 00000000000..74d8ac38f72 --- /dev/null +++ b/erpnext/patches/v15_0/update_payment_schedule_fields_in_invoices.py @@ -0,0 +1,18 @@ +import frappe +from frappe.query_builder import DocType + + +def execute(): + invoice_types = ["Sales Invoice", "Purchase Invoice"] + for invoice_type in invoice_types: + invoice = DocType(invoice_type) + invoice_details = frappe.qb.from_(invoice).select(invoice.conversion_rate, invoice.name) + update_payment_schedule(invoice_details) + + +def update_payment_schedule(invoice_details): + ps = DocType("Payment Schedule") + + frappe.qb.update(ps).join(invoice_details).on(ps.parent == invoice_details.name).set( + ps.base_paid_amount, ps.paid_amount * invoice_details.conversion_rate + ).set(ps.base_outstanding, ps.outstanding * invoice_details.conversion_rate).run() diff --git a/erpnext/projects/doctype/project_user/project_user.json b/erpnext/projects/doctype/project_user/project_user.json index 2f452cc2d75..e82bbfab7bf 100644 --- a/erpnext/projects/doctype/project_user/project_user.json +++ b/erpnext/projects/doctype/project_user/project_user.json @@ -12,6 +12,7 @@ "full_name", "welcome_email_sent", "view_attachments", + "hide_timesheets", "section_break_5", "project_status" ], @@ -64,6 +65,13 @@ "in_list_view": 1, "label": "View attachments" }, + { + "columns": 2, + "default": "0", + "fieldname": "hide_timesheets", + "fieldtype": "Check", + "label": "Hide timesheets" + }, { "fieldname": "section_break_5", "fieldtype": "Section Break" diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index ca2bed20c7f..bbb2a88e629 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -447,22 +447,21 @@ erpnext.sales_common = { args: { project: this.frm.doc.project }, callback: function (r, rt) { if (!r.exc) { - $.each(me.frm.doc["items"] || [], function (i, row) { - if (r.message) { + if (r.message) { + $.each(me.frm.doc["items"] || [], function (i, row) { frappe.model.set_value( row.doctype, row.name, "cost_center", r.message ); - frappe.msgprint( - __( - "Cost Center For Item with Item Code {0} has been Changed to {1}", - [row.item_name, r.message] - ) - ); - } - }); + }); + frappe.msgprint( + __("Cost Center for Item rows has been updated to {0}", [ + r.message, + ]) + ); + } } }, }); diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html index 3b8698f4ab2..d88088c9819 100644 --- a/erpnext/templates/pages/projects.html +++ b/erpnext/templates/pages/projects.html @@ -56,8 +56,8 @@ {{ empty_state(_("Task")) }} {% endif %} -

{{ _("Timesheets") }}

{% if doc.timesheets %} +

{{ _("Timesheets") }}

@@ -73,8 +73,6 @@ {% include "erpnext/templates/includes/projects/project_timesheets.html" %}
- {% else %} - {{ empty_state(_("Timesheet")) }} {% endif %} {% if doc.attachments %} @@ -136,4 +134,4 @@
-{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index 787c7c0069b..446437bbb32 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -9,7 +9,7 @@ def get_context(context): project_user = frappe.db.get_value( "Project User", {"parent": frappe.form_dict.project, "user": frappe.session.user}, - ["user", "view_attachments"], + ["user", "view_attachments", "hide_timesheets"], as_dict=True, ) if frappe.session.user != "Administrator" and (not project_user or frappe.session.user == "Guest"): @@ -25,7 +25,8 @@ def get_context(context): project.name, start=0, item_status="open", search=frappe.form_dict.get("search") ) - project.timesheets = get_timesheets(project.name, start=0, search=frappe.form_dict.get("search")) + if project_user and not project_user.hide_timesheets: + project.timesheets = get_timesheets(project.name, start=0, search=frappe.form_dict.get("search")) if project_user and project_user.view_attachments: project.attachments = get_attachments(project.name)