Compare commits

...

14 Commits

Author SHA1 Message Date
khushi8112
ef54f6b802 fix: use correct date value 2025-06-26 15:44:41 +05:30
khushi8112
104b823b2b chore: fix typo 2025-06-26 14:35:48 +05:30
khushi8112
72084fd3fe fix: use correct field name 2025-06-26 14:30:32 +05:30
khushi8112
4cf901885d fix: validate pending reposting till acc frozen date 2025-06-26 11:52:16 +05:30
Khushi Rawat
1f347cbd8f chore: resolved conflicts 2025-06-26 11:52:16 +05:30
Khushi Rawat
5124c29d22 chore: remove debug flag accidentally left in code 2025-06-26 11:52:16 +05:30
Khushi Rawat
4be1172bda fix: update validation and test cases 2025-06-26 11:52:16 +05:30
Khushi Rawat
fd2f459995 chore: migration patch for account freezing fields 2025-06-26 11:52:16 +05:30
Khushi Rawat
89faeeb131 refactor: remove accounts freezing settings from accounts settings 2025-06-26 11:52:16 +05:30
Khushi Rawat
c51bcfce9b refactor: get frozen accounts settings from Company in tests 2025-06-26 11:52:15 +05:30
Khushi Rawat
7f8f4abbd3 refactor: get frozen accounts settings from Company in patches 2025-06-26 11:52:15 +05:30
Khushi Rawat
ea9c1e910e refactor: get frozen accounts settings from Company in Deferred Revenue 2025-06-26 11:52:15 +05:30
Khushi Rawat
456f5baf35 refactor: updated logic in depreciation and gl to validate acc frozen date company wise 2025-06-26 11:52:15 +05:30
Khushi Rawat
c26f7ad921 feat: move frozen account settings to Company for company-specific configuration 2025-06-26 11:52:15 +05:30
24 changed files with 239 additions and 170 deletions

View File

@@ -7,6 +7,7 @@ from frappe.utils import (
cint,
date_diff,
flt,
formatdate,
get_first_day,
get_last_day,
get_link_to_form,
@@ -317,7 +318,7 @@ def get_already_booked_amount(doc, item):
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
accounts_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
accounts_frozen_upto = frappe.db.get_value("Company", doc.company, "accounts_frozen_till_date")
def _book_deferred_revenue_or_expense(
item,

View File

@@ -92,8 +92,10 @@ class Account(NestedSet):
super().on_update()
def onload(self):
frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
role_allowed_for_frozen_entries = frappe.db.get_value(
"Company", self.company, "role_allowed_for_frozen_entries"
)
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries in frappe.get_roles():
self.set_onload("can_freeze_account", True)
def autoname(self):
@@ -270,10 +272,10 @@ class Account(NestedSet):
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
return
frozen_accounts_modifier = frappe.get_cached_value(
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
role_allowed_for_frozen_entries = frappe.get_cached_value(
"Company", self.company, "role_allowed_for_frozen_entries"
)
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries not in frappe.get_roles():
throw(_("You are not authorized to set Frozen value"))
def validate_balance_must_be_debit_or_credit(self):

View File

@@ -74,10 +74,8 @@
"book_asset_depreciation_entry_automatically",
"closing_settings_tab",
"period_closing_settings_section",
"acc_frozen_upto",
"ignore_account_closing_balance",
"column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
"show_balance_in_coa",
"banking_tab",
@@ -98,21 +96,6 @@
"use_new_budget_controller"
],
"fields": [
{
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
"fieldname": "acc_frozen_upto",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Accounts Frozen Till Date"
},
{
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
"fieldname": "frozen_accounts_modifier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
"options": "Role"
},
{
"default": "Billing Address",
"description": "Address used to determine Tax Category in transactions",

View File

@@ -11,7 +11,6 @@ from frappe.model.document import Document
from frappe.utils import cint
from erpnext.accounts.utils import sync_auto_reconcile_config
from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document):
@@ -23,7 +22,6 @@ class AccountsSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
acc_frozen_upto: DF.Date | None
add_taxes_from_item_tax_template: DF.Check
add_taxes_from_taxes_and_charges_template: DF.Check
allow_multi_currency_invoices_against_single_party_account: DF.Check
@@ -49,7 +47,6 @@ class AccountsSettings(Document):
enable_immutable_ledger: DF.Check
enable_party_matching: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
frozen_accounts_modifier: DF.Link | None
general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check
ignore_is_opening_check_for_reporting: DF.Check
@@ -98,9 +95,6 @@ class AccountsSettings(Document):
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
self.enable_payment_schedule_in_print()
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
self.validate_pending_reposts()
if clear_cache:
frappe.clear_cache()
@@ -127,10 +121,6 @@ class AccountsSettings(Document):
validate_fields_for_doctype=False,
)
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)
def validate_and_sync_auto_reconcile_config(self):
if self.has_value_changed("auto_reconciliation_job_trigger"):
if (

View File

@@ -1,11 +1,16 @@
frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) {
frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
frm.set_df_property(
"frozen_accounts_modifier",
"label",
"Role Allowed to Close Books & Make Changes to Closed Periods"
);
frm.set_df_property("credit_controller", "label", "Credit Manager");
},
});
frappe.ui.form.on("Company", {
refresh: function (frm) {
frm.set_df_property("accounts_frozen_till_date", "label", "Books Closed Through");
frm.set_df_property(
"role_allowed_for_frozen_entries",
"label",
"Role Allowed to Close Books & Make Changes to Closed Periods"
);
},
});

