diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json
index 5a50d7a1ab5..34e9a691fd2 100644
--- a/erpnext/accounts/doctype/account/account.json
+++ b/erpnext/accounts/doctype/account/account.json
@@ -124,7 +124,7 @@
"label": "Account Type",
"oldfieldname": "account_type",
"oldfieldtype": "Select",
- "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
+ "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
"search_index": 1
},
{
@@ -194,7 +194,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2024-06-27 16:23:04.444354",
+ "modified": "2024-08-19 15:19:11.095045",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 180edc61ced..f6b0a6f8056 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -60,6 +60,7 @@ class Account(NestedSet):
"Payable",
"Receivable",
"Round Off",
+ "Round Off for Opening",
"Stock",
"Stock Adjustment",
"Stock Received But Not Billed",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 0abc925ec34..11779dd00f6 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1524,10 +1524,29 @@ class PurchaseInvoice(BuyingController):
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
# then base_rounding_adjustment becomes zero and error is thrown in GL Entry
if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
- round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ (
+ round_off_account,
+ round_off_cost_center,
+ round_off_for_opening,
+ ) = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
+ if self.is_opening == "Yes" and self.rounding_adjustment:
+ if not round_off_for_opening:
+ frappe.throw(
+ _(
+ "Opening Invoice has rounding adjustment of {0}.
'{1}' account is required to post these values. Please set it in Company: {2}.
Or, '{3}' can be enabled to not post any rounding adjustment."
+ ).format(
+ frappe.bold(self.rounding_adjustment),
+ frappe.bold("Round Off for Opening"),
+ get_link_to_form("Company", self.company),
+ frappe.bold("Disable Rounded Total"),
+ )
+ )
+ else:
+ round_off_account = round_off_for_opening
+
gl_entries.append(
self.get_gl_dict(
{
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index d85abf93ea7..ccc14353c6b 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -2302,6 +2302,65 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
+ def test_opening_invoice_rounding_adjustment_validation(self):
+ pi = make_purchase_invoice(do_not_save=1)
+ pi.items[0].rate = 99.98
+ pi.items[0].qty = 1
+ pi.items[0].expense_account = "Temporary Opening - _TC"
+ pi.is_opening = "Yes"
+ pi.save()
+ self.assertRaises(frappe.ValidationError, pi.submit)
+
+ def _create_opening_roundoff_account(self, company_name):
+ liability_root = frappe.db.get_all(
+ "Account",
+ filters={"company": company_name, "root_type": "Liability", "disabled": 0},
+ order_by="lft",
+ limit=1,
+ )[0]
+
+ # setup round off account
+ if acc := frappe.db.exists(
+ "Account",
+ {
+ "account_name": "Round Off for Opening",
+ "account_type": "Round Off for Opening",
+ "company": company_name,
+ },
+ ):
+ frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
+ else:
+ acc = frappe.new_doc("Account")
+ acc.company = company_name
+ acc.parent_account = liability_root.name
+ acc.account_name = "Round Off for Opening"
+ acc.account_type = "Round Off for Opening"
+ acc.save()
+ frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
+
+ def test_ledger_entries_of_opening_invoice_with_rounding_adjustment(self):
+ pi = make_purchase_invoice(do_not_save=1)
+ pi.items[0].rate = 99.98
+ pi.items[0].qty = 1
+ pi.items[0].expense_account = "Temporary Opening - _TC"
+ pi.is_opening = "Yes"
+ pi.save()
+ self._create_opening_roundoff_account(pi.company)
+ pi.submit()
+ actual = frappe.db.get_all(
+ "GL Entry",
+ filters={"voucher_no": pi.name, "is_opening": "Yes", "is_cancelled": False},
+ fields=["account", "debit", "credit", "is_opening"],
+ order_by="account,debit",
+ )
+ expected = [
+ {"account": "Creditors - _TC", "debit": 0.0, "credit": 100.0, "is_opening": "Yes"},
+ {"account": "Round Off for Opening - _TC", "debit": 0.02, "credit": 0.0, "is_opening": "Yes"},
+ {"account": "Temporary Opening - _TC", "debit": 99.98, "credit": 0.0, "is_opening": "Yes"},
+ ]
+ self.assertEqual(len(actual), 3)
+ self.assertEqual(expected, actual)
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 4c90c908db3..8394eec2017 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1610,10 +1610,29 @@ class SalesInvoice(SellingController):
and self.base_rounding_adjustment
and not self.is_internal_transfer()
):
- round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ (
+ round_off_account,
+ round_off_cost_center,
+ round_off_for_opening,
+ ) = get_round_off_account_and_cost_center(
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
)
+ if self.is_opening == "Yes" and self.rounding_adjustment:
+ if not round_off_for_opening:
+ frappe.throw(
+ _(
+ "Opening Invoice has rounding adjustment of {0}.
'{1}' account is required to post these values. Please set it in Company: {2}.
Or, '{3}' can be enabled to not post any rounding adjustment."
+ ).format(
+ frappe.bold(self.rounding_adjustment),
+ frappe.bold("Round Off for Opening"),
+ get_link_to_form("Company", self.company),
+ frappe.bold("Disable Rounded Total"),
+ )
+ )
+ else:
+ round_off_account = round_off_for_opening
+
gl_entries.append(
self.get_gl_dict(
{
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 6542f6c7f9e..d2633af55c9 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -3943,6 +3943,108 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(len(res), 1)
self.assertEqual(res[0][0], pos_return.return_against)
+ def test_validation_on_opening_invoice_with_rounding(self):
+ si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
+ si.is_opening = "Yes"
+ si.items[0].income_account = "Temporary Opening - _TC"
+ si.save()
+ self.assertRaises(frappe.ValidationError, si.submit)
+
+ def _create_opening_roundoff_account(self, company_name):
+ liability_root = frappe.db.get_all(
+ "Account",
+ filters={"company": company_name, "root_type": "Liability", "disabled": 0},
+ order_by="lft",
+ limit=1,
+ )[0]
+
+ # setup round off account
+ if acc := frappe.db.exists(
+ "Account",
+ {
+ "account_name": "Round Off for Opening",
+ "account_type": "Round Off for Opening",
+ "company": company_name,
+ },
+ ):
+ frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
+ else:
+ acc = frappe.new_doc("Account")
+ acc.company = company_name
+ acc.parent_account = liability_root.name
+ acc.account_name = "Round Off for Opening"
+ acc.account_type = "Round Off for Opening"
+ acc.save()
+ frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
+
+ def test_opening_invoice_with_rounding_adjustment(self):
+ si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
+ si.is_opening = "Yes"
+ si.items[0].income_account = "Temporary Opening - _TC"
+ si.save()
+
+ self._create_opening_roundoff_account(si.company)
+
+ si.reload()
+ si.submit()
+ res = frappe.db.get_all(
+ "GL Entry",
+ filters={"voucher_no": si.name, "is_opening": "Yes"},
+ fields=["account", "debit", "credit", "is_opening"],
+ )
+ self.assertEqual(len(res), 3)
+
+ def _create_opening_invoice_with_inclusive_tax(self):
+ si = create_sales_invoice(qty=1, rate=90, do_not_submit=True)
+ si.is_opening = "Yes"
+ si.items[0].income_account = "Temporary Opening - _TC"
+ item_template = si.items[0].as_dict()
+ item_template.name = None
+ item_template.rate = 55
+ si.append("items", item_template)
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Testing...",
+ "rate": 5,
+ "included_in_print_rate": True,
+ },
+ )
+ # there will be 0.01 precision loss between Dr and Cr
+ # caused by 'included_in_print_tax' option
+ si.save()
+ return si
+
+ def test_rounding_validation_for_opening_with_inclusive_tax(self):
+ si = self._create_opening_invoice_with_inclusive_tax()
+ # 'Round Off for Opening' not set in Company master
+ # Ledger level validation must be thrown
+ self.assertRaises(frappe.ValidationError, si.submit)
+
+ def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(self):
+ si = self._create_opening_invoice_with_inclusive_tax()
+ # 'Round Off for Opening' is set in Company master
+ self._create_opening_roundoff_account(si.company)
+
+ si.submit()
+ actual = frappe.db.get_all(
+ "GL Entry",
+ filters={"voucher_no": si.name, "is_opening": "Yes", "is_cancelled": False},
+ fields=["account", "debit", "credit", "is_opening"],
+ order_by="account,debit",
+ )
+ expected = [
+ {"account": "_Test Account Service Tax - _TC", "debit": 0.0, "credit": 6.9, "is_opening": "Yes"},
+ {"account": "Debtors - _TC", "debit": 145.0, "credit": 0.0, "is_opening": "Yes"},
+ {"account": "Round Off for Opening - _TC", "debit": 0.0, "credit": 0.01, "is_opening": "Yes"},
+ {"account": "Temporary Opening - _TC", "debit": 0.0, "credit": 138.09, "is_opening": "Yes"},
+ ]
+ self.assertEqual(len(actual), 4)
+ self.assertEqual(expected, actual)
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 4fba1095546..84217da1a6b 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -7,7 +7,7 @@ import copy
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
-from frappe.utils import cint, flt, formatdate, getdate, now
+from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now
from frappe.utils.dashboard import cache_source
import erpnext
@@ -490,16 +490,36 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_
)
+def has_opening_entries(gl_map: list) -> bool:
+ for x in gl_map:
+ if x.is_opening == "Yes":
+ return True
+ return False
+
+
def make_round_off_gle(gl_map, debit_credit_diff, precision):
- round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center(
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
)
round_off_gle = frappe._dict()
round_off_account_exists = False
+ has_opening_entry = has_opening_entries(gl_map)
+
+ if has_opening_entry:
+ if not round_off_for_opening:
+ frappe.throw(
+ _("Please set '{0}' in Company: {1}").format(
+ frappe.bold("Round Off for Opening"), get_link_to_form("Company", gl_map[0].company)
+ )
+ )
+
+ account = round_off_for_opening
+ else:
+ account = round_off_account
if gl_map[0].voucher_type != "Period Closing Voucher":
for d in gl_map:
- if d.account == round_off_account:
+ if d.account == account:
round_off_gle = d
if d.debit:
debit_credit_diff -= flt(d.debit) - flt(d.credit)
@@ -517,7 +537,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
round_off_gle.update(
{
- "account": round_off_account,
+ "account": account,
"debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
"credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
"debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
@@ -531,6 +551,9 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
}
)
+ if has_opening_entry:
+ round_off_gle.update({"is_opening": "Yes"})
+
update_accounting_dimensions(round_off_gle)
if not round_off_account_exists:
gl_map.append(round_off_gle)
@@ -555,9 +578,9 @@ def update_accounting_dimensions(round_off_gle):
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False):
- round_off_account, round_off_cost_center = frappe.get_cached_value(
- "Company", company, ["round_off_account", "round_off_cost_center"]
- ) or [None, None]
+ round_off_account, round_off_cost_center, round_off_for_opening = frappe.get_cached_value(
+ "Company", company, ["round_off_account", "round_off_cost_center", "round_off_for_opening"]
+ ) or [None, None, None]
# Use expense account as fallback
if not round_off_account:
@@ -572,12 +595,20 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use
round_off_cost_center = parent_cost_center
if not round_off_account:
- frappe.throw(_("Please mention Round Off Account in Company"))
+ frappe.throw(
+ _("Please mention '{0}' in Company: {1}").format(
+ frappe.bold("Round Off Account"), get_link_to_form("Company", company)
+ )
+ )
if not round_off_cost_center:
- frappe.throw(_("Please mention Round Off Cost Center in Company"))
+ frappe.throw(
+ _("Please mention '{0}' in Company: {1}").format(
+ frappe.bold("Round Off Cost Center"), get_link_to_form("Company", company)
+ )
+ )
- return round_off_account, round_off_cost_center
+ return round_off_account, round_off_cost_center, round_off_for_opening
def make_reverse_gl_entries(
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 01a95c17a2e..a97938f90d8 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1266,7 +1266,11 @@ class AccountsController(TransactionBase):
d.exchange_gain_loss = difference
def make_precision_loss_gl_entry(self, gl_entries):
- round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ (
+ round_off_account,
+ round_off_cost_center,
+ round_off_for_opening,
+ ) = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index a9a996854f7..19df429e491 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -252,6 +252,7 @@ erpnext.company.setup_queries = function (frm) {
["default_expense_account", { root_type: "Expense" }],
["default_income_account", { root_type: "Income" }],
["round_off_account", { root_type: "Expense" }],
+ ["round_off_for_opening", { root_type: "Liability", account_type: "Round Off for Opening" }],
["write_off_account", { root_type: "Expense" }],
["default_deferred_expense_account", {}],
["default_deferred_revenue_account", {}],
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 7d74eb1192e..805bea18b53 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -49,6 +49,7 @@
"default_cash_account",
"default_receivable_account",
"round_off_account",
+ "round_off_for_opening",
"round_off_cost_center",
"write_off_account",
"exchange_gain_loss_account",
@@ -785,6 +786,12 @@
"fieldtype": "Link",
"label": "Default Operating Cost Account",
"options": "Account"
+ },
+ {
+ "fieldname": "round_off_for_opening",
+ "fieldtype": "Link",
+ "label": "Round Off for Opening",
+ "options": "Account"
}
],
"icon": "fa fa-building",
@@ -792,7 +799,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2024-07-24 18:17:56.413971",
+ "modified": "2024-08-02 11:34:46.785377",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 3bdf93f63a9..54765420ad4 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -91,6 +91,7 @@ class Company(NestedSet):
rgt: DF.Int
round_off_account: DF.Link | None
round_off_cost_center: DF.Link | None
+ round_off_for_opening: DF.Link | None
sales_monthly_history: DF.SmallText | None
series_for_depreciation_entry: DF.Data | None
stock_adjustment_account: DF.Link | None