diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 9ffdf186f02..a48ce9b4c63 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -360,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): ) if not amount: - return - - gl_posting_date = end_date - prev_posting_date = None - # check if books nor frozen till endate: - if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto): - gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1)) prev_posting_date = end_date - - if via_journal_entry: - book_revenue_via_journal_entry( - doc, - credit_account, - debit_account, - amount, - base_amount, - gl_posting_date, - project, - account_currency, - item.cost_center, - item, - deferred_process, - submit_journal_entry, - ) else: - make_gl_entries( - doc, - credit_account, - debit_account, - against, - amount, - base_amount, - gl_posting_date, - project, - account_currency, - item.cost_center, - item, - deferred_process, - ) + gl_posting_date = end_date + prev_posting_date = None + # check if books nor frozen till endate: + if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto): + gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1)) + prev_posting_date = end_date + + if via_journal_entry: + book_revenue_via_journal_entry( + doc, + credit_account, + debit_account, + amount, + base_amount, + gl_posting_date, + project, + account_currency, + item.cost_center, + item, + deferred_process, + submit_journal_entry, + ) + else: + make_gl_entries( + doc, + credit_account, + debit_account, + against, + amount, + base_amount, + gl_posting_date, + project, + account_currency, + item.cost_center, + item, + deferred_process, + ) # Returned in case of any errors because it tries to submit the same record again and again in case of errors if frappe.flags.deferred_accounting_error: diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json index 93fc4439d35..2969161949f 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json @@ -26,6 +26,7 @@ { "fieldname": "company", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Company", "options": "Company" }, @@ -118,7 +119,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-03-07 11:02:24.535714", + "modified": "2024-04-28 14:40:50.910884", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Reconciliation Tool", @@ -139,4 +140,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} 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 77cb3201dbc..05787665a76 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -719,7 +719,7 @@ def get_pe_matching_query( (ref_rank + amount_rank + party_rank + 1).as_("rank"), ConstantColumn("Payment Entry").as_("doctype"), pe.name, - pe.paid_amount, + pe.paid_amount_after_tax.as_("paid_amount"), pe.reference_no, pe.reference_date, pe.party, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 26819ef7518..2fb62b79ab1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -76,6 +76,7 @@ class PaymentEntry(AccountsController): self.setup_party_account_field() self.set_missing_values() self.set_liability_account() + self.validate_advance_account_currency() self.set_missing_ref_details(force=True) self.validate_payment_type() self.validate_party_details() @@ -158,6 +159,22 @@ class PaymentEntry(AccountsController): alert=True, ) + def validate_advance_account_currency(self): + if self.book_advance_payments_in_separate_party_account is True: + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + if self.payment_type == "Receive" and self.paid_from_account_currency != company_currency: + frappe.throw( + _("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format( + frappe.bold(self.paid_from), frappe.bold(self.paid_from_account_currency) + ) + ) + if self.payment_type == "Pay" and self.paid_to_account_currency != company_currency: + frappe.throw( + _("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format( + frappe.bold(self.paid_to), frappe.bold(self.paid_to_account_currency) + ) + ) + def on_cancel(self): self.ignore_linked_doctypes = ( "GL Entry", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index cc99fe7b583..c5b815b5e61 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -176,8 +176,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo }, callback: (r) => { if (!r.exc && r.message) { - this.frm.set_value("receivable_payable_account", r.message[0]); - this.frm.set_value("default_advance_account", r.message[1]); + if (typeof r.message === "string") { + this.frm.set_value("receivable_payable_account", r.message); + } else if (Array.isArray(r.message)) { + this.frm.set_value("receivable_payable_account", r.message[0]); + this.frm.set_value("default_advance_account", r.message[1]); + } } this.frm.refresh(); }, diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 1848bafe800..ee9a2137d55 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -573,6 +573,22 @@ def apply_price_discount_rule(pricing_rule, item_details, args): if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"): # Apply discount on discounted rate item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100) + elif args.price_list_rate: + value = pricing_rule.get(field, 0) + calculate_discount_percentage = False + if field == "discount_percentage": + field = "discount_amount" + value = args.price_list_rate * (value / 100) + calculate_discount_percentage = True + + if field not in item_details: + item_details.setdefault(field, 0) + + item_details[field] += value if pricing_rule else args.get(field, 0) + if calculate_discount_percentage and args.price_list_rate and item_details.discount_amount: + item_details.discount_percentage = flt( + (flt(item_details.discount_amount) / flt(args.price_list_rate)) * 100 + ) else: if field not in item_details: item_details.setdefault(field, 0) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 046a5995d9a..676ed4c2ad5 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -1104,6 +1104,59 @@ class TestPricingRule(unittest.TestCase): self.assertEqual(so.items[1].item_code, "_Test Item") self.assertEqual(so.items[1].qty, 4) + def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self): + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule 1", + "name": "_Test Pricing Rule 1", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "price_or_product_discount": "Price", + "rate_or_discount": "Discount Percentage", + "discount_percentage": 10, + "apply_multiple_pricing_rules": 1, + "company": "_Test Company", + } + + frappe.get_doc(test_record.copy()).insert() + + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule 2", + "name": "_Test Pricing Rule 2", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "price_or_product_discount": "Price", + "rate_or_discount": "Discount Amount", + "discount_amount": 100, + "apply_multiple_pricing_rules": 1, + "company": "_Test Company", + } + + frappe.get_doc(test_record.copy()).insert() + + so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True) + self.assertEqual(so.items[0].discount_amount, 200) + self.assertEqual(so.items[0].rate, 800) + + frappe.delete_doc_if_exists("Sales Order", so.name) + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") + test_dependencies = ["Campaign"] diff --git a/erpnext/accounts/doctype/tax_withholding_account/tax_withholding_account.json b/erpnext/accounts/doctype/tax_withholding_account/tax_withholding_account.json index 06d6b088e5b..8603584b7a8 100644 --- a/erpnext/accounts/doctype/tax_withholding_account/tax_withholding_account.json +++ b/erpnext/accounts/doctype/tax_withholding_account/tax_withholding_account.json @@ -21,7 +21,7 @@ "fieldname": "company", "fieldtype": "Link", "hidden": 0, - "ignore_user_permissions": 0, + "ignore_user_permissions": 1, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, @@ -53,7 +53,7 @@ "fieldname": "account", "fieldtype": "Link", "hidden": 0, - "ignore_user_permissions": 0, + "ignore_user_permissions": 1, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, @@ -87,7 +87,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-04-13 18:44:25.055382", + "modified": "2024-04-30 10:26:48.21829", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withholding Account", diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 2a2428a6c1b..a9a4090a02c 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -282,6 +282,14 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if taxable_vouchers: tax_deducted = get_deducted_tax(taxable_vouchers, tax_details) + # If advance is outside the current tax withholding period (usually a fiscal year), `get_deducted_tax` won't fetch it. + # updating `tax_deducted` with correct advance tax value (from current and previous previous withholding periods), will allow the + # rest of the below logic to function properly + # ---FY 2023-------------||---------------------FY 2024-----------------------||-- + # ---Advance-------------||---------Inv_1--------Inv_2------------------------||-- + if tax_deducted_on_advances: + tax_deducted += get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details) + tax_amount = 0 if party_type == "Supplier": @@ -418,7 +426,7 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details): frappe.qb.from_(at) .inner_join(pe) .on(pe.name == at.parent) - .select(at.parent, at.name, at.tax_amount, at.allocated_amount) + .select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount) .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category")) .where(at.parent.isin(advances)) .where(at.account_head == tax_details.account_head) @@ -443,6 +451,16 @@ def get_deducted_tax(taxable_vouchers, tax_details): return sum(entries) +def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details): + """ + Only applies for Taxes deducted on Advance Payments + """ + advance_tax_from_across_fiscal_year = sum( + [adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date] + ) + return advance_tax_from_across_fiscal_year + + def get_tds_amount(ldc, parties, inv, tax_details, vouchers): tds_amount = 0 invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 087e0d0ff6f..1e3939d98a4 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -1,18 +1,22 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import datetime import unittest import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields -from frappe.utils import today +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, today +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.utils import get_fiscal_year +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice test_dependencies = ["Supplier Group", "Customer Group"] -class TestTaxWithholdingCategory(unittest.TestCase): +class TestTaxWithholdingCategory(FrappeTestCase): @classmethod def setUpClass(self): # create relevant supplier, etc @@ -21,7 +25,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): make_pan_no_field() def tearDown(self): - cancel_invoices() + frappe.db.rollback() def test_cumulative_threshold_tds(self): frappe.db.set_value( @@ -317,8 +321,6 @@ class TestTaxWithholdingCategory(unittest.TestCase): d.cancel() def test_tds_deduction_for_po_via_payment_entry(self): - from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - frappe.db.set_value( "Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS" ) @@ -485,6 +487,133 @@ class TestTaxWithholdingCategory(unittest.TestCase): pi2.cancel() pi3.cancel() + def set_previous_fy_and_tax_category(self): + test_company = "_Test Company" + category = "Cumulative Threshold TDS" + + def add_company_to_fy(fy, company): + if not [x.company for x in fy.companies if x.company == company]: + fy.append("companies", {"company": company}) + fy.save() + + # setup previous fiscal year + fiscal_year = get_fiscal_year(today(), company=test_company) + if prev_fiscal_year := get_fiscal_year(add_days(fiscal_year[1], -10)): + self.prev_fy = frappe.get_doc("Fiscal Year", prev_fiscal_year[0]) + add_company_to_fy(self.prev_fy, test_company) + else: + # make previous fiscal year + start = datetime.date(fiscal_year[1].year - 1, fiscal_year[1].month, fiscal_year[1].day) + end = datetime.date(fiscal_year[2].year - 1, fiscal_year[2].month, fiscal_year[2].day) + self.prev_fy = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year_start_date": start, + "year_end_date": end, + "companies": [{"company": test_company}], + } + ) + self.prev_fy.save() + + # setup tax withholding category for previous fiscal year + cat = frappe.get_doc("Tax Withholding Category", category) + cat.append( + "rates", + { + "from_date": self.prev_fy.year_start_date, + "to_date": self.prev_fy.year_end_date, + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 30000, + }, + ) + cat.save() + + def test_tds_across_fiscal_year(self): + """ + Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year + --||-----FY 2023-----||-----FY 2024-----||-- + --||-----Advance-----||---Inv1---Inv2---||-- + """ + self.set_previous_fy_and_tax_category() + supplier = "Test TDS Supplier" + # Cumulative threshold 30000 and tax rate 10% + category = "Cumulative Threshold TDS" + frappe.db.set_value( + "Supplier", + supplier, + { + "tax_withholding_category": category, + "pan": "ABCTY1234D", + }, + ) + po_and_advance_posting_date = add_days(self.prev_fy.year_end_date, -10) + po = create_purchase_order(supplier=supplier, qty=10, rate=10000) + po.transaction_date = po_and_advance_posting_date + po.taxes = [] + po.apply_tds = False + po.tax_withholding_category = None + po.save().submit() + + # Partial advance + payment = get_payment_entry(po.doctype, po.name) + payment.posting_date = po_and_advance_posting_date + payment.paid_amount = 60000 + payment.apply_tax_withholding_amount = 1 + payment.tax_withholding_category = category + payment.references = [] + payment.taxes = [] + payment.save().submit() + + self.assertEqual(len(payment.taxes), 1) + self.assertEqual(payment.taxes[0].tax_amount, 6000) + + # Multiple partial invoices + payment.reload() + pi1 = make_purchase_invoice(source_name=po.name) + pi1.apply_tds = True + pi1.tax_withholding_category = category + pi1.items[0].qty = 3 + pi1.items[0].rate = 10000 + advances = pi1.get_advance_entries() + pi1.append( + "advances", + { + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "advance_amount": advances[0].amount, + "allocated_amount": 30000, + }, + ) + pi1.save().submit() + pi1.reload() + payment.reload() + self.assertEqual(pi1.taxes, []) + self.assertEqual(payment.taxes[0].tax_amount, 6000) + self.assertEqual(payment.taxes[0].allocated_amount, 3000) + + pi2 = make_purchase_invoice(source_name=po.name) + pi2.apply_tds = True + pi2.tax_withholding_category = category + pi2.items[0].qty = 3 + pi2.items[0].rate = 10000 + advances = pi2.get_advance_entries() + pi2.append( + "advances", + { + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "advance_amount": advances[0].amount, + "allocated_amount": 30000, + }, + ) + pi2.save().submit() + pi2.reload() + payment.reload() + self.assertEqual(pi2.taxes, []) + self.assertEqual(payment.taxes[0].tax_amount, 6000) + self.assertEqual(payment.taxes[0].allocated_amount, 6000) + def cancel_invoices(): purchase_invoices = frappe.get_all( diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index af1e1c79eb2..2bd493cd4d0 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -501,8 +501,9 @@ class ReceivablePayableReport: # Deduct that from paid amount pre allocation row.paid -= flt(payment_terms_details[0].total_advance) - # If no or single payment terms, no need to split the row - if len(payment_terms_details) <= 1: + # 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) return for d in payment_terms_details: diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index 8999ef710f0..c6d9eac5966 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -58,9 +58,9 @@ class Deferred_Item: For a given GL/Journal posting, get balance based on item type """ if self.type == "Deferred Sale Item": - return entry.debit - entry.credit + return flt(entry.debit) - flt(entry.credit) elif self.type == "Deferred Purchase Item": - return -(entry.credit - entry.debit) + return -(flt(entry.credit) - flt(entry.debit)) return 0 def get_item_total(self): @@ -147,7 +147,7 @@ class Deferred_Item: actual = 0 for posting in self.gle_entries: # if period.from_date <= posting.posting_date <= period.to_date: - if period.from_date <= posting.gle_posting_date <= period.to_date: + if period.from_date <= getdate(posting.gle_posting_date) <= period.to_date: period_sum += self.get_amount(posting) if posting.posted == "posted": actual += self.get_amount(posting) @@ -285,7 +285,7 @@ class Deferred_Revenue_and_Expense_Report: qb.from_(inv_item) .join(inv) .on(inv.name == inv_item.parent) - .join(gle) + .left_join(gle) .on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account)) .select( inv.name.as_("doc"), diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index f8a965b699c..4ca65dc04e5 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin): {"key": "aug_2021", "total": 0, "actual": 0}, ] self.assertEqual(report.period_total, expected) + + @change_settings( + "Accounts Settings", + {"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0}, + ) + def test_zero_amount(self): + self.create_item("_Test Office Desk", 0, self.warehouse, self.company) + item = frappe.get_doc("Item", self.item) + item.enable_deferred_expense = 1 + item.item_defaults[0].deferred_expense_account = self.deferred_expense_account + item.no_of_months_exp = 12 + item.save() + + pi = make_purchase_invoice( + item=self.item, + company=self.company, + supplier=self.supplier, + is_return=False, + update_stock=False, + posting_date=frappe.utils.datetime.date(2021, 12, 30), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + do_not_save=True, + rate=3910, + price_list_rate=3910, + warehouse=self.warehouse, + qty=1, + ) + pi.set_posting_time = True + pi.items[0].enable_deferred_expense = 1 + pi.items[0].service_start_date = "2021-12-30" + pi.items[0].service_end_date = "2022-12-30" + pi.items[0].deferred_expense_account = self.deferred_expense_account + pi.items[0].expense_account = self.expense_account + pi.save() + pi.submit() + + pda = frappe.get_doc( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2022-01-01", + end_date="2022-01-31", + type="Expense", + company=self.company, + ) + pda.insert() + pda.submit() + + # execute report + fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31")) + self.filters = frappe._dict( + { + "company": self.company, + "filter_based_on": "Date Range", + "period_start_date": "2022-01-01", + "period_end_date": "2022-01-31", + "from_fiscal_year": fiscal_year.year, + "to_fiscal_year": fiscal_year.year, + "periodicity": "Monthly", + "type": "Expense", + "with_upcoming_postings": False, + } + ) + + report = Deferred_Revenue_and_Expense_Report(filters=self.filters) + report.run() + + # fetch the invoice from deferred invoices list + inv = [d for d in report.deferred_invoices if d.name == pi.name] + # make sure the list isn't empty + self.assertTrue(inv) + # calculate the total deferred expense for the period + inv = inv[0].calculate_invoice_revenue_expense_for_period() + deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))]) + # make sure the total deferred expense is greater than 0 + self.assertLess(deferred_exp, 0) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 04ea2cb8530..bbb8900de39 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -516,6 +516,10 @@ def reconcile_against_document( doc.make_advance_gl_entries() else: gl_map = doc.build_gl_map() + # Make sure there is no overallocation + from erpnext.accounts.general_ledger import process_debit_credit_difference + + process_debit_credit_difference(gl_map) create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) # Only update outstanding for newly linked vouchers @@ -1094,7 +1098,7 @@ def get_companies(): def get_children(doctype, parent, company, is_root=False): from erpnext.accounts.report.financial_statements import sort_accounts - parent_fieldname = "parent_" + doctype.lower().replace(" ", "_") + parent_fieldname = "parent_" + frappe.scrub(doctype) fields = ["name as value", "is_group as expandable"] filters = [["docstatus", "<", 2]] diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 219a1d68c52..49252bd5b54 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1227,8 +1227,8 @@ def get_accounting_ledger_preview(doc, filters): "debit", "credit", "against", - "party", "party_type", + "party", "cost_center", "against_voucher_type", "against_voucher", @@ -1404,7 +1404,12 @@ def is_reposting_pending(): ) -def future_sle_exists(args, sl_entries=None): +def future_sle_exists(args, sl_entries=None, allow_force_reposting=True): + if allow_force_reposting and frappe.db.get_single_value( + "Stock Reposting Settings", "do_reposting_for_each_stock_transaction" + ): + return True + key = (args.voucher_type, args.voucher_no) if not hasattr(frappe.local, "future_sle"): frappe.local.future_sle = {} diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 17cf0e461d0..f8fce7ab697 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -121,7 +121,7 @@ def send_mail(entry, email_campaign): doctype="Email Campaign", name=email_campaign.name, subject=frappe.render_template(email_template.get("subject"), context), - content=frappe.render_template(email_template.get("response"), context), + content=frappe.render_template(email_template.response_, context), sender=sender, recipients=recipient_list, communication_medium="Email", diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 5bc4ffe1bff..0b482c3292f 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -373,6 +373,7 @@ erpnext.sales_common = { frappe.model.set_value(item.doctype, item.name, { serial_and_batch_bundle: r.name, use_serial_batch_fields: 0, + incoming_rate: r.avg_rate, qty: qty / flt( diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 3523cac822e..e330fe95cde 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -139,6 +139,7 @@ class Company(NestedSet): self.validate_abbr() self.validate_default_accounts() self.validate_currency() + self.validate_advance_account_currency() self.validate_coa_input() self.validate_perpetual_inventory() self.validate_provisional_account_for_non_stock_items() @@ -192,6 +193,29 @@ class Company(NestedSet): ).format(frappe.bold(account[0])) frappe.throw(error_message) + def validate_advance_account_currency(self): + if ( + self.default_advance_received_account + and frappe.get_cached_value("Account", self.default_advance_received_account, "account_currency") + != self.default_currency + ): + frappe.throw( + _("'{0}' should be in company currency {1}.").format( + frappe.bold("Default Advance Received Account"), frappe.bold(self.default_currency) + ) + ) + + if ( + self.default_advance_paid_account + and frappe.get_cached_value("Account", self.default_advance_paid_account, "account_currency") + != self.default_currency + ): + frappe.throw( + _("'{0}' should be in company currency {1}.").format( + frappe.bold("Default Advance Paid Account"), frappe.bold(self.default_currency) + ) + ) + def validate_currency(self): if self.is_new(): return diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index e3a155bbad2..db5d1e58a84 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -238,7 +238,7 @@ def update_qty(bin_name, args): sle = frappe.qb.DocType("Stock Ledger Entry") # actual qty is not up to date in case of backdated transaction - if future_sle_exists(args): + if future_sle_exists(args, allow_force_reposting=False): last_sle_qty = ( frappe.qb.from_(sle) .select(sle.qty_after_transaction) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index bce87b80c51..ec05738c68a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -1066,7 +1066,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None): @frappe.whitelist() -def make_delivery_trip(source_name, target_doc=None): +def make_delivery_trip(source_name, target_doc=None, kwargs=None): def update_stop_details(source_doc, target_doc, source_parent): target_doc.customer = source_parent.customer target_doc.address = source_parent.shipping_address_name diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index 4f8649c0bfa..77eae534d17 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -51,6 +51,7 @@ frappe.ui.form.on("Delivery Trip", { frm.add_custom_button( __("Delivery Note"), () => { + frm.clear_table("delivery_stops"); erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip", source_doctype: "Delivery Note", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 9b929f9f1b8..fb63f1c23c6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1340,6 +1340,7 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { frappe.model.set_value(item.doctype, item.name, { serial_and_batch_bundle: r.name, use_serial_batch_fields: 0, + basic_rate: r.avg_rate, qty: Math.abs(r.total_qty) / flt(item.conversion_factor || 1, precision("conversion_factor", item)), diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json index 68afd996b49..cbbb0ce0990 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json @@ -13,6 +13,7 @@ "end_time", "limits_dont_apply_on", "item_based_reposting", + "do_reposting_for_each_stock_transaction", "errors_notification_section", "notify_reposting_error_to_role" ], @@ -65,12 +66,18 @@ "fieldname": "errors_notification_section", "fieldtype": "Section Break", "label": "Errors Notification" + }, + { + "default": "0", + "fieldname": "do_reposting_for_each_stock_transaction", + "fieldtype": "Check", + "label": "Do reposting for each Stock Transaction" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-11-01 16:14:29.080697", + "modified": "2024-04-24 12:19:40.204888", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reposting Settings", @@ -91,4 +98,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py index 50f39817fff..eb3d38bfbfc 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py @@ -16,6 +16,7 @@ class StockRepostingSettings(Document): if TYPE_CHECKING: from frappe.types import DF + do_reposting_for_each_stock_transaction: DF.Check end_time: DF.Time | None item_based_reposting: DF.Check limit_reposting_timeslot: DF.Check @@ -29,6 +30,10 @@ class StockRepostingSettings(Document): def validate(self): self.set_minimum_reposting_time_slot() + def before_save(self): + if self.do_reposting_for_each_stock_transaction: + self.item_based_reposting = 1 + def set_minimum_reposting_time_slot(self): """Ensure that timeslot for reposting is at least 12 hours.""" if not self.limit_reposting_timeslot: diff --git a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py index a6dc72d7a42..e53659c1735 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py @@ -38,3 +38,51 @@ class TestStockRepostingSettings(unittest.TestCase): users = get_recipients() self.assertTrue(user in users) + + def test_do_reposting_for_each_stock_transaction(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1) + if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"): + frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0) + + item = make_item( + "_Test item for reposting check for each transaction", properties={"is_stock_item": 1} + ).name + + stock_entry = make_stock_entry( + item_code=item, + qty=1, + rate=100, + stock_entry_type="Material Receipt", + target="_Test Warehouse - _TC", + ) + + riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name") + self.assertTrue(riv) + + frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0) + + def test_do_not_reposting_for_each_stock_transaction(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0) + if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"): + frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0) + + item = make_item( + "_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1} + ).name + + stock_entry = make_stock_entry( + item_code=item, + qty=1, + rate=100, + stock_entry_type="Material Receipt", + target="_Test Warehouse - _TC", + ) + + riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name") + self.assertFalse(riv) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index 352fb19dd93..942b35e1afb 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -40,16 +40,25 @@ frappe.query_reports["Batch-Wise Balance History"] = { }; }, }, + { + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", + }, { fieldname: "warehouse", label: __("Warehouse"), fieldtype: "Link", options: "Warehouse", get_query: function () { + let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); let company = frappe.query_report.get_filter_value("company"); return { filters: { - company: company, + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), }, }; }, diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index fe0cefda644..2cce803481c 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -121,6 +121,16 @@ def get_stock_ledger_entries_for_batch_no(filters): ) query = apply_warehouse_filter(query, sle, filters) + if filters.warehouse_type and not filters.warehouse: + warehouses = frappe.get_all( + "Warehouse", + filters={"warehouse_type": filters.warehouse_type, "is_group": 0}, + pluck="name", + ) + + if warehouses: + query = query.where(sle.warehouse.isin(warehouses)) + for field in ["item_code", "batch_no", "company"]: if filters.get(field): query = query.where(sle[field] == filters.get(field)) @@ -154,6 +164,16 @@ def get_stock_ledger_entries_for_batch_bundle(filters): ) query = apply_warehouse_filter(query, sle, filters) + if filters.warehouse_type and not filters.warehouse: + warehouses = frappe.get_all( + "Warehouse", + filters={"warehouse_type": filters.warehouse_type, "is_group": 0}, + pluck="name", + ) + + if warehouses: + query = query.where(sle.warehouse.isin(warehouses)) + for field in ["item_code", "batch_no", "company"]: if filters.get(field): if field == "batch_no": diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.js b/erpnext/stock/report/stock_ageing/stock_ageing.js index 641084149ab..578869b6e93 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.js +++ b/erpnext/stock/report/stock_ageing/stock_ageing.js @@ -18,15 +18,24 @@ frappe.query_reports["Stock Ageing"] = { default: frappe.datetime.get_today(), reqd: 1, }, + { + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", + }, { fieldname: "warehouse", label: __("Warehouse"), fieldtype: "Link", options: "Warehouse", get_query: () => { - const company = frappe.query_report.get_filter_value("company"); + let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); + let company = frappe.query_report.get_filter_value("company"); return { filters: { + ...(warehouse_type && { warehouse_type }), ...(company && { company }), }, }; diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index c4156e7e64e..8738b65f50d 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -434,6 +434,15 @@ class FIFOSlots: if self.filters.get("warehouse"): sle_query = self.__get_warehouse_conditions(sle, sle_query) + elif self.filters.get("warehouse_type"): + warehouses = frappe.get_all( + "Warehouse", + filters={"warehouse_type": self.filters.get("warehouse_type"), "is_group": 0}, + pluck="name", + ) + + if warehouses: + sle_query = sle_query.where(sle.warehouse.isin(warehouses)) sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index af07dd7f73c..64ad36ff5b1 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -146,6 +146,8 @@ class StockBalanceReport: if self.filters.get("show_stock_ageing_data"): self.sle_entries = self.sle_query.run(as_dict=True) + # HACK: This is required to avoid causing db query in flt + _system_settings = frappe.get_cached_doc("System Settings") with frappe.db.unbuffered_cursor(): if not self.filters.get("show_stock_ageing_data"): self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 04cd1b846b3..b57333f9f35 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -231,13 +231,6 @@ def get_columns(filters): "width": 100, "convertible": "qty", }, - { - "label": _("Voucher #"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 150, - }, { "label": _("Warehouse"), "fieldname": "warehouse", diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index e1cce31329e..f5a059a7f61 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -56,13 +56,14 @@ def execute(filters=None): item_value.setdefault((item, item_map[item]["item_group"]), []) item_value[(item, item_map[item]["item_group"])].append(total_stock_value) + itemwise_brand = frappe._dict(get_itemwise_brand(items)) # sum bal_qty by item for (item, item_group), wh_balance in item_balance.items(): if not item_ageing.get(item): continue total_stock_value = sum(item_value[(item, item_group)]) - row = [item, item_map[item]["item_name"], item_group, total_stock_value] + row = [item, item_map[item]["item_name"], item_group, itemwise_brand.get(item), total_stock_value] fifo_queue = item_ageing[item]["fifo_queue"] average_age = 0.00 @@ -85,6 +86,10 @@ def execute(filters=None): return columns, data +def get_itemwise_brand(items): + return frappe.get_all("Item", filters={"name": ("in", items)}, fields=["name", "brand"], as_list=1) + + def get_columns(filters): """return columns""" @@ -92,6 +97,7 @@ def get_columns(filters): _("Item") + ":Link/Item:150", _("Item Name") + ":Link/Item:150", _("Item Group") + "::120", + _("Brand") + ":Link/Brand:120", _("Value") + ":Currency:120", _("Age") + ":Float:120", ] diff --git a/erpnext/translations/en.csv b/erpnext/translations/en.csv deleted file mode 100644 index 7fac9e1785b..00000000000 --- a/erpnext/translations/en.csv +++ /dev/null @@ -1 +0,0 @@ -Married,既婚,