View File

@@ -94,7 +94,7 @@ class GLEntry(Document):
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
validate_frozen_account(self.company, self.account, adv_adj)
if (
self.voucher_type == "Journal Entry"
@@ -271,7 +271,7 @@ class GLEntry(Document):
)
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
validate_party_frozen_disabled(self.company, self.party_type, self.party)
validate_account_party_type(self)
def validate_currency(self):
@@ -395,16 +395,16 @@ def update_outstanding_amt(
ref_doc.set_status(update=True)
def validate_frozen_account(account, adv_adj=None):
def validate_frozen_account(company, account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == "Yes" and not adv_adj:
frozen_accounts_modifier = frappe.get_cached_value(
"Accounts Settings", None, "frozen_accounts_modifier"
role_allowed_for_frozen_entries = frappe.get_cached_value(
"Company", company, "role_allowed_for_frozen_entries"
)
if not frozen_accounts_modifier:
if not role_allowed_for_frozen_entries:
frappe.throw(_("Account {0} is frozen").format(account))
elif frozen_accounts_modifier not in frappe.get_roles():
elif role_allowed_for_frozen_entries not in frappe.get_roles():
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))

View File

@@ -161,7 +161,7 @@ class PaymentLedgerEntry(Document):
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
validate_frozen_account(self.account, adv_adj)
validate_frozen_account(self.company, self.account, adv_adj)
if not self.delinked:
self.validate_account_details()
self.validate_dimensions_for_pl_and_bs()

View File

@@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import create_item
class TestProcessDeferredAccounting(IntegrationTestCase):
def test_creation_of_ledger_entry_on_submit(self):
"""test creation of gl entries on submission of document"""
change_acc_settings(acc_frozen_upto="2023-05-31", book_deferred_entries_based_on="Months")
change_acc_settings(acc_frozen_till_date="2023-05-31", book_deferred_entries_based_on="Months")
deferred_account = create_account(
account_name="Deferred Revenue for Accounts Frozen",
@@ -79,8 +79,10 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
pda.cancel()
def change_acc_settings(acc_frozen_upto="", book_deferred_entries_based_on="Days"):
def change_acc_settings(
company="_Test Company", acc_frozen_till_date=None, book_deferred_entries_based_on="Days"
):
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.acc_frozen_upto = acc_frozen_upto
acc_settings.book_deferred_entries_based_on = book_deferred_entries_based_on
frappe.db.set_value("Company", company, "accounts_frozen_till_date", acc_frozen_till_date)
acc_settings.save()

View File

@@ -63,7 +63,8 @@ class TestSalesInvoice(ERPNextTestSuite):
set_default_account_for_mode_of_payment(
mode_of_payment, "_Test Company with perpetual inventory", "_Test Bank - TCP1"
)
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
for company in frappe.get_all("Company", pluck="name"):
frappe.db.set_value("Company", company, "accounts_frozen_till_date", None)
@change_settings(
"Accounts Settings",
@@ -3374,8 +3375,8 @@ class TestSalesInvoice(ERPNextTestSuite):
si.commission_rate = commission_rate
self.assertRaises(frappe.ValidationError, si.save)
@IntegrationTestCase.change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.save()
@@ -3383,6 +3384,7 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, si.submit)
si.posting_date = getdate()
si.submit()
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
@IntegrationTestCase.change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_over_billing_case_against_delivery_note(self):
@@ -3449,7 +3451,7 @@ class TestSalesInvoice(ERPNextTestSuite):
si.save()
si.submit()
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", getdate("2019-01-31"))
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", getdate("2019-01-31"))
pda1 = frappe.get_doc(
dict(

View File

@@ -26,7 +26,7 @@ class TestSubscription(IntegrationTestCase):
make_plans()
create_parties()
reset_settings()
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
def tearDown(self):
frappe.db.rollback()

View File

@@ -387,7 +387,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
dimension_filter_map = get_dimension_filter_map()
if gl_map:
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
check_freezing_date(gl_map[0]["posting_date"], gl_map[0]["company"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
if gl_map[0]["voucher_type"] != "Period Closing Voucher":
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
@@ -734,7 +734,7 @@ def make_reverse_gl_entries(
make_entry(new_gle, adv_adj, "Yes")
def check_freezing_date(posting_date, adv_adj=False):
def check_freezing_date(posting_date, company, adv_adj=False):
"""
Nobody can do GL Entries where posting date is before freezing date
except authorized person
@@ -743,17 +743,17 @@ def check_freezing_date(posting_date, adv_adj=False):
Hence stop admin to bypass if accounts are freezed
"""
if not adv_adj:
acc_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
if acc_frozen_upto:
frozen_accounts_modifier = frappe.get_single_value(
"Accounts Settings", "frozen_accounts_modifier"
acc_frozen_till_date = frappe.db.get_value("Company", company, "accounts_frozen_till_date")
if acc_frozen_till_date:
frozen_accounts_modifier = frappe.db.get_value(
"Company", company, "role_allowed_for_frozen_entries"
)
if getdate(posting_date) <= getdate(acc_frozen_upto) and (
if getdate(posting_date) <= getdate(acc_frozen_till_date) and (
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
):
frappe.throw(
_("You are not authorized to add or update entries before {0}").format(
formatdate(acc_frozen_upto)
formatdate(acc_frozen_till_date)
)
)

View File

@@ -795,7 +795,7 @@ def get_payment_terms_template(party_name, party_type, company=None):
return template
def validate_party_frozen_disabled(party_type, party_name):
def validate_party_frozen_disabled(company, party_type, party_name):
if frappe.flags.ignore_party_validation:
return
@@ -805,10 +805,10 @@ def validate_party_frozen_disabled(party_type, party_name):
if party.disabled:
frappe.throw(_("{0} {1} is disabled").format(party_type, party_name), PartyDisabled)
elif party.get("is_frozen"):
frozen_accounts_modifier = frappe.get_single_value(
"Accounts Settings", "frozen_accounts_modifier"
role_allowed_for_frozen_entries = frappe.get_cached_value(
"Company", company, "role_allowed_for_frozen_entries"
)
if frozen_accounts_modifier not in frappe.get_roles():
if role_allowed_for_frozen_entries not in frappe.get_roles():
frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen)
elif party_type == "Employee":

View File

@@ -96,13 +96,26 @@ def get_depreciable_assets_data(date):
.orderby(a.creation, order=Order.desc)
)
acc_frozen_upto = get_acc_frozen_upto()
if acc_frozen_upto:
res = res.where(ds.schedule_date > acc_frozen_upto)
companies_with_frozen_limits = get_companies_with_frozen_limits()
res = res.run()
for company, frozen_upto in companies_with_frozen_limits.items():
res = res.where((a.company != company) | (ds.schedule_date > frozen_upto))
return res
return res.run()
def get_companies_with_frozen_limits():
companies_with_frozen_limits = {}
for d in frappe.get_all(
"Company", fields=["name", "accounts_frozen_till_date", "role_allowed_for_frozen_entries"]
):
if not d.accounts_frozen_till_date:
continue
if d.role_allowed_for_frozen_entries in frappe.get_roles() or frappe.session.user == "Administrator":
continue
companies_with_frozen_limits[d.name] = getdate(d.accounts_frozen_till_date)
return companies_with_frozen_limits
def make_depreciation_entry_on_disposal(asset_doc, disposal_date=None):
@@ -111,20 +124,6 @@ def make_depreciation_entry_on_disposal(asset_doc, disposal_date=None):
make_depreciation_entry(depr_schedule_name, disposal_date)
def get_acc_frozen_upto():
acc_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
if not acc_frozen_upto:
return
frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator":
return getdate(acc_frozen_upto)
return
def get_credit_debit_accounts_for_asset(asset_category, company):
# Returns credit and debit accounts for the given asset category and company.
(_, accumulated_depr_account, depr_expense_account) = get_depreciation_accounts(asset_category, company)

View File

@@ -2314,7 +2314,7 @@ class AccountsController(TransactionBase):
def validate_party(self):
party_type, party = self.get_party()
validate_party_frozen_disabled(party_type, party)
validate_party_frozen_disabled(self.company, party_type, party)
def get_party(self):
party_type = None

View File

@@ -263,6 +263,7 @@ erpnext.patches.v15_0.rename_subcontracting_fields
erpnext.patches.v15_0.unset_incorrect_additional_discount_percentage
[post_model_sync]
erpnext.patches.v15_0.migrate_account_freezing_settings_to_company
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes #22-02-2024

View File

@@ -10,15 +10,17 @@ def execute():
purchase_invoices = frappe.db.sql(
"""
select
parenttype as type, parent as name
PI.company, PI_ADV.parenttype as type, PI_ADV.parent as name
from
`tabPurchase Invoice Advance`
`tabPurchase Invoice Advance` as PI_ADV join `tabPurchase Invoice` as PI
on
PI_ADV.parent = PI.name
where
ref_exchange_rate = 1
and docstatus = 1
and ifnull(exchange_gain_loss, 0) != 0
PI_ADV.ref_exchange_rate = 1
and PI_ADV.docstatus = 1
and ifnull(PI_ADV.exchange_gain_loss, 0) != 0
group by
parent
PI_ADV.parent
""",
as_dict=1,
)
@@ -26,15 +28,17 @@ def execute():
sales_invoices = frappe.db.sql(
"""
select
parenttype as type, parent as name
SI.company, SI_ADV.parenttype as type, SI_ADV.parent as name
from
`tabSales Invoice Advance`
`tabSales Invoice Advance` as SI_ADV join `tabSales Invoice` as SI
on
SI_ADV.parent = SI.name
where
ref_exchange_rate = 1
and docstatus = 1
and ifnull(exchange_gain_loss, 0) != 0
SI_ADV.ref_exchange_rate = 1
and SI_ADV.docstatus = 1
and ifnull(SI_ADV.exchange_gain_loss, 0) != 0
group by
parent
SI_ADV.parent
""",
as_dict=1,
)
@@ -45,11 +49,21 @@ def execute():
message=json.dumps(purchase_invoices + sales_invoices, indent=2),
)
acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
if acc_frozen_upto:
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
original_frozen_dates = {}
for invoice in purchase_invoices + sales_invoices:
company = invoice.company
# Unfreeze only once per company
if company not in original_frozen_dates:
accounts_frozen_till_date = frappe.get_cached_value(
"Company", company, "accounts_frozen_till_date"
)
original_frozen_dates[company] = accounts_frozen_till_date
if accounts_frozen_till_date:
frappe.db.set_value("Company", company, "accounts_frozen_till_date", None)
try:
doc = frappe.get_doc(invoice.type, invoice.name)
doc.docstatus = 2
@@ -64,5 +78,6 @@ def execute():
frappe.db.rollback()
print(f"Failed to correct gl entries of {invoice.name}")
if acc_frozen_upto:
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", acc_frozen_upto)
for company, frozen_date in original_frozen_dates.items():
if frozen_date:
frappe.db.set_value("Company", company, "accounts_frozen_till_date", frozen_date)

View File

@@ -9,70 +9,72 @@ def execute():
# Migrate schema of all uncancelled dunnings
filters = {"docstatus": ("!=", 2)}
for company in frappe.get_all("Company", pluck="name"):
filters["company"] = company
can_edit_accounts_after = get_accounts_closing_date(company)
if can_edit_accounts_after:
# Get dunnings after the date when accounts were frozen/closed
filters["posting_date"] = (">", can_edit_accounts_after)
can_edit_accounts_after = get_accounts_closing_date()
if can_edit_accounts_after:
# Get dunnings after the date when accounts were frozen/closed
filters["posting_date"] = (">", can_edit_accounts_after)
all_dunnings = frappe.get_all("Dunning", filters=filters, pluck="name")
all_dunnings = frappe.get_all("Dunning", filters=filters, pluck="name")
for dunning_name in all_dunnings:
dunning = frappe.get_doc("Dunning", dunning_name)
if not dunning.sales_invoice:
# nothing we can do
continue
for dunning_name in all_dunnings:
dunning = frappe.get_doc("Dunning", dunning_name)
if not dunning.sales_invoice:
# nothing we can do
continue
if dunning.overdue_payments:
# something's already here, doesn't need patching
continue
if dunning.overdue_payments:
# something's already here, doesn't need patching
continue
payment_schedules = frappe.get_all(
"Payment Schedule",
filters={"parent": dunning.sales_invoice},
fields=[
"parent as sales_invoice",
"name as payment_schedule",
"payment_term",
"due_date",
"invoice_portion",
"payment_amount",
# at the time of creating this dunning, the full amount was outstanding
"payment_amount as outstanding",
"'0' as paid_amount",
"discounted_amount",
],
)
payment_schedules = frappe.get_all(
"Payment Schedule",
filters={"parent": dunning.sales_invoice},
fields=[
"parent as sales_invoice",
"name as payment_schedule",
"payment_term",
"due_date",
"invoice_portion",
"payment_amount",
# at the time of creating this dunning, the full amount was outstanding
"payment_amount as outstanding",
"'0' as paid_amount",
"discounted_amount",
],
)
dunning.extend("overdue_payments", payment_schedules)
dunning.validate()
dunning.extend("overdue_payments", payment_schedules)
dunning.validate()
dunning.flags.ignore_validate_update_after_submit = True
dunning.save()
dunning.flags.ignore_validate_update_after_submit = True
dunning.save()
# Reverse entries only if dunning is submitted and not resolved
if dunning.docstatus == 1 and dunning.status != "Resolved":
# With the new logic, dunning amount gets recorded as additional income
# at time of payment. We don't want to record the dunning amount twice,
# so we reverse previous GL Entries that recorded the dunning amount at
# time of submission of the Dunning.
make_reverse_gl_entries(voucher_type="Dunning", voucher_no=dunning.name)
# Reverse entries only if dunning is submitted and not resolved
if dunning.docstatus == 1 and dunning.status != "Resolved":
# With the new logic, dunning amount gets recorded as additional income
# at time of payment. We don't want to record the dunning amount twice,
# so we reverse previous GL Entries that recorded the dunning amount at
# time of submission of the Dunning.
make_reverse_gl_entries(voucher_type="Dunning", voucher_no=dunning.name)
def get_accounts_closing_date():
def get_accounts_closing_date(company):
"""Get the date when accounts were frozen/closed"""
accounts_frozen_till = frappe.db.get_single_value(
"Accounts Settings", "acc_frozen_upto"
) # always returns datetime.date
accounts_frozen_till_date = frappe.db.get_value("Company", company, "accounts_frozen_till_date")
period_closing_date = frappe.db.get_value(
"Period Closing Voucher", {"docstatus": 1}, "period_end_date", order_by="period_end_date desc"
"Period Closing Voucher",
{"docstatus": 1, "company": company},
"period_end_date",
order_by="period_end_date desc",
)
# Set most recent frozen/closing date as filter
if accounts_frozen_till and period_closing_date:
can_edit_accounts_after = max(accounts_frozen_till, period_closing_date)
if accounts_frozen_till_date and period_closing_date:
can_edit_accounts_after = max(accounts_frozen_till_date, period_closing_date)
else:
can_edit_accounts_after = accounts_frozen_till or period_closing_date
can_edit_accounts_after = accounts_frozen_till_date or period_closing_date
return can_edit_accounts_after

View File

@@ -0,0 +1,18 @@
import frappe
def execute():
accounts_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
accounts_frozen_till_date = accounts_settings.acc_frozen_upto
frozen_accounts_modifier = accounts_settings.frozen_accounts_modifier
for company in frappe.get_all("Company", pluck="name"):
frappe.db.set_value(
"Company",
company,
{
"accounts_frozen_till_date": accounts_frozen_till_date,
"role_allowed_for_frozen_entries": frozen_accounts_modifier,
},
)

View File

@@ -58,9 +58,9 @@ def execute():
):
posting_date = period_closing_voucher[0].period_end_date
acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
if acc_frozen_upto and getdate(acc_frozen_upto) > getdate(posting_date):
posting_date = acc_frozen_upto
acc_frozen_till_date = frappe.db.get_value("Company", company, "accounts_frozen_till_date")
if acc_frozen_till_date and getdate(acc_frozen_till_date) > getdate(posting_date):
posting_date = acc_frozen_till_date
stock_frozen_upto = frappe.db.get_single_value("Stock Settings", "stock_frozen_upto")
if stock_frozen_upto and getdate(stock_frozen_upto) > getdate(posting_date):

View File

@@ -94,6 +94,11 @@
"depreciation_cost_center",
"capital_work_in_progress_account",
"asset_received_but_not_billed",
"accounts_closing_tab",
"accounts_closing_section",
"accounts_frozen_till_date",
"column_break_tawz",
"role_allowed_for_frozen_entries",
"buying_and_selling_tab",
"sales_settings",
"default_buying_terms",
@@ -834,14 +839,42 @@
"fieldtype": "Select",
"label": "Reconciliation Takes Effect On",
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
},
{
"description": "Accounting entries are frozen up to this date. Only users with the specified role can create or modify entries before this date.",
"fieldname": "accounts_frozen_till_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Accounts Frozen Till Date"
},
{
"fieldname": "accounts_closing_tab",
"fieldtype": "Tab Break",
"label": "Accounts Closing"
},
{
"fieldname": "column_break_tawz",
"fieldtype": "Column Break"
},
{
"fieldname": "role_allowed_for_frozen_entries",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Roles Allowed to Set and Edit Frozen Account Entries",
"options": "Role"
},
{
"fieldname": "accounts_closing_section",
"fieldtype": "Section Break"
}
],
"grid_page_length": 50,
"icon": "fa fa-building",
"idx": 1,
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-01-09 20:12:25.471544",
"modified": "2025-05-30 14:33:32.171699",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
@@ -903,9 +936,10 @@
"select": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -16,6 +16,7 @@ from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency
from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges
from erpnext.stock.utils import check_pending_reposting
class Company(NestedSet):
@@ -28,6 +29,7 @@ class Company(NestedSet):
from frappe.types import DF
abbr: DF.Data
accounts_frozen_till_date: DF.Date | None
accumulated_depreciation_account: DF.Link | None
allow_account_creation_against_child_company: DF.Check
asset_received_but_not_billed: DF.Link | None
@@ -92,6 +94,7 @@ class Company(NestedSet):
]
registration_details: DF.Code | None
rgt: DF.Int
role_allowed_for_frozen_entries: DF.Link | None
round_off_account: DF.Link | None
round_off_cost_center: DF.Link | None
round_off_for_opening: DF.Link | None
@@ -153,6 +156,7 @@ class Company(NestedSet):
self.check_parent_changed()
self.set_chart_of_accounts()
self.validate_parent_company()
self.validate_pending_reposts()
def validate_abbr(self):
if not self.abbr:
@@ -490,6 +494,11 @@ class Company(NestedSet):
if not is_group:
frappe.throw(_("Parent Company must be a group company"))
def validate_pending_reposts(self):
if self.accounts_frozen_till_date:
if self.accounts_frozen_till_date:
check_pending_reposting(posting_date=self.accounts_frozen_till_date, company=self.name)
def set_default_accounts(self):
default_accounts = {
"default_cash_account": "Cash",

View File

@@ -138,16 +138,19 @@ class RepostItemValuation(Document):
return query[0][0] if query and query[0][0] else None
def validate_accounts_freeze(self):
acc_settings = frappe.get_cached_doc("Accounts Settings")
if not acc_settings.acc_frozen_upto:
acc_frozen_till_date = frappe.db.get_value("Company", self.company, "accounts_frozen_till_date")
frozen_accounts_modifier = frappe.db.get_value(
"Company", self.company, "role_allowed_for_frozen_entries"
)
if not acc_frozen_till_date:
return
if getdate(self.posting_date) <= getdate(acc_settings.acc_frozen_upto):
if acc_settings.frozen_accounts_modifier and frappe.session.user in get_users_with_role(
acc_settings.frozen_accounts_modifier
if getdate(self.posting_date) <= getdate(acc_frozen_till_date):
if frozen_accounts_modifier and frappe.session.user in get_users_with_role(
frozen_accounts_modifier
):
frappe.msgprint(_("Caution: This might alter frozen accounts."))
return
frappe.throw(_("You cannot repost item valuation before {}").format(acc_settings.acc_frozen_upto))
frappe.throw(_("You cannot repost item valuation before {}").format(acc_frozen_till_date))
def reset_field_values(self):
if self.based_on == "Transaction":

View File

@@ -356,6 +356,7 @@ class TestRepostItemValuation(IntegrationTestCase, StockTestMixin):
riv = frappe.get_doc(
doctype="Repost Item Valuation",
item_code="_Test Item",
company="_Test Company",
warehouse="_Test Warehouse - _TC",
based_on="Item and Warehouse",
posting_date=today,
@@ -363,15 +364,15 @@ class TestRepostItemValuation(IntegrationTestCase, StockTestMixin):
)
riv.flags.dont_run_in_test = True # keep it queued
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.acc_frozen_upto = today
accounts_settings.frozen_accounts_modifier = ""
accounts_settings.save()
company = frappe.get_doc("Company", "_Test Company")
company.accounts_frozen_till_date = today
company.role_allowed_for_frozen_entries = ""
company.save()
self.assertRaises(frappe.ValidationError, riv.save)
accounts_settings.acc_frozen_upto = ""
accounts_settings.save()
company.accounts_frozen_till_date = ""
company.save()
@IntegrationTestCase.change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_create_repost_entry_for_cancelled_document(self):

View File

@@ -554,7 +554,7 @@ def is_reposting_item_valuation_in_progress():
)
def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
def check_pending_reposting(posting_date: str, company: str | None = None, throw_error: bool = True) -> bool:
"""Check if there are pending reposting job till the specified posting date."""
filters = {
@@ -562,6 +562,8 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool
"status": ["in", ["Queued", "In Progress"]],
"posting_date": ["<=", posting_date],
}
if company:
filters["company"] = company
reposting_pending = frappe.db.exists("Repost Item Valuation", filters)
if reposting_pending and throw_error: