style: format code with black

This commit is contained in:
Ankush Menat 2022-03-28 18:52:46 +05:30
parent 21e00da3d6
commit 494bd9ef78
1395 changed files with 91704 additions and 62532 deletions

View File

@ -2,49 +2,57 @@ import inspect
import frappe import frappe
__version__ = '14.0.0-dev' __version__ = "14.0.0-dev"
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' """Get default company for user"""
from frappe.defaults import get_user_default_as_list from frappe.defaults import get_user_default_as_list
if not user: if not user:
user = frappe.session.user user = frappe.session.user
companies = get_user_default_as_list(user, 'company') companies = get_user_default_as_list(user, "company")
if companies: if companies:
default_company = companies[0] default_company = companies[0]
else: else:
default_company = frappe.db.get_single_value('Global Defaults', 'default_company') default_company = frappe.db.get_single_value("Global Defaults", "default_company")
return default_company return default_company
def get_default_currency(): def get_default_currency():
'''Returns the currency of the default company''' """Returns the currency of the default company"""
company = get_default_company() company = get_default_company()
if company: if company:
return frappe.get_cached_value('Company', company, 'default_currency') return frappe.get_cached_value("Company", company, "default_currency")
def get_default_cost_center(company): def get_default_cost_center(company):
'''Returns the default cost center of the company''' """Returns the default cost center of the company"""
if not company: if not company:
return None return None
if not frappe.flags.company_cost_center: if not frappe.flags.company_cost_center:
frappe.flags.company_cost_center = {} frappe.flags.company_cost_center = {}
if not company in frappe.flags.company_cost_center: if not company in frappe.flags.company_cost_center:
frappe.flags.company_cost_center[company] = frappe.get_cached_value('Company', company, 'cost_center') frappe.flags.company_cost_center[company] = frappe.get_cached_value(
"Company", company, "cost_center"
)
return frappe.flags.company_cost_center[company] return frappe.flags.company_cost_center[company]
def get_company_currency(company): def get_company_currency(company):
'''Returns the default company currency''' """Returns the default company currency"""
if not frappe.flags.company_currency: if not frappe.flags.company_currency:
frappe.flags.company_currency = {} frappe.flags.company_currency = {}
if not company in frappe.flags.company_currency: if not company in frappe.flags.company_currency:
frappe.flags.company_currency[company] = frappe.db.get_value('Company', company, 'default_currency', cache=True) frappe.flags.company_currency[company] = frappe.db.get_value(
"Company", company, "default_currency", cache=True
)
return frappe.flags.company_currency[company] return frappe.flags.company_currency[company]
def set_perpetual_inventory(enable=1, company=None): def set_perpetual_inventory(enable=1, company=None):
if not company: if not company:
company = "_Test Company" if frappe.flags.in_test else get_default_company() company = "_Test Company" if frappe.flags.in_test else get_default_company()
@ -53,9 +61,10 @@ def set_perpetual_inventory(enable=1, company=None):
company.enable_perpetual_inventory = enable company.enable_perpetual_inventory = enable
company.save() company.save()
def encode_company_abbr(name, company=None, abbr=None): def encode_company_abbr(name, company=None, abbr=None):
'''Returns name encoded with company abbreviation''' """Returns name encoded with company abbreviation"""
company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr") company_abbr = abbr or frappe.get_cached_value("Company", company, "abbr")
parts = name.rsplit(" - ", 1) parts = name.rsplit(" - ", 1)
if parts[-1].lower() != company_abbr.lower(): if parts[-1].lower() != company_abbr.lower():
@ -63,62 +72,69 @@ def encode_company_abbr(name, company=None, abbr=None):
return " - ".join(parts) return " - ".join(parts)
def is_perpetual_inventory_enabled(company): def is_perpetual_inventory_enabled(company):
if not company: if not company:
company = "_Test Company" if frappe.flags.in_test else get_default_company() company = "_Test Company" if frappe.flags.in_test else get_default_company()
if not hasattr(frappe.local, 'enable_perpetual_inventory'): if not hasattr(frappe.local, "enable_perpetual_inventory"):
frappe.local.enable_perpetual_inventory = {} frappe.local.enable_perpetual_inventory = {}
if not company in frappe.local.enable_perpetual_inventory: if not company in frappe.local.enable_perpetual_inventory:
frappe.local.enable_perpetual_inventory[company] = frappe.get_cached_value('Company', frappe.local.enable_perpetual_inventory[company] = (
company, "enable_perpetual_inventory") or 0 frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0
)
return frappe.local.enable_perpetual_inventory[company] return frappe.local.enable_perpetual_inventory[company]
def get_default_finance_book(company=None): def get_default_finance_book(company=None):
if not company: if not company:
company = get_default_company() company = get_default_company()
if not hasattr(frappe.local, 'default_finance_book'): if not hasattr(frappe.local, "default_finance_book"):
frappe.local.default_finance_book = {} frappe.local.default_finance_book = {}
if not company in frappe.local.default_finance_book: if not company in frappe.local.default_finance_book:
frappe.local.default_finance_book[company] = frappe.get_cached_value('Company', frappe.local.default_finance_book[company] = frappe.get_cached_value(
company, "default_finance_book") "Company", company, "default_finance_book"
)
return frappe.local.default_finance_book[company] return frappe.local.default_finance_book[company]
def get_party_account_type(party_type): def get_party_account_type(party_type):
if not hasattr(frappe.local, 'party_account_types'): if not hasattr(frappe.local, "party_account_types"):
frappe.local.party_account_types = {} frappe.local.party_account_types = {}
if not party_type in frappe.local.party_account_types: if not party_type in frappe.local.party_account_types:
frappe.local.party_account_types[party_type] = frappe.db.get_value("Party Type", frappe.local.party_account_types[party_type] = (
party_type, "account_type") or '' frappe.db.get_value("Party Type", party_type, "account_type") or ""
)
return frappe.local.party_account_types[party_type] return frappe.local.party_account_types[party_type]
def get_region(company=None): def get_region(company=None):
'''Return the default country based on flag, company or global settings """Return the default country based on flag, company or global settings
You can also set global company flag in `frappe.flags.company` You can also set global company flag in `frappe.flags.company`
''' """
if company or frappe.flags.company: if company or frappe.flags.company:
return frappe.get_cached_value('Company', return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
company or frappe.flags.company, 'country')
elif frappe.flags.country: elif frappe.flags.country:
return frappe.flags.country return frappe.flags.country
else: else:
return frappe.get_system_settings('country') return frappe.get_system_settings("country")
def allow_regional(fn): def allow_regional(fn):
'''Decorator to make a function regionally overridable """Decorator to make a function regionally overridable
Example: Example:
@erpnext.allow_regional @erpnext.allow_regional
def myfunction(): def myfunction():
pass''' pass"""
def caller(*args, **kwargs): def caller(*args, **kwargs):
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region()) overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())

View File

@ -21,37 +21,39 @@ class ERPNextAddress(Address):
return super(ERPNextAddress, self).link_address() return super(ERPNextAddress, self).link_address()
def update_compnay_address(self): def update_compnay_address(self):
for link in self.get('links'): for link in self.get("links"):
if link.link_doctype == 'Company': if link.link_doctype == "Company":
self.is_your_company_address = 1 self.is_your_company_address = 1
def validate_reference(self): def validate_reference(self):
if self.is_your_company_address and not [ if self.is_your_company_address and not [
row for row in self.links if row.link_doctype == "Company" row for row in self.links if row.link_doctype == "Company"
]: ]:
frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."), frappe.throw(
title=_("Company Not Linked")) _("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
title=_("Company Not Linked"),
)
def on_update(self): def on_update(self):
""" """
After Address is updated, update the related 'Primary Address' on Customer. After Address is updated, update the related 'Primary Address' on Customer.
""" """
address_display = get_address_display(self.as_dict()) address_display = get_address_display(self.as_dict())
filters = { "customer_primary_address": self.name } filters = {"customer_primary_address": self.name}
customers = frappe.db.get_all("Customer", filters=filters, as_list=True) customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
for customer_name in customers: for customer_name in customers:
frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display) frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display)
@frappe.whitelist() @frappe.whitelist()
def get_shipping_address(company, address = None): def get_shipping_address(company, address=None):
filters = [ filters = [
["Dynamic Link", "link_doctype", "=", "Company"], ["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company], ["Dynamic Link", "link_name", "=", company],
["Address", "is_your_company_address", "=", 1] ["Address", "is_your_company_address", "=", 1],
] ]
fields = ["*"] fields = ["*"]
if address and frappe.db.get_value('Dynamic Link', if address and frappe.db.get_value("Dynamic Link", {"parent": address, "link_name": company}):
{'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address]) filters.append(["Address", "name", "=", address])
if not address: if not address:
filters.append(["Address", "is_shipping_address", "=", 1]) filters.append(["Address", "is_shipping_address", "=", 1])

View File

@ -12,15 +12,24 @@ from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist() @frappe.whitelist()
@cache_source @cache_source
def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, def get(
to_date = None, timespan = None, time_interval = None, heatmap_year = None): chart_name=None,
chart=None,
no_cache=None,
filters=None,
from_date=None,
to_date=None,
timespan=None,
time_interval=None,
heatmap_year=None,
):
if chart_name: if chart_name:
chart = frappe.get_doc('Dashboard Chart', chart_name) chart = frappe.get_doc("Dashboard Chart", chart_name)
else: else:
chart = frappe._dict(frappe.parse_json(chart)) chart = frappe._dict(frappe.parse_json(chart))
timespan = chart.timespan timespan = chart.timespan
if chart.timespan == 'Select Date Range': if chart.timespan == "Select Date Range":
from_date = chart.from_date from_date = chart.from_date
to_date = chart.to_date to_date = chart.to_date
@ -31,17 +40,23 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
company = filters.get("company") company = filters.get("company")
if not account and chart_name: if not account and chart_name:
frappe.throw(_("Account is not set for the dashboard chart {0}") frappe.throw(
.format(get_link_to_form("Dashboard Chart", chart_name))) _("Account is not set for the dashboard chart {0}").format(
get_link_to_form("Dashboard Chart", chart_name)
)
)
if not frappe.db.exists("Account", account) and chart_name: if not frappe.db.exists("Account", account) and chart_name:
frappe.throw(_("Account {0} does not exists in the dashboard chart {1}") frappe.throw(
.format(account, get_link_to_form("Dashboard Chart", chart_name))) _("Account {0} does not exists in the dashboard chart {1}").format(
account, get_link_to_form("Dashboard Chart", chart_name)
)
)
if not to_date: if not to_date:
to_date = nowdate() to_date = nowdate()
if not from_date: if not from_date:
if timegrain in ('Monthly', 'Quarterly'): if timegrain in ("Monthly", "Quarterly"):
from_date = get_from_date_from_timespan(to_date, timespan) from_date = get_from_date_from_timespan(to_date, timespan)
# fetch dates to plot # fetch dates to plot
@ -54,16 +69,14 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
result = build_result(account, dates, gl_entries) result = build_result(account, dates, gl_entries)
return { return {
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result], "labels": [formatdate(r[0].strftime("%Y-%m-%d")) for r in result],
"datasets": [{ "datasets": [{"name": account, "values": [r[1] for r in result]}],
"name": account,
"values": [r[1] for r in result]
}]
} }
def build_result(account, dates, gl_entries): def build_result(account, dates, gl_entries):
result = [[getdate(date), 0.0] for date in dates] result = [[getdate(date), 0.0] for date in dates]
root_type = frappe.db.get_value('Account', account, 'root_type') root_type = frappe.db.get_value("Account", account, "root_type")
# start with the first date # start with the first date
date_index = 0 date_index = 0
@ -78,30 +91,34 @@ def build_result(account, dates, gl_entries):
result[date_index][1] += entry.debit - entry.credit result[date_index][1] += entry.debit - entry.credit
# if account type is credit, switch balances # if account type is credit, switch balances
if root_type not in ('Asset', 'Expense'): if root_type not in ("Asset", "Expense"):
for r in result: for r in result:
r[1] = -1 * r[1] r[1] = -1 * r[1]
# for balance sheet accounts, the totals are cumulative # for balance sheet accounts, the totals are cumulative
if root_type in ('Asset', 'Liability', 'Equity'): if root_type in ("Asset", "Liability", "Equity"):
for i, r in enumerate(result): for i, r in enumerate(result):
if i > 0: if i > 0:
r[1] = r[1] + result[i-1][1] r[1] = r[1] + result[i - 1][1]
return result return result
def get_gl_entries(account, to_date): def get_gl_entries(account, to_date):
child_accounts = get_descendants_of('Account', account, ignore_permissions=True) child_accounts = get_descendants_of("Account", account, ignore_permissions=True)
child_accounts.append(account) child_accounts.append(account)
return frappe.db.get_all('GL Entry', return frappe.db.get_all(
fields = ['posting_date', 'debit', 'credit'], "GL Entry",
filters = [ fields=["posting_date", "debit", "credit"],
dict(posting_date = ('<', to_date)), filters=[
dict(account = ('in', child_accounts)), dict(posting_date=("<", to_date)),
dict(voucher_type = ('!=', 'Period Closing Voucher')) dict(account=("in", child_accounts)),
dict(voucher_type=("!=", "Period Closing Voucher")),
], ],
order_by = 'posting_date asc') order_by="posting_date asc",
)
def get_dates_from_timegrain(from_date, to_date, timegrain): def get_dates_from_timegrain(from_date, to_date, timegrain):
days = months = years = 0 days = months = years = 0
@ -116,6 +133,8 @@ def get_dates_from_timegrain(from_date, to_date, timegrain):
dates = [get_period_ending(from_date, timegrain)] dates = [get_period_ending(from_date, timegrain)]
while getdate(dates[-1]) < getdate(to_date): while getdate(dates[-1]) < getdate(to_date):
date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain) date = get_period_ending(
add_to_date(dates[-1], years=years, months=months, days=days), timegrain
)
dates.append(date) dates.append(date)
return dates return dates

View File

@ -22,20 +22,23 @@ from erpnext.accounts.utils import get_account_currency
def validate_service_stop_date(doc): def validate_service_stop_date(doc):
''' Validates service_stop_date for Purchase Invoice and Sales Invoice ''' """Validates service_stop_date for Purchase Invoice and Sales Invoice"""
enable_check = "enable_deferred_revenue" \ enable_check = (
if doc.doctype=="Sales Invoice" else "enable_deferred_expense" "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
)
old_stop_dates = {} old_stop_dates = {}
old_doc = frappe.db.get_all("{0} Item".format(doc.doctype), old_doc = frappe.db.get_all(
{"parent": doc.name}, ["name", "service_stop_date"]) "{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"]
)
for d in old_doc: for d in old_doc:
old_stop_dates[d.name] = d.service_stop_date or "" old_stop_dates[d.name] = d.service_stop_date or ""
for item in doc.items: for item in doc.items:
if not item.get(enable_check): continue if not item.get(enable_check):
continue
if item.service_stop_date: if item.service_stop_date:
if date_diff(item.service_stop_date, item.service_start_date) < 0: if date_diff(item.service_stop_date, item.service_start_date) < 0:
@ -44,21 +47,31 @@ def validate_service_stop_date(doc):
if date_diff(item.service_stop_date, item.service_end_date) > 0: if date_diff(item.service_stop_date, item.service_end_date) > 0:
frappe.throw(_("Service Stop Date cannot be after Service End Date")) frappe.throw(_("Service Stop Date cannot be after Service End Date"))
if old_stop_dates and old_stop_dates.get(item.name) and item.service_stop_date!=old_stop_dates.get(item.name): if (
old_stop_dates
and old_stop_dates.get(item.name)
and item.service_stop_date != old_stop_dates.get(item.name)
):
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx)) frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
def build_conditions(process_type, account, company): def build_conditions(process_type, account, company):
conditions='' conditions = ""
deferred_account = "item.deferred_revenue_account" if process_type=="Income" else "item.deferred_expense_account" deferred_account = (
"item.deferred_revenue_account" if process_type == "Income" else "item.deferred_expense_account"
)
if account: if account:
conditions += "AND %s='%s'"%(deferred_account, account) conditions += "AND %s='%s'" % (deferred_account, account)
elif company: elif company:
conditions += f"AND p.company = {frappe.db.escape(company)}" conditions += f"AND p.company = {frappe.db.escape(company)}"
return conditions return conditions
def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=''):
def convert_deferred_expense_to_expense(
deferred_process, start_date=None, end_date=None, conditions=""
):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM # book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date: if not start_date:
@ -67,14 +80,19 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
end_date = add_days(today(), -1) end_date = add_days(today(), -1)
# check for the purchase invoice for which GL entries has to be done # check for the purchase invoice for which GL entries has to be done
invoices = frappe.db.sql_list(''' invoices = frappe.db.sql_list(
"""
select distinct item.parent select distinct item.parent
from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s where item.service_start_date<=%s and item.service_end_date>=%s
and item.enable_deferred_expense = 1 and item.parent=p.name and item.enable_deferred_expense = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0 and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{0} {0}
'''.format(conditions), (end_date, start_date)) #nosec """.format(
conditions
),
(end_date, start_date),
) # nosec
# For each invoice, book deferred expense # For each invoice, book deferred expense
for invoice in invoices: for invoice in invoices:
@ -84,7 +102,10 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
if frappe.flags.deferred_accounting_error: if frappe.flags.deferred_accounting_error:
send_mail(deferred_process) send_mail(deferred_process)
def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=''):
def convert_deferred_revenue_to_income(
deferred_process, start_date=None, end_date=None, conditions=""
):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM # book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date: if not start_date:
@ -93,14 +114,19 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da
end_date = add_days(today(), -1) end_date = add_days(today(), -1)
# check for the sales invoice for which GL entries has to be done # check for the sales invoice for which GL entries has to be done
invoices = frappe.db.sql_list(''' invoices = frappe.db.sql_list(
"""
select distinct item.parent select distinct item.parent
from `tabSales Invoice Item` item, `tabSales Invoice` p from `tabSales Invoice Item` item, `tabSales Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s where item.service_start_date<=%s and item.service_end_date>=%s
and item.enable_deferred_revenue = 1 and item.parent=p.name and item.enable_deferred_revenue = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0 and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{0} {0}
'''.format(conditions), (end_date, start_date)) #nosec """.format(
conditions
),
(end_date, start_date),
) # nosec
for invoice in invoices: for invoice in invoices:
doc = frappe.get_doc("Sales Invoice", invoice) doc = frappe.get_doc("Sales Invoice", invoice)
@ -109,31 +135,43 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da
if frappe.flags.deferred_accounting_error: if frappe.flags.deferred_accounting_error:
send_mail(deferred_process) send_mail(deferred_process)
def get_booking_dates(doc, item, posting_date=None): def get_booking_dates(doc, item, posting_date=None):
if not posting_date: if not posting_date:
posting_date = add_days(today(), -1) posting_date = add_days(today(), -1)
last_gl_entry = False last_gl_entry = False
deferred_account = "deferred_revenue_account" if doc.doctype=="Sales Invoice" else "deferred_expense_account" deferred_account = (
"deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account"
)
prev_gl_entry = frappe.db.sql(''' prev_gl_entry = frappe.db.sql(
"""
select name, posting_date from `tabGL Entry` where company=%s and account=%s and select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0 and is_cancelled = 0
order by posting_date desc limit 1 order by posting_date desc limit 1
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) """,
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
prev_gl_via_je = frappe.db.sql(''' prev_gl_via_je = frappe.db.sql(
"""
SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
WHERE p.name = c.parent and p.company=%s and c.account=%s WHERE p.name = c.parent and p.company=%s and c.account=%s
and c.reference_type=%s and c.reference_name=%s and c.reference_type=%s and c.reference_name=%s
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1 and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) """,
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
if prev_gl_via_je: if prev_gl_via_je:
if (not prev_gl_entry) or (prev_gl_entry and if (not prev_gl_entry) or (
prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date): prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date
):
prev_gl_entry = prev_gl_via_je prev_gl_entry = prev_gl_via_je
if prev_gl_entry: if prev_gl_entry:
@ -157,66 +195,94 @@ def get_booking_dates(doc, item, posting_date=None):
else: else:
return None, None, None return None, None, None
def calculate_monthly_amount(doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency):
def calculate_monthly_amount(
doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency
):
amount, base_amount = 0, 0 amount, base_amount = 0, 0
if not last_gl_entry: if not last_gl_entry:
total_months = (item.service_end_date.year - item.service_start_date.year) * 12 + \ total_months = (
(item.service_end_date.month - item.service_start_date.month) + 1 (item.service_end_date.year - item.service_start_date.year) * 12
+ (item.service_end_date.month - item.service_start_date.month)
+ 1
)
prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) \ prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) / flt(
/ flt(date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date))) date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date))
)
actual_months = rounded(total_months * prorate_factor, 1) actual_months = rounded(total_months * prorate_factor, 1)
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item) already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
doc, item
)
base_amount = flt(item.base_net_amount / actual_months, item.precision("base_net_amount")) base_amount = flt(item.base_net_amount / actual_months, item.precision("base_net_amount"))
if base_amount + already_booked_amount > item.base_net_amount: if base_amount + already_booked_amount > item.base_net_amount:
base_amount = item.base_net_amount - already_booked_amount base_amount = item.base_net_amount - already_booked_amount
if account_currency==doc.company_currency: if account_currency == doc.company_currency:
amount = base_amount amount = base_amount
else: else:
amount = flt(item.net_amount/actual_months, item.precision("net_amount")) amount = flt(item.net_amount / actual_months, item.precision("net_amount"))
if amount + already_booked_amount_in_account_currency > item.net_amount: if amount + already_booked_amount_in_account_currency > item.net_amount:
amount = item.net_amount - already_booked_amount_in_account_currency amount = item.net_amount - already_booked_amount_in_account_currency
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date): if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
partial_month = flt(date_diff(end_date, start_date)) \ partial_month = flt(date_diff(end_date, start_date)) / flt(
/ flt(date_diff(get_last_day(end_date), get_first_day(start_date))) date_diff(get_last_day(end_date), get_first_day(start_date))
)
base_amount = rounded(partial_month, 1) * base_amount base_amount = rounded(partial_month, 1) * base_amount
amount = rounded(partial_month, 1) * amount amount = rounded(partial_month, 1) * amount
else: else:
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item) already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount")) doc, item
if account_currency==doc.company_currency: )
base_amount = flt(
item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
)
if account_currency == doc.company_currency:
amount = base_amount amount = base_amount
else: else:
amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")) amount = flt(
item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
)
return amount, base_amount return amount, base_amount
def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency): def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency):
amount, base_amount = 0, 0 amount, base_amount = 0, 0
if not last_gl_entry: if not last_gl_entry:
base_amount = flt(item.base_net_amount*total_booking_days/flt(total_days), item.precision("base_net_amount")) base_amount = flt(
if account_currency==doc.company_currency: item.base_net_amount * total_booking_days / flt(total_days), item.precision("base_net_amount")
)
if account_currency == doc.company_currency:
amount = base_amount amount = base_amount
else: else:
amount = flt(item.net_amount*total_booking_days/flt(total_days), item.precision("net_amount")) amount = flt(
item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount")
)
else: else:
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item) already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
doc, item
)
base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount")) base_amount = flt(
if account_currency==doc.company_currency: item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
)
if account_currency == doc.company_currency:
amount = base_amount amount = base_amount
else: else:
amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")) amount = flt(
item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
)
return amount, base_amount return amount, base_amount
def get_already_booked_amount(doc, item): def get_already_booked_amount(doc, item):
if doc.doctype == "Sales Invoice": if doc.doctype == "Sales Invoice":
total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency" total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency"
@ -225,21 +291,31 @@ def get_already_booked_amount(doc, item):
total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency" total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency"
deferred_account = "deferred_expense_account" deferred_account = "deferred_expense_account"
gl_entries_details = frappe.db.sql(''' gl_entries_details = frappe.db.sql(
"""
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0 and is_cancelled = 0
group by voucher_detail_no group by voucher_detail_no
'''.format(total_credit_debit, total_credit_debit_currency), """.format(
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) total_credit_debit, total_credit_debit_currency
),
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
journal_entry_details = frappe.db.sql(''' journal_entry_details = frappe.db.sql(
"""
SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no
FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and
p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s
and p.docstatus < 2 group by reference_detail_no and p.docstatus < 2 group by reference_detail_no
'''.format(total_credit_debit, total_credit_debit_currency), """.format(
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) total_credit_debit, total_credit_debit_currency
),
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0 already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0
already_booked_amount += journal_entry_details[0].total_credit if journal_entry_details else 0 already_booked_amount += journal_entry_details[0].total_credit if journal_entry_details else 0
@ -247,20 +323,29 @@ def get_already_booked_amount(doc, item):
if doc.currency == doc.company_currency: if doc.currency == doc.company_currency:
already_booked_amount_in_account_currency = already_booked_amount already_booked_amount_in_account_currency = already_booked_amount
else: else:
already_booked_amount_in_account_currency = gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0 already_booked_amount_in_account_currency = (
already_booked_amount_in_account_currency += journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0 gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0
)
already_booked_amount_in_account_currency += (
journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0
)
return already_booked_amount, already_booked_amount_in_account_currency return already_booked_amount, already_booked_amount_in_account_currency
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" \ enable_check = (
if doc.doctype=="Sales Invoice" else "enable_deferred_expense" "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
)
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto') accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on): def _book_deferred_revenue_or_expense(
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
):
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date) start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date): return if not (start_date and end_date):
return
account_currency = get_account_currency(item.expense_account or item.income_account) account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice": if doc.doctype == "Sales Invoice":
@ -273,12 +358,21 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
total_days = date_diff(item.service_end_date, item.service_start_date) + 1 total_days = date_diff(item.service_end_date, item.service_start_date) + 1
total_booking_days = date_diff(end_date, start_date) + 1 total_booking_days = date_diff(end_date, start_date) + 1
if book_deferred_entries_based_on == 'Months': if book_deferred_entries_based_on == "Months":
amount, base_amount = calculate_monthly_amount(doc, item, last_gl_entry, amount, base_amount = calculate_monthly_amount(
start_date, end_date, total_days, total_booking_days, account_currency) doc,
item,
last_gl_entry,
start_date,
end_date,
total_days,
total_booking_days,
account_currency,
)
else: else:
amount, base_amount = calculate_amount(doc, item, last_gl_entry, amount, base_amount = calculate_amount(
total_days, total_booking_days, account_currency) doc, item, last_gl_entry, total_days, total_booking_days, account_currency
)
if not amount: if not amount:
return return
@ -288,92 +382,156 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
end_date = get_last_day(add_days(accounts_frozen_upto, 1)) end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry: if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, book_revenue_via_journal_entry(
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) doc,
credit_account,
debit_account,
against,
amount,
base_amount,
end_date,
project,
account_currency,
item.cost_center,
item,
deferred_process,
submit_journal_entry,
)
else: else:
make_gl_entries(doc, credit_account, debit_account, against, make_gl_entries(
amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process) doc,
credit_account,
debit_account,
against,
amount,
base_amount,
end_date,
project,
account_currency,
item.cost_center,
item,
deferred_process,
)
# Returned in case of any errors because it tries to submit the same record again and again in case of errors # Returned in case of any errors because it tries to submit the same record again and again in case of errors
if frappe.flags.deferred_accounting_error: if frappe.flags.deferred_accounting_error:
return return
if getdate(end_date) < getdate(posting_date) and not last_gl_entry: if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
_book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on) _book_deferred_revenue_or_expense(
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
)
via_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_via_journal_entry')) via_journal_entry = cint(
submit_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'submit_journal_entries')) frappe.db.get_singles_value("Accounts Settings", "book_deferred_entries_via_journal_entry")
book_deferred_entries_based_on = frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_based_on') )
submit_journal_entry = cint(
frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries")
)
book_deferred_entries_based_on = frappe.db.get_singles_value(
"Accounts Settings", "book_deferred_entries_based_on"
)
for item in doc.get('items'): for item in doc.get("items"):
if item.get(enable_check): if item.get(enable_check):
_book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on) _book_deferred_revenue_or_expense(
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
)
def process_deferred_accounting(posting_date=None): def process_deferred_accounting(posting_date=None):
''' Converts deferred income/expense into income/expense """Converts deferred income/expense into income/expense
Executed via background jobs on every month end ''' Executed via background jobs on every month end"""
if not posting_date: if not posting_date:
posting_date = today() posting_date = today()
if not cint(frappe.db.get_singles_value('Accounts Settings', 'automatically_process_deferred_accounting_entry')): if not cint(
frappe.db.get_singles_value(
"Accounts Settings", "automatically_process_deferred_accounting_entry"
)
):
return return
start_date = add_months(today(), -1) start_date = add_months(today(), -1)
end_date = add_days(today(), -1) end_date = add_days(today(), -1)
companies = frappe.get_all('Company') companies = frappe.get_all("Company")
for company in companies: for company in companies:
for record_type in ('Income', 'Expense'): for record_type in ("Income", "Expense"):
doc = frappe.get_doc(dict( doc = frappe.get_doc(
doctype='Process Deferred Accounting', dict(
company=company.name, doctype="Process Deferred Accounting",
posting_date=posting_date, company=company.name,
start_date=start_date, posting_date=posting_date,
end_date=end_date, start_date=start_date,
type=record_type end_date=end_date,
)) type=record_type,
)
)
doc.insert() doc.insert()
doc.submit() doc.submit()
def make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): def make_gl_entries(
doc,
credit_account,
debit_account,
against,
amount,
base_amount,
posting_date,
project,
account_currency,
cost_center,
item,
deferred_process=None,
):
# GL Entry for crediting the amount in the deferred expense # GL Entry for crediting the amount in the deferred expense
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
if amount == 0: return if amount == 0:
return
gl_entries = [] gl_entries = []
gl_entries.append( gl_entries.append(
doc.get_gl_dict({ doc.get_gl_dict(
"account": credit_account, {
"against": against, "account": credit_account,
"credit": base_amount, "against": against,
"credit_in_account_currency": amount, "credit": base_amount,
"cost_center": cost_center, "credit_in_account_currency": amount,
"voucher_detail_no": item.name, "cost_center": cost_center,
'posting_date': posting_date, "voucher_detail_no": item.name,
'project': project, "posting_date": posting_date,
'against_voucher_type': 'Process Deferred Accounting', "project": project,
'against_voucher': deferred_process "against_voucher_type": "Process Deferred Accounting",
}, account_currency, item=item) "against_voucher": deferred_process,
},
account_currency,
item=item,
)
) )
# GL Entry to debit the amount from the expense # GL Entry to debit the amount from the expense
gl_entries.append( gl_entries.append(
doc.get_gl_dict({ doc.get_gl_dict(
"account": debit_account, {
"against": against, "account": debit_account,
"debit": base_amount, "against": against,
"debit_in_account_currency": amount, "debit": base_amount,
"cost_center": cost_center, "debit_in_account_currency": amount,
"voucher_detail_no": item.name, "cost_center": cost_center,
'posting_date': posting_date, "voucher_detail_no": item.name,
'project': project, "posting_date": posting_date,
'against_voucher_type': 'Process Deferred Accounting', "project": project,
'against_voucher': deferred_process "against_voucher_type": "Process Deferred Accounting",
}, account_currency, item=item) "against_voucher": deferred_process,
},
account_currency,
item=item,
)
) )
if gl_entries: if gl_entries:
@ -383,68 +541,88 @@ def make_gl_entries(doc, credit_account, debit_account, against,
except Exception as e: except Exception as e:
if frappe.flags.in_test: if frappe.flags.in_test:
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.log_error(
title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
message=traceback,
)
raise e raise e
else: else:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.log_error(
title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
message=traceback,
)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process): def send_mail(deferred_process):
title = _("Error while processing deferred accounting for {0}").format(deferred_process) title = _("Error while processing deferred accounting for {0}").format(deferred_process)
link = get_link_to_form('Process Deferred Accounting', deferred_process) link = get_link_to_form("Process Deferred Accounting", deferred_process)
content = _("Deferred accounting failed for some invoices:") + "\n" content = _("Deferred accounting failed for some invoices:") + "\n"
content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link) content += _(
"Please check Process Deferred Accounting {0} and submit manually after resolving errors."
).format(link)
sendmail_to_system_managers(title, content) sendmail_to_system_managers(title, content)
def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, item,
deferred_process=None, submit='No'):
if amount == 0: return def book_revenue_via_journal_entry(
doc,
credit_account,
debit_account,
against,
amount,
base_amount,
posting_date,
project,
account_currency,
cost_center,
item,
deferred_process=None,
submit="No",
):
journal_entry = frappe.new_doc('Journal Entry') if amount == 0:
return
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.posting_date = posting_date journal_entry.posting_date = posting_date
journal_entry.company = doc.company journal_entry.company = doc.company
journal_entry.voucher_type = 'Deferred Revenue' if doc.doctype == 'Sales Invoice' \ journal_entry.voucher_type = (
else 'Deferred Expense' "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
)
debit_entry = { debit_entry = {
'account': credit_account, "account": credit_account,
'credit': base_amount, "credit": base_amount,
'credit_in_account_currency': amount, "credit_in_account_currency": amount,
'account_currency': account_currency, "account_currency": account_currency,
'reference_name': doc.name, "reference_name": doc.name,
'reference_type': doc.doctype, "reference_type": doc.doctype,
'reference_detail_no': item.name, "reference_detail_no": item.name,
'cost_center': cost_center, "cost_center": cost_center,
'project': project, "project": project,
} }
credit_entry = { credit_entry = {
'account': debit_account, "account": debit_account,
'debit': base_amount, "debit": base_amount,
'debit_in_account_currency': amount, "debit_in_account_currency": amount,
'account_currency': account_currency, "account_currency": account_currency,
'reference_name': doc.name, "reference_name": doc.name,
'reference_type': doc.doctype, "reference_type": doc.doctype,
'reference_detail_no': item.name, "reference_detail_no": item.name,
'cost_center': cost_center, "cost_center": cost_center,
'project': project, "project": project,
} }
for dimension in get_accounting_dimensions(): for dimension in get_accounting_dimensions():
debit_entry.update({ debit_entry.update({dimension: item.get(dimension)})
dimension: item.get(dimension)
})
credit_entry.update({ credit_entry.update({dimension: item.get(dimension)})
dimension: item.get(dimension)
})
journal_entry.append('accounts', debit_entry) journal_entry.append("accounts", debit_entry)
journal_entry.append('accounts', credit_entry) journal_entry.append("accounts", credit_entry)
try: try:
journal_entry.save() journal_entry.save()
@ -456,20 +634,30 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.log_error(
title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
message=traceback,
)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True
def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr): def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
if doctype == 'Sales Invoice': if doctype == "Sales Invoice":
credit_account, debit_account = frappe.db.get_value('Sales Invoice Item', {'name': voucher_detail_no}, credit_account, debit_account = frappe.db.get_value(
['income_account', 'deferred_revenue_account']) "Sales Invoice Item",
{"name": voucher_detail_no},
["income_account", "deferred_revenue_account"],
)
else: else:
credit_account, debit_account = frappe.db.get_value('Purchase Invoice Item', {'name': voucher_detail_no}, credit_account, debit_account = frappe.db.get_value(
['deferred_expense_account', 'expense_account']) "Purchase Invoice Item",
{"name": voucher_detail_no},
["deferred_expense_account", "expense_account"],
)
if dr_or_cr == 'Debit': if dr_or_cr == "Debit":
return debit_account return debit_account
else: else:
return credit_account return credit_account

View File

@ -10,11 +10,17 @@ from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_
import erpnext import erpnext
class RootNotEditable(frappe.ValidationError): pass class RootNotEditable(frappe.ValidationError):
class BalanceMismatchError(frappe.ValidationError): pass pass
class BalanceMismatchError(frappe.ValidationError):
pass
class Account(NestedSet): class Account(NestedSet):
nsm_parent_field = 'parent_account' nsm_parent_field = "parent_account"
def on_update(self): def on_update(self):
if frappe.local.flags.ignore_update_nsm: if frappe.local.flags.ignore_update_nsm:
return return
@ -22,17 +28,20 @@ class Account(NestedSet):
super(Account, self).on_update() super(Account, self).on_update()
def onload(self): def onload(self):
frozen_accounts_modifier = frappe.db.get_value("Accounts Settings", "Accounts Settings", frozen_accounts_modifier = frappe.db.get_value(
"frozen_accounts_modifier") "Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
)
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles(): if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
self.set_onload("can_freeze_account", True) self.set_onload("can_freeze_account", True)
def autoname(self): def autoname(self):
from erpnext.accounts.utils import get_autoname_with_number from erpnext.accounts.utils import get_autoname_with_number
self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company) self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company)
def validate(self): def validate(self):
from erpnext.accounts.utils import validate_field_number from erpnext.accounts.utils import validate_field_number
if frappe.local.flags.allow_unverified_charts: if frappe.local.flags.allow_unverified_charts:
return return
self.validate_parent() self.validate_parent()
@ -49,22 +58,33 @@ class Account(NestedSet):
def validate_parent(self): def validate_parent(self):
"""Fetch Parent Details and validate parent account""" """Fetch Parent Details and validate parent account"""
if self.parent_account: if self.parent_account:
par = frappe.db.get_value("Account", self.parent_account, par = frappe.db.get_value(
["name", "is_group", "company"], as_dict=1) "Account", self.parent_account, ["name", "is_group", "company"], as_dict=1
)
if not par: if not par:
throw(_("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account)) throw(
_("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account)
)
elif par.name == self.name: elif par.name == self.name:
throw(_("Account {0}: You can not assign itself as parent account").format(self.name)) throw(_("Account {0}: You can not assign itself as parent account").format(self.name))
elif not par.is_group: elif not par.is_group:
throw(_("Account {0}: Parent account {1} can not be a ledger").format(self.name, self.parent_account)) throw(
_("Account {0}: Parent account {1} can not be a ledger").format(
self.name, self.parent_account
)
)
elif par.company != self.company: elif par.company != self.company:
throw(_("Account {0}: Parent account {1} does not belong to company: {2}") throw(
.format(self.name, self.parent_account, self.company)) _("Account {0}: Parent account {1} does not belong to company: {2}").format(
self.name, self.parent_account, self.company
)
)
def set_root_and_report_type(self): def set_root_and_report_type(self):
if self.parent_account: if self.parent_account:
par = frappe.db.get_value("Account", self.parent_account, par = frappe.db.get_value(
["report_type", "root_type"], as_dict=1) "Account", self.parent_account, ["report_type", "root_type"], as_dict=1
)
if par.report_type: if par.report_type:
self.report_type = par.report_type self.report_type = par.report_type
@ -75,15 +95,20 @@ class Account(NestedSet):
db_value = frappe.db.get_value("Account", self.name, ["report_type", "root_type"], as_dict=1) db_value = frappe.db.get_value("Account", self.name, ["report_type", "root_type"], as_dict=1)
if db_value: if db_value:
if self.report_type != db_value.report_type: if self.report_type != db_value.report_type:
frappe.db.sql("update `tabAccount` set report_type=%s where lft > %s and rgt < %s", frappe.db.sql(
(self.report_type, self.lft, self.rgt)) "update `tabAccount` set report_type=%s where lft > %s and rgt < %s",
(self.report_type, self.lft, self.rgt),
)
if self.root_type != db_value.root_type: if self.root_type != db_value.root_type:
frappe.db.sql("update `tabAccount` set root_type=%s where lft > %s and rgt < %s", frappe.db.sql(
(self.root_type, self.lft, self.rgt)) "update `tabAccount` set root_type=%s where lft > %s and rgt < %s",
(self.root_type, self.lft, self.rgt),
)
if self.root_type and not self.report_type: if self.root_type and not self.report_type:
self.report_type = "Balance Sheet" \ self.report_type = (
if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss" "Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
)
def validate_root_details(self): def validate_root_details(self):
# does not exists parent # does not exists parent
@ -96,21 +121,26 @@ class Account(NestedSet):
def validate_root_company_and_sync_account_to_children(self): def validate_root_company_and_sync_account_to_children(self):
# ignore validation while creating new compnay or while syncing to child companies # ignore validation while creating new compnay or while syncing to child companies
if frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation: if (
frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation
):
return return
ancestors = get_root_company(self.company) ancestors = get_root_company(self.company)
if ancestors: if ancestors:
if frappe.get_value("Company", self.company, "allow_account_creation_against_child_company"): if frappe.get_value("Company", self.company, "allow_account_creation_against_child_company"):
return return
if not frappe.db.get_value("Account", if not frappe.db.get_value(
{'account_name': self.account_name, 'company': ancestors[0]}, 'name'): "Account", {"account_name": self.account_name, "company": ancestors[0]}, "name"
):
frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0])) frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0]))
elif self.parent_account: elif self.parent_account:
descendants = get_descendants_of('Company', self.company) descendants = get_descendants_of("Company", self.company)
if not descendants: return if not descendants:
return
parent_acc_name_map = {} parent_acc_name_map = {}
parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \ parent_acc_name, parent_acc_number = frappe.db.get_value(
["account_name", "account_number"]) "Account", self.parent_account, ["account_name", "account_number"]
)
filters = { filters = {
"company": ["in", descendants], "company": ["in", descendants],
"account_name": parent_acc_name, "account_name": parent_acc_name,
@ -118,10 +148,13 @@ class Account(NestedSet):
if parent_acc_number: if parent_acc_number:
filters["account_number"] = parent_acc_number filters["account_number"] = parent_acc_number
for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True): for d in frappe.db.get_values(
"Account", filters=filters, fieldname=["company", "name"], as_dict=True
):
parent_acc_name_map[d["company"]] = d["name"] parent_acc_name_map[d["company"]] = d["name"]
if not parent_acc_name_map: return if not parent_acc_name_map:
return
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
@ -142,24 +175,34 @@ class Account(NestedSet):
def validate_frozen_accounts_modifier(self): def validate_frozen_accounts_modifier(self):
old_value = frappe.db.get_value("Account", self.name, "freeze_account") old_value = frappe.db.get_value("Account", self.name, "freeze_account")
if old_value and old_value != self.freeze_account: if old_value and old_value != self.freeze_account:
frozen_accounts_modifier = frappe.db.get_value('Accounts Settings', None, 'frozen_accounts_modifier') frozen_accounts_modifier = frappe.db.get_value(
if not frozen_accounts_modifier or \ "Accounts Settings", None, "frozen_accounts_modifier"
frozen_accounts_modifier not in frappe.get_roles(): )
throw(_("You are not authorized to set Frozen value")) if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
throw(_("You are not authorized to set Frozen value"))
def validate_balance_must_be_debit_or_credit(self): def validate_balance_must_be_debit_or_credit(self):
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
if not self.get("__islocal") and self.balance_must_be: if not self.get("__islocal") and self.balance_must_be:
account_balance = get_balance_on(self.name) account_balance = get_balance_on(self.name)
if account_balance > 0 and self.balance_must_be == "Credit": if account_balance > 0 and self.balance_must_be == "Credit":
frappe.throw(_("Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'")) frappe.throw(
_(
"Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'"
)
)
elif account_balance < 0 and self.balance_must_be == "Debit": elif account_balance < 0 and self.balance_must_be == "Debit":
frappe.throw(_("Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'")) frappe.throw(
_(
"Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"
)
)
def validate_account_currency(self): def validate_account_currency(self):
if not self.account_currency: if not self.account_currency:
self.account_currency = frappe.get_cached_value('Company', self.company, "default_currency") self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"): elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"):
if frappe.db.get_value("GL Entry", {"account": self.name}): if frappe.db.get_value("GL Entry", {"account": self.name}):
@ -170,45 +213,52 @@ class Account(NestedSet):
company_bold = frappe.bold(company) company_bold = frappe.bold(company)
parent_acc_name_bold = frappe.bold(parent_acc_name) parent_acc_name_bold = frappe.bold(parent_acc_name)
if not parent_acc_name_map.get(company): if not parent_acc_name_map.get(company):
frappe.throw(_("While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA") frappe.throw(
.format(company_bold, parent_acc_name_bold), title=_("Account Not Found")) _(
"While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA"
).format(company_bold, parent_acc_name_bold),
title=_("Account Not Found"),
)
# validate if parent of child company account to be added is a group # validate if parent of child company account to be added is a group
if (frappe.db.get_value("Account", self.parent_account, "is_group") if frappe.db.get_value("Account", self.parent_account, "is_group") and not frappe.db.get_value(
and not frappe.db.get_value("Account", parent_acc_name_map[company], "is_group")): "Account", parent_acc_name_map[company], "is_group"
msg = _("While creating account for Child Company {0}, parent account {1} found as a ledger account.").format(company_bold, parent_acc_name_bold) ):
msg = _(
"While creating account for Child Company {0}, parent account {1} found as a ledger account."
).format(company_bold, parent_acc_name_bold)
msg += "<br><br>" msg += "<br><br>"
msg += _("Please convert the parent account in corresponding child company to a group account.") msg += _(
"Please convert the parent account in corresponding child company to a group account."
)
frappe.throw(msg, title=_("Invalid Parent Account")) frappe.throw(msg, title=_("Invalid Parent Account"))
filters = { filters = {"account_name": self.account_name, "company": company}
"account_name": self.account_name,
"company": company
}
if self.account_number: if self.account_number:
filters["account_number"] = self.account_number filters["account_number"] = self.account_number
child_account = frappe.db.get_value("Account", filters, 'name') child_account = frappe.db.get_value("Account", filters, "name")
if not child_account: if not child_account:
doc = frappe.copy_doc(self) doc = frappe.copy_doc(self)
doc.flags.ignore_root_company_validation = True doc.flags.ignore_root_company_validation = True
doc.update({ doc.update(
"company": company, {
# parent account's currency should be passed down to child account's curreny "company": company,
# if it is None, it picks it up from default company currency, which might be unintended # parent account's currency should be passed down to child account's curreny
"account_currency": erpnext.get_company_currency(company), # if it is None, it picks it up from default company currency, which might be unintended
"parent_account": parent_acc_name_map[company] "account_currency": erpnext.get_company_currency(company),
}) "parent_account": parent_acc_name_map[company],
}
)
doc.save() doc.save()
frappe.msgprint(_("Account {0} is added in the child company {1}") frappe.msgprint(_("Account {0} is added in the child company {1}").format(doc.name, company))
.format(doc.name, company))
elif child_account: elif child_account:
# update the parent company's value in child companies # update the parent company's value in child companies
doc = frappe.get_doc("Account", child_account) doc = frappe.get_doc("Account", child_account)
parent_value_changed = False parent_value_changed = False
for field in ['account_type', 'freeze_account', 'balance_must_be']: for field in ["account_type", "freeze_account", "balance_must_be"]:
if doc.get(field) != self.get(field): if doc.get(field) != self.get(field):
parent_value_changed = True parent_value_changed = True
doc.set(field, self.get(field)) doc.set(field, self.get(field))
@ -243,8 +293,11 @@ class Account(NestedSet):
return frappe.db.get_value("GL Entry", {"account": self.name}) return frappe.db.get_value("GL Entry", {"account": self.name})
def check_if_child_exists(self): def check_if_child_exists(self):
return frappe.db.sql("""select name from `tabAccount` where parent_account = %s return frappe.db.sql(
and docstatus != 2""", self.name) """select name from `tabAccount` where parent_account = %s
and docstatus != 2""",
self.name,
)
def validate_mandatory(self): def validate_mandatory(self):
if not self.root_type: if not self.root_type:
@ -260,73 +313,99 @@ class Account(NestedSet):
super(Account, self).on_trash(True) super(Account, self).on_trash(True)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_parent_account(doctype, txt, searchfield, start, page_len, filters): def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select name from tabAccount return frappe.db.sql(
"""select name from tabAccount
where is_group = 1 and docstatus != 2 and company = %s where is_group = 1 and docstatus != 2 and company = %s
and %s like %s order by name limit %s, %s""" % and %s like %s order by name limit %s, %s"""
("%s", searchfield, "%s", "%s", "%s"), % ("%s", searchfield, "%s", "%s", "%s"),
(filters["company"], "%%%s%%" % txt, start, page_len), as_list=1) (filters["company"], "%%%s%%" % txt, start, page_len),
as_list=1,
)
def get_account_currency(account): def get_account_currency(account):
"""Helper function to get account currency""" """Helper function to get account currency"""
if not account: if not account:
return return
def generator(): def generator():
account_currency, company = frappe.get_cached_value("Account", account, ["account_currency", "company"]) account_currency, company = frappe.get_cached_value(
"Account", account, ["account_currency", "company"]
)
if not account_currency: if not account_currency:
account_currency = frappe.get_cached_value('Company', company, "default_currency") account_currency = frappe.get_cached_value("Company", company, "default_currency")
return account_currency return account_currency
return frappe.local_cache("account_currency", account, generator) return frappe.local_cache("account_currency", account, generator)
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Account", ["lft", "rgt"]) frappe.db.add_index("Account", ["lft", "rgt"])
def get_account_autoname(account_number, account_name, company): def get_account_autoname(account_number, account_name, company):
# first validate if company exists # first validate if company exists
company = frappe.get_cached_value('Company', company, ["abbr", "name"], as_dict=True) company = frappe.get_cached_value("Company", company, ["abbr", "name"], as_dict=True)
if not company: if not company:
frappe.throw(_('Company {0} does not exist').format(company)) frappe.throw(_("Company {0} does not exist").format(company))
parts = [account_name.strip(), company.abbr] parts = [account_name.strip(), company.abbr]
if cstr(account_number).strip(): if cstr(account_number).strip():
parts.insert(0, cstr(account_number).strip()) parts.insert(0, cstr(account_number).strip())
return ' - '.join(parts) return " - ".join(parts)
def validate_account_number(name, account_number, company): def validate_account_number(name, account_number, company):
if account_number: if account_number:
account_with_same_number = frappe.db.get_value("Account", account_with_same_number = frappe.db.get_value(
{"account_number": account_number, "company": company, "name": ["!=", name]}) "Account", {"account_number": account_number, "company": company, "name": ["!=", name]}
)
if account_with_same_number: if account_with_same_number:
frappe.throw(_("Account Number {0} already used in account {1}") frappe.throw(
.format(account_number, account_with_same_number)) _("Account Number {0} already used in account {1}").format(
account_number, account_with_same_number
)
)
@frappe.whitelist() @frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False): def update_account_number(name, account_name, account_number=None, from_descendant=False):
account = frappe.db.get_value("Account", name, "company", as_dict=True) account = frappe.db.get_value("Account", name, "company", as_dict=True)
if not account: return if not account:
return
old_acc_name, old_acc_number = frappe.db.get_value('Account', name, \ old_acc_name, old_acc_number = frappe.db.get_value(
["account_name", "account_number"]) "Account", name, ["account_name", "account_number"]
)
# check if account exists in parent company # check if account exists in parent company
ancestors = get_ancestors_of("Company", account.company) ancestors = get_ancestors_of("Company", account.company)
allow_independent_account_creation = frappe.get_value("Company", account.company, "allow_account_creation_against_child_company") allow_independent_account_creation = frappe.get_value(
"Company", account.company, "allow_account_creation_against_child_company"
)
if ancestors and not allow_independent_account_creation: if ancestors and not allow_independent_account_creation:
for ancestor in ancestors: for ancestor in ancestors:
if frappe.db.get_value("Account", {'account_name': old_acc_name, 'company': ancestor}, 'name'): if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
# same account in parent company exists # same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company") allow_child_account_creation = _("Allow Account Creation Against Child Company")
message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor)) message = _("Account {0} exists in parent company {1}.").format(
frappe.bold(old_acc_name), frappe.bold(ancestor)
)
message += "<br>" message += "<br>"
message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(frappe.bold(ancestor)) message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(
frappe.bold(ancestor)
)
message += "<br><br>" message += "<br><br>"
message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company)) message += _("To overrule this, enable '{0}' in company {1}").format(
allow_child_account_creation, frappe.bold(account.company)
)
frappe.throw(message, title=_("Rename Not Allowed")) frappe.throw(message, title=_("Rename Not Allowed"))
@ -339,42 +418,53 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if not from_descendant: if not from_descendant:
# Update and rename in child company accounts as well # Update and rename in child company accounts as well
descendants = get_descendants_of('Company', account.company) descendants = get_descendants_of("Company", account.company)
if descendants: if descendants:
sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number, old_acc_number) sync_update_account_number_in_child(
descendants, old_acc_name, account_name, account_number, old_acc_number
)
new_name = get_account_autoname(account_number, account_name, account.company) new_name = get_account_autoname(account_number, account_name, account.company)
if name != new_name: if name != new_name:
frappe.rename_doc("Account", name, new_name, force=1) frappe.rename_doc("Account", name, new_name, force=1)
return new_name return new_name
@frappe.whitelist() @frappe.whitelist()
def merge_account(old, new, is_group, root_type, company): def merge_account(old, new, is_group, root_type, company):
# Validate properties before merging # Validate properties before merging
if not frappe.db.exists("Account", new): if not frappe.db.exists("Account", new):
throw(_("Account {0} does not exist").format(new)) throw(_("Account {0} does not exist").format(new))
val = list(frappe.db.get_value("Account", new, val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"]))
["is_group", "root_type", "company"]))
if val != [cint(is_group), root_type, company]: if val != [cint(is_group), root_type, company]:
throw(_("""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company""")) throw(
_(
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""
)
)
if is_group and frappe.db.get_value("Account", new, "parent_account") == old: if is_group and frappe.db.get_value("Account", new, "parent_account") == old:
frappe.db.set_value("Account", new, "parent_account", frappe.db.set_value(
frappe.db.get_value("Account", old, "parent_account")) "Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account")
)
frappe.rename_doc("Account", old, new, merge=1, force=1) frappe.rename_doc("Account", old, new, merge=1, force=1)
return new return new
@frappe.whitelist() @frappe.whitelist()
def get_root_company(company): def get_root_company(company):
# return the topmost company in the hierarchy # return the topmost company in the hierarchy
ancestors = get_ancestors_of('Company', company, "lft asc") ancestors = get_ancestors_of("Company", company, "lft asc")
return [ancestors[0]] if ancestors else [] return [ancestors[0]] if ancestors else []
def sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number=None, old_acc_number=None):
def sync_update_account_number_in_child(
descendants, old_acc_name, account_name, account_number=None, old_acc_number=None
):
filters = { filters = {
"company": ["in", descendants], "company": ["in", descendants],
"account_name": old_acc_name, "account_name": old_acc_name,
@ -382,5 +472,7 @@ def sync_update_account_number_in_child(descendants, old_acc_name, account_name,
if old_acc_number: if old_acc_number:
filters["account_number"] = old_acc_number filters["account_number"] = old_acc_number
for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True): for d in frappe.db.get_values(
update_account_number(d["name"], account_name, account_number, from_descendant=True) "Account", filters=filters, fieldname=["company", "name"], as_dict=True
):
update_account_number(d["name"], account_name, account_number, from_descendant=True)

View File

@ -10,7 +10,9 @@ from frappe.utils.nestedset import rebuild_tree
from unidecode import unidecode from unidecode import unidecode
def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None): def create_charts(
company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None
):
chart = custom_chart or get_chart(chart_template, existing_company) chart = custom_chart or get_chart(chart_template, existing_company)
if chart: if chart:
accounts = [] accounts = []
@ -20,30 +22,41 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
if root_account: if root_account:
root_type = child.get("root_type") root_type = child.get("root_type")
if account_name not in ["account_name", "account_number", "account_type", if account_name not in [
"root_type", "is_group", "tax_rate"]: "account_name",
"account_number",
"account_type",
"root_type",
"is_group",
"tax_rate",
]:
account_number = cstr(child.get("account_number")).strip() account_number = cstr(child.get("account_number")).strip()
account_name, account_name_in_db = add_suffix_if_duplicate(account_name, account_name, account_name_in_db = add_suffix_if_duplicate(
account_number, accounts) account_name, account_number, accounts
)
is_group = identify_is_group(child) is_group = identify_is_group(child)
report_type = "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] \ report_type = (
else "Profit and Loss" "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] else "Profit and Loss"
)
account = frappe.get_doc({ account = frappe.get_doc(
"doctype": "Account", {
"account_name": child.get('account_name') if from_coa_importer else account_name, "doctype": "Account",
"company": company, "account_name": child.get("account_name") if from_coa_importer else account_name,
"parent_account": parent, "company": company,
"is_group": is_group, "parent_account": parent,
"root_type": root_type, "is_group": is_group,
"report_type": report_type, "root_type": root_type,
"account_number": account_number, "report_type": report_type,
"account_type": child.get("account_type"), "account_number": account_number,
"account_currency": child.get('account_currency') or frappe.db.get_value('Company', company, "default_currency"), "account_type": child.get("account_type"),
"tax_rate": child.get("tax_rate") "account_currency": child.get("account_currency")
}) or frappe.db.get_value("Company", company, "default_currency"),
"tax_rate": child.get("tax_rate"),
}
)
if root_account or frappe.local.flags.allow_unverified_charts: if root_account or frappe.local.flags.allow_unverified_charts:
account.flags.ignore_mandatory = True account.flags.ignore_mandatory = True
@ -63,10 +76,10 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
rebuild_tree("Account", "parent_account") rebuild_tree("Account", "parent_account")
frappe.local.flags.ignore_update_nsm = False frappe.local.flags.ignore_update_nsm = False
def add_suffix_if_duplicate(account_name, account_number, accounts): def add_suffix_if_duplicate(account_name, account_number, accounts):
if account_number: if account_number:
account_name_in_db = unidecode(" - ".join([account_number, account_name_in_db = unidecode(" - ".join([account_number, account_name.strip().lower()]))
account_name.strip().lower()]))
else: else:
account_name_in_db = unidecode(account_name.strip().lower()) account_name_in_db = unidecode(account_name.strip().lower())
@ -76,16 +89,21 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
return account_name, account_name_in_db return account_name, account_name_in_db
def identify_is_group(child): def identify_is_group(child):
if child.get("is_group"): if child.get("is_group"):
is_group = child.get("is_group") is_group = child.get("is_group")
elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])): elif len(
set(child.keys())
- set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
):
is_group = 1 is_group = 1
else: else:
is_group = 0 is_group = 0
return is_group return is_group
def get_chart(chart_template, existing_company=None): def get_chart(chart_template, existing_company=None):
chart = {} chart = {}
if existing_company: if existing_company:
@ -95,11 +113,13 @@ def get_chart(chart_template, existing_company=None):
from erpnext.accounts.doctype.account.chart_of_accounts.verified import ( from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
standard_chart_of_accounts, standard_chart_of_accounts,
) )
return standard_chart_of_accounts.get() return standard_chart_of_accounts.get()
elif chart_template == "Standard with Numbers": elif chart_template == "Standard with Numbers":
from erpnext.accounts.doctype.account.chart_of_accounts.verified import ( from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
standard_chart_of_accounts_with_account_number, standard_chart_of_accounts_with_account_number,
) )
return standard_chart_of_accounts_with_account_number.get() return standard_chart_of_accounts_with_account_number.get()
else: else:
folders = ("verified",) folders = ("verified",)
@ -115,6 +135,7 @@ def get_chart(chart_template, existing_company=None):
if chart and json.loads(chart).get("name") == chart_template: if chart and json.loads(chart).get("name") == chart_template:
return json.loads(chart).get("tree") return json.loads(chart).get("tree")
@frappe.whitelist() @frappe.whitelist()
def get_charts_for_country(country, with_standard=False): def get_charts_for_country(country, with_standard=False):
charts = [] charts = []
@ -122,9 +143,10 @@ def get_charts_for_country(country, with_standard=False):
def _get_chart_name(content): def _get_chart_name(content):
if content: if content:
content = json.loads(content) content = json.loads(content)
if (content and content.get("disabled", "No") == "No") \ if (
or frappe.local.flags.allow_unverified_charts: content and content.get("disabled", "No") == "No"
charts.append(content["name"]) ) or frappe.local.flags.allow_unverified_charts:
charts.append(content["name"])
country_code = frappe.db.get_value("Country", country, "code") country_code = frappe.db.get_value("Country", country, "code")
if country_code: if country_code:
@ -151,11 +173,21 @@ def get_charts_for_country(country, with_standard=False):
def get_account_tree_from_existing_company(existing_company): def get_account_tree_from_existing_company(existing_company):
all_accounts = frappe.get_all('Account', all_accounts = frappe.get_all(
filters={'company': existing_company}, "Account",
fields = ["name", "account_name", "parent_account", "account_type", filters={"company": existing_company},
"is_group", "root_type", "tax_rate", "account_number"], fields=[
order_by="lft, rgt") "name",
"account_name",
"parent_account",
"account_type",
"is_group",
"root_type",
"tax_rate",
"account_number",
],
order_by="lft, rgt",
)
account_tree = {} account_tree = {}
@ -164,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
build_account_tree(account_tree, None, all_accounts) build_account_tree(account_tree, None, all_accounts)
return account_tree return account_tree
def build_account_tree(tree, parent, all_accounts): def build_account_tree(tree, parent, all_accounts):
# find children # find children
parent_account = parent.name if parent else "" parent_account = parent.name if parent else ""
@ -192,27 +225,29 @@ def build_account_tree(tree, parent, all_accounts):
# call recursively to build a subtree for current account # call recursively to build a subtree for current account
build_account_tree(tree[child.account_name], child, all_accounts) build_account_tree(tree[child.account_name], child, all_accounts)
@frappe.whitelist() @frappe.whitelist()
def validate_bank_account(coa, bank_account): def validate_bank_account(coa, bank_account):
accounts = [] accounts = []
chart = get_chart(coa) chart = get_chart(coa)
if chart: if chart:
def _get_account_names(account_master): def _get_account_names(account_master):
for account_name, child in account_master.items(): for account_name, child in account_master.items():
if account_name not in ["account_number", "account_type", if account_name not in ["account_number", "account_type", "root_type", "is_group", "tax_rate"]:
"root_type", "is_group", "tax_rate"]:
accounts.append(account_name) accounts.append(account_name)
_get_account_names(child) _get_account_names(child)
_get_account_names(chart) _get_account_names(chart)
return (bank_account in accounts) return bank_account in accounts
@frappe.whitelist() @frappe.whitelist()
def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False): def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
''' get chart template from its folder and parse the json to be rendered as tree ''' """get chart template from its folder and parse the json to be rendered as tree"""
chart = chart_data or get_chart(chart_template) chart = chart_data or get_chart(chart_template)
# if no template selected, return as it is # if no template selected, return as it is
@ -220,22 +255,33 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
return return
accounts = [] accounts = []
def _import_accounts(children, parent): def _import_accounts(children, parent):
''' recursively called to form a parent-child based list of dict from chart template ''' """recursively called to form a parent-child based list of dict from chart template"""
for account_name, child in children.items(): for account_name, child in children.items():
account = {} account = {}
if account_name in ["account_name", "account_number", "account_type",\ if account_name in [
"root_type", "is_group", "tax_rate"]: continue "account_name",
"account_number",
"account_type",
"root_type",
"is_group",
"tax_rate",
]:
continue
if from_coa_importer: if from_coa_importer:
account_name = child['account_name'] account_name = child["account_name"]
account['parent_account'] = parent account["parent_account"] = parent
account['expandable'] = True if identify_is_group(child) else False account["expandable"] = True if identify_is_group(child) else False
account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \ account["value"] = (
if child.get('account_number') else account_name (cstr(child.get("account_number")).strip() + " - " + account_name)
if child.get("account_number")
else account_name
)
accounts.append(account) accounts.append(account)
_import_accounts(child, account['value']) _import_accounts(child, account["value"])
_import_accounts(chart, None) _import_accounts(chart, None)
return accounts return accounts

View File

@ -20,6 +20,7 @@ charts = {}
all_account_types = [] all_account_types = []
all_roots = {} all_roots = {}
def go(): def go():
global accounts, charts global accounts, charts
default_account_types = get_default_account_types() default_account_types = get_default_account_types()
@ -34,14 +35,16 @@ def go():
accounts, charts = {}, {} accounts, charts = {}, {}
country_path = os.path.join(path, country_dir) country_path = os.path.join(path, country_dir)
manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read()) manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read())
data_files = manifest.get("data", []) + manifest.get("init_xml", []) + \ data_files = (
manifest.get("update_xml", []) manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", [])
)
files_path = [os.path.join(country_path, d) for d in data_files] files_path = [os.path.join(country_path, d) for d in data_files]
xml_roots = get_xml_roots(files_path) xml_roots = get_xml_roots(files_path)
csv_content = get_csv_contents(files_path) csv_content = get_csv_contents(files_path)
prefix = country_dir if csv_content else None prefix = country_dir if csv_content else None
account_types = get_account_types(xml_roots.get("account.account.type", []), account_types = get_account_types(
csv_content.get("account.account.type", []), prefix) xml_roots.get("account.account.type", []), csv_content.get("account.account.type", []), prefix
)
account_types.update(default_account_types) account_types.update(default_account_types)
if xml_roots: if xml_roots:
@ -54,12 +57,15 @@ def go():
create_all_roots_file() create_all_roots_file()
def get_default_account_types(): def get_default_account_types():
default_types_root = [] default_types_root = []
default_types_root.append(ET.parse(os.path.join(path, "account", "data", default_types_root.append(
"data_account_type.xml")).getroot()) ET.parse(os.path.join(path, "account", "data", "data_account_type.xml")).getroot()
)
return get_account_types(default_types_root, None, prefix="account") return get_account_types(default_types_root, None, prefix="account")
def get_xml_roots(files_path): def get_xml_roots(files_path):
xml_roots = frappe._dict() xml_roots = frappe._dict()
for filepath in files_path: for filepath in files_path:
@ -68,64 +74,69 @@ def get_xml_roots(files_path):
tree = ET.parse(filepath) tree = ET.parse(filepath)
root = tree.getroot() root = tree.getroot()
for node in root[0].findall("record"): for node in root[0].findall("record"):
if node.get("model") in ["account.account.template", if node.get("model") in [
"account.chart.template", "account.account.type"]: "account.account.template",
"account.chart.template",
"account.account.type",
]:
xml_roots.setdefault(node.get("model"), []).append(root) xml_roots.setdefault(node.get("model"), []).append(root)
break break
return xml_roots return xml_roots
def get_csv_contents(files_path): def get_csv_contents(files_path):
csv_content = {} csv_content = {}
for filepath in files_path: for filepath in files_path:
fname = os.path.basename(filepath) fname = os.path.basename(filepath)
for file_type in ["account.account.template", "account.account.type", for file_type in ["account.account.template", "account.account.type", "account.chart.template"]:
"account.chart.template"]:
if fname.startswith(file_type) and fname.endswith(".csv"): if fname.startswith(file_type) and fname.endswith(".csv"):
with open(filepath, "r") as csvfile: with open(filepath, "r") as csvfile:
try: try:
csv_content.setdefault(file_type, [])\ csv_content.setdefault(file_type, []).append(read_csv_content(csvfile.read()))
.append(read_csv_content(csvfile.read()))
except Exception as e: except Exception as e:
continue continue
return csv_content return csv_content
def get_account_types(root_list, csv_content, prefix=None): def get_account_types(root_list, csv_content, prefix=None):
types = {} types = {}
account_type_map = { account_type_map = {
'cash': 'Cash', "cash": "Cash",
'bank': 'Bank', "bank": "Bank",
'tr_cash': 'Cash', "tr_cash": "Cash",
'tr_bank': 'Bank', "tr_bank": "Bank",
'receivable': 'Receivable', "receivable": "Receivable",
'tr_receivable': 'Receivable', "tr_receivable": "Receivable",
'account rec': 'Receivable', "account rec": "Receivable",
'payable': 'Payable', "payable": "Payable",
'tr_payable': 'Payable', "tr_payable": "Payable",
'equity': 'Equity', "equity": "Equity",
'stocks': 'Stock', "stocks": "Stock",
'stock': 'Stock', "stock": "Stock",
'tax': 'Tax', "tax": "Tax",
'tr_tax': 'Tax', "tr_tax": "Tax",
'tax-out': 'Tax', "tax-out": "Tax",
'tax-in': 'Tax', "tax-in": "Tax",
'charges_personnel': 'Chargeable', "charges_personnel": "Chargeable",
'fixed asset': 'Fixed Asset', "fixed asset": "Fixed Asset",
'cogs': 'Cost of Goods Sold', "cogs": "Cost of Goods Sold",
} }
for root in root_list: for root in root_list:
for node in root[0].findall("record"): for node in root[0].findall("record"):
if node.get("model")=="account.account.type": if node.get("model") == "account.account.type":
data = {} data = {}
for field in node.findall("field"): for field in node.findall("field"):
if field.get("name")=="code" and field.text.lower() != "none" \ if (
and account_type_map.get(field.text): field.get("name") == "code"
data["account_type"] = account_type_map[field.text] and field.text.lower() != "none"
and account_type_map.get(field.text)
):
data["account_type"] = account_type_map[field.text]
node_id = prefix + "." + node.get("id") if prefix else node.get("id") node_id = prefix + "." + node.get("id") if prefix else node.get("id")
types[node_id] = data types[node_id] = data
if csv_content and csv_content[0][0]=="id": if csv_content and csv_content[0][0] == "id":
for row in csv_content[1:]: for row in csv_content[1:]:
row_dict = dict(zip(csv_content[0], row)) row_dict = dict(zip(csv_content[0], row))
data = {} data = {}
@ -136,21 +147,22 @@ def get_account_types(root_list, csv_content, prefix=None):
types[node_id] = data types[node_id] = data
return types return types
def make_maps_for_xml(xml_roots, account_types, country_dir): def make_maps_for_xml(xml_roots, account_types, country_dir):
"""make maps for `charts` and `accounts`""" """make maps for `charts` and `accounts`"""
for model, root_list in xml_roots.items(): for model, root_list in xml_roots.items():
for root in root_list: for root in root_list:
for node in root[0].findall("record"): for node in root[0].findall("record"):
if node.get("model")=="account.account.template": if node.get("model") == "account.account.template":
data = {} data = {}
for field in node.findall("field"): for field in node.findall("field"):
if field.get("name")=="name": if field.get("name") == "name":
data["name"] = field.text data["name"] = field.text
if field.get("name")=="parent_id": if field.get("name") == "parent_id":
parent_id = field.get("ref") or field.get("eval") parent_id = field.get("ref") or field.get("eval")
data["parent_id"] = parent_id data["parent_id"] = parent_id
if field.get("name")=="user_type": if field.get("name") == "user_type":
value = field.get("ref") value = field.get("ref")
if account_types.get(value, {}).get("account_type"): if account_types.get(value, {}).get("account_type"):
data["account_type"] = account_types[value]["account_type"] data["account_type"] = account_types[value]["account_type"]
@ -160,16 +172,17 @@ def make_maps_for_xml(xml_roots, account_types, country_dir):
data["children"] = [] data["children"] = []
accounts[node.get("id")] = data accounts[node.get("id")] = data
if node.get("model")=="account.chart.template": if node.get("model") == "account.chart.template":
data = {} data = {}
for field in node.findall("field"): for field in node.findall("field"):
if field.get("name")=="name": if field.get("name") == "name":
data["name"] = field.text data["name"] = field.text
if field.get("name")=="account_root_id": if field.get("name") == "account_root_id":
data["account_root_id"] = field.get("ref") data["account_root_id"] = field.get("ref")
data["id"] = country_dir data["id"] = country_dir
charts.setdefault(node.get("id"), {}).update(data) charts.setdefault(node.get("id"), {}).update(data)
def make_maps_for_csv(csv_content, account_types, country_dir): def make_maps_for_csv(csv_content, account_types, country_dir):
for content in csv_content.get("account.account.template", []): for content in csv_content.get("account.account.template", []):
for row in content[1:]: for row in content[1:]:
@ -177,7 +190,7 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
account = { account = {
"name": data.get("name"), "name": data.get("name"),
"parent_id": data.get("parent_id:id") or data.get("parent_id/id"), "parent_id": data.get("parent_id:id") or data.get("parent_id/id"),
"children": [] "children": [],
} }
user_type = data.get("user_type/id") or data.get("user_type:id") user_type = data.get("user_type/id") or data.get("user_type:id")
if account_types.get(user_type, {}).get("account_type"): if account_types.get(user_type, {}).get("account_type"):
@ -194,12 +207,14 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
for row in content[1:]: for row in content[1:]:
if row: if row:
data = dict(zip(content[0], row)) data = dict(zip(content[0], row))
charts.setdefault(data.get("id"), {}).update({ charts.setdefault(data.get("id"), {}).update(
"account_root_id": data.get("account_root_id:id") or \ {
data.get("account_root_id/id"), "account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"),
"name": data.get("name"), "name": data.get("name"),
"id": country_dir "id": country_dir,
}) }
)
def make_account_trees(): def make_account_trees():
"""build tree hierarchy""" """build tree hierarchy"""
@ -218,6 +233,7 @@ def make_account_trees():
if "children" in accounts[id] and not accounts[id].get("children"): if "children" in accounts[id] and not accounts[id].get("children"):
del accounts[id]["children"] del accounts[id]["children"]
def make_charts(): def make_charts():
"""write chart files in app/setup/doctype/company/charts""" """write chart files in app/setup/doctype/company/charts"""
for chart_id in charts: for chart_id in charts:
@ -236,34 +252,38 @@ def make_charts():
chart["country_code"] = src["id"][5:] chart["country_code"] = src["id"][5:]
chart["tree"] = accounts[src["account_root_id"]] chart["tree"] = accounts[src["account_root_id"]]
for key, val in chart["tree"].items(): for key, val in chart["tree"].items():
if key in ["name", "parent_id"]: if key in ["name", "parent_id"]:
chart["tree"].pop(key) chart["tree"].pop(key)
if type(val) == dict: if type(val) == dict:
val["root_type"] = "" val["root_type"] = ""
if chart: if chart:
fpath = os.path.join("erpnext", "erpnext", "accounts", "doctype", "account", fpath = os.path.join(
"chart_of_accounts", filename + ".json") "erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json"
)
with open(fpath, "r") as chartfile: with open(fpath, "r") as chartfile:
old_content = chartfile.read() old_content = chartfile.read()
if not old_content or (json.loads(old_content).get("is_active", "No") == "No" \ if not old_content or (
and json.loads(old_content).get("disabled", "No") == "No"): json.loads(old_content).get("is_active", "No") == "No"
and json.loads(old_content).get("disabled", "No") == "No"
):
with open(fpath, "w") as chartfile: with open(fpath, "w") as chartfile:
chartfile.write(json.dumps(chart, indent=4, sort_keys=True)) chartfile.write(json.dumps(chart, indent=4, sort_keys=True))
all_roots.setdefault(filename, chart["tree"].keys()) all_roots.setdefault(filename, chart["tree"].keys())
def create_all_roots_file(): def create_all_roots_file():
with open('all_roots.txt', 'w') as f: with open("all_roots.txt", "w") as f:
for filename, roots in sorted(all_roots.items()): for filename, roots in sorted(all_roots.items()):
f.write(filename) f.write(filename)
f.write('\n----------------------\n') f.write("\n----------------------\n")
for r in sorted(roots): for r in sorted(roots):
f.write(r.encode('utf-8')) f.write(r.encode("utf-8"))
f.write('\n') f.write("\n")
f.write('\n\n\n') f.write("\n\n\n")
if __name__=="__main__":
if __name__ == "__main__":
go() go()

View File

@ -7,182 +7,103 @@ from frappe import _
def get(): def get():
return { return {
_("Application of Funds (Assets)"): { _("Application of Funds (Assets)"): {
_("Current Assets"): { _("Current Assets"): {
_("Accounts Receivable"): { _("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
_("Debtors"): { _("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
"account_type": "Receivable" _("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
} _("Loans and Advances (Assets)"): {
}, _("Employee Advances"): {},
_("Bank Accounts"): {
"account_type": "Bank",
"is_group": 1
},
_("Cash In Hand"): {
_("Cash"): {
"account_type": "Cash"
},
"account_type": "Cash"
},
_("Loans and Advances (Assets)"): {
_("Employee Advances"): {
},
},
_("Securities and Deposits"): {
_("Earnest Money"): {}
},
_("Stock Assets"): {
_("Stock In Hand"): {
"account_type": "Stock"
},
"account_type": "Stock",
},
_("Tax Assets"): {
"is_group": 1
}
},
_("Fixed Assets"): {
_("Capital Equipments"): {
"account_type": "Fixed Asset"
},
_("Electronic Equipments"): {
"account_type": "Fixed Asset"
},
_("Furnitures and Fixtures"): {
"account_type": "Fixed Asset"
},
_("Office Equipments"): {
"account_type": "Fixed Asset"
},
_("Plants and Machineries"): {
"account_type": "Fixed Asset"
},
_("Buildings"): {
"account_type": "Fixed Asset"
}, },
_("Softwares"): { _("Securities and Deposits"): {_("Earnest Money"): {}},
"account_type": "Fixed Asset" _("Stock Assets"): {
_("Stock In Hand"): {"account_type": "Stock"},
"account_type": "Stock",
}, },
_("Accumulated Depreciation"): { _("Tax Assets"): {"is_group": 1},
"account_type": "Accumulated Depreciation" },
}, _("Fixed Assets"): {
_("CWIP Account"): { _("Capital Equipments"): {"account_type": "Fixed Asset"},
"account_type": "Capital Work in Progress", _("Electronic Equipments"): {"account_type": "Fixed Asset"},
} _("Furnitures and Fixtures"): {"account_type": "Fixed Asset"},
}, _("Office Equipments"): {"account_type": "Fixed Asset"},
_("Investments"): { _("Plants and Machineries"): {"account_type": "Fixed Asset"},
"is_group": 1 _("Buildings"): {"account_type": "Fixed Asset"},
}, _("Softwares"): {"account_type": "Fixed Asset"},
_("Temporary Accounts"): { _("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
_("Temporary Opening"): { _("CWIP Account"): {
"account_type": "Temporary" "account_type": "Capital Work in Progress",
} },
}, },
"root_type": "Asset" _("Investments"): {"is_group": 1},
}, _("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
_("Expenses"): { "root_type": "Asset",
_("Direct Expenses"): { },
_("Stock Expenses"): { _("Expenses"): {
_("Cost of Goods Sold"): { _("Direct Expenses"): {
"account_type": "Cost of Goods Sold" _("Stock Expenses"): {
}, _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
_("Expenses Included In Asset Valuation"): { _("Expenses Included In Asset Valuation"): {
"account_type": "Expenses Included In Asset Valuation" "account_type": "Expenses Included In Asset Valuation"
}, },
_("Expenses Included In Valuation"): { _("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
"account_type": "Expenses Included In Valuation" _("Stock Adjustment"): {"account_type": "Stock Adjustment"},
}, },
_("Stock Adjustment"): { },
"account_type": "Stock Adjustment" _("Indirect Expenses"): {
} _("Administrative Expenses"): {},
}, _("Commission on Sales"): {},
}, _("Depreciation"): {"account_type": "Depreciation"},
_("Indirect Expenses"): { _("Entertainment Expenses"): {},
_("Administrative Expenses"): {}, _("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
_("Commission on Sales"): {}, _("Legal Expenses"): {},
_("Depreciation"): { _("Marketing Expenses"): {"account_type": "Chargeable"},
"account_type": "Depreciation" _("Miscellaneous Expenses"): {"account_type": "Chargeable"},
}, _("Office Maintenance Expenses"): {},
_("Entertainment Expenses"): {}, _("Office Rent"): {},
_("Freight and Forwarding Charges"): { _("Postal Expenses"): {},
"account_type": "Chargeable" _("Print and Stationery"): {},
}, _("Round Off"): {"account_type": "Round Off"},
_("Legal Expenses"): {}, _("Salary"): {},
_("Marketing Expenses"): { _("Sales Expenses"): {},
"account_type": "Chargeable" _("Telephone Expenses"): {},
}, _("Travel Expenses"): {},
_("Miscellaneous Expenses"): { _("Utility Expenses"): {},
"account_type": "Chargeable"
},
_("Office Maintenance Expenses"): {},
_("Office Rent"): {},
_("Postal Expenses"): {},
_("Print and Stationery"): {},
_("Round Off"): {
"account_type": "Round Off"
},
_("Salary"): {},
_("Sales Expenses"): {},
_("Telephone Expenses"): {},
_("Travel Expenses"): {},
_("Utility Expenses"): {},
_("Write Off"): {}, _("Write Off"): {},
_("Exchange Gain/Loss"): {}, _("Exchange Gain/Loss"): {},
_("Gain/Loss on Asset Disposal"): {} _("Gain/Loss on Asset Disposal"): {},
}, },
"root_type": "Expense" "root_type": "Expense",
}, },
_("Income"): { _("Income"): {
_("Direct Income"): { _("Direct Income"): {_("Sales"): {}, _("Service"): {}},
_("Sales"): {}, _("Indirect Income"): {"is_group": 1},
_("Service"): {} "root_type": "Income",
}, },
_("Indirect Income"): { _("Source of Funds (Liabilities)"): {
"is_group": 1 _("Current Liabilities"): {
}, _("Accounts Payable"): {
"root_type": "Income" _("Creditors"): {"account_type": "Payable"},
}, _("Payroll Payable"): {},
_("Source of Funds (Liabilities)"): {
_("Current Liabilities"): {
_("Accounts Payable"): {
_("Creditors"): {
"account_type": "Payable"
},
_("Payroll Payable"): {},
},
_("Stock Liabilities"): {
_("Stock Received But Not Billed"): {
"account_type": "Stock Received But Not Billed"
},
_("Asset Received But Not Billed"): {
"account_type": "Asset Received But Not Billed"
}
},
_("Duties and Taxes"): {
"account_type": "Tax",
"is_group": 1
}, },
_("Stock Liabilities"): {
_("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
_("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
},
_("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
_("Loans (Liabilities)"): { _("Loans (Liabilities)"): {
_("Secured Loans"): {}, _("Secured Loans"): {},
_("Unsecured Loans"): {}, _("Unsecured Loans"): {},
_("Bank Overdraft Account"): {}, _("Bank Overdraft Account"): {},
}, },
}, },
"root_type": "Liability" "root_type": "Liability",
}, },
_("Equity"): { _("Equity"): {
_("Capital Stock"): { _("Capital Stock"): {"account_type": "Equity"},
"account_type": "Equity" _("Dividends Paid"): {"account_type": "Equity"},
}, _("Opening Balance Equity"): {"account_type": "Equity"},
_("Dividends Paid"): { _("Retained Earnings"): {"account_type": "Equity"},
"account_type": "Equity" "root_type": "Equity",
}, },
_("Opening Balance Equity"): {
"account_type": "Equity"
},
_("Retained Earnings"): {
"account_type": "Equity"
},
"root_type": "Equity"
}
} }

View File

@ -6,288 +6,153 @@ from frappe import _
def get(): def get():
return { return {
_("Application of Funds (Assets)"): { _("Application of Funds (Assets)"): {
_("Current Assets"): { _("Current Assets"): {
_("Accounts Receivable"): { _("Accounts Receivable"): {
_("Debtors"): { _("Debtors"): {"account_type": "Receivable", "account_number": "1310"},
"account_type": "Receivable", "account_number": "1300",
"account_number": "1310" },
}, _("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
"account_number": "1300" _("Cash In Hand"): {
}, _("Cash"): {"account_type": "Cash", "account_number": "1110"},
_("Bank Accounts"): { "account_type": "Cash",
"account_type": "Bank", "account_number": "1100",
"is_group": 1, },
"account_number": "1200" _("Loans and Advances (Assets)"): {
}, _("Employee Advances"): {"account_number": "1610"},
_("Cash In Hand"): { "account_number": "1600",
_("Cash"): { },
"account_type": "Cash", _("Securities and Deposits"): {
"account_number": "1110" _("Earnest Money"): {"account_number": "1651"},
}, "account_number": "1650",
"account_type": "Cash", },
"account_number": "1100" _("Stock Assets"): {
}, _("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"},
_("Loans and Advances (Assets)"): { "account_type": "Stock",
_("Employee Advances"): { "account_number": "1400",
"account_number": "1610" },
}, _("Tax Assets"): {"is_group": 1, "account_number": "1500"},
"account_number": "1600" "account_number": "1100-1600",
}, },
_("Securities and Deposits"): { _("Fixed Assets"): {
_("Earnest Money"): { _("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"},
"account_number": "1651" _("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"},
}, _("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
"account_number": "1650" _("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"},
}, _("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
_("Stock Assets"): { _("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
_("Stock In Hand"): { _("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"},
"account_type": "Stock", _("Accumulated Depreciation"): {
"account_number": "1410" "account_type": "Accumulated Depreciation",
}, "account_number": "1780",
"account_type": "Stock", },
"account_number": "1400" _("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"},
}, "account_number": "1700",
_("Tax Assets"): { },
"is_group": 1, _("Investments"): {"is_group": 1, "account_number": "1800"},
"account_number": "1500" _("Temporary Accounts"): {
}, _("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"},
"account_number": "1100-1600" "account_number": "1900",
}, },
_("Fixed Assets"): { "root_type": "Asset",
_("Capital Equipments"): { "account_number": "1000",
"account_type": "Fixed Asset", },
"account_number": "1710" _("Expenses"): {
}, _("Direct Expenses"): {
_("Electronic Equipments"): { _("Stock Expenses"): {
"account_type": "Fixed Asset", _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"},
"account_number": "1720" _("Expenses Included In Asset Valuation"): {
}, "account_type": "Expenses Included In Asset Valuation",
_("Furnitures and Fixtures"): { "account_number": "5112",
"account_type": "Fixed Asset", },
"account_number": "1730" _("Expenses Included In Valuation"): {
}, "account_type": "Expenses Included In Valuation",
_("Office Equipments"): { "account_number": "5118",
"account_type": "Fixed Asset", },
"account_number": "1740" _("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"},
}, "account_number": "5110",
_("Plants and Machineries"): { },
"account_type": "Fixed Asset", "account_number": "5100",
"account_number": "1750" },
}, _("Indirect Expenses"): {
_("Buildings"): { _("Administrative Expenses"): {"account_number": "5201"},
"account_type": "Fixed Asset", _("Commission on Sales"): {"account_number": "5202"},
"account_number": "1760" _("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"},
}, _("Entertainment Expenses"): {"account_number": "5204"},
_("Softwares"): { _("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"},
"account_type": "Fixed Asset", _("Legal Expenses"): {"account_number": "5206"},
"account_number": "1770" _("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"},
}, _("Office Maintenance Expenses"): {"account_number": "5208"},
_("Accumulated Depreciation"): { _("Office Rent"): {"account_number": "5209"},
"account_type": "Accumulated Depreciation", _("Postal Expenses"): {"account_number": "5210"},
"account_number": "1780" _("Print and Stationery"): {"account_number": "5211"},
}, _("Round Off"): {"account_type": "Round Off", "account_number": "5212"},
_("CWIP Account"): { _("Salary"): {"account_number": "5213"},
"account_type": "Capital Work in Progress", _("Sales Expenses"): {"account_number": "5214"},
"account_number": "1790" _("Telephone Expenses"): {"account_number": "5215"},
}, _("Travel Expenses"): {"account_number": "5216"},
"account_number": "1700" _("Utility Expenses"): {"account_number": "5217"},
}, _("Write Off"): {"account_number": "5218"},
_("Investments"): { _("Exchange Gain/Loss"): {"account_number": "5219"},
"is_group": 1, _("Gain/Loss on Asset Disposal"): {"account_number": "5220"},
"account_number": "1800" _("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"},
}, "account_number": "5200",
_("Temporary Accounts"): { },
_("Temporary Opening"): { "root_type": "Expense",
"account_type": "Temporary", "account_number": "5000",
"account_number": "1910" },
}, _("Income"): {
"account_number": "1900" _("Direct Income"): {
}, _("Sales"): {"account_number": "4110"},
"root_type": "Asset", _("Service"): {"account_number": "4120"},
"account_number": "1000" "account_number": "4100",
}, },
_("Expenses"): { _("Indirect Income"): {"is_group": 1, "account_number": "4200"},
_("Direct Expenses"): { "root_type": "Income",
_("Stock Expenses"): { "account_number": "4000",
_("Cost of Goods Sold"): { },
"account_type": "Cost of Goods Sold", _("Source of Funds (Liabilities)"): {
"account_number": "5111" _("Current Liabilities"): {
}, _("Accounts Payable"): {
_("Expenses Included In Asset Valuation"): { _("Creditors"): {"account_type": "Payable", "account_number": "2110"},
"account_type": "Expenses Included In Asset Valuation", _("Payroll Payable"): {"account_number": "2120"},
"account_number": "5112" "account_number": "2100",
}, },
_("Expenses Included In Valuation"): { _("Stock Liabilities"): {
"account_type": "Expenses Included In Valuation", _("Stock Received But Not Billed"): {
"account_number": "5118" "account_type": "Stock Received But Not Billed",
}, "account_number": "2210",
_("Stock Adjustment"): { },
"account_type": "Stock Adjustment", _("Asset Received But Not Billed"): {
"account_number": "5119" "account_type": "Asset Received But Not Billed",
}, "account_number": "2211",
"account_number": "5110" },
}, "account_number": "2200",
"account_number": "5100" },
}, _("Duties and Taxes"): {
_("Indirect Expenses"): { _("TDS Payable"): {"account_number": "2310"},
_("Administrative Expenses"): { "account_type": "Tax",
"account_number": "5201" "is_group": 1,
}, "account_number": "2300",
_("Commission on Sales"): { },
"account_number": "5202" _("Loans (Liabilities)"): {
}, _("Secured Loans"): {"account_number": "2410"},
_("Depreciation"): { _("Unsecured Loans"): {"account_number": "2420"},
"account_type": "Depreciation", _("Bank Overdraft Account"): {"account_number": "2430"},
"account_number": "5203" "account_number": "2400",
}, },
_("Entertainment Expenses"): { "account_number": "2100-2400",
"account_number": "5204" },
}, "root_type": "Liability",
_("Freight and Forwarding Charges"): { "account_number": "2000",
"account_type": "Chargeable", },
"account_number": "5205" _("Equity"): {
}, _("Capital Stock"): {"account_type": "Equity", "account_number": "3100"},
_("Legal Expenses"): { _("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"},
"account_number": "5206" _("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"},
}, _("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
_("Marketing Expenses"): { "root_type": "Equity",
"account_type": "Chargeable", "account_number": "3000",
"account_number": "5207" },
}, }
_("Office Maintenance Expenses"): {
"account_number": "5208"
},
_("Office Rent"): {
"account_number": "5209"
},
_("Postal Expenses"): {
"account_number": "5210"
},
_("Print and Stationery"): {
"account_number": "5211"
},
_("Round Off"): {
"account_type": "Round Off",
"account_number": "5212"
},
_("Salary"): {
"account_number": "5213"
},
_("Sales Expenses"): {
"account_number": "5214"
},
_("Telephone Expenses"): {
"account_number": "5215"
},
_("Travel Expenses"): {
"account_number": "5216"
},
_("Utility Expenses"): {
"account_number": "5217"
},
_("Write Off"): {
"account_number": "5218"
},
_("Exchange Gain/Loss"): {
"account_number": "5219"
},
_("Gain/Loss on Asset Disposal"): {
"account_number": "5220"
},
_("Miscellaneous Expenses"): {
"account_type": "Chargeable",
"account_number": "5221"
},
"account_number": "5200"
},
"root_type": "Expense",
"account_number": "5000"
},
_("Income"): {
_("Direct Income"): {
_("Sales"): {
"account_number": "4110"
},
_("Service"): {
"account_number": "4120"
},
"account_number": "4100"
},
_("Indirect Income"): {
"is_group": 1,
"account_number": "4200"
},
"root_type": "Income",
"account_number": "4000"
},
_("Source of Funds (Liabilities)"): {
_("Current Liabilities"): {
_("Accounts Payable"): {
_("Creditors"): {
"account_type": "Payable",
"account_number": "2110"
},
_("Payroll Payable"): {
"account_number": "2120"
},
"account_number": "2100"
},
_("Stock Liabilities"): {
_("Stock Received But Not Billed"): {
"account_type": "Stock Received But Not Billed",
"account_number": "2210"
},
_("Asset Received But Not Billed"): {
"account_type": "Asset Received But Not Billed",
"account_number": "2211"
},
"account_number": "2200"
},
_("Duties and Taxes"): {
_("TDS Payable"): {
"account_number": "2310"
},
"account_type": "Tax",
"is_group": 1,
"account_number": "2300"
},
_("Loans (Liabilities)"): {
_("Secured Loans"): {
"account_number": "2410"
},
_("Unsecured Loans"): {
"account_number": "2420"
},
_("Bank Overdraft Account"): {
"account_number": "2430"
},
"account_number": "2400"
},
"account_number": "2100-2400"
},
"root_type": "Liability",
"account_number": "2000"
},
_("Equity"): {
_("Capital Stock"): {
"account_type": "Equity",
"account_number": "3100"
},
_("Dividends Paid"): {
"account_type": "Equity",
"account_number": "3200"
},
_("Opening Balance Equity"): {
"account_type": "Equity",
"account_number": "3300"
},
_("Retained Earnings"): {
"account_type": "Equity",
"account_number": "3400"
},
"root_type": "Equity",
"account_number": "3000"
}
}

View File

@ -20,8 +20,9 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company" acc.company = "_Test Company"
acc.insert() acc.insert()
account_number, account_name = frappe.db.get_value("Account", "1210 - Debtors - _TC", account_number, account_name = frappe.db.get_value(
["account_number", "account_name"]) "Account", "1210 - Debtors - _TC", ["account_number", "account_name"]
)
self.assertEqual(account_number, "1210") self.assertEqual(account_number, "1210")
self.assertEqual(account_name, "Debtors") self.assertEqual(account_name, "Debtors")
@ -30,8 +31,12 @@ class TestAccount(unittest.TestCase):
update_account_number("1210 - Debtors - _TC", new_account_name, new_account_number) update_account_number("1210 - Debtors - _TC", new_account_name, new_account_number)
new_acc = frappe.db.get_value("Account", "1211-11-4 - 6 - - Debtors 1 - Test - - _TC", new_acc = frappe.db.get_value(
["account_name", "account_number"], as_dict=1) "Account",
"1211-11-4 - 6 - - Debtors 1 - Test - - _TC",
["account_name", "account_number"],
as_dict=1,
)
self.assertEqual(new_acc.account_name, "Debtors 1 - Test -") self.assertEqual(new_acc.account_name, "Debtors 1 - Test -")
self.assertEqual(new_acc.account_number, "1211-11-4 - 6 -") self.assertEqual(new_acc.account_number, "1211-11-4 - 6 -")
@ -79,7 +84,9 @@ class TestAccount(unittest.TestCase):
self.assertEqual(parent, "Securities and Deposits - _TC") self.assertEqual(parent, "Securities and Deposits - _TC")
merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company) merge_account(
"Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
)
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account") parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
# Parent account of the child account changes after merging # Parent account of the child account changes after merging
@ -91,14 +98,28 @@ class TestAccount(unittest.TestCase):
doc = frappe.get_doc("Account", "Current Assets - _TC") doc = frappe.get_doc("Account", "Current Assets - _TC")
# Raise error as is_group property doesn't match # Raise error as is_group property doesn't match
self.assertRaises(frappe.ValidationError, merge_account, "Current Assets - _TC",\ self.assertRaises(
"Accumulated Depreciation - _TC", doc.is_group, doc.root_type, doc.company) frappe.ValidationError,
merge_account,
"Current Assets - _TC",
"Accumulated Depreciation - _TC",
doc.is_group,
doc.root_type,
doc.company,
)
doc = frappe.get_doc("Account", "Capital Stock - _TC") doc = frappe.get_doc("Account", "Capital Stock - _TC")
# Raise error as root_type property doesn't match # Raise error as root_type property doesn't match
self.assertRaises(frappe.ValidationError, merge_account, "Capital Stock - _TC",\ self.assertRaises(
"Softwares - _TC", doc.is_group, doc.root_type, doc.company) frappe.ValidationError,
merge_account,
"Capital Stock - _TC",
"Softwares - _TC",
doc.is_group,
doc.root_type,
doc.company,
)
def test_account_sync(self): def test_account_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None) frappe.local.flags.pop("ignore_root_company_validation", None)
@ -109,8 +130,12 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company 3" acc.company = "_Test Company 3"
acc.insert() acc.insert()
acc_tc_4 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 4"}) acc_tc_4 = frappe.db.get_value(
acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 5"}) "Account", {"account_name": "Test Sync Account", "company": "_Test Company 4"}
)
acc_tc_5 = frappe.db.get_value(
"Account", {"account_name": "Test Sync Account", "company": "_Test Company 5"}
)
self.assertEqual(acc_tc_4, "Test Sync Account - _TC4") self.assertEqual(acc_tc_4, "Test Sync Account - _TC4")
self.assertEqual(acc_tc_5, "Test Sync Account - _TC5") self.assertEqual(acc_tc_5, "Test Sync Account - _TC5")
@ -138,8 +163,26 @@ class TestAccount(unittest.TestCase):
update_account_number(acc.name, "Test Rename Sync Account", "1234") update_account_number(acc.name, "Test Rename Sync Account", "1234")
# Check if renamed in children # Check if renamed in children
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 4", "account_number": "1234"})) self.assertTrue(
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 5", "account_number": "1234"})) frappe.db.exists(
"Account",
{
"account_name": "Test Rename Sync Account",
"company": "_Test Company 4",
"account_number": "1234",
},
)
)
self.assertTrue(
frappe.db.exists(
"Account",
{
"account_name": "Test Rename Sync Account",
"company": "_Test Company 5",
"account_number": "1234",
},
)
)
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC3") frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC3")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4") frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
@ -155,22 +198,46 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company 3" acc.company = "_Test Company 3"
acc.insert() acc.insert()
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 4"})) self.assertTrue(
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 5"})) frappe.db.exists(
"Account", {"account_name": "Test Group Account", "company": "_Test Company 4"}
)
)
self.assertTrue(
frappe.db.exists(
"Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
)
)
# Try renaming child company account # Try renaming child company account
acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Group Account", "company": "_Test Company 5"}) acc_tc_5 = frappe.db.get_value(
self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account") "Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
)
self.assertRaises(
frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account"
)
# Rename child company account with allow_account_creation_against_child_company enabled # Rename child company account with allow_account_creation_against_child_company enabled
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1) frappe.db.set_value(
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 1
)
update_account_number(acc_tc_5, "Test Modified Account") update_account_number(acc_tc_5, "Test Modified Account")
self.assertTrue(frappe.db.exists("Account", {'name': "Test Modified Account - _TC5", "company": "_Test Company 5"})) self.assertTrue(
frappe.db.exists(
"Account", {"name": "Test Modified Account - _TC5", "company": "_Test Company 5"}
)
)
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0) frappe.db.set_value(
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 0
)
to_delete = ["Test Group Account - _TC3", "Test Group Account - _TC4", "Test Modified Account - _TC5"] to_delete = [
"Test Group Account - _TC3",
"Test Group Account - _TC4",
"Test Modified Account - _TC5",
]
for doc in to_delete: for doc in to_delete:
frappe.delete_doc("Account", doc) frappe.delete_doc("Account", doc)
@ -184,20 +251,16 @@ def _make_test_records(verbose=None):
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"], ["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"], ["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
["_Test Cash", "Cash In Hand", 0, "Cash", None], ["_Test Cash", "Cash In Hand", 0, "Cash", None],
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None], ["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None], ["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None], ["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None], ["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None], ["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
["_Test Employee Advance", "Current Liabilities", 0, None, None], ["_Test Employee Advance", "Current Liabilities", 0, None, None],
["_Test Account Tax Assets", "Current Assets", 1, None, None], ["_Test Account Tax Assets", "Current Assets", 1, None, None],
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None], ["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None], ["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None], ["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None],
["_Test Account Cost for Goods Sold", "Expenses", 0, None, None], ["_Test Account Cost for Goods Sold", "Expenses", 0, None, None],
["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None], ["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None], ["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
@ -206,38 +269,45 @@ def _make_test_records(verbose=None):
["_Test Account Discount", "Direct Expenses", 0, None, None], ["_Test Account Discount", "Direct Expenses", 0, None, None],
["_Test Write Off", "Indirect Expenses", 0, None, None], ["_Test Write Off", "Indirect Expenses", 0, None, None],
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None], ["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
["_Test Account Sales", "Direct Income", 0, None, None], ["_Test Account Sales", "Direct Income", 0, None, None],
# related to Account Inventory Integration # related to Account Inventory Integration
["_Test Account Stock In Hand", "Current Assets", 0, None, None], ["_Test Account Stock In Hand", "Current Assets", 0, None, None],
# fixed asset depreciation # fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None], ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None], ["_Test Depreciations", "Expenses", 0, None, None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account # Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None], ["_Test Receivable", "Current Assets", 0, "Receivable", None],
["_Test Payable", "Current Liabilities", 0, "Payable", None], ["_Test Payable", "Current Liabilities", 0, "Payable", None],
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"], ["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"],
["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"] ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"],
] ]
for company, abbr in [["_Test Company", "_TC"], ["_Test Company 1", "_TC1"], ["_Test Company with perpetual inventory", "TCP1"]]: for company, abbr in [
test_objects = make_test_objects("Account", [{ ["_Test Company", "_TC"],
"doctype": "Account", ["_Test Company 1", "_TC1"],
"account_name": account_name, ["_Test Company with perpetual inventory", "TCP1"],
"parent_account": parent_account + " - " + abbr, ]:
"company": company, test_objects = make_test_objects(
"is_group": is_group, "Account",
"account_type": account_type, [
"account_currency": currency {
} for account_name, parent_account, is_group, account_type, currency in accounts]) "doctype": "Account",
"account_name": account_name,
"parent_account": parent_account + " - " + abbr,
"company": company,
"is_group": is_group,
"account_type": account_type,
"account_currency": currency,
}
for account_name, parent_account, is_group, account_type, currency in accounts
],
)
return test_objects return test_objects
def get_inventory_account(company, warehouse=None): def get_inventory_account(company, warehouse=None):
account = None account = None
if warehouse: if warehouse:
@ -247,19 +317,24 @@ def get_inventory_account(company, warehouse=None):
return account return account
def create_account(**kwargs): def create_account(**kwargs):
account = frappe.db.get_value("Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}) account = frappe.db.get_value(
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
)
if account: if account:
return account return account
else: else:
account = frappe.get_doc(dict( account = frappe.get_doc(
doctype = "Account", dict(
account_name = kwargs.get('account_name'), doctype="Account",
account_type = kwargs.get('account_type'), account_name=kwargs.get("account_name"),
parent_account = kwargs.get('parent_account'), account_type=kwargs.get("account_type"),
company = kwargs.get('company'), parent_account=kwargs.get("parent_account"),
account_currency = kwargs.get('account_currency') company=kwargs.get("company"),
)) account_currency=kwargs.get("account_currency"),
)
)
account.save() account.save()
return account.name return account.name

View File

@ -17,13 +17,21 @@ class AccountingDimension(Document):
self.set_fieldname_and_label() self.set_fieldname_and_label()
def validate(self): def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', if self.document_type in core_doctypes_list + (
'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') : "Accounting Dimension",
"Project",
"Cost Center",
"Accounting Dimension Detail",
"Company",
"Account",
):
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg) frappe.throw(msg)
exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name']) exists = frappe.db.get_value(
"Accounting Dimension", {"document_type": self.document_type}, ["name"]
)
if exists and self.is_new(): if exists and self.is_new():
frappe.throw(_("Document Type already used as a dimension")) frappe.throw(_("Document Type already used as a dimension"))
@ -42,13 +50,13 @@ class AccountingDimension(Document):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)
else: else:
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue='long') frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
def on_trash(self): def on_trash(self):
if frappe.flags.in_test: if frappe.flags.in_test:
delete_accounting_dimension(doc=self) delete_accounting_dimension(doc=self)
else: else:
frappe.enqueue(delete_accounting_dimension, doc=self, queue='long') frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
def set_fieldname_and_label(self): def set_fieldname_and_label(self):
if not self.label: if not self.label:
@ -60,6 +68,7 @@ class AccountingDimension(Document):
def on_update(self): def on_update(self):
frappe.flags.accounting_dimensions = None frappe.flags.accounting_dimensions = None
def make_dimension_in_accounting_doctypes(doc, doclist=None): def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist: if not doclist:
doclist = get_doctypes_with_dimensions() doclist = get_doctypes_with_dimensions()
@ -70,9 +79,9 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
for doctype in doclist: for doctype in doclist:
if (doc_count + 1) % 2 == 0: if (doc_count + 1) % 2 == 0:
insert_after_field = 'dimension_col_break' insert_after_field = "dimension_col_break"
else: else:
insert_after_field = 'accounting_dimensions_section' insert_after_field = "accounting_dimensions_section"
df = { df = {
"fieldname": doc.fieldname, "fieldname": doc.fieldname,
@ -80,13 +89,13 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
"fieldtype": "Link", "fieldtype": "Link",
"options": doc.document_type, "options": doc.document_type,
"insert_after": insert_after_field, "insert_after": insert_after_field,
"owner": "Administrator" "owner": "Administrator",
} }
meta = frappe.get_meta(doctype, cached=False) meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")] fieldnames = [d.fieldname for d in meta.get("fields")]
if df['fieldname'] not in fieldnames: if df["fieldname"] not in fieldnames:
if doctype == "Budget": if doctype == "Budget":
add_dimension_to_budget_doctype(df.copy(), doc) add_dimension_to_budget_doctype(df.copy(), doc)
else: else:
@ -94,14 +103,17 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
count += 1 count += 1
frappe.publish_progress(count*100/len(doclist), title = _("Creating Dimensions...")) frappe.publish_progress(count * 100 / len(doclist), title=_("Creating Dimensions..."))
frappe.clear_cache(doctype=doctype) frappe.clear_cache(doctype=doctype)
def add_dimension_to_budget_doctype(df, doc): def add_dimension_to_budget_doctype(df, doc):
df.update({ df.update(
"insert_after": "cost_center", {
"depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type) "insert_after": "cost_center",
}) "depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type),
}
)
create_custom_field("Budget", df) create_custom_field("Budget", df)
@ -112,36 +124,44 @@ def add_dimension_to_budget_doctype(df, doc):
property_setter_doc.value = property_setter_doc.value + "\n" + doc.document_type property_setter_doc.value = property_setter_doc.value + "\n" + doc.document_type
property_setter_doc.save() property_setter_doc.save()
frappe.clear_cache(doctype='Budget') frappe.clear_cache(doctype="Budget")
else: else:
frappe.get_doc({ frappe.get_doc(
"doctype": "Property Setter", {
"doctype_or_field": "DocField", "doctype": "Property Setter",
"doc_type": "Budget", "doctype_or_field": "DocField",
"field_name": "budget_against", "doc_type": "Budget",
"property": "options", "field_name": "budget_against",
"property_type": "Text", "property": "options",
"value": "\nCost Center\nProject\n" + doc.document_type "property_type": "Text",
}).insert(ignore_permissions=True) "value": "\nCost Center\nProject\n" + doc.document_type,
}
).insert(ignore_permissions=True)
def delete_accounting_dimension(doc): def delete_accounting_dimension(doc):
doclist = get_doctypes_with_dimensions() doclist = get_doctypes_with_dimensions()
frappe.db.sql(""" frappe.db.sql(
"""
DELETE FROM `tabCustom Field` DELETE FROM `tabCustom Field`
WHERE fieldname = %s WHERE fieldname = %s
AND dt IN (%s)""" % #nosec AND dt IN (%s)"""
('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist)) % ("%s", ", ".join(["%s"] * len(doclist))), # nosec
tuple([doc.fieldname] + doclist),
)
frappe.db.sql(""" frappe.db.sql(
"""
DELETE FROM `tabProperty Setter` DELETE FROM `tabProperty Setter`
WHERE field_name = %s WHERE field_name = %s
AND doc_type IN (%s)""" % #nosec AND doc_type IN (%s)"""
('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist)) % ("%s", ", ".join(["%s"] * len(doclist))), # nosec
tuple([doc.fieldname] + doclist),
)
budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options") budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options")
value_list = budget_against_property.value.split('\n')[3:] value_list = budget_against_property.value.split("\n")[3:]
if doc.document_type in value_list: if doc.document_type in value_list:
value_list.remove(doc.document_type) value_list.remove(doc.document_type)
@ -152,6 +172,7 @@ def delete_accounting_dimension(doc):
for doctype in doclist: for doctype in doclist:
frappe.clear_cache(doctype=doctype) frappe.clear_cache(doctype=doctype)
@frappe.whitelist() @frappe.whitelist()
def disable_dimension(doc): def disable_dimension(doc):
if frappe.flags.in_test: if frappe.flags.in_test:
@ -159,10 +180,11 @@ def disable_dimension(doc):
else: else:
frappe.enqueue(toggle_disabling, doc=doc) frappe.enqueue(toggle_disabling, doc=doc)
def toggle_disabling(doc): def toggle_disabling(doc):
doc = json.loads(doc) doc = json.loads(doc)
if doc.get('disabled'): if doc.get("disabled"):
df = {"read_only": 1} df = {"read_only": 1}
else: else:
df = {"read_only": 0} df = {"read_only": 0}
@ -170,7 +192,7 @@ def toggle_disabling(doc):
doclist = get_doctypes_with_dimensions() doclist = get_doctypes_with_dimensions()
for doctype in doclist: for doctype in doclist:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get('fieldname')}) field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get("fieldname")})
if field: if field:
custom_field = frappe.get_doc("Custom Field", field) custom_field = frappe.get_doc("Custom Field", field)
custom_field.update(df) custom_field.update(df)
@ -178,26 +200,34 @@ def toggle_disabling(doc):
frappe.clear_cache(doctype=doctype) frappe.clear_cache(doctype=doctype)
def get_doctypes_with_dimensions(): def get_doctypes_with_dimensions():
return frappe.get_hooks("accounting_dimension_doctypes") return frappe.get_hooks("accounting_dimension_doctypes")
def get_accounting_dimensions(as_list=True): def get_accounting_dimensions(as_list=True):
if frappe.flags.accounting_dimensions is None: if frappe.flags.accounting_dimensions is None:
frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension", frappe.flags.accounting_dimensions = frappe.get_all(
fields=["label", "fieldname", "disabled", "document_type"]) "Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"]
)
if as_list: if as_list:
return [d.fieldname for d in frappe.flags.accounting_dimensions] return [d.fieldname for d in frappe.flags.accounting_dimensions]
else: else:
return frappe.flags.accounting_dimensions return frappe.flags.accounting_dimensions
def get_checks_for_pl_and_bs_accounts(): def get_checks_for_pl_and_bs_accounts():
dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs dimensions = frappe.db.sql(
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
WHERE p.name = c.parent""", as_dict=1) WHERE p.name = c.parent""",
as_dict=1,
)
return dimensions return dimensions
def get_dimension_with_children(doctype, dimension): def get_dimension_with_children(doctype, dimension):
if isinstance(dimension, list): if isinstance(dimension, list):
@ -205,34 +235,39 @@ def get_dimension_with_children(doctype, dimension):
all_dimensions = [] all_dimensions = []
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"]) lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft") children = frappe.get_all(
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
)
all_dimensions += [c.name for c in children] all_dimensions += [c.name for c in children]
return all_dimensions return all_dimensions
@frappe.whitelist() @frappe.whitelist()
def get_dimensions(with_cost_center_and_project=False): def get_dimensions(with_cost_center_and_project=False):
dimension_filters = frappe.db.sql(""" dimension_filters = frappe.db.sql(
"""
SELECT label, fieldname, document_type SELECT label, fieldname, document_type
FROM `tabAccounting Dimension` FROM `tabAccounting Dimension`
WHERE disabled = 0 WHERE disabled = 0
""", as_dict=1) """,
as_dict=1,
)
default_dimensions = frappe.db.sql("""SELECT p.fieldname, c.company, c.default_dimension default_dimensions = frappe.db.sql(
"""SELECT p.fieldname, c.company, c.default_dimension
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
WHERE c.parent = p.name""", as_dict=1) WHERE c.parent = p.name""",
as_dict=1,
)
if with_cost_center_and_project: if with_cost_center_and_project:
dimension_filters.extend([ dimension_filters.extend(
{ [
'fieldname': 'cost_center', {"fieldname": "cost_center", "document_type": "Cost Center"},
'document_type': 'Cost Center' {"fieldname": "project", "document_type": "Project"},
}, ]
{ )
'fieldname': 'project',
'document_type': 'Project'
}
])
default_dimensions_map = {} default_dimensions_map = {}
for dimension in default_dimensions: for dimension in default_dimensions:

View File

@ -8,7 +8,8 @@ import frappe
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department'] test_dependencies = ["Cost Center", "Location", "Warehouse", "Department"]
class TestAccountingDimension(unittest.TestCase): class TestAccountingDimension(unittest.TestCase):
def setUp(self): def setUp(self):
@ -18,24 +19,27 @@ class TestAccountingDimension(unittest.TestCase):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
si.location = "Block 1" si.location = "Block 1"
si.append("items", { si.append(
"item_code": "_Test Item", "items",
"warehouse": "_Test Warehouse - _TC", {
"qty": 1, "item_code": "_Test Item",
"rate": 100, "warehouse": "_Test Warehouse - _TC",
"income_account": "Sales - _TC", "qty": 1,
"expense_account": "Cost of Goods Sold - _TC", "rate": 100,
"cost_center": "_Test Cost Center - _TC", "income_account": "Sales - _TC",
"department": "_Test Department - _TC", "expense_account": "Cost of Goods Sold - _TC",
"location": "Block 1" "cost_center": "_Test Cost Center - _TC",
}) "department": "_Test Department - _TC",
"location": "Block 1",
},
)
si.save() si.save()
si.submit() si.submit()
gle = frappe.get_doc("GL Entry", {"voucher_no": si.name, "account": "Sales - _TC"}) gle = frappe.get_doc("GL Entry", {"voucher_no": si.name, "account": "Sales - _TC"})
self.assertEqual(gle.get('department'), "_Test Department - _TC") self.assertEqual(gle.get("department"), "_Test Department - _TC")
def test_dimension_against_journal_entry(self): def test_dimension_against_journal_entry(self):
je = make_journal_entry("Sales - _TC", "Sales Expenses - _TC", 500, save=False) je = make_journal_entry("Sales - _TC", "Sales Expenses - _TC", 500, save=False)
@ -50,21 +54,24 @@ class TestAccountingDimension(unittest.TestCase):
gle = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales - _TC"}) gle = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales - _TC"})
gle1 = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales Expenses - _TC"}) gle1 = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales Expenses - _TC"})
self.assertEqual(gle.get('department'), "_Test Department - _TC") self.assertEqual(gle.get("department"), "_Test Department - _TC")
self.assertEqual(gle1.get('department'), "_Test Department - _TC") self.assertEqual(gle1.get("department"), "_Test Department - _TC")
def test_mandatory(self): def test_mandatory(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
si.append("items", { si.append(
"item_code": "_Test Item", "items",
"warehouse": "_Test Warehouse - _TC", {
"qty": 1, "item_code": "_Test Item",
"rate": 100, "warehouse": "_Test Warehouse - _TC",
"income_account": "Sales - _TC", "qty": 1,
"expense_account": "Cost of Goods Sold - _TC", "rate": 100,
"cost_center": "_Test Cost Center - _TC", "income_account": "Sales - _TC",
"location": "" "expense_account": "Cost of Goods Sold - _TC",
}) "cost_center": "_Test Cost Center - _TC",
"location": "",
},
)
si.save() si.save()
self.assertRaises(frappe.ValidationError, si.submit) self.assertRaises(frappe.ValidationError, si.submit)
@ -72,31 +79,39 @@ class TestAccountingDimension(unittest.TestCase):
def tearDown(self): def tearDown(self):
disable_dimension() disable_dimension()
def create_dimension(): def create_dimension():
frappe.set_user("Administrator") frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
frappe.get_doc({ frappe.get_doc(
"doctype": "Accounting Dimension", {
"document_type": "Department", "doctype": "Accounting Dimension",
}).insert() "document_type": "Department",
}
).insert()
else: else:
dimension = frappe.get_doc("Accounting Dimension", "Department") dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0 dimension.disabled = 0
dimension.save() dimension.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc({ dimension1 = frappe.get_doc(
"doctype": "Accounting Dimension", {
"document_type": "Location", "doctype": "Accounting Dimension",
}) "document_type": "Location",
}
)
dimension1.append("dimension_defaults", { dimension1.append(
"company": "_Test Company", "dimension_defaults",
"reference_document": "Location", {
"default_dimension": "Block 1", "company": "_Test Company",
"mandatory_for_bs": 1 "reference_document": "Location",
}) "default_dimension": "Block 1",
"mandatory_for_bs": 1,
},
)
dimension1.insert() dimension1.insert()
dimension1.save() dimension1.save()
@ -105,6 +120,7 @@ def create_dimension():
dimension1.disabled = 0 dimension1.disabled = 0
dimension1.save() dimension1.save()
def disable_dimension(): def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department") dimension1 = frappe.get_doc("Accounting Dimension", "Department")
dimension1.disabled = 1 dimension1.disabled = 1

View File

@ -19,17 +19,27 @@ class AccountingDimensionFilter(Document):
WHERE d.name = a.parent WHERE d.name = a.parent
and d.name != %s and d.name != %s
and d.accounting_dimension = %s and d.accounting_dimension = %s
""", (self.name, self.accounting_dimension), as_dict=1) """,
(self.name, self.accounting_dimension),
as_dict=1,
)
account_list = [d.account for d in accounts] account_list = [d.account for d in accounts]
for account in self.get('accounts'): for account in self.get("accounts"):
if account.applicable_on_account in account_list: if account.applicable_on_account in account_list:
frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format( frappe.throw(
account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension))) _("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
account.idx,
frappe.bold(account.applicable_on_account),
frappe.bold(self.accounting_dimension),
)
)
def get_dimension_filter_map(): def get_dimension_filter_map():
filters = frappe.db.sql(""" filters = frappe.db.sql(
"""
SELECT SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension, a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory p.allow_or_restrict, a.is_mandatory
@ -40,22 +50,30 @@ def get_dimension_filter_map():
p.name = a.parent p.name = a.parent
AND p.disabled = 0 AND p.disabled = 0
AND p.name = d.parent AND p.name = d.parent
""", as_dict=1) """,
as_dict=1,
)
dimension_filter_map = {} dimension_filter_map = {}
for f in filters: for f in filters:
f.fieldname = scrub(f.accounting_dimension) f.fieldname = scrub(f.accounting_dimension)
build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value, build_map(
f.allow_or_restrict, f.is_mandatory) dimension_filter_map,
f.fieldname,
f.applicable_on_account,
f.dimension_value,
f.allow_or_restrict,
f.is_mandatory,
)
return dimension_filter_map return dimension_filter_map
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory): def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
map_object.setdefault((dimension, account), { map_object.setdefault(
'allowed_dimensions': [], (dimension, account),
'is_mandatory': is_mandatory, {"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict},
'allow_or_restrict': allow_or_restrict )
}) map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)
map_object[(dimension, account)]['allowed_dimensions'].append(filter_value)

View File

@ -12,7 +12,8 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
test_dependencies = ['Location', 'Cost Center', 'Department'] test_dependencies = ["Location", "Cost Center", "Department"]
class TestAccountingDimensionFilter(unittest.TestCase): class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self): def setUp(self):
@ -22,9 +23,9 @@ class TestAccountingDimensionFilter(unittest.TestCase):
def test_allowed_dimension_validation(self): def test_allowed_dimension_validation(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
si.items[0].cost_center = 'Main - _TC' si.items[0].cost_center = "Main - _TC"
si.department = 'Accounts - _TC' si.department = "Accounts - _TC"
si.location = 'Block 1' si.location = "Block 1"
si.save() si.save()
self.assertRaises(InvalidAccountDimensionError, si.submit) self.assertRaises(InvalidAccountDimensionError, si.submit)
@ -32,12 +33,12 @@ class TestAccountingDimensionFilter(unittest.TestCase):
def test_mandatory_dimension_validation(self): def test_mandatory_dimension_validation(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
si.department = '' si.department = ""
si.location = 'Block 1' si.location = "Block 1"
# Test with no department for Sales Account # Test with no department for Sales Account
si.items[0].department = '' si.items[0].department = ""
si.items[0].cost_center = '_Test Cost Center 2 - _TC' si.items[0].cost_center = "_Test Cost Center 2 - _TC"
si.save() si.save()
self.assertRaises(MandatoryAccountDimensionError, si.submit) self.assertRaises(MandatoryAccountDimensionError, si.submit)
@ -52,53 +53,54 @@ class TestAccountingDimensionFilter(unittest.TestCase):
if si.docstatus == 1: if si.docstatus == 1:
si.cancel() si.cancel()
def create_accounting_dimension_filter(): def create_accounting_dimension_filter():
if not frappe.db.get_value('Accounting Dimension Filter', if not frappe.db.get_value(
{'accounting_dimension': 'Cost Center'}): "Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}
frappe.get_doc({ ):
'doctype': 'Accounting Dimension Filter', frappe.get_doc(
'accounting_dimension': 'Cost Center', {
'allow_or_restrict': 'Allow', "doctype": "Accounting Dimension Filter",
'company': '_Test Company', "accounting_dimension": "Cost Center",
'accounts': [{ "allow_or_restrict": "Allow",
'applicable_on_account': 'Sales - _TC', "company": "_Test Company",
}], "accounts": [
'dimensions': [{ {
'accounting_dimension': 'Cost Center', "applicable_on_account": "Sales - _TC",
'dimension_value': '_Test Cost Center 2 - _TC' }
}] ],
}).insert() "dimensions": [
{"accounting_dimension": "Cost Center", "dimension_value": "_Test Cost Center 2 - _TC"}
],
}
).insert()
else: else:
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"})
doc.disabled = 0 doc.disabled = 0
doc.save() doc.save()
if not frappe.db.get_value('Accounting Dimension Filter', if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Department"}):
{'accounting_dimension': 'Department'}): frappe.get_doc(
frappe.get_doc({ {
'doctype': 'Accounting Dimension Filter', "doctype": "Accounting Dimension Filter",
'accounting_dimension': 'Department', "accounting_dimension": "Department",
'allow_or_restrict': 'Allow', "allow_or_restrict": "Allow",
'company': '_Test Company', "company": "_Test Company",
'accounts': [{ "accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}],
'applicable_on_account': 'Sales - _TC', "dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}],
'is_mandatory': 1 }
}], ).insert()
'dimensions': [{
'accounting_dimension': 'Department',
'dimension_value': 'Accounts - _TC'
}]
}).insert()
else: else:
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"})
doc.disabled = 0 doc.disabled = 0
doc.save() doc.save()
def disable_dimension_filter(): def disable_dimension_filter():
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"})
doc.disabled = 1 doc.disabled = 1
doc.save() doc.save()
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"})
doc.disabled = 1 doc.disabled = 1
doc.save() doc.save()

View File

@ -7,7 +7,9 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError):
pass
class AccountingPeriod(Document): class AccountingPeriod(Document):
def validate(self): def validate(self):
@ -17,11 +19,12 @@ class AccountingPeriod(Document):
self.bootstrap_doctypes_for_closing() self.bootstrap_doctypes_for_closing()
def autoname(self): def autoname(self):
company_abbr = frappe.get_cached_value('Company', self.company, "abbr") company_abbr = frappe.get_cached_value("Company", self.company, "abbr")
self.name = " - ".join([self.period_name, company_abbr]) self.name = " - ".join([self.period_name, company_abbr])
def validate_overlap(self): def validate_overlap(self):
existing_accounting_period = frappe.db.sql("""select name from `tabAccounting Period` existing_accounting_period = frappe.db.sql(
"""select name from `tabAccounting Period`
where ( where (
(%(start_date)s between start_date and end_date) (%(start_date)s between start_date and end_date)
or (%(end_date)s between start_date and end_date) or (%(end_date)s between start_date and end_date)
@ -32,18 +35,29 @@ class AccountingPeriod(Document):
"start_date": self.start_date, "start_date": self.start_date,
"end_date": self.end_date, "end_date": self.end_date,
"name": self.name, "name": self.name,
"company": self.company "company": self.company,
}, as_dict=True) },
as_dict=True,
)
if len(existing_accounting_period) > 0: if len(existing_accounting_period) > 0:
frappe.throw(_("Accounting Period overlaps with {0}") frappe.throw(
.format(existing_accounting_period[0].get("name")), OverlapError) _("Accounting Period overlaps with {0}").format(existing_accounting_period[0].get("name")),
OverlapError,
)
@frappe.whitelist() @frappe.whitelist()
def get_doctypes_for_closing(self): def get_doctypes_for_closing(self):
docs_for_closing = [] docs_for_closing = []
doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \ doctypes = [
"Bank Clearance", "Asset", "Stock Entry"] "Sales Invoice",
"Purchase Invoice",
"Journal Entry",
"Payroll Entry",
"Bank Clearance",
"Asset",
"Stock Entry",
]
closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes] closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes]
for closed_doctype in closed_doctypes: for closed_doctype in closed_doctypes:
docs_for_closing.append(closed_doctype) docs_for_closing.append(closed_doctype)
@ -53,7 +67,7 @@ class AccountingPeriod(Document):
def bootstrap_doctypes_for_closing(self): def bootstrap_doctypes_for_closing(self):
if len(self.closed_documents) == 0: if len(self.closed_documents) == 0:
for doctype_for_closing in self.get_doctypes_for_closing(): for doctype_for_closing in self.get_doctypes_for_closing():
self.append('closed_documents', { self.append(
"document_type": doctype_for_closing.document_type, "closed_documents",
"closed": doctype_for_closing.closed {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
}) )

View File

@ -10,29 +10,38 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import ClosedAccountingPeriod from erpnext.accounts.general_ledger import ClosedAccountingPeriod
test_dependencies = ['Item'] test_dependencies = ["Item"]
class TestAccountingPeriod(unittest.TestCase): class TestAccountingPeriod(unittest.TestCase):
def test_overlap(self): def test_overlap(self):
ap1 = create_accounting_period(start_date = "2018-04-01", ap1 = create_accounting_period(
end_date = "2018-06-30", company = "Wind Power LLC") start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC"
)
ap1.save() ap1.save()
ap2 = create_accounting_period(start_date = "2018-06-30", ap2 = create_accounting_period(
end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1") start_date="2018-06-30",
end_date="2018-07-10",
company="Wind Power LLC",
period_name="Test Accounting Period 1",
)
self.assertRaises(OverlapError, ap2.save) self.assertRaises(OverlapError, ap2.save)
def test_accounting_period(self): def test_accounting_period(self):
ap1 = create_accounting_period(period_name = "Test Accounting Period 2") ap1 = create_accounting_period(period_name="Test Accounting Period 2")
ap1.save() ap1.save()
doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC") doc = create_sales_invoice(
do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
)
self.assertRaises(ClosedAccountingPeriod, doc.submit) self.assertRaises(ClosedAccountingPeriod, doc.submit)
def tearDown(self): def tearDown(self):
for d in frappe.get_all("Accounting Period"): for d in frappe.get_all("Accounting Period"):
frappe.delete_doc("Accounting Period", d.name) frappe.delete_doc("Accounting Period", d.name)
def create_accounting_period(**args): def create_accounting_period(**args):
args = frappe._dict(args) args = frappe._dict(args)
@ -41,8 +50,6 @@ def create_accounting_period(**args):
accounting_period.end_date = args.end_date or add_months(nowdate(), 1) accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company" accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1" accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", { accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
"document_type": 'Sales Invoice', "closed": 1
})
return accounting_period return accounting_period

View File

@ -18,11 +18,13 @@ class AccountsSettings(Document):
frappe.clear_cache() frappe.clear_cache()
def validate(self): def validate(self):
frappe.db.set_default("add_taxes_from_item_tax_template", frappe.db.set_default(
self.get("add_taxes_from_item_tax_template", 0)) "add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
)
frappe.db.set_default("enable_common_party_accounting", frappe.db.set_default(
self.get("enable_common_party_accounting", 0)) "enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
)
self.validate_stale_days() self.validate_stale_days()
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
@ -32,34 +34,91 @@ class AccountsSettings(Document):
def validate_stale_days(self): def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0: if not self.allow_stale and cint(self.stale_days) <= 0:
frappe.msgprint( frappe.msgprint(
_("Stale Days should start from 1."), title='Error', indicator='red', _("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1
raise_exception=1) )
def enable_payment_schedule_in_print(self): def enable_payment_schedule_in_print(self):
show_in_print = cint(self.show_payment_schedule_in_print) show_in_print = cint(self.show_payment_schedule_in_print)
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"): for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False) make_property_setter(
make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False) doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False
)
make_property_setter(
doctype,
"payment_schedule",
"print_hide",
0 if show_in_print else 1,
"Check",
validate_fields_for_doctype=False,
)
def toggle_discount_accounting_fields(self): def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting) enable_discount_accounting = cint(self.enable_discount_accounting)
for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) make_property_setter(
doctype,
"discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting: if enable_discount_accounting:
make_property_setter(doctype, "discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False) make_property_setter(
doctype,
"discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else: else:
make_property_setter(doctype, "discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) make_property_setter(
doctype,
"discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
for doctype in ["Sales Invoice", "Purchase Invoice"]: for doctype in ["Sales Invoice", "Purchase Invoice"]:
make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) make_property_setter(
doctype,
"additional_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting: if enable_discount_accounting:
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False) make_property_setter(
doctype,
"additional_discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else: else:
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) make_property_setter(
doctype,
make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) "additional_discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
make_property_setter(
"Item",
"default_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
def validate_pending_reposts(self): def validate_pending_reposts(self):
if self.acc_frozen_upto: if self.acc_frozen_upto:

View File

@ -7,12 +7,12 @@ class TestAccountsSettings(unittest.TestCase):
def tearDown(self): def tearDown(self):
# Just in case `save` method succeeds, we need to take things back to default so that other tests # Just in case `save` method succeeds, we need to take things back to default so that other tests
# don't break # don't break
cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
cur_settings.allow_stale = 1 cur_settings.allow_stale = 1
cur_settings.save() cur_settings.save()
def test_stale_days(self): def test_stale_days(self):
cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
cur_settings.allow_stale = 0 cur_settings.allow_stale = 0
cur_settings.stale_days = 0 cur_settings.stale_days = 0

View File

@ -15,4 +15,4 @@ class Bank(Document):
load_address_and_contact(self) load_address_and_contact(self)
def on_trash(self): def on_trash(self):
delete_contact_and_address('Bank', self.name) delete_contact_and_address("Bank", self.name)

View File

@ -3,11 +3,6 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'bank', "fieldname": "bank",
'transactions': [ "transactions": [{"label": _("Bank Details"), "items": ["Bank Account", "Bank Guarantee"]}],
{
'label': _('Bank Details'),
'items': ['Bank Account', 'Bank Guarantee']
}
]
} }

View File

@ -20,7 +20,7 @@ class BankAccount(Document):
self.name = self.account_name + " - " + self.bank self.name = self.account_name + " - " + self.bank
def on_trash(self): def on_trash(self):
delete_contact_and_address('BankAccount', self.name) delete_contact_and_address("BankAccount", self.name)
def validate(self): def validate(self):
self.validate_company() self.validate_company()
@ -31,9 +31,9 @@ class BankAccount(Document):
frappe.throw(_("Company is manadatory for company account")) frappe.throw(_("Company is manadatory for company account"))
def validate_iban(self): def validate_iban(self):
''' """
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
''' """
# IBAN field is optional # IBAN field is optional
if not self.iban: if not self.iban:
return return
@ -43,7 +43,7 @@ class BankAccount(Document):
return str(9 + ord(c) - 64) return str(9 + ord(c) - 64)
# remove whitespaces, upper case to get the right number from ord() # remove whitespaces, upper case to get the right number from ord()
iban = ''.join(self.iban.split(' ')).upper() iban = "".join(self.iban.split(" ")).upper()
# Move country code and checksum from the start to the end # Move country code and checksum from the start to the end
flipped = iban[4:] + iban[:4] flipped = iban[4:] + iban[:4]
@ -52,12 +52,12 @@ class BankAccount(Document):
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped] encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
try: try:
to_check = int(''.join(encoded)) to_check = int("".join(encoded))
except ValueError: except ValueError:
frappe.throw(_('IBAN is not valid')) frappe.throw(_("IBAN is not valid"))
if to_check % 97 != 1: if to_check % 97 != 1:
frappe.throw(_('IBAN is not valid')) frappe.throw(_("IBAN is not valid"))
@frappe.whitelist() @frappe.whitelist()
@ -69,12 +69,14 @@ def make_bank_account(doctype, docname):
return doc return doc
@frappe.whitelist() @frappe.whitelist()
def get_party_bank_account(party_type, party): def get_party_bank_account(party_type, party):
return frappe.db.get_value(party_type, return frappe.db.get_value(party_type, party, "default_bank_account")
party, 'default_bank_account')
@frappe.whitelist() @frappe.whitelist()
def get_bank_account_details(bank_account): def get_bank_account_details(bank_account):
return frappe.db.get_value("Bank Account", return frappe.db.get_value(
bank_account, ['account', 'bank', 'bank_account_no'], as_dict=1) "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
)

View File

@ -3,25 +3,18 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'bank_account', "fieldname": "bank_account",
'non_standard_fieldnames': { "non_standard_fieldnames": {
'Customer': 'default_bank_account', "Customer": "default_bank_account",
'Supplier': 'default_bank_account', "Supplier": "default_bank_account",
}, },
'transactions': [ "transactions": [
{ {
'label': _('Payments'), "label": _("Payments"),
'items': ['Payment Entry', 'Payment Request', 'Payment Order', 'Payroll Entry'] "items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"],
}, },
{ {"label": _("Party"), "items": ["Customer", "Supplier"]},
'label': _('Party'), {"items": ["Bank Guarantee"]},
'items': ['Customer', 'Supplier'] {"items": ["Journal Entry"]},
}, ],
{
'items': ['Bank Guarantee']
},
{
'items': ['Journal Entry']
}
]
} }

View File

@ -8,28 +8,28 @@ from frappe import ValidationError
# test_records = frappe.get_test_records('Bank Account') # test_records = frappe.get_test_records('Bank Account')
class TestBankAccount(unittest.TestCase):
class TestBankAccount(unittest.TestCase):
def test_validate_iban(self): def test_validate_iban(self):
valid_ibans = [ valid_ibans = [
'GB82 WEST 1234 5698 7654 32', "GB82 WEST 1234 5698 7654 32",
'DE91 1000 0000 0123 4567 89', "DE91 1000 0000 0123 4567 89",
'FR76 3000 6000 0112 3456 7890 189' "FR76 3000 6000 0112 3456 7890 189",
] ]
invalid_ibans = [ invalid_ibans = [
# wrong checksum (3rd place) # wrong checksum (3rd place)
'GB72 WEST 1234 5698 7654 32', "GB72 WEST 1234 5698 7654 32",
'DE81 1000 0000 0123 4567 89', "DE81 1000 0000 0123 4567 89",
'FR66 3000 6000 0112 3456 7890 189' "FR66 3000 6000 0112 3456 7890 189",
] ]
bank_account = frappe.get_doc({'doctype':'Bank Account'}) bank_account = frappe.get_doc({"doctype": "Bank Account"})
try: try:
bank_account.validate_iban() bank_account.validate_iban()
except AttributeError: except AttributeError:
msg = 'BankAccount.validate_iban() failed for empty IBAN' msg = "BankAccount.validate_iban() failed for empty IBAN"
self.fail(msg=msg) self.fail(msg=msg)
for iban in valid_ibans: for iban in valid_ibans:
@ -37,11 +37,11 @@ class TestBankAccount(unittest.TestCase):
try: try:
bank_account.validate_iban() bank_account.validate_iban()
except ValidationError: except ValidationError:
msg = 'BankAccount.validate_iban() failed for valid IBAN {}'.format(iban) msg = "BankAccount.validate_iban() failed for valid IBAN {}".format(iban)
self.fail(msg=msg) self.fail(msg=msg)
for not_iban in invalid_ibans: for not_iban in invalid_ibans:
bank_account.iban = not_iban bank_account.iban = not_iban
msg = 'BankAccount.validate_iban() accepted invalid IBAN {}'.format(not_iban) msg = "BankAccount.validate_iban() accepted invalid IBAN {}".format(not_iban)
with self.assertRaises(ValidationError, msg=msg): with self.assertRaises(ValidationError, msg=msg):
bank_account.validate_iban() bank_account.validate_iban()

View File

@ -7,9 +7,8 @@ from frappe import _, msgprint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, fmt_money, getdate, nowdate from frappe.utils import flt, fmt_money, getdate, nowdate
form_grid_templates = { form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"
}
class BankClearance(Document): class BankClearance(Document):
@frappe.whitelist() @frappe.whitelist()
@ -24,7 +23,8 @@ class BankClearance(Document):
if not self.include_reconciled_entries: if not self.include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')" condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
journal_entries = frappe.db.sql(""" journal_entries = frappe.db.sql(
"""
select select
"Journal Entry" as payment_document, t1.name as payment_entry, "Journal Entry" as payment_document, t1.name as payment_entry,
t1.cheque_no as cheque_number, t1.cheque_date, t1.cheque_no as cheque_number, t1.cheque_date,
@ -38,12 +38,18 @@ class BankClearance(Document):
and ifnull(t1.is_opening, 'No') = 'No' {condition} and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC order by t1.posting_date ASC, t1.name DESC
""".format(condition=condition), {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1) """.format(
condition=condition
),
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
)
if self.bank_account: if self.bank_account:
condition += 'and bank_account = %(bank_account)s' condition += "and bank_account = %(bank_account)s"
payment_entries = frappe.db.sql(""" payment_entries = frappe.db.sql(
"""
select select
"Payment Entry" as payment_document, name as payment_entry, "Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date, reference_no as cheque_number, reference_date as cheque_date,
@ -58,12 +64,22 @@ class BankClearance(Document):
{condition} {condition}
order by order by
posting_date ASC, name DESC posting_date ASC, name DESC
""".format(condition=condition), {"account": self.account, "from":self.from_date, """.format(
"to": self.to_date, "bank_account": self.bank_account}, as_dict=1) condition=condition
),
{
"account": self.account,
"from": self.from_date,
"to": self.to_date,
"bank_account": self.bank_account,
},
as_dict=1,
)
pos_sales_invoices, pos_purchase_invoices = [], [] pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions: if self.include_pos_transactions:
pos_sales_invoices = frappe.db.sql(""" pos_sales_invoices = frappe.db.sql(
"""
select select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.customer as against_account, sip.clearance_date, si.posting_date, si.customer as against_account, sip.clearance_date,
@ -74,9 +90,13 @@ class BankClearance(Document):
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by order by
si.posting_date ASC, si.name DESC si.posting_date ASC, si.name DESC
""", {"account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1) """,
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
)
pos_purchase_invoices = frappe.db.sql(""" pos_purchase_invoices = frappe.db.sql(
"""
select select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit, "Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
pi.posting_date, pi.supplier as against_account, pi.clearance_date, pi.posting_date, pi.supplier as against_account, pi.clearance_date,
@ -87,18 +107,24 @@ class BankClearance(Document):
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by order by
pi.posting_date ASC, pi.name DESC pi.posting_date ASC, pi.name DESC
""", {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1) """,
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
)
entries = sorted(list(payment_entries) + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)), entries = sorted(
key=lambda k: k['posting_date'] or getdate(nowdate())) list(payment_entries)
+ list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)),
key=lambda k: k["posting_date"] or getdate(nowdate()),
)
self.set('payment_entries', []) self.set("payment_entries", [])
self.total_amount = 0.0 self.total_amount = 0.0
for d in entries: for d in entries:
row = self.append('payment_entries', {}) row = self.append("payment_entries", {})
amount = flt(d.get('debit', 0)) - flt(d.get('credit', 0)) amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
formatted_amount = fmt_money(abs(amount), 2, d.account_currency) formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
@ -112,21 +138,24 @@ class BankClearance(Document):
@frappe.whitelist() @frappe.whitelist()
def update_clearance_date(self): def update_clearance_date(self):
clearance_date_updated = False clearance_date_updated = False
for d in self.get('payment_entries'): for d in self.get("payment_entries"):
if d.clearance_date: if d.clearance_date:
if not d.payment_document: if not d.payment_document:
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction")) frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date): if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
frappe.throw(_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}") frappe.throw(
.format(d.idx, d.clearance_date, d.cheque_date)) _("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
d.idx, d.clearance_date, d.cheque_date
)
)
if d.clearance_date or self.include_reconciled_entries: if d.clearance_date or self.include_reconciled_entries:
if not d.clearance_date: if not d.clearance_date:
d.clearance_date = None d.clearance_date = None
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry) payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
payment_entry.db_set('clearance_date', d.clearance_date) payment_entry.db_set("clearance_date", d.clearance_date)
clearance_date_updated = True clearance_date_updated = True

View File

@ -23,10 +23,16 @@ class BankGuarantee(Document):
if not self.bank: if not self.bank:
frappe.throw(_("Enter the name of the bank or lending institution before submittting.")) frappe.throw(_("Enter the name of the bank or lending institution before submittting."))
@frappe.whitelist() @frappe.whitelist()
def get_vouchar_detials(column_list, doctype, docname): def get_vouchar_detials(column_list, doctype, docname):
column_list = json.loads(column_list) column_list = json.loads(column_list)
for col in column_list: for col in column_list:
sanitize_searchfield(col) sanitize_searchfield(col)
return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s''' return frappe.db.sql(
.format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0] """ select {columns} from `tab{doctype}` where name=%s""".format(
columns=", ".join(column_list), doctype=doctype
),
docname,
as_dict=1,
)[0]

View File

@ -22,48 +22,63 @@ from erpnext.accounts.utils import get_balance_on
class BankReconciliationTool(Document): class BankReconciliationTool(Document):
pass pass
@frappe.whitelist() @frappe.whitelist()
def get_bank_transactions(bank_account, from_date = None, to_date = None): def get_bank_transactions(bank_account, from_date=None, to_date=None):
# returns bank transactions for a bank account # returns bank transactions for a bank account
filters = [] filters = []
filters.append(['bank_account', '=', bank_account]) filters.append(["bank_account", "=", bank_account])
filters.append(['docstatus', '=', 1]) filters.append(["docstatus", "=", 1])
filters.append(['unallocated_amount', '>', 0]) filters.append(["unallocated_amount", ">", 0])
if to_date: if to_date:
filters.append(['date', '<=', to_date]) filters.append(["date", "<=", to_date])
if from_date: if from_date:
filters.append(['date', '>=', from_date]) filters.append(["date", ">=", from_date])
transactions = frappe.get_all( transactions = frappe.get_all(
'Bank Transaction', "Bank Transaction",
fields = ['date', 'deposit', 'withdrawal', 'currency', fields=[
'description', 'name', 'bank_account', 'company', "date",
'unallocated_amount', 'reference_number', 'party_type', 'party'], "deposit",
filters = filters "withdrawal",
"currency",
"description",
"name",
"bank_account",
"company",
"unallocated_amount",
"reference_number",
"party_type",
"party",
],
filters=filters,
) )
return transactions return transactions
@frappe.whitelist() @frappe.whitelist()
def get_account_balance(bank_account, till_date): def get_account_balance(bank_account, till_date):
# returns account balance till the specified date # returns account balance till the specified date
account = frappe.db.get_value('Bank Account', bank_account, 'account') account = frappe.db.get_value("Bank Account", bank_account, "account")
filters = frappe._dict({ filters = frappe._dict(
"account": account, {"account": account, "report_date": till_date, "include_pos_transactions": 1}
"report_date": till_date, )
"include_pos_transactions": 1
})
data = get_entries(filters) data = get_entries(filters)
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0,0 total_debit, total_credit = 0, 0
for d in data: for d in data:
total_debit += flt(d.debit) total_debit += flt(d.debit)
total_credit += flt(d.credit) total_credit += flt(d.credit)
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters) amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \ bank_bal = (
flt(balance_as_per_system)
- flt(total_debit)
+ flt(total_credit)
+ amounts_not_reflected_in_system + amounts_not_reflected_in_system
)
return bank_bal return bank_bal
@ -76,71 +91,94 @@ def update_bank_transaction(bank_transaction_name, reference_number, party_type=
bank_transaction.party_type = party_type bank_transaction.party_type = party_type
bank_transaction.party = party bank_transaction.party = party
bank_transaction.save() bank_transaction.save()
return frappe.db.get_all('Bank Transaction', return frappe.db.get_all(
filters={ "Bank Transaction",
'name': bank_transaction_name filters={"name": bank_transaction_name},
}, fields=[
fields=['date', 'deposit', 'withdrawal', 'currency', "date",
'description', 'name', 'bank_account', 'company', "deposit",
'unallocated_amount', 'reference_number', "withdrawal",
'party_type', 'party'], "currency",
"description",
"name",
"bank_account",
"company",
"unallocated_amount",
"reference_number",
"party_type",
"party",
],
)[0] )[0]
@frappe.whitelist() @frappe.whitelist()
def create_journal_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, posting_date=None, entry_type=None, def create_journal_entry_bts(
second_account=None, mode_of_payment=None, party_type=None, party=None, allow_edit=None): bank_transaction_name,
reference_number=None,
reference_date=None,
posting_date=None,
entry_type=None,
second_account=None,
mode_of_payment=None,
party_type=None,
party=None,
allow_edit=None,
):
# Create a new journal entry based on the bank transaction # Create a new journal entry based on the bank transaction
bank_transaction = frappe.db.get_values( bank_transaction = frappe.db.get_values(
"Bank Transaction", bank_transaction_name, "Bank Transaction",
fieldname=["name", "deposit", "withdrawal", "bank_account"] , bank_transaction_name,
as_dict=True fieldname=["name", "deposit", "withdrawal", "bank_account"],
as_dict=True,
)[0] )[0]
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
account_type = frappe.db.get_value("Account", second_account, "account_type") account_type = frappe.db.get_value("Account", second_account, "account_type")
if account_type in ["Receivable", "Payable"]: if account_type in ["Receivable", "Payable"]:
if not (party_type and party): if not (party_type and party):
frappe.throw(_("Party Type and Party is required for Receivable / Payable account {0}").format( second_account)) frappe.throw(
_("Party Type and Party is required for Receivable / Payable account {0}").format(
second_account
)
)
accounts = [] accounts = []
# Multi Currency? # Multi Currency?
accounts.append({ accounts.append(
{
"account": second_account, "account": second_account,
"credit_in_account_currency": bank_transaction.deposit "credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
if bank_transaction.deposit > 0 "debit_in_account_currency": bank_transaction.withdrawal
else 0, if bank_transaction.withdrawal > 0
"debit_in_account_currency":bank_transaction.withdrawal else 0,
if bank_transaction.withdrawal > 0 "party_type": party_type,
else 0, "party": party,
"party_type":party_type, }
"party":party, )
})
accounts.append({ accounts.append(
{
"account": company_account, "account": company_account,
"bank_account": bank_transaction.bank_account, "bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal "credit_in_account_currency": bank_transaction.withdrawal
if bank_transaction.withdrawal > 0 if bank_transaction.withdrawal > 0
else 0, else 0,
"debit_in_account_currency":bank_transaction.deposit "debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
if bank_transaction.deposit > 0 }
else 0, )
})
company = frappe.get_value("Account", company_account, "company") company = frappe.get_value("Account", company_account, "company")
journal_entry_dict = { journal_entry_dict = {
"voucher_type" : entry_type, "voucher_type": entry_type,
"company" : company, "company": company,
"posting_date" : posting_date, "posting_date": posting_date,
"cheque_date" : reference_date, "cheque_date": reference_date,
"cheque_no" : reference_number, "cheque_no": reference_number,
"mode_of_payment" : mode_of_payment "mode_of_payment": mode_of_payment,
} }
journal_entry = frappe.new_doc('Journal Entry') journal_entry = frappe.new_doc("Journal Entry")
journal_entry.update(journal_entry_dict) journal_entry.update(journal_entry_dict)
journal_entry.set("accounts", accounts) journal_entry.set("accounts", accounts)
if allow_edit: if allow_edit:
return journal_entry return journal_entry
@ -152,21 +190,32 @@ def create_journal_entry_bts( bank_transaction_name, reference_number=None, refe
else: else:
paid_amount = bank_transaction.withdrawal paid_amount = bank_transaction.withdrawal
vouchers = json.dumps([{ vouchers = json.dumps(
"payment_doctype":"Journal Entry", [{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
"payment_name":journal_entry.name, )
"amount":paid_amount}])
return reconcile_vouchers(bank_transaction.name, vouchers) return reconcile_vouchers(bank_transaction.name, vouchers)
@frappe.whitelist() @frappe.whitelist()
def create_payment_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, party_type=None, party=None, posting_date=None, def create_payment_entry_bts(
mode_of_payment=None, project=None, cost_center=None, allow_edit=None): bank_transaction_name,
reference_number=None,
reference_date=None,
party_type=None,
party=None,
posting_date=None,
mode_of_payment=None,
project=None,
cost_center=None,
allow_edit=None,
):
# Create a new payment entry based on the bank transaction # Create a new payment entry based on the bank transaction
bank_transaction = frappe.db.get_values( bank_transaction = frappe.db.get_values(
"Bank Transaction", bank_transaction_name, "Bank Transaction",
fieldname=["name", "unallocated_amount", "deposit", "bank_account"] , bank_transaction_name,
as_dict=True fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
as_dict=True,
)[0] )[0]
paid_amount = bank_transaction.unallocated_amount paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay" payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
@ -174,27 +223,26 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_value("Account", company_account, "company") company = frappe.get_value("Account", company_account, "company")
payment_entry_dict = { payment_entry_dict = {
"company" : company, "company": company,
"payment_type" : payment_type, "payment_type": payment_type,
"reference_no" : reference_number, "reference_no": reference_number,
"reference_date" : reference_date, "reference_date": reference_date,
"party_type" : party_type, "party_type": party_type,
"party" : party, "party": party,
"posting_date" : posting_date, "posting_date": posting_date,
"paid_amount": paid_amount, "paid_amount": paid_amount,
"received_amount": paid_amount "received_amount": paid_amount,
} }
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")
payment_entry.update(payment_entry_dict) payment_entry.update(payment_entry_dict)
if mode_of_payment: if mode_of_payment:
payment_entry.mode_of_payment = mode_of_payment payment_entry.mode_of_payment = mode_of_payment
if project: if project:
payment_entry.project = project payment_entry.project = project
if cost_center: if cost_center:
payment_entry.cost_center = cost_center payment_entry.cost_center = cost_center
if payment_type == "Receive": if payment_type == "Receive":
payment_entry.paid_to = company_account payment_entry.paid_to = company_account
else: else:
@ -208,84 +256,111 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe
payment_entry.insert() payment_entry.insert()
payment_entry.submit() payment_entry.submit()
vouchers = json.dumps([{ vouchers = json.dumps(
"payment_doctype":"Payment Entry", [{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
"payment_name":payment_entry.name, )
"amount":paid_amount}])
return reconcile_vouchers(bank_transaction.name, vouchers) return reconcile_vouchers(bank_transaction.name, vouchers)
@frappe.whitelist() @frappe.whitelist()
def reconcile_vouchers(bank_transaction_name, vouchers): def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction # updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers) vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account') company_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
if transaction.unallocated_amount == 0: if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled")) frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0 total_amount = 0
for voucher in vouchers: for voucher in vouchers:
voucher['payment_entry'] = frappe.get_doc(voucher['payment_doctype'], voucher['payment_name']) voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
total_amount += get_paid_amount(frappe._dict({ total_amount += get_paid_amount(
'payment_document': voucher['payment_doctype'], frappe._dict(
'payment_entry': voucher['payment_name'], {
}), transaction.currency, company_account) "payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
}
),
transaction.currency,
company_account,
)
if total_amount > transaction.unallocated_amount: if total_amount > transaction.unallocated_amount:
frappe.throw(_("The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction")) frappe.throw(
_(
"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
)
)
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers: for voucher in vouchers:
gl_entry = frappe.db.get_value("GL Entry", dict(account=account, voucher_type=voucher['payment_doctype'], voucher_no=voucher['payment_name']), ['credit', 'debit'], as_dict=1) gl_entry = frappe.db.get_value(
gl_amount, transaction_amount = (gl_entry.credit, transaction.deposit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.withdrawal) "GL Entry",
dict(
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
),
["credit", "debit"],
as_dict=1,
)
gl_amount, transaction_amount = (
(gl_entry.credit, transaction.deposit)
if gl_entry.credit > 0
else (gl_entry.debit, transaction.withdrawal)
)
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
transaction.append("payment_entries", { transaction.append(
"payment_document": voucher['payment_entry'].doctype, "payment_entries",
"payment_entry": voucher['payment_entry'].name, {
"allocated_amount": allocated_amount "payment_document": voucher["payment_entry"].doctype,
}) "payment_entry": voucher["payment_entry"].name,
"allocated_amount": allocated_amount,
},
)
transaction.save() transaction.save()
transaction.update_allocations() transaction.update_allocations()
return frappe.get_doc("Bank Transaction", bank_transaction_name) return frappe.get_doc("Bank Transaction", bank_transaction_name)
@frappe.whitelist() @frappe.whitelist()
def get_linked_payments(bank_transaction_name, document_types = None): def get_linked_payments(bank_transaction_name, document_types=None):
# get all matching payments for a bank transaction # get all matching payments for a bank transaction
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values( bank_account = frappe.db.get_values(
"Bank Account", "Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
transaction.bank_account, )[0]
["account", "company"],
as_dict=True)[0]
(account, company) = (bank_account.account, bank_account.company) (account, company) = (bank_account.account, bank_account.company)
matching = check_matching(account, company, transaction, document_types) matching = check_matching(account, company, transaction, document_types)
return matching return matching
def check_matching(bank_account, company, transaction, document_types): def check_matching(bank_account, company, transaction, document_types):
# combine all types of vouchers # combine all types of vouchers
subquery = get_queries(bank_account, company, transaction, document_types) subquery = get_queries(bank_account, company, transaction, document_types)
filters = { filters = {
"amount": transaction.unallocated_amount, "amount": transaction.unallocated_amount,
"payment_type" : "Receive" if transaction.deposit > 0 else "Pay", "payment_type": "Receive" if transaction.deposit > 0 else "Pay",
"reference_no": transaction.reference_number, "reference_no": transaction.reference_number,
"party_type": transaction.party_type, "party_type": transaction.party_type,
"party": transaction.party, "party": transaction.party,
"bank_account": bank_account "bank_account": bank_account,
} }
matching_vouchers = [] matching_vouchers = []
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
document_types, filters))
for query in subquery: for query in subquery:
matching_vouchers.extend( matching_vouchers.extend(
frappe.db.sql(query, filters,) frappe.db.sql(
query,
filters,
)
) )
return sorted(matching_vouchers, key = lambda x: x[0], reverse=True) if matching_vouchers else [] return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
def get_queries(bank_account, company, transaction, document_types): def get_queries(bank_account, company, transaction, document_types):
# get queries to get matching vouchers # get queries to get matching vouchers
@ -302,7 +377,7 @@ def get_queries(bank_account, company, transaction, document_types):
queries.extend([je_amount_matching]) queries.extend([je_amount_matching])
if transaction.deposit > 0 and "sales_invoice" in document_types: if transaction.deposit > 0 and "sales_invoice" in document_types:
si_amount_matching = get_si_matching_query(amount_condition) si_amount_matching = get_si_matching_query(amount_condition)
queries.extend([si_amount_matching]) queries.extend([si_amount_matching])
if transaction.withdrawal > 0: if transaction.withdrawal > 0:
@ -316,6 +391,7 @@ def get_queries(bank_account, company, transaction, document_types):
return queries return queries
def get_loan_vouchers(bank_account, transaction, document_types, filters): def get_loan_vouchers(bank_account, transaction, document_types, filters):
vouchers = [] vouchers = []
amount_condition = True if "exact_match" in document_types else False amount_condition = True if "exact_match" in document_types else False
@ -328,109 +404,90 @@ def get_loan_vouchers(bank_account, transaction, document_types, filters):
return vouchers return vouchers
def get_ld_matching_query(bank_account, amount_condition, filters): def get_ld_matching_query(bank_account, amount_condition, filters):
loan_disbursement = frappe.qb.DocType("Loan Disbursement") loan_disbursement = frappe.qb.DocType("Loan Disbursement")
matching_reference = loan_disbursement.reference_number == filters.get("reference_number") matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
matching_party = loan_disbursement.applicant_type == filters.get("party_type") and \ matching_party = loan_disbursement.applicant_type == filters.get(
loan_disbursement.applicant == filters.get("party") "party_type"
) and loan_disbursement.applicant == filters.get("party")
rank = ( rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
frappe.qb.terms.Case()
.when(matching_reference, 1) rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
.else_(0)
query = (
frappe.qb.from_(loan_disbursement)
.select(
rank + rank1 + 1,
ConstantColumn("Loan Disbursement").as_("doctype"),
loan_disbursement.name,
loan_disbursement.disbursed_amount,
loan_disbursement.reference_number,
loan_disbursement.reference_date,
loan_disbursement.applicant_type,
loan_disbursement.disbursement_date,
) )
.where(loan_disbursement.docstatus == 1)
rank1 = ( .where(loan_disbursement.clearance_date.isnull())
frappe.qb.terms.Case() .where(loan_disbursement.disbursement_account == bank_account)
.when(matching_party, 1)
.else_(0)
)
query = frappe.qb.from_(loan_disbursement).select(
rank + rank1 + 1,
ConstantColumn("Loan Disbursement").as_("doctype"),
loan_disbursement.name,
loan_disbursement.disbursed_amount,
loan_disbursement.reference_number,
loan_disbursement.reference_date,
loan_disbursement.applicant_type,
loan_disbursement.disbursement_date
).where(
loan_disbursement.docstatus == 1
).where(
loan_disbursement.clearance_date.isnull()
).where(
loan_disbursement.disbursement_account == bank_account
) )
if amount_condition: if amount_condition:
query.where( query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
loan_disbursement.disbursed_amount == filters.get('amount')
)
else: else:
query.where( query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
loan_disbursement.disbursed_amount <= filters.get('amount')
)
vouchers = query.run(as_list=True) vouchers = query.run(as_list=True)
return vouchers return vouchers
def get_lr_matching_query(bank_account, amount_condition, filters): def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment = frappe.qb.DocType("Loan Repayment") loan_repayment = frappe.qb.DocType("Loan Repayment")
matching_reference = loan_repayment.reference_number == filters.get("reference_number") matching_reference = loan_repayment.reference_number == filters.get("reference_number")
matching_party = loan_repayment.applicant_type == filters.get("party_type") and \ matching_party = loan_repayment.applicant_type == filters.get(
loan_repayment.applicant == filters.get("party") "party_type"
) and loan_repayment.applicant == filters.get("party")
rank = ( rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
frappe.qb.terms.Case()
.when(matching_reference, 1) rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
.else_(0)
query = (
frappe.qb.from_(loan_repayment)
.select(
rank + rank1 + 1,
ConstantColumn("Loan Repayment").as_("doctype"),
loan_repayment.name,
loan_repayment.amount_paid,
loan_repayment.reference_number,
loan_repayment.reference_date,
loan_repayment.applicant_type,
loan_repayment.posting_date,
) )
.where(loan_repayment.docstatus == 1)
rank1 = ( .where(loan_repayment.clearance_date.isnull())
frappe.qb.terms.Case() .where(loan_repayment.payment_account == bank_account)
.when(matching_party, 1)
.else_(0)
)
query = frappe.qb.from_(loan_repayment).select(
rank + rank1 + 1,
ConstantColumn("Loan Repayment").as_("doctype"),
loan_repayment.name,
loan_repayment.amount_paid,
loan_repayment.reference_number,
loan_repayment.reference_date,
loan_repayment.applicant_type,
loan_repayment.posting_date
).where(
loan_repayment.docstatus == 1
).where(
loan_repayment.clearance_date.isnull()
).where(
loan_repayment.payment_account == bank_account
) )
if amount_condition: if amount_condition:
query.where( query.where(loan_repayment.amount_paid == filters.get("amount"))
loan_repayment.amount_paid == filters.get('amount')
)
else: else:
query.where( query.where(loan_repayment.amount_paid <= filters.get("amount"))
loan_repayment.amount_paid <= filters.get('amount')
)
vouchers = query.run() vouchers = query.run()
return vouchers return vouchers
def get_pe_matching_query(amount_condition, account_from_to, transaction): def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query # get matching payment entries query
if transaction.deposit > 0: if transaction.deposit > 0:
currency_field = "paid_to_account_currency as currency" currency_field = "paid_to_account_currency as currency"
else: else:
currency_field = "paid_from_account_currency as currency" currency_field = "paid_from_account_currency as currency"
return f""" return f"""
SELECT SELECT
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
@ -519,6 +576,7 @@ def get_si_matching_query(amount_condition):
AND si.docstatus = 1 AND si.docstatus = 1
""" """
def get_pi_matching_query(amount_condition): def get_pi_matching_query(amount_condition):
# get matching purchase invoice query # get matching purchase invoice query
return f""" return f"""
@ -544,11 +602,16 @@ def get_pi_matching_query(amount_condition):
AND cash_bank_account = %(bank_account)s AND cash_bank_account = %(bank_account)s
""" """
def get_ec_matching_query(bank_account, company, amount_condition): def get_ec_matching_query(bank_account, company, amount_condition):
# get matching Expense Claim query # get matching Expense Claim query
mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account", mode_of_payments = [
filters={"default_account": bank_account}, fields=["parent"])] x["parent"]
mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )' for x in frappe.db.get_all(
"Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"]
)
]
mode_of_payments = "('" + "', '".join(mode_of_payments) + "' )"
company_currency = get_company_currency(company) company_currency = get_company_currency(company)
return f""" return f"""
SELECT SELECT

View File

@ -18,6 +18,7 @@ from openpyxl.utils import get_column_letter
INVALID_VALUES = ("", None) INVALID_VALUES = ("", None)
class BankStatementImport(DataImport): class BankStatementImport(DataImport):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BankStatementImport, self).__init__(*args, **kwargs) super(BankStatementImport, self).__init__(*args, **kwargs)
@ -49,16 +50,14 @@ class BankStatementImport(DataImport):
self.import_file, self.google_sheets_url self.import_file, self.google_sheets_url
) )
if 'Bank Account' not in json.dumps(preview['columns']): if "Bank Account" not in json.dumps(preview["columns"]):
frappe.throw(_("Please add the Bank Account column")) frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw( frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")
)
enqueued_jobs = [d.get("job_name") for d in get_info()] enqueued_jobs = [d.get("job_name") for d in get_info()]
@ -81,21 +80,25 @@ class BankStatementImport(DataImport):
return False return False
@frappe.whitelist() @frappe.whitelist()
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None): def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template( return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
import_file, google_sheets_url import_file, google_sheets_url
) )
@frappe.whitelist() @frappe.whitelist()
def form_start_import(data_import): def form_start_import(data_import):
return frappe.get_doc("Bank Statement Import", data_import).start_import() return frappe.get_doc("Bank Statement Import", data_import).start_import()
@frappe.whitelist() @frappe.whitelist()
def download_errored_template(data_import_name): def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name) data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows() data_import.export_errored_rows()
def parse_data_from_template(raw_data): def parse_data_from_template(raw_data):
data = [] data = []
@ -108,7 +111,10 @@ def parse_data_from_template(raw_data):
return data return data
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
def start_import(
data_import, bank_account, import_file_path, google_sheets_url, bank, template_options
):
"""This method runs in background job""" """This method runs in background job"""
update_mapping_db(bank, template_options) update_mapping_db(bank, template_options)
@ -116,7 +122,7 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
data_import = frappe.get_doc("Bank Statement Import", data_import) data_import = frappe.get_doc("Bank Statement Import", data_import)
file = import_file_path if import_file_path else google_sheets_url file = import_file_path if import_file_path else google_sheets_url
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records") import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records")
data = parse_data_from_template(import_file.raw_data) data = parse_data_from_template(import_file.raw_data)
@ -136,16 +142,18 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name}) frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name})
def update_mapping_db(bank, template_options): def update_mapping_db(bank, template_options):
bank = frappe.get_doc("Bank", bank) bank = frappe.get_doc("Bank", bank)
for d in bank.bank_transaction_mapping: for d in bank.bank_transaction_mapping:
d.delete() d.delete()
for d in json.loads(template_options)["column_to_field_map"].items(): for d in json.loads(template_options)["column_to_field_map"].items():
bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1] ,"file_field": d[0]} ) bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1], "file_field": d[0]})
bank.save() bank.save()
def add_bank_account(data, bank_account): def add_bank_account(data, bank_account):
bank_account_loc = None bank_account_loc = None
if "Bank Account" not in data[0]: if "Bank Account" not in data[0]:
@ -161,6 +169,7 @@ def add_bank_account(data, bank_account):
else: else:
row.append(bank_account) row.append(bank_account)
def write_files(import_file, data): def write_files(import_file, data):
full_file_path = import_file.file_doc.get_full_path() full_file_path = import_file.file_doc.get_full_path()
parts = import_file.file_doc.get_extension() parts = import_file.file_doc.get_extension()
@ -168,11 +177,12 @@ def write_files(import_file, data):
extension = extension.lstrip(".") extension = extension.lstrip(".")
if extension == "csv": if extension == "csv":
with open(full_file_path, 'w', newline='') as file: with open(full_file_path, "w", newline="") as file:
writer = csv.writer(file) writer = csv.writer(file)
writer.writerows(data) writer.writerows(data)
elif extension == "xlsx" or "xls": elif extension == "xlsx" or "xls":
write_xlsx(data, "trans", file_path = full_file_path) write_xlsx(data, "trans", file_path=full_file_path)
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None): def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
# from xlsx utils with changes # from xlsx utils with changes
@ -187,19 +197,19 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
ws.column_dimensions[get_column_letter(i + 1)].width = column_width ws.column_dimensions[get_column_letter(i + 1)].width = column_width
row1 = ws.row_dimensions[1] row1 = ws.row_dimensions[1]
row1.font = Font(name='Calibri', bold=True) row1.font = Font(name="Calibri", bold=True)
for row in data: for row in data:
clean_row = [] clean_row = []
for item in row: for item in row:
if isinstance(item, str) and (sheet_name not in ['Data Import Template', 'Data Export']): if isinstance(item, str) and (sheet_name not in ["Data Import Template", "Data Export"]):
value = handle_html(item) value = handle_html(item)
else: else:
value = item value = item
if isinstance(item, str) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None): if isinstance(item, str) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
# Remove illegal characters from the string # Remove illegal characters from the string
value = re.sub(ILLEGAL_CHARACTERS_RE, '', value) value = re.sub(ILLEGAL_CHARACTERS_RE, "", value)
clean_row.append(value) clean_row.append(value)
@ -208,19 +218,20 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
wb.save(file_path) wb.save(file_path)
return True return True
@frappe.whitelist() @frappe.whitelist()
def upload_bank_statement(**args): def upload_bank_statement(**args):
args = frappe._dict(args) args = frappe._dict(args)
bsi = frappe.new_doc("Bank Statement Import") bsi = frappe.new_doc("Bank Statement Import")
if args.company: if args.company:
bsi.update({ bsi.update(
"company": args.company, {
}) "company": args.company,
}
)
if args.bank_account: if args.bank_account:
bsi.update({ bsi.update({"bank_account": args.bank_account})
"bank_account": args.bank_account
})
return bsi return bsi

View File

@ -29,17 +29,26 @@ class BankTransaction(StatusUpdater):
def update_allocations(self): def update_allocations(self):
if self.payment_entries: if self.payment_entries:
allocated_amount = reduce(lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]) allocated_amount = reduce(
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
)
else: else:
allocated_amount = 0 allocated_amount = 0
if allocated_amount: if allocated_amount:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount)) frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount)) frappe.db.set_value(
self.doctype,
self.name,
"unallocated_amount",
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
)
else: else:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0) frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))) frappe.db.set_value(
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
)
amount = self.deposit or self.withdrawal amount = self.deposit or self.withdrawal
if amount == self.allocated_amount: if amount == self.allocated_amount:
@ -49,8 +58,14 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False): def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment", if payment_entry.payment_document in [
"Loan Disbursement"]: "Payment Entry",
"Journal Entry",
"Purchase Invoice",
"Expense Claim",
"Loan Repayment",
"Loan Disbursement",
]:
self.clear_simple_entry(payment_entry, for_cancel=for_cancel) self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice": elif payment_entry.payment_document == "Sales Invoice":
@ -58,38 +73,41 @@ class BankTransaction(StatusUpdater):
def clear_simple_entry(self, payment_entry, for_cancel=False): def clear_simple_entry(self, payment_entry, for_cancel=False):
if payment_entry.payment_document == "Payment Entry": if payment_entry.payment_document == "Payment Entry":
if frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") == "Internal Transfer": if (
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
== "Internal Transfer"
):
if len(get_reconciled_bank_transactions(payment_entry)) < 2: if len(get_reconciled_bank_transactions(payment_entry)) < 2:
return return
clearance_date = self.date if not for_cancel else None clearance_date = self.date if not for_cancel else None
frappe.db.set_value( frappe.db.set_value(
payment_entry.payment_document, payment_entry.payment_entry, payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
"clearance_date", clearance_date) )
def clear_sales_invoice(self, payment_entry, for_cancel=False): def clear_sales_invoice(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None clearance_date = self.date if not for_cancel else None
frappe.db.set_value( frappe.db.set_value(
"Sales Invoice Payment", "Sales Invoice Payment",
dict( dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
parenttype=payment_entry.payment_document, "clearance_date",
parent=payment_entry.payment_entry clearance_date,
), )
"clearance_date", clearance_date)
def get_reconciled_bank_transactions(payment_entry): def get_reconciled_bank_transactions(payment_entry):
reconciled_bank_transactions = frappe.get_all( reconciled_bank_transactions = frappe.get_all(
'Bank Transaction Payments', "Bank Transaction Payments",
filters = { filters={"payment_entry": payment_entry.payment_entry},
'payment_entry': payment_entry.payment_entry fields=["parent"],
},
fields = ['parent']
) )
return reconciled_bank_transactions return reconciled_bank_transactions
def get_total_allocated_amount(payment_entry): def get_total_allocated_amount(payment_entry):
return frappe.db.sql(""" return frappe.db.sql(
"""
SELECT SELECT
SUM(btp.allocated_amount) as allocated_amount, SUM(btp.allocated_amount) as allocated_amount,
bt.name bt.name
@ -102,48 +120,73 @@ def get_total_allocated_amount(payment_entry):
AND AND
btp.payment_entry = %s btp.payment_entry = %s
AND AND
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True) bt.docstatus = 1""",
(payment_entry.payment_document, payment_entry.payment_entry),
as_dict=True,
)
def get_paid_amount(payment_entry, currency, bank_account): def get_paid_amount(payment_entry, currency, bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount" paid_amount_field = "paid_amount"
if payment_entry.payment_document == 'Payment Entry': if payment_entry.payment_document == "Payment Entry":
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry) doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
if doc.payment_type == 'Receive': if doc.payment_type == "Receive":
paid_amount_field = ("received_amount" paid_amount_field = (
if doc.paid_to_account_currency == currency else "base_received_amount") "received_amount" if doc.paid_to_account_currency == currency else "base_received_amount"
elif doc.payment_type == 'Pay': )
paid_amount_field = ("paid_amount" elif doc.payment_type == "Pay":
if doc.paid_to_account_currency == currency else "base_paid_amount") paid_amount_field = (
"paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
)
return frappe.db.get_value(payment_entry.payment_document, return frappe.db.get_value(
payment_entry.payment_entry, paid_amount_field) payment_entry.payment_document, payment_entry.payment_entry, paid_amount_field
)
elif payment_entry.payment_document == "Journal Entry": elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, return frappe.db.get_value(
"sum(credit_in_account_currency)") "Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": bank_account},
"sum(credit_in_account_currency)",
)
elif payment_entry.payment_document == "Expense Claim": elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed") return frappe.db.get_value(
payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed"
)
elif payment_entry.payment_document == "Loan Disbursement": elif payment_entry.payment_document == "Loan Disbursement":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount") return frappe.db.get_value(
payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount"
)
elif payment_entry.payment_document == "Loan Repayment": elif payment_entry.payment_document == "Loan Repayment":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid") return frappe.db.get_value(
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
)
else: else:
frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry)) frappe.throw(
"Please reconcile {0}: {1} manually".format(
payment_entry.payment_document, payment_entry.payment_entry
)
)
@frappe.whitelist() @frappe.whitelist()
def unclear_reference_payment(doctype, docname): def unclear_reference_payment(doctype, docname):
if frappe.db.exists(doctype, docname): if frappe.db.exists(doctype, docname):
doc = frappe.get_doc(doctype, docname) doc = frappe.get_doc(doctype, docname)
if doctype == "Sales Invoice": if doctype == "Sales Invoice":
frappe.db.set_value("Sales Invoice Payment", dict(parenttype=doc.payment_document, frappe.db.set_value(
parent=doc.payment_entry), "clearance_date", None) "Sales Invoice Payment",
dict(parenttype=doc.payment_document, parent=doc.payment_entry),
"clearance_date",
None,
)
else: else:
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None) frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)

View File

@ -18,12 +18,14 @@ def upload_bank_statement():
fcontent = frappe.local.uploaded_file fcontent = frappe.local.uploaded_file
fname = frappe.local.uploaded_filename fname = frappe.local.uploaded_filename
if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')): if frappe.safe_encode(fname).lower().endswith("csv".encode("utf-8")):
from frappe.utils.csvutils import read_csv_content from frappe.utils.csvutils import read_csv_content
rows = read_csv_content(fcontent, False) rows = read_csv_content(fcontent, False)
elif frappe.safe_encode(fname).lower().endswith("xlsx".encode('utf-8')): elif frappe.safe_encode(fname).lower().endswith("xlsx".encode("utf-8")):
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
rows = read_xlsx_file_from_attached_file(fcontent=fcontent) rows = read_xlsx_file_from_attached_file(fcontent=fcontent)
columns = rows[0] columns = rows[0]
@ -43,12 +45,10 @@ def create_bank_entries(columns, data, bank_account):
continue continue
fields = {} fields = {}
for key, value in header_map.items(): for key, value in header_map.items():
fields.update({key: d[int(value)-1]}) fields.update({key: d[int(value) - 1]})
try: try:
bank_transaction = frappe.get_doc({ bank_transaction = frappe.get_doc({"doctype": "Bank Transaction"})
"doctype": "Bank Transaction"
})
bank_transaction.update(fields) bank_transaction.update(fields)
bank_transaction.date = getdate(parse_date(bank_transaction.date)) bank_transaction.date = getdate(parse_date(bank_transaction.date))
bank_transaction.bank_account = bank_account bank_transaction.bank_account = bank_account
@ -61,6 +61,7 @@ def create_bank_entries(columns, data, bank_account):
return {"success": success, "errors": errors} return {"success": success, "errors": errors}
def get_header_mapping(columns, bank_account): def get_header_mapping(columns, bank_account):
mapping = get_bank_mapping(bank_account) mapping = get_bank_mapping(bank_account)
@ -71,10 +72,11 @@ def get_header_mapping(columns, bank_account):
return header_map return header_map
def get_bank_mapping(bank_account): def get_bank_mapping(bank_account):
bank_name = frappe.db.get_value("Bank Account", bank_account, "bank") bank_name = frappe.db.get_value("Bank Account", bank_account, "bank")
bank = frappe.get_doc("Bank", bank_name) bank = frappe.get_doc("Bank", bank_name)
mapping = {row.file_field:row.bank_transaction_field for row in bank.bank_transaction_mapping} mapping = {row.file_field: row.bank_transaction_field for row in bank.bank_transaction_mapping}
return mapping return mapping

View File

@ -17,6 +17,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
test_dependencies = ["Item", "Cost Center"] test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase): class TestBankTransaction(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -41,21 +42,34 @@ class TestBankTransaction(unittest.TestCase):
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self): def test_linked_payments(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic")) bank_transaction = frappe.get_doc(
linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match']) "Bank Transaction",
dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"),
)
linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
self.assertTrue(linked_payments[0][6] == "Conrad Electronic") self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self): def test_reconcile(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G")) bank_transaction = frappe.get_doc(
"Bank Transaction",
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700)) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
vouchers = json.dumps([{ vouchers = json.dumps(
"payment_doctype":"Payment Entry", [
"payment_name":payment.name, {
"amount":bank_transaction.unallocated_amount}]) "payment_doctype": "Payment Entry",
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers) reconcile_vouchers(bank_transaction.name, vouchers)
unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount") unallocated_amount = frappe.db.get_value(
"Bank Transaction", bank_transaction.name, "unallocated_amount"
)
self.assertTrue(unallocated_amount == 0) self.assertTrue(unallocated_amount == 0)
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
@ -69,122 +83,177 @@ class TestBankTransaction(unittest.TestCase):
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self): def test_debit_credit_output(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) bank_transaction = frappe.get_doc(
linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match']) "Bank Transaction",
dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"),
)
linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
self.assertTrue(linked_payments[0][3]) self.assertTrue(linked_payments[0][3])
# Check error if already reconciled # Check error if already reconciled
def test_already_reconciled(self): def test_already_reconciled(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) bank_transaction = frappe.get_doc(
"Bank Transaction",
dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
vouchers = json.dumps([{ vouchers = json.dumps(
"payment_doctype":"Payment Entry", [
"payment_name":payment.name, {
"amount":bank_transaction.unallocated_amount}]) "payment_doctype": "Payment Entry",
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers) reconcile_vouchers(bank_transaction.name, vouchers)
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) bank_transaction = frappe.get_doc(
"Bank Transaction",
dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
vouchers = json.dumps([{ vouchers = json.dumps(
"payment_doctype":"Payment Entry", [
"payment_name":payment.name, {
"amount":bank_transaction.unallocated_amount}]) "payment_doctype": "Payment Entry",
self.assertRaises(frappe.ValidationError, reconcile_vouchers, bank_transaction_name=bank_transaction.name, vouchers=vouchers) "payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
self.assertRaises(
frappe.ValidationError,
reconcile_vouchers,
bank_transaction_name=bank_transaction.name,
vouchers=vouchers,
)
# Raise an error if debitor transaction vs debitor payment # Raise an error if debitor transaction vs debitor payment
def test_clear_sales_invoice(self): def test_clear_sales_invoice(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio")) bank_transaction = frappe.get_doc(
"Bank Transaction",
dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"),
)
payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"])) payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"]))
vouchers = json.dumps([{ vouchers = json.dumps(
"payment_doctype":"Sales Invoice", [
"payment_name":payment.name, {
"amount":bank_transaction.unallocated_amount}]) "payment_doctype": "Sales Invoice",
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers=vouchers) reconcile_vouchers(bank_transaction.name, vouchers=vouchers)
self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0) self.assertEqual(
self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None) frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0
)
self.assertTrue(
frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date")
is not None
)
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try: try:
frappe.get_doc({ frappe.get_doc(
"doctype": "Bank", {
"bank_name":bank_name, "doctype": "Bank",
}).insert(ignore_if_duplicate=True) "bank_name": bank_name,
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
try: try:
frappe.get_doc({ frappe.get_doc(
"doctype": "Bank Account", {
"account_name":"Checking Account", "doctype": "Bank Account",
"bank": bank_name, "account_name": "Checking Account",
"account": account_name "bank": bank_name,
}).insert(ignore_if_duplicate=True) "account": account_name,
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
def add_transactions(): def add_transactions():
create_bank_account() create_bank_account()
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "Bank Transaction", {
"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", "doctype": "Bank Transaction",
"date": "2018-10-23", "description": "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
"deposit": 1200, "date": "2018-10-23",
"currency": "INR", "deposit": 1200,
"bank_account": "Checking Account - Citi Bank" "currency": "INR",
}).insert() "bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit() doc.submit()
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "Bank Transaction", {
"description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G", "doctype": "Bank Transaction",
"date": "2018-10-23", "description": "1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
"deposit": 1700, "date": "2018-10-23",
"currency": "INR", "deposit": 1700,
"bank_account": "Checking Account - Citi Bank" "currency": "INR",
}).insert() "bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit() doc.submit()
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "Bank Transaction", {
"description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic", "doctype": "Bank Transaction",
"date": "2018-10-26", "description": "Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
"withdrawal": 690, "date": "2018-10-26",
"currency": "INR", "withdrawal": 690,
"bank_account": "Checking Account - Citi Bank" "currency": "INR",
}).insert() "bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit() doc.submit()
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "Bank Transaction", {
"description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07", "doctype": "Bank Transaction",
"date": "2018-10-27", "description": "Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
"deposit": 3900, "date": "2018-10-27",
"currency": "INR", "deposit": 3900,
"bank_account": "Checking Account - Citi Bank" "currency": "INR",
}).insert() "bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit() doc.submit()
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "Bank Transaction", {
"description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio", "doctype": "Bank Transaction",
"date": "2018-10-27", "description": "I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
"withdrawal": 109080, "date": "2018-10-27",
"currency": "INR", "withdrawal": 109080,
"bank_account": "Checking Account - Citi Bank" "currency": "INR",
}).insert() "bank_account": "Checking Account - Citi Bank",
}
).insert()
doc.submit() doc.submit()
def add_vouchers(): def add_vouchers():
try: try:
frappe.get_doc({ frappe.get_doc(
"doctype": "Supplier", {
"supplier_group":"All Supplier Groups", "doctype": "Supplier",
"supplier_type": "Company", "supplier_group": "All Supplier Groups",
"supplier_name": "Conrad Electronic" "supplier_type": "Company",
}).insert(ignore_if_duplicate=True) "supplier_name": "Conrad Electronic",
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -198,12 +267,14 @@ def add_vouchers():
pe.submit() pe.submit()
try: try:
frappe.get_doc({ frappe.get_doc(
"doctype": "Supplier", {
"supplier_group":"All Supplier Groups", "doctype": "Supplier",
"supplier_type": "Company", "supplier_group": "All Supplier Groups",
"supplier_name": "Mr G" "supplier_type": "Company",
}).insert(ignore_if_duplicate=True) "supplier_name": "Mr G",
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -222,26 +293,30 @@ def add_vouchers():
pe.submit() pe.submit()
try: try:
frappe.get_doc({ frappe.get_doc(
"doctype": "Supplier", {
"supplier_group":"All Supplier Groups", "doctype": "Supplier",
"supplier_type": "Company", "supplier_group": "All Supplier Groups",
"supplier_name": "Poore Simon's" "supplier_type": "Company",
}).insert(ignore_if_duplicate=True) "supplier_name": "Poore Simon's",
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
try: try:
frappe.get_doc({ frappe.get_doc(
"doctype": "Customer", {
"customer_group":"All Customer Groups", "doctype": "Customer",
"customer_type": "Company", "customer_group": "All Customer Groups",
"customer_name": "Poore Simon's" "customer_type": "Company",
}).insert(ignore_if_duplicate=True) "customer_name": "Poore Simon's",
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save =1) pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
pi.cash_bank_account = "_Test Bank - _TC" pi.cash_bank_account = "_Test Bank - _TC"
pi.insert() pi.insert()
pi.submit() pi.submit()
@ -261,33 +336,31 @@ def add_vouchers():
pe.submit() pe.submit()
try: try:
frappe.get_doc({ frappe.get_doc(
"doctype": "Customer", {
"customer_group":"All Customer Groups", "doctype": "Customer",
"customer_type": "Company", "customer_group": "All Customer Groups",
"customer_name": "Fayva" "customer_type": "Company",
}).insert(ignore_if_duplicate=True) "customer_name": "Fayva",
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
mode_of_payment = frappe.get_doc({ mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
"doctype": "Mode of Payment",
"name": "Cash"
})
if not frappe.db.get_value('Mode of Payment Account', {'company': "_Test Company", 'parent': "Cash"}): if not frappe.db.get_value(
mode_of_payment.append("accounts", { "Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
"company": "_Test Company", ):
"default_account": "_Test Bank - _TC" mode_of_payment.append(
}) "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
)
mode_of_payment.save() mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1) si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1 si.is_pos = 1
si.append("payments", { si.append(
"mode_of_payment": "Cash", "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
"account": "_Test Bank - _TC", )
"amount": 109080
})
si.insert() si.insert()
si.submit() si.submit()

View File

@ -14,13 +14,19 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
class BudgetError(frappe.ValidationError): pass class BudgetError(frappe.ValidationError):
class DuplicateBudgetError(frappe.ValidationError): pass pass
class DuplicateBudgetError(frappe.ValidationError):
pass
class Budget(Document): class Budget(Document):
def autoname(self): def autoname(self):
self.name = make_autoname(self.get(frappe.scrub(self.budget_against)) self.name = make_autoname(
+ "/" + self.fiscal_year + "/.###") self.get(frappe.scrub(self.budget_against)) + "/" + self.fiscal_year + "/.###"
)
def validate(self): def validate(self):
if not self.get(frappe.scrub(self.budget_against)): if not self.get(frappe.scrub(self.budget_against)):
@ -35,34 +41,44 @@ class Budget(Document):
budget_against = self.get(budget_against_field) budget_against = self.get(budget_against_field)
accounts = [d.account for d in self.accounts] or [] accounts = [d.account for d in self.accounts] or []
existing_budget = frappe.db.sql(""" existing_budget = frappe.db.sql(
"""
select select
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
where where
ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and
b.fiscal_year=%s and b.name != %s and ba.account in (%s) """ b.fiscal_year=%s and b.name != %s and ba.account in (%s) """
% ('%s', budget_against_field, '%s', '%s', '%s', ','.join(['%s'] * len(accounts))), % ("%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))),
(self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts), as_dict=1) (self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts),
as_dict=1,
)
for d in existing_budget: for d in existing_budget:
frappe.throw(_("Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}") frappe.throw(
.format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year), DuplicateBudgetError) _(
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}"
).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
DuplicateBudgetError,
)
def validate_accounts(self): def validate_accounts(self):
account_list = [] account_list = []
for d in self.get('accounts'): for d in self.get("accounts"):
if d.account: if d.account:
account_details = frappe.db.get_value("Account", d.account, account_details = frappe.db.get_value(
["is_group", "company", "report_type"], as_dict=1) "Account", d.account, ["is_group", "company", "report_type"], as_dict=1
)
if account_details.is_group: if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account)) frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
elif account_details.company != self.company: elif account_details.company != self.company:
frappe.throw(_("Account {0} does not belongs to company {1}") frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company))
.format(d.account, self.company))
elif account_details.report_type != "Profit and Loss": elif account_details.report_type != "Profit and Loss":
frappe.throw(_("Budget cannot be assigned against {0}, as it's not an Income or Expense account") frappe.throw(
.format(d.account)) _("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
d.account
)
)
if d.account in account_list: if d.account in account_list:
frappe.throw(_("Account {0} has been entered multiple times").format(d.account)) frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
@ -70,51 +86,66 @@ class Budget(Document):
account_list.append(d.account) account_list.append(d.account)
def set_null_value(self): def set_null_value(self):
if self.budget_against == 'Cost Center': if self.budget_against == "Cost Center":
self.project = None self.project = None
else: else:
self.cost_center = None self.cost_center = None
def validate_applicable_for(self): def validate_applicable_for(self):
if (self.applicable_on_material_request if self.applicable_on_material_request and not (
and not (self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses)): self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses
frappe.throw(_("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses")) ):
frappe.throw(
_("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses")
)
elif (self.applicable_on_purchase_order elif self.applicable_on_purchase_order and not (self.applicable_on_booking_actual_expenses):
and not (self.applicable_on_booking_actual_expenses)):
frappe.throw(_("Please enable Applicable on Booking Actual Expenses")) frappe.throw(_("Please enable Applicable on Booking Actual Expenses"))
elif not(self.applicable_on_material_request elif not (
or self.applicable_on_purchase_order or self.applicable_on_booking_actual_expenses): self.applicable_on_material_request
or self.applicable_on_purchase_order
or self.applicable_on_booking_actual_expenses
):
self.applicable_on_booking_actual_expenses = 1 self.applicable_on_booking_actual_expenses = 1
def validate_expense_against_budget(args): def validate_expense_against_budget(args):
args = frappe._dict(args) args = frappe._dict(args)
if args.get('company') and not args.fiscal_year: if args.get("company") and not args.fiscal_year:
args.fiscal_year = get_fiscal_year(args.get('posting_date'), company=args.get('company'))[0] args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
frappe.flags.exception_approver_role = frappe.get_cached_value('Company', frappe.flags.exception_approver_role = frappe.get_cached_value(
args.get('company'), 'exception_budget_approver_role') "Company", args.get("company"), "exception_budget_approver_role"
)
if not args.account: if not args.account:
args.account = args.get("expense_account") args.account = args.get("expense_account")
if not (args.get('account') and args.get('cost_center')) and args.item_code: if not (args.get("account") and args.get("cost_center")) and args.item_code:
args.cost_center, args.account = get_item_details(args) args.cost_center, args.account = get_item_details(args)
if not args.account: if not args.account:
return return
for budget_against in ['project', 'cost_center'] + get_accounting_dimensions(): for budget_against in ["project", "cost_center"] + get_accounting_dimensions():
if (args.get(budget_against) and args.account if (
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})): args.get(budget_against)
and args.account
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
):
doctype = frappe.unscrub(budget_against) doctype = frappe.unscrub(budget_against)
if frappe.get_cached_value('DocType', doctype, 'is_tree'): if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
condition = """and exists(select name from `tab%s` condition = """and exists(select name from `tab%s`
where lft<=%s and rgt>=%s and name=b.%s)""" % (doctype, lft, rgt, budget_against) #nosec where lft<=%s and rgt>=%s and name=b.%s)""" % (
doctype,
lft,
rgt,
budget_against,
) # nosec
args.is_tree = True args.is_tree = True
else: else:
condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against))) condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against)))
@ -123,7 +154,8 @@ def validate_expense_against_budget(args):
args.budget_against_field = budget_against args.budget_against_field = budget_against
args.budget_against_doctype = doctype args.budget_against_doctype = doctype
budget_records = frappe.db.sql(""" budget_records = frappe.db.sql(
"""
select select
b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution, b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution,
ifnull(b.applicable_on_material_request, 0) as for_material_request, ifnull(b.applicable_on_material_request, 0) as for_material_request,
@ -138,11 +170,17 @@ def validate_expense_against_budget(args):
b.name=ba.parent and b.fiscal_year=%s b.name=ba.parent and b.fiscal_year=%s
and ba.account=%s and b.docstatus=1 and ba.account=%s and b.docstatus=1
{condition} {condition}
""".format(condition=condition, budget_against_field=budget_against), (args.fiscal_year, args.account), as_dict=True) #nosec """.format(
condition=condition, budget_against_field=budget_against
),
(args.fiscal_year, args.account),
as_dict=True,
) # nosec
if budget_records: if budget_records:
validate_budget_records(args, budget_records) validate_budget_records(args, budget_records)
def validate_budget_records(args, budget_records): def validate_budget_records(args, budget_records):
for budget in budget_records: for budget in budget_records:
if flt(budget.budget_amount): if flt(budget.budget_amount):
@ -150,88 +188,118 @@ def validate_budget_records(args, budget_records):
yearly_action, monthly_action = get_actions(args, budget) yearly_action, monthly_action = get_actions(args, budget)
if monthly_action in ["Stop", "Warn"]: if monthly_action in ["Stop", "Warn"]:
budget_amount = get_accumulated_monthly_budget(budget.monthly_distribution, budget_amount = get_accumulated_monthly_budget(
args.posting_date, args.fiscal_year, budget.budget_amount) budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
)
args["month_end_date"] = get_last_day(args.posting_date) args["month_end_date"] = get_last_day(args.posting_date)
compare_expense_with_budget(args, budget_amount, compare_expense_with_budget(
_("Accumulated Monthly"), monthly_action, budget.budget_against, amount) args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount
)
if (
yearly_action in ("Stop", "Warn")
and monthly_action != "Stop"
and yearly_action != monthly_action
):
compare_expense_with_budget(
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
)
if yearly_action in ("Stop", "Warn") and monthly_action != "Stop" \
and yearly_action != monthly_action:
compare_expense_with_budget(args, flt(budget.budget_amount),
_("Annual"), yearly_action, budget.budget_against, amount)
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0): def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
actual_expense = amount or get_actual_expense(args) actual_expense = amount or get_actual_expense(args)
if actual_expense > budget_amount: if actual_expense > budget_amount:
diff = actual_expense - budget_amount diff = actual_expense - budget_amount
currency = frappe.get_cached_value('Company', args.company, 'default_currency') currency = frappe.get_cached_value("Company", args.company, "default_currency")
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format( msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format(
_(action_for), frappe.bold(args.account), args.budget_against_field, _(action_for),
frappe.bold(budget_against), frappe.bold(args.account),
frappe.bold(fmt_money(budget_amount, currency=currency)), args.budget_against_field,
frappe.bold(fmt_money(diff, currency=currency))) frappe.bold(budget_against),
frappe.bold(fmt_money(budget_amount, currency=currency)),
frappe.bold(fmt_money(diff, currency=currency)),
)
if (frappe.flags.exception_approver_role if (
and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)): frappe.flags.exception_approver_role
and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)
):
action = "Warn" action = "Warn"
if action=="Stop": if action == "Stop":
frappe.throw(msg, BudgetError) frappe.throw(msg, BudgetError)
else: else:
frappe.msgprint(msg, indicator='orange') frappe.msgprint(msg, indicator="orange")
def get_actions(args, budget): def get_actions(args, budget):
yearly_action = budget.action_if_annual_budget_exceeded yearly_action = budget.action_if_annual_budget_exceeded
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
if args.get('doctype') == 'Material Request' and budget.for_material_request: if args.get("doctype") == "Material Request" and budget.for_material_request:
yearly_action = budget.action_if_annual_budget_exceeded_on_mr yearly_action = budget.action_if_annual_budget_exceeded_on_mr
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order: elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
yearly_action = budget.action_if_annual_budget_exceeded_on_po yearly_action = budget.action_if_annual_budget_exceeded_on_po
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
return yearly_action, monthly_action return yearly_action, monthly_action
def get_amount(args, budget): def get_amount(args, budget):
amount = 0 amount = 0
if args.get('doctype') == 'Material Request' and budget.for_material_request: if args.get("doctype") == "Material Request" and budget.for_material_request:
amount = (get_requested_amount(args, budget) amount = (
+ get_ordered_amount(args, budget) + get_actual_expense(args)) get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args)
)
elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order: elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
amount = get_ordered_amount(args, budget) + get_actual_expense(args) amount = get_ordered_amount(args, budget) + get_actual_expense(args)
return amount return amount
def get_requested_amount(args, budget):
item_code = args.get('item_code')
condition = get_other_condition(args, budget, 'Material Request')
data = frappe.db.sql(""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount def get_requested_amount(args, budget):
item_code = args.get("item_code")
condition = get_other_condition(args, budget, "Material Request")
data = frappe.db.sql(
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and
child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {0} and child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {0} and
parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition), item_code, as_list=1) parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(
condition
),
item_code,
as_list=1,
)
return data[0][0] if data else 0 return data[0][0] if data else 0
def get_ordered_amount(args, budget): def get_ordered_amount(args, budget):
item_code = args.get('item_code') item_code = args.get("item_code")
condition = get_other_condition(args, budget, 'Purchase Order') condition = get_other_condition(args, budget, "Purchase Order")
data = frappe.db.sql(""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount data = frappe.db.sql(
""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
from `tabPurchase Order Item` child, `tabPurchase Order` parent where from `tabPurchase Order Item` child, `tabPurchase Order` parent where
parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt
and parent.status != 'Closed' and {0}""".format(condition), item_code, as_list=1) and parent.status != 'Closed' and {0}""".format(
condition
),
item_code,
as_list=1,
)
return data[0][0] if data else 0 return data[0][0] if data else 0
def get_other_condition(args, budget, for_doc): def get_other_condition(args, budget, for_doc):
condition = "expense_account = '%s'" % (args.expense_account) condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = args.get("budget_against_field") budget_against_field = args.get("budget_against_field")
@ -239,41 +307,51 @@ def get_other_condition(args, budget, for_doc):
if budget_against_field and args.get(budget_against_field): if budget_against_field and args.get(budget_against_field):
condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field)) condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field))
if args.get('fiscal_year'): if args.get("fiscal_year"):
date_field = 'schedule_date' if for_doc == 'Material Request' else 'transaction_date' date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
start_date, end_date = frappe.db.get_value('Fiscal Year', args.get('fiscal_year'), start_date, end_date = frappe.db.get_value(
['year_start_date', 'year_end_date']) "Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
)
condition += """ and parent.%s condition += """ and parent.%s
between '%s' and '%s' """ %(date_field, start_date, end_date) between '%s' and '%s' """ % (
date_field,
start_date,
end_date,
)
return condition return condition
def get_actual_expense(args): def get_actual_expense(args):
if not args.budget_against_doctype: if not args.budget_against_doctype:
args.budget_against_doctype = frappe.unscrub(args.budget_against_field) args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
budget_against_field = args.get('budget_against_field') budget_against_field = args.get("budget_against_field")
condition1 = " and gle.posting_date <= %(month_end_date)s" \ condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
if args.get("month_end_date") else ""
if args.is_tree: if args.is_tree:
lft_rgt = frappe.db.get_value(args.budget_against_doctype, lft_rgt = frappe.db.get_value(
args.get(budget_against_field), ["lft", "rgt"], as_dict=1) args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
)
args.update(lft_rgt) args.update(lft_rgt)
condition2 = """and exists(select name from `tab{doctype}` condition2 = """and exists(select name from `tab{doctype}`
where lft>=%(lft)s and rgt<=%(rgt)s where lft>=%(lft)s and rgt<=%(rgt)s
and name=gle.{budget_against_field})""".format(doctype=args.budget_against_doctype, #nosec and name=gle.{budget_against_field})""".format(
budget_against_field=budget_against_field) doctype=args.budget_against_doctype, budget_against_field=budget_against_field # nosec
)
else: else:
condition2 = """and exists(select name from `tab{doctype}` condition2 = """and exists(select name from `tab{doctype}`
where name=gle.{budget_against} and where name=gle.{budget_against} and
gle.{budget_against} = %({budget_against})s)""".format(doctype=args.budget_against_doctype, gle.{budget_against} = %({budget_against})s)""".format(
budget_against = budget_against_field) doctype=args.budget_against_doctype, budget_against=budget_against_field
)
amount = flt(frappe.db.sql(""" amount = flt(
frappe.db.sql(
"""
select sum(gle.debit) - sum(gle.credit) select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle from `tabGL Entry` gle
where gle.account=%(account)s where gle.account=%(account)s
@ -282,46 +360,59 @@ def get_actual_expense(args):
and gle.company=%(company)s and gle.company=%(company)s
and gle.docstatus=1 and gle.docstatus=1
{condition2} {condition2}
""".format(condition1=condition1, condition2=condition2), (args))[0][0]) #nosec """.format(
condition1=condition1, condition2=condition2
),
(args),
)[0][0]
) # nosec
return amount return amount
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget): def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
distribution = {} distribution = {}
if monthly_distribution: if monthly_distribution:
for d in frappe.db.sql("""select mdp.month, mdp.percentage_allocation for d in frappe.db.sql(
"""select mdp.month, mdp.percentage_allocation
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
where mdp.parent=md.name and md.fiscal_year=%s""", fiscal_year, as_dict=1): where mdp.parent=md.name and md.fiscal_year=%s""",
distribution.setdefault(d.month, d.percentage_allocation) fiscal_year,
as_dict=1,
):
distribution.setdefault(d.month, d.percentage_allocation)
dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date") dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date")
accumulated_percentage = 0.0 accumulated_percentage = 0.0
while(dt <= getdate(posting_date)): while dt <= getdate(posting_date):
if monthly_distribution: if monthly_distribution:
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0) accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
else: else:
accumulated_percentage += 100.0/12 accumulated_percentage += 100.0 / 12
dt = add_months(dt, 1) dt = add_months(dt, 1)
return annual_budget * accumulated_percentage / 100 return annual_budget * accumulated_percentage / 100
def get_item_details(args): def get_item_details(args):
cost_center, expense_account = None, None cost_center, expense_account = None, None
if not args.get('company'): if not args.get("company"):
return cost_center, expense_account return cost_center, expense_account
if args.item_code: if args.item_code:
item_defaults = frappe.db.get_value('Item Default', item_defaults = frappe.db.get_value(
{'parent': args.item_code, 'company': args.get('company')}, "Item Default",
['buying_cost_center', 'expense_account']) {"parent": args.item_code, "company": args.get("company")},
["buying_cost_center", "expense_account"],
)
if item_defaults: if item_defaults:
cost_center, expense_account = item_defaults cost_center, expense_account = item_defaults
if not (cost_center and expense_account): if not (cost_center and expense_account):
for doctype in ['Item Group', 'Company']: for doctype in ["Item Group", "Company"]:
data = get_expense_cost_center(doctype, args) data = get_expense_cost_center(doctype, args)
if not cost_center and data: if not cost_center and data:
@ -335,11 +426,15 @@ def get_item_details(args):
return cost_center, expense_account return cost_center, expense_account
def get_expense_cost_center(doctype, args): def get_expense_cost_center(doctype, args):
if doctype == 'Item Group': if doctype == "Item Group":
return frappe.db.get_value('Item Default', return frappe.db.get_value(
{'parent': args.get(frappe.scrub(doctype)), 'company': args.get('company')}, "Item Default",
['buying_cost_center', 'expense_account']) {"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
["buying_cost_center", "expense_account"],
)
else: else:
return frappe.db.get_value(doctype, args.get(frappe.scrub(doctype)),\ return frappe.db.get_value(
['cost_center', 'default_expense_account']) doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
)

View File

@ -11,7 +11,8 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
test_dependencies = ['Monthly Distribution'] test_dependencies = ["Monthly Distribution"]
class TestBudget(unittest.TestCase): class TestBudget(unittest.TestCase):
def test_monthly_budget_crossed_ignore(self): def test_monthly_budget_crossed_ignore(self):
@ -19,11 +20,18 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
40000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
submit=True,
)
self.assertTrue(frappe.db.get_value("GL Entry", self.assertTrue(
{"voucher_type": "Journal Entry", "voucher_no": jv.name})) frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
)
budget.cancel() budget.cancel()
jv.cancel() jv.cancel()
@ -33,10 +41,17 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value(
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate()) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
40000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -48,49 +63,65 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value(
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate()) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
40000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', 'Accounts User') frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "Accounts User")
jv.submit() jv.submit()
self.assertEqual(frappe.db.get_value('Journal Entry', jv.name, 'docstatus'), 1) self.assertEqual(frappe.db.get_value("Journal Entry", jv.name, "docstatus"), 1)
jv.cancel() jv.cancel()
frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', '') frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "")
budget.load_from_db() budget.load_from_db()
budget.cancel() budget.cancel()
def test_monthly_budget_crossed_for_mr(self): def test_monthly_budget_crossed_for_mr(self):
budget = make_budget(applicable_on_material_request=1, budget = make_budget(
applicable_on_purchase_order=1, action_if_accumulated_monthly_budget_exceeded_on_mr="Stop", applicable_on_material_request=1,
budget_against="Cost Center") applicable_on_purchase_order=1,
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
budget_against="Cost Center",
)
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value(
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
mr = frappe.get_doc({ mr = frappe.get_doc(
"doctype": "Material Request", {
"material_request_type": "Purchase", "doctype": "Material Request",
"transaction_date": nowdate(), "material_request_type": "Purchase",
"company": budget.company, "transaction_date": nowdate(),
"items": [{ "company": budget.company,
'item_code': '_Test Item', "items": [
'qty': 1, {
'uom': "_Test UOM", "item_code": "_Test Item",
'warehouse': '_Test Warehouse - _TC', "qty": 1,
'schedule_date': nowdate(), "uom": "_Test UOM",
'rate': 100000, "warehouse": "_Test Warehouse - _TC",
'expense_account': '_Test Account Cost for Goods Sold - _TC', "schedule_date": nowdate(),
'cost_center': '_Test Cost Center - _TC' "rate": 100000,
}] "expense_account": "_Test Account Cost for Goods Sold - _TC",
}) "cost_center": "_Test Cost Center - _TC",
}
],
}
)
mr.set_missing_values() mr.set_missing_values()
@ -100,11 +131,16 @@ class TestBudget(unittest.TestCase):
budget.cancel() budget.cancel()
def test_monthly_budget_crossed_for_po(self): def test_monthly_budget_crossed_for_po(self):
budget = make_budget(applicable_on_purchase_order=1, budget = make_budget(
action_if_accumulated_monthly_budget_exceeded_on_po="Stop", budget_against="Cost Center") applicable_on_purchase_order=1,
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
budget_against="Cost Center",
)
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value(
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True) po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True)
@ -122,12 +158,20 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value(
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
project = frappe.get_value("Project", {"project_name": "_Test Project"}) project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate()) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
40000,
"_Test Cost Center - _TC",
project=project,
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -139,8 +183,13 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 250000, "_Test Cost Center - _TC", posting_date=nowdate()) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
250000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -153,9 +202,14 @@ class TestBudget(unittest.TestCase):
project = frappe.get_value("Project", {"project_name": "_Test Project"}) project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 250000, "_Test Cost Center - _TC", "_Test Account Cost for Goods Sold - _TC",
project=project, posting_date=nowdate()) "_Test Bank - _TC",
250000,
"_Test Cost Center - _TC",
project=project,
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -169,14 +223,23 @@ class TestBudget(unittest.TestCase):
if month > 9: if month > 9:
month = 9 month = 9
for i in range(month+1): for i in range(month + 1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
20000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
submit=True,
)
self.assertTrue(frappe.db.get_value("GL Entry", self.assertTrue(
{"voucher_type": "Journal Entry", "voucher_no": jv.name})) frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value(
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
self.assertRaises(BudgetError, jv.cancel) self.assertRaises(BudgetError, jv.cancel)
@ -193,14 +256,23 @@ class TestBudget(unittest.TestCase):
project = frappe.get_value("Project", {"project_name": "_Test Project"}) project = frappe.get_value("Project", {"project_name": "_Test Project"})
for i in range(month + 1): for i in range(month + 1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, "_Test Account Cost for Goods Sold - _TC",
project=project) "_Test Bank - _TC",
20000,
"_Test Cost Center - _TC",
posting_date=nowdate(),
submit=True,
project=project,
)
self.assertTrue(frappe.db.get_value("GL Entry", self.assertTrue(
{"voucher_type": "Journal Entry", "voucher_no": jv.name})) frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value(
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
self.assertRaises(BudgetError, jv.cancel) self.assertRaises(BudgetError, jv.cancel)
@ -212,10 +284,17 @@ class TestBudget(unittest.TestCase):
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC") set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC") budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value(
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 40000, "_Test Cost Center 2 - _TC", posting_date=nowdate()) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
40000,
"_Test Cost Center 2 - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -226,19 +305,28 @@ class TestBudget(unittest.TestCase):
cost_center = "_Test Cost Center 3 - _TC" cost_center = "_Test Cost Center 3 - _TC"
if not frappe.db.exists("Cost Center", cost_center): if not frappe.db.exists("Cost Center", cost_center):
frappe.get_doc({ frappe.get_doc(
'doctype': 'Cost Center', {
'cost_center_name': '_Test Cost Center 3', "doctype": "Cost Center",
'parent_cost_center': "_Test Company - _TC", "cost_center_name": "_Test Cost Center 3",
'company': '_Test Company', "parent_cost_center": "_Test Company - _TC",
'is_group': 0 "company": "_Test Company",
}).insert(ignore_permissions=True) "is_group": 0,
}
).insert(ignore_permissions=True)
budget = make_budget(budget_against="Cost Center", cost_center=cost_center) budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value(
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
)
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 40000, cost_center, posting_date=nowdate()) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
40000,
cost_center,
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -255,14 +343,16 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
args = frappe._dict({ args = frappe._dict(
"account": "_Test Account Cost for Goods Sold - _TC", {
"cost_center": "_Test Cost Center - _TC", "account": "_Test Account Cost for Goods Sold - _TC",
"monthly_end_date": posting_date, "cost_center": "_Test Cost Center - _TC",
"company": "_Test Company", "monthly_end_date": posting_date,
"fiscal_year": fiscal_year, "company": "_Test Company",
"budget_against_field": budget_against_field, "fiscal_year": fiscal_year,
}) "budget_against_field": budget_against_field,
}
)
if not args.get(budget_against_field): if not args.get(budget_against_field):
args[budget_against_field] = budget_against args[budget_against_field] = budget_against
@ -271,26 +361,42 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
if existing_expense: if existing_expense:
if budget_against_field == "cost_center": if budget_against_field == "cost_center":
make_journal_entry("_Test Account Cost for Goods Sold - _TC", make_journal_entry(
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
-existing_expense,
"_Test Cost Center - _TC",
posting_date=nowdate(),
submit=True,
)
elif budget_against_field == "project": elif budget_against_field == "project":
make_journal_entry("_Test Account Cost for Goods Sold - _TC", make_journal_entry(
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate()) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
-existing_expense,
"_Test Cost Center - _TC",
submit=True,
project=budget_against,
posting_date=nowdate(),
)
def make_budget(**args): def make_budget(**args):
args = frappe._dict(args) args = frappe._dict(args)
budget_against=args.budget_against budget_against = args.budget_against
cost_center=args.cost_center cost_center = args.cost_center
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
if budget_against == "Project": if budget_against == "Project":
project_name = "{0}%".format("_Test Project/" + fiscal_year) project_name = "{0}%".format("_Test Project/" + fiscal_year)
budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", project_name)}) budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
else: else:
cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year) cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", cost_center_name)}) budget_list = frappe.get_all(
"Budget", fields=["name"], filters={"name": ("like", cost_center_name)}
)
for d in budget_list: for d in budget_list:
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d) frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d) frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
@ -300,7 +406,7 @@ def make_budget(**args):
if budget_against == "Project": if budget_against == "Project":
budget.project = frappe.get_value("Project", {"project_name": "_Test Project"}) budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else: else:
budget.cost_center =cost_center or "_Test Cost Center - _TC" budget.cost_center = cost_center or "_Test Cost Center - _TC"
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution") monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
monthly_distribution.fiscal_year = fiscal_year monthly_distribution.fiscal_year = fiscal_year
@ -312,20 +418,27 @@ def make_budget(**args):
budget.action_if_annual_budget_exceeded = "Stop" budget.action_if_annual_budget_exceeded = "Stop"
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore" budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
budget.budget_against = budget_against budget.budget_against = budget_against
budget.append("accounts", { budget.append(
"account": "_Test Account Cost for Goods Sold - _TC", "accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000}
"budget_amount": 200000 )
})
if args.applicable_on_material_request: if args.applicable_on_material_request:
budget.applicable_on_material_request = 1 budget.applicable_on_material_request = 1
budget.action_if_annual_budget_exceeded_on_mr = args.action_if_annual_budget_exceeded_on_mr or 'Warn' budget.action_if_annual_budget_exceeded_on_mr = (
budget.action_if_accumulated_monthly_budget_exceeded_on_mr = args.action_if_accumulated_monthly_budget_exceeded_on_mr or 'Warn' args.action_if_annual_budget_exceeded_on_mr or "Warn"
)
budget.action_if_accumulated_monthly_budget_exceeded_on_mr = (
args.action_if_accumulated_monthly_budget_exceeded_on_mr or "Warn"
)
if args.applicable_on_purchase_order: if args.applicable_on_purchase_order:
budget.applicable_on_purchase_order = 1 budget.applicable_on_purchase_order = 1
budget.action_if_annual_budget_exceeded_on_po = args.action_if_annual_budget_exceeded_on_po or 'Warn' budget.action_if_annual_budget_exceeded_on_po = (
budget.action_if_accumulated_monthly_budget_exceeded_on_po = args.action_if_accumulated_monthly_budget_exceeded_on_po or 'Warn' args.action_if_annual_budget_exceeded_on_po or "Warn"
)
budget.action_if_accumulated_monthly_budget_exceeded_on_po = (
args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
)
budget.insert() budget.insert()
budget.submit() budget.submit()

View File

@ -11,28 +11,42 @@ from frappe.utils import flt
class CForm(Document): class CForm(Document):
def validate(self): def validate(self):
"""Validate invoice that c-form is applicable """Validate invoice that c-form is applicable
and no other c-form is received for that""" and no other c-form is received for that"""
for d in self.get('invoices'): for d in self.get("invoices"):
if d.invoice_no: if d.invoice_no:
inv = frappe.db.sql("""select c_form_applicable, c_form_no from inv = frappe.db.sql(
`tabSales Invoice` where name = %s and docstatus = 1""", d.invoice_no) """select c_form_applicable, c_form_no from
`tabSales Invoice` where name = %s and docstatus = 1""",
d.invoice_no,
)
if inv and inv[0][0] != 'Yes': if inv and inv[0][0] != "Yes":
frappe.throw(_("C-form is not applicable for Invoice: {0}").format(d.invoice_no)) frappe.throw(_("C-form is not applicable for Invoice: {0}").format(d.invoice_no))
elif inv and inv[0][1] and inv[0][1] != self.name: elif inv and inv[0][1] and inv[0][1] != self.name:
frappe.throw(_("""Invoice {0} is tagged in another C-form: {1}. frappe.throw(
_(
"""Invoice {0} is tagged in another C-form: {1}.
If you want to change C-form no for this invoice, If you want to change C-form no for this invoice,
please remove invoice no from the previous c-form and then try again"""\ please remove invoice no from the previous c-form and then try again""".format(
.format(d.invoice_no, inv[0][1]))) d.invoice_no, inv[0][1]
)
)
)
elif not inv: elif not inv:
frappe.throw(_("Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \ frappe.throw(
Please enter a valid Invoice".format(d.idx, d.invoice_no))) _(
"Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \
Please enter a valid Invoice".format(
d.idx, d.invoice_no
)
)
)
def on_update(self): def on_update(self):
""" Update C-Form No on invoices""" """Update C-Form No on invoices"""
self.set_total_invoiced_amount() self.set_total_invoiced_amount()
def on_submit(self): def on_submit(self):
@ -43,30 +57,40 @@ class CForm(Document):
frappe.db.sql("""update `tabSales Invoice` set c_form_no=null where c_form_no=%s""", self.name) frappe.db.sql("""update `tabSales Invoice` set c_form_no=null where c_form_no=%s""", self.name)
def set_cform_in_sales_invoices(self): def set_cform_in_sales_invoices(self):
inv = [d.invoice_no for d in self.get('invoices')] inv = [d.invoice_no for d in self.get("invoices")]
if inv: if inv:
frappe.db.sql("""update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)""" % frappe.db.sql(
('%s', '%s', ', '.join(['%s'] * len(inv))), tuple([self.name, self.modified] + inv)) """update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)"""
% ("%s", "%s", ", ".join(["%s"] * len(inv))),
tuple([self.name, self.modified] + inv),
)
frappe.db.sql("""update `tabSales Invoice` set c_form_no = null, modified = %s frappe.db.sql(
where name not in (%s) and ifnull(c_form_no, '') = %s""" % """update `tabSales Invoice` set c_form_no = null, modified = %s
('%s', ', '.join(['%s']*len(inv)), '%s'), tuple([self.modified] + inv + [self.name])) where name not in (%s) and ifnull(c_form_no, '') = %s"""
% ("%s", ", ".join(["%s"] * len(inv)), "%s"),
tuple([self.modified] + inv + [self.name]),
)
else: else:
frappe.throw(_("Please enter atleast 1 invoice in the table")) frappe.throw(_("Please enter atleast 1 invoice in the table"))
def set_total_invoiced_amount(self): def set_total_invoiced_amount(self):
total = sum(flt(d.grand_total) for d in self.get('invoices')) total = sum(flt(d.grand_total) for d in self.get("invoices"))
frappe.db.set(self, 'total_invoiced_amount', total) frappe.db.set(self, "total_invoiced_amount", total)
@frappe.whitelist() @frappe.whitelist()
def get_invoice_details(self, invoice_no): def get_invoice_details(self, invoice_no):
""" Pull details from invoices for referrence """ """Pull details from invoices for referrence"""
if invoice_no: if invoice_no:
inv = frappe.db.get_value("Sales Invoice", invoice_no, inv = frappe.db.get_value(
["posting_date", "territory", "base_net_total", "base_grand_total"], as_dict=True) "Sales Invoice",
invoice_no,
["posting_date", "territory", "base_net_total", "base_grand_total"],
as_dict=True,
)
return { return {
'invoice_date' : inv.posting_date, "invoice_date": inv.posting_date,
'territory' : inv.territory, "territory": inv.territory,
'net_total' : inv.base_net_total, "net_total": inv.base_net_total,
'grand_total' : inv.base_grand_total "grand_total": inv.base_grand_total,
} }

View File

@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('C-Form') # test_records = frappe.get_test_records('C-Form')
class TestCForm(unittest.TestCase): class TestCForm(unittest.TestCase):
pass pass

View File

@ -1,25 +1,25 @@
DEFAULT_MAPPERS = [ DEFAULT_MAPPERS = [
{ {
'doctype': 'Cash Flow Mapper', "doctype": "Cash Flow Mapper",
'section_footer': 'Net cash generated by operating activities', "section_footer": "Net cash generated by operating activities",
'section_header': 'Cash flows from operating activities', "section_header": "Cash flows from operating activities",
'section_leader': 'Adjustments for', "section_leader": "Adjustments for",
'section_name': 'Operating Activities', "section_name": "Operating Activities",
'position': 0, "position": 0,
'section_subtotal': 'Cash generated from operations', "section_subtotal": "Cash generated from operations",
}, },
{ {
'doctype': 'Cash Flow Mapper', "doctype": "Cash Flow Mapper",
'position': 1, "position": 1,
'section_footer': 'Net cash used in investing activities', "section_footer": "Net cash used in investing activities",
'section_header': 'Cash flows from investing activities', "section_header": "Cash flows from investing activities",
'section_name': 'Investing Activities' "section_name": "Investing Activities",
}, },
{ {
'doctype': 'Cash Flow Mapper', "doctype": "Cash Flow Mapper",
'position': 2, "position": 2,
'section_footer': 'Net cash used in financing activites', "section_footer": "Net cash used in financing activites",
'section_header': 'Cash flows from financing activities', "section_header": "Cash flows from financing activities",
'section_name': 'Financing Activities', "section_name": "Financing Activities",
} },
] ]

View File

@ -11,9 +11,11 @@ class CashFlowMapping(Document):
self.validate_checked_options() self.validate_checked_options()
def validate_checked_options(self): def validate_checked_options(self):
checked_fields = [d for d in self.meta.fields if d.fieldtype == 'Check' and self.get(d.fieldname) == 1] checked_fields = [
d for d in self.meta.fields if d.fieldtype == "Check" and self.get(d.fieldname) == 1
]
if len(checked_fields) > 1: if len(checked_fields) > 1:
frappe.throw( frappe.throw(
frappe._('You can only select a maximum of one option from the list of check boxes.'), frappe._("You can only select a maximum of one option from the list of check boxes."),
title='Error' title="Error",
) )

View File

@ -9,19 +9,16 @@ import frappe
class TestCashFlowMapping(unittest.TestCase): class TestCashFlowMapping(unittest.TestCase):
def setUp(self): def setUp(self):
if frappe.db.exists("Cash Flow Mapping", "Test Mapping"): if frappe.db.exists("Cash Flow Mapping", "Test Mapping"):
frappe.delete_doc('Cash Flow Mappping', 'Test Mapping') frappe.delete_doc("Cash Flow Mappping", "Test Mapping")
def tearDown(self): def tearDown(self):
frappe.delete_doc('Cash Flow Mapping', 'Test Mapping') frappe.delete_doc("Cash Flow Mapping", "Test Mapping")
def test_multiple_selections_not_allowed(self): def test_multiple_selections_not_allowed(self):
doc = frappe.new_doc('Cash Flow Mapping') doc = frappe.new_doc("Cash Flow Mapping")
doc.mapping_name = 'Test Mapping' doc.mapping_name = "Test Mapping"
doc.label = 'Test label' doc.label = "Test label"
doc.append( doc.append("accounts", {"account": "Accounts Receivable - _TC"})
'accounts',
{'account': 'Accounts Receivable - _TC'}
)
doc.is_working_capital = 1 doc.is_working_capital = 1
doc.is_finance_cost = 1 doc.is_finance_cost = 1

View File

@ -17,11 +17,14 @@ class CashierClosing(Document):
self.make_calculations() self.make_calculations()
def get_outstanding(self): def get_outstanding(self):
values = frappe.db.sql(""" values = frappe.db.sql(
"""
select sum(outstanding_amount) select sum(outstanding_amount)
from `tabSales Invoice` from `tabSales Invoice`
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
""", (self.date, self.from_time, self.time, self.user)) """,
(self.date, self.from_time, self.time, self.user),
)
self.outstanding_amount = flt(values[0][0] if values else 0) self.outstanding_amount = flt(values[0][0] if values else 0)
def make_calculations(self): def make_calculations(self):
@ -29,7 +32,9 @@ class CashierClosing(Document):
for i in self.payments: for i in self.payments:
total += flt(i.amount) total += flt(i.amount)
self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns) self.net_amount = (
total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
)
def validate_time(self): def validate_time(self):
if self.from_time >= self.time: if self.from_time >= self.time:

View File

@ -25,33 +25,41 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
class ChartofAccountsImporter(Document): class ChartofAccountsImporter(Document):
def validate(self): def validate(self):
if self.import_file: if self.import_file:
get_coa('Chart of Accounts Importer', 'All Accounts', file_name=self.import_file, for_validate=1) get_coa(
"Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1
)
def validate_columns(data): def validate_columns(data):
if not data: if not data:
frappe.throw(_('No data found. Seems like you uploaded a blank file')) frappe.throw(_("No data found. Seems like you uploaded a blank file"))
no_of_columns = max([len(d) for d in data]) no_of_columns = max([len(d) for d in data])
if no_of_columns > 7: if no_of_columns > 7:
frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'), frappe.throw(
title=(_("Wrong Template"))) _("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")),
)
@frappe.whitelist() @frappe.whitelist()
def validate_company(company): def validate_company(company):
parent_company, allow_account_creation_against_child_company = frappe.db.get_value('Company', parent_company, allow_account_creation_against_child_company = frappe.db.get_value(
{'name': company}, ['parent_company', "Company", {"name": company}, ["parent_company", "allow_account_creation_against_child_company"]
'allow_account_creation_against_child_company']) )
if parent_company and (not allow_account_creation_against_child_company): if parent_company and (not allow_account_creation_against_child_company):
msg = _("{} is a child company.").format(frappe.bold(company)) + " " msg = _("{} is a child company.").format(frappe.bold(company)) + " "
msg += _("Please import accounts against parent company or enable {} in company master.").format( msg += _("Please import accounts against parent company or enable {} in company master.").format(
frappe.bold('Allow Account Creation Against Child Company')) frappe.bold("Allow Account Creation Against Child Company")
frappe.throw(msg, title=_('Wrong Company')) )
frappe.throw(msg, title=_("Wrong Company"))
if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1): if frappe.db.get_all("GL Entry", {"company": company}, "name", limit=1):
return False return False
@frappe.whitelist() @frappe.whitelist()
def import_coa(file_name, company): def import_coa(file_name, company):
# delete existing data for accounts # delete existing data for accounts
@ -60,7 +68,7 @@ def import_coa(file_name, company):
# create accounts # create accounts
file_doc, extension = get_file(file_name) file_doc, extension = get_file(file_name)
if extension == 'csv': if extension == "csv":
data = generate_data_from_csv(file_doc) data = generate_data_from_csv(file_doc)
else: else:
data = generate_data_from_excel(file_doc, extension) data = generate_data_from_excel(file_doc, extension)
@ -72,27 +80,33 @@ def import_coa(file_name, company):
# trigger on_update for company to reset default accounts # trigger on_update for company to reset default accounts
set_default_accounts(company) set_default_accounts(company)
def get_file(file_name): def get_file(file_name):
file_doc = frappe.get_doc("File", {"file_url": file_name}) file_doc = frappe.get_doc("File", {"file_url": file_name})
parts = file_doc.get_extension() parts = file_doc.get_extension()
extension = parts[1] extension = parts[1]
extension = extension.lstrip(".") extension = extension.lstrip(".")
if extension not in ('csv', 'xlsx', 'xls'): if extension not in ("csv", "xlsx", "xls"):
frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload")) frappe.throw(
_(
"Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"
)
)
return file_doc, extension
return file_doc, extension
def generate_data_from_csv(file_doc, as_dict=False): def generate_data_from_csv(file_doc, as_dict=False):
''' read csv file and return the generated nested tree ''' """read csv file and return the generated nested tree"""
file_path = file_doc.get_full_path() file_path = file_doc.get_full_path()
data = [] data = []
with open(file_path, 'r') as in_file: with open(file_path, "r") as in_file:
csv_reader = list(csv.reader(in_file)) csv_reader = list(csv.reader(in_file))
headers = csv_reader[0] headers = csv_reader[0]
del csv_reader[0] # delete top row and headers row del csv_reader[0] # delete top row and headers row
for row in csv_reader: for row in csv_reader:
if as_dict: if as_dict:
@ -106,6 +120,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
# convert csv data # convert csv data
return data return data
def generate_data_from_excel(file_doc, extension, as_dict=False): def generate_data_from_excel(file_doc, extension, as_dict=False):
content = file_doc.get_content() content = file_doc.get_content()
@ -123,20 +138,21 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else: else:
if not row[1]: if not row[1]:
row[1] = row[0] row[1] = row[0]
row[3] = row[2] row[3] = row[2]
data.append(row) data.append(row)
return data return data
@frappe.whitelist() @frappe.whitelist()
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0): def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
''' called by tree view (to fetch node's children) ''' """called by tree view (to fetch node's children)"""
file_doc, extension = get_file(file_name) file_doc, extension = get_file(file_name)
parent = None if parent==_('All Accounts') else parent parent = None if parent == _("All Accounts") else parent
if extension == 'csv': if extension == "csv":
data = generate_data_from_csv(file_doc) data = generate_data_from_csv(file_doc)
else: else:
data = generate_data_from_excel(file_doc, extension) data = generate_data_from_excel(file_doc, extension)
@ -146,32 +162,33 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
if not for_validate: if not for_validate:
forest = build_forest(data) forest = build_forest(data)
accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form accounts = build_tree_from_json(
"", chart_data=forest, from_coa_importer=True
) # returns a list of dict in a tree render-able form
# filter out to show data for the selected node only # filter out to show data for the selected node only
accounts = [d for d in accounts if d['parent_account']==parent] accounts = [d for d in accounts if d["parent_account"] == parent]
return accounts return accounts
else: else:
return { return {"show_import_button": 1}
'show_import_button': 1
}
def build_forest(data): def build_forest(data):
''' """
converts list of list into a nested tree converts list of list into a nested tree
if a = [[1,1], [1,2], [3,2], [4,4], [5,4]] if a = [[1,1], [1,2], [3,2], [4,4], [5,4]]
tree = { tree = {
1: { 1: {
2: { 2: {
3: {} 3: {}
} }
}, },
4: { 4: {
5: {} 5: {}
} }
} }
''' """
# set the value of nested dictionary # set the value of nested dictionary
def set_nested(d, path, value): def set_nested(d, path, value):
@ -195,8 +212,11 @@ def build_forest(data):
elif account_name == child: elif account_name == child:
parent_account_list = return_parent(data, parent_account) parent_account_list = return_parent(data, parent_account)
if not parent_account_list and parent_account: if not parent_account_list and parent_account:
frappe.throw(_("The parent account {0} does not exists in the uploaded template").format( frappe.throw(
frappe.bold(parent_account))) _("The parent account {0} does not exists in the uploaded template").format(
frappe.bold(parent_account)
)
)
return [child] + parent_account_list return [child] + parent_account_list
charts_map, paths = {}, [] charts_map, paths = {}, []
@ -205,7 +225,15 @@ def build_forest(data):
error_messages = [] error_messages = []
for i in data: for i in data:
account_name, parent_account, account_number, parent_account_number, is_group, account_type, root_type = i (
account_name,
parent_account,
account_number,
parent_account_number,
is_group,
account_type,
root_type,
) = i
if not account_name: if not account_name:
error_messages.append("Row {0}: Please enter Account Name".format(line_no)) error_messages.append("Row {0}: Please enter Account Name".format(line_no))
@ -216,13 +244,17 @@ def build_forest(data):
account_name = "{} - {}".format(account_number, account_name) account_name = "{} - {}".format(account_number, account_name)
charts_map[account_name] = {} charts_map[account_name] = {}
charts_map[account_name]['account_name'] = name charts_map[account_name]["account_name"] = name
if account_number: charts_map[account_name]["account_number"] = account_number if account_number:
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group charts_map[account_name]["account_number"] = account_number
if account_type: charts_map[account_name]["account_type"] = account_type if cint(is_group) == 1:
if root_type: charts_map[account_name]["root_type"] = root_type charts_map[account_name]["is_group"] = is_group
if account_type:
charts_map[account_name]["account_type"] = account_type
if root_type:
charts_map[account_name]["root_type"] = root_type
path = return_parent(data, account_name)[::-1] path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created paths.append(path) # List of path is created
line_no += 1 line_no += 1
if error_messages: if error_messages:
@ -231,27 +263,32 @@ def build_forest(data):
out = {} out = {}
for path in paths: for path in paths:
for n, account_name in enumerate(path): for n, account_name in enumerate(path):
set_nested(out, path[:n+1], charts_map[account_name]) # setting the value of nested dictionary. set_nested(
out, path[: n + 1], charts_map[account_name]
) # setting the value of nested dictionary.
return out return out
def build_response_as_excel(writer): def build_response_as_excel(writer):
filename = frappe.generate_hash("", 10) filename = frappe.generate_hash("", 10)
with open(filename, 'wb') as f: with open(filename, "wb") as f:
f.write(cstr(writer.getvalue()).encode('utf-8')) f.write(cstr(writer.getvalue()).encode("utf-8"))
f = open(filename) f = open(filename)
reader = csv.reader(f) reader = csv.reader(f)
from frappe.utils.xlsxutils import make_xlsx from frappe.utils.xlsxutils import make_xlsx
xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template") xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template")
f.close() f.close()
os.remove(filename) os.remove(filename)
# write out response as a xlsx type # write out response as a xlsx type
frappe.response['filename'] = 'coa_importer_template.xlsx' frappe.response["filename"] = "coa_importer_template.xlsx"
frappe.response['filecontent'] = xlsx_file.getvalue() frappe.response["filecontent"] = xlsx_file.getvalue()
frappe.response['type'] = 'binary' frappe.response["type"] = "binary"
@frappe.whitelist() @frappe.whitelist()
def download_template(file_type, template_type): def download_template(file_type, template_type):
@ -259,34 +296,46 @@ def download_template(file_type, template_type):
writer = get_template(template_type) writer = get_template(template_type)
if file_type == 'CSV': if file_type == "CSV":
# download csv file # download csv file
frappe.response['result'] = cstr(writer.getvalue()) frappe.response["result"] = cstr(writer.getvalue())
frappe.response['type'] = 'csv' frappe.response["type"] = "csv"
frappe.response['doctype'] = 'Chart of Accounts Importer' frappe.response["doctype"] = "Chart of Accounts Importer"
else: else:
build_response_as_excel(writer) build_response_as_excel(writer)
def get_template(template_type): def get_template(template_type):
fields = ["Account Name", "Parent Account", "Account Number", "Parent Account Number", "Is Group", "Account Type", "Root Type"] fields = [
"Account Name",
"Parent Account",
"Account Number",
"Parent Account Number",
"Is Group",
"Account Type",
"Root Type",
]
writer = UnicodeWriter() writer = UnicodeWriter()
writer.writerow(fields) writer.writerow(fields)
if template_type == 'Blank Template': if template_type == "Blank Template":
for root_type in get_root_types(): for root_type in get_root_types():
writer.writerow(['', '', '', 1, '', root_type]) writer.writerow(["", "", "", 1, "", root_type])
for account in get_mandatory_group_accounts(): for account in get_mandatory_group_accounts():
writer.writerow(['', '', '', 1, account, "Asset"]) writer.writerow(["", "", "", 1, account, "Asset"])
for account_type in get_mandatory_account_types(): for account_type in get_mandatory_account_types():
writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')]) writer.writerow(
["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
)
else: else:
writer = get_sample_template(writer) writer = get_sample_template(writer)
return writer return writer
def get_sample_template(writer): def get_sample_template(writer):
template = [ template = [
["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"], ["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"],
@ -316,7 +365,7 @@ def get_sample_template(writer):
@frappe.whitelist() @frappe.whitelist()
def validate_accounts(file_doc, extension): def validate_accounts(file_doc, extension):
if extension == 'csv': if extension == "csv":
accounts = generate_data_from_csv(file_doc, as_dict=True) accounts = generate_data_from_csv(file_doc, as_dict=True)
else: else:
accounts = generate_data_from_excel(file_doc, extension, as_dict=True) accounts = generate_data_from_excel(file_doc, extension, as_dict=True)
@ -325,7 +374,9 @@ def validate_accounts(file_doc, extension):
for account in accounts: for account in accounts:
accounts_dict.setdefault(account["account_name"], account) accounts_dict.setdefault(account["account_name"], account)
if "parent_account" not in account: if "parent_account" not in account:
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.") msg = _(
"Please make sure the file you are using has 'Parent Account' column present in the header."
)
msg += "<br><br>" msg += "<br><br>"
msg += _("Alternatively, you can download the template and fill your data in.") msg += _("Alternatively, you can download the template and fill your data in.")
frappe.throw(msg, title=_("Parent Account Missing")) frappe.throw(msg, title=_("Parent Account Missing"))
@ -336,77 +387,106 @@ def validate_accounts(file_doc, extension):
return [True, len(accounts)] return [True, len(accounts)]
def validate_root(accounts): def validate_root(accounts):
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')] roots = [accounts[d] for d in accounts if not accounts[d].get("parent_account")]
error_messages = [] error_messages = []
for account in roots: for account in roots:
if not account.get("root_type") and account.get("account_name"): if not account.get("root_type") and account.get("account_name"):
error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name"))) error_messages.append(
_("Please enter Root Type for account- {0}").format(account.get("account_name"))
)
elif account.get("root_type") not in get_root_types() and account.get("account_name"): elif account.get("root_type") not in get_root_types() and account.get("account_name"):
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name"))) error_messages.append(
_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(
account.get("account_name")
)
)
validate_missing_roots(roots) validate_missing_roots(roots)
if error_messages: if error_messages:
frappe.throw("<br>".join(error_messages)) frappe.throw("<br>".join(error_messages))
def validate_missing_roots(roots): def validate_missing_roots(roots):
root_types_added = set(d.get('root_type') for d in roots) root_types_added = set(d.get("root_type") for d in roots)
missing = list(set(get_root_types()) - root_types_added) missing = list(set(get_root_types()) - root_types_added)
if missing: if missing:
frappe.throw(_("Please add Root Account for - {0}").format(' , '.join(missing))) frappe.throw(_("Please add Root Account for - {0}").format(" , ".join(missing)))
def get_root_types(): def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity') return ("Asset", "Liability", "Expense", "Income", "Equity")
def get_report_type(root_type): def get_report_type(root_type):
if root_type in ('Asset', 'Liability', 'Equity'): if root_type in ("Asset", "Liability", "Equity"):
return 'Balance Sheet' return "Balance Sheet"
else: else:
return 'Profit and Loss' return "Profit and Loss"
def get_mandatory_group_accounts(): def get_mandatory_group_accounts():
return ('Bank', 'Cash', 'Stock') return ("Bank", "Cash", "Stock")
def get_mandatory_account_types(): def get_mandatory_account_types():
return [ return [
{'account_type': 'Cost of Goods Sold', 'root_type': 'Expense'}, {"account_type": "Cost of Goods Sold", "root_type": "Expense"},
{'account_type': 'Depreciation', 'root_type': 'Expense'}, {"account_type": "Depreciation", "root_type": "Expense"},
{'account_type': 'Fixed Asset', 'root_type': 'Asset'}, {"account_type": "Fixed Asset", "root_type": "Asset"},
{'account_type': 'Payable', 'root_type': 'Liability'}, {"account_type": "Payable", "root_type": "Liability"},
{'account_type': 'Receivable', 'root_type': 'Asset'}, {"account_type": "Receivable", "root_type": "Asset"},
{'account_type': 'Stock Adjustment', 'root_type': 'Expense'}, {"account_type": "Stock Adjustment", "root_type": "Expense"},
{'account_type': 'Bank', 'root_type': 'Asset'}, {"account_type": "Bank", "root_type": "Asset"},
{'account_type': 'Cash', 'root_type': 'Asset'}, {"account_type": "Cash", "root_type": "Asset"},
{'account_type': 'Stock', 'root_type': 'Asset'} {"account_type": "Stock", "root_type": "Asset"},
] ]
def unset_existing_data(company): def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField linked = frappe.db.sql(
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True) '''select fieldname from tabDocField
where fieldtype="Link" and options="Account" and parent="Company"''',
as_dict=True,
)
# remove accounts data from company # remove accounts data from company
update_values = {d.fieldname: '' for d in linked} update_values = {d.fieldname: "" for d in linked}
frappe.db.set_value('Company', company, update_values, update_values) frappe.db.set_value("Company", company, update_values, update_values)
# remove accounts data from various doctypes # remove accounts data from various doctypes
for doctype in ["Account", "Party Account", "Mode of Payment Account", "Tax Withholding Account", for doctype in [
"Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: "Account",
frappe.db.sql('''delete from `tab{0}` where `company`="%s"''' # nosec "Party Account",
.format(doctype) % (company)) "Mode of Payment Account",
"Tax Withholding Account",
"Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template",
]:
frappe.db.sql(
'''delete from `tab{0}` where `company`="%s"'''.format(doctype) % (company) # nosec
)
def set_default_accounts(company): def set_default_accounts(company):
from erpnext.setup.doctype.company.company import install_country_fixtures from erpnext.setup.doctype.company.company import install_country_fixtures
company = frappe.get_doc('Company', company)
company.update({ company = frappe.get_doc("Company", company)
"default_receivable_account": frappe.db.get_value("Account", company.update(
{"company": company.name, "account_type": "Receivable", "is_group": 0}), {
"default_payable_account": frappe.db.get_value("Account", "default_receivable_account": frappe.db.get_value(
{"company": company.name, "account_type": "Payable", "is_group": 0}) "Account", {"company": company.name, "account_type": "Receivable", "is_group": 0}
}) ),
"default_payable_account": frappe.db.get_value(
"Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
),
}
)
company.save() company.save()
install_country_fixtures(company.name, company.country) install_country_fixtures(company.name, company.country)

View File

@ -10,17 +10,20 @@ from frappe.model.document import Document
class ChequePrintTemplate(Document): class ChequePrintTemplate(Document):
pass pass
@frappe.whitelist() @frappe.whitelist()
def create_or_update_cheque_print_format(template_name): def create_or_update_cheque_print_format(template_name):
if not frappe.db.exists("Print Format", template_name): if not frappe.db.exists("Print Format", template_name):
cheque_print = frappe.new_doc("Print Format") cheque_print = frappe.new_doc("Print Format")
cheque_print.update({ cheque_print.update(
"doc_type": "Payment Entry", {
"standard": "No", "doc_type": "Payment Entry",
"custom_format": 1, "standard": "No",
"print_format_type": "Jinja", "custom_format": 1,
"name": template_name "print_format_type": "Jinja",
}) "name": template_name,
}
)
else: else:
cheque_print = frappe.get_doc("Print Format", template_name) cheque_print = frappe.get_doc("Print Format", template_name)
@ -69,10 +72,12 @@ def create_or_update_cheque_print_format(template_name):
{{doc.company}} {{doc.company}}
</span> </span>
</div> </div>
</div>"""%{ </div>""" % {
"starting_position_from_top_edge": doc.starting_position_from_top_edge \ "starting_position_from_top_edge": doc.starting_position_from_top_edge
if doc.cheque_size == "A4" else 0.0, if doc.cheque_size == "A4"
"cheque_width": doc.cheque_width, "cheque_height": doc.cheque_height, else 0.0,
"cheque_width": doc.cheque_width,
"cheque_height": doc.cheque_height,
"acc_pay_dist_from_top_edge": doc.acc_pay_dist_from_top_edge, "acc_pay_dist_from_top_edge": doc.acc_pay_dist_from_top_edge,
"acc_pay_dist_from_left_edge": doc.acc_pay_dist_from_left_edge, "acc_pay_dist_from_left_edge": doc.acc_pay_dist_from_left_edge,
"message_to_show": doc.message_to_show if doc.message_to_show else _("Account Pay Only"), "message_to_show": doc.message_to_show if doc.message_to_show else _("Account Pay Only"),
@ -89,7 +94,7 @@ def create_or_update_cheque_print_format(template_name):
"amt_in_figures_from_top_edge": doc.amt_in_figures_from_top_edge, "amt_in_figures_from_top_edge": doc.amt_in_figures_from_top_edge,
"amt_in_figures_from_left_edge": doc.amt_in_figures_from_left_edge, "amt_in_figures_from_left_edge": doc.amt_in_figures_from_left_edge,
"signatory_from_top_edge": doc.signatory_from_top_edge, "signatory_from_top_edge": doc.signatory_from_top_edge,
"signatory_from_left_edge": doc.signatory_from_left_edge "signatory_from_left_edge": doc.signatory_from_left_edge,
} }
cheque_print.save(ignore_permissions=True) cheque_print.save(ignore_permissions=True)

View File

@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Cheque Print Template') # test_records = frappe.get_test_records('Cheque Print Template')
class TestChequePrintTemplate(unittest.TestCase): class TestChequePrintTemplate(unittest.TestCase):
pass pass

View File

@ -10,11 +10,14 @@ from erpnext.accounts.utils import validate_field_number
class CostCenter(NestedSet): class CostCenter(NestedSet):
nsm_parent_field = 'parent_cost_center' nsm_parent_field = "parent_cost_center"
def autoname(self): def autoname(self):
from erpnext.accounts.utils import get_autoname_with_number from erpnext.accounts.utils import get_autoname_with_number
self.name = get_autoname_with_number(self.cost_center_number, self.cost_center_name, None, self.company)
self.name = get_autoname_with_number(
self.cost_center_number, self.cost_center_name, None, self.company
)
def validate(self): def validate(self):
self.validate_mandatory() self.validate_mandatory()
@ -28,9 +31,12 @@ class CostCenter(NestedSet):
def validate_parent_cost_center(self): def validate_parent_cost_center(self):
if self.parent_cost_center: if self.parent_cost_center:
if not frappe.db.get_value('Cost Center', self.parent_cost_center, 'is_group'): if not frappe.db.get_value("Cost Center", self.parent_cost_center, "is_group"):
frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format( frappe.throw(
frappe.bold(self.parent_cost_center))) _("{0} is not a group node. Please select a group node as parent cost center").format(
frappe.bold(self.parent_cost_center)
)
)
@frappe.whitelist() @frappe.whitelist()
def convert_group_to_ledger(self): def convert_group_to_ledger(self):
@ -48,7 +54,9 @@ class CostCenter(NestedSet):
if self.if_allocation_exists_against_cost_center(): if self.if_allocation_exists_against_cost_center():
frappe.throw(_("Cost Center with Allocation records can not be converted to a group")) frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
if self.check_if_part_of_cost_center_allocation(): if self.check_if_part_of_cost_center_allocation():
frappe.throw(_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group")) frappe.throw(
_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group")
)
if self.check_gle_exists(): if self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to group")) frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
self.is_group = 1 self.is_group = 1
@ -59,24 +67,26 @@ class CostCenter(NestedSet):
return frappe.db.get_value("GL Entry", {"cost_center": self.name}) return frappe.db.get_value("GL Entry", {"cost_center": self.name})
def check_if_child_exists(self): def check_if_child_exists(self):
return frappe.db.sql("select name from `tabCost Center` where \ return frappe.db.sql(
parent_cost_center = %s and docstatus != 2", self.name) "select name from `tabCost Center` where \
parent_cost_center = %s and docstatus != 2",
self.name,
)
def if_allocation_exists_against_cost_center(self): def if_allocation_exists_against_cost_center(self):
return frappe.db.get_value("Cost Center Allocation", filters = { return frappe.db.get_value(
"main_cost_center": self.name, "Cost Center Allocation", filters={"main_cost_center": self.name, "docstatus": 1}
"docstatus": 1 )
})
def check_if_part_of_cost_center_allocation(self): def check_if_part_of_cost_center_allocation(self):
return frappe.db.get_value("Cost Center Allocation Percentage", filters = { return frappe.db.get_value(
"cost_center": self.name, "Cost Center Allocation Percentage", filters={"cost_center": self.name, "docstatus": 1}
"docstatus": 1 )
})
def before_rename(self, olddn, newdn, merge=False): def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided # Add company abbr if not provided
from erpnext.setup.doctype.company.company import get_name_with_abbr from erpnext.setup.doctype.company.company import get_name_with_abbr
new_cost_center = get_name_with_abbr(newdn, self.company) new_cost_center = get_name_with_abbr(newdn, self.company)
# Validate properties before merging # Validate properties before merging
@ -90,7 +100,9 @@ class CostCenter(NestedSet):
super(CostCenter, self).after_rename(olddn, newdn, merge) super(CostCenter, self).after_rename(olddn, newdn, merge)
if not merge: if not merge:
new_cost_center = frappe.db.get_value("Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1) new_cost_center = frappe.db.get_value(
"Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1
)
# exclude company abbr # exclude company abbr
new_parts = newdn.split(" - ")[:-1] new_parts = newdn.split(" - ")[:-1]
@ -99,7 +111,9 @@ class CostCenter(NestedSet):
if len(new_parts) == 1: if len(new_parts) == 1:
new_parts = newdn.split(" ") new_parts = newdn.split(" ")
if new_cost_center.cost_center_number != new_parts[0]: if new_cost_center.cost_center_number != new_parts[0]:
validate_field_number("Cost Center", self.name, new_parts[0], self.company, "cost_center_number") validate_field_number(
"Cost Center", self.name, new_parts[0], self.company, "cost_center_number"
)
self.cost_center_number = new_parts[0] self.cost_center_number = new_parts[0]
self.db_set("cost_center_number", new_parts[0]) self.db_set("cost_center_number", new_parts[0])
new_parts = new_parts[1:] new_parts = new_parts[1:]
@ -110,10 +124,12 @@ class CostCenter(NestedSet):
self.cost_center_name = cost_center_name self.cost_center_name = cost_center_name
self.db_set("cost_center_name", cost_center_name) self.db_set("cost_center_name", cost_center_name)
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Cost Center", ["lft", "rgt"]) frappe.db.add_index("Cost Center", ["lft", "rgt"])
def get_name_with_number(new_account, account_number): def get_name_with_number(new_account, account_number):
if account_number and not new_account[0].isdigit(): if account_number and not new_account[0].isdigit():
new_account = account_number + " - " + new_account new_account = account_number + " - " + new_account
return new_account return new_account

View File

@ -3,11 +3,6 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'cost_center', "fieldname": "cost_center",
'reports': [ "reports": [{"label": _("Reports"), "items": ["Budget Variance Report", "General Ledger"]}],
{
'label': _('Reports'),
'items': ['Budget Variance Report', 'General Ledger']
}
]
} }

View File

@ -5,24 +5,28 @@ import unittest
import frappe import frappe
test_records = frappe.get_test_records('Cost Center') test_records = frappe.get_test_records("Cost Center")
class TestCostCenter(unittest.TestCase): class TestCostCenter(unittest.TestCase):
def test_cost_center_creation_against_child_node(self): def test_cost_center_creation_against_child_node(self):
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}): if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}):
frappe.get_doc(test_records[1]).insert() frappe.get_doc(test_records[1]).insert()
cost_center = frappe.get_doc({ cost_center = frappe.get_doc(
'doctype': 'Cost Center', {
'cost_center_name': '_Test Cost Center 3', "doctype": "Cost Center",
'parent_cost_center': '_Test Cost Center 2 - _TC', "cost_center_name": "_Test Cost Center 3",
'is_group': 0, "parent_cost_center": "_Test Cost Center 2 - _TC",
'company': '_Test Company' "is_group": 0,
}) "company": "_Test Company",
}
)
self.assertRaises(frappe.ValidationError, cost_center.save) self.assertRaises(frappe.ValidationError, cost_center.save)
def create_cost_center(**args): def create_cost_center(**args):
args = frappe._dict(args) args = frappe._dict(args)
if args.cost_center_name: if args.cost_center_name:

View File

@ -9,15 +9,24 @@ from frappe.utils import add_days, format_date, getdate
class MainCostCenterCantBeChild(frappe.ValidationError): class MainCostCenterCantBeChild(frappe.ValidationError):
pass pass
class InvalidMainCostCenter(frappe.ValidationError): class InvalidMainCostCenter(frappe.ValidationError):
pass pass
class InvalidChildCostCenter(frappe.ValidationError): class InvalidChildCostCenter(frappe.ValidationError):
pass pass
class WrongPercentageAllocation(frappe.ValidationError): class WrongPercentageAllocation(frappe.ValidationError):
pass pass
class InvalidDateError(frappe.ValidationError): class InvalidDateError(frappe.ValidationError):
pass pass
class CostCenterAllocation(Document): class CostCenterAllocation(Document):
def validate(self): def validate(self):
self.validate_total_allocation_percentage() self.validate_total_allocation_percentage()
@ -30,61 +39,96 @@ class CostCenterAllocation(Document):
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])]) total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
if total_percentage != 100: if total_percentage != 100:
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation) frappe.throw(
_("Total percentage against cost centers should be 100"), WrongPercentageAllocation
)
def validate_from_date_based_on_existing_gle(self): def validate_from_date_based_on_existing_gle(self):
# Check if GLE exists against the main cost center # Check if GLE exists against the main cost center
# If exists ensure from date is set after posting date of last GLE # If exists ensure from date is set after posting date of last GLE
last_gle_date = frappe.db.get_value("GL Entry", last_gle_date = frappe.db.get_value(
"GL Entry",
{"cost_center": self.main_cost_center, "is_cancelled": 0}, {"cost_center": self.main_cost_center, "is_cancelled": 0},
"posting_date", order_by="posting_date desc") "posting_date",
order_by="posting_date desc",
)
if last_gle_date: if last_gle_date:
if getdate(self.valid_from) <= getdate(last_gle_date): if getdate(self.valid_from) <= getdate(last_gle_date):
frappe.throw(_("Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date") frappe.throw(
.format(last_gle_date, self.main_cost_center), InvalidDateError) _(
"Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date"
).format(last_gle_date, self.main_cost_center),
InvalidDateError,
)
def validate_backdated_allocation(self): def validate_backdated_allocation(self):
# Check if there are any future existing allocation records against the main cost center # Check if there are any future existing allocation records against the main cost center
# If exists, warn the user about it # If exists, warn the user about it
future_allocation = frappe.db.get_value("Cost Center Allocation", filters = { future_allocation = frappe.db.get_value(
"main_cost_center": self.main_cost_center, "Cost Center Allocation",
"valid_from": (">=", self.valid_from), filters={
"name": ("!=", self.name), "main_cost_center": self.main_cost_center,
"docstatus": 1 "valid_from": (">=", self.valid_from),
}, fieldname=['valid_from', 'name'], order_by='valid_from', as_dict=1) "name": ("!=", self.name),
"docstatus": 1,
},
fieldname=["valid_from", "name"],
order_by="valid_from",
as_dict=1,
)
if future_allocation: if future_allocation:
frappe.msgprint(_("Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}") frappe.msgprint(
.format(frappe.bold(future_allocation.name), frappe.bold(format_date(future_allocation.valid_from)), _(
frappe.bold(format_date(add_days(future_allocation.valid_from, -1)))), "Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}"
title=_("Warning!"), indicator="orange", alert=1 ).format(
frappe.bold(future_allocation.name),
frappe.bold(format_date(future_allocation.valid_from)),
frappe.bold(format_date(add_days(future_allocation.valid_from, -1))),
),
title=_("Warning!"),
indicator="orange",
alert=1,
) )
def validate_main_cost_center(self): def validate_main_cost_center(self):
# Main cost center itself cannot be entered in child table # Main cost center itself cannot be entered in child table
if self.main_cost_center in [d.cost_center for d in self.allocation_percentages]: if self.main_cost_center in [d.cost_center for d in self.allocation_percentages]:
frappe.throw(_("Main Cost Center {0} cannot be entered in the child table") frappe.throw(
.format(self.main_cost_center), MainCostCenterCantBeChild) _("Main Cost Center {0} cannot be entered in the child table").format(self.main_cost_center),
MainCostCenterCantBeChild,
)
# If main cost center is used for allocation under any other cost center, # If main cost center is used for allocation under any other cost center,
# allocation cannot be done against it # allocation cannot be done against it
parent = frappe.db.get_value("Cost Center Allocation Percentage", filters = { parent = frappe.db.get_value(
"cost_center": self.main_cost_center, "Cost Center Allocation Percentage",
"docstatus": 1 filters={"cost_center": self.main_cost_center, "docstatus": 1},
}, fieldname='parent') fieldname="parent",
)
if parent: if parent:
frappe.throw(_("{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}") frappe.throw(
.format(self.main_cost_center, parent), InvalidMainCostCenter) _(
"{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}"
).format(self.main_cost_center, parent),
InvalidMainCostCenter,
)
def validate_child_cost_centers(self): def validate_child_cost_centers(self):
# Check if child cost center is used as main cost center in any existing allocation # Check if child cost center is used as main cost center in any existing allocation
main_cost_centers = [d.main_cost_center for d in main_cost_centers = [
frappe.get_all("Cost Center Allocation", {'docstatus': 1}, 'main_cost_center')] d.main_cost_center
for d in frappe.get_all("Cost Center Allocation", {"docstatus": 1}, "main_cost_center")
]
for d in self.allocation_percentages: for d in self.allocation_percentages:
if d.cost_center in main_cost_centers: if d.cost_center in main_cost_centers:
frappe.throw(_("Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.") frappe.throw(
.format(d.cost_center), InvalidChildCostCenter) _(
"Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record."
).format(d.cost_center),
InvalidChildCostCenter,
)

View File

@ -19,33 +19,35 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
class TestCostCenterAllocation(unittest.TestCase): class TestCostCenterAllocation(unittest.TestCase):
def setUp(self): def setUp(self):
cost_centers = ["Main Cost Center 1", "Main Cost Center 2", "Sub Cost Center 1", "Sub Cost Center 2"] cost_centers = [
"Main Cost Center 1",
"Main Cost Center 2",
"Sub Cost Center 1",
"Sub Cost Center 2",
]
for cc in cost_centers: for cc in cost_centers:
create_cost_center(cost_center_name=cc, company="_Test Company") create_cost_center(cost_center_name=cc, company="_Test Company")
def test_gle_based_on_cost_center_allocation(self): def test_gle_based_on_cost_center_allocation(self):
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC", cca = create_cost_center_allocation(
{ "_Test Company",
"Sub Cost Center 1 - _TC": 60, "Main Cost Center 1 - _TC",
"Sub Cost Center 2 - _TC": 40 {"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
}
) )
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100, jv = make_journal_entry(
cost_center = "Main Cost Center 1 - _TC", submit=True) "_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
)
expected_values = [ expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]]
["Sub Cost Center 1 - _TC", 0.0, 60],
["Sub Cost Center 2 - _TC", 0.0, 40]
]
gle = frappe.qb.DocType("GL Entry") gle = frappe.qb.DocType("GL Entry")
gl_entries = ( gl_entries = (
frappe.qb.from_(gle) frappe.qb.from_(gle)
.select(gle.cost_center, gle.debit, gle.credit) .select(gle.cost_center, gle.debit, gle.credit)
.where(gle.voucher_type == 'Journal Entry') .where(gle.voucher_type == "Journal Entry")
.where(gle.voucher_no == jv.name) .where(gle.voucher_no == jv.name)
.where(gle.account == 'Sales - _TC') .where(gle.account == "Sales - _TC")
.orderby(gle.cost_center) .orderby(gle.cost_center)
).run(as_dict=1) ).run(as_dict=1)
@ -61,11 +63,11 @@ class TestCostCenterAllocation(unittest.TestCase):
def test_main_cost_center_cant_be_child(self): def test_main_cost_center_cant_be_child(self):
# Main cost center itself cannot be entered in child table # Main cost center itself cannot be entered in child table
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC", cca = create_cost_center_allocation(
{ "_Test Company",
"Sub Cost Center 1 - _TC": 60, "Main Cost Center 1 - _TC",
"Main Cost Center 1 - _TC": 40 {"Sub Cost Center 1 - _TC": 60, "Main Cost Center 1 - _TC": 40},
}, save=False save=False,
) )
self.assertRaises(MainCostCenterCantBeChild, cca.save) self.assertRaises(MainCostCenterCantBeChild, cca.save)
@ -73,17 +75,14 @@ class TestCostCenterAllocation(unittest.TestCase):
def test_invalid_main_cost_center(self): def test_invalid_main_cost_center(self):
# If main cost center is used for allocation under any other cost center, # If main cost center is used for allocation under any other cost center,
# allocation cannot be done against it # allocation cannot be done against it
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC", cca1 = create_cost_center_allocation(
{ "_Test Company",
"Sub Cost Center 1 - _TC": 60, "Main Cost Center 1 - _TC",
"Sub Cost Center 2 - _TC": 40 {"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
}
) )
cca2 = create_cost_center_allocation("_Test Company", "Sub Cost Center 1 - _TC", cca2 = create_cost_center_allocation(
{ "_Test Company", "Sub Cost Center 1 - _TC", {"Sub Cost Center 2 - _TC": 100}, save=False
"Sub Cost Center 2 - _TC": 100
}, save=False
) )
self.assertRaises(InvalidMainCostCenter, cca2.save) self.assertRaises(InvalidMainCostCenter, cca2.save)
@ -92,18 +91,17 @@ class TestCostCenterAllocation(unittest.TestCase):
def test_if_child_cost_center_has_any_allocation_record(self): def test_if_child_cost_center_has_any_allocation_record(self):
# Check if any child cost center is used as main cost center in any other existing allocation # Check if any child cost center is used as main cost center in any other existing allocation
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC", cca1 = create_cost_center_allocation(
{ "_Test Company",
"Sub Cost Center 1 - _TC": 60, "Main Cost Center 1 - _TC",
"Sub Cost Center 2 - _TC": 40 {"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
}
) )
cca2 = create_cost_center_allocation("_Test Company", "Main Cost Center 2 - _TC", cca2 = create_cost_center_allocation(
{ "_Test Company",
"Main Cost Center 1 - _TC": 60, "Main Cost Center 2 - _TC",
"Sub Cost Center 1 - _TC": 40 {"Main Cost Center 1 - _TC": 60, "Sub Cost Center 1 - _TC": 40},
}, save=False save=False,
) )
self.assertRaises(InvalidChildCostCenter, cca2.save) self.assertRaises(InvalidChildCostCenter, cca2.save)
@ -111,46 +109,58 @@ class TestCostCenterAllocation(unittest.TestCase):
cca1.cancel() cca1.cancel()
def test_total_percentage(self): def test_total_percentage(self):
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC", cca = create_cost_center_allocation(
{ "_Test Company",
"Sub Cost Center 1 - _TC": 40, "Main Cost Center 1 - _TC",
"Sub Cost Center 2 - _TC": 40 {"Sub Cost Center 1 - _TC": 40, "Sub Cost Center 2 - _TC": 40},
}, save=False save=False,
) )
self.assertRaises(WrongPercentageAllocation, cca.save) self.assertRaises(WrongPercentageAllocation, cca.save)
def test_valid_from_based_on_existing_gle(self): def test_valid_from_based_on_existing_gle(self):
# GLE posted against Sub Cost Center 1 on today # GLE posted against Sub Cost Center 1 on today
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100, jv = make_journal_entry(
cost_center = "Main Cost Center 1 - _TC", posting_date=today(), submit=True) "_Test Cash - _TC",
"Sales - _TC",
100,
cost_center="Main Cost Center 1 - _TC",
posting_date=today(),
submit=True,
)
# try to set valid from as yesterday # try to set valid from as yesterday
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC", cca = create_cost_center_allocation(
{ "_Test Company",
"Sub Cost Center 1 - _TC": 60, "Main Cost Center 1 - _TC",
"Sub Cost Center 2 - _TC": 40 {"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
}, valid_from=add_days(today(), -1), save=False valid_from=add_days(today(), -1),
save=False,
) )
self.assertRaises(InvalidDateError, cca.save) self.assertRaises(InvalidDateError, cca.save)
jv.cancel() jv.cancel()
def create_cost_center_allocation(company, main_cost_center, allocation_percentages,
valid_from=None, valid_upto=None, save=True, submit=True): def create_cost_center_allocation(
company,
main_cost_center,
allocation_percentages,
valid_from=None,
valid_upto=None,
save=True,
submit=True,
):
doc = frappe.new_doc("Cost Center Allocation") doc = frappe.new_doc("Cost Center Allocation")
doc.main_cost_center = main_cost_center doc.main_cost_center = main_cost_center
doc.company = company doc.company = company
doc.valid_from = valid_from or today() doc.valid_from = valid_from or today()
doc.valid_upto = valid_upto doc.valid_upto = valid_upto
for cc, percentage in allocation_percentages.items(): for cc, percentage in allocation_percentages.items():
doc.append("allocation_percentages", { doc.append("allocation_percentages", {"cost_center": cc, "percentage": percentage})
"cost_center": cc,
"percentage": percentage
})
if save: if save:
doc.save() doc.save()
if submit: if submit:
doc.submit() doc.submit()
return doc return doc

View File

@ -15,7 +15,7 @@ class CouponCode(Document):
if not self.coupon_code: if not self.coupon_code:
if self.coupon_type == "Promotional": if self.coupon_type == "Promotional":
self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper() self.coupon_code = "".join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
elif self.coupon_type == "Gift Card": elif self.coupon_type == "Gift Card":
self.coupon_code = frappe.generate_hash()[:10].upper() self.coupon_code = frappe.generate_hash()[:10].upper()

View File

@ -7,92 +7,110 @@ import frappe
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
test_dependencies = ['Item'] test_dependencies = ["Item"]
def test_create_test_data(): def test_create_test_data():
frappe.set_user("Administrator") frappe.set_user("Administrator")
# create test item # create test item
if not frappe.db.exists("Item","_Test Tesla Car"): if not frappe.db.exists("Item", "_Test Tesla Car"):
item = frappe.get_doc({ item = frappe.get_doc(
"description": "_Test Tesla Car", {
"doctype": "Item", "description": "_Test Tesla Car",
"has_batch_no": 0, "doctype": "Item",
"has_serial_no": 0, "has_batch_no": 0,
"inspection_required": 0, "has_serial_no": 0,
"is_stock_item": 1, "inspection_required": 0,
"opening_stock":100, "is_stock_item": 1,
"is_sub_contracted_item": 0, "opening_stock": 100,
"item_code": "_Test Tesla Car", "is_sub_contracted_item": 0,
"item_group": "_Test Item Group", "item_code": "_Test Tesla Car",
"item_name": "_Test Tesla Car", "item_group": "_Test Item Group",
"apply_warehouse_wise_reorder_level": 0, "item_name": "_Test Tesla Car",
"warehouse":"Stores - _TC", "apply_warehouse_wise_reorder_level": 0,
"gst_hsn_code": "999800", "warehouse": "Stores - _TC",
"valuation_rate": 5000, "gst_hsn_code": "999800",
"standard_rate":5000, "valuation_rate": 5000,
"item_defaults": [{ "standard_rate": 5000,
"company": "_Test Company", "item_defaults": [
"default_warehouse": "Stores - _TC", {
"default_price_list":"_Test Price List", "company": "_Test Company",
"expense_account": "Cost of Goods Sold - _TC", "default_warehouse": "Stores - _TC",
"buying_cost_center": "Main - _TC", "default_price_list": "_Test Price List",
"selling_cost_center": "Main - _TC", "expense_account": "Cost of Goods Sold - _TC",
"income_account": "Sales - _TC" "buying_cost_center": "Main - _TC",
}], "selling_cost_center": "Main - _TC",
}) "income_account": "Sales - _TC",
}
],
}
)
item.insert() item.insert()
# create test item price # create test item price
item_price = frappe.get_list('Item Price', filters={'item_code': '_Test Tesla Car', 'price_list': '_Test Price List'}, fields=['name']) item_price = frappe.get_list(
if len(item_price)==0: "Item Price",
item_price = frappe.get_doc({ filters={"item_code": "_Test Tesla Car", "price_list": "_Test Price List"},
"doctype": "Item Price", fields=["name"],
"item_code": "_Test Tesla Car", )
"price_list": "_Test Price List", if len(item_price) == 0:
"price_list_rate": 5000 item_price = frappe.get_doc(
}) {
"doctype": "Item Price",
"item_code": "_Test Tesla Car",
"price_list": "_Test Price List",
"price_list_rate": 5000,
}
)
item_price.insert() item_price.insert()
# create test item pricing rule # create test item pricing rule
if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}): if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}):
item_pricing_rule = frappe.get_doc({ item_pricing_rule = frappe.get_doc(
"doctype": "Pricing Rule", {
"title": "_Test Pricing Rule for _Test Item", "doctype": "Pricing Rule",
"apply_on": "Item Code", "title": "_Test Pricing Rule for _Test Item",
"items": [{ "apply_on": "Item Code",
"item_code": "_Test Tesla Car" "items": [{"item_code": "_Test Tesla Car"}],
}], "warehouse": "Stores - _TC",
"warehouse":"Stores - _TC", "coupon_code_based": 1,
"coupon_code_based":1, "selling": 1,
"selling": 1, "rate_or_discount": "Discount Percentage",
"rate_or_discount": "Discount Percentage", "discount_percentage": 30,
"discount_percentage": 30, "company": "_Test Company",
"company": "_Test Company", "currency": "INR",
"currency":"INR", "for_price_list": "_Test Price List",
"for_price_list":"_Test Price List" }
}) )
item_pricing_rule.insert() item_pricing_rule.insert()
# create test item sales partner # create test item sales partner
if not frappe.db.exists("Sales Partner","_Test Coupon Partner"): if not frappe.db.exists("Sales Partner", "_Test Coupon Partner"):
sales_partner = frappe.get_doc({ sales_partner = frappe.get_doc(
"doctype": "Sales Partner", {
"partner_name":"_Test Coupon Partner", "doctype": "Sales Partner",
"commission_rate":2, "partner_name": "_Test Coupon Partner",
"referral_code": "COPART" "commission_rate": 2,
}) "referral_code": "COPART",
}
)
sales_partner.insert() sales_partner.insert()
# create test item coupon code # create test item coupon code
if not frappe.db.exists("Coupon Code", "SAVE30"): if not frappe.db.exists("Coupon Code", "SAVE30"):
pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ['name']) pricing_rule = frappe.db.get_value(
coupon_code = frappe.get_doc({ "Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ["name"]
"doctype": "Coupon Code", )
"coupon_name":"SAVE30", coupon_code = frappe.get_doc(
"coupon_code":"SAVE30", {
"pricing_rule": pricing_rule, "doctype": "Coupon Code",
"valid_from": "2014-01-01", "coupon_name": "SAVE30",
"maximum_use":1, "coupon_code": "SAVE30",
"used":0 "pricing_rule": pricing_rule,
}) "valid_from": "2014-01-01",
"maximum_use": 1,
"used": 0,
}
)
coupon_code.insert() coupon_code.insert()
class TestCouponCode(unittest.TestCase): class TestCouponCode(unittest.TestCase):
def setUp(self): def setUp(self):
test_create_test_data() test_create_test_data()
@ -103,15 +121,21 @@ class TestCouponCode(unittest.TestCase):
def test_sales_order_with_coupon_code(self): def test_sales_order_with_coupon_code(self):
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0) frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
so = make_sales_order(company='_Test Company', warehouse='Stores - _TC', so = make_sales_order(
customer="_Test Customer", selling_price_list="_Test Price List", company="_Test Company",
item_code="_Test Tesla Car", rate=5000, qty=1, warehouse="Stores - _TC",
do_not_submit=True) customer="_Test Customer",
selling_price_list="_Test Price List",
item_code="_Test Tesla Car",
rate=5000,
qty=1,
do_not_submit=True,
)
self.assertEqual(so.items[0].rate, 5000) self.assertEqual(so.items[0].rate, 5000)
so.coupon_code='SAVE30' so.coupon_code = "SAVE30"
so.sales_partner='_Test Coupon Partner' so.sales_partner = "_Test Coupon Partner"
so.save() so.save()
# check item price after coupon code is applied # check item price after coupon code is applied

View File

@ -15,24 +15,24 @@ class CurrencyExchangeSettings(Document):
self.validate_result(response, value) self.validate_result(response, value)
def set_parameters_and_result(self): def set_parameters_and_result(self):
if self.service_provider == 'exchangerate.host': if self.service_provider == "exchangerate.host":
self.set('result_key', []) self.set("result_key", [])
self.set('req_params', []) self.set("req_params", [])
self.api_endpoint = "https://api.exchangerate.host/convert" self.api_endpoint = "https://api.exchangerate.host/convert"
self.append('result_key', {'key': 'result'}) self.append("result_key", {"key": "result"})
self.append('req_params', {'key': 'date', 'value': '{transaction_date}'}) self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append('req_params', {'key': 'from', 'value': '{from_currency}'}) self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append('req_params', {'key': 'to', 'value': '{to_currency}'}) self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider == 'frankfurter.app': elif self.service_provider == "frankfurter.app":
self.set('result_key', []) self.set("result_key", [])
self.set('req_params', []) self.set("req_params", [])
self.api_endpoint = "https://frankfurter.app/{transaction_date}" self.api_endpoint = "https://frankfurter.app/{transaction_date}"
self.append('result_key', {'key': 'rates'}) self.append("result_key", {"key": "rates"})
self.append('result_key', {'key': '{to_currency}'}) self.append("result_key", {"key": "{to_currency}"})
self.append('req_params', {'key': 'base', 'value': '{from_currency}'}) self.append("req_params", {"key": "base", "value": "{from_currency}"})
self.append('req_params', {'key': 'symbols', 'value': '{to_currency}'}) self.append("req_params", {"key": "symbols", "value": "{to_currency}"})
def validate_parameters(self): def validate_parameters(self):
if frappe.flags.in_test: if frappe.flags.in_test:
@ -41,15 +41,11 @@ class CurrencyExchangeSettings(Document):
params = {} params = {}
for row in self.req_params: for row in self.req_params:
params[row.key] = row.value.format( params[row.key] = row.value.format(
transaction_date=nowdate(), transaction_date=nowdate(), to_currency="INR", from_currency="USD"
to_currency='INR',
from_currency='USD'
) )
api_url = self.api_endpoint.format( api_url = self.api_endpoint.format(
transaction_date=nowdate(), transaction_date=nowdate(), to_currency="INR", from_currency="USD"
to_currency='INR',
from_currency='USD'
) )
try: try:
@ -68,11 +64,9 @@ class CurrencyExchangeSettings(Document):
try: try:
for key in self.result_key: for key in self.result_key:
value = value[str(key.key).format( value = value[
transaction_date=nowdate(), str(key.key).format(transaction_date=nowdate(), to_currency="INR", from_currency="USD")
to_currency='INR', ]
from_currency='USD'
)]
except Exception: except Exception:
frappe.throw("Invalid result key. Response: " + response.text) frappe.throw("Invalid result key. Response: " + response.text)
if not isinstance(value, (int, float)): if not isinstance(value, (int, float)):

View File

@ -19,78 +19,99 @@ class Dunning(AccountsController):
self.validate_overdue_days() self.validate_overdue_days()
self.validate_amount() self.validate_amount()
if not self.income_account: if not self.income_account:
self.income_account = frappe.db.get_value('Company', self.company, 'default_income_account') self.income_account = frappe.db.get_value("Company", self.company, "default_income_account")
def validate_overdue_days(self): def validate_overdue_days(self):
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
def validate_amount(self): def validate_amount(self):
amounts = calculate_interest_and_amount( amounts = calculate_interest_and_amount(
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
if self.interest_amount != amounts.get('interest_amount'): )
self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) if self.interest_amount != amounts.get("interest_amount"):
if self.dunning_amount != amounts.get('dunning_amount'): self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount"))
self.dunning_amount = flt(amounts.get('dunning_amount'), self.precision('dunning_amount')) if self.dunning_amount != amounts.get("dunning_amount"):
if self.grand_total != amounts.get('grand_total'): self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount"))
self.grand_total = flt(amounts.get('grand_total'), self.precision('grand_total')) if self.grand_total != amounts.get("grand_total"):
self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total"))
def on_submit(self): def on_submit(self):
self.make_gl_entries() self.make_gl_entries()
def on_cancel(self): def on_cancel(self):
if self.dunning_amount: if self.dunning_amount:
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def make_gl_entries(self): def make_gl_entries(self):
if not self.dunning_amount: if not self.dunning_amount:
return return
gl_entries = [] gl_entries = []
invoice_fields = ["project", "cost_center", "debit_to", "party_account_currency", "conversion_rate", "cost_center"] invoice_fields = [
"project",
"cost_center",
"debit_to",
"party_account_currency",
"conversion_rate",
"cost_center",
]
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1) inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
accounting_dimensions = get_accounting_dimensions() accounting_dimensions = get_accounting_dimensions()
invoice_fields.extend(accounting_dimensions) invoice_fields.extend(accounting_dimensions)
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate) dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center') default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict(
"account": inv.debit_to, {
"party_type": "Customer", "account": inv.debit_to,
"party": self.customer, "party_type": "Customer",
"due_date": self.due_date, "party": self.customer,
"against": self.income_account, "due_date": self.due_date,
"debit": dunning_in_company_currency, "against": self.income_account,
"debit_in_account_currency": self.dunning_amount, "debit": dunning_in_company_currency,
"against_voucher": self.name, "debit_in_account_currency": self.dunning_amount,
"against_voucher_type": "Dunning", "against_voucher": self.name,
"cost_center": inv.cost_center or default_cost_center, "against_voucher_type": "Dunning",
"project": inv.project "cost_center": inv.cost_center or default_cost_center,
}, inv.party_account_currency, item=inv) "project": inv.project,
},
inv.party_account_currency,
item=inv,
)
) )
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict(
"account": self.income_account, {
"against": self.customer, "account": self.income_account,
"credit": dunning_in_company_currency, "against": self.customer,
"cost_center": inv.cost_center or default_cost_center, "credit": dunning_in_company_currency,
"credit_in_account_currency": self.dunning_amount, "cost_center": inv.cost_center or default_cost_center,
"project": inv.project "credit_in_account_currency": self.dunning_amount,
}, item=inv) "project": inv.project,
},
item=inv,
)
)
make_gl_entries(
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
) )
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False)
def resolve_dunning(doc, state): def resolve_dunning(doc, state):
for reference in doc.references: for reference in doc.references:
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0:
dunnings = frappe.get_list('Dunning', filters={ dunnings = frappe.get_list(
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True) "Dunning",
filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")},
ignore_permissions=True,
)
for dunning in dunnings: for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
interest_amount = 0 interest_amount = 0
@ -101,23 +122,26 @@ def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_
grand_total += flt(interest_amount) grand_total += flt(interest_amount)
dunning_amount = flt(interest_amount) + flt(dunning_fee) dunning_amount = flt(interest_amount) + flt(dunning_fee)
return { return {
'interest_amount': interest_amount, "interest_amount": interest_amount,
'grand_total': grand_total, "grand_total": grand_total,
'dunning_amount': dunning_amount} "dunning_amount": dunning_amount,
}
@frappe.whitelist() @frappe.whitelist()
def get_dunning_letter_text(dunning_type, doc, language=None): def get_dunning_letter_text(dunning_type, doc, language=None):
if isinstance(doc, str): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)
if language: if language:
filters = {'parent': dunning_type, 'language': language} filters = {"parent": dunning_type, "language": language}
else: else:
filters = {'parent': dunning_type, 'is_default_language': 1} filters = {"parent": dunning_type, "is_default_language": 1}
letter_text = frappe.db.get_value('Dunning Letter Text', filters, letter_text = frappe.db.get_value(
['body_text', 'closing_text', 'language'], as_dict=1) "Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
)
if letter_text: if letter_text:
return { return {
'body_text': frappe.render_template(letter_text.body_text, doc), "body_text": frappe.render_template(letter_text.body_text, doc),
'closing_text': frappe.render_template(letter_text.closing_text, doc), "closing_text": frappe.render_template(letter_text.closing_text, doc),
'language': letter_text.language "language": letter_text.language,
} }

View File

@ -3,15 +3,10 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'dunning', "fieldname": "dunning",
'non_standard_fieldnames': { "non_standard_fieldnames": {
'Journal Entry': 'reference_name', "Journal Entry": "reference_name",
'Payment Entry': 'reference_name' "Payment Entry": "reference_name",
}, },
'transactions': [ "transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
{
'label': _('Payment'),
'items': ['Payment Entry', 'Journal Entry']
}
]
} }

View File

@ -30,30 +30,35 @@ class TestDunning(unittest.TestCase):
def test_dunning(self): def test_dunning(self):
dunning = create_dunning() dunning = create_dunning()
amounts = calculate_interest_and_amount( amounts = calculate_interest_and_amount(
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44) )
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44) self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44) self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
def test_dunning_with_zero_interest_rate(self): def test_dunning_with_zero_interest_rate(self):
dunning = create_dunning_with_zero_interest_rate() dunning = create_dunning_with_zero_interest_rate()
amounts = calculate_interest_and_amount( amounts = calculate_interest_and_amount(
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
self.assertEqual(round(amounts.get('interest_amount'), 2), 0) )
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20) self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
self.assertEqual(round(amounts.get('grand_total'), 2), 120) self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
self.assertEqual(round(amounts.get("grand_total"), 2), 120)
def test_gl_entries(self): def test_gl_entries(self):
dunning = create_dunning() dunning = create_dunning()
dunning.submit() dunning.submit()
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql(
"""select account, debit, credit
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
order by account asc""", dunning.name, as_dict=1) order by account asc""",
dunning.name,
as_dict=1,
)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [ expected_values = dict(
['Debtors - _TC', 20.44, 0.0], (d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
['Sales - _TC', 0.0, 20.44] )
])
for gle in gl_entries: for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.debit)
@ -71,7 +76,7 @@ class TestDunning(unittest.TestCase):
pe.target_exchange_rate = 1 pe.target_exchange_rate = 1
pe.insert() pe.insert()
pe.submit() pe.submit()
si_doc = frappe.get_doc('Sales Invoice', dunning.sales_invoice) si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
self.assertEqual(si_doc.outstanding_amount, 0) self.assertEqual(si_doc.outstanding_amount, 0)
@ -79,8 +84,9 @@ def create_dunning():
posting_date = add_days(today(), -20) posting_date = add_days(today(), -20)
due_date = add_days(today(), -15) due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center( sales_invoice = create_sales_invoice_against_cost_center(
posting_date=posting_date, due_date=due_date, status='Overdue') posting_date=posting_date, due_date=due_date, status="Overdue"
dunning_type = frappe.get_doc("Dunning Type", 'First Notice') )
dunning_type = frappe.get_doc("Dunning Type", "First Notice")
dunning = frappe.new_doc("Dunning") dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name dunning.customer_name = sales_invoice.customer_name
@ -90,18 +96,20 @@ def create_dunning():
dunning.company = sales_invoice.company dunning.company = sales_invoice.company
dunning.posting_date = nowdate() dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date dunning.due_date = sales_invoice.due_date
dunning.dunning_type = 'First Notice' dunning.dunning_type = "First Notice"
dunning.rate_of_interest = dunning_type.rate_of_interest dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee dunning.dunning_fee = dunning_type.dunning_fee
dunning.save() dunning.save()
return dunning return dunning
def create_dunning_with_zero_interest_rate(): def create_dunning_with_zero_interest_rate():
posting_date = add_days(today(), -20) posting_date = add_days(today(), -20)
due_date = add_days(today(), -15) due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center( sales_invoice = create_sales_invoice_against_cost_center(
posting_date=posting_date, due_date=due_date, status='Overdue') posting_date=posting_date, due_date=due_date, status="Overdue"
dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest') )
dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
dunning = frappe.new_doc("Dunning") dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name dunning.customer_name = sales_invoice.customer_name
@ -111,40 +119,44 @@ def create_dunning_with_zero_interest_rate():
dunning.company = sales_invoice.company dunning.company = sales_invoice.company
dunning.posting_date = nowdate() dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date dunning.due_date = sales_invoice.due_date
dunning.dunning_type = 'First Notice with 0% Rate of Interest' dunning.dunning_type = "First Notice with 0% Rate of Interest"
dunning.rate_of_interest = dunning_type.rate_of_interest dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee dunning.dunning_fee = dunning_type.dunning_fee
dunning.save() dunning.save()
return dunning return dunning
def create_dunning_type(): def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type") dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = 'First Notice' dunning_type.dunning_type = "First Notice"
dunning_type.start_day = 10 dunning_type.start_day = 10
dunning_type.end_day = 20 dunning_type.end_day = 20
dunning_type.dunning_fee = 20 dunning_type.dunning_fee = 20
dunning_type.rate_of_interest = 8 dunning_type.rate_of_interest = 8
dunning_type.append( dunning_type.append(
"dunning_letter_text", { "dunning_letter_text",
'language': 'en', {
'body_text': 'We have still not received payment for our invoice ', "language": "en",
'closing_text': 'We kindly request that you pay the outstanding amount immediately, including interest and late fees.' "body_text": "We have still not received payment for our invoice ",
} "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
},
) )
dunning_type.save() dunning_type.save()
def create_dunning_type_with_zero_interest_rate(): def create_dunning_type_with_zero_interest_rate():
dunning_type = frappe.new_doc("Dunning Type") dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = 'First Notice with 0% Rate of Interest' dunning_type.dunning_type = "First Notice with 0% Rate of Interest"
dunning_type.start_day = 10 dunning_type.start_day = 10
dunning_type.end_day = 20 dunning_type.end_day = 20
dunning_type.dunning_fee = 20 dunning_type.dunning_fee = 20
dunning_type.rate_of_interest = 0 dunning_type.rate_of_interest = 0
dunning_type.append( dunning_type.append(
"dunning_letter_text", { "dunning_letter_text",
'language': 'en', {
'body_text': 'We have still not received payment for our invoice ', "language": "en",
'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.' "body_text": "We have still not received payment for our invoice ",
} "closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.",
},
) )
dunning_type.save() dunning_type.save()

View File

@ -20,8 +20,9 @@ class ExchangeRateRevaluation(Document):
def set_total_gain_loss(self): def set_total_gain_loss(self):
total_gain_loss = 0 total_gain_loss = 0
for d in self.accounts: for d in self.accounts:
d.gain_loss = flt(d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")) \ d.gain_loss = flt(
- flt(d.balance_in_base_currency, d.precision("balance_in_base_currency")) d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
total_gain_loss += flt(d.gain_loss, d.precision("gain_loss")) total_gain_loss += flt(d.gain_loss, d.precision("gain_loss"))
self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss")) self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss"))
@ -30,15 +31,15 @@ class ExchangeRateRevaluation(Document):
frappe.throw(_("Please select Company and Posting Date to getting entries")) frappe.throw(_("Please select Company and Posting Date to getting entries"))
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry') self.ignore_linked_doctypes = "GL Entry"
@frappe.whitelist() @frappe.whitelist()
def check_journal_entry_condition(self): def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", { total_debit = frappe.db.get_value(
'reference_type': 'Exchange Rate Revaluation', "Journal Entry Account",
'reference_name': self.name, {"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1},
'docstatus': 1 "sum(debit) as sum",
}, "sum(debit) as sum") )
total_amt = 0 total_amt = 0
for d in self.accounts: for d in self.accounts:
@ -54,28 +55,33 @@ class ExchangeRateRevaluation(Document):
accounts = [] accounts = []
self.validate_mandatory() self.validate_mandatory()
company_currency = erpnext.get_company_currency(self.company) company_currency = erpnext.get_company_currency(self.company)
precision = get_field_precision(frappe.get_meta("Exchange Rate Revaluation Account") precision = get_field_precision(
.get_field("new_balance_in_base_currency"), company_currency) frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
)
account_details = self.get_accounts_from_gle() account_details = self.get_accounts_from_gle()
for d in account_details: for d in account_details:
current_exchange_rate = d.balance / d.balance_in_account_currency \ current_exchange_rate = (
if d.balance_in_account_currency else 0 d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
)
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date) new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate) new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
if gain_loss: if gain_loss:
accounts.append({ accounts.append(
"account": d.account, {
"party_type": d.party_type, "account": d.account,
"party": d.party, "party_type": d.party_type,
"account_currency": d.account_currency, "party": d.party,
"balance_in_base_currency": d.balance, "account_currency": d.account_currency,
"balance_in_account_currency": d.balance_in_account_currency, "balance_in_base_currency": d.balance,
"current_exchange_rate": current_exchange_rate, "balance_in_account_currency": d.balance_in_account_currency,
"new_exchange_rate": new_exchange_rate, "current_exchange_rate": current_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency "new_exchange_rate": new_exchange_rate,
}) "new_balance_in_base_currency": new_balance_in_base_currency,
}
)
if not accounts: if not accounts:
self.throw_invalid_response_message(account_details) self.throw_invalid_response_message(account_details)
@ -84,7 +90,8 @@ class ExchangeRateRevaluation(Document):
def get_accounts_from_gle(self): def get_accounts_from_gle(self):
company_currency = erpnext.get_company_currency(self.company) company_currency = erpnext.get_company_currency(self.company)
accounts = frappe.db.sql_list(""" accounts = frappe.db.sql_list(
"""
select name select name
from tabAccount from tabAccount
where is_group = 0 where is_group = 0
@ -93,11 +100,14 @@ class ExchangeRateRevaluation(Document):
and account_type != 'Stock' and account_type != 'Stock'
and company=%s and company=%s
and account_currency != %s and account_currency != %s
order by name""",(self.company, company_currency)) order by name""",
(self.company, company_currency),
)
account_details = [] account_details = []
if accounts: if accounts:
account_details = frappe.db.sql(""" account_details = frappe.db.sql(
"""
select select
account, party_type, party, account_currency, account, party_type, party, account_currency,
sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency, sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency,
@ -109,7 +119,11 @@ class ExchangeRateRevaluation(Document):
group by account, NULLIF(party_type,''), NULLIF(party,'') group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit) having sum(debit) != sum(credit)
order by account order by account
""" % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1) """
% (", ".join(["%s"] * len(accounts)), "%s"),
tuple(accounts + [self.posting_date]),
as_dict=1,
)
return account_details return account_details
@ -125,77 +139,107 @@ class ExchangeRateRevaluation(Document):
if self.total_gain_loss == 0: if self.total_gain_loss == 0:
return return
unrealized_exchange_gain_loss_account = frappe.get_cached_value('Company', self.company, unrealized_exchange_gain_loss_account = frappe.get_cached_value(
"unrealized_exchange_gain_loss_account") "Company", self.company, "unrealized_exchange_gain_loss_account"
)
if not unrealized_exchange_gain_loss_account: if not unrealized_exchange_gain_loss_account:
frappe.throw(_("Please set Unrealized Exchange Gain/Loss Account in Company {0}") frappe.throw(
.format(self.company)) _("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company)
)
journal_entry = frappe.new_doc('Journal Entry') journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = 'Exchange Rate Revaluation' journal_entry.voucher_type = "Exchange Rate Revaluation"
journal_entry.company = self.company journal_entry.company = self.company
journal_entry.posting_date = self.posting_date journal_entry.posting_date = self.posting_date
journal_entry.multi_currency = 1 journal_entry.multi_currency = 1
journal_entry_accounts = [] journal_entry_accounts = []
for d in self.accounts: for d in self.accounts:
dr_or_cr = "debit_in_account_currency" \ dr_or_cr = (
if d.get("balance_in_account_currency") > 0 else "credit_in_account_currency" "debit_in_account_currency"
if d.get("balance_in_account_currency") > 0
else "credit_in_account_currency"
)
reverse_dr_or_cr = "debit_in_account_currency" \ reverse_dr_or_cr = (
if dr_or_cr=="credit_in_account_currency" else "credit_in_account_currency" "debit_in_account_currency"
if dr_or_cr == "credit_in_account_currency"
else "credit_in_account_currency"
)
journal_entry_accounts.append({ journal_entry_accounts.append(
"account": d.get("account"), {
"party_type": d.get("party_type"), "account": d.get("account"),
"party": d.get("party"), "party_type": d.get("party_type"),
"account_currency": d.get("account_currency"), "party": d.get("party"),
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")), "account_currency": d.get("account_currency"),
dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")), "balance": flt(
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")), d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
),
dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
),
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
journal_entry_accounts.append(
{
"account": d.get("account"),
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
"balance": flt(
d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
),
reverse_dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
),
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
journal_entry_accounts.append(
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0,
"credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0,
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
}) }
journal_entry_accounts.append({ )
"account": d.get("account"),
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name
})
journal_entry_accounts.append({
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0,
"credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0,
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
})
journal_entry.set("accounts", journal_entry_accounts) journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency() journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit() journal_entry.set_total_debit_credit()
return journal_entry.as_dict() return journal_entry.as_dict()
@frappe.whitelist() @frappe.whitelist()
def get_account_details(account, company, posting_date, party_type=None, party=None): def get_account_details(account, company, posting_date, party_type=None, party=None):
account_currency, account_type = frappe.db.get_value("Account", account, account_currency, account_type = frappe.db.get_value(
["account_currency", "account_type"]) "Account", account, ["account_currency", "account_type"]
)
if account_type in ["Receivable", "Payable"] and not (party_type and party): if account_type in ["Receivable", "Payable"] and not (party_type and party):
frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type)) frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type))
account_details = {} account_details = {}
company_currency = erpnext.get_company_currency(company) company_currency = erpnext.get_company_currency(company)
balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False) balance = get_balance_on(
account, date=posting_date, party_type=party_type, party=party, in_account_currency=False
)
if balance: if balance:
balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party) balance_in_account_currency = get_balance_on(
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0 account, date=posting_date, party_type=party_type, party=party
)
current_exchange_rate = (
balance / balance_in_account_currency if balance_in_account_currency else 0
)
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
account_details = { account_details = {
@ -204,7 +248,7 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
"balance_in_account_currency": balance_in_account_currency, "balance_in_account_currency": balance_in_account_currency,
"current_exchange_rate": current_exchange_rate, "current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate, "new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency "new_balance_in_base_currency": new_balance_in_base_currency,
} }
return account_details return account_details

View File

@ -1,9 +1,2 @@
def get_data(): def get_data():
return { return {"fieldname": "reference_name", "transactions": [{"items": ["Journal Entry"]}]}
'fieldname': 'reference_name',
'transactions': [
{
'items': ['Journal Entry']
}
]
}

View File

@ -3,21 +3,11 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'finance_book', "fieldname": "finance_book",
'non_standard_fieldnames': { "non_standard_fieldnames": {"Asset": "default_finance_book", "Company": "default_finance_book"},
'Asset': 'default_finance_book', "transactions": [
'Company': 'default_finance_book' {"label": _("Assets"), "items": ["Asset", "Asset Value Adjustment"]},
}, {"items": ["Company"]},
'transactions': [ {"items": ["Journal Entry"]},
{ ],
'label': _('Assets'),
'items': ['Asset', 'Asset Value Adjustment']
},
{
'items': ['Company']
},
{
'items': ['Journal Entry']
}
]
} }

View File

@ -13,30 +13,29 @@ class TestFinanceBook(unittest.TestCase):
finance_book = create_finance_book() finance_book = create_finance_book()
# create jv entry # create jv entry
jv = make_journal_entry("_Test Bank - _TC", jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False)
"_Test Receivable - _TC", 100, save=False)
jv.accounts[1].update({ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer"})
"party_type": "Customer",
"party": "_Test Customer"
})
jv.finance_book = finance_book.finance_book_name jv.finance_book = finance_book.finance_book_name
jv.submit() jv.submit()
# check the Finance Book in the GL Entry # check the Finance Book in the GL Entry
gl_entries = frappe.get_all("GL Entry", fields=["name", "finance_book"], gl_entries = frappe.get_all(
filters={"voucher_type": "Journal Entry", "voucher_no": jv.name}) "GL Entry",
fields=["name", "finance_book"],
filters={"voucher_type": "Journal Entry", "voucher_no": jv.name},
)
for gl_entry in gl_entries: for gl_entry in gl_entries:
self.assertEqual(gl_entry.finance_book, finance_book.name) self.assertEqual(gl_entry.finance_book, finance_book.name)
def create_finance_book(): def create_finance_book():
if not frappe.db.exists("Finance Book", "_Test Finance Book"): if not frappe.db.exists("Finance Book", "_Test Finance Book"):
finance_book = frappe.get_doc({ finance_book = frappe.get_doc(
"doctype": "Finance Book", {"doctype": "Finance Book", "finance_book_name": "_Test Finance Book"}
"finance_book_name": "_Test Finance Book" ).insert()
}).insert()
else: else:
finance_book = frappe.get_doc("Finance Book", "_Test Finance Book") finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")

View File

@ -9,7 +9,9 @@ from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate from frappe.utils import add_days, add_years, cstr, getdate
class FiscalYearIncorrectDate(frappe.ValidationError): pass class FiscalYearIncorrectDate(frappe.ValidationError):
pass
class FiscalYear(Document): class FiscalYear(Document):
@frappe.whitelist() @frappe.whitelist()
@ -22,19 +24,33 @@ class FiscalYear(Document):
# clear cache # clear cache
frappe.clear_cache() frappe.clear_cache()
msgprint(_("{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect.").format(self.name)) msgprint(
_(
"{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect."
).format(self.name)
)
def validate(self): def validate(self):
self.validate_dates() self.validate_dates()
self.validate_overlap() self.validate_overlap()
if not self.is_new(): if not self.is_new():
year_start_end_dates = frappe.db.sql("""select year_start_date, year_end_date year_start_end_dates = frappe.db.sql(
from `tabFiscal Year` where name=%s""", (self.name)) """select year_start_date, year_end_date
from `tabFiscal Year` where name=%s""",
(self.name),
)
if year_start_end_dates: if year_start_end_dates:
if getdate(self.year_start_date) != year_start_end_dates[0][0] or getdate(self.year_end_date) != year_start_end_dates[0][1]: if (
frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved.")) getdate(self.year_start_date) != year_start_end_dates[0][0]
or getdate(self.year_end_date) != year_start_end_dates[0][1]
):
frappe.throw(
_(
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
)
)
def validate_dates(self): def validate_dates(self):
if self.is_short_year: if self.is_short_year:
@ -43,14 +59,18 @@ class FiscalYear(Document):
return return
if getdate(self.year_start_date) > getdate(self.year_end_date): if getdate(self.year_start_date) > getdate(self.year_end_date):
frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"), frappe.throw(
FiscalYearIncorrectDate) _("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
FiscalYearIncorrectDate,
)
date = getdate(self.year_start_date) + relativedelta(years=1) - relativedelta(days=1) date = getdate(self.year_start_date) + relativedelta(years=1) - relativedelta(days=1)
if getdate(self.year_end_date) != date: if getdate(self.year_end_date) != date:
frappe.throw(_("Fiscal Year End Date should be one year after Fiscal Year Start Date"), frappe.throw(
FiscalYearIncorrectDate) _("Fiscal Year End Date should be one year after Fiscal Year Start Date"),
FiscalYearIncorrectDate,
)
def on_update(self): def on_update(self):
check_duplicate_fiscal_year(self) check_duplicate_fiscal_year(self)
@ -59,11 +79,16 @@ class FiscalYear(Document):
def on_trash(self): def on_trash(self):
global_defaults = frappe.get_doc("Global Defaults") global_defaults = frappe.get_doc("Global Defaults")
if global_defaults.current_fiscal_year == self.name: if global_defaults.current_fiscal_year == self.name:
frappe.throw(_("You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings").format(self.name)) frappe.throw(
_(
"You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings"
).format(self.name)
)
frappe.cache().delete_value("fiscal_years") frappe.cache().delete_value("fiscal_years")
def validate_overlap(self): def validate_overlap(self):
existing_fiscal_years = frappe.db.sql("""select name from `tabFiscal Year` existing_fiscal_years = frappe.db.sql(
"""select name from `tabFiscal Year`
where ( where (
(%(year_start_date)s between year_start_date and year_end_date) (%(year_start_date)s between year_start_date and year_end_date)
or (%(year_end_date)s between year_start_date and year_end_date) or (%(year_end_date)s between year_start_date and year_end_date)
@ -73,13 +98,18 @@ class FiscalYear(Document):
{ {
"year_start_date": self.year_start_date, "year_start_date": self.year_start_date,
"year_end_date": self.year_end_date, "year_end_date": self.year_end_date,
"name": self.name or "No Name" "name": self.name or "No Name",
}, as_dict=True) },
as_dict=True,
)
if existing_fiscal_years: if existing_fiscal_years:
for existing in existing_fiscal_years: for existing in existing_fiscal_years:
company_for_existing = frappe.db.sql_list("""select company from `tabFiscal Year Company` company_for_existing = frappe.db.sql_list(
where parent=%s""", existing.name) """select company from `tabFiscal Year Company`
where parent=%s""",
existing.name,
)
overlap = False overlap = False
if not self.get("companies") or not company_for_existing: if not self.get("companies") or not company_for_existing:
@ -90,20 +120,36 @@ class FiscalYear(Document):
overlap = True overlap = True
if overlap: if overlap:
frappe.throw(_("Year start date or end date is overlapping with {0}. To avoid please set company") frappe.throw(
.format(existing.name), frappe.NameError) _("Year start date or end date is overlapping with {0}. To avoid please set company").format(
existing.name
),
frappe.NameError,
)
@frappe.whitelist() @frappe.whitelist()
def check_duplicate_fiscal_year(doc): def check_duplicate_fiscal_year(doc):
year_start_end_dates = frappe.db.sql("""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""", (doc.name)) year_start_end_dates = frappe.db.sql(
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
(doc.name),
)
for fiscal_year, ysd, yed in year_start_end_dates: for fiscal_year, ysd, yed in year_start_end_dates:
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (not frappe.flags.in_test): if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
frappe.throw(_("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(fiscal_year)) not frappe.flags.in_test
):
frappe.throw(
_("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(
fiscal_year
)
)
@frappe.whitelist() @frappe.whitelist()
def auto_create_fiscal_year(): def auto_create_fiscal_year():
for d in frappe.db.sql("""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""): for d in frappe.db.sql(
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
):
try: try:
current_fy = frappe.get_doc("Fiscal Year", d[0]) current_fy = frappe.get_doc("Fiscal Year", d[0])
@ -114,16 +160,14 @@ def auto_create_fiscal_year():
start_year = cstr(new_fy.year_start_date.year) start_year = cstr(new_fy.year_start_date.year)
end_year = cstr(new_fy.year_end_date.year) end_year = cstr(new_fy.year_end_date.year)
new_fy.year = start_year if start_year==end_year else (start_year + "-" + end_year) new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
new_fy.auto_created = 1 new_fy.auto_created = 1
new_fy.insert(ignore_permissions=True) new_fy.insert(ignore_permissions=True)
except frappe.NameError: except frappe.NameError:
pass pass
def get_from_and_to_date(fiscal_year): def get_from_and_to_date(fiscal_year):
fields = [ fields = ["year_start_date as from_date", "year_end_date as to_date"]
"year_start_date as from_date",
"year_end_date as to_date"
]
return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1) return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)

View File

@ -3,19 +3,13 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'fiscal_year', "fieldname": "fiscal_year",
'transactions': [ "transactions": [
{"label": _("Budgets"), "items": ["Budget"]},
{"label": _("References"), "items": ["Period Closing Voucher"]},
{ {
'label': _('Budgets'), "label": _("Target Details"),
'items': ['Budget'] "items": ["Sales Person", "Sales Partner", "Territory", "Monthly Distribution"],
}, },
{ ],
'label': _('References'),
'items': ['Period Closing Voucher']
},
{
'label': _('Target Details'),
'items': ['Sales Person', 'Sales Partner', 'Territory', 'Monthly Distribution']
}
]
} }

View File

@ -11,43 +11,48 @@ from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrect
test_ignore = ["Company"] test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase):
class TestFiscalYear(unittest.TestCase):
def test_extra_year(self): def test_extra_year(self):
if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"): if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"):
frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000") frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000")
fy = frappe.get_doc({ fy = frappe.get_doc(
"doctype": "Fiscal Year", {
"year": "_Test Fiscal Year 2000", "doctype": "Fiscal Year",
"year_end_date": "2002-12-31", "year": "_Test Fiscal Year 2000",
"year_start_date": "2000-04-01" "year_end_date": "2002-12-31",
}) "year_start_date": "2000-04-01",
}
)
self.assertRaises(FiscalYearIncorrectDate, fy.insert) self.assertRaises(FiscalYearIncorrectDate, fy.insert)
def test_record_generator(): def test_record_generator():
test_records = [ test_records = [
{ {
"doctype": "Fiscal Year", "doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011", "year": "_Test Short Fiscal Year 2011",
"is_short_year": 1, "is_short_year": 1,
"year_end_date": "2011-04-01", "year_end_date": "2011-04-01",
"year_start_date": "2011-12-31" "year_start_date": "2011-12-31",
} }
] ]
start = 2012 start = 2012
end = now_datetime().year + 5 end = now_datetime().year + 5
for year in range(start, end): for year in range(start, end):
test_records.append({ test_records.append(
"doctype": "Fiscal Year", {
"year": f"_Test Fiscal Year {year}", "doctype": "Fiscal Year",
"year_start_date": f"{year}-01-01", "year": f"_Test Fiscal Year {year}",
"year_end_date": f"{year}-12-31" "year_start_date": f"{year}-01-01",
}) "year_end_date": f"{year}-12-31",
}
)
return test_records return test_records
test_records = test_record_generator() test_records = test_record_generator()

View File

@ -25,6 +25,8 @@ from erpnext.exceptions import (
) )
exclude_from_linked_with = True exclude_from_linked_with = True
class GLEntry(Document): class GLEntry(Document):
def autoname(self): def autoname(self):
""" """
@ -57,14 +59,18 @@ class GLEntry(Document):
validate_frozen_account(self.account, adv_adj) validate_frozen_account(self.account, adv_adj)
# Update outstanding amt on against voucher # Update outstanding amt on against voucher
if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] if (
and self.against_voucher and self.flags.update_outstanding == 'Yes' self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
and not frappe.flags.is_reverse_depr_entry): and self.against_voucher
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, and self.flags.update_outstanding == "Yes"
self.against_voucher) and not frappe.flags.is_reverse_depr_entry
):
update_outstanding_amt(
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
)
def check_mandatory(self): def check_mandatory(self):
mandatory = ['account','voucher_type','voucher_no','company'] mandatory = ["account", "voucher_type", "voucher_no", "company"]
for k in mandatory: for k in mandatory:
if not self.get(k): if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k)))) frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
@ -72,29 +78,40 @@ class GLEntry(Document):
if not (self.party_type and self.party): if not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type") account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type == "Receivable": if account_type == "Receivable":
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}") frappe.throw(
.format(self.voucher_type, self.voucher_no, self.account)) _("{0} {1}: Customer is required against Receivable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
elif account_type == "Payable": elif account_type == "Payable":
frappe.throw(_("{0} {1}: Supplier is required against Payable account {2}") frappe.throw(
.format(self.voucher_type, self.voucher_no, self.account)) _("{0} {1}: Supplier is required against Payable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
# Zero value transaction is not allowed # Zero value transaction is not allowed
if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))): if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))):
frappe.throw(_("{0} {1}: Either debit or credit amount is required for {2}") frappe.throw(
.format(self.voucher_type, self.voucher_no, self.account)) _("{0} {1}: Either debit or credit amount is required for {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
def pl_must_have_cost_center(self): def pl_must_have_cost_center(self):
"""Validate that profit and loss type account GL entries have a cost center.""" """Validate that profit and loss type account GL entries have a cost center."""
if self.cost_center or self.voucher_type == 'Period Closing Voucher': if self.cost_center or self.voucher_type == "Period Closing Voucher":
return return
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss": if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format( msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
self.voucher_type, self.voucher_no, self.account) self.voucher_type, self.voucher_no, self.account
)
msg += " " msg += " "
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format( msg += _(
self.voucher_type) "Please set the cost center field in {0} or setup a default Cost Center for the Company."
).format(self.voucher_type)
frappe.throw(msg, title=_("Missing Cost Center")) frappe.throw(msg, title=_("Missing Cost Center"))
@ -102,17 +119,31 @@ class GLEntry(Document):
account_type = frappe.db.get_value("Account", self.account, "report_type") account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts(): for dimension in get_checks_for_pl_and_bs_accounts():
if account_type == "Profit and Loss" \ if (
and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled: account_type == "Profit and Loss"
and self.company == dimension.company
and dimension.mandatory_for_pl
and not dimension.disabled
):
if not self.get(dimension.fieldname): if not self.get(dimension.fieldname):
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.") frappe.throw(
.format(dimension.label, self.account)) _("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
dimension.label, self.account
)
)
if account_type == "Balance Sheet" \ if (
and self.company == dimension.company and dimension.mandatory_for_bs and not dimension.disabled: account_type == "Balance Sheet"
and self.company == dimension.company
and dimension.mandatory_for_bs
and not dimension.disabled
):
if not self.get(dimension.fieldname): if not self.get(dimension.fieldname):
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.") frappe.throw(
.format(dimension.label, self.account)) _("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
dimension.label, self.account
)
)
def validate_allowed_dimensions(self): def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map() dimension_filter_map = get_dimension_filter_map()
@ -121,56 +152,97 @@ class GLEntry(Document):
account = key[1] account = key[1]
if self.account == account: if self.account == account:
if value['is_mandatory'] and not self.get(dimension): if value["is_mandatory"] and not self.get(dimension):
frappe.throw(_("{0} is mandatory for account {1}").format( frappe.throw(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError) _("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
),
MandatoryAccountDimensionError,
)
if value['allow_or_restrict'] == 'Allow': if value["allow_or_restrict"] == "Allow":
if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']: if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
frappe.throw(_("Invalid value {0} for {1} against account {2}").format( frappe.throw(
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) _("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
else: else:
if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']: if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
frappe.throw(_("Invalid value {0} for {1} against account {2}").format( frappe.throw(
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) _("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
def check_pl_account(self): def check_pl_account(self):
if self.is_opening=='Yes' and \ if (
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and not self.is_cancelled: self.is_opening == "Yes"
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss"
.format(self.voucher_type, self.voucher_no, self.account)) and not self.is_cancelled
):
frappe.throw(
_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry").format(
self.voucher_type, self.voucher_no, self.account
)
)
def validate_account_details(self, adv_adj): def validate_account_details(self, adv_adj):
"""Account must be ledger, active and not freezed""" """Account must be ledger, active and not freezed"""
ret = frappe.db.sql("""select is_group, docstatus, company ret = frappe.db.sql(
from tabAccount where name=%s""", self.account, as_dict=1)[0] """select is_group, docstatus, company
from tabAccount where name=%s""",
self.account,
as_dict=1,
)[0]
if ret.is_group==1: if ret.is_group == 1:
frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''') frappe.throw(
.format(self.voucher_type, self.voucher_no, self.account)) _(
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
).format(self.voucher_type, self.voucher_no, self.account)
)
if ret.docstatus==2: if ret.docstatus == 2:
frappe.throw(_("{0} {1}: Account {2} is inactive") frappe.throw(
.format(self.voucher_type, self.voucher_no, self.account)) _("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
)
if ret.company != self.company: if ret.company != self.company:
frappe.throw(_("{0} {1}: Account {2} does not belong to Company {3}") frappe.throw(
.format(self.voucher_type, self.voucher_no, self.account, self.company)) _("{0} {1}: Account {2} does not belong to Company {3}").format(
self.voucher_type, self.voucher_no, self.account, self.company
)
)
def validate_cost_center(self): def validate_cost_center(self):
if not self.cost_center: return if not self.cost_center:
return
is_group, company = frappe.get_cached_value('Cost Center', is_group, company = frappe.get_cached_value(
self.cost_center, ['is_group', 'company']) "Cost Center", self.cost_center, ["is_group", "company"]
)
if company != self.company: if company != self.company:
frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") frappe.throw(
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) _("{0} {1}: Cost Center {2} does not belong to Company {3}").format(
self.voucher_type, self.voucher_no, self.cost_center, self.company
)
)
if (self.voucher_type != 'Period Closing Voucher' and is_group): if self.voucher_type != "Period Closing Voucher" and is_group:
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format( frappe.throw(
self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) _(
"""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions"""
).format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))
)
def validate_party(self): def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party) validate_party_frozen_disabled(self.party_type, self.party)
@ -183,9 +255,12 @@ class GLEntry(Document):
self.account_currency = account_currency or company_currency self.account_currency = account_currency or company_currency
if account_currency != self.account_currency: if account_currency != self.account_currency:
frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}") frappe.throw(
.format(self.voucher_type, self.voucher_no, self.account, _("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}").format(
(account_currency or company_currency)), InvalidAccountCurrency) self.voucher_type, self.voucher_no, self.account, (account_currency or company_currency)
),
InvalidAccountCurrency,
)
if self.party_type and self.party: if self.party_type and self.party:
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency) validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
@ -194,51 +269,80 @@ class GLEntry(Document):
if not self.fiscal_year: if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
def validate_balance_type(account, adv_adj=False): def validate_balance_type(account, adv_adj=False):
if not adv_adj and account: if not adv_adj and account:
balance_must_be = frappe.db.get_value("Account", account, "balance_must_be") balance_must_be = frappe.db.get_value("Account", account, "balance_must_be")
if balance_must_be: if balance_must_be:
balance = frappe.db.sql("""select sum(debit) - sum(credit) balance = frappe.db.sql(
from `tabGL Entry` where account = %s""", account)[0][0] """select sum(debit) - sum(credit)
from `tabGL Entry` where account = %s""",
account,
)[0][0]
if (balance_must_be=="Debit" and flt(balance) < 0) or \ if (balance_must_be == "Debit" and flt(balance) < 0) or (
(balance_must_be=="Credit" and flt(balance) > 0): balance_must_be == "Credit" and flt(balance) > 0
frappe.throw(_("Balance for Account {0} must always be {1}").format(account, _(balance_must_be))) ):
frappe.throw(
_("Balance for Account {0} must always be {1}").format(account, _(balance_must_be))
)
def update_outstanding_amt(account, party_type, party, against_voucher_type, against_voucher, on_cancel=False):
def update_outstanding_amt(
account, party_type, party, against_voucher_type, against_voucher, on_cancel=False
):
if party_type and party: if party_type and party:
party_condition = " and party_type={0} and party={1}"\ party_condition = " and party_type={0} and party={1}".format(
.format(frappe.db.escape(party_type), frappe.db.escape(party)) frappe.db.escape(party_type), frappe.db.escape(party)
)
else: else:
party_condition = "" party_condition = ""
if against_voucher_type == "Sales Invoice": if against_voucher_type == "Sales Invoice":
party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to") party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to")
account_condition = "and account in ({0}, {1})".format(frappe.db.escape(account), frappe.db.escape(party_account)) account_condition = "and account in ({0}, {1})".format(
frappe.db.escape(account), frappe.db.escape(party_account)
)
else: else:
account_condition = " and account = {0}".format(frappe.db.escape(account)) account_condition = " and account = {0}".format(frappe.db.escape(account))
# get final outstanding amt # get final outstanding amt
bal = flt(frappe.db.sql(""" bal = flt(
frappe.db.sql(
"""
select sum(debit_in_account_currency) - sum(credit_in_account_currency) select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry` from `tabGL Entry`
where against_voucher_type=%s and against_voucher=%s where against_voucher_type=%s and against_voucher=%s
and voucher_type != 'Invoice Discounting' and voucher_type != 'Invoice Discounting'
{0} {1}""".format(party_condition, account_condition), {0} {1}""".format(
(against_voucher_type, against_voucher))[0][0] or 0.0) party_condition, account_condition
),
(against_voucher_type, against_voucher),
)[0][0]
or 0.0
)
if against_voucher_type == 'Purchase Invoice': if against_voucher_type == "Purchase Invoice":
bal = -bal bal = -bal
elif against_voucher_type == "Journal Entry": elif against_voucher_type == "Journal Entry":
against_voucher_amount = flt(frappe.db.sql(""" against_voucher_amount = flt(
frappe.db.sql(
"""
select sum(debit_in_account_currency) - sum(credit_in_account_currency) select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s
and account = %s and (against_voucher is null or against_voucher='') {0}""" and account = %s and (against_voucher is null or against_voucher='') {0}""".format(
.format(party_condition), (against_voucher, account))[0][0]) party_condition
),
(against_voucher, account),
)[0][0]
)
if not against_voucher_amount: if not against_voucher_amount:
frappe.throw(_("Against Journal Entry {0} is already adjusted against some other voucher") frappe.throw(
.format(against_voucher)) _("Against Journal Entry {0} is already adjusted against some other voucher").format(
against_voucher
)
)
bal = against_voucher_amount + bal bal = against_voucher_amount + bal
if against_voucher_amount < 0: if against_voucher_amount < 0:
@ -246,44 +350,51 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
# Validation : Outstanding can not be negative for JV # Validation : Outstanding can not be negative for JV
if bal < 0 and not on_cancel: if bal < 0 and not on_cancel:
frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))) frappe.throw(
_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))
)
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]: if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
ref_doc = frappe.get_doc(against_voucher_type, against_voucher) ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
# Didn't use db_set for optimisation purpose # Didn't use db_set for optimisation purpose
ref_doc.outstanding_amount = bal ref_doc.outstanding_amount = bal
frappe.db.set_value(against_voucher_type, against_voucher, 'outstanding_amount', bal) frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)
ref_doc.set_status(update=True) ref_doc.set_status(update=True)
def validate_frozen_account(account, adv_adj=None): def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account") frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == 'Yes' and not adv_adj: if frozen_account == "Yes" and not adv_adj:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None, frozen_accounts_modifier = frappe.db.get_value(
'frozen_accounts_modifier') "Accounts Settings", None, "frozen_accounts_modifier"
)
if not frozen_accounts_modifier: if not frozen_accounts_modifier:
frappe.throw(_("Account {0} is frozen").format(account)) frappe.throw(_("Account {0} is frozen").format(account))
elif frozen_accounts_modifier not in frappe.get_roles(): elif frozen_accounts_modifier not in frappe.get_roles():
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account)) frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
def update_against_account(voucher_type, voucher_no): def update_against_account(voucher_type, voucher_no):
entries = frappe.db.get_all("GL Entry", entries = frappe.db.get_all(
"GL Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["name", "party", "against", "debit", "credit", "account", "company"]) fields=["name", "party", "against", "debit", "credit", "account", "company"],
)
if not entries: if not entries:
return return
company_currency = erpnext.get_company_currency(entries[0].company) company_currency = erpnext.get_company_currency(entries[0].company)
precision = get_field_precision(frappe.get_meta("GL Entry") precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
.get_field("debit"), company_currency)
accounts_debited, accounts_credited = [], [] accounts_debited, accounts_credited = [], []
for d in entries: for d in entries:
if flt(d.debit, precision) > 0: accounts_debited.append(d.party or d.account) if flt(d.debit, precision) > 0:
if flt(d.credit, precision) > 0: accounts_credited.append(d.party or d.account) accounts_debited.append(d.party or d.account)
if flt(d.credit, precision) > 0:
accounts_credited.append(d.party or d.account)
for d in entries: for d in entries:
if flt(d.debit, precision) > 0: if flt(d.debit, precision) > 0:
@ -294,14 +405,17 @@ def update_against_account(voucher_type, voucher_no):
if d.against != new_against: if d.against != new_against:
frappe.db.set_value("GL Entry", d.name, "against", new_against) frappe.db.set_value("GL Entry", d.name, "against", new_against)
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"]) frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"])
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"]) frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
def rename_gle_sle_docs(): def rename_gle_sle_docs():
for doctype in ["GL Entry", "Stock Ledger Entry"]: for doctype in ["GL Entry", "Stock Ledger Entry"]:
rename_temporarily_named_docs(doctype) rename_temporarily_named_docs(doctype)
def rename_temporarily_named_docs(doctype): def rename_temporarily_named_docs(doctype):
"""Rename temporarily named docs using autoname options""" """Rename temporarily named docs using autoname options"""
docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000) docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000)
@ -312,5 +426,5 @@ def rename_temporarily_named_docs(doctype):
frappe.db.sql( frappe.db.sql(
"UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype), "UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
(newname, oldname), (newname, oldname),
auto_commit=True auto_commit=True,
) )

View File

@ -14,48 +14,68 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
class TestGLEntry(unittest.TestCase): class TestGLEntry(unittest.TestCase):
def test_round_off_entry(self): def test_round_off_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC") frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC")
frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC") frappe.db.set_value(
"Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC"
)
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 100, "_Test Cost Center - _TC", submit=False) "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
100,
"_Test Cost Center - _TC",
submit=False,
)
jv.get("accounts")[0].debit = 100.01 jv.get("accounts")[0].debit = 100.01
jv.flags.ignore_validate = True jv.flags.ignore_validate = True
jv.submit() jv.submit()
round_off_entry = frappe.db.sql("""select name from `tabGL Entry` round_off_entry = frappe.db.sql(
"""select name from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s where voucher_type='Journal Entry' and voucher_no = %s
and account='_Test Write Off - _TC' and cost_center='_Test Cost Center - _TC' and account='_Test Write Off - _TC' and cost_center='_Test Cost Center - _TC'
and debit = 0 and credit = '.01'""", jv.name) and debit = 0 and credit = '.01'""",
jv.name,
)
self.assertTrue(round_off_entry) self.assertTrue(round_off_entry)
def test_rename_entries(self): def test_rename_entries(self):
je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True) je = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True
)
rename_gle_sle_docs() rename_gle_sle_docs()
naming_series = parse_naming_series(parts=frappe.get_meta("GL Entry").autoname.split(".")[:-1]) naming_series = parse_naming_series(parts=frappe.get_meta("GL Entry").autoname.split(".")[:-1])
je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True) je = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True
)
gl_entries = frappe.get_all("GL Entry", gl_entries = frappe.get_all(
"GL Entry",
fields=["name", "to_rename"], fields=["name", "to_rename"],
filters={"voucher_type": "Journal Entry", "voucher_no": je.name}, filters={"voucher_type": "Journal Entry", "voucher_no": je.name},
order_by="creation" order_by="creation",
) )
self.assertTrue(all(entry.to_rename == 1 for entry in gl_entries)) self.assertTrue(all(entry.to_rename == 1 for entry in gl_entries))
old_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0] old_naming_series_current_value = frappe.db.sql(
"SELECT current from tabSeries where name = %s", naming_series
)[0][0]
rename_gle_sle_docs() rename_gle_sle_docs()
new_gl_entries = frappe.get_all("GL Entry", new_gl_entries = frappe.get_all(
"GL Entry",
fields=["name", "to_rename"], fields=["name", "to_rename"],
filters={"voucher_type": "Journal Entry", "voucher_no": je.name}, filters={"voucher_type": "Journal Entry", "voucher_no": je.name},
order_by="creation" order_by="creation",
) )
self.assertTrue(all(entry.to_rename == 0 for entry in new_gl_entries)) self.assertTrue(all(entry.to_rename == 0 for entry in new_gl_entries))
self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries))) self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries)))
new_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0] new_naming_series_current_value = frappe.db.sql(
"SELECT current from tabSeries where name = %s", naming_series
)[0][0]
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value) self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)

View File

@ -33,19 +33,32 @@ class InvoiceDiscounting(AccountsController):
frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting")) frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting"))
def validate_invoices(self): def validate_invoices(self):
discounted_invoices = [record.sales_invoice for record in discounted_invoices = [
frappe.get_all("Discounted Invoice",fields=["sales_invoice"], filters={"docstatus":1})] record.sales_invoice
for record in frappe.get_all(
"Discounted Invoice", fields=["sales_invoice"], filters={"docstatus": 1}
)
]
for record in self.invoices: for record in self.invoices:
if record.sales_invoice in discounted_invoices: if record.sales_invoice in discounted_invoices:
frappe.throw(_("Row({0}): {1} is already discounted in {2}") frappe.throw(
.format(record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent))) _("Row({0}): {1} is already discounted in {2}").format(
record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent)
)
)
actual_outstanding = frappe.db.get_value("Sales Invoice", record.sales_invoice,"outstanding_amount") actual_outstanding = frappe.db.get_value(
if record.outstanding_amount > actual_outstanding : "Sales Invoice", record.sales_invoice, "outstanding_amount"
frappe.throw(_ )
("Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}").format( if record.outstanding_amount > actual_outstanding:
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice))) frappe.throw(
_(
"Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}"
).format(
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)
)
)
def calculate_total_amount(self): def calculate_total_amount(self):
self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices) self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices)
@ -73,24 +86,21 @@ class InvoiceDiscounting(AccountsController):
self.status = "Cancelled" self.status = "Cancelled"
if cancel: if cancel:
self.db_set('status', self.status, update_modified = True) self.db_set("status", self.status, update_modified=True)
def update_sales_invoice(self): def update_sales_invoice(self):
for d in self.invoices: for d in self.invoices:
if self.docstatus == 1: if self.docstatus == 1:
is_discounted = 1 is_discounted = 1
else: else:
discounted_invoice = frappe.db.exists({ discounted_invoice = frappe.db.exists(
"doctype": "Discounted Invoice", {"doctype": "Discounted Invoice", "sales_invoice": d.sales_invoice, "docstatus": 1}
"sales_invoice": d.sales_invoice, )
"docstatus": 1
})
is_discounted = 1 if discounted_invoice else 0 is_discounted = 1 if discounted_invoice else 0
frappe.db.set_value("Sales Invoice", d.sales_invoice, "is_discounted", is_discounted) frappe.db.set_value("Sales Invoice", d.sales_invoice, "is_discounted", is_discounted)
def make_gl_entries(self): def make_gl_entries(self):
company_currency = frappe.get_cached_value('Company', self.company, "default_currency") company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
gl_entries = [] gl_entries = []
invoice_fields = ["debit_to", "party_account_currency", "conversion_rate", "cost_center"] invoice_fields = ["debit_to", "party_account_currency", "conversion_rate", "cost_center"]
@ -102,135 +112,182 @@ class InvoiceDiscounting(AccountsController):
inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, invoice_fields, as_dict=1) inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, invoice_fields, as_dict=1)
if d.outstanding_amount: if d.outstanding_amount:
outstanding_in_company_currency = flt(d.outstanding_amount * inv.conversion_rate, outstanding_in_company_currency = flt(
d.precision("outstanding_amount")) d.outstanding_amount * inv.conversion_rate, d.precision("outstanding_amount")
ar_credit_account_currency = frappe.get_cached_value("Account", self.accounts_receivable_credit, "currency") )
ar_credit_account_currency = frappe.get_cached_value(
"Account", self.accounts_receivable_credit, "currency"
)
gl_entries.append(self.get_gl_dict({ gl_entries.append(
"account": inv.debit_to, self.get_gl_dict(
"party_type": "Customer", {
"party": d.customer, "account": inv.debit_to,
"against": self.accounts_receivable_credit, "party_type": "Customer",
"credit": outstanding_in_company_currency, "party": d.customer,
"credit_in_account_currency": outstanding_in_company_currency \ "against": self.accounts_receivable_credit,
if inv.party_account_currency==company_currency else d.outstanding_amount, "credit": outstanding_in_company_currency,
"cost_center": inv.cost_center, "credit_in_account_currency": outstanding_in_company_currency
"against_voucher": d.sales_invoice, if inv.party_account_currency == company_currency
"against_voucher_type": "Sales Invoice" else d.outstanding_amount,
}, inv.party_account_currency, item=inv)) "cost_center": inv.cost_center,
"against_voucher": d.sales_invoice,
"against_voucher_type": "Sales Invoice",
},
inv.party_account_currency,
item=inv,
)
)
gl_entries.append(self.get_gl_dict({ gl_entries.append(
"account": self.accounts_receivable_credit, self.get_gl_dict(
"party_type": "Customer", {
"party": d.customer, "account": self.accounts_receivable_credit,
"against": inv.debit_to, "party_type": "Customer",
"debit": outstanding_in_company_currency, "party": d.customer,
"debit_in_account_currency": outstanding_in_company_currency \ "against": inv.debit_to,
if ar_credit_account_currency==company_currency else d.outstanding_amount, "debit": outstanding_in_company_currency,
"cost_center": inv.cost_center, "debit_in_account_currency": outstanding_in_company_currency
"against_voucher": d.sales_invoice, if ar_credit_account_currency == company_currency
"against_voucher_type": "Sales Invoice" else d.outstanding_amount,
}, ar_credit_account_currency, item=inv)) "cost_center": inv.cost_center,
"against_voucher": d.sales_invoice,
"against_voucher_type": "Sales Invoice",
},
ar_credit_account_currency,
item=inv,
)
)
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No') make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No")
@frappe.whitelist() @frappe.whitelist()
def create_disbursement_entry(self): def create_disbursement_entry(self):
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Journal Entry' je.voucher_type = "Journal Entry"
je.company = self.company je.company = self.company
je.remark = 'Loan Disbursement entry against Invoice Discounting: ' + self.name je.remark = "Loan Disbursement entry against Invoice Discounting: " + self.name
je.append("accounts", { je.append(
"account": self.bank_account, "accounts",
"debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges), {
"cost_center": erpnext.get_default_cost_center(self.company) "account": self.bank_account,
}) "debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges),
"cost_center": erpnext.get_default_cost_center(self.company),
},
)
if self.bank_charges: if self.bank_charges:
je.append("accounts", { je.append(
"account": self.bank_charges_account, "accounts",
"debit_in_account_currency": flt(self.bank_charges), {
"cost_center": erpnext.get_default_cost_center(self.company) "account": self.bank_charges_account,
}) "debit_in_account_currency": flt(self.bank_charges),
"cost_center": erpnext.get_default_cost_center(self.company),
},
)
je.append("accounts", { je.append(
"account": self.short_term_loan, "accounts",
"credit_in_account_currency": flt(self.total_amount), {
"cost_center": erpnext.get_default_cost_center(self.company), "account": self.short_term_loan,
"reference_type": "Invoice Discounting", "credit_in_account_currency": flt(self.total_amount),
"reference_name": self.name "cost_center": erpnext.get_default_cost_center(self.company),
}) "reference_type": "Invoice Discounting",
"reference_name": self.name,
},
)
for d in self.invoices: for d in self.invoices:
je.append("accounts", { je.append(
"account": self.accounts_receivable_discounted, "accounts",
"debit_in_account_currency": flt(d.outstanding_amount), {
"cost_center": erpnext.get_default_cost_center(self.company), "account": self.accounts_receivable_discounted,
"reference_type": "Invoice Discounting", "debit_in_account_currency": flt(d.outstanding_amount),
"reference_name": self.name, "cost_center": erpnext.get_default_cost_center(self.company),
"party_type": "Customer", "reference_type": "Invoice Discounting",
"party": d.customer "reference_name": self.name,
}) "party_type": "Customer",
"party": d.customer,
},
)
je.append("accounts", { je.append(
"account": self.accounts_receivable_credit, "accounts",
"credit_in_account_currency": flt(d.outstanding_amount), {
"cost_center": erpnext.get_default_cost_center(self.company), "account": self.accounts_receivable_credit,
"reference_type": "Invoice Discounting", "credit_in_account_currency": flt(d.outstanding_amount),
"reference_name": self.name, "cost_center": erpnext.get_default_cost_center(self.company),
"party_type": "Customer", "reference_type": "Invoice Discounting",
"party": d.customer "reference_name": self.name,
}) "party_type": "Customer",
"party": d.customer,
},
)
return je return je
@frappe.whitelist() @frappe.whitelist()
def close_loan(self): def close_loan(self):
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Journal Entry' je.voucher_type = "Journal Entry"
je.company = self.company je.company = self.company
je.remark = 'Loan Settlement entry against Invoice Discounting: ' + self.name je.remark = "Loan Settlement entry against Invoice Discounting: " + self.name
je.append("accounts", { je.append(
"account": self.short_term_loan, "accounts",
"debit_in_account_currency": flt(self.total_amount), {
"cost_center": erpnext.get_default_cost_center(self.company), "account": self.short_term_loan,
"reference_type": "Invoice Discounting", "debit_in_account_currency": flt(self.total_amount),
"reference_name": self.name, "cost_center": erpnext.get_default_cost_center(self.company),
}) "reference_type": "Invoice Discounting",
"reference_name": self.name,
},
)
je.append("accounts", { je.append(
"account": self.bank_account, "accounts",
"credit_in_account_currency": flt(self.total_amount), {
"cost_center": erpnext.get_default_cost_center(self.company) "account": self.bank_account,
}) "credit_in_account_currency": flt(self.total_amount),
"cost_center": erpnext.get_default_cost_center(self.company),
},
)
if getdate(self.loan_end_date) > getdate(nowdate()): if getdate(self.loan_end_date) > getdate(nowdate()):
for d in self.invoices: for d in self.invoices:
outstanding_amount = frappe.db.get_value("Sales Invoice", d.sales_invoice, "outstanding_amount") outstanding_amount = frappe.db.get_value(
"Sales Invoice", d.sales_invoice, "outstanding_amount"
)
if flt(outstanding_amount) > 0: if flt(outstanding_amount) > 0:
je.append("accounts", { je.append(
"account": self.accounts_receivable_discounted, "accounts",
"credit_in_account_currency": flt(outstanding_amount), {
"cost_center": erpnext.get_default_cost_center(self.company), "account": self.accounts_receivable_discounted,
"reference_type": "Invoice Discounting", "credit_in_account_currency": flt(outstanding_amount),
"reference_name": self.name, "cost_center": erpnext.get_default_cost_center(self.company),
"party_type": "Customer", "reference_type": "Invoice Discounting",
"party": d.customer "reference_name": self.name,
}) "party_type": "Customer",
"party": d.customer,
},
)
je.append("accounts", { je.append(
"account": self.accounts_receivable_unpaid, "accounts",
"debit_in_account_currency": flt(outstanding_amount), {
"cost_center": erpnext.get_default_cost_center(self.company), "account": self.accounts_receivable_unpaid,
"reference_type": "Invoice Discounting", "debit_in_account_currency": flt(outstanding_amount),
"reference_name": self.name, "cost_center": erpnext.get_default_cost_center(self.company),
"party_type": "Customer", "reference_type": "Invoice Discounting",
"party": d.customer "reference_name": self.name,
}) "party_type": "Customer",
"party": d.customer,
},
)
return je return je
@frappe.whitelist() @frappe.whitelist()
def get_invoices(filters): def get_invoices(filters):
filters = frappe._dict(json.loads(filters)) filters = frappe._dict(json.loads(filters))
@ -250,7 +307,8 @@ def get_invoices(filters):
if cond: if cond:
where_condition += " and " + " and ".join(cond) where_condition += " and " + " and ".join(cond)
return frappe.db.sql(""" return frappe.db.sql(
"""
select select
name as sales_invoice, name as sales_invoice,
customer, customer,
@ -264,17 +322,26 @@ def get_invoices(filters):
%s %s
and not exists(select di.name from `tabDiscounted Invoice` di and not exists(select di.name from `tabDiscounted Invoice` di
where di.docstatus=1 and di.sales_invoice=si.name) where di.docstatus=1 and di.sales_invoice=si.name)
""" % where_condition, filters, as_dict=1) """
% where_condition,
filters,
as_dict=1,
)
def get_party_account_based_on_invoice_discounting(sales_invoice): def get_party_account_based_on_invoice_discounting(sales_invoice):
party_account = None party_account = None
invoice_discounting = frappe.db.sql(""" invoice_discounting = frappe.db.sql(
"""
select par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status select par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status
from `tabInvoice Discounting` par, `tabDiscounted Invoice` ch from `tabInvoice Discounting` par, `tabDiscounted Invoice` ch
where par.name=ch.parent where par.name=ch.parent
and par.docstatus=1 and par.docstatus=1
and ch.sales_invoice = %s and ch.sales_invoice = %s
""", (sales_invoice), as_dict=1) """,
(sales_invoice),
as_dict=1,
)
if invoice_discounting: if invoice_discounting:
if invoice_discounting[0].status == "Disbursed": if invoice_discounting[0].status == "Disbursed":
party_account = invoice_discounting[0].accounts_receivable_discounted party_account = invoice_discounting[0].accounts_receivable_discounted

View File

@ -3,18 +3,10 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'reference_name', "fieldname": "reference_name",
'internal_links': { "internal_links": {"Sales Invoice": ["invoices", "sales_invoice"]},
'Sales Invoice': ['invoices', 'sales_invoice'] "transactions": [
}, {"label": _("Reference"), "items": ["Sales Invoice"]},
'transactions': [ {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]},
{ ],
'label': _('Reference'),
'items': ['Sales Invoice']
},
{
'label': _('Payment'),
'items': ['Payment Entry', 'Journal Entry']
}
]
} }

View File

@ -14,52 +14,74 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_
class TestInvoiceDiscounting(unittest.TestCase): class TestInvoiceDiscounting(unittest.TestCase):
def setUp(self): def setUp(self):
self.ar_credit = create_account(account_name="_Test Accounts Receivable Credit", parent_account = "Accounts Receivable - _TC", company="_Test Company") self.ar_credit = create_account(
self.ar_discounted = create_account(account_name="_Test Accounts Receivable Discounted", parent_account = "Accounts Receivable - _TC", company="_Test Company") account_name="_Test Accounts Receivable Credit",
self.ar_unpaid = create_account(account_name="_Test Accounts Receivable Unpaid", parent_account = "Accounts Receivable - _TC", company="_Test Company") parent_account="Accounts Receivable - _TC",
self.short_term_loan = create_account(account_name="_Test Short Term Loan", parent_account = "Source of Funds (Liabilities) - _TC", company="_Test Company") company="_Test Company",
self.bank_account = create_account(account_name="_Test Bank 2", parent_account = "Bank Accounts - _TC", company="_Test Company") )
self.bank_charges_account = create_account(account_name="_Test Bank Charges Account", parent_account = "Expenses - _TC", company="_Test Company") self.ar_discounted = create_account(
account_name="_Test Accounts Receivable Discounted",
parent_account="Accounts Receivable - _TC",
company="_Test Company",
)
self.ar_unpaid = create_account(
account_name="_Test Accounts Receivable Unpaid",
parent_account="Accounts Receivable - _TC",
company="_Test Company",
)
self.short_term_loan = create_account(
account_name="_Test Short Term Loan",
parent_account="Source of Funds (Liabilities) - _TC",
company="_Test Company",
)
self.bank_account = create_account(
account_name="_Test Bank 2", parent_account="Bank Accounts - _TC", company="_Test Company"
)
self.bank_charges_account = create_account(
account_name="_Test Bank Charges Account",
parent_account="Expenses - _TC",
company="_Test Company",
)
frappe.db.set_value("Company", "_Test Company", "default_bank_account", self.bank_account) frappe.db.set_value("Company", "_Test Company", "default_bank_account", self.bank_account)
def test_total_amount(self): def test_total_amount(self):
inv1 = create_sales_invoice(rate=200) inv1 = create_sales_invoice(rate=200)
inv2 = create_sales_invoice(rate=500) inv2 = create_sales_invoice(rate=500)
inv_disc = create_invoice_discounting([inv1.name, inv2.name], inv_disc = create_invoice_discounting(
[inv1.name, inv2.name],
do_not_submit=True, do_not_submit=True,
accounts_receivable_credit=self.ar_credit, accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted, accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid, accounts_receivable_unpaid=self.ar_unpaid,
short_term_loan=self.short_term_loan, short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account, bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account bank_account=self.bank_account,
) )
self.assertEqual(inv_disc.total_amount, 700) self.assertEqual(inv_disc.total_amount, 700)
def test_gl_entries_in_base_currency(self): def test_gl_entries_in_base_currency(self):
inv = create_sales_invoice(rate=200) inv = create_sales_invoice(rate=200)
inv_disc = create_invoice_discounting([inv.name], inv_disc = create_invoice_discounting(
[inv.name],
accounts_receivable_credit=self.ar_credit, accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted, accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid, accounts_receivable_unpaid=self.ar_unpaid,
short_term_loan=self.short_term_loan, short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account, bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account bank_account=self.bank_account,
) )
gle = get_gl_entries("Invoice Discounting", inv_disc.name) gle = get_gl_entries("Invoice Discounting", inv_disc.name)
expected_gle = { expected_gle = {inv.debit_to: [0.0, 200], self.ar_credit: [200, 0.0]}
inv.debit_to: [0.0, 200],
self.ar_credit: [200, 0.0]
}
for i, gle in enumerate(gle): for i, gle in enumerate(gle):
self.assertEqual([gle.debit, gle.credit], expected_gle.get(gle.account)) self.assertEqual([gle.debit, gle.credit], expected_gle.get(gle.account))
def test_loan_on_submit(self): def test_loan_on_submit(self):
inv = create_sales_invoice(rate=300) inv = create_sales_invoice(rate=300)
inv_disc = create_invoice_discounting([inv.name], inv_disc = create_invoice_discounting(
[inv.name],
accounts_receivable_credit=self.ar_credit, accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted, accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid, accounts_receivable_unpaid=self.ar_unpaid,
@ -67,28 +89,33 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account, bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account, bank_account=self.bank_account,
start=nowdate(), start=nowdate(),
period=60 period=60,
) )
self.assertEqual(inv_disc.status, "Sanctioned") self.assertEqual(inv_disc.status, "Sanctioned")
self.assertEqual(inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period)) self.assertEqual(
inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period)
)
def test_on_disbursed(self): def test_on_disbursed(self):
inv = create_sales_invoice(rate=500) inv = create_sales_invoice(rate=500)
inv_disc = create_invoice_discounting([inv.name], inv_disc = create_invoice_discounting(
[inv.name],
accounts_receivable_credit=self.ar_credit, accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted, accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid, accounts_receivable_unpaid=self.ar_unpaid,
short_term_loan=self.short_term_loan, short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account, bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account, bank_account=self.bank_account,
bank_charges=100 bank_charges=100,
) )
je = inv_disc.create_disbursement_entry() je = inv_disc.create_disbursement_entry()
self.assertEqual(je.accounts[0].account, self.bank_account) self.assertEqual(je.accounts[0].account, self.bank_account)
self.assertEqual(je.accounts[0].debit_in_account_currency, flt(inv_disc.total_amount) - flt(inv_disc.bank_charges)) self.assertEqual(
je.accounts[0].debit_in_account_currency,
flt(inv_disc.total_amount) - flt(inv_disc.bank_charges),
)
self.assertEqual(je.accounts[1].account, self.bank_charges_account) self.assertEqual(je.accounts[1].account, self.bank_charges_account)
self.assertEqual(je.accounts[1].debit_in_account_currency, flt(inv_disc.bank_charges)) self.assertEqual(je.accounts[1].debit_in_account_currency, flt(inv_disc.bank_charges))
@ -102,7 +129,6 @@ class TestInvoiceDiscounting(unittest.TestCase):
self.assertEqual(je.accounts[4].account, self.ar_credit) self.assertEqual(je.accounts[4].account, self.ar_credit)
self.assertEqual(je.accounts[4].credit_in_account_currency, flt(inv.outstanding_amount)) self.assertEqual(je.accounts[4].credit_in_account_currency, flt(inv.outstanding_amount))
je.posting_date = nowdate() je.posting_date = nowdate()
je.submit() je.submit()
@ -114,7 +140,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
def test_on_close_after_loan_period(self): def test_on_close_after_loan_period(self):
inv = create_sales_invoice(rate=600) inv = create_sales_invoice(rate=600)
inv_disc = create_invoice_discounting([inv.name], inv_disc = create_invoice_discounting(
[inv.name],
accounts_receivable_credit=self.ar_credit, accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted, accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid, accounts_receivable_unpaid=self.ar_unpaid,
@ -122,8 +149,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account, bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account, bank_account=self.bank_account,
start=nowdate(), start=nowdate(),
period=60 period=60,
) )
je1 = inv_disc.create_disbursement_entry() je1 = inv_disc.create_disbursement_entry()
je1.posting_date = nowdate() je1.posting_date = nowdate()
@ -151,7 +178,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
def test_on_close_after_loan_period_after_inv_payment(self): def test_on_close_after_loan_period_after_inv_payment(self):
inv = create_sales_invoice(rate=600) inv = create_sales_invoice(rate=600)
inv_disc = create_invoice_discounting([inv.name], inv_disc = create_invoice_discounting(
[inv.name],
accounts_receivable_credit=self.ar_credit, accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted, accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid, accounts_receivable_unpaid=self.ar_unpaid,
@ -159,8 +187,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account, bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account, bank_account=self.bank_account,
start=nowdate(), start=nowdate(),
period=60 period=60,
) )
je1 = inv_disc.create_disbursement_entry() je1 = inv_disc.create_disbursement_entry()
je1.posting_date = nowdate() je1.posting_date = nowdate()
@ -183,7 +211,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
def test_on_close_before_loan_period(self): def test_on_close_before_loan_period(self):
inv = create_sales_invoice(rate=700) inv = create_sales_invoice(rate=700)
inv_disc = create_invoice_discounting([inv.name], inv_disc = create_invoice_discounting(
[inv.name],
accounts_receivable_credit=self.ar_credit, accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted, accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid, accounts_receivable_unpaid=self.ar_unpaid,
@ -191,7 +220,7 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account, bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account, bank_account=self.bank_account,
start=add_days(nowdate(), -80), start=add_days(nowdate(), -80),
period=60 period=60,
) )
je1 = inv_disc.create_disbursement_entry() je1 = inv_disc.create_disbursement_entry()
@ -209,16 +238,17 @@ class TestInvoiceDiscounting(unittest.TestCase):
self.assertEqual(je2.accounts[1].credit_in_account_currency, flt(inv_disc.total_amount)) self.assertEqual(je2.accounts[1].credit_in_account_currency, flt(inv_disc.total_amount))
def test_make_payment_before_loan_period(self): def test_make_payment_before_loan_period(self):
#it has problem # it has problem
inv = create_sales_invoice(rate=700) inv = create_sales_invoice(rate=700)
inv_disc = create_invoice_discounting([inv.name], inv_disc = create_invoice_discounting(
accounts_receivable_credit=self.ar_credit, [inv.name],
accounts_receivable_discounted=self.ar_discounted, accounts_receivable_credit=self.ar_credit,
accounts_receivable_unpaid=self.ar_unpaid, accounts_receivable_discounted=self.ar_discounted,
short_term_loan=self.short_term_loan, accounts_receivable_unpaid=self.ar_unpaid,
bank_charges_account=self.bank_charges_account, short_term_loan=self.short_term_loan,
bank_account=self.bank_account bank_charges_account=self.bank_charges_account,
) bank_account=self.bank_account,
)
je = inv_disc.create_disbursement_entry() je = inv_disc.create_disbursement_entry()
inv_disc.reload() inv_disc.reload()
je.posting_date = nowdate() je.posting_date = nowdate()
@ -232,26 +262,31 @@ class TestInvoiceDiscounting(unittest.TestCase):
je_on_payment.submit() je_on_payment.submit()
self.assertEqual(je_on_payment.accounts[0].account, self.ar_discounted) self.assertEqual(je_on_payment.accounts[0].account, self.ar_discounted)
self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)) self.assertEqual(
je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
)
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account) self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)) self.assertEqual(
je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
)
inv.reload() inv.reload()
self.assertEqual(inv.outstanding_amount, 0) self.assertEqual(inv.outstanding_amount, 0)
def test_make_payment_before_after_period(self): def test_make_payment_before_after_period(self):
#it has problem # it has problem
inv = create_sales_invoice(rate=700) inv = create_sales_invoice(rate=700)
inv_disc = create_invoice_discounting([inv.name], inv_disc = create_invoice_discounting(
accounts_receivable_credit=self.ar_credit, [inv.name],
accounts_receivable_discounted=self.ar_discounted, accounts_receivable_credit=self.ar_credit,
accounts_receivable_unpaid=self.ar_unpaid, accounts_receivable_discounted=self.ar_discounted,
short_term_loan=self.short_term_loan, accounts_receivable_unpaid=self.ar_unpaid,
bank_charges_account=self.bank_charges_account, short_term_loan=self.short_term_loan,
bank_account=self.bank_account, bank_charges_account=self.bank_charges_account,
loan_start_date=add_days(nowdate(), -10), bank_account=self.bank_account,
period=5 loan_start_date=add_days(nowdate(), -10),
) period=5,
)
je = inv_disc.create_disbursement_entry() je = inv_disc.create_disbursement_entry()
inv_disc.reload() inv_disc.reload()
je.posting_date = nowdate() je.posting_date = nowdate()
@ -269,9 +304,13 @@ class TestInvoiceDiscounting(unittest.TestCase):
je_on_payment.submit() je_on_payment.submit()
self.assertEqual(je_on_payment.accounts[0].account, self.ar_unpaid) self.assertEqual(je_on_payment.accounts[0].account, self.ar_unpaid)
self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)) self.assertEqual(
je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
)
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account) self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)) self.assertEqual(
je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
)
inv.reload() inv.reload()
self.assertEqual(inv.outstanding_amount, 0) self.assertEqual(inv.outstanding_amount, 0)
@ -287,17 +326,15 @@ def create_invoice_discounting(invoices, **args):
inv_disc.accounts_receivable_credit = args.accounts_receivable_credit inv_disc.accounts_receivable_credit = args.accounts_receivable_credit
inv_disc.accounts_receivable_discounted = args.accounts_receivable_discounted inv_disc.accounts_receivable_discounted = args.accounts_receivable_discounted
inv_disc.accounts_receivable_unpaid = args.accounts_receivable_unpaid inv_disc.accounts_receivable_unpaid = args.accounts_receivable_unpaid
inv_disc.short_term_loan=args.short_term_loan inv_disc.short_term_loan = args.short_term_loan
inv_disc.bank_charges_account=args.bank_charges_account inv_disc.bank_charges_account = args.bank_charges_account
inv_disc.bank_account=args.bank_account inv_disc.bank_account = args.bank_account
inv_disc.loan_start_date = args.start or nowdate() inv_disc.loan_start_date = args.start or nowdate()
inv_disc.loan_period = args.period or 30 inv_disc.loan_period = args.period or 30
inv_disc.bank_charges = flt(args.bank_charges) inv_disc.bank_charges = flt(args.bank_charges)
for d in invoices: for d in invoices:
inv_disc.append("invoices", { inv_disc.append("invoices", {"sales_invoice": d})
"sales_invoice": d
})
inv_disc.insert() inv_disc.insert()
if not args.do_not_submit: if not args.do_not_submit:

View File

@ -13,20 +13,28 @@ class ItemTaxTemplate(Document):
def autoname(self): def autoname(self):
if self.company and self.title: if self.company and self.title:
abbr = frappe.get_cached_value('Company', self.company, 'abbr') abbr = frappe.get_cached_value("Company", self.company, "abbr")
self.name = '{0} - {1}'.format(self.title, abbr) self.name = "{0} - {1}".format(self.title, abbr)
def validate_tax_accounts(self): def validate_tax_accounts(self):
"""Check whether Tax Rate is not entered twice for same Tax Type""" """Check whether Tax Rate is not entered twice for same Tax Type"""
check_list = [] check_list = []
for d in self.get('taxes'): for d in self.get("taxes"):
if d.tax_type: if d.tax_type:
account_type = frappe.db.get_value("Account", d.tax_type, "account_type") account_type = frappe.db.get_value("Account", d.tax_type, "account_type")
if account_type not in ['Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation']: if account_type not in [
"Tax",
"Chargeable",
"Income Account",
"Expense Account",
"Expenses Included In Valuation",
]:
frappe.throw( frappe.throw(
_("Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable").format( _(
d.idx)) "Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable"
).format(d.idx)
)
else: else:
if d.tax_type in check_list: if d.tax_type in check_list:
frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type)) frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type))

View File

@ -3,23 +3,11 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'item_tax_template', "fieldname": "item_tax_template",
'transactions': [ "transactions": [
{ {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
'label': _('Pre Sales'), {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
'items': ['Quotation', 'Supplier Quotation'] {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]},
}, {"label": _("Stock"), "items": ["Item Groups", "Item"]},
{ ],
'label': _('Sales'),
'items': ['Sales Invoice', 'Sales Order', 'Delivery Note']
},
{
'label': _('Purchase'),
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
},
{
'label': _('Stock'),
'items': ['Item Groups', 'Item']
}
]
} }

File diff suppressed because it is too large Load Diff

View File

@ -39,14 +39,25 @@ class TestJournalEntry(unittest.TestCase):
test_voucher.submit() test_voucher.submit()
if test_voucher.doctype == "Journal Entry": if test_voucher.doctype == "Journal Entry":
self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` self.assertTrue(
frappe.db.sql(
"""select name from `tabJournal Entry Account`
where account = %s and docstatus = 1 and parent = %s""", where account = %s and docstatus = 1 and parent = %s""",
("_Test Receivable - _TC", test_voucher.name))) ("_Test Receivable - _TC", test_voucher.name),
)
)
self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account` self.assertFalse(
where reference_type = %s and reference_name = %s""", (test_voucher.doctype, test_voucher.name))) frappe.db.sql(
"""select name from `tabJournal Entry Account`
where reference_type = %s and reference_name = %s""",
(test_voucher.doctype, test_voucher.name),
)
)
base_jv.get("accounts")[0].is_advance = "Yes" if (test_voucher.doctype in ["Sales Order", "Purchase Order"]) else "No" base_jv.get("accounts")[0].is_advance = (
"Yes" if (test_voucher.doctype in ["Sales Order", "Purchase Order"]) else "No"
)
base_jv.get("accounts")[0].set("reference_type", test_voucher.doctype) base_jv.get("accounts")[0].set("reference_type", test_voucher.doctype)
base_jv.get("accounts")[0].set("reference_name", test_voucher.name) base_jv.get("accounts")[0].set("reference_name", test_voucher.name)
base_jv.insert() base_jv.insert()
@ -54,18 +65,28 @@ class TestJournalEntry(unittest.TestCase):
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name) submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` self.assertTrue(
where reference_type = %s and reference_name = %s and {0}=400""".format(dr_or_cr), frappe.db.sql(
(submitted_voucher.doctype, submitted_voucher.name))) """select name from `tabJournal Entry Account`
where reference_type = %s and reference_name = %s and {0}=400""".format(
dr_or_cr
),
(submitted_voucher.doctype, submitted_voucher.name),
)
)
if base_jv.get("accounts")[0].is_advance == "Yes": if base_jv.get("accounts")[0].is_advance == "Yes":
self.advance_paid_testcase(base_jv, submitted_voucher, dr_or_cr) self.advance_paid_testcase(base_jv, submitted_voucher, dr_or_cr)
self.cancel_against_voucher_testcase(submitted_voucher) self.cancel_against_voucher_testcase(submitted_voucher)
def advance_paid_testcase(self, base_jv, test_voucher, dr_or_cr): def advance_paid_testcase(self, base_jv, test_voucher, dr_or_cr):
#Test advance paid field # Test advance paid field
advance_paid = frappe.db.sql("""select advance_paid from `tab%s` advance_paid = frappe.db.sql(
where name=%s""" % (test_voucher.doctype, '%s'), (test_voucher.name)) """select advance_paid from `tab%s`
where name=%s"""
% (test_voucher.doctype, "%s"),
(test_voucher.name),
)
payment_against_order = base_jv.get("accounts")[0].get(dr_or_cr) payment_against_order = base_jv.get("accounts")[0].get(dr_or_cr)
self.assertTrue(flt(advance_paid[0][0]) == flt(payment_against_order)) self.assertTrue(flt(advance_paid[0][0]) == flt(payment_against_order))
@ -74,13 +95,19 @@ class TestJournalEntry(unittest.TestCase):
if test_voucher.doctype == "Journal Entry": if test_voucher.doctype == "Journal Entry":
# if test_voucher is a Journal Entry, test cancellation of test_voucher # if test_voucher is a Journal Entry, test cancellation of test_voucher
test_voucher.cancel() test_voucher.cancel()
self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account` self.assertFalse(
where reference_type='Journal Entry' and reference_name=%s""", test_voucher.name)) frappe.db.sql(
"""select name from `tabJournal Entry Account`
where reference_type='Journal Entry' and reference_name=%s""",
test_voucher.name,
)
)
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]: elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher # if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
frappe.db.set_value("Accounts Settings", "Accounts Settings", frappe.db.set_value(
"unlink_advance_payment_on_cancelation_of_order", 0) "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0
)
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name) submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel) self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
@ -89,7 +116,10 @@ class TestJournalEntry(unittest.TestCase):
stock_account = get_inventory_account(company) stock_account = get_inventory_account(company)
from erpnext.accounts.utils import get_stock_and_account_balance from erpnext.accounts.utils import get_stock_and_account_balance
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
stock_account, nowdate(), company
)
diff = flt(account_bal) - flt(stock_bal) diff = flt(account_bal) - flt(stock_bal)
if not diff: if not diff:
@ -98,19 +128,25 @@ class TestJournalEntry(unittest.TestCase):
jv = frappe.new_doc("Journal Entry") jv = frappe.new_doc("Journal Entry")
jv.company = company jv.company = company
jv.posting_date = nowdate() jv.posting_date = nowdate()
jv.append("accounts", { jv.append(
"account": stock_account, "accounts",
"cost_center": "Main - TCP1", {
"debit_in_account_currency": 0 if diff > 0 else abs(diff), "account": stock_account,
"credit_in_account_currency": diff if diff > 0 else 0 "cost_center": "Main - TCP1",
}) "debit_in_account_currency": 0 if diff > 0 else abs(diff),
"credit_in_account_currency": diff if diff > 0 else 0,
},
)
jv.append("accounts", { jv.append(
"account": "Stock Adjustment - TCP1", "accounts",
"cost_center": "Main - TCP1", {
"debit_in_account_currency": diff if diff > 0 else 0, "account": "Stock Adjustment - TCP1",
"credit_in_account_currency": 0 if diff > 0 else abs(diff) "cost_center": "Main - TCP1",
}) "debit_in_account_currency": diff if diff > 0 else 0,
"credit_in_account_currency": 0 if diff > 0 else abs(diff),
},
)
jv.insert() jv.insert()
if account_bal == stock_bal: if account_bal == stock_bal:
@ -121,16 +157,21 @@ class TestJournalEntry(unittest.TestCase):
jv.cancel() jv.cancel()
def test_multi_currency(self): def test_multi_currency(self):
jv = make_journal_entry("_Test Bank USD - _TC", jv = make_journal_entry(
"_Test Bank - _TC", 100, exchange_rate=50, save=False) "_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False
)
jv.get("accounts")[1].credit_in_account_currency = 5000 jv.get("accounts")[1].credit_in_account_currency = 5000
jv.submit() jv.submit()
gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, gl_entries = frappe.db.sql(
"""select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""", jv.name, as_dict=1) order by account asc""",
jv.name,
as_dict=1,
)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
@ -140,33 +181,42 @@ class TestJournalEntry(unittest.TestCase):
"debit": 5000, "debit": 5000,
"debit_in_account_currency": 100, "debit_in_account_currency": 100,
"credit": 0, "credit": 0,
"credit_in_account_currency": 0 "credit_in_account_currency": 0,
}, },
"_Test Bank - _TC": { "_Test Bank - _TC": {
"account_currency": "INR", "account_currency": "INR",
"debit": 0, "debit": 0,
"debit_in_account_currency": 0, "debit_in_account_currency": 0,
"credit": 5000, "credit": 5000,
"credit_in_account_currency": 5000 "credit_in_account_currency": 5000,
} },
} }
for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): for field in (
"account_currency",
"debit",
"debit_in_account_currency",
"credit",
"credit_in_account_currency",
):
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field]) self.assertEqual(expected_values[gle.account][field], gle[field])
# cancel # cancel
jv.cancel() jv.cancel()
gle = frappe.db.sql("""select name from `tabGL Entry` gle = frappe.db.sql(
where voucher_type='Sales Invoice' and voucher_no=%s""", jv.name) """select name from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no=%s""",
jv.name,
)
self.assertFalse(gle) self.assertFalse(gle)
def test_reverse_journal_entry(self): def test_reverse_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank USD - _TC",
"Sales - _TC", 100, exchange_rate=50, save=False) jv = make_journal_entry("_Test Bank USD - _TC", "Sales - _TC", 100, exchange_rate=50, save=False)
jv.get("accounts")[1].credit_in_account_currency = 5000 jv.get("accounts")[1].credit_in_account_currency = 5000
jv.get("accounts")[1].exchange_rate = 1 jv.get("accounts")[1].exchange_rate = 1
@ -176,15 +226,17 @@ class TestJournalEntry(unittest.TestCase):
rjv.posting_date = nowdate() rjv.posting_date = nowdate()
rjv.submit() rjv.submit()
gl_entries = frappe.db.sql(
gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, """select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""", rjv.name, as_dict=1) order by account asc""",
rjv.name,
as_dict=1,
)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
expected_values = { expected_values = {
"_Test Bank USD - _TC": { "_Test Bank USD - _TC": {
"account_currency": "USD", "account_currency": "USD",
@ -199,44 +251,38 @@ class TestJournalEntry(unittest.TestCase):
"debit_in_account_currency": 5000, "debit_in_account_currency": 5000,
"credit": 0, "credit": 0,
"credit_in_account_currency": 0, "credit_in_account_currency": 0,
} },
} }
for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): for field in (
"account_currency",
"debit",
"debit_in_account_currency",
"credit",
"credit_in_account_currency",
):
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field]) self.assertEqual(expected_values[gle.account][field], gle[field])
def test_disallow_change_in_account_currency_for_a_party(self): def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD # create jv in USD
jv = make_journal_entry("_Test Bank USD - _TC", jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False)
"_Test Receivable USD - _TC", 100, save=False)
jv.accounts[1].update({ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
"party_type": "Customer",
"party": "_Test Customer USD"
})
jv.submit() jv.submit()
# create jv in USD, but account currency in INR # create jv in USD, but account currency in INR
jv = make_journal_entry("_Test Bank - _TC", jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False)
"_Test Receivable - _TC", 100, save=False)
jv.accounts[1].update({ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
"party_type": "Customer",
"party": "_Test Customer USD"
})
self.assertRaises(InvalidAccountCurrency, jv.submit) self.assertRaises(InvalidAccountCurrency, jv.submit)
# back in USD # back in USD
jv = make_journal_entry("_Test Bank USD - _TC", jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False)
"_Test Receivable USD - _TC", 100, save=False)
jv.accounts[1].update({ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
"party_type": "Customer",
"party": "_Test Customer USD"
})
jv.submit() jv.submit()
@ -245,13 +291,27 @@ class TestJournalEntry(unittest.TestCase):
frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1) frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1)
frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1) frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1)
frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1) frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1)
jv = make_journal_entry("Sales Expenses - _TC", "Buildings - _TC", 100, posting_date=nowdate(), cost_center = "Main - _TC", save=False) jv = make_journal_entry(
"Sales Expenses - _TC",
"Buildings - _TC",
100,
posting_date=nowdate(),
cost_center="Main - _TC",
save=False,
)
jv.voucher_type = "Inter Company Journal Entry" jv.voucher_type = "Inter Company Journal Entry"
jv.multi_currency = 0 jv.multi_currency = 0
jv.insert() jv.insert()
jv.submit() jv.submit()
jv1 = make_journal_entry("Sales Expenses - _TC1", "Buildings - _TC1", 100, posting_date=nowdate(), cost_center = "Main - _TC1", save=False) jv1 = make_journal_entry(
"Sales Expenses - _TC1",
"Buildings - _TC1",
100,
posting_date=nowdate(),
cost_center="Main - _TC1",
save=False,
)
jv1.inter_company_journal_entry_reference = jv.name jv1.inter_company_journal_entry_reference = jv.name
jv1.company = "_Test Company 1" jv1.company = "_Test Company 1"
jv1.voucher_type = "Inter Company Journal Entry" jv1.voucher_type = "Inter Company Journal Entry"
@ -273,9 +333,12 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_with_cost_centre(self): def test_jv_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
cost_center = "_Test Cost Center for BS Account - _TC" cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center = cost_center, save=False) jv = make_journal_entry(
"_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center=cost_center, save=False
)
jv.voucher_type = "Bank Entry" jv.voucher_type = "Bank Entry"
jv.multi_currency = 0 jv.multi_currency = 0
jv.cheque_no = "112233" jv.cheque_no = "112233"
@ -284,17 +347,17 @@ class TestJournalEntry(unittest.TestCase):
jv.submit() jv.submit()
expected_values = { expected_values = {
"_Test Cash - _TC": { "_Test Cash - _TC": {"cost_center": cost_center},
"cost_center": cost_center "_Test Bank - _TC": {"cost_center": cost_center},
},
"_Test Bank - _TC": {
"cost_center": cost_center
}
} }
gl_entries = frappe.db.sql("""select account, cost_center, debit, credit gl_entries = frappe.db.sql(
"""select account, cost_center, debit, credit
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""", jv.name, as_dict=1) order by account asc""",
jv.name,
as_dict=1,
)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
@ -305,11 +368,13 @@ class TestJournalEntry(unittest.TestCase):
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}): if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}):
project = make_project({ project = make_project(
'project_name': 'Journal Entry Project', {
'project_template_name': 'Test Project Template', "project_name": "Journal Entry Project",
'start_date': '2020-01-01' "project_template_name": "Test Project Template",
}) "start_date": "2020-01-01",
}
)
project_name = project.name project_name = project.name
else: else:
project_name = frappe.get_value("Project", {"project_name": "_Test Project"}) project_name = frappe.get_value("Project", {"project_name": "_Test Project"})
@ -325,17 +390,17 @@ class TestJournalEntry(unittest.TestCase):
jv.submit() jv.submit()
expected_values = { expected_values = {
"_Test Cash - _TC": { "_Test Cash - _TC": {"project": project_name},
"project": project_name "_Test Bank - _TC": {"project": project_name},
},
"_Test Bank - _TC": {
"project": project_name
}
} }
gl_entries = frappe.db.sql("""select account, project, debit, credit gl_entries = frappe.db.sql(
"""select account, project, debit, credit
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""", jv.name, as_dict=1) order by account asc""",
jv.name,
as_dict=1,
)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
@ -345,9 +410,12 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_account_and_party_balance_with_cost_centre(self): def test_jv_account_and_party_balance_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
cost_center = "_Test Cost Center for BS Account - _TC" cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center = cost_center, save=False) jv = make_journal_entry(
"_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center=cost_center, save=False
)
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center) account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center)
jv.voucher_type = "Bank Entry" jv.voucher_type = "Bank Entry"
jv.multi_currency = 0 jv.multi_currency = 0
@ -360,7 +428,18 @@ class TestJournalEntry(unittest.TestCase):
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center) account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center)
self.assertEqual(expected_account_balance, account_balance) self.assertEqual(expected_account_balance, account_balance)
def make_journal_entry(account1, account2, amount, cost_center=None, posting_date=None, exchange_rate=1, save=True, submit=False, project=None):
def make_journal_entry(
account1,
account2,
amount,
cost_center=None,
posting_date=None,
exchange_rate=1,
save=True,
submit=False,
project=None,
):
if not cost_center: if not cost_center:
cost_center = "_Test Cost Center - _TC" cost_center = "_Test Cost Center - _TC"
@ -369,23 +448,27 @@ def make_journal_entry(account1, account2, amount, cost_center=None, posting_dat
jv.company = "_Test Company" jv.company = "_Test Company"
jv.user_remark = "test" jv.user_remark = "test"
jv.multi_currency = 1 jv.multi_currency = 1
jv.set("accounts", [ jv.set(
{ "accounts",
"account": account1, [
"cost_center": cost_center, {
"project": project, "account": account1,
"debit_in_account_currency": amount if amount > 0 else 0, "cost_center": cost_center,
"credit_in_account_currency": abs(amount) if amount < 0 else 0, "project": project,
"exchange_rate": exchange_rate "debit_in_account_currency": amount if amount > 0 else 0,
}, { "credit_in_account_currency": abs(amount) if amount < 0 else 0,
"account": account2, "exchange_rate": exchange_rate,
"cost_center": cost_center, },
"project": project, {
"credit_in_account_currency": amount if amount > 0 else 0, "account": account2,
"debit_in_account_currency": abs(amount) if amount < 0 else 0, "cost_center": cost_center,
"exchange_rate": exchange_rate "project": project,
} "credit_in_account_currency": amount if amount > 0 else 0,
]) "debit_in_account_currency": abs(amount) if amount < 0 else 0,
"exchange_rate": exchange_rate,
},
],
)
if save or submit: if save or submit:
jv.insert() jv.insert()
@ -394,4 +477,5 @@ def make_journal_entry(account1, account2, amount, cost_center=None, posting_dat
return jv return jv
test_records = frappe.get_test_records('Journal Entry')
test_records = frappe.get_test_records("Journal Entry")

View File

@ -9,6 +9,7 @@ from frappe.model.document import Document
class JournalEntryTemplate(Document): class JournalEntryTemplate(Document):
pass pass
@frappe.whitelist() @frappe.whitelist()
def get_naming_series(): def get_naming_series():
return frappe.get_meta("Journal Entry").get_field("naming_series").options return frappe.get_meta("Journal Entry").get_field("naming_series").options

View File

@ -15,9 +15,7 @@ class LedgerMerge(Document):
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw( frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive"))
_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive")
)
enqueued_jobs = [d.get("job_name") for d in get_info()] enqueued_jobs = [d.get("job_name") for d in get_info()]
@ -35,10 +33,12 @@ class LedgerMerge(Document):
return False return False
@frappe.whitelist() @frappe.whitelist()
def form_start_merge(docname): def form_start_merge(docname):
return frappe.get_doc("Ledger Merge", docname).start_merge() return frappe.get_doc("Ledger Merge", docname).start_merge()
def start_merge(docname): def start_merge(docname):
ledger_merge = frappe.get_doc("Ledger Merge", docname) ledger_merge = frappe.get_doc("Ledger Merge", docname)
successful_merges = 0 successful_merges = 0
@ -51,26 +51,24 @@ def start_merge(docname):
ledger_merge.account, ledger_merge.account,
ledger_merge.is_group, ledger_merge.is_group,
ledger_merge.root_type, ledger_merge.root_type,
ledger_merge.company ledger_merge.company,
) )
row.db_set('merged', 1) row.db_set("merged", 1)
frappe.db.commit() frappe.db.commit()
successful_merges += 1 successful_merges += 1
frappe.publish_realtime("ledger_merge_progress", { frappe.publish_realtime(
"ledger_merge": ledger_merge.name, "ledger_merge_progress",
"current": successful_merges, {"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total},
"total": total
}
) )
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
frappe.log_error(title=ledger_merge.name) frappe.log_error(title=ledger_merge.name)
finally: finally:
if successful_merges == total: if successful_merges == total:
ledger_merge.db_set('status', 'Success') ledger_merge.db_set("status", "Success")
elif successful_merges > 0: elif successful_merges > 0:
ledger_merge.db_set('status', 'Partial Success') ledger_merge.db_set("status", "Partial Success")
else: else:
ledger_merge.db_set('status', 'Error') ledger_merge.db_set("status", "Error")
frappe.publish_realtime("ledger_merge_refresh", {"ledger_merge": ledger_merge.name}) frappe.publish_realtime("ledger_merge_refresh", {"ledger_merge": ledger_merge.name})

View File

@ -31,18 +31,17 @@ class TestLedgerMerge(unittest.TestCase):
acc.company = "_Test Company" acc.company = "_Test Company"
acc.insert() acc.insert()
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "Ledger Merge", {
"company": "_Test Company", "doctype": "Ledger Merge",
"root_type": frappe.db.get_value("Account", "Indirect Test Expenses - _TC", "root_type"), "company": "_Test Company",
"account": "Indirect Expenses - _TC", "root_type": frappe.db.get_value("Account", "Indirect Test Expenses - _TC", "root_type"),
"merge_accounts": [ "account": "Indirect Expenses - _TC",
{ "merge_accounts": [
"account": "Indirect Test Expenses - _TC", {"account": "Indirect Test Expenses - _TC", "account_name": "Indirect Expenses"}
"account_name": "Indirect Expenses" ],
} }
] ).insert(ignore_permissions=True)
}).insert(ignore_permissions=True)
parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account") parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account")
self.assertEqual(parent, "Indirect Test Expenses - _TC") self.assertEqual(parent, "Indirect Test Expenses - _TC")
@ -76,22 +75,18 @@ class TestLedgerMerge(unittest.TestCase):
acc.company = "_Test Company" acc.company = "_Test Company"
acc.insert() acc.insert()
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "Ledger Merge", {
"company": "_Test Company", "doctype": "Ledger Merge",
"root_type": frappe.db.get_value("Account", "Indirect Income - _TC", "root_type"), "company": "_Test Company",
"account": "Indirect Income - _TC", "root_type": frappe.db.get_value("Account", "Indirect Income - _TC", "root_type"),
"merge_accounts": [ "account": "Indirect Income - _TC",
{ "merge_accounts": [
"account": "Indirect Test Income - _TC", {"account": "Indirect Test Income - _TC", "account_name": "Indirect Test Income"},
"account_name": "Indirect Test Income" {"account": "Administrative Test Income - _TC", "account_name": "Administrative Test Income"},
}, ],
{ }
"account": "Administrative Test Income - _TC", ).insert(ignore_permissions=True)
"account_name": "Administrative Test Income"
}
]
}).insert(ignore_permissions=True)
parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account") parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account")
self.assertEqual(parent, "Indirect Test Income - _TC") self.assertEqual(parent, "Indirect Test Income - _TC")
@ -112,7 +107,7 @@ class TestLedgerMerge(unittest.TestCase):
"Indirect Test Expenses - _TC", "Indirect Test Expenses - _TC",
"Administrative Test Expenses - _TC", "Administrative Test Expenses - _TC",
"Indirect Test Income - _TC", "Indirect Test Income - _TC",
"Administrative Test Income - _TC" "Administrative Test Income - _TC",
] ]
for account in test_accounts: for account in test_accounts:
frappe.delete_doc_if_exists("Account", account) frappe.delete_doc_if_exists("Account", account)

View File

@ -8,6 +8,7 @@ from frappe.utils import today
exclude_from_linked_with = True exclude_from_linked_with = True
class LoyaltyPointEntry(Document): class LoyaltyPointEntry(Document):
pass pass
@ -16,18 +17,28 @@ def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=No
if not expiry_date: if not expiry_date:
expiry_date = today() expiry_date = today()
return frappe.db.sql(''' return frappe.db.sql(
"""
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice
from `tabLoyalty Point Entry` from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s where customer=%s and loyalty_program=%s
and expiry_date>=%s and loyalty_points>0 and company=%s and expiry_date>=%s and loyalty_points>0 and company=%s
order by expiry_date order by expiry_date
''', (customer, loyalty_program, expiry_date, company), as_dict=1) """,
(customer, loyalty_program, expiry_date, company),
as_dict=1,
)
def get_redemption_details(customer, loyalty_program, company): def get_redemption_details(customer, loyalty_program, company):
return frappe._dict(frappe.db.sql(''' return frappe._dict(
frappe.db.sql(
"""
select redeem_against, sum(loyalty_points) select redeem_against, sum(loyalty_points)
from `tabLoyalty Point Entry` from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and loyalty_points<0 and company=%s where customer=%s and loyalty_program=%s and loyalty_points<0 and company=%s
group by redeem_against group by redeem_against
''', (customer, loyalty_program, company))) """,
(customer, loyalty_program, company),
)
)

View File

@ -12,39 +12,61 @@ class LoyaltyProgram(Document):
pass pass
def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=None, include_expired_entry=False): def get_loyalty_details(
customer, loyalty_program, expiry_date=None, company=None, include_expired_entry=False
):
if not expiry_date: if not expiry_date:
expiry_date = today() expiry_date = today()
condition = '' condition = ""
if company: if company:
condition = " and company=%s " % frappe.db.escape(company) condition = " and company=%s " % frappe.db.escape(company)
if not include_expired_entry: if not include_expired_entry:
condition += " and expiry_date>='%s' " % expiry_date condition += " and expiry_date>='%s' " % expiry_date
loyalty_point_details = frappe.db.sql('''select sum(loyalty_points) as loyalty_points, loyalty_point_details = frappe.db.sql(
"""select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry` sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s where customer=%s and loyalty_program=%s and posting_date <= %s
{condition} {condition}
group by customer'''.format(condition=condition), group by customer""".format(
(customer, loyalty_program, expiry_date), as_dict=1) condition=condition
),
(customer, loyalty_program, expiry_date),
as_dict=1,
)
if loyalty_point_details: if loyalty_point_details:
return loyalty_point_details[0] return loyalty_point_details[0]
else: else:
return {"loyalty_points": 0, "total_spent": 0} return {"loyalty_points": 0, "total_spent": 0}
@frappe.whitelist()
def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, \
silent=False, include_expired_entry=False, current_transaction_amount=0):
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry))
tier_spent_level = sorted([d.as_dict() for d in loyalty_program.collection_rules], @frappe.whitelist()
key=lambda rule:rule.min_spent, reverse=True) def get_loyalty_program_details_with_points(
customer,
loyalty_program=None,
expiry_date=None,
company=None,
silent=False,
include_expired_entry=False,
current_transaction_amount=0,
):
lp_details = get_loyalty_program_details(
customer, loyalty_program, company=company, silent=silent
)
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
lp_details.update(
get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry)
)
tier_spent_level = sorted(
[d.as_dict() for d in loyalty_program.collection_rules],
key=lambda rule: rule.min_spent,
reverse=True,
)
for i, d in enumerate(tier_spent_level): for i, d in enumerate(tier_spent_level):
if i==0 or (lp_details.total_spent+current_transaction_amount) <= d.min_spent: if i == 0 or (lp_details.total_spent + current_transaction_amount) <= d.min_spent:
lp_details.tier_name = d.tier_name lp_details.tier_name = d.tier_name
lp_details.collection_factor = d.collection_factor lp_details.collection_factor = d.collection_factor
else: else:
@ -52,8 +74,16 @@ def get_loyalty_program_details_with_points(customer, loyalty_program=None, expi
return lp_details return lp_details
@frappe.whitelist() @frappe.whitelist()
def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None, company=None, silent=False, include_expired_entry=False): def get_loyalty_program_details(
customer,
loyalty_program=None,
expiry_date=None,
company=None,
silent=False,
include_expired_entry=False,
):
lp_details = frappe._dict() lp_details = frappe._dict()
if not loyalty_program: if not loyalty_program:
@ -72,6 +102,7 @@ def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None
lp_details.update(loyalty_program.as_dict()) lp_details.update(loyalty_program.as_dict())
return lp_details return lp_details
@frappe.whitelist() @frappe.whitelist()
def get_redeemption_factor(loyalty_program=None, customer=None): def get_redeemption_factor(loyalty_program=None, customer=None):
customer_loyalty_program = None customer_loyalty_program = None
@ -98,13 +129,16 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
else: else:
loyalty_program = frappe.db.get_value("Customer", ref_doc.customer, ["loyalty_program"]) loyalty_program = frappe.db.get_value("Customer", ref_doc.customer, ["loyalty_program"])
if loyalty_program and frappe.db.get_value("Loyalty Program", loyalty_program, ["company"]) !=\ if (
ref_doc.company: loyalty_program
and frappe.db.get_value("Loyalty Program", loyalty_program, ["company"]) != ref_doc.company
):
frappe.throw(_("The Loyalty Program isn't valid for the selected company")) frappe.throw(_("The Loyalty Program isn't valid for the selected company"))
if loyalty_program and points_to_redeem: if loyalty_program and points_to_redeem:
loyalty_program_details = get_loyalty_program_details_with_points(ref_doc.customer, loyalty_program, loyalty_program_details = get_loyalty_program_details_with_points(
posting_date, ref_doc.company) ref_doc.customer, loyalty_program, posting_date, ref_doc.company
)
if points_to_redeem > loyalty_program_details.loyalty_points: if points_to_redeem > loyalty_program_details.loyalty_points:
frappe.throw(_("You don't have enought Loyalty Points to redeem")) frappe.throw(_("You don't have enought Loyalty Points to redeem"))

View File

@ -1,9 +1,5 @@
def get_data(): def get_data():
return { return {
'fieldname': 'loyalty_program', "fieldname": "loyalty_program",
'transactions': [ "transactions": [{"items": ["Sales Invoice", "Customer"]}],
{
'items': ['Sales Invoice', 'Customer']
}
]
} }

View File

@ -19,19 +19,28 @@ class TestLoyaltyProgram(unittest.TestCase):
create_records() create_records()
def test_loyalty_points_earned_single_tier(self): def test_loyalty_points_earned_single_tier(self):
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") frappe.db.set_value(
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
)
# create a new sales invoice # create a new sales invoice
si_original = create_sales_invoice_record() si_original = create_sales_invoice_record()
si_original.insert() si_original.insert()
si_original.submit() si_original.submit()
customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
earned_points = get_points_earned(si_original) earned_points = get_points_earned(si_original)
lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) lpe = frappe.get_doc(
"Loyalty Point Entry",
{
"invoice_type": "Sales Invoice",
"invoice": si_original.name,
"customer": si_original.customer,
},
)
self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program) self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program)
self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier) self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier)
self.assertEqual(lpe.loyalty_points, earned_points) self.assertEqual(lpe.loyalty_points, earned_points)
# add redemption point # add redemption point
@ -43,21 +52,31 @@ class TestLoyaltyProgram(unittest.TestCase):
earned_after_redemption = get_points_earned(si_redeem) earned_after_redemption = get_points_earned(si_redeem)
lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name}) lpe_redeem = frappe.get_doc(
lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) "Loyalty Point Entry",
{"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "redeem_against": lpe.name},
)
lpe_earn = frappe.get_doc(
"Loyalty Point Entry",
{"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "name": ["!=", lpe_redeem.name]},
)
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption) self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points)) self.assertEqual(lpe_redeem.loyalty_points, (-1 * earned_points))
# cancel and delete # cancel and delete
for d in [si_redeem, si_original]: for d in [si_redeem, si_original]:
d.cancel() d.cancel()
def test_loyalty_points_earned_multiple_tier(self): def test_loyalty_points_earned_multiple_tier(self):
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty") frappe.db.set_value(
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty"
)
# assign multiple tier program to the customer # assign multiple tier program to the customer
customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
customer.loyalty_program = frappe.get_doc('Loyalty Program', {'loyalty_program_name': 'Test Multiple Loyalty'}).name customer.loyalty_program = frappe.get_doc(
"Loyalty Program", {"loyalty_program_name": "Test Multiple Loyalty"}
).name
customer.save() customer.save()
# create a new sales invoice # create a new sales invoice
@ -67,10 +86,17 @@ class TestLoyaltyProgram(unittest.TestCase):
earned_points = get_points_earned(si_original) earned_points = get_points_earned(si_original)
lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) lpe = frappe.get_doc(
"Loyalty Point Entry",
{
"invoice_type": "Sales Invoice",
"invoice": si_original.name,
"customer": si_original.customer,
},
)
self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program) self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program)
self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier) self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier)
self.assertEqual(lpe.loyalty_points, earned_points) self.assertEqual(lpe.loyalty_points, earned_points)
# add redemption point # add redemption point
@ -80,14 +106,20 @@ class TestLoyaltyProgram(unittest.TestCase):
si_redeem.insert() si_redeem.insert()
si_redeem.submit() si_redeem.submit()
customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
earned_after_redemption = get_points_earned(si_redeem) earned_after_redemption = get_points_earned(si_redeem)
lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name}) lpe_redeem = frappe.get_doc(
lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) "Loyalty Point Entry",
{"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "redeem_against": lpe.name},
)
lpe_earn = frappe.get_doc(
"Loyalty Point Entry",
{"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "name": ["!=", lpe_redeem.name]},
)
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption) self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points)) self.assertEqual(lpe_redeem.loyalty_points, (-1 * earned_points))
self.assertEqual(lpe_earn.loyalty_program_tier, customer.loyalty_program_tier) self.assertEqual(lpe_earn.loyalty_program_tier, customer.loyalty_program_tier)
# cancel and delete # cancel and delete
@ -95,23 +127,30 @@ class TestLoyaltyProgram(unittest.TestCase):
d.cancel() d.cancel()
def test_cancel_sales_invoice(self): def test_cancel_sales_invoice(self):
''' cancelling the sales invoice should cancel the earned points''' """cancelling the sales invoice should cancel the earned points"""
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") frappe.db.set_value(
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
)
# create a new sales invoice # create a new sales invoice
si = create_sales_invoice_record() si = create_sales_invoice_record()
si.insert() si.insert()
si.submit() si.submit()
lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si.name, 'customer': si.customer}) lpe = frappe.get_doc(
"Loyalty Point Entry",
{"invoice_type": "Sales Invoice", "invoice": si.name, "customer": si.customer},
)
self.assertEqual(True, not (lpe is None)) self.assertEqual(True, not (lpe is None))
# cancelling sales invoice # cancelling sales invoice
si.cancel() si.cancel()
lpe = frappe.db.exists('Loyalty Point Entry', lpe.name) lpe = frappe.db.exists("Loyalty Point Entry", lpe.name)
self.assertEqual(True, (lpe is None)) self.assertEqual(True, (lpe is None))
def test_sales_invoice_return(self): def test_sales_invoice_return(self):
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") frappe.db.set_value(
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
)
# create a new sales invoice # create a new sales invoice
si_original = create_sales_invoice_record(2) si_original = create_sales_invoice_record(2)
si_original.conversion_rate = flt(1) si_original.conversion_rate = flt(1)
@ -119,7 +158,14 @@ class TestLoyaltyProgram(unittest.TestCase):
si_original.submit() si_original.submit()
earned_points = get_points_earned(si_original) earned_points = get_points_earned(si_original)
lpe_original = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) lpe_original = frappe.get_doc(
"Loyalty Point Entry",
{
"invoice_type": "Sales Invoice",
"invoice": si_original.name,
"customer": si_original.customer,
},
)
self.assertEqual(lpe_original.loyalty_points, earned_points) self.assertEqual(lpe_original.loyalty_points, earned_points)
# create sales invoice return # create sales invoice return
@ -131,10 +177,17 @@ class TestLoyaltyProgram(unittest.TestCase):
si_return.submit() si_return.submit()
# fetch original invoice again as its status would have been updated # fetch original invoice again as its status would have been updated
si_original = frappe.get_doc('Sales Invoice', lpe_original.invoice) si_original = frappe.get_doc("Sales Invoice", lpe_original.invoice)
earned_points = get_points_earned(si_original) earned_points = get_points_earned(si_original)
lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) lpe_after_return = frappe.get_doc(
"Loyalty Point Entry",
{
"invoice_type": "Sales Invoice",
"invoice": si_original.name,
"customer": si_original.customer,
},
)
self.assertEqual(lpe_after_return.loyalty_points, earned_points) self.assertEqual(lpe_after_return.loyalty_points, earned_points)
self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points)) self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points))
@ -143,144 +196,164 @@ class TestLoyaltyProgram(unittest.TestCase):
try: try:
d.cancel() d.cancel()
except frappe.TimestampMismatchError: except frappe.TimestampMismatchError:
frappe.get_doc('Sales Invoice', d.name).cancel() frappe.get_doc("Sales Invoice", d.name).cancel()
def test_loyalty_points_for_dashboard(self): def test_loyalty_points_for_dashboard(self):
doc = frappe.get_doc('Customer', 'Test Loyalty Customer') doc = frappe.get_doc("Customer", "Test Loyalty Customer")
company_wise_info = get_dashboard_info("Customer", doc.name, doc.loyalty_program) company_wise_info = get_dashboard_info("Customer", doc.name, doc.loyalty_program)
for d in company_wise_info: for d in company_wise_info:
self.assertTrue(d.get("loyalty_points")) self.assertTrue(d.get("loyalty_points"))
def get_points_earned(self): def get_points_earned(self):
def get_returned_amount(): def get_returned_amount():
returned_amount = frappe.db.sql(""" returned_amount = frappe.db.sql(
"""
select sum(grand_total) select sum(grand_total)
from `tabSales Invoice` from `tabSales Invoice`
where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
""", self.name) """,
self.name,
)
return abs(flt(returned_amount[0][0])) if returned_amount else 0 return abs(flt(returned_amount[0][0])) if returned_amount else 0
lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company, lp_details = get_loyalty_program_details_with_points(
loyalty_program=self.loyalty_program, expiry_date=self.posting_date, include_expired_entry=True) self.customer,
if lp_details and getdate(lp_details.from_date) <= getdate(self.posting_date) and \ company=self.company,
(not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)): loyalty_program=self.loyalty_program,
expiry_date=self.posting_date,
include_expired_entry=True,
)
if (
lp_details
and getdate(lp_details.from_date) <= getdate(self.posting_date)
and (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date))
):
returned_amount = get_returned_amount() returned_amount = get_returned_amount()
eligible_amount = flt(self.grand_total) - cint(self.loyalty_amount) - returned_amount eligible_amount = flt(self.grand_total) - cint(self.loyalty_amount) - returned_amount
points_earned = cint(eligible_amount/lp_details.collection_factor) points_earned = cint(eligible_amount / lp_details.collection_factor)
return points_earned or 0 return points_earned or 0
def create_sales_invoice_record(qty=1): def create_sales_invoice_record(qty=1):
# return sales invoice doc object # return sales invoice doc object
return frappe.get_doc({ return frappe.get_doc(
"doctype": "Sales Invoice", {
"customer": frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}).name, "doctype": "Sales Invoice",
"company": '_Test Company', "customer": frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"}).name,
"due_date": today(), "company": "_Test Company",
"posting_date": today(), "due_date": today(),
"currency": "INR", "posting_date": today(),
"taxes_and_charges": "", "currency": "INR",
"debit_to": "Debtors - _TC", "taxes_and_charges": "",
"taxes": [], "debit_to": "Debtors - _TC",
"items": [{ "taxes": [],
'doctype': 'Sales Invoice Item', "items": [
'item_code': frappe.get_doc('Item', {'item_name': 'Loyal Item'}).name, {
'qty': qty, "doctype": "Sales Invoice Item",
"rate": 10000, "item_code": frappe.get_doc("Item", {"item_name": "Loyal Item"}).name,
'income_account': 'Sales - _TC', "qty": qty,
'cost_center': 'Main - _TC', "rate": 10000,
'expense_account': 'Cost of Goods Sold - _TC' "income_account": "Sales - _TC",
}] "cost_center": "Main - _TC",
}) "expense_account": "Cost of Goods Sold - _TC",
}
],
}
)
def create_records(): def create_records():
# create a new loyalty Account # create a new loyalty Account
if not frappe.db.exists("Account", "Loyalty - _TC"): if not frappe.db.exists("Account", "Loyalty - _TC"):
frappe.get_doc({ frappe.get_doc(
"doctype": "Account", {
"account_name": "Loyalty", "doctype": "Account",
"parent_account": "Direct Expenses - _TC", "account_name": "Loyalty",
"company": "_Test Company", "parent_account": "Direct Expenses - _TC",
"is_group": 0, "company": "_Test Company",
"account_type": "Expense Account", "is_group": 0,
}).insert() "account_type": "Expense Account",
}
).insert()
# create a new loyalty program Single tier # create a new loyalty program Single tier
if not frappe.db.exists("Loyalty Program","Test Single Loyalty"): if not frappe.db.exists("Loyalty Program", "Test Single Loyalty"):
frappe.get_doc({ frappe.get_doc(
"doctype": "Loyalty Program", {
"loyalty_program_name": "Test Single Loyalty", "doctype": "Loyalty Program",
"auto_opt_in": 1, "loyalty_program_name": "Test Single Loyalty",
"from_date": today(), "auto_opt_in": 1,
"loyalty_program_type": "Single Tier Program", "from_date": today(),
"conversion_factor": 1, "loyalty_program_type": "Single Tier Program",
"expiry_duration": 10, "conversion_factor": 1,
"company": "_Test Company", "expiry_duration": 10,
"cost_center": "Main - _TC", "company": "_Test Company",
"expense_account": "Loyalty - _TC", "cost_center": "Main - _TC",
"collection_rules": [{ "expense_account": "Loyalty - _TC",
'tier_name': 'Silver', "collection_rules": [{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 1000}],
'collection_factor': 1000, }
'min_spent': 1000 ).insert()
}]
}).insert()
# create a new customer # create a new customer
if not frappe.db.exists("Customer","Test Loyalty Customer"): if not frappe.db.exists("Customer", "Test Loyalty Customer"):
frappe.get_doc({ frappe.get_doc(
"customer_group": "_Test Customer Group", {
"customer_name": "Test Loyalty Customer", "customer_group": "_Test Customer Group",
"customer_type": "Individual", "customer_name": "Test Loyalty Customer",
"doctype": "Customer", "customer_type": "Individual",
"territory": "_Test Territory" "doctype": "Customer",
}).insert() "territory": "_Test Territory",
}
).insert()
# create a new loyalty program Multiple tier # create a new loyalty program Multiple tier
if not frappe.db.exists("Loyalty Program","Test Multiple Loyalty"): if not frappe.db.exists("Loyalty Program", "Test Multiple Loyalty"):
frappe.get_doc({ frappe.get_doc(
"doctype": "Loyalty Program", {
"loyalty_program_name": "Test Multiple Loyalty", "doctype": "Loyalty Program",
"auto_opt_in": 1, "loyalty_program_name": "Test Multiple Loyalty",
"from_date": today(), "auto_opt_in": 1,
"loyalty_program_type": "Multiple Tier Program", "from_date": today(),
"conversion_factor": 1, "loyalty_program_type": "Multiple Tier Program",
"expiry_duration": 10, "conversion_factor": 1,
"company": "_Test Company", "expiry_duration": 10,
"cost_center": "Main - _TC", "company": "_Test Company",
"expense_account": "Loyalty - _TC", "cost_center": "Main - _TC",
"collection_rules": [ "expense_account": "Loyalty - _TC",
{ "collection_rules": [
'tier_name': 'Silver', {"tier_name": "Silver", "collection_factor": 1000, "min_spent": 10000},
'collection_factor': 1000, {"tier_name": "Gold", "collection_factor": 1000, "min_spent": 19000},
'min_spent': 10000 ],
}, }
{ ).insert()
'tier_name': 'Gold',
'collection_factor': 1000,
'min_spent': 19000
}
]
}).insert()
# create an item # create an item
if not frappe.db.exists("Item", "Loyal Item"): if not frappe.db.exists("Item", "Loyal Item"):
frappe.get_doc({ frappe.get_doc(
"doctype": "Item", {
"item_code": "Loyal Item", "doctype": "Item",
"item_name": "Loyal Item", "item_code": "Loyal Item",
"item_group": "All Item Groups", "item_name": "Loyal Item",
"company": "_Test Company", "item_group": "All Item Groups",
"is_stock_item": 1, "company": "_Test Company",
"opening_stock": 100, "is_stock_item": 1,
"valuation_rate": 10000, "opening_stock": 100,
}).insert() "valuation_rate": 10000,
}
).insert()
# create item price # create item price
if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}): if not frappe.db.exists(
frappe.get_doc({ "Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}
"doctype": "Item Price", ):
"price_list": "Standard Selling", frappe.get_doc(
"item_code": "Loyal Item", {
"price_list_rate": 10000 "doctype": "Item Price",
}).insert() "price_list": "Standard Selling",
"item_code": "Loyal Item",
"price_list_rate": 10000,
}
).insert()

View File

@ -19,23 +19,35 @@ class ModeofPayment(Document):
for entry in self.accounts: for entry in self.accounts:
accounts_list.append(entry.company) accounts_list.append(entry.company)
if len(accounts_list)!= len(set(accounts_list)): if len(accounts_list) != len(set(accounts_list)):
frappe.throw(_("Same Company is entered more than once")) frappe.throw(_("Same Company is entered more than once"))
def validate_accounts(self): def validate_accounts(self):
for entry in self.accounts: for entry in self.accounts:
"""Error when Company of Ledger account doesn't match with Company Selected""" """Error when Company of Ledger account doesn't match with Company Selected"""
if frappe.db.get_value("Account", entry.default_account, "company") != entry.company: if frappe.db.get_value("Account", entry.default_account, "company") != entry.company:
frappe.throw(_("Account {0} does not match with Company {1} in Mode of Account: {2}") frappe.throw(
.format(entry.default_account, entry.company, self.name)) _("Account {0} does not match with Company {1} in Mode of Account: {2}").format(
entry.default_account, entry.company, self.name
)
)
def validate_pos_mode_of_payment(self): def validate_pos_mode_of_payment(self):
if not self.enabled: if not self.enabled:
pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip pos_profiles = frappe.db.sql(
WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""", (self.name)) """SELECT sip.parent FROM `tabSales Invoice Payment` sip
WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""",
(self.name),
)
pos_profiles = list(map(lambda x: x[0], pos_profiles)) pos_profiles = list(map(lambda x: x[0], pos_profiles))
if pos_profiles: if pos_profiles:
message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \ message = (
Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode." "POS Profile "
+ frappe.bold(", ".join(pos_profiles))
+ " contains \
Mode of Payment "
+ frappe.bold(str(self.name))
+ ". Please remove them to disable this mode."
)
frappe.throw(_(message), title="Not Allowed") frappe.throw(_(message), title="Not Allowed")

View File

@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Mode of Payment') # test_records = frappe.get_test_records('Mode of Payment')
class TestModeofPayment(unittest.TestCase): class TestModeofPayment(unittest.TestCase):
pass pass

View File

@ -11,13 +11,25 @@ from frappe.utils import add_months, flt
class MonthlyDistribution(Document): class MonthlyDistribution(Document):
@frappe.whitelist() @frappe.whitelist()
def get_months(self): def get_months(self):
month_list = ['January','February','March','April','May','June','July','August','September', month_list = [
'October','November','December'] "January",
idx =1 "February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
idx = 1
for m in month_list: for m in month_list:
mnth = self.append('percentages') mnth = self.append("percentages")
mnth.month = m mnth.month = m
mnth.percentage_allocation = 100.0/12 mnth.percentage_allocation = 100.0 / 12
mnth.idx = idx mnth.idx = idx
idx += 1 idx += 1
@ -25,18 +37,15 @@ class MonthlyDistribution(Document):
total = sum(flt(d.percentage_allocation) for d in self.get("percentages")) total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
if flt(total, 2) != 100.0: if flt(total, 2) != 100.0:
frappe.throw(_("Percentage Allocation should be equal to 100%") + \ frappe.throw(
" ({0}%)".format(str(flt(total, 2)))) _("Percentage Allocation should be equal to 100%") + " ({0}%)".format(str(flt(total, 2)))
)
def get_periodwise_distribution_data(distribution_id, period_list, periodicity): def get_periodwise_distribution_data(distribution_id, period_list, periodicity):
doc = frappe.get_doc('Monthly Distribution', distribution_id) doc = frappe.get_doc("Monthly Distribution", distribution_id)
months_to_add = { months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
"Yearly": 12,
"Half-Yearly": 6,
"Quarterly": 3,
"Monthly": 1
}[periodicity]
period_dict = {} period_dict = {}
@ -45,6 +54,7 @@ def get_periodwise_distribution_data(distribution_id, period_list, periodicity):
return period_dict return period_dict
def get_percentage(doc, start_date, period): def get_percentage(doc, start_date, period):
percentage = 0 percentage = 0
months = [start_date.strftime("%B").title()] months = [start_date.strftime("%B").title()]

View File

@ -3,19 +3,14 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'monthly_distribution', "fieldname": "monthly_distribution",
'non_standard_fieldnames': { "non_standard_fieldnames": {
'Sales Person': 'distribution_id', "Sales Person": "distribution_id",
'Territory': 'distribution_id', "Territory": "distribution_id",
'Sales Partner': 'distribution_id', "Sales Partner": "distribution_id",
}, },
'transactions': [ "transactions": [
{ {"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]},
'label': _('Target Details'), {"items": ["Budget"]},
'items': ['Sales Person', 'Territory', 'Sales Partner'] ],
},
{
'items': ['Budget']
}
]
} }

View File

@ -6,7 +6,8 @@ import unittest
import frappe import frappe
test_records = frappe.get_test_records('Monthly Distribution') test_records = frappe.get_test_records("Monthly Distribution")
class TestMonthlyDistribution(unittest.TestCase): class TestMonthlyDistribution(unittest.TestCase):
pass pass

View File

@ -20,9 +20,9 @@ class OpeningInvoiceCreationTool(Document):
def onload(self): def onload(self):
"""Load the Opening Invoice summary""" """Load the Opening Invoice summary"""
summary, max_count = self.get_opening_invoice_summary() summary, max_count = self.get_opening_invoice_summary()
self.set_onload('opening_invoices_summary', summary) self.set_onload("opening_invoices_summary", summary)
self.set_onload('max_count', max_count) self.set_onload("max_count", max_count)
self.set_onload('temporary_opening_account', get_temporary_opening_account(self.company)) self.set_onload("temporary_opening_account", get_temporary_opening_account(self.company))
def get_opening_invoice_summary(self): def get_opening_invoice_summary(self):
def prepare_invoice_summary(doctype, invoices): def prepare_invoice_summary(doctype, invoices):
@ -32,10 +32,7 @@ class OpeningInvoiceCreationTool(Document):
for invoice in invoices: for invoice in invoices:
company = invoice.pop("company") company = invoice.pop("company")
_summary = invoices_summary.get(company, {}) _summary = invoices_summary.get(company, {})
_summary.update({ _summary.update({"currency": company_wise_currency.get(company), doctype: invoice})
"currency": company_wise_currency.get(company),
doctype: invoice
})
invoices_summary.update({company: _summary}) invoices_summary.update({company: _summary})
if invoice.paid_amount: if invoice.paid_amount:
@ -44,17 +41,21 @@ class OpeningInvoiceCreationTool(Document):
outstanding_amount.append(invoice.outstanding_amount) outstanding_amount.append(invoice.outstanding_amount)
if paid_amount or outstanding_amount: if paid_amount or outstanding_amount:
max_count.update({ max_count.update(
doctype: { {
"max_paid": max(paid_amount) if paid_amount else 0.0, doctype: {
"max_due": max(outstanding_amount) if outstanding_amount else 0.0 "max_paid": max(paid_amount) if paid_amount else 0.0,
"max_due": max(outstanding_amount) if outstanding_amount else 0.0,
}
} }
}) )
invoices_summary = {} invoices_summary = {}
max_count = {} max_count = {}
fields = [ fields = [
"company", "count(name) as total_invoices", "sum(outstanding_amount) as outstanding_amount" "company",
"count(name) as total_invoices",
"sum(outstanding_amount) as outstanding_amount",
] ]
companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"]) companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"])
if not companies: if not companies:
@ -62,8 +63,9 @@ class OpeningInvoiceCreationTool(Document):
company_wise_currency = {row.company: row.currency for row in companies} company_wise_currency = {row.company: row.currency for row in companies}
for doctype in ["Sales Invoice", "Purchase Invoice"]: for doctype in ["Sales Invoice", "Purchase Invoice"]:
invoices = frappe.get_all(doctype, filters=dict(is_opening="Yes", docstatus=1), invoices = frappe.get_all(
fields=fields, group_by="company") doctype, filters=dict(is_opening="Yes", docstatus=1), fields=fields, group_by="company"
)
prepare_invoice_summary(doctype, invoices) prepare_invoice_summary(doctype, invoices)
return invoices_summary, max_count return invoices_summary, max_count
@ -74,7 +76,9 @@ class OpeningInvoiceCreationTool(Document):
def set_missing_values(self, row): def set_missing_values(self, row):
row.qty = row.qty or 1.0 row.qty = row.qty or 1.0
row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(self.company) row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(
self.company
)
row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier" row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier"
row.item_name = row.item_name or _("Opening Invoice Item") row.item_name = row.item_name or _("Opening Invoice Item")
row.posting_date = row.posting_date or nowdate() row.posting_date = row.posting_date or nowdate()
@ -85,7 +89,11 @@ class OpeningInvoiceCreationTool(Document):
if self.create_missing_party: if self.create_missing_party:
self.add_party(row.party_type, row.party) self.add_party(row.party_type, row.party)
else: else:
frappe.throw(_("Row #{}: {} {} does not exist.").format(row.idx, frappe.bold(row.party_type), frappe.bold(row.party))) frappe.throw(
_("Row #{}: {} {} does not exist.").format(
row.idx, frappe.bold(row.party_type), frappe.bold(row.party)
)
)
mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices") mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices")
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"): for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
@ -100,12 +108,19 @@ class OpeningInvoiceCreationTool(Document):
self.set_missing_values(row) self.set_missing_values(row)
self.validate_mandatory_invoice_fields(row) self.validate_mandatory_invoice_fields(row)
invoice = self.get_invoice_dict(row) invoice = self.get_invoice_dict(row)
company_details = frappe.get_cached_value('Company', self.company, ["default_currency", "default_letter_head"], as_dict=1) or {} company_details = (
frappe.get_cached_value(
"Company", self.company, ["default_currency", "default_letter_head"], as_dict=1
)
or {}
)
if company_details: if company_details:
invoice.update({ invoice.update(
"currency": company_details.get("default_currency"), {
"letter_head": company_details.get("default_letter_head") "currency": company_details.get("default_currency"),
}) "letter_head": company_details.get("default_letter_head"),
}
)
invoices.append(invoice) invoices.append(invoice)
return invoices return invoices
@ -127,55 +142,61 @@ class OpeningInvoiceCreationTool(Document):
def get_invoice_dict(self, row=None): def get_invoice_dict(self, row=None):
def get_item_dict(): def get_item_dict():
cost_center = row.get('cost_center') or frappe.get_cached_value('Company', self.company, "cost_center") cost_center = row.get("cost_center") or frappe.get_cached_value(
"Company", self.company, "cost_center"
)
if not cost_center: if not cost_center:
frappe.throw(_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))) frappe.throw(
_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))
)
income_expense_account_field = "income_account" if row.party_type == "Customer" else "expense_account" income_expense_account_field = (
"income_account" if row.party_type == "Customer" else "expense_account"
)
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos") default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
rate = flt(row.outstanding_amount) / flt(row.qty) rate = flt(row.outstanding_amount) / flt(row.qty)
item_dict = frappe._dict({ item_dict = frappe._dict(
"uom": default_uom, {
"rate": rate or 0.0, "uom": default_uom,
"qty": row.qty, "rate": rate or 0.0,
"conversion_factor": 1.0, "qty": row.qty,
"item_name": row.item_name or "Opening Invoice Item", "conversion_factor": 1.0,
"description": row.item_name or "Opening Invoice Item", "item_name": row.item_name or "Opening Invoice Item",
income_expense_account_field: row.temporary_opening_account, "description": row.item_name or "Opening Invoice Item",
"cost_center": cost_center income_expense_account_field: row.temporary_opening_account,
}) "cost_center": cost_center,
}
)
for dimension in get_accounting_dimensions(): for dimension in get_accounting_dimensions():
item_dict.update({ item_dict.update({dimension: row.get(dimension)})
dimension: row.get(dimension)
})
return item_dict return item_dict
item = get_item_dict() item = get_item_dict()
invoice = frappe._dict({ invoice = frappe._dict(
"items": [item], {
"is_opening": "Yes", "items": [item],
"set_posting_time": 1, "is_opening": "Yes",
"company": self.company, "set_posting_time": 1,
"cost_center": self.cost_center, "company": self.company,
"due_date": row.due_date, "cost_center": self.cost_center,
"posting_date": row.posting_date, "due_date": row.due_date,
frappe.scrub(row.party_type): row.party, "posting_date": row.posting_date,
"is_pos": 0, frappe.scrub(row.party_type): row.party,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", "is_pos": 0,
"update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559 "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"invoice_number": row.invoice_number, "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
"disable_rounded_total": 1 "invoice_number": row.invoice_number,
}) "disable_rounded_total": 1,
}
)
accounting_dimension = get_accounting_dimensions() accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension: for dimension in accounting_dimension:
invoice.update({ invoice.update({dimension: self.get(dimension) or item.get(dimension)})
dimension: self.get(dimension) or item.get(dimension)
})
return invoice return invoice
@ -201,9 +222,10 @@ class OpeningInvoiceCreationTool(Document):
event="opening_invoice_creation", event="opening_invoice_creation",
job_name=self.name, job_name=self.name,
invoices=invoices, invoices=invoices,
now=frappe.conf.developer_mode or frappe.flags.in_test now=frappe.conf.developer_mode or frappe.flags.in_test,
) )
def start_import(invoices): def start_import(invoices):
errors = 0 errors = 0
names = [] names = []
@ -222,14 +244,22 @@ def start_import(invoices):
except Exception: except Exception:
errors += 1 errors += 1
frappe.db.rollback() frappe.db.rollback()
message = "\n".join(["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) message = "\n".join(
["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]
)
frappe.log_error(title="Error while creating Opening Invoice", message=message) frappe.log_error(title="Error while creating Opening Invoice", message=message)
frappe.db.commit() frappe.db.commit()
if errors: if errors:
frappe.msgprint(_("You had {} errors while creating opening invoices. Check {} for more details") frappe.msgprint(
.format(errors, "<a href='/app/List/Error Log' class='variant-click'>Error Log</a>"), indicator="red", title=_("Error Occured")) _("You had {} errors while creating opening invoices. Check {} for more details").format(
errors, "<a href='/app/List/Error Log' class='variant-click'>Error Log</a>"
),
indicator="red",
title=_("Error Occured"),
)
return names return names
def publish(index, total, doctype): def publish(index, total, doctype):
if total < 50: if total < 50:
return return
@ -237,21 +267,20 @@ def publish(index, total, doctype):
"opening_invoice_creation_progress", "opening_invoice_creation_progress",
dict( dict(
title=_("Opening Invoice Creation In Progress"), title=_("Opening Invoice Creation In Progress"),
message=_('Creating {} out of {} {}').format(index + 1, total, doctype), message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
user=frappe.session.user, user=frappe.session.user,
count=index+1, count=index + 1,
total=total total=total,
)) ),
)
@frappe.whitelist() @frappe.whitelist()
def get_temporary_opening_account(company=None): def get_temporary_opening_account(company=None):
if not company: if not company:
return return
accounts = frappe.get_all("Account", filters={ accounts = frappe.get_all("Account", filters={"company": company, "account_type": "Temporary"})
'company': company,
'account_type': 'Temporary'
})
if not accounts: if not accounts:
frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts")) frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts"))

View File

@ -14,6 +14,7 @@ from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_crea
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(FrappeTestCase): class TestOpeningInvoiceCreationTool(FrappeTestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(self):
@ -22,10 +23,24 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
create_dimension() create_dimension()
return super().setUpClass() return super().setUpClass()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None): def make_invoices(
self,
invoice_type="Sales",
company=None,
party_1=None,
party_2=None,
invoice_number=None,
department=None,
):
doc = frappe.get_single("Opening Invoice Creation Tool") doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company, args = get_opening_invoice_creation_dict(
party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department) invoice_type=invoice_type,
company=company,
party_1=party_1,
party_2=party_2,
invoice_number=invoice_number,
department=department,
)
doc.update(args) doc.update(args)
return doc.make_invoices() return doc.make_invoices()
@ -68,15 +83,30 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
company = "_Test Opening Invoice Company" company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account") old_default_receivable_account = frappe.db.get_value(
"Company", company, "default_receivable_account"
)
frappe.db.set_value("Company", company, "default_receivable_account", "") frappe.db.set_value("Company", company, "default_receivable_account", "")
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"): if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company", cc = frappe.get_doc(
"is_group": 1, "company": "_Test Opening Invoice Company"}) {
"doctype": "Cost Center",
"cost_center_name": "_Test Opening Invoice Company",
"is_group": 1,
"company": "_Test Opening Invoice Company",
}
)
cc.insert(ignore_mandatory=True) cc.insert(ignore_mandatory=True)
cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0, cc2 = frappe.get_doc(
"company": "_Test Opening Invoice Company", "parent_cost_center": cc.name}) {
"doctype": "Cost Center",
"cost_center_name": "Main",
"is_group": 0,
"company": "_Test Opening Invoice Company",
"parent_cost_center": cc.name,
}
)
cc2.insert() cc2.insert()
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC") frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
@ -84,28 +114,37 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2) self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
# Check if missing debit account error raised # Check if missing debit account error raised
error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]}) error_log = frappe.db.exists(
"Error Log",
{"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]},
)
self.assertTrue(error_log) self.assertTrue(error_log)
# teardown # teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) frappe.db.set_value(
"Company", company, "default_receivable_account", old_default_receivable_account
)
def test_renaming_of_invoice_using_invoice_number_field(self): def test_renaming_of_invoice_using_invoice_number_field(self):
company = "_Test Opening Invoice Company" company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11") self.make_invoices(
company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11"
)
sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name") sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name")
sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name") sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name")
self.assertEqual(sales_inv1, "TEST-NEW-INV-11") self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
#teardown # teardown
for inv in [sales_inv1, sales_inv2]: for inv in [sales_inv1, sales_inv2]:
doc = frappe.get_doc('Sales Invoice', inv) doc = frappe.get_doc("Sales Invoice", inv)
doc.cancel() doc.cancel()
def test_opening_invoice_with_accounting_dimension(self): def test_opening_invoice_with_accounting_dimension(self):
invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC') invoices = self.make_invoices(
invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC"
)
expected_value = { expected_value = {
"keys": ["customer", "outstanding_amount", "status", "department"], "keys": ["customer", "outstanding_amount", "status", "department"],
@ -117,40 +156,44 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
def tearDown(self): def tearDown(self):
disable_dimension() disable_dimension()
def get_opening_invoice_creation_dict(**args): def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company") company = args.get("company", "_Test Company")
invoice_dict = frappe._dict({ invoice_dict = frappe._dict(
"company": company, {
"invoice_type": args.get("invoice_type", "Sales"), "company": company,
"invoices": [ "invoice_type": args.get("invoice_type", "Sales"),
{ "invoices": [
"qty": 1.0, {
"outstanding_amount": 300, "qty": 1.0,
"party": args.get("party_1") or "_Test {0}".format(party), "outstanding_amount": 300,
"item_name": "Opening Item", "party": args.get("party_1") or "_Test {0}".format(party),
"due_date": "2016-09-10", "item_name": "Opening Item",
"posting_date": "2016-09-05", "due_date": "2016-09-10",
"temporary_opening_account": get_temporary_opening_account(company), "posting_date": "2016-09-05",
"invoice_number": args.get("invoice_number") "temporary_opening_account": get_temporary_opening_account(company),
}, "invoice_number": args.get("invoice_number"),
{ },
"qty": 2.0, {
"outstanding_amount": 250, "qty": 2.0,
"party": args.get("party_2") or "_Test {0} 1".format(party), "outstanding_amount": 250,
"item_name": "Opening Item", "party": args.get("party_2") or "_Test {0} 1".format(party),
"due_date": "2016-09-10", "item_name": "Opening Item",
"posting_date": "2016-09-05", "due_date": "2016-09-10",
"temporary_opening_account": get_temporary_opening_account(company), "posting_date": "2016-09-05",
"invoice_number": None "temporary_opening_account": get_temporary_opening_account(company),
} "invoice_number": None,
] },
}) ],
}
)
invoice_dict.update(args) invoice_dict.update(args)
return invoice_dict return invoice_dict
def make_company(): def make_company():
if frappe.db.exists("Company", "_Test Opening Invoice Company"): if frappe.db.exists("Company", "_Test Opening Invoice Company"):
return frappe.get_doc("Company", "_Test Opening Invoice Company") return frappe.get_doc("Company", "_Test Opening Invoice Company")
@ -163,15 +206,18 @@ def make_company():
company.insert() company.insert()
return company return company
def make_customer(customer=None): def make_customer(customer=None):
customer_name = customer or "Opening Customer" customer_name = customer or "Opening Customer"
customer = frappe.get_doc({ customer = frappe.get_doc(
"doctype": "Customer", {
"customer_name": customer_name, "doctype": "Customer",
"customer_group": "All Customer Groups", "customer_name": customer_name,
"customer_type": "Company", "customer_group": "All Customer Groups",
"territory": "All Territories" "customer_type": "Company",
}) "territory": "All Territories",
}
)
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):
customer.insert(ignore_permissions=True) customer.insert(ignore_permissions=True)
return customer.name return customer.name

View File

@ -8,45 +8,55 @@ from frappe.model.document import Document
class PartyLink(Document): class PartyLink(Document):
def validate(self): def validate(self):
if self.primary_role not in ['Customer', 'Supplier']: if self.primary_role not in ["Customer", "Supplier"]:
frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."), frappe.throw(
title=_("Invalid Primary Role")) _(
"Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."
),
title=_("Invalid Primary Role"),
)
existing_party_link = frappe.get_all('Party Link', { existing_party_link = frappe.get_all(
'primary_party': self.primary_party, "Party Link",
'secondary_party': self.secondary_party {"primary_party": self.primary_party, "secondary_party": self.secondary_party},
}, pluck="primary_role") pluck="primary_role",
)
if existing_party_link: if existing_party_link:
frappe.throw(_('{} {} is already linked with {} {}') frappe.throw(
.format( _("{} {} is already linked with {} {}").format(
self.primary_role, bold(self.primary_party), self.primary_role, bold(self.primary_party), self.secondary_role, bold(self.secondary_party)
self.secondary_role, bold(self.secondary_party) )
)) )
existing_party_link = frappe.get_all('Party Link', { existing_party_link = frappe.get_all(
'primary_party': self.secondary_party "Party Link", {"primary_party": self.secondary_party}, pluck="primary_role"
}, pluck="primary_role") )
if existing_party_link: if existing_party_link:
frappe.throw(_('{} {} is already linked with another {}') frappe.throw(
.format(self.secondary_role, self.secondary_party, existing_party_link[0])) _("{} {} is already linked with another {}").format(
self.secondary_role, self.secondary_party, existing_party_link[0]
)
)
existing_party_link = frappe.get_all('Party Link', { existing_party_link = frappe.get_all(
'secondary_party': self.primary_party "Party Link", {"secondary_party": self.primary_party}, pluck="primary_role"
}, pluck="primary_role") )
if existing_party_link: if existing_party_link:
frappe.throw(_('{} {} is already linked with another {}') frappe.throw(
.format(self.primary_role, self.primary_party, existing_party_link[0])) _("{} {} is already linked with another {}").format(
self.primary_role, self.primary_party, existing_party_link[0]
)
)
@frappe.whitelist() @frappe.whitelist()
def create_party_link(primary_role, primary_party, secondary_party): def create_party_link(primary_role, primary_party, secondary_party):
party_link = frappe.new_doc('Party Link') party_link = frappe.new_doc("Party Link")
party_link.primary_role = primary_role party_link.primary_role = primary_role
party_link.primary_party = primary_party party_link.primary_party = primary_party
party_link.secondary_role = 'Customer' if primary_role == 'Supplier' else 'Supplier' party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
party_link.secondary_party = secondary_party party_link.secondary_party = secondary_party
party_link.save(ignore_permissions=True) party_link.save(ignore_permissions=True)
return party_link return party_link

File diff suppressed because it is too large Load Diff

View File

@ -32,10 +32,9 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert() pe.insert()
pe.submit() pe.submit()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
["Debtors - _TC", 0, 1000, so.name], (d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]]
["_Test Cash - _TC", 1000.0, 0, None] )
])
self.validate_gl_entries(pe.name, expected_gle) self.validate_gl_entries(pe.name, expected_gle)
@ -48,9 +47,9 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(so_advance_paid, 0) self.assertEqual(so_advance_paid, 0)
def test_payment_entry_for_blocked_supplier_invoice(self): def test_payment_entry_for_blocked_supplier_invoice(self):
supplier = frappe.get_doc('Supplier', '_Test Supplier') supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1 supplier.on_hold = 1
supplier.hold_type = 'Invoices' supplier.hold_type = "Invoices"
supplier.save() supplier.save()
self.assertRaises(frappe.ValidationError, make_purchase_invoice) self.assertRaises(frappe.ValidationError, make_purchase_invoice)
@ -59,32 +58,40 @@ class TestPaymentEntry(unittest.TestCase):
supplier.save() supplier.save()
def test_payment_entry_for_blocked_supplier_payments(self): def test_payment_entry_for_blocked_supplier_payments(self):
supplier = frappe.get_doc('Supplier', '_Test Supplier') supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1 supplier.on_hold = 1
supplier.hold_type = 'Payments' supplier.hold_type = "Payments"
supplier.save() supplier.save()
pi = make_purchase_invoice() pi = make_purchase_invoice()
self.assertRaises( self.assertRaises(
frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, frappe.ValidationError,
bank_account="_Test Bank - _TC") get_payment_entry,
dt="Purchase Invoice",
dn=pi.name,
bank_account="_Test Bank - _TC",
)
supplier.on_hold = 0 supplier.on_hold = 0
supplier.save() supplier.save()
def test_payment_entry_for_blocked_supplier_payments_today_date(self): def test_payment_entry_for_blocked_supplier_payments_today_date(self):
supplier = frappe.get_doc('Supplier', '_Test Supplier') supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1 supplier.on_hold = 1
supplier.hold_type = 'Payments' supplier.hold_type = "Payments"
supplier.release_date = nowdate() supplier.release_date = nowdate()
supplier.save() supplier.save()
pi = make_purchase_invoice() pi = make_purchase_invoice()
self.assertRaises( self.assertRaises(
frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, frappe.ValidationError,
bank_account="_Test Bank - _TC") get_payment_entry,
dt="Purchase Invoice",
dn=pi.name,
bank_account="_Test Bank - _TC",
)
supplier.on_hold = 0 supplier.on_hold = 0
supplier.save() supplier.save()
@ -93,15 +100,15 @@ class TestPaymentEntry(unittest.TestCase):
# this test is meant to fail only if something fails in the try block # this test is meant to fail only if something fails in the try block
with self.assertRaises(Exception): with self.assertRaises(Exception):
try: try:
supplier = frappe.get_doc('Supplier', '_Test Supplier') supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1 supplier.on_hold = 1
supplier.hold_type = 'Payments' supplier.hold_type = "Payments"
supplier.release_date = '2018-03-01' supplier.release_date = "2018-03-01"
supplier.save() supplier.save()
pi = make_purchase_invoice() pi = make_purchase_invoice()
get_payment_entry('Purchase Invoice', pi.name, bank_account="_Test Bank - _TC") get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
supplier.on_hold = 0 supplier.on_hold = 0
supplier.save() supplier.save()
@ -111,8 +118,12 @@ class TestPaymentEntry(unittest.TestCase):
raise Exception raise Exception
def test_payment_entry_against_si_usd_to_usd(self): def test_payment_entry_against_si_usd_to_usd(self):
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", si = create_sales_invoice(
currency="USD", conversion_rate=50) customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
@ -120,10 +131,13 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert() pe.insert()
pe.submit() pe.submit()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
["_Test Receivable USD - _TC", 0, 5000, si.name], (d[0], d)
["_Test Bank USD - _TC", 5000.0, 0, None] for d in [
]) ["_Test Receivable USD - _TC", 0, 5000, si.name],
["_Test Bank USD - _TC", 5000.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle) self.validate_gl_entries(pe.name, expected_gle)
@ -136,8 +150,12 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(outstanding_amount, 100) self.assertEqual(outstanding_amount, 100)
def test_payment_entry_against_pi(self): def test_payment_entry_against_pi(self):
pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC", pi = make_purchase_invoice(
currency="USD", conversion_rate=50) supplier="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC") pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
@ -145,20 +163,26 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert() pe.insert()
pe.submit() pe.submit()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
["_Test Payable USD - _TC", 12500, 0, pi.name], (d[0], d)
["_Test Bank USD - _TC", 0, 12500, None] for d in [
]) ["_Test Payable USD - _TC", 12500, 0, pi.name],
["_Test Bank USD - _TC", 0, 12500, None],
]
)
self.validate_gl_entries(pe.name, expected_gle) self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", pi.name, "outstanding_amount")) outstanding_amount = flt(frappe.db.get_value("Sales Invoice", pi.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0) self.assertEqual(outstanding_amount, 0)
def test_payment_against_sales_invoice_to_check_status(self): def test_payment_against_sales_invoice_to_check_status(self):
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", si = create_sales_invoice(
currency="USD", conversion_rate=50) customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1" pe.reference_no = "1"
@ -167,28 +191,35 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert() pe.insert()
pe.submit() pe.submit()
outstanding_amount, status = frappe.db.get_value("Sales Invoice", si.name, ["outstanding_amount", "status"]) outstanding_amount, status = frappe.db.get_value(
"Sales Invoice", si.name, ["outstanding_amount", "status"]
)
self.assertEqual(flt(outstanding_amount), 0) self.assertEqual(flt(outstanding_amount), 0)
self.assertEqual(status, 'Paid') self.assertEqual(status, "Paid")
pe.cancel() pe.cancel()
outstanding_amount, status = frappe.db.get_value("Sales Invoice", si.name, ["outstanding_amount", "status"]) outstanding_amount, status = frappe.db.get_value(
"Sales Invoice", si.name, ["outstanding_amount", "status"]
)
self.assertEqual(flt(outstanding_amount), 100) self.assertEqual(flt(outstanding_amount), 100)
self.assertEqual(status, 'Unpaid') self.assertEqual(status, "Unpaid")
def test_payment_entry_against_payment_terms(self): def test_payment_entry_against_payment_terms(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200) si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template() create_payment_terms_template()
si.payment_terms_template = 'Test Receivable Template' si.payment_terms_template = "Test Receivable Template"
si.append('taxes', { si.append(
"charge_type": "On Net Total", "taxes",
"account_head": "_Test Account Service Tax - _TC", {
"cost_center": "_Test Cost Center - _TC", "charge_type": "On Net Total",
"description": "Service Tax", "account_head": "_Test Account Service Tax - _TC",
"rate": 18 "cost_center": "_Test Cost Center - _TC",
}) "description": "Service Tax",
"rate": 18,
},
)
si.save() si.save()
si.submit() si.submit()
@ -197,25 +228,28 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit() pe.submit()
si.load_from_db() si.load_from_db()
self.assertEqual(pe.references[0].payment_term, 'Basic Amount Receivable') self.assertEqual(pe.references[0].payment_term, "Basic Amount Receivable")
self.assertEqual(pe.references[1].payment_term, 'Tax Receivable') self.assertEqual(pe.references[1].payment_term, "Tax Receivable")
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0) self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0) self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
def test_payment_entry_against_payment_terms_with_discount(self): def test_payment_entry_against_payment_terms_with_discount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200) si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount() create_payment_terms_template_with_discount()
si.payment_terms_template = 'Test Discount Template' si.payment_terms_template = "Test Discount Template"
frappe.db.set_value('Company', si.company, 'default_discount_account', 'Write Off - _TC') frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.append('taxes', { si.append(
"charge_type": "On Net Total", "taxes",
"account_head": "_Test Account Service Tax - _TC", {
"cost_center": "_Test Cost Center - _TC", "charge_type": "On Net Total",
"description": "Service Tax", "account_head": "_Test Account Service Tax - _TC",
"rate": 18 "cost_center": "_Test Cost Center - _TC",
}) "description": "Service Tax",
"rate": 18,
},
)
si.save() si.save()
si.submit() si.submit()
@ -224,16 +258,19 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit() pe.submit()
si.load_from_db() si.load_from_db()
self.assertEqual(pe.references[0].payment_term, '30 Credit Days with 10% Discount') self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0) self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
self.assertEqual(si.payment_schedule[0].paid_amount, 212.40) self.assertEqual(si.payment_schedule[0].paid_amount, 212.40)
self.assertEqual(si.payment_schedule[0].outstanding, 0) self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6) self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
def test_payment_against_purchase_invoice_to_check_status(self): def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC", pi = make_purchase_invoice(
currency="USD", conversion_rate=50) supplier="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC") pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1" pe.reference_no = "1"
@ -242,21 +279,27 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert() pe.insert()
pe.submit() pe.submit()
outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"]) outstanding_amount, status = frappe.db.get_value(
"Purchase Invoice", pi.name, ["outstanding_amount", "status"]
)
self.assertEqual(flt(outstanding_amount), 0) self.assertEqual(flt(outstanding_amount), 0)
self.assertEqual(status, 'Paid') self.assertEqual(status, "Paid")
pe.cancel() pe.cancel()
outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"]) outstanding_amount, status = frappe.db.get_value(
"Purchase Invoice", pi.name, ["outstanding_amount", "status"]
)
self.assertEqual(flt(outstanding_amount), 250) self.assertEqual(flt(outstanding_amount), 250)
self.assertEqual(status, 'Unpaid') self.assertEqual(status, "Unpaid")
def test_payment_entry_against_ec(self): def test_payment_entry_against_ec(self):
payable = frappe.get_cached_value('Company', "_Test Company", 'default_payable_account') payable = frappe.get_cached_value("Company", "_Test Company", "default_payable_account")
ec = make_expense_claim(payable, 300, 300, "_Test Company", "Travel Expenses - _TC") ec = make_expense_claim(payable, 300, 300, "_Test Company", "Travel Expenses - _TC")
pe = get_payment_entry("Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300) pe = get_payment_entry(
"Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300
)
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
pe.source_exchange_rate = 1 pe.source_exchange_rate = 1
@ -264,68 +307,87 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert() pe.insert()
pe.submit() pe.submit()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
[payable, 300, 0, ec.name], (d[0], d) for d in [[payable, 300, 0, ec.name], ["_Test Bank USD - _TC", 0, 300, None]]
["_Test Bank USD - _TC", 0, 300, None] )
])
self.validate_gl_entries(pe.name, expected_gle) self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Expense Claim", ec.name, "total_sanctioned_amount")) - \ outstanding_amount = flt(
flt(frappe.db.get_value("Expense Claim", ec.name, "total_amount_reimbursed")) frappe.db.get_value("Expense Claim", ec.name, "total_sanctioned_amount")
) - flt(frappe.db.get_value("Expense Claim", ec.name, "total_amount_reimbursed"))
self.assertEqual(outstanding_amount, 0) self.assertEqual(outstanding_amount, 0)
def test_payment_entry_against_si_usd_to_inr(self): def test_payment_entry_against_si_usd_to_inr(self):
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", si = create_sales_invoice(
currency="USD", conversion_rate=50) customer="_Test Customer USD",
pe = get_payment_entry("Sales Invoice", si.name, party_amount=20, debit_to="_Test Receivable USD - _TC",
bank_account="_Test Bank - _TC", bank_amount=900) currency="USD",
conversion_rate=50,
)
pe = get_payment_entry(
"Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank - _TC", bank_amount=900
)
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
self.assertEqual(pe.difference_amount, 100) self.assertEqual(pe.difference_amount, 100)
pe.append("deductions", { pe.append(
"account": "_Test Exchange Gain/Loss - _TC", "deductions",
"cost_center": "_Test Cost Center - _TC", {
"amount": 100 "account": "_Test Exchange Gain/Loss - _TC",
}) "cost_center": "_Test Cost Center - _TC",
"amount": 100,
},
)
pe.insert() pe.insert()
pe.submit() pe.submit()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
["_Test Receivable USD - _TC", 0, 1000, si.name], (d[0], d)
["_Test Bank - _TC", 900, 0, None], for d in [
["_Test Exchange Gain/Loss - _TC", 100.0, 0, None], ["_Test Receivable USD - _TC", 0, 1000, si.name],
]) ["_Test Bank - _TC", 900, 0, None],
["_Test Exchange Gain/Loss - _TC", 100.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle) self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 80) self.assertEqual(outstanding_amount, 80)
def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency (self): def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency(self):
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", si = create_sales_invoice(
currency="USD", conversion_rate=50, do_not_save=1) customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_save=1,
)
si.plc_conversion_rate = 50 si.plc_conversion_rate = 50
si.save() si.save()
si.submit() si.submit()
pe = get_payment_entry("Sales Invoice", si.name, party_amount=20, pe = get_payment_entry(
bank_account="_Test Bank USD - _TC", bank_amount=900) "Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank USD - _TC", bank_amount=900
)
pe.source_exchange_rate = 45.263 pe.source_exchange_rate = 45.263
pe.target_exchange_rate = 45.263 pe.target_exchange_rate = 45.263
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
pe.append(
pe.append("deductions", { "deductions",
"account": "_Test Exchange Gain/Loss - _TC", {
"cost_center": "_Test Cost Center - _TC", "account": "_Test Exchange Gain/Loss - _TC",
"amount": 94.80 "cost_center": "_Test Cost Center - _TC",
}) "amount": 94.80,
},
)
pe.save() pe.save()
@ -359,8 +421,7 @@ class TestPaymentEntry(unittest.TestCase):
pe.set_amounts() pe.set_amounts()
self.assertEqual( self.assertEqual(
pe.source_exchange_rate, 65.1, pe.source_exchange_rate, 65.1, "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1)
"{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1)
) )
def test_internal_transfer_usd_to_inr(self): def test_internal_transfer_usd_to_inr(self):
@ -382,20 +443,26 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(pe.difference_amount, 500) self.assertEqual(pe.difference_amount, 500)
pe.append("deductions", { pe.append(
"account": "_Test Exchange Gain/Loss - _TC", "deductions",
"cost_center": "_Test Cost Center - _TC", {
"amount": 500 "account": "_Test Exchange Gain/Loss - _TC",
}) "cost_center": "_Test Cost Center - _TC",
"amount": 500,
},
)
pe.insert() pe.insert()
pe.submit() pe.submit()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
["_Test Bank USD - _TC", 0, 5000, None], (d[0], d)
["_Test Bank - _TC", 4500, 0, None], for d in [
["_Test Exchange Gain/Loss - _TC", 500.0, 0, None], ["_Test Bank USD - _TC", 0, 5000, None],
]) ["_Test Bank - _TC", 4500, 0, None],
["_Test Exchange Gain/Loss - _TC", 500.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle) self.validate_gl_entries(pe.name, expected_gle)
@ -435,10 +502,9 @@ class TestPaymentEntry(unittest.TestCase):
pe3.insert() pe3.insert()
pe3.submit() pe3.submit()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
["Debtors - _TC", 100, 0, si1.name], (d[0], d) for d in [["Debtors - _TC", 100, 0, si1.name], ["_Test Cash - _TC", 0, 100, None]]
["_Test Cash - _TC", 0, 100, None] )
])
self.validate_gl_entries(pe3.name, expected_gle) self.validate_gl_entries(pe3.name, expected_gle)
@ -462,12 +528,16 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(expected_gle[gle.account][3], gle.against_voucher) self.assertEqual(expected_gle[gle.account][3], gle.against_voucher)
def get_gle(self, voucher_no): def get_gle(self, voucher_no):
return frappe.db.sql("""select account, debit, credit, against_voucher return frappe.db.sql(
"""select account, debit, credit, against_voucher
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
order by account asc""", voucher_no, as_dict=1) order by account asc""",
voucher_no,
as_dict=1,
)
def test_payment_entry_write_off_difference(self): def test_payment_entry_write_off_difference(self):
si = create_sales_invoice() si = create_sales_invoice()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
@ -477,11 +547,10 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(pe.unallocated_amount, 10) self.assertEqual(pe.unallocated_amount, 10)
pe.received_amount = pe.paid_amount = 95 pe.received_amount = pe.paid_amount = 95
pe.append("deductions", { pe.append(
"account": "_Test Write Off - _TC", "deductions",
"cost_center": "_Test Cost Center - _TC", {"account": "_Test Write Off - _TC", "cost_center": "_Test Cost Center - _TC", "amount": 5},
"amount": 5 )
})
pe.save() pe.save()
self.assertEqual(pe.unallocated_amount, 0) self.assertEqual(pe.unallocated_amount, 0)
@ -489,27 +558,37 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit() pe.submit()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
["Debtors - _TC", 0, 100, si.name], (d[0], d)
["_Test Cash - _TC", 95, 0, None], for d in [
["_Test Write Off - _TC", 5, 0, None] ["Debtors - _TC", 0, 100, si.name],
]) ["_Test Cash - _TC", 95, 0, None],
["_Test Write Off - _TC", 5, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle) self.validate_gl_entries(pe.name, expected_gle)
def test_payment_entry_exchange_gain_loss(self): def test_payment_entry_exchange_gain_loss(self):
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", si = create_sales_invoice(
currency="USD", conversion_rate=50) customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
pe.source_exchange_rate = 55 pe.source_exchange_rate = 55
pe.append("deductions", { pe.append(
"account": "_Test Exchange Gain/Loss - _TC", "deductions",
"cost_center": "_Test Cost Center - _TC", {
"amount": -500 "account": "_Test Exchange Gain/Loss - _TC",
}) "cost_center": "_Test Cost Center - _TC",
"amount": -500,
},
)
pe.save() pe.save()
self.assertEqual(pe.unallocated_amount, 0) self.assertEqual(pe.unallocated_amount, 0)
@ -517,11 +596,14 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit() pe.submit()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
["_Test Receivable USD - _TC", 0, 5000, si.name], (d[0], d)
["_Test Bank USD - _TC", 5500, 0, None], for d in [
["_Test Exchange Gain/Loss - _TC", 0, 500, None], ["_Test Receivable USD - _TC", 0, 5000, si.name],
]) ["_Test Bank USD - _TC", 5500, 0, None],
["_Test Exchange Gain/Loss - _TC", 0, 500, None],
]
)
self.validate_gl_entries(pe.name, expected_gle) self.validate_gl_entries(pe.name, expected_gle)
@ -530,10 +612,11 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_against_sales_invoice_with_cost_centre(self): def test_payment_entry_against_sales_invoice_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
cost_center = "_Test Cost Center for BS Account - _TC" cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
self.assertEqual(pe.cost_center, si.cost_center) self.assertEqual(pe.cost_center, si.cost_center)
@ -546,18 +629,18 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit() pe.submit()
expected_values = { expected_values = {
"_Test Bank - _TC": { "_Test Bank - _TC": {"cost_center": cost_center},
"cost_center": cost_center "Debtors - _TC": {"cost_center": cost_center},
},
"Debtors - _TC": {
"cost_center": cost_center
}
} }
gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, gl_entries = frappe.db.sql(
"""select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
order by account asc""", pe.name, as_dict=1) order by account asc""",
pe.name,
as_dict=1,
)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
@ -566,10 +649,13 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_against_purchase_invoice_with_cost_center(self): def test_payment_entry_against_purchase_invoice_with_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
cost_center = "_Test Cost Center for BS Account - _TC" cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC") pi = make_purchase_invoice_against_cost_center(
cost_center=cost_center, credit_to="Creditors - _TC"
)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
self.assertEqual(pe.cost_center, pi.cost_center) self.assertEqual(pe.cost_center, pi.cost_center)
@ -582,18 +668,18 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit() pe.submit()
expected_values = { expected_values = {
"_Test Bank - _TC": { "_Test Bank - _TC": {"cost_center": cost_center},
"cost_center": cost_center "Creditors - _TC": {"cost_center": cost_center},
},
"Creditors - _TC": {
"cost_center": cost_center
}
} }
gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, gl_entries = frappe.db.sql(
"""select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
order by account asc""", pe.name, as_dict=1) order by account asc""",
pe.name,
as_dict=1,
)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
@ -603,13 +689,16 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_account_and_party_balance_with_cost_center(self): def test_payment_entry_account_and_party_balance_with_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
cost_center = "_Test Cost Center for BS Account - _TC" cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=si.cost_center) account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=si.cost_center)
party_balance = get_balance_on(party_type="Customer", party=si.customer, cost_center=si.cost_center) party_balance = get_balance_on(
party_type="Customer", party=si.customer, cost_center=si.cost_center
)
party_account_balance = get_balance_on(si.debit_to, cost_center=si.cost_center) party_account_balance = get_balance_on(si.debit_to, cost_center=si.cost_center)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
@ -634,94 +723,109 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(flt(expected_party_account_balance), party_account_balance) self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def test_multi_currency_payment_entry_with_taxes(self): def test_multi_currency_payment_entry_with_taxes(self):
payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC', payment_entry = create_payment_entry(
save=True) party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
payment_entry.append('taxes', { )
'account_head': '_Test Account Service Tax - _TC', payment_entry.append(
'charge_type': 'Actual', "taxes",
'tax_amount': 10, {
'add_deduct_tax': 'Add', "account_head": "_Test Account Service Tax - _TC",
'description': 'Test' "charge_type": "Actual",
}) "tax_amount": 10,
"add_deduct_tax": "Add",
"description": "Test",
},
)
payment_entry.save() payment_entry.save()
self.assertEqual(payment_entry.base_total_taxes_and_charges, 10) self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)) self.assertEqual(
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
)
def create_payment_entry(**args): def create_payment_entry(**args):
payment_entry = frappe.new_doc('Payment Entry') payment_entry = frappe.new_doc("Payment Entry")
payment_entry.company = args.get('company') or '_Test Company' payment_entry.company = args.get("company") or "_Test Company"
payment_entry.payment_type = args.get('payment_type') or 'Pay' payment_entry.payment_type = args.get("payment_type") or "Pay"
payment_entry.party_type = args.get('party_type') or 'Supplier' payment_entry.party_type = args.get("party_type") or "Supplier"
payment_entry.party = args.get('party') or '_Test Supplier' payment_entry.party = args.get("party") or "_Test Supplier"
payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC' payment_entry.paid_from = args.get("paid_from") or "_Test Bank - _TC"
payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC' payment_entry.paid_to = args.get("paid_to") or "Creditors - _TC"
payment_entry.paid_amount = args.get('paid_amount') or 1000 payment_entry.paid_amount = args.get("paid_amount") or 1000
payment_entry.setup_party_account_field() payment_entry.setup_party_account_field()
payment_entry.set_missing_values() payment_entry.set_missing_values()
payment_entry.set_exchange_rate() payment_entry.set_exchange_rate()
payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
payment_entry.reference_no = 'Test001' payment_entry.reference_no = "Test001"
payment_entry.reference_date = nowdate() payment_entry.reference_date = nowdate()
if args.get('save'): if args.get("save"):
payment_entry.save() payment_entry.save()
if args.get('submit'): if args.get("submit"):
payment_entry.submit() payment_entry.submit()
return payment_entry return payment_entry
def create_payment_terms_template(): def create_payment_terms_template():
create_payment_term('Basic Amount Receivable') create_payment_term("Basic Amount Receivable")
create_payment_term('Tax Receivable') create_payment_term("Tax Receivable")
if not frappe.db.exists('Payment Terms Template', 'Test Receivable Template'): if not frappe.db.exists("Payment Terms Template", "Test Receivable Template"):
payment_term_template = frappe.get_doc({ payment_term_template = frappe.get_doc(
'doctype': 'Payment Terms Template',
'template_name': 'Test Receivable Template',
'allocate_payment_based_on_payment_terms': 1,
'terms': [{
'doctype': 'Payment Terms Template Detail',
'payment_term': 'Basic Amount Receivable',
'invoice_portion': 84.746,
'credit_days_based_on': 'Day(s) after invoice date',
'credit_days': 1
},
{ {
'doctype': 'Payment Terms Template Detail', "doctype": "Payment Terms Template",
'payment_term': 'Tax Receivable', "template_name": "Test Receivable Template",
'invoice_portion': 15.254, "allocate_payment_based_on_payment_terms": 1,
'credit_days_based_on': 'Day(s) after invoice date', "terms": [
'credit_days': 2 {
}] "doctype": "Payment Terms Template Detail",
}).insert() "payment_term": "Basic Amount Receivable",
"invoice_portion": 84.746,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 1,
},
{
"doctype": "Payment Terms Template Detail",
"payment_term": "Tax Receivable",
"invoice_portion": 15.254,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 2,
},
],
}
).insert()
def create_payment_terms_template_with_discount(): def create_payment_terms_template_with_discount():
create_payment_term('30 Credit Days with 10% Discount') create_payment_term("30 Credit Days with 10% Discount")
if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
payment_term_template = frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "Test Discount Template",
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
"payment_term": "30 Credit Days with 10% Discount",
"invoice_portion": 100,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 2,
"discount": 10,
"discount_validity_based_on": "Day(s) after invoice date",
"discount_validity": 1,
}
],
}
).insert()
if not frappe.db.exists('Payment Terms Template', 'Test Discount Template'):
payment_term_template = frappe.get_doc({
'doctype': 'Payment Terms Template',
'template_name': 'Test Discount Template',
'allocate_payment_based_on_payment_terms': 1,
'terms': [{
'doctype': 'Payment Terms Template Detail',
'payment_term': '30 Credit Days with 10% Discount',
'invoice_portion': 100,
'credit_days_based_on': 'Day(s) after invoice date',
'credit_days': 2,
'discount': 10,
'discount_validity_based_on': 'Day(s) after invoice date',
'discount_validity': 1
}]
}).insert()
def create_payment_term(name): def create_payment_term(name):
if not frappe.db.exists('Payment Term', name): if not frappe.db.exists("Payment Term", name):
frappe.get_doc({ frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
'doctype': 'Payment Term',
'payment_term_name': name
}).insert()

View File

@ -18,10 +18,13 @@ class PaymentGatewayAccount(Document):
def update_default_payment_gateway(self): def update_default_payment_gateway(self):
if self.is_default: if self.is_default:
frappe.db.sql("""update `tabPayment Gateway Account` set is_default = 0 frappe.db.sql(
where is_default = 1 """) """update `tabPayment Gateway Account` set is_default = 0
where is_default = 1 """
)
def set_as_default_if_not_set(self): def set_as_default_if_not_set(self):
if not frappe.db.get_value("Payment Gateway Account", if not frappe.db.get_value(
{"is_default": 1, "name": ("!=", self.name)}, "name"): "Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name"
):
self.is_default = 1 self.is_default = 1

View File

@ -1,15 +1,6 @@
def get_data(): def get_data():
return { return {
'fieldname': 'payment_gateway_account', "fieldname": "payment_gateway_account",
'non_standard_fieldnames': { "non_standard_fieldnames": {"Subscription Plan": "payment_gateway"},
'Subscription Plan': 'payment_gateway' "transactions": [{"items": ["Payment Request"]}, {"items": ["Subscription Plan"]}],
},
'transactions': [
{
'items': ['Payment Request']
},
{
'items': ['Subscription Plan']
}
]
} }

View File

@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Payment Gateway Account') # test_records = frappe.get_test_records('Payment Gateway Account')
class TestPaymentGatewayAccount(unittest.TestCase): class TestPaymentGatewayAccount(unittest.TestCase):
pass pass

View File

@ -18,9 +18,9 @@ class PaymentOrder(Document):
self.update_payment_status(cancel=True) self.update_payment_status(cancel=True)
def update_payment_status(self, cancel=False): def update_payment_status(self, cancel=False):
status = 'Payment Ordered' status = "Payment Ordered"
if cancel: if cancel:
status = 'Initiated' status = "Initiated"
if self.payment_order_type == "Payment Request": if self.payment_order_type == "Payment Request":
ref_field = "status" ref_field = "status"
@ -32,67 +32,67 @@ class PaymentOrder(Document):
for d in self.references: for d in self.references:
frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status) frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_mop_query(doctype, txt, searchfield, start, page_len, filters): def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(""" select mode_of_payment from `tabPayment Order Reference` return frappe.db.sql(
""" select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s where parent = %(parent)s and mode_of_payment like %(txt)s
limit %(start)s, %(page_len)s""", { limit %(start)s, %(page_len)s""",
'parent': filters.get("parent"), {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
'start': start, )
'page_len': page_len,
'txt': "%%%s%%" % txt
})
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_supplier_query(doctype, txt, searchfield, start, page_len, filters): def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(""" select supplier from `tabPayment Order Reference` return frappe.db.sql(
""" select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and where parent = %(parent)s and supplier like %(txt)s and
(payment_reference is null or payment_reference='') (payment_reference is null or payment_reference='')
limit %(start)s, %(page_len)s""", { limit %(start)s, %(page_len)s""",
'parent': filters.get("parent"), {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
'start': start, )
'page_len': page_len,
'txt': "%%%s%%" % txt
})
@frappe.whitelist() @frappe.whitelist()
def make_payment_records(name, supplier, mode_of_payment=None): def make_payment_records(name, supplier, mode_of_payment=None):
doc = frappe.get_doc('Payment Order', name) doc = frappe.get_doc("Payment Order", name)
make_journal_entry(doc, supplier, mode_of_payment) make_journal_entry(doc, supplier, mode_of_payment)
def make_journal_entry(doc, supplier, mode_of_payment=None): def make_journal_entry(doc, supplier, mode_of_payment=None):
je = frappe.new_doc('Journal Entry') je = frappe.new_doc("Journal Entry")
je.payment_order = doc.name je.payment_order = doc.name
je.posting_date = nowdate() je.posting_date = nowdate()
mode_of_payment_type = frappe._dict(frappe.get_all('Mode of Payment', mode_of_payment_type = frappe._dict(
fields = ["name", "type"], as_list=1)) frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1)
)
je.voucher_type = 'Bank Entry' je.voucher_type = "Bank Entry"
if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == 'Cash': if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == "Cash":
je.voucher_type = "Cash Entry" je.voucher_type = "Cash Entry"
paid_amt = 0 paid_amt = 0
party_account = get_party_account('Supplier', supplier, doc.company) party_account = get_party_account("Supplier", supplier, doc.company)
for d in doc.references: for d in doc.references:
if (d.supplier == supplier if d.supplier == supplier and (not mode_of_payment or mode_of_payment == d.mode_of_payment):
and (not mode_of_payment or mode_of_payment == d.mode_of_payment)): je.append(
je.append('accounts', { "accounts",
'account': party_account, {
'debit_in_account_currency': d.amount, "account": party_account,
'party_type': 'Supplier', "debit_in_account_currency": d.amount,
'party': supplier, "party_type": "Supplier",
'reference_type': d.reference_doctype, "party": supplier,
'reference_name': d.reference_name "reference_type": d.reference_doctype,
}) "reference_name": d.reference_name,
},
)
paid_amt += d.amount paid_amt += d.amount
je.append('accounts', { je.append("accounts", {"account": doc.account, "credit_in_account_currency": paid_amt})
'account': doc.account,
'credit_in_account_currency': paid_amt
})
je.flags.ignore_mandatory = True je.flags.ignore_mandatory = True
je.save() je.save()

View File

@ -1,9 +1,5 @@
def get_data(): def get_data():
return { return {
'fieldname': 'payment_order', "fieldname": "payment_order",
'transactions': [ "transactions": [{"items": ["Payment Entry", "Journal Entry"]}],
{
'items': ['Payment Entry', 'Journal Entry']
}
]
} }

View File

@ -26,7 +26,9 @@ class TestPaymentOrder(unittest.TestCase):
def test_payment_order_creation_against_payment_entry(self): def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice() purchase_invoice = make_purchase_invoice()
payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC") payment_entry = get_payment_entry(
"Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC"
)
payment_entry.reference_no = "_Test_Payment_Order" payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate() payment_entry.reference_date = getdate()
payment_entry.party_bank_account = "Checking Account - Citi Bank" payment_entry.party_bank_account = "Checking Account - Citi Bank"
@ -40,13 +42,16 @@ class TestPaymentOrder(unittest.TestCase):
self.assertEqual(reference_doc.supplier, "_Test Supplier") self.assertEqual(reference_doc.supplier, "_Test Supplier")
self.assertEqual(reference_doc.amount, 250) self.assertEqual(reference_doc.amount, 250)
def create_payment_order_against_payment_entry(ref_doc, order_type): def create_payment_order_against_payment_entry(ref_doc, order_type):
payment_order = frappe.get_doc(dict( payment_order = frappe.get_doc(
doctype="Payment Order", dict(
company="_Test Company", doctype="Payment Order",
payment_order_type=order_type, company="_Test Company",
company_bank_account="Checking Account - Citi Bank" payment_order_type=order_type,
)) company_bank_account="Checking Account - Citi Bank",
)
)
doc = make_payment_order(ref_doc.name, payment_order) doc = make_payment_order(ref_doc.name, payment_order)
doc.save() doc.save()
doc.submit() doc.submit()

View File

@ -32,30 +32,43 @@ class PaymentReconciliation(Document):
non_reconciled_payments = payment_entries + journal_entries + dr_or_cr_notes non_reconciled_payments = payment_entries + journal_entries + dr_or_cr_notes
if self.payment_limit: if self.payment_limit:
non_reconciled_payments = non_reconciled_payments[:self.payment_limit] non_reconciled_payments = non_reconciled_payments[: self.payment_limit]
non_reconciled_payments = sorted(non_reconciled_payments, key=lambda k: k['posting_date'] or getdate(nowdate())) non_reconciled_payments = sorted(
non_reconciled_payments, key=lambda k: k["posting_date"] or getdate(nowdate())
)
self.add_payment_entries(non_reconciled_payments) self.add_payment_entries(non_reconciled_payments)
def get_payment_entries(self): def get_payment_entries(self):
order_doctype = "Sales Order" if self.party_type=="Customer" else "Purchase Order" order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True) condition = self.get_conditions(get_payments=True)
payment_entries = get_advance_payment_entries(self.party_type, self.party, payment_entries = get_advance_payment_entries(
self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.payment_limit, self.party_type,
condition=condition) self.party,
self.receivable_payable_account,
order_doctype,
against_all_orders=True,
limit=self.payment_limit,
condition=condition,
)
return payment_entries return payment_entries
def get_jv_entries(self): def get_jv_entries(self):
condition = self.get_conditions() condition = self.get_conditions()
dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' dr_or_cr = (
else "debit_in_account_currency") "credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
bank_account_condition = "t2.against_account like %(bank_cash_account)s" \ bank_account_condition = (
if self.bank_cash_account else "1=1" "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
)
journal_entries = frappe.db.sql(""" journal_entries = frappe.db.sql(
"""
select select
"Journal Entry" as reference_type, t1.name as reference_name, "Journal Entry" as reference_type, t1.name as reference_name,
t1.posting_date, t1.remark as remarks, t2.name as reference_row, t1.posting_date, t1.remark as remarks, t2.name as reference_row,
@ -76,31 +89,42 @@ class PaymentReconciliation(Document):
ELSE {bank_account_condition} ELSE {bank_account_condition}
END) END)
order by t1.posting_date order by t1.posting_date
""".format(**{ """.format(
"dr_or_cr": dr_or_cr, **{
"bank_account_condition": bank_account_condition, "dr_or_cr": dr_or_cr,
"condition": condition "bank_account_condition": bank_account_condition,
}), { "condition": condition,
}
),
{
"party_type": self.party_type, "party_type": self.party_type,
"party": self.party, "party": self.party,
"account": self.receivable_payable_account, "account": self.receivable_payable_account,
"bank_cash_account": "%%%s%%" % self.bank_cash_account "bank_cash_account": "%%%s%%" % self.bank_cash_account,
}, as_dict=1) },
as_dict=1,
)
return list(journal_entries) return list(journal_entries)
def get_dr_or_cr_notes(self): def get_dr_or_cr_notes(self):
condition = self.get_conditions(get_return_invoices=True) condition = self.get_conditions(get_return_invoices=True)
dr_or_cr = ("credit_in_account_currency" dr_or_cr = (
if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") "credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
reconciled_dr_or_cr = ("debit_in_account_currency" reconciled_dr_or_cr = (
if dr_or_cr == "credit_in_account_currency" else "credit_in_account_currency") "debit_in_account_currency"
if dr_or_cr == "credit_in_account_currency"
else "credit_in_account_currency"
)
voucher_type = ('Sales Invoice' voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
if self.party_type == 'Customer' else "Purchase Invoice")
return frappe.db.sql(""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type, return frappe.db.sql(
""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date, (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
account_currency as currency account_currency as currency
FROM `tab{doc}` doc, `tabGL Entry` gl FROM `tab{doc}` doc, `tabGL Entry` gl
@ -117,106 +141,115 @@ class PaymentReconciliation(Document):
amount > 0 amount > 0
ORDER BY doc.posting_date ORDER BY doc.posting_date
""".format( """.format(
doc=voucher_type, doc=voucher_type,
dr_or_cr=dr_or_cr, dr_or_cr=dr_or_cr,
reconciled_dr_or_cr=reconciled_dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr,
party_type_field=frappe.scrub(self.party_type), party_type_field=frappe.scrub(self.party_type),
condition=condition or ""), condition=condition or "",
),
{ {
'party': self.party, "party": self.party,
'party_type': self.party_type, "party_type": self.party_type,
'voucher_type': voucher_type, "voucher_type": voucher_type,
'account': self.receivable_payable_account "account": self.receivable_payable_account,
}, as_dict=1) },
as_dict=1,
)
def add_payment_entries(self, non_reconciled_payments): def add_payment_entries(self, non_reconciled_payments):
self.set('payments', []) self.set("payments", [])
for payment in non_reconciled_payments: for payment in non_reconciled_payments:
row = self.append('payments', {}) row = self.append("payments", {})
row.update(payment) row.update(payment)
def get_invoice_entries(self): def get_invoice_entries(self):
#Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against # Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
condition = self.get_conditions(get_invoices=True) condition = self.get_conditions(get_invoices=True)
non_reconciled_invoices = get_outstanding_invoices(self.party_type, self.party, non_reconciled_invoices = get_outstanding_invoices(
self.receivable_payable_account, condition=condition) self.party_type, self.party, self.receivable_payable_account, condition=condition
)
if self.invoice_limit: if self.invoice_limit:
non_reconciled_invoices = non_reconciled_invoices[:self.invoice_limit] non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
self.add_invoice_entries(non_reconciled_invoices) self.add_invoice_entries(non_reconciled_invoices)
def add_invoice_entries(self, non_reconciled_invoices): def add_invoice_entries(self, non_reconciled_invoices):
#Populate 'invoices' with JVs and Invoices to reconcile against # Populate 'invoices' with JVs and Invoices to reconcile against
self.set('invoices', []) self.set("invoices", [])
for entry in non_reconciled_invoices: for entry in non_reconciled_invoices:
inv = self.append('invoices', {}) inv = self.append("invoices", {})
inv.invoice_type = entry.get('voucher_type') inv.invoice_type = entry.get("voucher_type")
inv.invoice_number = entry.get('voucher_no') inv.invoice_number = entry.get("voucher_no")
inv.invoice_date = entry.get('posting_date') inv.invoice_date = entry.get("posting_date")
inv.amount = flt(entry.get('invoice_amount')) inv.amount = flt(entry.get("invoice_amount"))
inv.currency = entry.get('currency') inv.currency = entry.get("currency")
inv.outstanding_amount = flt(entry.get('outstanding_amount')) inv.outstanding_amount = flt(entry.get("outstanding_amount"))
@frappe.whitelist() @frappe.whitelist()
def allocate_entries(self, args): def allocate_entries(self, args):
self.validate_entries() self.validate_entries()
entries = [] entries = []
for pay in args.get('payments'): for pay in args.get("payments"):
pay.update({'unreconciled_amount': pay.get('amount')}) pay.update({"unreconciled_amount": pay.get("amount")})
for inv in args.get('invoices'): for inv in args.get("invoices"):
if pay.get('amount') >= inv.get('outstanding_amount'): if pay.get("amount") >= inv.get("outstanding_amount"):
res = self.get_allocated_entry(pay, inv, inv['outstanding_amount']) res = self.get_allocated_entry(pay, inv, inv["outstanding_amount"])
pay['amount'] = flt(pay.get('amount')) - flt(inv.get('outstanding_amount')) pay["amount"] = flt(pay.get("amount")) - flt(inv.get("outstanding_amount"))
inv['outstanding_amount'] = 0 inv["outstanding_amount"] = 0
else: else:
res = self.get_allocated_entry(pay, inv, pay['amount']) res = self.get_allocated_entry(pay, inv, pay["amount"])
inv['outstanding_amount'] = flt(inv.get('outstanding_amount')) - flt(pay.get('amount')) inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
pay['amount'] = 0 pay["amount"] = 0
if pay.get('amount') == 0: if pay.get("amount") == 0:
entries.append(res) entries.append(res)
break break
elif inv.get('outstanding_amount') == 0: elif inv.get("outstanding_amount") == 0:
entries.append(res) entries.append(res)
continue continue
else: else:
break break
self.set('allocation', []) self.set("allocation", [])
for entry in entries: for entry in entries:
if entry['allocated_amount'] != 0: if entry["allocated_amount"] != 0:
row = self.append('allocation', {}) row = self.append("allocation", {})
row.update(entry) row.update(entry)
def get_allocated_entry(self, pay, inv, allocated_amount): def get_allocated_entry(self, pay, inv, allocated_amount):
return frappe._dict({ return frappe._dict(
'reference_type': pay.get('reference_type'), {
'reference_name': pay.get('reference_name'), "reference_type": pay.get("reference_type"),
'reference_row': pay.get('reference_row'), "reference_name": pay.get("reference_name"),
'invoice_type': inv.get('invoice_type'), "reference_row": pay.get("reference_row"),
'invoice_number': inv.get('invoice_number'), "invoice_type": inv.get("invoice_type"),
'unreconciled_amount': pay.get('unreconciled_amount'), "invoice_number": inv.get("invoice_number"),
'amount': pay.get('amount'), "unreconciled_amount": pay.get("unreconciled_amount"),
'allocated_amount': allocated_amount, "amount": pay.get("amount"),
'difference_amount': pay.get('difference_amount') "allocated_amount": allocated_amount,
}) "difference_amount": pay.get("difference_amount"),
}
)
@frappe.whitelist() @frappe.whitelist()
def reconcile(self): def reconcile(self):
self.validate_allocation() self.validate_allocation()
dr_or_cr = ("credit_in_account_currency" dr_or_cr = (
if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") "credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
entry_list = [] entry_list = []
dr_or_cr_notes = [] dr_or_cr_notes = []
for row in self.get('allocation'): for row in self.get("allocation"):
reconciled_entry = [] reconciled_entry = []
if row.invoice_number and row.allocated_amount: if row.invoice_number and row.allocated_amount:
if row.reference_type in ['Sales Invoice', 'Purchase Invoice']: if row.reference_type in ["Sales Invoice", "Purchase Invoice"]:
reconciled_entry = dr_or_cr_notes reconciled_entry = dr_or_cr_notes
else: else:
reconciled_entry = entry_list reconciled_entry = entry_list
@ -233,23 +266,25 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries() self.get_unreconciled_entries()
def get_payment_details(self, row, dr_or_cr): def get_payment_details(self, row, dr_or_cr):
return frappe._dict({ return frappe._dict(
'voucher_type': row.get('reference_type'), {
'voucher_no' : row.get('reference_name'), "voucher_type": row.get("reference_type"),
'voucher_detail_no' : row.get('reference_row'), "voucher_no": row.get("reference_name"),
'against_voucher_type' : row.get('invoice_type'), "voucher_detail_no": row.get("reference_row"),
'against_voucher' : row.get('invoice_number'), "against_voucher_type": row.get("invoice_type"),
'account' : self.receivable_payable_account, "against_voucher": row.get("invoice_number"),
'party_type': self.party_type, "account": self.receivable_payable_account,
'party': self.party, "party_type": self.party_type,
'is_advance' : row.get('is_advance'), "party": self.party,
'dr_or_cr' : dr_or_cr, "is_advance": row.get("is_advance"),
'unreconciled_amount': flt(row.get('unreconciled_amount')), "dr_or_cr": dr_or_cr,
'unadjusted_amount' : flt(row.get('amount')), "unreconciled_amount": flt(row.get("unreconciled_amount")),
'allocated_amount' : flt(row.get('allocated_amount')), "unadjusted_amount": flt(row.get("amount")),
'difference_amount': flt(row.get('difference_amount')), "allocated_amount": flt(row.get("allocated_amount")),
'difference_account': row.get('difference_account') "difference_amount": flt(row.get("difference_amount")),
}) "difference_account": row.get("difference_account"),
}
)
def check_mandatory_to_fetch(self): def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
@ -267,7 +302,9 @@ class PaymentReconciliation(Document):
unreconciled_invoices = frappe._dict() unreconciled_invoices = frappe._dict()
for inv in self.get("invoices"): for inv in self.get("invoices"):
unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(inv.invoice_number, inv.outstanding_amount) unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(
inv.invoice_number, inv.outstanding_amount
)
invoices_to_reconcile = [] invoices_to_reconcile = []
for row in self.get("allocation"): for row in self.get("allocation"):
@ -275,13 +312,19 @@ class PaymentReconciliation(Document):
invoices_to_reconcile.append(row.invoice_number) invoices_to_reconcile.append(row.invoice_number)
if flt(row.amount) - flt(row.allocated_amount) < 0: if flt(row.amount) - flt(row.allocated_amount) < 0:
frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}") frappe.throw(
.format(row.idx, row.allocated_amount, row.amount)) _(
"Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}"
).format(row.idx, row.allocated_amount, row.amount)
)
invoice_outstanding = unreconciled_invoices.get(row.invoice_type, {}).get(row.invoice_number) invoice_outstanding = unreconciled_invoices.get(row.invoice_type, {}).get(row.invoice_number)
if flt(row.allocated_amount) - invoice_outstanding > 0.009: if flt(row.allocated_amount) - invoice_outstanding > 0.009:
frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}") frappe.throw(
.format(row.idx, row.allocated_amount, invoice_outstanding)) _(
"Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}"
).format(row.idx, row.allocated_amount, invoice_outstanding)
)
if not invoices_to_reconcile: if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table")) frappe.throw(_("No records found in Allocation table"))
@ -290,10 +333,21 @@ class PaymentReconciliation(Document):
condition = " and company = '{0}' ".format(self.company) condition = " and company = '{0}' ".format(self.company)
if get_invoices: if get_invoices:
condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) if self.from_invoice_date else "" condition += (
condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date)) if self.to_invoice_date else "" " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' if self.from_invoice_date
else "credit_in_account_currency") else ""
)
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date))
if self.to_invoice_date
else ""
)
dr_or_cr = (
"debit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "credit_in_account_currency"
)
if self.minimum_invoice_amount: if self.minimum_invoice_amount:
condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount)) condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount))
@ -302,10 +356,21 @@ class PaymentReconciliation(Document):
elif get_return_invoices: elif get_return_invoices:
condition = " and doc.company = '{0}' ".format(self.company) condition = " and doc.company = '{0}' ".format(self.company)
condition += " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else "" condition += (
condition += " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else "" " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
dr_or_cr = ("gl.debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' if self.from_payment_date
else "gl.credit_in_account_currency") else ""
)
condition += (
" and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
dr_or_cr = (
"gl.debit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "gl.credit_in_account_currency"
)
if self.minimum_invoice_amount: if self.minimum_invoice_amount:
condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount)) condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount))
@ -313,55 +378,77 @@ class PaymentReconciliation(Document):
condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount)) condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount))
else: else:
condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else "" condition += (
condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else "" " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
else ""
)
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
if self.minimum_payment_amount: if self.minimum_payment_amount:
condition += " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) if get_payments \ condition += (
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount)) else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
)
if self.maximum_payment_amount: if self.maximum_payment_amount:
condition += " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) if get_payments \ condition += (
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount)) else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
)
return condition return condition
def reconcile_dr_cr_note(dr_cr_notes, company): def reconcile_dr_cr_note(dr_cr_notes, company):
for inv in dr_cr_notes: for inv in dr_cr_notes:
voucher_type = ('Credit Note' voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
if inv.voucher_type == 'Sales Invoice' else 'Debit Note')
reconcile_dr_or_cr = ('debit_in_account_currency' reconcile_dr_or_cr = (
if inv.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') "debit_in_account_currency"
if inv.dr_or_cr == "credit_in_account_currency"
else "credit_in_account_currency"
)
company_currency = erpnext.get_company_currency(company) company_currency = erpnext.get_company_currency(company)
jv = frappe.get_doc({ jv = frappe.get_doc(
"doctype": "Journal Entry", {
"voucher_type": voucher_type, "doctype": "Journal Entry",
"posting_date": today(), "voucher_type": voucher_type,
"company": company, "posting_date": today(),
"multi_currency": 1 if inv.currency != company_currency else 0, "company": company,
"accounts": [ "multi_currency": 1 if inv.currency != company_currency else 0,
{ "accounts": [
'account': inv.account, {
'party': inv.party, "account": inv.account,
'party_type': inv.party_type, "party": inv.party,
inv.dr_or_cr: abs(inv.allocated_amount), "party_type": inv.party_type,
'reference_type': inv.against_voucher_type, inv.dr_or_cr: abs(inv.allocated_amount),
'reference_name': inv.against_voucher, "reference_type": inv.against_voucher_type,
'cost_center': erpnext.get_default_cost_center(company) "reference_name": inv.against_voucher,
}, "cost_center": erpnext.get_default_cost_center(company),
{ },
'account': inv.account, {
'party': inv.party, "account": inv.account,
'party_type': inv.party_type, "party": inv.party,
reconcile_dr_or_cr: (abs(inv.allocated_amount) "party_type": inv.party_type,
if abs(inv.unadjusted_amount) > abs(inv.allocated_amount) else abs(inv.unadjusted_amount)), reconcile_dr_or_cr: (
'reference_type': inv.voucher_type, abs(inv.allocated_amount)
'reference_name': inv.voucher_no, if abs(inv.unadjusted_amount) > abs(inv.allocated_amount)
'cost_center': erpnext.get_default_cost_center(company) else abs(inv.unadjusted_amount)
} ),
] "reference_type": inv.voucher_type,
}) "reference_name": inv.voucher_no,
"cost_center": erpnext.get_default_cost_center(company),
},
],
}
)
jv.flags.ignore_mandatory = True jv.flags.ignore_mandatory = True
jv.submit() jv.submit()

View File

@ -24,7 +24,7 @@ from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscr
class PaymentRequest(Document): class PaymentRequest(Document):
def validate(self): def validate(self):
if self.get("__islocal"): if self.get("__islocal"):
self.status = 'Draft' self.status = "Draft"
self.validate_reference_document() self.validate_reference_document()
self.validate_payment_request_amount() self.validate_payment_request_amount()
self.validate_currency() self.validate_currency()
@ -35,51 +35,67 @@ class PaymentRequest(Document):
frappe.throw(_("To create a Payment Request reference document is required")) frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self): def validate_payment_request_amount(self):
existing_payment_request_amount = \ existing_payment_request_amount = get_existing_payment_request_amount(
get_existing_payment_request_amount(self.reference_doctype, self.reference_name) self.reference_doctype, self.reference_name
)
if existing_payment_request_amount: if existing_payment_request_amount:
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if (hasattr(ref_doc, "order_type") \ if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") != "Shopping Cart":
and getattr(ref_doc, "order_type") != "Shopping Cart"):
ref_amount = get_amount(ref_doc, self.payment_account) ref_amount = get_amount(ref_doc, self.payment_account)
if existing_payment_request_amount + flt(self.grand_total)> ref_amount: if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount") frappe.throw(
.format(self.reference_doctype)) _("Total Payment Request amount cannot be greater than {0} amount").format(
self.reference_doctype
)
)
def validate_currency(self): def validate_currency(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if self.payment_account and ref_doc.currency != frappe.db.get_value("Account", self.payment_account, "account_currency"): if self.payment_account and ref_doc.currency != frappe.db.get_value(
"Account", self.payment_account, "account_currency"
):
frappe.throw(_("Transaction currency must be same as Payment Gateway currency")) frappe.throw(_("Transaction currency must be same as Payment Gateway currency"))
def validate_subscription_details(self): def validate_subscription_details(self):
if self.is_a_subscription: if self.is_a_subscription:
amount = 0 amount = 0
for subscription_plan in self.subscription_plans: for subscription_plan in self.subscription_plans:
payment_gateway = frappe.db.get_value("Subscription Plan", subscription_plan.plan, "payment_gateway") payment_gateway = frappe.db.get_value(
"Subscription Plan", subscription_plan.plan, "payment_gateway"
)
if payment_gateway != self.payment_gateway_account: if payment_gateway != self.payment_gateway_account:
frappe.throw(_('The payment gateway account in plan {0} is different from the payment gateway account in this payment request').format(subscription_plan.name)) frappe.throw(
_(
"The payment gateway account in plan {0} is different from the payment gateway account in this payment request"
).format(subscription_plan.name)
)
rate = get_plan_rate(subscription_plan.plan, quantity=subscription_plan.qty) rate = get_plan_rate(subscription_plan.plan, quantity=subscription_plan.qty)
amount += rate amount += rate
if amount != self.grand_total: if amount != self.grand_total:
frappe.msgprint(_("The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.").format(self.grand_total, amount)) frappe.msgprint(
_(
"The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document."
).format(self.grand_total, amount)
)
def on_submit(self): def on_submit(self):
if self.payment_request_type == 'Outward': if self.payment_request_type == "Outward":
self.db_set('status', 'Initiated') self.db_set("status", "Initiated")
return return
elif self.payment_request_type == 'Inward': elif self.payment_request_type == "Inward":
self.db_set('status', 'Requested') self.db_set("status", "Requested")
send_mail = self.payment_gateway_validation() if self.payment_gateway else None send_mail = self.payment_gateway_validation() if self.payment_gateway else None
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if (hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart") \ if (
or self.flags.mute_email: hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart"
) or self.flags.mute_email:
send_mail = False send_mail = False
if send_mail and self.payment_channel != "Phone": if send_mail and self.payment_channel != "Phone":
@ -101,23 +117,27 @@ class PaymentRequest(Document):
request_amount=request_amount, request_amount=request_amount,
sender=self.email_to, sender=self.email_to,
currency=self.currency, currency=self.currency,
payment_gateway=self.payment_gateway payment_gateway=self.payment_gateway,
) )
controller.validate_transaction_currency(self.currency) controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record) controller.request_for_payment(**payment_record)
def get_request_amount(self): def get_request_amount(self):
data_of_completed_requests = frappe.get_all("Integration Request", filters={ data_of_completed_requests = frappe.get_all(
'reference_doctype': self.doctype, "Integration Request",
'reference_docname': self.name, filters={
'status': 'Completed' "reference_doctype": self.doctype,
}, pluck="data") "reference_docname": self.name,
"status": "Completed",
},
pluck="data",
)
if not data_of_completed_requests: if not data_of_completed_requests:
return self.grand_total return self.grand_total
request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests) request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests)
return request_amounts return request_amounts
def on_cancel(self): def on_cancel(self):
@ -126,8 +146,9 @@ class PaymentRequest(Document):
def make_invoice(self): def make_invoice(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if (hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart"): if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
si = make_sales_invoice(self.reference_name, ignore_permissions=True) si = make_sales_invoice(self.reference_name, ignore_permissions=True)
si.allocate_advances_automatically = True si.allocate_advances_automatically = True
si = si.insert(ignore_permissions=True) si = si.insert(ignore_permissions=True)
@ -136,7 +157,7 @@ class PaymentRequest(Document):
def payment_gateway_validation(self): def payment_gateway_validation(self):
try: try:
controller = get_payment_gateway_controller(self.payment_gateway) controller = get_payment_gateway_controller(self.payment_gateway)
if hasattr(controller, 'on_payment_request_submission'): if hasattr(controller, "on_payment_request_submission"):
return controller.on_payment_request_submission(self) return controller.on_payment_request_submission(self)
else: else:
return True return True
@ -148,36 +169,45 @@ class PaymentRequest(Document):
self.payment_url = self.get_payment_url() self.payment_url = self.get_payment_url()
if self.payment_url: if self.payment_url:
self.db_set('payment_url', self.payment_url) self.db_set("payment_url", self.payment_url)
if self.payment_url or not self.payment_gateway_account \ if (
or (self.payment_gateway_account and self.payment_channel == "Phone"): self.payment_url
self.db_set('status', 'Initiated') or not self.payment_gateway_account
or (self.payment_gateway_account and self.payment_channel == "Phone")
):
self.db_set("status", "Initiated")
def get_payment_url(self): def get_payment_url(self):
if self.reference_doctype != "Fees": if self.reference_doctype != "Fees":
data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1) data = frappe.db.get_value(
self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
)
else: else:
data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["student_name"], as_dict=1) data = frappe.db.get_value(
self.reference_doctype, self.reference_name, ["student_name"], as_dict=1
)
data.update({"company": frappe.defaults.get_defaults().company}) data.update({"company": frappe.defaults.get_defaults().company})
controller = get_payment_gateway_controller(self.payment_gateway) controller = get_payment_gateway_controller(self.payment_gateway)
controller.validate_transaction_currency(self.currency) controller.validate_transaction_currency(self.currency)
if hasattr(controller, 'validate_minimum_transaction_amount'): if hasattr(controller, "validate_minimum_transaction_amount"):
controller.validate_minimum_transaction_amount(self.currency, self.grand_total) controller.validate_minimum_transaction_amount(self.currency, self.grand_total)
return controller.get_payment_url(**{ return controller.get_payment_url(
"amount": flt(self.grand_total, self.precision("grand_total")), **{
"title": data.company.encode("utf-8"), "amount": flt(self.grand_total, self.precision("grand_total")),
"description": self.subject.encode("utf-8"), "title": data.company.encode("utf-8"),
"reference_doctype": "Payment Request", "description": self.subject.encode("utf-8"),
"reference_docname": self.name, "reference_doctype": "Payment Request",
"payer_email": self.email_to or frappe.session.user, "reference_docname": self.name,
"payer_name": frappe.safe_encode(data.customer_name), "payer_email": self.email_to or frappe.session.user,
"order_id": self.name, "payer_name": frappe.safe_encode(data.customer_name),
"currency": self.currency "order_id": self.name,
}) "currency": self.currency,
}
)
def set_as_paid(self): def set_as_paid(self):
if self.payment_channel == "Phone": if self.payment_channel == "Phone":
@ -202,32 +232,47 @@ class PaymentRequest(Document):
else: else:
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company) party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account) party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(
party_account
)
bank_amount = self.grand_total bank_amount = self.grand_total
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: if (
party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
):
party_amount = ref_doc.base_grand_total party_amount = ref_doc.base_grand_total
else: else:
party_amount = self.grand_total party_amount = self.grand_total
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount, payment_entry = get_payment_entry(
bank_account=self.payment_account, bank_amount=bank_amount) self.reference_doctype,
self.reference_name,
party_amount=party_amount,
bank_account=self.payment_account,
bank_amount=bank_amount,
)
payment_entry.update({ payment_entry.update(
"reference_no": self.name, {
"reference_date": nowdate(), "reference_no": self.name,
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(self.reference_doctype, "reference_date": nowdate(),
self.reference_name, self.name) "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
}) self.reference_doctype, self.reference_name, self.name
),
}
)
if payment_entry.difference_amount: if payment_entry.difference_amount:
company_details = get_company_defaults(ref_doc.company) company_details = get_company_defaults(ref_doc.company)
payment_entry.append("deductions", { payment_entry.append(
"account": company_details.exchange_gain_loss_account, "deductions",
"cost_center": company_details.cost_center, {
"amount": payment_entry.difference_amount "account": company_details.exchange_gain_loss_account,
}) "cost_center": company_details.cost_center,
"amount": payment_entry.difference_amount,
},
)
if submit: if submit:
payment_entry.insert(ignore_permissions=True) payment_entry.insert(ignore_permissions=True)
@ -243,16 +288,23 @@ class PaymentRequest(Document):
"subject": self.subject, "subject": self.subject,
"message": self.get_message(), "message": self.get_message(),
"now": True, "now": True,
"attachments": [frappe.attach_print(self.reference_doctype, self.reference_name, "attachments": [
file_name=self.reference_name, print_format=self.print_format)]} frappe.attach_print(
enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) self.reference_doctype,
self.reference_name,
file_name=self.reference_name,
print_format=self.print_format,
)
],
}
enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
def get_message(self): def get_message(self):
"""return message with payment gateway link""" """return message with payment gateway link"""
context = { context = {
"doc": frappe.get_doc(self.reference_doctype, self.reference_name), "doc": frappe.get_doc(self.reference_doctype, self.reference_name),
"payment_url": self.payment_url "payment_url": self.payment_url,
} }
if self.message: if self.message:
@ -266,22 +318,26 @@ class PaymentRequest(Document):
def check_if_payment_entry_exists(self): def check_if_payment_entry_exists(self):
if self.status == "Paid": if self.status == "Paid":
if frappe.get_all("Payment Entry Reference", if frappe.get_all(
"Payment Entry Reference",
filters={"reference_name": self.reference_name, "docstatus": ["<", 2]}, filters={"reference_name": self.reference_name, "docstatus": ["<", 2]},
fields=["parent"], fields=["parent"],
limit=1): limit=1,
frappe.throw(_("Payment Entry already exists"), title=_('Error')) ):
frappe.throw(_("Payment Entry already exists"), title=_("Error"))
def make_communication_entry(self): def make_communication_entry(self):
"""Make communication entry""" """Make communication entry"""
comm = frappe.get_doc({ comm = frappe.get_doc(
"doctype":"Communication", {
"subject": self.subject, "doctype": "Communication",
"content": self.get_message(), "subject": self.subject,
"sent_or_received": "Sent", "content": self.get_message(),
"reference_doctype": self.reference_doctype, "sent_or_received": "Sent",
"reference_name": self.reference_name "reference_doctype": self.reference_doctype,
}) "reference_name": self.reference_name,
}
)
comm.insert(ignore_permissions=True) comm.insert(ignore_permissions=True)
def get_payment_success_url(self): def get_payment_success_url(self):
@ -298,16 +354,17 @@ class PaymentRequest(Document):
self.set_as_paid() self.set_as_paid()
# if shopping cart enabled and in session # if shopping cart enabled and in session
if (shopping_cart_settings.enabled and hasattr(frappe.local, "session") if (
and frappe.local.session.user != "Guest") and self.payment_channel != "Phone": shopping_cart_settings.enabled
and hasattr(frappe.local, "session")
and frappe.local.session.user != "Guest"
) and self.payment_channel != "Phone":
success_url = shopping_cart_settings.payment_success_url success_url = shopping_cart_settings.payment_success_url
if success_url: if success_url:
redirect_to = ({ redirect_to = ({"Orders": "/orders", "Invoices": "/invoices", "My Account": "/me"}).get(
"Orders": "/orders", success_url, "/me"
"Invoices": "/invoices", )
"My Account": "/me"
}).get(success_url, "/me")
else: else:
redirect_to = get_url("/orders/{0}".format(self.reference_name)) redirect_to = get_url("/orders/{0}".format(self.reference_name))
@ -317,6 +374,7 @@ class PaymentRequest(Document):
if payment_provider == "stripe": if payment_provider == "stripe":
return create_stripe_subscription(gateway_controller, data) return create_stripe_subscription(gateway_controller, data)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def make_payment_request(**args): def make_payment_request(**args):
"""Make payment request""" """Make payment request"""
@ -329,49 +387,62 @@ def make_payment_request(**args):
grand_total = get_amount(ref_doc, gateway_account.get("payment_account")) grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if args.loyalty_points and args.dt == "Sales Order": if args.loyalty_points and args.dt == "Sales Order":
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
frappe.db.set_value("Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False) frappe.db.set_value(
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False) "Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False
)
frappe.db.set_value(
"Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False
)
grand_total = grand_total - loyalty_amount grand_total = grand_total - loyalty_amount
bank_account = (get_party_bank_account(args.get('party_type'), args.get('party')) bank_account = (
if args.get('party_type') else '') get_party_bank_account(args.get("party_type"), args.get("party"))
if args.get("party_type")
else ""
)
existing_payment_request = None existing_payment_request = None
if args.order_type == "Shopping Cart": if args.order_type == "Shopping Cart":
existing_payment_request = frappe.db.get_value("Payment Request", existing_payment_request = frappe.db.get_value(
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)}) "Payment Request",
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
)
if existing_payment_request: if existing_payment_request:
frappe.db.set_value("Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False) frappe.db.set_value(
"Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False
)
pr = frappe.get_doc("Payment Request", existing_payment_request) pr = frappe.get_doc("Payment Request", existing_payment_request)
else: else:
if args.order_type != "Shopping Cart": if args.order_type != "Shopping Cart":
existing_payment_request_amount = \ existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount: if existing_payment_request_amount:
grand_total -= existing_payment_request_amount grand_total -= existing_payment_request_amount
pr = frappe.new_doc("Payment Request") pr = frappe.new_doc("Payment Request")
pr.update({ pr.update(
"payment_gateway_account": gateway_account.get("name"), {
"payment_gateway": gateway_account.get("payment_gateway"), "payment_gateway_account": gateway_account.get("name"),
"payment_account": gateway_account.get("payment_account"), "payment_gateway": gateway_account.get("payment_gateway"),
"payment_channel": gateway_account.get("payment_channel"), "payment_account": gateway_account.get("payment_account"),
"payment_request_type": args.get("payment_request_type"), "payment_channel": gateway_account.get("payment_channel"),
"currency": ref_doc.currency, "payment_request_type": args.get("payment_request_type"),
"grand_total": grand_total, "currency": ref_doc.currency,
"mode_of_payment": args.mode_of_payment, "grand_total": grand_total,
"email_to": args.recipient_id or ref_doc.owner, "mode_of_payment": args.mode_of_payment,
"subject": _("Payment Request for {0}").format(args.dn), "email_to": args.recipient_id or ref_doc.owner,
"message": gateway_account.get("message") or get_dummy_message(ref_doc), "subject": _("Payment Request for {0}").format(args.dn),
"reference_doctype": args.dt, "message": gateway_account.get("message") or get_dummy_message(ref_doc),
"reference_name": args.dn, "reference_doctype": args.dt,
"party_type": args.get("party_type") or "Customer", "reference_name": args.dn,
"party": args.get("party") or ref_doc.get("customer"), "party_type": args.get("party_type") or "Customer",
"bank_account": bank_account "party": args.get("party") or ref_doc.get("customer"),
}) "bank_account": bank_account,
}
)
if args.order_type == "Shopping Cart" or args.mute_email: if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True pr.flags.mute_email = True
@ -390,6 +461,7 @@ def make_payment_request(**args):
return pr.as_dict() return pr.as_dict()
def get_amount(ref_doc, payment_account=None): def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype""" """get amount based on doctype"""
dt = ref_doc.doctype dt = ref_doc.doctype
@ -411,18 +483,20 @@ def get_amount(ref_doc, payment_account=None):
elif dt == "Fees": elif dt == "Fees":
grand_total = ref_doc.outstanding_amount grand_total = ref_doc.outstanding_amount
if grand_total > 0 : if grand_total > 0:
return grand_total return grand_total
else: else:
frappe.throw(_("Payment Entry is already created")) frappe.throw(_("Payment Entry is already created"))
def get_existing_payment_request_amount(ref_dt, ref_dn): def get_existing_payment_request_amount(ref_dt, ref_dn):
""" """
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
and get the summation of existing paid payment request for Phone payment channel. and get the summation of existing paid payment request for Phone payment channel.
""" """
existing_payment_request_amount = frappe.db.sql(""" existing_payment_request_amount = frappe.db.sql(
"""
select sum(grand_total) select sum(grand_total)
from `tabPayment Request` from `tabPayment Request`
where where
@ -432,10 +506,13 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
and (status != 'Paid' and (status != 'Paid'
or (payment_channel = 'Phone' or (payment_channel = 'Phone'
and status = 'Paid')) and status = 'Paid'))
""", (ref_dt, ref_dn)) """,
(ref_dt, ref_dn),
)
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0 return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
def get_gateway_details(args): # nosemgrep
def get_gateway_details(args): # nosemgrep
"""return gateway and payment account of default payment gateway""" """return gateway and payment account of default payment gateway"""
if args.get("payment_gateway_account"): if args.get("payment_gateway_account"):
return get_payment_gateway_account(args.get("payment_gateway_account")) return get_payment_gateway_account(args.get("payment_gateway_account"))
@ -448,58 +525,74 @@ def get_gateway_details(args): # nosemgrep
return gateway_account return gateway_account
def get_payment_gateway_account(args): def get_payment_gateway_account(args):
return frappe.db.get_value("Payment Gateway Account", args, return frappe.db.get_value(
"Payment Gateway Account",
args,
["name", "payment_gateway", "payment_account", "message"], ["name", "payment_gateway", "payment_account", "message"],
as_dict=1) as_dict=1,
)
@frappe.whitelist() @frappe.whitelist()
def get_print_format_list(ref_doctype): def get_print_format_list(ref_doctype):
print_format_list = ["Standard"] print_format_list = ["Standard"]
print_format_list.extend([p.name for p in frappe.get_all("Print Format", print_format_list.extend(
filters={"doc_type": ref_doctype})]) [p.name for p in frappe.get_all("Print Format", filters={"doc_type": ref_doctype})]
)
return {"print_format": print_format_list}
return {
"print_format": print_format_list
}
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def resend_payment_email(docname): def resend_payment_email(docname):
return frappe.get_doc("Payment Request", docname).send_email() return frappe.get_doc("Payment Request", docname).send_email()
@frappe.whitelist() @frappe.whitelist()
def make_payment_entry(docname): def make_payment_entry(docname):
doc = frappe.get_doc("Payment Request", docname) doc = frappe.get_doc("Payment Request", docname)
return doc.create_payment_entry(submit=False).as_dict() return doc.create_payment_entry(submit=False).as_dict()
def update_payment_req_status(doc, method): def update_payment_req_status(doc, method):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
for ref in doc.references: for ref in doc.references:
payment_request_name = frappe.db.get_value("Payment Request", payment_request_name = frappe.db.get_value(
{"reference_doctype": ref.reference_doctype, "reference_name": ref.reference_name, "Payment Request",
"docstatus": 1}) {
"reference_doctype": ref.reference_doctype,
"reference_name": ref.reference_name,
"docstatus": 1,
},
)
if payment_request_name: if payment_request_name:
ref_details = get_reference_details(ref.reference_doctype, ref.reference_name, doc.party_account_currency) ref_details = get_reference_details(
pay_req_doc = frappe.get_doc('Payment Request', payment_request_name) ref.reference_doctype, ref.reference_name, doc.party_account_currency
)
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
status = pay_req_doc.status status = pay_req_doc.status
if status != "Paid" and not ref_details.outstanding_amount: if status != "Paid" and not ref_details.outstanding_amount:
status = 'Paid' status = "Paid"
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount: elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
status = 'Partially Paid' status = "Partially Paid"
elif ref_details.outstanding_amount == ref_details.total_amount: elif ref_details.outstanding_amount == ref_details.total_amount:
if pay_req_doc.payment_request_type == 'Outward': if pay_req_doc.payment_request_type == "Outward":
status = 'Initiated' status = "Initiated"
elif pay_req_doc.payment_request_type == 'Inward': elif pay_req_doc.payment_request_type == "Inward":
status = 'Requested' status = "Requested"
pay_req_doc.db_set("status", status)
pay_req_doc.db_set('status', status)
def get_dummy_message(doc): def get_dummy_message(doc):
return frappe.render_template("""{% if doc.contact_person -%} return frappe.render_template(
"""{% if doc.contact_person -%}
<p>Dear {{ doc.contact_person }},</p> <p>Dear {{ doc.contact_person }},</p>
{%- else %}<p>Hello,</p>{% endif %} {%- else %}<p>Hello,</p>{% endif %}
@ -511,12 +604,19 @@ def get_dummy_message(doc):
<p>{{ _("If you have any questions, please get back to us.") }}</p> <p>{{ _("If you have any questions, please get back to us.") }}</p>
<p>{{ _("Thank you for your business!") }}</p> <p>{{ _("Thank you for your business!") }}</p>
""", dict(doc=doc, payment_url = '{{ payment_url }}')) """,
dict(doc=doc, payment_url="{{ payment_url }}"),
)
@frappe.whitelist() @frappe.whitelist()
def get_subscription_details(reference_doctype, reference_name): def get_subscription_details(reference_doctype, reference_name):
if reference_doctype == "Sales Invoice": if reference_doctype == "Sales Invoice":
subscriptions = frappe.db.sql("""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",reference_name, as_dict=1) subscriptions = frappe.db.sql(
"""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",
reference_name,
as_dict=1,
)
subscription_plans = [] subscription_plans = []
for subscription in subscriptions: for subscription in subscriptions:
plans = frappe.get_doc("Subscription", subscription.sub_name).plans plans = frappe.get_doc("Subscription", subscription.sub_name).plans
@ -524,38 +624,50 @@ def get_subscription_details(reference_doctype, reference_name):
subscription_plans.append(plan) subscription_plans.append(plan)
return subscription_plans return subscription_plans
@frappe.whitelist() @frappe.whitelist()
def make_payment_order(source_name, target_doc=None): def make_payment_order(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target): def set_missing_values(source, target):
target.payment_order_type = "Payment Request" target.payment_order_type = "Payment Request"
target.append('references', { target.append(
'reference_doctype': source.reference_doctype, "references",
'reference_name': source.reference_name, {
'amount': source.grand_total, "reference_doctype": source.reference_doctype,
'supplier': source.party, "reference_name": source.reference_name,
'payment_request': source_name, "amount": source.grand_total,
'mode_of_payment': source.mode_of_payment, "supplier": source.party,
'bank_account': source.bank_account, "payment_request": source_name,
'account': source.account "mode_of_payment": source.mode_of_payment,
}) "bank_account": source.bank_account,
"account": source.account,
},
)
doclist = get_mapped_doc("Payment Request", source_name, { doclist = get_mapped_doc(
"Payment Request": { "Payment Request",
"doctype": "Payment Order", source_name,
} {
}, target_doc, set_missing_values) "Payment Request": {
"doctype": "Payment Order",
}
},
target_doc,
set_missing_values,
)
return doclist return doclist
def validate_payment(doc, method=None): def validate_payment(doc, method=None):
if doc.reference_doctype != "Payment Request" or ( if doc.reference_doctype != "Payment Request" or (
frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status') frappe.db.get_value(doc.reference_doctype, doc.reference_docname, "status") != "Paid"
!= "Paid"
): ):
return return
frappe.throw( frappe.throw(
_("The Payment Request {0} is already paid, cannot process payment twice") _("The Payment Request {0} is already paid, cannot process payment twice").format(
.format(doc.reference_docname) doc.reference_docname
)
) )

View File

@ -12,10 +12,7 @@ from erpnext.setup.utils import get_exchange_rate
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"] test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
payment_gateway = { payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
"doctype": "Payment Gateway",
"gateway": "_Test Gateway"
}
payment_method = [ payment_method = [
{ {
@ -23,30 +20,38 @@ payment_method = [
"is_default": 1, "is_default": 1,
"payment_gateway": "_Test Gateway", "payment_gateway": "_Test Gateway",
"payment_account": "_Test Bank - _TC", "payment_account": "_Test Bank - _TC",
"currency": "INR" "currency": "INR",
}, },
{ {
"doctype": "Payment Gateway Account", "doctype": "Payment Gateway Account",
"payment_gateway": "_Test Gateway", "payment_gateway": "_Test Gateway",
"payment_account": "_Test Bank USD - _TC", "payment_account": "_Test Bank USD - _TC",
"currency": "USD" "currency": "USD",
} },
] ]
class TestPaymentRequest(unittest.TestCase): class TestPaymentRequest(unittest.TestCase):
def setUp(self): def setUp(self):
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"): if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
frappe.get_doc(payment_gateway).insert(ignore_permissions=True) frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
for method in payment_method: for method in payment_method:
if not frappe.db.get_value("Payment Gateway Account", {"payment_gateway": method["payment_gateway"], if not frappe.db.get_value(
"currency": method["currency"]}, "name"): "Payment Gateway Account",
{"payment_gateway": method["payment_gateway"], "currency": method["currency"]},
"name",
):
frappe.get_doc(method).insert(ignore_permissions=True) frappe.get_doc(method).insert(ignore_permissions=True)
def test_payment_request_linkings(self): def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR") so_inr = make_sales_order(currency="INR")
pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(
payment_gateway_account="_Test Gateway - INR") dt="Sales Order",
dn=so_inr.name,
recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - INR",
)
self.assertEqual(pr.reference_doctype, "Sales Order") self.assertEqual(pr.reference_doctype, "Sales Order")
self.assertEqual(pr.reference_name, so_inr.name) self.assertEqual(pr.reference_name, so_inr.name)
@ -55,45 +60,75 @@ class TestPaymentRequest(unittest.TestCase):
conversion_rate = get_exchange_rate("USD", "INR") conversion_rate = get_exchange_rate("USD", "INR")
si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate) si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(
payment_gateway_account="_Test Gateway - USD") dt="Sales Invoice",
dn=si_usd.name,
recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - USD",
)
self.assertEqual(pr.reference_doctype, "Sales Invoice") self.assertEqual(pr.reference_doctype, "Sales Invoice")
self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.reference_name, si_usd.name)
self.assertEqual(pr.currency, "USD") self.assertEqual(pr.currency, "USD")
def test_payment_entry(self): def test_payment_entry(self):
frappe.db.set_value("Company", "_Test Company", frappe.db.set_value(
"exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC") "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
)
frappe.db.set_value("Company", "_Test Company", "write_off_account", "_Test Write Off - _TC") frappe.db.set_value("Company", "_Test Company", "write_off_account", "_Test Write Off - _TC")
frappe.db.set_value("Company", "_Test Company", "cost_center", "_Test Cost Center - _TC") frappe.db.set_value("Company", "_Test Company", "cost_center", "_Test Cost Center - _TC")
so_inr = make_sales_order(currency="INR") so_inr = make_sales_order(currency="INR")
pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(
mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1) dt="Sales Order",
dn=so_inr.name,
recipient_id="saurabh@erpnext.com",
mute_email=1,
payment_gateway_account="_Test Gateway - INR",
submit_doc=1,
return_doc=1,
)
pe = pr.set_as_paid() pe = pr.set_as_paid()
so_inr = frappe.get_doc("Sales Order", so_inr.name) so_inr = frappe.get_doc("Sales Order", so_inr.name)
self.assertEqual(so_inr.advance_paid, 1000) self.assertEqual(so_inr.advance_paid, 1000)
si_usd = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", si_usd = create_sales_invoice(
currency="USD", conversion_rate=50) customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(
mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1) dt="Sales Invoice",
dn=si_usd.name,
recipient_id="saurabh@erpnext.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
submit_doc=1,
return_doc=1,
)
pe = pr.set_as_paid() pe = pr.set_as_paid()
expected_gle = dict((d[0], d) for d in [ expected_gle = dict(
["_Test Receivable USD - _TC", 0, 5000, si_usd.name], (d[0], d)
[pr.payment_account, 6290.0, 0, None], for d in [
["_Test Exchange Gain/Loss - _TC", 0, 1290, None] ["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
]) [pr.payment_account, 6290.0, 0, None],
["_Test Exchange Gain/Loss - _TC", 0, 1290, None],
]
)
gl_entries = frappe.db.sql("""select account, debit, credit, against_voucher gl_entries = frappe.db.sql(
"""select account, debit, credit, against_voucher
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
order by account asc""", pe.name, as_dict=1) order by account asc""",
pe.name,
as_dict=1,
)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
@ -104,35 +139,48 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(expected_gle[gle.account][3], gle.against_voucher) self.assertEqual(expected_gle[gle.account][3], gle.against_voucher)
def test_status(self): def test_status(self):
si_usd = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", si_usd = create_sales_invoice(
currency="USD", conversion_rate=50) customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(
mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1) dt="Sales Invoice",
dn=si_usd.name,
recipient_id="saurabh@erpnext.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
submit_doc=1,
return_doc=1,
)
pe = pr.create_payment_entry() pe = pr.create_payment_entry()
pr.load_from_db() pr.load_from_db()
self.assertEqual(pr.status, 'Paid') self.assertEqual(pr.status, "Paid")
pe.cancel() pe.cancel()
pr.load_from_db() pr.load_from_db()
self.assertEqual(pr.status, 'Requested') self.assertEqual(pr.status, "Requested")
def test_multiple_payment_entries_against_sales_order(self): def test_multiple_payment_entries_against_sales_order(self):
# Make Sales Order, grand_total = 1000 # Make Sales Order, grand_total = 1000
so = make_sales_order() so = make_sales_order()
# Payment Request amount = 200 # Payment Request amount = 200
pr1 = make_payment_request(dt="Sales Order", dn=so.name, pr1 = make_payment_request(
recipient_id="nabin@erpnext.com", return_doc=1) dt="Sales Order", dn=so.name, recipient_id="nabin@erpnext.com", return_doc=1
)
pr1.grand_total = 200 pr1.grand_total = 200
pr1.submit() pr1.submit()
# Make a 2nd Payment Request # Make a 2nd Payment Request
pr2 = make_payment_request(dt="Sales Order", dn=so.name, pr2 = make_payment_request(
recipient_id="nabin@erpnext.com", return_doc=1) dt="Sales Order", dn=so.name, recipient_id="nabin@erpnext.com", return_doc=1
)
self.assertEqual(pr2.grand_total, 800) self.assertEqual(pr2.grand_total, 800)

View File

@ -3,18 +3,10 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'payment_term', "fieldname": "payment_term",
'transactions': [ "transactions": [
{ {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Quotation"]},
'label': _('Sales'), {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order"]},
'items': ['Sales Invoice', 'Sales Order', 'Quotation'] {"items": ["Payment Terms Template"]},
}, ],
{
'label': _('Purchase'),
'items': ['Purchase Invoice', 'Purchase Order']
},
{
'items': ['Payment Terms Template']
}
]
} }

View File

@ -16,10 +16,12 @@ class PaymentTermsTemplate(Document):
def validate_invoice_portion(self): def validate_invoice_portion(self):
total_portion = 0 total_portion = 0
for term in self.terms: for term in self.terms:
total_portion += flt(term.get('invoice_portion', 0)) total_portion += flt(term.get("invoice_portion", 0))
if flt(total_portion, 2) != 100.00: if flt(total_portion, 2) != 100.00:
frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red') frappe.msgprint(
_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red"
)
def check_duplicate_terms(self): def check_duplicate_terms(self):
terms = [] terms = []
@ -27,8 +29,9 @@ class PaymentTermsTemplate(Document):
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on) term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
if term_info in terms: if term_info in terms:
frappe.msgprint( frappe.msgprint(
_('The Payment Term at row {0} is possibly a duplicate.').format(term.idx), _("The Payment Term at row {0} is possibly a duplicate.").format(term.idx),
raise_exception=1, indicator='red' raise_exception=1,
indicator="red",
) )
else: else:
terms.append(term_info) terms.append(term_info)

View File

@ -3,29 +3,17 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'payment_terms_template', "fieldname": "payment_terms_template",
'non_standard_fieldnames': { "non_standard_fieldnames": {
'Customer Group': 'payment_terms', "Customer Group": "payment_terms",
'Supplier Group': 'payment_terms', "Supplier Group": "payment_terms",
'Supplier': 'payment_terms', "Supplier": "payment_terms",
'Customer': 'payment_terms' "Customer": "payment_terms",
}, },
'transactions': [ "transactions": [
{ {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Quotation"]},
'label': _('Sales'), {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order"]},
'items': ['Sales Invoice', 'Sales Order', 'Quotation'] {"label": _("Party"), "items": ["Customer", "Supplier"]},
}, {"label": _("Group"), "items": ["Customer Group", "Supplier Group"]},
{ ],
'label': _('Purchase'),
'items': ['Purchase Invoice', 'Purchase Order']
},
{
'label': _('Party'),
'items': ['Customer', 'Supplier']
},
{
'label': _('Group'),
'items': ['Customer Group', 'Supplier Group']
}
]
} }

View File

@ -8,64 +8,76 @@ import frappe
class TestPaymentTermsTemplate(unittest.TestCase): class TestPaymentTermsTemplate(unittest.TestCase):
def tearDown(self): def tearDown(self):
frappe.delete_doc('Payment Terms Template', '_Test Payment Terms Template For Test', force=1) frappe.delete_doc("Payment Terms Template", "_Test Payment Terms Template For Test", force=1)
def test_create_template(self): def test_create_template(self):
template = frappe.get_doc({ template = frappe.get_doc(
'doctype': 'Payment Terms Template', {
'template_name': '_Test Payment Terms Template For Test', "doctype": "Payment Terms Template",
'terms': [{ "template_name": "_Test Payment Terms Template For Test",
'doctype': 'Payment Terms Template Detail', "terms": [
'invoice_portion': 50.00, {
'credit_days_based_on': 'Day(s) after invoice date', "doctype": "Payment Terms Template Detail",
'credit_days': 30 "invoice_portion": 50.00,
}] "credit_days_based_on": "Day(s) after invoice date",
}) "credit_days": 30,
}
],
}
)
self.assertRaises(frappe.ValidationError, template.insert) self.assertRaises(frappe.ValidationError, template.insert)
template.append('terms', { template.append(
'doctype': 'Payment Terms Template Detail', "terms",
'invoice_portion': 50.00, {
'credit_days_based_on': 'Day(s) after invoice date', "doctype": "Payment Terms Template Detail",
'credit_days': 0 "invoice_portion": 50.00,
}) "credit_days_based_on": "Day(s) after invoice date",
"credit_days": 0,
},
)
template.insert() template.insert()
def test_credit_days(self): def test_credit_days(self):
template = frappe.get_doc({ template = frappe.get_doc(
'doctype': 'Payment Terms Template', {
'template_name': '_Test Payment Terms Template For Test', "doctype": "Payment Terms Template",
'terms': [{ "template_name": "_Test Payment Terms Template For Test",
'doctype': 'Payment Terms Template Detail', "terms": [
'invoice_portion': 100.00, {
'credit_days_based_on': 'Day(s) after invoice date', "doctype": "Payment Terms Template Detail",
'credit_days': -30 "invoice_portion": 100.00,
}] "credit_days_based_on": "Day(s) after invoice date",
}) "credit_days": -30,
}
],
}
)
self.assertRaises(frappe.ValidationError, template.insert) self.assertRaises(frappe.ValidationError, template.insert)
def test_duplicate_terms(self): def test_duplicate_terms(self):
template = frappe.get_doc({ template = frappe.get_doc(
'doctype': 'Payment Terms Template', {
'template_name': '_Test Payment Terms Template For Test', "doctype": "Payment Terms Template",
'terms': [ "template_name": "_Test Payment Terms Template For Test",
{ "terms": [
'doctype': 'Payment Terms Template Detail', {
'invoice_portion': 50.00, "doctype": "Payment Terms Template Detail",
'credit_days_based_on': 'Day(s) after invoice date', "invoice_portion": 50.00,
'credit_days': 30 "credit_days_based_on": "Day(s) after invoice date",
}, "credit_days": 30,
{ },
'doctype': 'Payment Terms Template Detail', {
'invoice_portion': 50.00, "doctype": "Payment Terms Template Detail",
'credit_days_based_on': 'Day(s) after invoice date', "invoice_portion": 50.00,
'credit_days': 30 "credit_days_based_on": "Day(s) after invoice date",
} "credit_days": 30,
},
] ],
}) }
)
self.assertRaises(frappe.ValidationError, template.insert) self.assertRaises(frappe.ValidationError, template.insert)

View File

@ -23,40 +23,52 @@ class PeriodClosingVoucher(AccountsController):
self.make_gl_entries() self.make_gl_entries()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
from erpnext.accounts.general_ledger import make_reverse_gl_entries from erpnext.accounts.general_ledger import make_reverse_gl_entries
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name) make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
def validate_account_head(self): def validate_account_head(self):
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type") closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
if closing_account_type not in ["Liability", "Equity"]: if closing_account_type not in ["Liability", "Equity"]:
frappe.throw(_("Closing Account {0} must be of type Liability / Equity") frappe.throw(
.format(self.closing_account_head)) _("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head)
)
account_currency = get_account_currency(self.closing_account_head) account_currency = get_account_currency(self.closing_account_head)
company_currency = frappe.get_cached_value('Company', self.company, "default_currency") company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
if account_currency != company_currency: if account_currency != company_currency:
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
def validate_posting_date(self): def validate_posting_date(self):
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
validate_fiscal_year(self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self) validate_fiscal_year(
self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
)
self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1] self.year_start_date = get_fiscal_year(
self.posting_date, self.fiscal_year, company=self.company
)[1]
pce = frappe.db.sql("""select name from `tabPeriod Closing Voucher` pce = frappe.db.sql(
"""select name from `tabPeriod Closing Voucher`
where posting_date > %s and fiscal_year = %s and docstatus = 1""", where posting_date > %s and fiscal_year = %s and docstatus = 1""",
(self.posting_date, self.fiscal_year)) (self.posting_date, self.fiscal_year),
)
if pce and pce[0][0]: if pce and pce[0][0]:
frappe.throw(_("Another Period Closing Entry {0} has been made after {1}") frappe.throw(
.format(pce[0][0], self.posting_date)) _("Another Period Closing Entry {0} has been made after {1}").format(
pce[0][0], self.posting_date
)
)
def make_gl_entries(self): def make_gl_entries(self):
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
if gl_entries: if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries) make_gl_entries(gl_entries)
def get_gl_entries(self): def get_gl_entries(self):
@ -65,16 +77,29 @@ class PeriodClosingVoucher(AccountsController):
for acc in pl_accounts: for acc in pl_accounts:
if flt(acc.bal_in_company_currency): if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gl_dict({ gl_entries.append(
"account": acc.account, self.get_gl_dict(
"cost_center": acc.cost_center, {
"finance_book": acc.finance_book, "account": acc.account,
"account_currency": acc.account_currency, "cost_center": acc.cost_center,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, "finance_book": acc.finance_book,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, "account_currency": acc.account_currency,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0 if flt(acc.bal_in_account_currency) < 0
}, item=acc)) else 0,
"debit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) > 0
else 0,
},
item=acc,
)
)
if gl_entries: if gl_entries:
gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts) gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
@ -89,16 +114,27 @@ class PeriodClosingVoucher(AccountsController):
for acc in pl_accounts: for acc in pl_accounts:
if flt(acc.bal_in_company_currency): if flt(acc.bal_in_company_currency):
cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
gl_entry = self.get_gl_dict({ gl_entry = self.get_gl_dict(
"account": self.closing_account_head, {
"cost_center": cost_center, "account": self.closing_account_head,
"finance_book": acc.finance_book, "cost_center": cost_center,
"account_currency": acc.account_currency, "finance_book": acc.finance_book,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, "account_currency": acc.account_currency,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, if flt(acc.bal_in_account_currency) > 0
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0 else 0,
}, item=acc) "debit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) > 0
else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
},
item=acc,
)
self.update_default_dimensions(gl_entry) self.update_default_dimensions(gl_entry)
@ -112,20 +148,19 @@ class PeriodClosingVoucher(AccountsController):
_, default_dimensions = get_dimensions() _, default_dimensions = get_dimensions()
for dimension in self.accounting_dimensions: for dimension in self.accounting_dimensions:
gl_entry.update({ gl_entry.update({dimension: default_dimensions.get(self.company, {}).get(dimension)})
dimension: default_dimensions.get(self.company, {}).get(dimension)
})
def get_pl_balances(self): def get_pl_balances(self):
"""Get balance for dimension-wise pl accounts""" """Get balance for dimension-wise pl accounts"""
dimension_fields = ['t1.cost_center', 't1.finance_book'] dimension_fields = ["t1.cost_center", "t1.finance_book"]
self.accounting_dimensions = get_accounting_dimensions() self.accounting_dimensions = get_accounting_dimensions()
for dimension in self.accounting_dimensions: for dimension in self.accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension)) dimension_fields.append("t1.{0}".format(dimension))
return frappe.db.sql(""" return frappe.db.sql(
"""
select select
t1.account, t2.account_currency, {dimension_fields}, t1.account, t2.account_currency, {dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency, sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
@ -135,4 +170,9 @@ class PeriodClosingVoucher(AccountsController):
and t2.docstatus < 2 and t2.company = %s and t2.docstatus < 2 and t2.company = %s
and t1.posting_date between %s and %s and t1.posting_date between %s and %s
group by t1.account, {dimension_fields} group by t1.account, {dimension_fields}
""".format(dimension_fields = ', '.join(dimension_fields)), (self.company, self.get("year_start_date"), self.posting_date), as_dict=1) """.format(
dimension_fields=", ".join(dimension_fields)
),
(self.company, self.get("year_start_date"), self.posting_date),
as_dict=1,
)

View File

@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest import unittest
import frappe import frappe
@ -19,7 +18,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
company = create_company() company = create_company()
cost_center = create_cost_center('Test Cost Center 1') cost_center = create_cost_center("Test Cost Center 1")
jv1 = make_journal_entry( jv1 = make_journal_entry(
amount=400, amount=400,
@ -27,7 +26,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
account2="Sales - TPC", account2="Sales - TPC",
cost_center=cost_center, cost_center=cost_center,
posting_date=now(), posting_date=now(),
save=False save=False,
) )
jv1.company = company jv1.company = company
jv1.save() jv1.save()
@ -39,7 +38,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
account2="Cash - TPC", account2="Cash - TPC",
cost_center=cost_center, cost_center=cost_center,
posting_date=now(), posting_date=now(),
save=False save=False,
) )
jv2.company = company jv2.company = company
jv2.save() jv2.save()
@ -49,14 +48,17 @@ class TestPeriodClosingVoucher(unittest.TestCase):
surplus_account = pcv.closing_account_head surplus_account = pcv.closing_account_head
expected_gle = ( expected_gle = (
('Cost of Goods Sold - TPC', 0.0, 600.0), ("Cost of Goods Sold - TPC", 0.0, 600.0),
(surplus_account, 600.0, 400.0), (surplus_account, 600.0, 400.0),
('Sales - TPC', 400.0, 0.0) ("Sales - TPC", 400.0, 0.0),
) )
pcv_gle = frappe.db.sql(""" pcv_gle = frappe.db.sql(
"""
select account, debit, credit from `tabGL Entry` where voucher_no=%s order by account select account, debit, credit from `tabGL Entry` where voucher_no=%s order by account
""", (pcv.name)) """,
(pcv.name),
)
self.assertEqual(pcv_gle, expected_gle) self.assertEqual(pcv_gle, expected_gle)
@ -75,7 +77,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
income_account="Sales - TPC", income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC", expense_account="Cost of Goods Sold - TPC",
rate=400, rate=400,
debit_to="Debtors - TPC" debit_to="Debtors - TPC",
) )
create_sales_invoice( create_sales_invoice(
company=company, company=company,
@ -83,7 +85,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
income_account="Sales - TPC", income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC", expense_account="Cost of Goods Sold - TPC",
rate=200, rate=200,
debit_to="Debtors - TPC" debit_to="Debtors - TPC",
) )
pcv = self.make_period_closing_voucher(submit=False) pcv = self.make_period_closing_voucher(submit=False)
@ -95,15 +97,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expected_gle = ( expected_gle = (
(surplus_account, 0.0, 400.0, cost_center1), (surplus_account, 0.0, 400.0, cost_center1),
(surplus_account, 0.0, 200.0, cost_center2), (surplus_account, 0.0, 200.0, cost_center2),
('Sales - TPC', 400.0, 0.0, cost_center1), ("Sales - TPC", 400.0, 0.0, cost_center1),
('Sales - TPC', 200.0, 0.0, cost_center2), ("Sales - TPC", 200.0, 0.0, cost_center2),
) )
pcv_gle = frappe.db.sql(""" pcv_gle = frappe.db.sql(
"""
select account, debit, credit, cost_center select account, debit, credit, cost_center
from `tabGL Entry` where voucher_no=%s from `tabGL Entry` where voucher_no=%s
order by account, cost_center order by account, cost_center
""", (pcv.name)) """,
(pcv.name),
)
self.assertEqual(pcv_gle, expected_gle) self.assertEqual(pcv_gle, expected_gle)
@ -120,14 +125,14 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expense_account="Cost of Goods Sold - TPC", expense_account="Cost of Goods Sold - TPC",
cost_center=cost_center, cost_center=cost_center,
rate=400, rate=400,
debit_to="Debtors - TPC" debit_to="Debtors - TPC",
) )
jv = make_journal_entry( jv = make_journal_entry(
account1="Cash - TPC", account1="Cash - TPC",
account2="Sales - TPC", account2="Sales - TPC",
amount=400, amount=400,
cost_center=cost_center, cost_center=cost_center,
posting_date=now() posting_date=now(),
) )
jv.company = company jv.company = company
jv.finance_book = create_finance_book().name jv.finance_book = create_finance_book().name
@ -140,69 +145,84 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expected_gle = ( expected_gle = (
(surplus_account, 0.0, 400.0, None), (surplus_account, 0.0, 400.0, None),
(surplus_account, 0.0, 400.0, jv.finance_book), (surplus_account, 0.0, 400.0, jv.finance_book),
('Sales - TPC', 400.0, 0.0, None), ("Sales - TPC", 400.0, 0.0, None),
('Sales - TPC', 400.0, 0.0, jv.finance_book) ("Sales - TPC", 400.0, 0.0, jv.finance_book),
) )
pcv_gle = frappe.db.sql(""" pcv_gle = frappe.db.sql(
"""
select account, debit, credit, finance_book select account, debit, credit, finance_book
from `tabGL Entry` where voucher_no=%s from `tabGL Entry` where voucher_no=%s
order by account, finance_book order by account, finance_book
""", (pcv.name)) """,
(pcv.name),
)
self.assertEqual(pcv_gle, expected_gle) self.assertEqual(pcv_gle, expected_gle)
def make_period_closing_voucher(self, submit=True): def make_period_closing_voucher(self, submit=True):
surplus_account = create_account() surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1") cost_center = create_cost_center("Test Cost Center 1")
pcv = frappe.get_doc({ pcv = frappe.get_doc(
"doctype": "Period Closing Voucher", {
"transaction_date": today(), "doctype": "Period Closing Voucher",
"posting_date": today(), "transaction_date": today(),
"company": "Test PCV Company", "posting_date": today(),
"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0], "company": "Test PCV Company",
"cost_center": cost_center, "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
"closing_account_head": surplus_account, "cost_center": cost_center,
"remarks": "test" "closing_account_head": surplus_account,
}) "remarks": "test",
}
)
pcv.insert() pcv.insert()
if submit: if submit:
pcv.submit() pcv.submit()
return pcv return pcv
def create_company(): def create_company():
company = frappe.get_doc({ company = frappe.get_doc(
'doctype': 'Company', {
'company_name': "Test PCV Company", "doctype": "Company",
'country': 'United States', "company_name": "Test PCV Company",
'default_currency': 'USD' "country": "United States",
}) "default_currency": "USD",
company.insert(ignore_if_duplicate = True) }
)
company.insert(ignore_if_duplicate=True)
return company.name return company.name
def create_account(): def create_account():
account = frappe.get_doc({ account = frappe.get_doc(
"account_name": "Reserve and Surplus", {
"is_group": 0, "account_name": "Reserve and Surplus",
"company": "Test PCV Company", "is_group": 0,
"root_type": "Liability", "company": "Test PCV Company",
"report_type": "Balance Sheet", "root_type": "Liability",
"account_currency": "USD", "report_type": "Balance Sheet",
"parent_account": "Current Liabilities - TPC", "account_currency": "USD",
"doctype": "Account" "parent_account": "Current Liabilities - TPC",
}).insert(ignore_if_duplicate = True) "doctype": "Account",
}
).insert(ignore_if_duplicate=True)
return account.name return account.name
def create_cost_center(cc_name): def create_cost_center(cc_name):
costcenter = frappe.get_doc({ costcenter = frappe.get_doc(
"company": "Test PCV Company", {
"cost_center_name": cc_name, "company": "Test PCV Company",
"doctype": "Cost Center", "cost_center_name": cc_name,
"parent_cost_center": "Test PCV Company - TPC" "doctype": "Cost Center",
}) "parent_cost_center": "Test PCV Company - TPC",
costcenter.insert(ignore_if_duplicate = True) }
)
costcenter.insert(ignore_if_duplicate=True)
return costcenter.name return costcenter.name
test_dependencies = ["Customer", "Cost Center"] test_dependencies = ["Customer", "Cost Center"]
test_records = frappe.get_test_records("Period Closing Voucher") test_records = frappe.get_test_records("Period Closing Voucher")

Some files were not shown because too many files have changed in this diff Show More