Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Provisional accounting for expenses #29451

Merged
20 changes: 14 additions & 6 deletions erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
Expand Up @@ -548,6 +548,10 @@ def make_item_gl_entries(self, gl_entries):
exchange_rate_map, net_rate_map = get_purchase_document_details(self)

enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, \
'enable_provisional_accounting_for_non_stock_items'))

purchase_receipt_doc_map = {}

for item in self.get("items"):
if flt(item.base_net_amount):
Expand Down Expand Up @@ -643,19 +647,23 @@ def make_item_gl_entries(self, gl_entries):
else:
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))

auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
provisional_account = self.get_company_default("default_provisional_account")
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)

if auto_accounting_for_non_stock_items:
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
if not purchase_receipt_doc:
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc

if item.purchase_receipt:
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
'account':service_received_but_not_billed_account}, ['name'])
'account':provisional_account}, ['name'])

if expense_booked_in_pr:
expense_account = service_received_but_not_billed_account
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1)

if not self.is_internal_transfer():
gl_entries.append(self.get_gl_dict({
Expand Down
Expand Up @@ -11,12 +11,17 @@
import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice_from_receipt,
)
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_taxes,
make_purchase_receipt,
Expand Down Expand Up @@ -1147,8 +1152,6 @@ def test_gain_loss_with_advance_entry(self):

def test_purchase_invoice_advance_taxes(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order

# create a new supplier to test
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
Expand Down Expand Up @@ -1221,6 +1224,45 @@ def test_purchase_invoice_advance_taxes(self):
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)

def test_provisional_accounting_entry(self):
item = create_item("_Test Non Stock Item", is_stock_item=0)
provisional_account = create_account(account_name="Provision Account",
parent_account="Current Liabilities - _TC", company="_Test Company")

company = frappe.get_doc('Company', '_Test Company')
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()

pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2))

pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
pi.items[0].expense_account = 'Cost of Goods Sold - _TC'
pi.save()
pi.submit()

# Check GLE for Purchase Invoice
expected_gle = [
['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)],
['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)]
]

check_gl_entries(self, pi.name, expected_gle, pi.posting_date)

expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 250, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
["Provision Account - _TC", 0, 250, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date]
]

check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)

company.enable_provisional_accounting_for_non_stock_items = 0
company.save()

def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
Expand Down
5 changes: 4 additions & 1 deletion erpnext/controllers/stock_controller.py
Expand Up @@ -40,7 +40,10 @@ def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)

if cint(erpnext.is_perpetual_inventory_enabled(self.company)):
provisional_accounting_for_non_stock_items = \
cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items'))

if cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items:
warehouse_account = get_warehouse_account_map(self.company)

if self.docstatus==1:
Expand Down
1 change: 1 addition & 0 deletions erpnext/patches.txt
Expand Up @@ -323,6 +323,7 @@ erpnext.patches.v13_0.hospitality_deprecation_warning
erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting

[post_model_sync]
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
Expand Down
19 changes: 19 additions & 0 deletions erpnext/patches/v13_0/enable_provisional_accounting.py
@@ -0,0 +1,19 @@
import frappe


def execute():
frappe.reload_doc("setup", "doctype", "company")

company = frappe.qb.DocType("Company")

