From 938be22ae9f12d8577fe61e3278dc046063f9901 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 17 Jun 2025 16:10:08 +0530 Subject: [PATCH] feat: periodic accounting --- .../doctype/journal_entry/journal_entry.js | 33 ++++++++ .../doctype/journal_entry/journal_entry.json | 53 +++++++++++-- .../doctype/journal_entry/journal_entry.py | 78 +++++++++++++++++++ .../doctype/stock_entry/test_stock_entry.py | 69 ++++++++++++++++ 4 files changed, 228 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 7df92066980..25b09583d57 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -20,6 +20,39 @@ frappe.ui.form.on("Journal Entry", { "Unreconcile Payment Entries", "Bank Transaction", ]; + + frm.trigger("set_queries"); + }, + + set_queries(frm) { + frm.set_query("periodic_entry_difference_account", function () { + return { + filters: { + is_group: 0, + company: frm.doc.company, + }, + }; + }); + + frm.set_query("stock_asset_account", function () { + return { + filters: { + is_group: 0, + account_type: "Stock", + company: frm.doc.company, + }, + }; + }); + }, + + get_balance_for_periodic_accounting(frm) { + frm.call({ + method: "get_balance_for_periodic_accounting", + doc: frm.doc, + callback: function (r) { + refresh_field("accounts"); + }, + }); }, refresh: function (frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index c7430dbf00c..74e20cebb9d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -13,15 +13,21 @@ "title", "voucher_type", "naming_series", - "finance_book", "process_deferred_accounting", "reversal_of", - "tax_withholding_category", "column_break1", "from_template", "company", "posting_date", + "finance_book", "apply_tds", + "tax_withholding_category", + "section_break_tcvw", + "for_all_stock_asset_accounts", + "column_break_wpau", + "stock_asset_account", + "periodic_entry_difference_account", + "get_balance_for_periodic_accounting", "2_add_edit_gl_entries", "accounts", "section_break99", @@ -89,7 +95,7 @@ "label": "Entry Type", "oldfieldname": "voucher_type", "oldfieldtype": "Select", - "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nAsset Disposal\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense", + "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nAsset Disposal\nPeriodic Accounting Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense", "reqd": 1, "search_index": 1 }, @@ -543,6 +549,42 @@ "label": "Is System Generated", "no_copy": 1, "read_only": 1 + }, + { + "depends_on": "eval:doc.voucher_type === \"Periodic Accounting Entry\"", + "fieldname": "periodic_entry_difference_account", + "fieldtype": "Link", + "label": "Periodic Entry Difference Account", + "mandatory_depends_on": "eval:doc.voucher_type === \"Periodic Accounting Entry\"", + "options": "Account" + }, + { + "depends_on": "eval:doc.voucher_type === \"Periodic Accounting Entry\"", + "fieldname": "section_break_tcvw", + "fieldtype": "Section Break", + "label": "Periodic Accounting" + }, + { + "default": "1", + "fieldname": "for_all_stock_asset_accounts", + "fieldtype": "Check", + "label": "For All Stock Asset Accounts" + }, + { + "depends_on": "eval:doc.for_all_stock_asset_accounts === 0", + "fieldname": "stock_asset_account", + "fieldtype": "Link", + "label": "Stock Asset Account", + "options": "Account" + }, + { + "fieldname": "column_break_wpau", + "fieldtype": "Column Break" + }, + { + "fieldname": "get_balance_for_periodic_accounting", + "fieldtype": "Button", + "label": "Get Balance" } ], "icon": "fa fa-file-text", @@ -557,7 +599,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2024-12-26 15:32:20.730666", + "modified": "2025-06-17 15:18:13.322681", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", @@ -602,10 +644,11 @@ "role": "Auditor" } ], + "row_format": "Dynamic", "search_fields": "voucher_type,posting_date, due_date, cheque_no", "sort_field": "creation", "sort_order": "DESC", "states": [], "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index de67a3974b0..57eb24cbf8d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -62,6 +62,7 @@ class JournalEntry(AccountsController): difference: DF.Currency due_date: DF.Date | None finance_book: DF.Link | None + for_all_stock_asset_accounts: DF.Check from_template: DF.Link | None inter_company_journal_entry_reference: DF.Link | None is_opening: DF.Literal["No", "Yes"] @@ -73,11 +74,13 @@ class JournalEntry(AccountsController): paid_loan: DF.Data | None pay_to_recd_from: DF.Data | None payment_order: DF.Link | None + periodic_entry_difference_account: DF.Link | None posting_date: DF.Date process_deferred_accounting: DF.Link | None remark: DF.SmallText | None reversal_of: DF.Link | None select_print_heading: DF.Link | None + stock_asset_account: DF.Link | None stock_entry: DF.Link | None tax_withholding_category: DF.Link | None title: DF.Data | None @@ -101,6 +104,7 @@ class JournalEntry(AccountsController): "Opening Entry", "Depreciation Entry", "Asset Disposal", + "Periodic Accounting Entry", "Exchange Rate Revaluation", "Exchange Gain Or Loss", "Deferred Revenue", @@ -198,6 +202,76 @@ class JournalEntry(AccountsController): self.update_inter_company_jv() self.update_invoice_discounting() + @frappe.whitelist() + def get_balance_for_periodic_accounting(self): + self.validate_company_for_periodic_accounting() + + stock_accounts = self.get_stock_accounts_for_periodic_accounting() + self.set("accounts", []) + for account in stock_accounts: + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( + account, self.posting_date, self.company + ) + + difference_value = flt(stock_bal - account_bal, self.precision("difference")) + + if difference_value == 0: + frappe.msgprint( + _("No difference found for stock account {0}").format(frappe.bold(account)), + alert=True, + ) + continue + + self.append( + "accounts", + { + "account": account, + "debit_in_account_currency": difference_value if difference_value > 0 else 0, + "credit_in_account_currency": abs(difference_value) if difference_value < 0 else 0, + }, + ) + + self.append( + "accounts", + { + "account": self.periodic_entry_difference_account, + "credit_in_account_currency": difference_value if difference_value > 0 else 0, + "debit_in_account_currency": abs(difference_value) if difference_value < 0 else 0, + }, + ) + + def validate_company_for_periodic_accounting(self): + if erpnext.is_perpetual_inventory_enabled(self.company): + frappe.throw( + _( + "Periodic Accounting Entry is not allowed for company {0} with perpetual inventory enabled" + ).format(self.company) + ) + + if not self.periodic_entry_difference_account: + frappe.throw(_("Please select Periodic Accounting Entry Difference Account")) + + def get_stock_accounts_for_periodic_accounting(self): + if self.voucher_type != "Periodic Accounting Entry": + return [] + + if self.for_all_stock_asset_accounts: + return frappe.get_all( + "Account", + filters={ + "company": self.company, + "account_type": "Stock", + "root_type": "Asset", + "is_group": 0, + }, + pluck="name", + ) + + if not self.stock_asset_account: + frappe.throw(_("Please select Stock Asset Account")) + + return [self.stock_asset_account] + def on_update_after_submit(self): # Flag will be set on Reconciliation # Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost. @@ -280,6 +354,10 @@ class JournalEntry(AccountsController): frappe.throw(_("Account {0} should be of type Expense").format(d.account)) def validate_stock_accounts(self): + if self.voucher_type == "Periodic Accounting Entry": + # Skip validation for periodic accounting entry + return + stock_accounts = get_stock_accounts(self.company, accounts=self.accounts) for account in stock_accounts: account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 0ae619b3049..5140ef3b481 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2016,6 +2016,75 @@ class TestStockEntry(IntegrationTestCase): self.assertEqual(se.items[0].basic_rate, 300) + def test_periodic_accounting_entries(self): + item_code = "_Test Periodic Accounting Item" + make_item(item_code, {"is_stock_item": 1}) + + company = "_Test Periodic Accounting Company" + + frappe.get_doc( + { + "doctype": "Company", + "company_name": company, + "abbr": "_TPC", + "default_currency": "INR", + "enable_perpetual_inventory": 0, + } + ).insert(ignore_permissions=True) + + warehouse = frappe.db.get_value("Warehouse", {"company": company, "is_group": 0}, "name") + + make_stock_entry( + item_code=item_code, + qty=10, + to_warehouse=warehouse, + basic_rate=100, + posting_date=add_days(nowdate(), -2), + ) + + jv = frappe.new_doc("Journal Entry") + jv.voucher_type = "Periodic Accounting Entry" + jv.posting_date = add_days(nowdate(), -1) + jv.posting_time = nowtime() + jv.company = company + jv.for_all_stock_asset_accounts = 1 + jv.periodic_entry_difference_account = "Stock Adjustment - _TPC" + jv.get_balance_for_periodic_accounting() + jv.save() + jv.submit() + + self.assertEqual(len(jv.accounts), 2) + self.assertEqual(jv.accounts[0].debit_in_account_currency, 1000) + self.assertEqual(jv.accounts[1].credit_in_account_currency, 1000) + self.assertEqual(jv.accounts[0].account, "Stock In Hand - _TPC") + self.assertEqual(jv.accounts[1].account, "Stock Adjustment - _TPC") + + make_stock_entry( + item_code=item_code, + qty=5, + from_warehouse=warehouse, + company=company, + posting_date=nowdate(), + posting_time=nowtime(), + ) + + jv = frappe.new_doc("Journal Entry") + jv.voucher_type = "Periodic Accounting Entry" + jv.posting_date = nowdate() + jv.posting_time = nowtime() + jv.company = company + jv.for_all_stock_asset_accounts = 1 + jv.periodic_entry_difference_account = "Stock Adjustment - _TPC" + jv.get_balance_for_periodic_accounting() + jv.save() + jv.submit() + + self.assertEqual(len(jv.accounts), 2) + self.assertEqual(jv.accounts[0].credit_in_account_currency, 500) + self.assertEqual(jv.accounts[1].debit_in_account_currency, 500) + self.assertEqual(jv.accounts[0].account, "Stock In Hand - _TPC") + self.assertEqual(jv.accounts[1].account, "Stock Adjustment - _TPC") + def make_serialized_item(self, **args): args = frappe._dict(args)