Skip to content

Commit

Permalink
Merge pull request #42588 from ruthra-kumar/round_off_account_for_ope…
Browse files Browse the repository at this point in the history
…ning

refactor: separate round off account for opening
  • Loading branch information
ruthra-kumar authored Sep 6, 2024
2 parents 6652806 + cf11ac8 commit 75cf3e2
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 16 deletions.
4 changes: 2 additions & 2 deletions erpnext/accounts/doctype/account/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
{
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions erpnext/accounts/doctype/account/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class Account(NestedSet):
"Payable",
"Receivable",
"Round Off",
"Round Off for Opening",
"Stock",
"Stock Adjustment",
"Stock Received But Not Billed",
Expand Down
21 changes: 20 additions & 1 deletion erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -1524,10 +1524,29 @@ def make_gle_for_rounding_adjustment(self, gl_entries):
# 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}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> 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(
{
Expand Down
59 changes: 59 additions & 0 deletions erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,65 @@ def test_adjust_incoming_rate_from_pi_with_multi_currency(self):

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(
Expand Down
21 changes: 20 additions & 1 deletion erpnext/accounts/doctype/sales_invoice/sales_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -1610,10 +1610,29 @@ def make_gle_for_rounding_adjustment(self, gl_entries):
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}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> 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(
{
Expand Down
102 changes: 102 additions & 0 deletions erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -3943,6 +3943,108 @@ def test_pos_returns_without_update_outstanding_for_self(self):
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(
Expand Down
51 changes: 41 additions & 10 deletions erpnext/accounts/general_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
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
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion erpnext/controllers/accounts_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -1266,7 +1266,11 @@ def set_advance_gain_or_loss(self):
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
)

Expand Down
1 change: 1 addition & 0 deletions erpnext/setup/doctype/company/company.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", {}],
Expand Down
Loading

0 comments on commit 75cf3e2

Please sign in to comment.