frappe.qb.update(
company
).set(
company.enable_provisional_accounting_for_non_stock_items, company.enable_perpetual_inventory_for_non_stock_items
).set(
company.default_provisional_account, company.service_received_but_not_billed
).where(
company.enable_perpetual_inventory_for_non_stock_items == 1
).where(
company.service_received_but_not_billed.isnotnull()
).run()
37 changes: 19 additions & 18 deletions erpnext/setup/doctype/company/company.json
Expand Up @@ -3,7 +3,7 @@
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:company_name",
"creation": "2013-04-10 08:35:39",
"creation": "2022-01-25 10:29:55.938239",
"description": "Legal Entity / Subsidiary with a separate Chart of Accounts belonging to the Organization.",
"doctype": "DocType",
"document_type": "Setup",
Expand Down Expand Up @@ -77,13 +77,13 @@
"default_finance_book",
"auto_accounting_for_stock_settings",
"enable_perpetual_inventory",
"enable_perpetual_inventory_for_non_stock_items",
"enable_provisional_accounting_for_non_stock_items",
"default_inventory_account",
"stock_adjustment_account",
"default_in_transit_warehouse",
"column_break_32",
"stock_received_but_not_billed",
"service_received_but_not_billed",
"default_provisional_account",
"expenses_included_in_valuation",
"fixed_asset_defaults",
"accumulated_depreciation_account",
Expand Down Expand Up @@ -684,20 +684,6 @@
"label": "Default Buying Terms",
"options": "Terms and Conditions"
},
{
"fieldname": "service_received_but_not_billed",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Service Received But Not Billed",
"no_copy": 1,
"options": "Account"
},
{
"default": "0",
"fieldname": "enable_perpetual_inventory_for_non_stock_items",
"fieldtype": "Check",
"label": "Enable Perpetual Inventory For Non Stock Items"
},
{
"fieldname": "default_in_transit_warehouse",
"fieldtype": "Link",
Expand Down Expand Up @@ -741,14 +727,28 @@
"fieldname": "section_break_28",
"fieldtype": "Section Break",
"label": "Chart of Accounts"
},
{
"default": "0",
"fieldname": "enable_provisional_accounting_for_non_stock_items",
"fieldtype": "Check",
"label": "Enable Provisional Accounting For Non Stock Items"
},
{
"fieldname": "default_provisional_account",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Default Provisional Account",
"no_copy": 1,
"options": "Account"
}
],
"icon": "fa fa-building",
"idx": 1,
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2021-10-04 12:09:25.833133",
"modified": "2022-01-25 10:33:16.826067",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
Expand Down Expand Up @@ -809,5 +809,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
14 changes: 9 additions & 5 deletions erpnext/setup/doctype/company/company.py
Expand Up @@ -10,6 +10,7 @@
from frappe import _
from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.utils import cint, formatdate, get_timestamp, today
from frappe.utils.nestedset import NestedSet

Expand Down Expand Up @@ -45,7 +46,7 @@ def validate(self):
self.validate_currency()
self.validate_coa_input()
self.validate_perpetual_inventory()
self.validate_perpetual_inventory_for_non_stock_items()
self.validate_provisional_account_for_non_stock_items()
self.check_country_change()
self.check_parent_changed()
self.set_chart_of_accounts()
Expand Down Expand Up @@ -187,11 +188,14 @@ def validate_perpetual_inventory(self):
frappe.msgprint(_("Set default inventory account for perpetual inventory"),
alert=True, indicator='orange')

def validate_perpetual_inventory_for_non_stock_items(self):
def validate_provisional_account_for_non_stock_items(self):
if not self.get("__islocal"):
if cint(self.enable_perpetual_inventory_for_non_stock_items) == 1 and not self.service_received_but_not_billed:
frappe.throw(_("Set default {0} account for perpetual inventory for non stock items").format(
frappe.bold('Service Received But Not Billed')))
if cint(self.enable_provisional_accounting_for_non_stock_items) == 1 and not self.default_provisional_account:
frappe.throw(_("Set default {0} account for non stock items").format(
frappe.bold('Provisional Account')))

make_property_setter("Purchase Receipt", "provisional_expense_account", "hidden",
not self.enable_provisional_accounting_for_non_stock_items, "Check", validate_fields_for_doctype=False)

def check_country_change(self):
frappe.flags.country_change = False
Expand Down
19 changes: 18 additions & 1 deletion erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
Expand Up @@ -106,6 +106,8 @@
"terms",
"bill_no",
"bill_date",
"accounting_details_section",
"provisional_expense_account",
"more_info",
"project",
"status",
Expand Down Expand Up @@ -1144,16 +1146,30 @@
"label": "Represents Company",
"options": "Company",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "provisional_expense_account",
"fieldtype": "Link",
"hidden": 1,
"label": "Provisional Expense Account",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetch from company if blank

"options": "Account"
}
],
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2021-09-28 13:11:10.181328",
"modified": "2022-02-01 11:40:52.690984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
Expand Down Expand Up @@ -1214,6 +1230,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
Expand Down