mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-27 00:44:45 +00:00
new feature: validation on account negative balance
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"creation": "2013-01-19 10:23:33",
|
"creation": "2013-01-30 12:49:46",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"modified": "2013-01-29 16:27:57",
|
"modified": "2013-02-05 15:38:32",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"owner": "Administrator"
|
"owner": "Administrator"
|
||||||
},
|
},
|
||||||
@@ -232,6 +232,15 @@
|
|||||||
"options": "[Select]",
|
"options": "[Select]",
|
||||||
"permlevel": 0
|
"permlevel": 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"depends_on": "eval:doc.group_or_ledger==\"Ledger\"",
|
||||||
|
"doctype": "DocField",
|
||||||
|
"fieldname": "allow_negative_balance",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow Negative Balance",
|
||||||
|
"permlevel": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"doctype": "DocField",
|
"doctype": "DocField",
|
||||||
"fieldname": "lft",
|
"fieldname": "lft",
|
||||||
@@ -262,13 +271,21 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"doctype": "DocPerm",
|
||||||
|
"permlevel": 0,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cancel": 0,
|
"cancel": 0,
|
||||||
"create": 0,
|
"create": 0,
|
||||||
"doctype": "DocPerm",
|
"doctype": "DocPerm",
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"role": "Auditor",
|
"role": "Auditor",
|
||||||
"write": 1
|
"write": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cancel": 0,
|
"cancel": 0,
|
||||||
@@ -290,9 +307,9 @@
|
|||||||
"cancel": 0,
|
"cancel": 0,
|
||||||
"create": 0,
|
"create": 0,
|
||||||
"doctype": "DocPerm",
|
"doctype": "DocPerm",
|
||||||
"permlevel": 0,
|
"permlevel": 2,
|
||||||
"role": "Accounts User",
|
"role": "Auditor",
|
||||||
"write": 1
|
"write": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cancel": 1,
|
"cancel": 1,
|
||||||
@@ -309,5 +326,13 @@
|
|||||||
"permlevel": 2,
|
"permlevel": 2,
|
||||||
"role": "Accounts Manager",
|
"role": "Accounts Manager",
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 0,
|
||||||
|
"create": 0,
|
||||||
|
"doctype": "DocPerm",
|
||||||
|
"permlevel": 2,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"write": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import webnotes
|
import webnotes
|
||||||
|
|
||||||
from webnotes.utils import flt, fmt_money, get_first_day, get_last_day, getdate
|
from webnotes.utils import flt, fmt_money, getdate
|
||||||
from webnotes.model.code import get_obj
|
from webnotes.model.code import get_obj
|
||||||
from webnotes import msgprint
|
from webnotes import msgprint, _
|
||||||
|
|
||||||
sql = webnotes.conn.sql
|
sql = webnotes.conn.sql
|
||||||
|
|
||||||
@@ -27,122 +27,6 @@ class DocType:
|
|||||||
def __init__(self,d,dl):
|
def __init__(self,d,dl):
|
||||||
self.doc, self.doclist = d, dl
|
self.doc, self.doclist = d, dl
|
||||||
|
|
||||||
# Validate mandatory
|
|
||||||
#-------------------
|
|
||||||
def check_mandatory(self):
|
|
||||||
# Following fields are mandatory in GL Entry
|
|
||||||
mandatory = ['account','remarks','voucher_type','voucher_no','fiscal_year','company']
|
|
||||||
for k in mandatory:
|
|
||||||
if not self.doc.fields.get(k):
|
|
||||||
msgprint("%s is mandatory for GL Entry" % k, raise_exception=1)
|
|
||||||
|
|
||||||
# Zero value transaction is not allowed
|
|
||||||
if not (flt(self.doc.debit) or flt(self.doc.credit)):
|
|
||||||
msgprint("GL Entry: Debit or Credit amount is mandatory for %s" % self.doc.account)
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
def pl_must_have_cost_center(self):
|
|
||||||
if sql("select name from tabAccount where name=%s and is_pl_account='Yes'", self.doc.account):
|
|
||||||
if not self.doc.cost_center and self.doc.voucher_type != 'Period Closing Voucher':
|
|
||||||
msgprint("Error: Cost Center must be specified for PL Account: %s" %
|
|
||||||
self.doc.account, raise_exception=1)
|
|
||||||
else: # not pl
|
|
||||||
if self.doc.cost_center:
|
|
||||||
self.doc.cost_center = ''
|
|
||||||
|
|
||||||
# Account must be ledger, active and not freezed
|
|
||||||
#-----------------------------------------------
|
|
||||||
def validate_account_details(self, adv_adj):
|
|
||||||
ret = sql("select group_or_ledger, docstatus, freeze_account, company from tabAccount where name=%s", self.doc.account)
|
|
||||||
|
|
||||||
# 1. Checks whether Account type is group or ledger
|
|
||||||
if ret and ret[0][0]=='Group':
|
|
||||||
msgprint("Error: All accounts must be Ledgers. Account %s is a group" % self.doc.account)
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
# 2. Checks whether Account is active
|
|
||||||
if ret and ret[0][1]==2:
|
|
||||||
msgprint("Error: All accounts must be Active. Account %s moved to Trash" % self.doc.account)
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
# 3. Account has been freezed for other users except account manager
|
|
||||||
if ret and ret[0][2]== 'Yes' and not adv_adj and not 'Accounts Manager' in webnotes.user.get_roles():
|
|
||||||
msgprint("Error: Account %s has been freezed. Only Accounts Manager can do transaction against this account." % self.doc.account)
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
# 4. Check whether account is within the company
|
|
||||||
if ret and ret[0][3] != self.doc.company:
|
|
||||||
msgprint("Account: %s does not belong to the company: %s" % (self.doc.account, self.doc.company))
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
# Posting date must be in selected fiscal year and fiscal year is active
|
|
||||||
#-------------------------------------------------------------------------
|
|
||||||
def validate_posting_date(self):
|
|
||||||
fy = sql("select docstatus, year_start_date from `tabFiscal Year` where name=%s ", self.doc.fiscal_year)
|
|
||||||
ysd = fy[0][1]
|
|
||||||
yed = get_last_day(get_first_day(ysd,0,11))
|
|
||||||
pd = getdate(self.doc.posting_date)
|
|
||||||
if fy[0][0] == 2:
|
|
||||||
msgprint("Fiscal Year is not active. You can restore it from Trash")
|
|
||||||
raise Exception
|
|
||||||
if pd < ysd or pd > yed:
|
|
||||||
msgprint("Posting date must be in the Selected Financial Year")
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
|
|
||||||
# Nobody can do GL Entries where posting date is before freezing date except authorized person
|
|
||||||
#----------------------------------------------------------------------------------------------
|
|
||||||
def check_freezing_date(self, adv_adj):
|
|
||||||
if not adv_adj:
|
|
||||||
acc_frozen_upto = webnotes.conn.get_value('Global Defaults', None, 'acc_frozen_upto')
|
|
||||||
if acc_frozen_upto:
|
|
||||||
bde_auth_role = webnotes.conn.get_value( 'Global Defaults', None,'bde_auth_role')
|
|
||||||
if getdate(self.doc.posting_date) <= getdate(acc_frozen_upto) and not bde_auth_role in webnotes.user.get_roles():
|
|
||||||
msgprint("You are not authorized to do/modify back dated accounting entries before %s." % getdate(acc_frozen_upto).strftime('%d-%m-%Y'), raise_exception=1)
|
|
||||||
|
|
||||||
def update_outstanding_amt(self):
|
|
||||||
# get final outstanding amt
|
|
||||||
bal = flt(sql("select sum(debit)-sum(credit) from `tabGL Entry` where against_voucher=%s and against_voucher_type=%s and ifnull(is_cancelled,'No') = 'No'", (self.doc.against_voucher, self.doc.against_voucher_type))[0][0] or 0.0)
|
|
||||||
|
|
||||||
if self.doc.against_voucher_type=='Purchase Invoice':
|
|
||||||
# amount to debit
|
|
||||||
bal = -bal
|
|
||||||
|
|
||||||
# Validation : Outstanding can not be negative
|
|
||||||
if bal < 0 and self.doc.is_cancelled == 'No':
|
|
||||||
msgprint("""Outstanding for Voucher %s will become %s.
|
|
||||||
Outstanding cannot be less than zero. Please match exact outstanding.""" %
|
|
||||||
(self.doc.against_voucher, fmt_money(bal)))
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
# Update outstanding amt on against voucher
|
|
||||||
sql("update `tab%s` set outstanding_amount=%s where name='%s'"%
|
|
||||||
(self.doc.against_voucher_type, bal, self.doc.against_voucher))
|
|
||||||
|
|
||||||
|
|
||||||
# Total outstanding can not be greater than credit limit for any time for any customer
|
|
||||||
#---------------------------------------------------------------------------------------------
|
|
||||||
def check_credit_limit(self):
|
|
||||||
#check for user role Freezed
|
|
||||||
master_type=sql("select master_type, master_name from `tabAccount` where name='%s' " %self.doc.account)
|
|
||||||
tot_outstanding = 0 #needed when there is no GL Entry in the system for that acc head
|
|
||||||
if (self.doc.voucher_type=='Journal Voucher' or self.doc.voucher_type=='Sales Invoice') and (master_type and master_type[0][0]=='Customer' and master_type[0][1]):
|
|
||||||
dbcr = sql("select sum(debit),sum(credit) from `tabGL Entry` where account = '%s' and is_cancelled='No'" % self.doc.account)
|
|
||||||
if dbcr:
|
|
||||||
tot_outstanding = flt(dbcr[0][0])-flt(dbcr[0][1])+flt(self.doc.debit)-flt(self.doc.credit)
|
|
||||||
get_obj('Account',self.doc.account).check_credit_limit(self.doc.account, self.doc.company, tot_outstanding)
|
|
||||||
|
|
||||||
#for opening entry account can not be pl account
|
|
||||||
#-----------------------------------------------
|
|
||||||
def check_pl_account(self):
|
|
||||||
if self.doc.is_opening=='Yes':
|
|
||||||
is_pl_account=sql("select is_pl_account from `tabAccount` where name='%s'"%(self.doc.account))
|
|
||||||
if is_pl_account and is_pl_account[0][0]=='Yes':
|
|
||||||
msgprint("For opening balance entry account can not be a PL account")
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
# Validate
|
|
||||||
# --------
|
|
||||||
def validate(self): # not called on cancel
|
def validate(self): # not called on cancel
|
||||||
self.check_mandatory()
|
self.check_mandatory()
|
||||||
self.pl_must_have_cost_center()
|
self.pl_must_have_cost_center()
|
||||||
@@ -151,15 +35,131 @@ class DocType:
|
|||||||
self.check_credit_limit()
|
self.check_credit_limit()
|
||||||
self.check_pl_account()
|
self.check_pl_account()
|
||||||
|
|
||||||
# On Update
|
|
||||||
#----------
|
|
||||||
def on_update(self,adv_adj, cancel, update_outstanding = 'Yes'):
|
def on_update(self,adv_adj, cancel, update_outstanding = 'Yes'):
|
||||||
# Account must be ledger, active and not freezed
|
|
||||||
self.validate_account_details(adv_adj)
|
self.validate_account_details(adv_adj)
|
||||||
|
|
||||||
# Posting date must be after freezing date
|
|
||||||
self.check_freezing_date(adv_adj)
|
self.check_freezing_date(adv_adj)
|
||||||
|
self.check_negative_balance(adv_adj)
|
||||||
|
|
||||||
# Update outstanding amt on against voucher
|
# Update outstanding amt on against voucher
|
||||||
if self.doc.against_voucher and self.doc.against_voucher_type not in ('Journal Voucher','POS') and update_outstanding == 'Yes':
|
if self.doc.against_voucher and self.doc.against_voucher_type not in \
|
||||||
|
('Journal Voucher','POS') and update_outstanding == 'Yes':
|
||||||
self.update_outstanding_amt()
|
self.update_outstanding_amt()
|
||||||
|
|
||||||
|
def check_mandatory(self):
|
||||||
|
mandatory = ['account','remarks','voucher_type','voucher_no','fiscal_year','company']
|
||||||
|
for k in mandatory:
|
||||||
|
if not self.doc.fields.get(k):
|
||||||
|
msgprint(k + _(" is mandatory for GL Entry"), raise_exception=1)
|
||||||
|
|
||||||
|
# Zero value transaction is not allowed
|
||||||
|
if not (flt(self.doc.debit) or flt(self.doc.credit)):
|
||||||
|
msgprint(_("GL Entry: Debit or Credit amount is mandatory for ") + self.doc.account,
|
||||||
|
raise_exception=1)
|
||||||
|
|
||||||
|
def pl_must_have_cost_center(self):
|
||||||
|
if webnotes.conn.get_value("Account", self.doc.account, "is_pl_account") == "Yes":
|
||||||
|
if not self.doc.cost_center and self.doc.voucher_type != 'Period Closing Voucher':
|
||||||
|
msgprint(_("Cost Center must be specified for PL Account: ") + self.doc.account,
|
||||||
|
raise_exception=1)
|
||||||
|
else:
|
||||||
|
if self.doc.cost_center:
|
||||||
|
self.doc.cost_center = ""
|
||||||
|
|
||||||
|
def validate_posting_date(self):
|
||||||
|
from accounts.utils import get_fiscal_year
|
||||||
|
fiscal_year = get_fiscal_year(self.doc.posting_date)[0]
|
||||||
|
|
||||||
|
if fiscal_year != self.doc.fiscal_year:
|
||||||
|
msgprint(_("Posting date must be in the Selected Fiscal Year"), raise_exception=1)
|
||||||
|
|
||||||
|
def check_credit_limit(self):
|
||||||
|
master_type, master_name = webnotes.conn.get_value("Account",
|
||||||
|
self.doc.account, ["master_type", "master_name"])
|
||||||
|
|
||||||
|
tot_outstanding = 0 #needed when there is no GL Entry in the system for that acc head
|
||||||
|
if (self.doc.voucher_type=='Journal Voucher' or self.doc.voucher_type=='Sales Invoice') \
|
||||||
|
and (master_type =='Customer' and master_name):
|
||||||
|
dbcr = sql("""select sum(debit), sum(credit) from `tabGL Entry`
|
||||||
|
where account = '%s' and is_cancelled='No'""" % self.doc.account)
|
||||||
|
if dbcr:
|
||||||
|
tot_outstanding = flt(dbcr[0][0]) - flt(dbcr[0][1]) + \
|
||||||
|
flt(self.doc.debit) - flt(self.doc.credit)
|
||||||
|
get_obj('Account',self.doc.account).check_credit_limit(self.doc.account,
|
||||||
|
self.doc.company, tot_outstanding)
|
||||||
|
|
||||||
|
def check_pl_account(self):
|
||||||
|
if self.doc.is_opening=='Yes' and \
|
||||||
|
webnotes.conn.get_value("Account", self.doc.account, "is_pl_account") == "Yes":
|
||||||
|
msgprint(_("For opening balance entry account can not be a PL account"),
|
||||||
|
raise_exception=1)
|
||||||
|
|
||||||
|
def validate_account_details(self, adv_adj):
|
||||||
|
"""Account must be ledger, active and not freezed"""
|
||||||
|
|
||||||
|
ret = sql("""select group_or_ledger, docstatus, freeze_account, company
|
||||||
|
from tabAccount where name=%s""", self.doc.account, as_dict=1)
|
||||||
|
|
||||||
|
if ret and ret[0]["group_or_ledger"]=='Group':
|
||||||
|
msgprint(_("Account: ") + self.doc.account + _(" is not a ledger"), raise_exception=1)
|
||||||
|
|
||||||
|
if ret and ret[0]["docstatus"]==2:
|
||||||
|
msgprint(_("Account: ") + self.doc.account + _(" is not active"), raise_exception=1)
|
||||||
|
|
||||||
|
# Account has been freezed for other users except account manager
|
||||||
|
if ret and ret[0]["freeze_account"]== 'Yes' and not adv_adj \
|
||||||
|
and not 'Accounts Manager' in webnotes.user.get_roles():
|
||||||
|
msgprint(_("Account: ") + self.doc.account + _(" has been freezed. \
|
||||||
|
Only Accounts Manager can do transaction against this account"), raise_exception=1)
|
||||||
|
|
||||||
|
if ret and ret[0]["company"] != self.doc.company:
|
||||||
|
msgprint(_("Account: ") + self.doc.account + _(" does not belong to the company: ") +
|
||||||
|
self.doc.company, raise_exception=1)
|
||||||
|
|
||||||
|
def check_freezing_date(self, adv_adj):
|
||||||
|
"""
|
||||||
|
Nobody can do GL Entries where posting date is before freezing date
|
||||||
|
except authorized person
|
||||||
|
"""
|
||||||
|
if not adv_adj:
|
||||||
|
acc_frozen_upto = webnotes.conn.get_value('Global Defaults', None, 'acc_frozen_upto')
|
||||||
|
if acc_frozen_upto:
|
||||||
|
bde_auth_role = webnotes.conn.get_value( 'Global Defaults', None,'bde_auth_role')
|
||||||
|
if getdate(self.doc.posting_date) <= getdate(acc_frozen_upto) \
|
||||||
|
and not bde_auth_role in webnotes.user.get_roles():
|
||||||
|
msgprint(_("You are not authorized to do/modify back dated entries before ") +
|
||||||
|
getdate(acc_frozen_upto).strftime('%d-%m-%Y'), raise_exception=1)
|
||||||
|
|
||||||
|
def check_negative_balance(self, adv_adj):
|
||||||
|
if not adv_adj:
|
||||||
|
account = webnotes.conn.get_value("Account", self.doc.account,
|
||||||
|
["allow_negative_balance", "debit_or_credit"], as_dict=True)
|
||||||
|
if not account["allow_negative_balance"]:
|
||||||
|
balance = webnotes.conn.sql("""select sum(debit) - sum(credit) from `tabGL Entry`
|
||||||
|
where account = %s and ifnull(is_cancelled, 'No') = 'No'""", self.doc.account)
|
||||||
|
balance = account["debit_or_credit"] == "Debit" and \
|
||||||
|
balance[0][0] or -1*balance[0][0]
|
||||||
|
|
||||||
|
if flt(balance) < 0:
|
||||||
|
msgprint(_("Negative balance is not allowed for account ") + self.doc.account,
|
||||||
|
raise_exception=1)
|
||||||
|
|
||||||
|
def update_outstanding_amt(self):
|
||||||
|
# get final outstanding amt
|
||||||
|
bal = flt(sql("""select sum(debit) - sum(credit) from `tabGL Entry`
|
||||||
|
where against_voucher=%s and against_voucher_type=%s
|
||||||
|
and ifnull(is_cancelled,'No') = 'No'""",
|
||||||
|
(self.doc.against_voucher, self.doc.against_voucher_type))[0][0] or 0.0)
|
||||||
|
|
||||||
|
if self.doc.against_voucher_type=='Purchase Invoice':
|
||||||
|
# amount to debit
|
||||||
|
bal = -bal
|
||||||
|
|
||||||
|
# Validation : Outstanding can not be negative
|
||||||
|
if bal < 0 and self.doc.is_cancelled == 'No':
|
||||||
|
msgprint(_("Outstanding for Voucher ") + self.doc.against_voucher +
|
||||||
|
_(" will become ") + fmt_money(bal) + _("Outstanding cannot be less than zero. \
|
||||||
|
Please match exact outstanding."), raise_exception=1)
|
||||||
|
|
||||||
|
# Update outstanding amt on against voucher
|
||||||
|
sql("update `tab%s` set outstanding_amount=%s where name='%s'"%
|
||||||
|
(self.doc.against_voucher_type, bal, self.doc.against_voucher))
|
||||||
4
patches/february_2013/account_negative_balance.py
Normal file
4
patches/february_2013/account_negative_balance.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
def execute():
|
||||||
|
import webnotes
|
||||||
|
webnotes.reload_doc("accounts", "doctype", "Account")
|
||||||
|
webnotes.conn.sql("update `tabAccount` set allow_negative_balance = 1")
|
||||||
@@ -164,4 +164,5 @@ patch_list = [
|
|||||||
"patches.february_2013.reload_bom_replace_tool_permission",
|
"patches.february_2013.reload_bom_replace_tool_permission",
|
||||||
"patches.february_2013.payment_reconciliation_reset_values",
|
"patches.february_2013.payment_reconciliation_reset_values",
|
||||||
"patches.february_2013.remove_sales_order_pending_items",
|
"patches.february_2013.remove_sales_order_pending_items",
|
||||||
|
"patches.february_2013.account_negative_balance",
|
||||||
]
|
]
|
||||||
Reference in New Issue
Block a user