mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-22 02:40:02 +00:00
* fix: validate reverse GL entries on current date under immutable ledger When Immutable Ledger is enabled, the reverse GL entry is posted on the current date, but the closed-period checks in make_reverse_gl_entries still validate against the original (backdated) posting date. This blocks cancelling a backdated voucher, such as a suspense Journal Entry for a migrated NPA loan, with a books-closed error even though the reverse entry lands in an open period. Validate both check_freezing_date and validate_against_pcv against the current date when Immutable Ledger is enabled. When it is disabled, behaviour is unchanged. Follow-up to #55268. * test: reset frozen till date after reverse entry test The freeze date set on the company was not reset, so it leaked into the next test which posts entries in that period. Reset it in a finally block. * fix: prefer explicit posting_date under immutable ledger Prefer the posting_date argument before frappe.form_dict and getdate, at both the validation and the GL entry site, so an explicit date passed by the caller is honoured and validation still matches the posted date.
740 lines
22 KiB
Python
740 lines
22 KiB
Python
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: GNU General Public License v3. See license.txt
|
|
|
|
|
|
import copy
|
|
|
|
import frappe
|
|
from frappe import _
|
|
from frappe.model.meta import get_field_precision
|
|
from frappe.utils import cint, flt, get_link_to_form, getdate, now
|
|
from frappe.utils.caching import request_cache
|
|
|
|
import erpnext
|
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
|
get_accounting_dimensions,
|
|
get_checks_for_pl_and_bs_accounts,
|
|
)
|
|
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
|
get_dimension_filter_map,
|
|
)
|
|
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
|
from erpnext.accounts.services.gl_validator import (
|
|
check_freezing_date,
|
|
validate_accounting_period,
|
|
validate_against_pcv,
|
|
validate_allowed_dimensions,
|
|
validate_cwip_accounts,
|
|
validate_disabled_accounts,
|
|
)
|
|
from erpnext.accounts.utils import create_payment_ledger_entry, is_immutable_ledger_enabled
|
|
from erpnext.controllers.budget_controller import BudgetValidation
|
|
|
|
|
|
def make_gl_entries(
|
|
gl_map,
|
|
cancel=False,
|
|
adv_adj=False,
|
|
merge_entries=True,
|
|
update_outstanding="Yes",
|
|
from_repost=False,
|
|
):
|
|
if gl_map:
|
|
if (
|
|
not cancel
|
|
and not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
|
|
and gl_map[0].voucher_type != "Period Closing Voucher"
|
|
):
|
|
bud_val = BudgetValidation(gl_map=gl_map)
|
|
bud_val.validate()
|
|
|
|
if not cancel:
|
|
make_acc_dimensions_offsetting_entry(gl_map)
|
|
validate_accounting_period(gl_map)
|
|
validate_disabled_accounts(gl_map)
|
|
gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost)
|
|
if gl_map and len(gl_map) > 1:
|
|
if gl_map[0].voucher_type != "Period Closing Voucher":
|
|
create_payment_ledger_entry(
|
|
gl_map,
|
|
cancel=0,
|
|
adv_adj=adv_adj,
|
|
update_outstanding=update_outstanding,
|
|
from_repost=from_repost,
|
|
)
|
|
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
|
# Post GL Map process there may no be any GL Entries
|
|
elif gl_map:
|
|
frappe.throw(
|
|
_(
|
|
"Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."
|
|
)
|
|
)
|
|
else:
|
|
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
|
|
|
|
|
def make_acc_dimensions_offsetting_entry(gl_map):
|
|
accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(
|
|
gl_map, gl_map[0].company
|
|
)
|
|
no_of_dimensions = len(accounting_dimensions_to_offset)
|
|
if no_of_dimensions == 0:
|
|
return
|
|
|
|
offsetting_entries = []
|
|
|
|
for gle in gl_map:
|
|
for dimension in accounting_dimensions_to_offset:
|
|
offsetting_entry = gle.copy()
|
|
debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0
|
|
credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0
|
|
offsetting_entry.update(
|
|
{
|
|
"account": dimension.offsetting_account,
|
|
"debit": debit,
|
|
"credit": credit,
|
|
"debit_in_account_currency": debit,
|
|
"credit_in_account_currency": credit,
|
|
"remarks": _("Offsetting for Accounting Dimension") + f" - {dimension.name}",
|
|
"against_voucher": None,
|
|
"account_currency": dimension.account_currency,
|
|
# Party Type and Party are restricted to Receivable and Payable accounts
|
|
"party_type": None,
|
|
"party": None,
|
|
}
|
|
)
|
|
offsetting_entry["against_voucher_type"] = None
|
|
offsetting_entries.append(offsetting_entry)
|
|
|
|
gl_map += offsetting_entries
|
|
|
|
|
|
def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
|
|
acc_dimension = frappe.qb.DocType("Accounting Dimension")
|
|
dimension_detail = frappe.qb.DocType("Accounting Dimension Detail")
|
|
|
|
acc_dimensions = (
|
|
frappe.qb.from_(acc_dimension)
|
|
.inner_join(dimension_detail)
|
|
.on(acc_dimension.name == dimension_detail.parent)
|
|
.select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account)
|
|
.where(
|
|
(acc_dimension.disabled == 0)
|
|
& (dimension_detail.company == company)
|
|
& (dimension_detail.automatically_post_balancing_accounting_entry == 1)
|
|
)
|
|
).run(as_dict=True)
|
|
|
|
accounting_dimensions_to_offset = []
|
|
for acc_dimension in acc_dimensions:
|
|
values = set([entry.get(acc_dimension.fieldname) for entry in gl_map])
|
|
acc_dimension.account_currency = frappe.get_cached_value(
|
|
"Account", acc_dimension.offsetting_account, "account_currency"
|
|
)
|
|
if len(values) > 1:
|
|
accounting_dimensions_to_offset.append(acc_dimension)
|
|
|
|
return accounting_dimensions_to_offset
|
|
|
|
|
|
def process_gl_map(gl_map, merge_entries=True, precision=None, from_repost=False):
|
|
if not gl_map:
|
|
return []
|
|
|
|
if gl_map[0].voucher_type != "Period Closing Voucher":
|
|
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision, from_repost)
|
|
|
|
if merge_entries:
|
|
gl_map = merge_similar_entries(gl_map, precision)
|
|
|
|
gl_map = toggle_debit_credit_if_negative(gl_map)
|
|
|
|
return gl_map
|
|
|
|
|
|
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_repost=False):
|
|
round_off_account, default_currency = frappe.get_cached_value(
|
|
"Company", gl_map[0].company, ["round_off_account", "default_currency"]
|
|
)
|
|
if not precision:
|
|
precision = get_field_precision(
|
|
frappe.get_meta("GL Entry").get_field("debit"),
|
|
currency=default_currency,
|
|
)
|
|
|
|
new_gl_map = []
|
|
for d in gl_map:
|
|
cost_center = d.get("cost_center")
|
|
|
|
cost_center_allocation = get_cost_center_allocation_data(
|
|
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
|
)
|
|
|
|
if not cost_center_allocation:
|
|
new_gl_map.append(d)
|
|
continue
|
|
|
|
# Validate budget against main cost center
|
|
if not from_repost:
|
|
validate_expense_against_budget(
|
|
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
|
|
)
|
|
|
|
if d.account == round_off_account:
|
|
d.cost_center = cost_center_allocation[0][0]
|
|
new_gl_map.append(d)
|
|
continue
|
|
|
|
for sub_cost_center, percentage in cost_center_allocation:
|
|
gle = copy.deepcopy(d)
|
|
gle.cost_center = sub_cost_center
|
|
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
|
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
|
new_gl_map.append(gle)
|
|
|
|
return new_gl_map
|
|
|
|
|
|
@request_cache
|
|
def get_cost_center_allocation_data(company, posting_date, cost_center):
|
|
cost_center_allocation = frappe.db.get_value(
|
|
"Cost Center Allocation",
|
|
{
|
|
"docstatus": 1,
|
|
"company": company,
|
|
"valid_from": ("<=", posting_date),
|
|
"main_cost_center": cost_center,
|
|
},
|
|
pluck=True,
|
|
order_by="valid_from desc",
|
|
)
|
|
|
|
if not cost_center_allocation:
|
|
return []
|
|
|
|
records = frappe.db.get_all(
|
|
"Cost Center Allocation Percentage",
|
|
{"parent": cost_center_allocation},
|
|
["cost_center", "percentage"],
|
|
as_list=True,
|
|
)
|
|
|
|
return records
|
|
|
|
|
|
def merge_similar_entries(gl_map, precision=None):
|
|
merged_gl_map = []
|
|
accounting_dimensions = get_accounting_dimensions()
|
|
merge_properties = get_merge_properties(accounting_dimensions)
|
|
|
|
for entry in gl_map:
|
|
if entry._skip_merge:
|
|
merged_gl_map.append(entry)
|
|
continue
|
|
|
|
entry.merge_key = get_merge_key(entry, merge_properties)
|
|
# if there is already an entry in this account then just add it
|
|
# to that entry
|
|
same_head = check_if_in_list(entry, merged_gl_map)
|
|
if same_head:
|
|
same_head.debit = flt(same_head.debit) + flt(entry.debit)
|
|
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
|
entry.debit_in_account_currency
|
|
)
|
|
same_head.debit_in_transaction_currency = flt(same_head.debit_in_transaction_currency) + flt(
|
|
entry.debit_in_transaction_currency
|
|
)
|
|
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
|
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
|
entry.credit_in_account_currency
|
|
)
|
|
same_head.credit_in_transaction_currency = flt(same_head.credit_in_transaction_currency) + flt(
|
|
entry.credit_in_transaction_currency
|
|
)
|
|
else:
|
|
merged_gl_map.append(entry)
|
|
|
|
company = gl_map[0].company if gl_map else erpnext.get_default_company()
|
|
company_currency = erpnext.get_company_currency(company)
|
|
|
|
if not precision:
|
|
precision = get_field_precision(
|
|
frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency
|
|
)
|
|
|
|
# filter zero debit and credit entries
|
|
merged_gl_map = filter(
|
|
lambda x: flt(x.debit, precision) != 0
|
|
or flt(x.credit, precision) != 0
|
|
or (
|
|
x.voucher_type == "Journal Entry"
|
|
and frappe.get_cached_value("Journal Entry", x.voucher_no, "voucher_type")
|
|
== "Exchange Gain Or Loss"
|
|
),
|
|
merged_gl_map,
|
|
)
|
|
merged_gl_map = list(merged_gl_map)
|
|
|
|
return merged_gl_map
|
|
|
|
|
|
def get_merge_properties(dimensions=None):
|
|
merge_properties = [
|
|
"account",
|
|
"cost_center",
|
|
"party",
|
|
"party_type",
|
|
"voucher_detail_no",
|
|
"against_voucher",
|
|
"against_voucher_type",
|
|
"project",
|
|
"finance_book",
|
|
"voucher_no",
|
|
"advance_voucher_type",
|
|
"advance_voucher_no",
|
|
]
|
|
if dimensions:
|
|
merge_properties.extend(dimensions)
|
|
return merge_properties
|
|
|
|
|
|
def get_merge_key(entry, merge_properties):
|
|
merge_key = []
|
|
for fieldname in merge_properties:
|
|
merge_key.append(entry.get(fieldname, ""))
|
|
|
|
return tuple(merge_key)
|
|
|
|
|
|
def check_if_in_list(gle, gl_map):
|
|
for e in gl_map:
|
|
if e.merge_key == gle.merge_key:
|
|
return e
|
|
|
|
|
|
def toggle_debit_credit_if_negative(gl_map):
|
|
debit_credit_field_map = {
|
|
"debit": "credit",
|
|
"debit_in_account_currency": "credit_in_account_currency",
|
|
"debit_in_transaction_currency": "credit_in_transaction_currency",
|
|
}
|
|
|
|
for entry in gl_map:
|
|
# toggle debit, credit if negative entry
|
|
for debit_field, credit_field in debit_credit_field_map.items():
|
|
debit = flt(entry.get(debit_field))
|
|
credit = flt(entry.get(credit_field))
|
|
|
|
if debit < 0 and credit < 0 and debit == credit:
|
|
debit *= -1
|
|
credit *= -1
|
|
|
|
if debit < 0:
|
|
credit = credit - debit
|
|
debit = 0.0
|
|
|
|
if credit < 0:
|
|
debit = debit - credit
|
|
credit = 0.0
|
|
|
|
# update net values
|
|
# In some scenarios net value needs to be shown in the ledger
|
|
# This method updates net values as debit or credit
|
|
if entry.post_net_value and debit and credit:
|
|
if debit > credit:
|
|
debit = debit - credit
|
|
credit = 0.0
|
|
|
|
else:
|
|
credit = credit - debit
|
|
debit = 0.0
|
|
|
|
entry[debit_field] = debit
|
|
entry[credit_field] = credit
|
|
|
|
return gl_map
|
|
|
|
|
|
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
|
if not from_repost:
|
|
validate_cwip_accounts(gl_map)
|
|
|
|
process_debit_credit_difference(gl_map)
|
|
|
|
dimension_filter_map = get_dimension_filter_map()
|
|
if gl_map:
|
|
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"])
|
|
|
|
for entry in gl_map:
|
|
validate_allowed_dimensions(entry, dimension_filter_map)
|
|
make_entry(entry, adv_adj, update_outstanding, from_repost)
|
|
|
|
|
|
def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
|
gle = frappe.new_doc("GL Entry")
|
|
gle.update(args)
|
|
gle.flags.ignore_permissions = 1
|
|
gle.flags.from_repost = from_repost
|
|
gle.flags.adv_adj = adv_adj
|
|
gle.flags.update_outstanding = update_outstanding or "Yes"
|
|
gle.flags.notify_update = False
|
|
if gle.is_cancelled or is_immutable_ledger_enabled():
|
|
gle.flags.ignore_links = True
|
|
gle.submit()
|
|
|
|
if (
|
|
not from_repost
|
|
and gle.voucher_type != "Period Closing Voucher"
|
|
and (gle.is_cancelled == 0 or gle.voucher_type == "Journal Entry")
|
|
):
|
|
validate_expense_against_budget(args)
|
|
|
|
|
|
def process_debit_credit_difference(gl_map):
|
|
precision = get_field_precision(
|
|
frappe.get_meta("GL Entry").get_field("debit"),
|
|
currency=frappe.get_cached_value("Company", gl_map[0].company, "default_currency"),
|
|
)
|
|
|
|
voucher_type = gl_map[0].voucher_type
|
|
voucher_no = gl_map[0].voucher_no
|
|
allowance = get_debit_credit_allowance(voucher_type, precision)
|
|
|
|
debit_credit_diff, trx_cur_debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
|
|
|
if abs(debit_credit_diff) > allowance:
|
|
if not (
|
|
voucher_type == "Journal Entry"
|
|
and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
|
|
== "Exchange Gain Or Loss"
|
|
):
|
|
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
|
|
|
|
elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
|
|
make_round_off_gle(gl_map, debit_credit_diff, trx_cur_debit_credit_diff, precision)
|
|
|
|
debit_credit_diff, trx_cur_debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
|
if abs(debit_credit_diff) > allowance:
|
|
if not (
|
|
voucher_type == "Journal Entry"
|
|
and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
|
|
== "Exchange Gain Or Loss"
|
|
):
|
|
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
|
|
|
|
|
|
def get_debit_credit_difference(gl_map, precision):
|
|
debit_credit_diff = 0.0
|
|
trx_cur_debit_credit_diff = 0
|
|
|
|
for entry in gl_map:
|
|
entry.debit = flt(entry.debit, precision)
|
|
entry.credit = flt(entry.credit, precision)
|
|
debit_credit_diff += entry.debit - entry.credit
|
|
|
|
entry.debit_in_transaction_currency = flt(entry.debit_in_transaction_currency, precision)
|
|
entry.credit_in_transaction_currency = flt(entry.credit_in_transaction_currency, precision)
|
|
trx_cur_debit_credit_diff += (
|
|
entry.debit_in_transaction_currency - entry.credit_in_transaction_currency
|
|
)
|
|
|
|
debit_credit_diff = flt(debit_credit_diff, precision)
|
|
trx_cur_debit_credit_diff = flt(trx_cur_debit_credit_diff, precision)
|
|
|
|
return debit_credit_diff, trx_cur_debit_credit_diff
|
|
|
|
|
|
def get_debit_credit_allowance(voucher_type, precision):
|
|
if voucher_type in ("Journal Entry", "Payment Entry"):
|
|
allowance = 5.0 / (10**precision)
|
|
else:
|
|
allowance = 0.5
|
|
|
|
return allowance
|
|
|
|
|
|
def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no):
|
|
frappe.throw(
|
|
_("Debit and Credit not equal for {0} #{1}. Difference is {2}.").format(
|
|
voucher_type, voucher_no, debit_credit_diff
|
|
)
|
|
)
|
|
|
|
|
|
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, trx_cur_debit_credit_diff, precision):
|
|
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 == account:
|
|
round_off_gle = d
|
|
if d.debit:
|
|
debit_credit_diff -= flt(d.debit) - flt(d.credit)
|
|
else:
|
|
debit_credit_diff += flt(d.credit)
|
|
round_off_account_exists = True
|
|
|
|
if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
|
|
gl_map.remove(round_off_gle)
|
|
return
|
|
|
|
if not round_off_gle:
|
|
for k in ["voucher_type", "voucher_no", "company", "posting_date", "remarks"]:
|
|
round_off_gle[k] = gl_map[0][k]
|
|
|
|
round_off_gle.update(
|
|
{
|
|
"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,
|
|
"credit": debit_credit_diff if debit_credit_diff > 0 else 0,
|
|
"debit_in_transaction_currency": abs(trx_cur_debit_credit_diff)
|
|
if trx_cur_debit_credit_diff < 0
|
|
else 0,
|
|
"credit_in_transaction_currency": trx_cur_debit_credit_diff
|
|
if trx_cur_debit_credit_diff > 0
|
|
else 0,
|
|
"cost_center": round_off_cost_center,
|
|
"party_type": None,
|
|
"party": None,
|
|
"is_opening": "No",
|
|
"against_voucher_type": None,
|
|
"against_voucher": None,
|
|
}
|
|
)
|
|
|
|
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)
|
|
|
|
|
|
def update_accounting_dimensions(round_off_gle):
|
|
dimensions = get_accounting_dimensions()
|
|
meta = frappe.get_meta(round_off_gle["voucher_type"])
|
|
has_all_dimensions = True
|
|
|
|
for dimension in dimensions:
|
|
if not meta.has_field(dimension):
|
|
has_all_dimensions = False
|
|
|
|
if dimensions and has_all_dimensions:
|
|
dimension_values = frappe.db.get_value(
|
|
round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1
|
|
)
|
|
|
|
for dimension in dimensions:
|
|
round_off_gle[dimension] = dimension_values.get(dimension)
|
|
else:
|
|
report_type = frappe.get_cached_value("Account", round_off_gle.account, "report_type")
|
|
for dimension in get_checks_for_pl_and_bs_accounts():
|
|
if (
|
|
round_off_gle.company == dimension.company
|
|
and (
|
|
(report_type == "Profit and Loss" and dimension.mandatory_for_pl)
|
|
or (report_type == "Balance Sheet" and dimension.mandatory_for_bs)
|
|
)
|
|
and dimension.default_dimension
|
|
):
|
|
round_off_gle[dimension.fieldname] = dimension.default_dimension
|
|
|
|
|
|
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False):
|
|
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:
|
|
round_off_account = frappe.get_cached_value("Company", company, "default_expense_account")
|
|
|
|
meta = frappe.get_meta(voucher_type)
|
|
|
|
# Give first preference to parent cost center for round off GLE
|
|
if not use_company_default and meta.has_field("cost_center"):
|
|
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
|
|
if parent_cost_center:
|
|
round_off_cost_center = parent_cost_center
|
|
|
|
if not round_off_account:
|
|
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 '{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, round_off_for_opening
|
|
|
|
|
|
def make_reverse_gl_entries(
|
|
gl_entries=None,
|
|
voucher_type=None,
|
|
voucher_no=None,
|
|
adv_adj=False,
|
|
update_outstanding="Yes",
|
|
partial_cancel=False,
|
|
posting_date=None,
|
|
):
|
|
"""
|
|
Get original gl entries of the voucher
|
|
and make reverse gl entries by swapping debit and credit
|
|
"""
|
|
|
|
immutable_ledger_enabled = is_immutable_ledger_enabled()
|
|
|
|
if not gl_entries:
|
|
gl_entry = frappe.qb.DocType("GL Entry")
|
|
gl_entries = (
|
|
frappe.qb.from_(gl_entry)
|
|
.select("*")
|
|
.where(gl_entry.voucher_type == voucher_type)
|
|
.where(gl_entry.voucher_no == voucher_no)
|
|
.where(gl_entry.is_cancelled == 0)
|
|
.for_update()
|
|
).run(as_dict=1)
|
|
|
|
if gl_entries:
|
|
create_payment_ledger_entry(
|
|
gl_entries,
|
|
cancel=1,
|
|
adv_adj=adv_adj,
|
|
update_outstanding=update_outstanding,
|
|
partial_cancel=partial_cancel,
|
|
)
|
|
validate_accounting_period(gl_entries)
|
|
|
|
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
|
|
|
|
if immutable_ledger_enabled:
|
|
validation_date = posting_date or frappe.form_dict.get("posting_date") or getdate()
|
|
else:
|
|
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
|
|
|
|
check_freezing_date(validation_date, gl_entries[0]["company"], adv_adj)
|
|
validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"])
|
|
|
|
if partial_cancel:
|
|
# Partial cancel is only used by `Advance` in separate account feature.
|
|
# Only cancel GL entries for unlinked reference using `voucher_detail_no`
|
|
gle = frappe.qb.DocType("GL Entry")
|
|
for x in gl_entries:
|
|
query = (
|
|
frappe.qb.update(gle)
|
|
.set(gle.modified, now())
|
|
.set(gle.modified_by, frappe.session.user)
|
|
.where(
|
|
(gle.company == x.company)
|
|
& (gle.account == x.account)
|
|
& (gle.party_type == x.party_type)
|
|
& (gle.party == x.party)
|
|
& (gle.voucher_type == x.voucher_type)
|
|
& (gle.voucher_no == x.voucher_no)
|
|
& (gle.against_voucher_type == x.against_voucher_type)
|
|
& (gle.against_voucher == x.against_voucher)
|
|
& (gle.voucher_detail_no == x.voucher_detail_no)
|
|
)
|
|
)
|
|
|
|
if not immutable_ledger_enabled:
|
|
query = query.set(gle.is_cancelled, 1) # smallint column; postgres rejects boolean true
|
|
|
|
query.run()
|
|
else:
|
|
if not immutable_ledger_enabled:
|
|
gle_names = [x.get("name") for x in gl_entries]
|
|
|
|
# if names are available, cancel only that set of entries
|
|
if not all(gle_names):
|
|
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
|
else:
|
|
gle = frappe.qb.DocType("GL Entry")
|
|
(
|
|
frappe.qb.update(gle)
|
|
.set(gle.is_cancelled, 1)
|
|
.set(gle.modified, now())
|
|
.set(gle.modified_by, frappe.session.user)
|
|
.where(gle.name.isin(gle_names) & (gle.is_cancelled == 0))
|
|
).run()
|
|
|
|
for entry in gl_entries:
|
|
new_gle = copy.deepcopy(entry)
|
|
new_gle["name"] = None
|
|
debit = new_gle.get("debit", 0)
|
|
credit = new_gle.get("credit", 0)
|
|
|
|
debit_in_account_currency = new_gle.get("debit_in_account_currency", 0)
|
|
credit_in_account_currency = new_gle.get("credit_in_account_currency", 0)
|
|
debit_in_transaction_currency = new_gle.get("debit_in_transaction_currency", 0)
|
|
credit_in_transaction_currency = new_gle.get("credit_in_transaction_currency", 0)
|
|
|
|
new_gle["debit"] = credit
|
|
new_gle["credit"] = debit
|
|
new_gle["debit_in_account_currency"] = credit_in_account_currency
|
|
new_gle["credit_in_account_currency"] = debit_in_account_currency
|
|
new_gle["debit_in_transaction_currency"] = credit_in_transaction_currency
|
|
new_gle["credit_in_transaction_currency"] = debit_in_transaction_currency
|
|
|
|
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
|
new_gle["is_cancelled"] = 1
|
|
|
|
if immutable_ledger_enabled:
|
|
new_gle["is_cancelled"] = 0
|
|
new_gle["posting_date"] = posting_date or frappe.form_dict.get("posting_date") or getdate()
|
|
elif posting_date:
|
|
new_gle["posting_date"] = posting_date
|
|
|
|
if new_gle["debit"] or new_gle["credit"]:
|
|
make_entry(new_gle, adv_adj, "Yes")
|
|
|
|
|
|
def set_as_cancel(voucher_type, voucher_no):
|
|
"""
|
|
Set is_cancelled=1 in all original gl entries for the voucher
|
|
"""
|
|
gle = frappe.qb.DocType("GL Entry")
|
|
(
|
|
frappe.qb.update(gle)
|
|
.set(gle.is_cancelled, 1)
|
|
.set(gle.modified, now())
|
|
.set(gle.modified_by, frappe.session.user)
|
|
.where((gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no) & (gle.is_cancelled == 0))
|
|
).run()
|