diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index 3258a8cbe8a..95ab540d657 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -8,6 +8,7 @@ from frappe.utils import today from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.general_ledger import make_reverse_gl_entries from erpnext.accounts.utils import get_fiscal_year from erpnext.tests.utils import ERPNextTestSuite @@ -334,6 +335,48 @@ class TestPeriodClosingVoucher(ERPNextTestSuite): return pcv + @ERPNextTestSuite.change_settings( + "Accounts Settings", + {"enable_immutable_ledger": 1}, + ) + def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self): + company = create_company() + cost_center = create_cost_center("Test Cost Center 1") + + jv = make_journal_entry( + posting_date="2021-03-15", + amount=400, + account1="Cash - TPC", + account2="Sales - TPC", + cost_center=cost_center, + company=company, + save=False, + ) + jv.company = company + jv.save() + jv.submit() + + self.make_period_closing_voucher(posting_date="2021-03-31") + + # Passed posting_date is after PCV end date, so cancellation should not fail. + make_reverse_gl_entries( + voucher_type="Journal Entry", + voucher_no=jv.name, + posting_date="2022-01-01", + ) + + totals_after_cancel = frappe.db.sql( + """ + select sum(debit) as total_debit, sum(credit) as total_credit + from `tabGL Entry` + where voucher_type=%s and voucher_no=%s and is_cancelled=0 + """, + ("Journal Entry", jv.name), + as_dict=True, + )[0] + + self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit) + def create_company(): company = frappe.get_doc( diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ef64a3ef79b..8a7648d20d8 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -968,9 +968,6 @@ class SalesInvoice(SellingController): if selling_price_list: self.set("selling_price_list", selling_price_list) - if not for_validate: - self.update_stock = cint(pos.get("update_stock")) - # set pos values in items for item in self.get("items"): if item.get("item_code"): @@ -981,6 +978,10 @@ class SalesInvoice(SellingController): if (not for_validate) or (for_validate and not item.get(fname)): item.set(fname, val) + if not for_validate: + dn_flag = any(d.get("dn_detail") for d in self.get("items")) + self.update_stock = 0 if dn_flag else cint(pos.get("update_stock")) + # fetch terms if self.tc_name and not self.terms: self.terms = frappe.db.get_value("Terms and Conditions", self.tc_name, "terms") diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index e5480fd4244..341488381a0 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -719,7 +719,12 @@ def make_reverse_gl_entries( check_freezing_date(gl_entries[0]["posting_date"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) - validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"]) + + # For reverse entries, use the posting_date parameter if provided and valid + # Otherwise fall back to original posting_date + validation_date = posting_date if posting_date else gl_entries[0]["posting_date"] + validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"]) + if partial_cancel: # Partial cancel is only used by `Advance` in separate account feature. # Only cancel GL entries for unlinked reference using `voucher_detail_no` diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index b4adc01b102..d400a620b0b 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -44,7 +44,7 @@ frappe.ui.form.on("Employee", { }, refresh: function (frm) { - frm.fields_dict.date_of_birth.datepicker.update({ maxDate: new Date() }); + frm.fields_dict.date_of_birth.datepicker?.update({ maxDate: new Date() }); if (!frm.is_new() && !frm.doc.user_id) { frm.add_custom_button(__("Create User"), () => { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 7806a51209b..c3331ef81c4 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1374,7 +1374,7 @@ def get_billed_qty_amount_against_purchase_receipt(pr_doc): .on(parent_table.name == table.parent) .select( table.pr_detail, - fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"), + fn.Sum(table.base_net_amount).as_("amount"), fn.Sum(table.qty).as_("qty"), ) .where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1)) @@ -1420,7 +1420,7 @@ def get_billed_qty_amount_against_purchase_order(pr_doc): .select( table.po_detail, fn.Sum(table.qty).as_("qty"), - fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"), + fn.Sum(table.base_net_amount).as_("amount"), ) .where((table.po_detail.isin(po_names)) & (table.docstatus == 1) & (table.pr_detail.isnull())) .groupby(table.po_detail) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 66e228d7e90..fb5db1c95e2 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -5606,6 +5606,157 @@ class TestPurchaseReceipt(ERPNextTestSuite): self.assertIn(company.default_inventory_account, gl_accounts) pr.cancel() + @ERPNextTestSuite.change_settings( + "Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0} + ) + def test_srbnb_with_inclusive_tax_and_rate_change_in_pi(self): + """ + When 'Set Landed Cost Based on PI Rate' is enabled and PI has an inclusive tax: + - PR: qty=2, rate=1000 INR → base_net_amount=2000 + - PI: rate changed to 2000, 5% tax included in basic rate + → PI base_net_amount = 2 * 2000 / 1.05 ≈ 3809.52 + + The system must use PI's base_net_amount (not amount=4000) so that + SRBNB credit on PR = 3809.52, not 4000. + """ + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + cost_center = "Main - TCP1" + + item_code = make_item( + "Test Item for SRBNB Inclusive Tax Rate Change", + {"is_stock_item": 1}, + ).name + + pr = make_purchase_receipt( + item_code=item_code, + qty=2, + rate=1000, + company=company, + warehouse=warehouse, + cost_center=cost_center, + ) + + pi = make_purchase_invoice(pr.name) + pi.items[0].rate = 2000 + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - TCP1", + "category": "Total", + "add_deduct_tax": "Add", + "included_in_print_rate": 1, + "rate": 5, + "description": "Test Inclusive Tax", + "cost_center": cost_center, + }, + ) + pi.save() + pi.submit() + + pr.reload() + + # PI base_net_amount = qty * (rate / (1 + tax_rate/100)) = 2 * (2000 / 1.05) + pi_base_net_amount = flt(2 * 2000 / 1.05, 2) + pr_base_net_amount = flt(pr.items[0].amount, 2) # 2 * 1000 = 2000 + expected_diff = flt(pi_base_net_amount - pr_base_net_amount, 2) + + self.assertAlmostEqual(pr.items[0].amount_difference_with_purchase_invoice, expected_diff, places=2) + + # Total SRBNB credit = PR base_net_amount + amount_difference = PI base_net_amount + srbnb_account = "Stock Received But Not Billed - TCP1" + gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True) + srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account) + self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2) + + @ERPNextTestSuite.change_settings( + "Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0} + ) + def test_srbnb_with_inclusive_tax_and_exchange_rate_change_in_pi(self): + """ + When 'Set Landed Cost Based on PI Rate' is enabled, PI has an inclusive tax, and only + the exchange rate changes on the PI (rate stays the same): + - PR: qty=2, rate=100 USD, conversion_rate=70 → base_net_amount=14000 INR + - PI: same rate=100 USD, conversion_rate changed to 90, 5% tax included in basic rate + → PI base_net_amount = 2 * (100 / 1.05) * 90 ≈ 17142.86 INR + + The system must use PI's base_net_amount (not amount = 2*100*90 = 18000) so that + SRBNB credit on PR = 17142.86, not 18000. + """ + from erpnext.accounts.doctype.account.test_account import create_account + + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + cost_center = "Main - TCP1" + + party_account = create_account( + account_name="USD Payable For SRBNB Exchange Rate Test", + parent_account="Accounts Payable - TCP1", + account_type="Payable", + company=company, + account_currency="USD", + ) + + supplier = create_supplier( + supplier_name="_Test USD Supplier for SRBNB Exchange Rate", + default_currency="USD", + party_account=party_account, + ).name + + item_code = make_item( + "Test Item for SRBNB Inclusive Tax Exchange Rate Change", + {"is_stock_item": 1}, + ).name + + pr = make_purchase_receipt( + item_code=item_code, + qty=2, + rate=100, + currency="USD", + conversion_rate=70, + company=company, + warehouse=warehouse, + supplier=supplier, + ) + + pi = make_purchase_invoice(pr.name) + pi.conversion_rate = 90 + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - TCP1", + "category": "Total", + "add_deduct_tax": "Add", + "included_in_print_rate": 1, + "rate": 5, + "description": "Test Inclusive Tax", + "cost_center": cost_center, + }, + ) + pi.save() + pi.submit() + + pr.reload() + + # PI base_net_amount = qty * (rate / (1 + tax_rate/100)) * new_conversion_rate + # = 2 * (100 / 1.05) * 90 ≈ 17142.86 INR + # PR base_net_amount = qty * rate * pr_conversion_rate = 2 * 100 * 70 = 14000 INR + tax_amount_pr = (200 - flt(200 / 1.05, 2)) * 90 + + pi_base_net_amount = flt(2 * 100 * 90) - flt(tax_amount_pr) + pr_base_net_amount = flt(2 * 100 * 70) + expected_diff = flt(pi_base_net_amount - pr_base_net_amount) + + self.assertAlmostEqual(pr.items[0].amount_difference_with_purchase_invoice, expected_diff, places=2) + + # Total SRBNB credit = PR base_net_amount + amount_difference = PI base_net_amount + srbnb_account = "Stock Received But Not Billed - TCP1" + gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True) + srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account) + self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2) + def create_asset_category_for_pr_test(): category_name = "Test Asset Category for PR" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 35557dd0d1e..995e07f74ae 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3638,12 +3638,12 @@ class StockEntry(StockController, SubcontractingInwardController): args = { "source_dt": "Stock Entry Detail", "target_field": "transferred_qty", - "target_ref_field": "qty", + "target_ref_field": "transfer_qty", "target_dt": "Stock Entry Detail", "join_field": "ste_detail", "target_parent_dt": "Stock Entry", "target_parent_field": "per_transferred", - "source_field": "qty", + "source_field": "transfer_qty", "percent_join_field": "against_stock_entry", } diff --git a/frappe-semgrep-rules b/frappe-semgrep-rules deleted file mode 160000 index a05bce32ad3..00000000000 --- a/frappe-semgrep-rules +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a05bce32ad3e37cf9a87a6913e9b08e45c8ba8cf