Merge branch 'develop' into develop

This commit is contained in:
Nabin Hait 2020-02-26 13:21:38 +05:30 committed by GitHub
commit 2bd6ac56c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
245 changed files with 8733 additions and 5258 deletions

16
.github/workflows/backport.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: Backport
on:
pull_request:
types:
- closed
- labeled
jobs:
backport:
runs-on: ubuntu-18.04
name: Backport
steps:
- name: Backport
uses: tibdex/backport@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -77,5 +77,6 @@ install:
- bench --site test_site reinstall --yes
after_script:
- pip install coverage==4.5.4
- pip install python-coveralls
- coveralls -b apps/erpnext -d ../../sites/.coverage

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, json
from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form
from erpnext.accounts.report.general_ledger.general_ledger import execute
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
@ -30,8 +30,13 @@ def get(chart_name = None, chart = None, no_cache = None, from_date = None, to_d
account = filters.get("account")
company = filters.get("company")
if not account and chart:
frappe.throw(_("Account is not set for the dashboard chart {0}").format(chart))
if not account and chart_name:
frappe.throw(_("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:
frappe.throw(_("Account {0} does not exists in the dashboard chart {1}")
.format(account, get_link_to_form("Dashboard Chart", chart_name)))
if not to_date:
to_date = nowdate()

View File

@ -0,0 +1,526 @@
{
"country_code": "de",
"name": "SKR03 mit Kontonummern",
"tree": {
"Aktiva": {
"is_group": 1,
"root_type": "Asset",
"A - Anlagevermögen": {
"is_group": 1,
"EDV-Software": {
"account_number": "0027",
"account_type": "Fixed Asset"
},
"Gesch\u00e4ftsausstattung": {
"account_number": "0410",
"account_type": "Fixed Asset"
},
"B\u00fcroeinrichtung": {
"account_number": "0420",
"account_type": "Fixed Asset"
},
"Darlehen": {
"account_number": "0565"
},
"Maschinen": {
"account_number": "0210",
"account_type": "Fixed Asset"
},
"Betriebsausstattung": {
"account_number": "0400",
"account_type": "Fixed Asset"
},
"Ladeneinrichtung": {
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
}
},
"B - Umlaufvermögen": {
"is_group": 1,
"I. Vorräte": {
"is_group": 1,
"Roh-, Hilfs- und Betriebsstoffe (Bestand)": {
"account_number": "3970",
"account_type": "Stock"
},
"Waren (Bestand)": {
"account_number": "3980",
"account_type": "Stock"
}
},
"II. Forderungen und sonstige Vermögensgegenstände": {
"is_group": 1,
"Ford. a. Lieferungen und Leistungen": {
"account_number": "1400",
"account_type": "Receivable"
},
"Durchlaufende Posten": {
"account_number": "1590"
},
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": {
"account_number": "1371"
},
"Abziehbare VSt. 7%": {
"account_number": "1571"
},
"Abziehbare VSt. 19%": {
"account_number": "1576"
},
"Abziehbare VStr. nach \u00a713b UStG 19%": {
"account_number": "1577"
},
"Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
"account_number": "3120"
}
},
"III. Wertpapiere": {
"is_group": 1
},
"IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": {
"is_group": 1,
"Kasse": {
"account_type": "Cash",
"is_group": 1,
"Kasse": {
"is_group": 1,
"account_number": "1000",
"account_type": "Cash"
}
},
"Bank": {
"is_group": 1,
"account_type": "Bank",
"Postbank": {
"account_number": "1100",
"account_type": "Bank"
},
"Bankkonto": {
"account_number": "1200",
"account_type": "Bank"
}
}
}
},
"C - Rechnungsabgrenzungsposten": {
"is_group": 1,
"Aktive Rechnungsabgrenzung": {
"account_number": "0980"
}
},
"D - Aktive latente Steuern": {
"is_group": 1,
"Aktive latente Steuern": {
"account_number": "0983"
}
},
"E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": {
"is_group": 1
}
},
"Passiva": {
"is_group": 1,
"root_type": "Liability",
"A. Eigenkapital": {
"is_group": 1,
"I. Gezeichnetes Kapital": {
"is_group": 1
},
"II. Kapitalrücklage": {
"is_group": 1
},
"III. Gewinnrücklagen": {
"is_group": 1
},
"IV. Gewinnvortrag/Verlustvortrag": {
"is_group": 1
},
"V. Jahresüberschuß/Jahresfehlbetrag": {
"is_group": 1
}
},
"B. Rückstellungen": {
"is_group": 1,
"I. Rückstellungen für Pensionen und ähnliche Verpflichtungen": {
"is_group": 1
},
"II. Steuerrückstellungen": {
"is_group": 1
},
"III. sonstige Rückstellungen": {
"is_group": 1
}
},
"C. Verbindlichkeiten": {
"is_group": 1,
"I. Anleihen": {
"is_group": 1
},
"II. Verbindlichkeiten gegenüber Kreditinstituten": {
"is_group": 1
},
"III. Erhaltene Anzahlungen auf Bestellungen": {
"is_group": 1
},
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
"is_group": 1,
"Verbindlichkeiten aus Lieferungen u. Leistungen": {
"account_number": "1600",
"account_type": "Payable"
}
},
"V. Verbindlichkeiten aus der Annahme gezogener Wechsel und der Ausstellung eigener Wechsel": {
"is_group": 1
},
"VI. Verbindlichkeiten gegenüber verbundenen Unternehmen": {
"is_group": 1
},
"VII. Verbindlichkeiten gegenüber Unternehmen, mit denen ein Beteiligungsverhältnis besteht": {
"is_group": 1
},
"VIII. sonstige Verbindlichkeiten": {
"is_group": 1,
"Sonstige Verbindlichkeiten": {
"account_number": "1700",
"account_type": "Asset Received But Not Billed"
},
"Sonstige Verbindlichkeiten (1 bis 5 Jahre)": {
"account_number": "1702",
"account_type": "Stock Received But Not Billed"
},
"Verbindlichkeiten aus Lohn und Gehalt": {
"account_number": "1740",
"account_type": "Payable"
},
"Umsatzsteuer": {
"is_group": 1,
"Umsatzsteuer 7%": {
"account_number": "1771"
},
"Umsatzsteuer 19%": {
"account_number": "1776"
},
"Umsatzsteuer-Vorauszahlung": {
"account_number": "1780"
},
"Umsatzsteuer-Vorauszahlung 1/11": {
"account_number": "1781"
},
"Umsatzsteuer \u00a7 13b UStG 19%": {
"account_number": "1787"
},
"Umsatzsteuer Vorjahr": {
"account_number": "1790"
},
"Umsatzsteuer fr\u00fchere Jahre": {
"account_number": "1791"
}
}
}
},
"D. Rechnungsabgrenzungsposten": {
"is_group": 1,
"Passive Rechnungsabgrenzung": {
"account_number": "0990"
}
},
"E. Passive latente Steuern": {
"is_group": 1
}
},
"Erl\u00f6se u. Ertr\u00e4ge 2/8": {
"is_group": 1,
"root_type": "Income",
"Erl\u00f6skonten 8": {
"is_group": 1,
"Erl\u00f6se": {
"account_number": "8200",
"account_type": "Income Account"
},
"Erl\u00f6se USt. 19%": {
"account_number": "8400",
"account_type": "Income Account"
},
"Erl\u00f6se USt. 7%": {
"account_number": "8300",
"account_type": "Income Account"
}
},
"Ertragskonten 2": {
"is_group": 1,
"sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": {
"account_number": "2650",
"account_type": "Income Account"
},
"Au\u00dferordentliche Ertr\u00e4ge": {
"account_number": "2500",
"account_type": "Income Account"
},
"Sonstige Ertr\u00e4ge": {
"account_number": "2700",
"account_type": "Income Account"
}
}
},
"Aufwendungen 2/4": {
"is_group": 1,
"root_type": "Expense",
"Wareneingang": {
"account_number": "3200"
},
"Bezugsnebenkosten": {
"account_number": "3800",
"account_type": "Expenses Included In Asset Valuation"
},
"Herstellungskosten": {
"account_number": "4996",
"account_type": "Cost of Goods Sold"
},
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
"account_number": "2320",
"account_type": "Stock Adjustment"
},
"Verwaltungskosten": {
"account_number": "4997",
"account_type": "Expenses Included In Valuation"
},
"Vertriebskosten": {
"account_number": "4998",
"account_type": "Expenses Included In Valuation"
},
"Gegenkonto 4996-4998": {
"account_number": "4999"
},
"Abschreibungen": {
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Accumulated Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",
"account_type": "Depreciation"
},
"Abschreibungen auf Kfz": {
"account_number": "4832",
"account_type": "Depreciation"
},
"Sofortabschreibung GWG": {
"account_number": "4855",
"account_type": "Expense Account"
}
},
"Kfz-Kosten": {
"is_group": 1,
"Kfz-Steuer": {
"account_number": "4510",
"account_type": "Expense Account"
},
"Kfz-Versicherungen": {
"account_number": "4520",
"account_type": "Expense Account"
},
"laufende Kfz-Betriebskosten": {
"account_number": "4530",
"account_type": "Expense Account"
},
"Kfz-Reparaturen": {
"account_number": "4540",
"account_type": "Expense Account"
},
"Fremdfahrzeuge": {
"account_number": "4570",
"account_type": "Expense Account"
},
"sonstige Kfz-Kosten": {
"account_number": "4580",
"account_type": "Expense Account"
}
},
"Personalkosten": {
"is_group": 1,
"Geh\u00e4lter": {
"account_number": "4120",
"account_type": "Expense Account"
},
"gesetzliche soziale Aufwendungen": {
"account_number": "4130",
"account_type": "Expense Account"
},
"Aufwendungen f\u00fcr Altersvorsorge": {
"account_number": "4165",
"account_type": "Expense Account"
},
"Verm\u00f6genswirksame Leistungen": {
"account_number": "4170",
"account_type": "Expense Account"
},
"Aushilfsl\u00f6hne": {
"account_number": "4190",
"account_type": "Expense Account"
}
},
"Raumkosten": {
"is_group": 1,
"Miete und Nebenkosten": {
"account_number": "4210",
"account_type": "Expense Account"
},
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
"account_number": "4240",
"account_type": "Expense Account"
},
"Reinigung": {
"account_number": "4250",
"account_type": "Expense Account"
}
},
"Reparatur/Instandhaltung": {
"is_group": 1,
"Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": {
"account_number": "4805",
"account_type": "Expense Account"
}
},
"Versicherungsbeitr\u00e4ge": {
"is_group": 1,
"Versicherungen": {
"account_number": "4360",
"account_type": "Expense Account"
},
"Beitr\u00e4ge": {
"account_number": "4380",
"account_type": "Expense Account"
},
"sonstige Ausgaben": {
"account_number": "4390",
"account_type": "Expense Account"
},
"steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": {
"account_number": "4396",
"account_type": "Expense Account"
}
},
"Werbe-/Reisekosten": {
"is_group": 1,
"Werbekosten": {
"account_number": "4610",
"account_type": "Expense Account"
},
"Aufmerksamkeiten": {
"account_number": "4653",
"account_type": "Expense Account"
},
"nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": {
"account_number": "4665",
"account_type": "Expense Account"
},
"Reisekosten Unternehmer": {
"account_number": "4670",
"account_type": "Expense Account"
}
},
"verschiedene Kosten": {
"is_group": 1,
"Porto": {
"account_number": "4910",
"account_type": "Expense Account"
},
"Telekom": {
"account_number": "4920",
"account_type": "Expense Account"
},
"Mobilfunk D2": {
"account_number": "4921",
"account_type": "Expense Account"
},
"Internet": {
"account_number": "4922",
"account_type": "Expense Account"
},
"B\u00fcrobedarf": {
"account_number": "4930",
"account_type": "Expense Account"
},
"Zeitschriften, B\u00fccher": {
"account_number": "4940",
"account_type": "Expense Account"
},
"Fortbildungskosten": {
"account_number": "4945",
"account_type": "Expense Account"
},
"Buchf\u00fchrungskosten": {
"account_number": "4955",
"account_type": "Expense Account"
},
"Abschlu\u00df- u. Pr\u00fcfungskosten": {
"account_number": "4957",
"account_type": "Expense Account"
},
"Nebenkosten des Geldverkehrs": {
"account_number": "4970",
"account_type": "Expense Account"
},
"Werkzeuge und Kleinger\u00e4te": {
"account_number": "4985",
"account_type": "Expense Account"
}
},
"Zinsaufwendungen": {
"is_group": 1,
"Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": {
"account_number": "2110",
"account_type": "Expense Account"
},
"Zinsaufwendungen f\u00fcr KFZ Finanzierung": {
"account_number": "2121",
"account_type": "Expense Account"
}
}
},
"Anfangsbestand 9": {
"is_group": 1,
"root_type": "Equity",
"Saldenvortragskonten": {
"is_group": 1,
"Saldenvortrag Sachkonten": {
"account_number": "9000"
},
"Saldenvortr\u00e4ge Debitoren": {
"account_number": "9008"
},
"Saldenvortr\u00e4ge Kreditoren": {
"account_number": "9009"
}
}
},
"Privatkonten 1": {
"is_group": 1,
"root_type": "Equity",
"Privatentnahmen/-einlagen": {
"is_group": 1,
"Privatentnahme allgemein": {
"account_number": "1800"
},
"Privatsteuern": {
"account_number": "1810"
},
"Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": {
"account_number": "1820"
},
"Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": {
"account_number": "1830"
},
"Au\u00dfergew\u00f6hnliche Belastungen": {
"account_number": "1850"
},
"Privateinlagen": {
"account_number": "1890"
}
}
}
}
}

View File

@ -3,16 +3,16 @@
frappe.ui.form.on("Bank Reconciliation", {
setup: function(frm) {
frm.add_fetch("bank_account", "account_currency", "account_currency");
frm.add_fetch("account", "account_currency", "account_currency");
},
onload: function(frm) {
let default_bank_account = frappe.defaults.get_user_default("Company")?
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "";
frm.set_value("bank_account", default_bank_account);
frm.set_value("account", default_bank_account);
frm.set_query("bank_account", function() {
frm.set_query("account", function() {
return {
"filters": {
"account_type": ["in",["Bank","Cash"]],

View File

@ -19,10 +19,9 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Select account head of the bank where cheque was deposited.",
"fetch_from": "bank_account_no.account",
"fetch_from": "bank_account.account",
"fetch_if_empty": 1,
"fieldname": "bank_account",
"fieldname": "account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
@ -31,7 +30,7 @@
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Bank Account",
"label": "Account",
"length": 0,
"no_copy": 0,
"options": "Account",
@ -164,7 +163,6 @@
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
@ -183,8 +181,9 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Select the Bank Account to reconcile.",
"fetch_if_empty": 0,
"fieldname": "bank_account_no",
"fieldname": "bank_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
@ -193,12 +192,11 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Bank Account No",
"label": "Bank Account",
"length": 0,
"no_copy": 0,
"options": "Bank Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
@ -450,7 +448,7 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2019-04-09 18:41:06.110453",
"modified": "2020-01-22 00:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Reconciliation",
@ -483,4 +481,4 @@
"track_changes": 0,
"track_seen": 0,
"track_views": 0
}
}

View File

@ -21,10 +21,6 @@ class BankReconciliation(Document):
if not self.include_reconciled_entries:
condition = " and (clearance_date is null or clearance_date='0000-00-00')"
account_cond = ""
if self.bank_account_no:
account_cond = " and t2.bank_account_no = {0}".format(frappe.db.escape(self.bank_account_no))
journal_entries = frappe.db.sql("""
select
"Journal Entry" as payment_document, t1.name as payment_entry,
@ -34,15 +30,12 @@ class BankReconciliation(Document):
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
t2.parent = t1.name and t2.account = %s and t1.docstatus=1
and t1.posting_date >= %s and t1.posting_date <= %s
and ifnull(t1.is_opening, 'No') = 'No' {0} {1}
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
and ifnull(t1.is_opening, 'No') = 'No' %(condition)s
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
""".format(condition, account_cond), (self.bank_account, self.from_date, self.to_date), as_dict=1)
if self.bank_account_no:
condition = " and bank_account = %(bank_account_no)s"
""", {"condition":condition, "account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
payment_entries = frappe.db.sql("""
select
@ -55,12 +48,12 @@ class BankReconciliation(Document):
from `tabPayment Entry`
where
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s {0}
and posting_date >= %(from)s and posting_date <= %(to)s
and bank_account = %(bank_account)s
order by
posting_date ASC, name DESC
""".format(condition),
{"account":self.bank_account, "from":self.from_date,
"to":self.to_date, "bank_account_no": self.bank_account_no}, as_dict=1)
""", {"account": self.account, "from":self.from_date,
"to": self.to_date, "bank_account": self.bank_account}, as_dict=1)
pos_entries = []
if self.include_pos_transactions:
@ -72,11 +65,10 @@ class BankReconciliation(Document):
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
where
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s {0}
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by
si.posting_date ASC, si.name DESC
""".format(condition),
{"account":self.bank_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_entries)),
key=lambda k: k['posting_date'] or getdate(nowdate()))

View File

@ -110,6 +110,15 @@
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"depends_on": "eval:doc.docstatus==1",
"fieldname": "clearance_date",
"fieldtype": "Date",
"label": "Clearance Date",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"has_web_view": 0,
@ -122,7 +131,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-12-06 10:57:02.635141",
"modified": "2020-01-22 00:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction Payments",
@ -138,4 +147,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
}

View File

@ -9,7 +9,6 @@ from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.utils import get_balance_on, get_account_currency
from erpnext.accounts.party import get_party_account
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.hr.doctype.loan.loan import update_disbursement_status, update_total_amount_paid
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting
from six import string_types, iteritems
@ -50,7 +49,6 @@ class JournalEntry(AccountsController):
self.make_gl_entries()
self.update_advance_paid()
self.update_expense_claim()
self.update_loan()
self.update_inter_company_jv()
self.update_invoice_discounting()
@ -62,7 +60,6 @@ class JournalEntry(AccountsController):
self.make_gl_entries(1)
self.update_advance_paid()
self.update_expense_claim()
self.update_loan()
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
self.unlink_inter_company_jv()
@ -597,17 +594,6 @@ class JournalEntry(AccountsController):
doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc)
def update_loan(self):
if self.paid_loan:
paid_loan = json.loads(self.paid_loan)
value = 1 if self.docstatus < 2 else 0
for name in paid_loan:
frappe.db.set_value("Repayment Schedule", name, "paid", value)
for d in self.accounts:
if d.reference_type=="Loan" and flt(d.debit) > 0:
doc = frappe.get_doc("Loan", d.reference_name)
update_disbursement_status(doc)
update_total_amount_paid(doc)
def validate_expense_claim(self):
for d in self.accounts:

View File

@ -11,6 +11,7 @@ class ModeofPayment(Document):
def validate(self):
self.validate_accounts()
self.validate_repeating_companies()
self.validate_pos_mode_of_payment()
def validate_repeating_companies(self):
"""Error when Same Company is entered multiple times in accounts"""
@ -27,3 +28,15 @@ class ModeofPayment(Document):
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}")
.format(entry.default_account, entry.company, self.name))
def validate_pos_mode_of_payment(self):
if not self.enabled:
pos_profiles = frappe.db.sql("""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))
if pos_profiles:
message = "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")

View File

@ -253,6 +253,19 @@ frappe.ui.form.on('Payment Entry', {
frappe.throw(__("Party can only be one of "+ party_types.join(", ")));
}
frm.set_query("party", function() {
if(frm.doc.party_type == 'Employee'){
return {
query: "erpnext.controllers.queries.employee_query"
}
}
else if(frm.doc.party_type == 'Customer'){
return {
query: "erpnext.controllers.queries.customer_query"
}
}
});
if(frm.doc.party) {
$.each(["party", "party_balance", "paid_from", "paid_to",
"paid_from_account_currency", "paid_from_account_balance",

View File

@ -92,6 +92,7 @@ class PaymentReconciliation(Document):
FROM `tab{doc}`, `tabGL Entry`
WHERE
(`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no)
and `tab{doc}`.{party_type_field} = %(party)s
and `tab{doc}`.is_return = 1 and `tab{doc}`.return_against IS NULL
and `tabGL Entry`.against_voucher_type = %(voucher_type)s
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
@ -99,12 +100,17 @@ class PaymentReconciliation(Document):
GROUP BY `tab{doc}`.name
Having
amount > 0
""".format(doc=voucher_type, dr_or_cr=dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr), {
'party': self.party,
'party_type': self.party_type,
'voucher_type': voucher_type,
'account': self.receivable_payable_account
}, as_dict=1)
""".format(
doc=voucher_type,
dr_or_cr=dr_or_cr,
reconciled_dr_or_cr=reconciled_dr_or_cr,
party_type_field=frappe.scrub(self.party_type)),
{
'party': self.party,
'party_type': self.party_type,
'voucher_type': voucher_type,
'account': self.receivable_payable_account
}, as_dict=1)
def add_payment_entries(self, entries):
self.set('payments', [])

View File

@ -29,27 +29,29 @@ class TestPOSProfile(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def make_pos_profile():
def make_pos_profile(**args):
frappe.db.sql("delete from `tabPOS Profile`")
args = frappe._dict(args)
pos_profile = frappe.get_doc({
"company": "_Test Company",
"cost_center": "_Test Cost Center - _TC",
"currency": "INR",
"company": args.company or "_Test Company",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"currency": args.currency or "INR",
"doctype": "POS Profile",
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"income_account": "Sales - _TC",
"name": "_Test POS Profile",
"expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC",
"income_account": args.income_account or "Sales - _TC",
"name": args.name or "_Test POS Profile",
"naming_series": "_T-POS Profile-",
"selling_price_list": "_Test Price List",
"territory": "_Test Territory",
"selling_price_list": args.selling_price_list or "_Test Price List",
"territory": args.territory or "_Test Territory",
"customer_group": frappe.db.get_value('Customer Group', {'is_group': 0}, 'name'),
"warehouse": "_Test Warehouse - _TC",
"write_off_account": "_Test Write Off - _TC",
"write_off_cost_center": "_Test Write Off Cost Center - _TC"
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
})
if not frappe.db.exists("POS Profile", "_Test POS Profile"):
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
pos_profile.insert()
return pos_profile

View File

@ -248,7 +248,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if pricing_rule.price_or_product_discount == "Price":
apply_price_discount_rule(pricing_rule, item_details, args)
else:
get_product_discount_rule(pricing_rule, item_details, doc)
get_product_discount_rule(pricing_rule, item_details, args, doc)
item_details.has_pricing_rule = 1

View File

@ -326,6 +326,66 @@ class TestPricingRule(unittest.TestCase):
self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990)
def test_pricing_rule_for_product_discount_on_same_item(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [{
"item_code": "_Test Item",
}],
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"min_qty": 0,
"max_qty": 7,
"discount_percentage": 17.5,
"price_or_product_discount": "Product",
"same_item": 1,
"free_qty": 1,
"company": "_Test Company"
}
frappe.get_doc(test_record.copy()).insert()
# With pricing rule
so = make_sales_order(item_code="_Test Item", qty=1)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
def test_pricing_rule_for_product_discount_on_different_item(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [{
"item_code": "_Test Item",
}],
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"min_qty": 0,
"max_qty": 7,
"discount_percentage": 17.5,
"price_or_product_discount": "Product",
"same_item": 0,
"free_item": "_Test Item 2",
"free_qty": 1,
"company": "_Test Company"
}
frappe.get_doc(test_record.copy()).insert()
# With pricing rule
so = make_sales_order(item_code="_Test Item", qty=1)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item 2")
def make_pricing_rule(**args):
args = frappe._dict(args)

View File

@ -435,7 +435,7 @@ def apply_pricing_rule_on_transaction(doc):
doc.calculate_taxes_and_totals()
elif d.price_or_product_discount == 'Product':
item_details = frappe._dict({'parenttype': doc.doctype})
get_product_discount_rule(d, item_details, doc)
get_product_discount_rule(d, item_details, doc=doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values()
@ -443,9 +443,10 @@ def get_applied_pricing_rules(item_row):
return (item_row.get("pricing_rules").split(',')
if item_row.get("pricing_rules") else [])
def get_product_discount_rule(pricing_rule, item_details, doc=None):
free_item = (pricing_rule.free_item
if not pricing_rule.same_item or pricing_rule.apply_on == 'Transaction' else item_details.item_code)
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item
if pricing_rule.same_item:
free_item = item_details.item_code or args.item_code
if not free_item:
frappe.throw(_("Free item not set in the pricing rule {0}")

View File

@ -149,6 +149,7 @@
"column_break_63",
"status",
"inter_company_invoice_reference",
"is_internal_supplier",
"remarks",
"subscription_section",
"from_date",
@ -418,7 +419,6 @@
"fieldname": "contact_email",
"fieldtype": "Small Text",
"label": "Contact Email",
"options": "Email",
"print_hide": 1,
"read_only": 1
},
@ -1284,6 +1284,14 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"default": "0",
"fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
"label": "Is Internal Supplier",
"read_only": 1
}
],
"icon": "fa fa-file-text",

View File

@ -224,7 +224,7 @@ class PurchaseInvoice(BuyingController):
for item in self.get("items"):
# in case of auto inventory accounting,
# expense account is always "Stock Received But Not Billed" for a stock item
# except epening entry, drop-ship entry and fixed asset items
# except opening entry, drop-ship entry and fixed asset items
if item.item_code:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
@ -233,10 +233,22 @@ class PurchaseInvoice(BuyingController):
and (not item.po_detail or
not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")):
if self.update_stock:
if self.update_stock and (not item.from_warehouse):
item.expense_account = warehouse_account[item.warehouse]["account"]
else:
item.expense_account = stock_not_billed_account
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
if item.purchase_receipt:
negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no=%s and account = %s""",
(item.purchase_receipt, stock_not_billed_account))
if negative_expense_booked_in_pr:
item.expense_account = stock_not_billed_account
else:
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
# This is done in cases when Purchase Invoice is created before Purchase Receipt
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
company = self.company)
@ -467,16 +479,47 @@ class PurchaseInvoice(BuyingController):
warehouse_debit_amount = self.make_stock_adjustment_entry(gl_entries,
item, voucher_wise_stock_value, account_currency)
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.supplier,
"debit": warehouse_debit_amount,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
if item.from_warehouse:
gl_entries.append(self.get_gl_dict({
"account": warehouse_account[item.warehouse]['account'],
"against": warehouse_account[item.from_warehouse]["account"],
"cost_center": item.cost_center,
"project": item.project
}, account_currency, item=item)
)
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": warehouse_debit_amount,
}, warehouse_account[item.warehouse]["account_currency"], item=item))
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
gl_entries.append(self.get_gl_dict({
"account": warehouse_account[item.from_warehouse]['account'],
"against": warehouse_account[item.warehouse]["account"],
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
}, warehouse_account[item.from_warehouse]["account_currency"], item=item))
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.supplier,
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project
}, account_currency, item=item)
)
else:
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.supplier,
"debit": warehouse_debit_amount,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project
}, account_currency, item=item)
)
# Amount added through landed-cost-voucher
if landed_cost_entries:
@ -1015,7 +1058,7 @@ def unblock_invoice(name):
@frappe.whitelist()
def block_invoice(name, hold_comment, release_date):
def block_invoice(name, release_date, hold_comment=None):
if frappe.db.exists('Purchase Invoice', name):
pi = frappe.get_doc('Purchase Invoice', name)
pi.block_invoice(hold_comment, release_date)

View File

@ -63,6 +63,7 @@
"warehouse_section",
"warehouse",
"rejected_warehouse",
"from_warehouse",
"quality_inspection",
"batch_no",
"col_br_wh",
@ -762,16 +763,22 @@
"fetch_from": "item_code.asset_category",
"fieldname": "asset_category",
"fieldtype": "Data",
"in_preview": 1,
"label": "Asset Category",
"options": "Asset Category",
"read_only": 1
},
{
"fieldname": "from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Supplier Warehouse",
"options": "Warehouse"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2019-12-04 12:23:17.046413",
"modified": "2020-01-13 16:04:14.200462",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@ -152,8 +152,11 @@ def update_multi_mode_option(doc, pos_profile):
def get_mode_of_payment(doc):
return frappe.db.sql(""" select mpa.default_account, mpa.parent, mp.type as type from `tabMode of Payment Account` mpa, \
`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %(company)s""", {'company': doc.company}, as_dict=1)
return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
{'company': doc.company}, as_dict=1)
def update_tax_table(doc):

View File

@ -153,6 +153,7 @@
"select_print_heading",
"more_information",
"inter_company_invoice_reference",
"is_internal_customer",
"customer_group",
"campaign",
"is_discounted",
@ -1563,6 +1564,14 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"default": "0",
"fetch_from": "customer.is_internal_customer",
"fieldname": "is_internal_customer",
"fieldtype": "Check",
"label": "Is Internal Customer",
"read_only": 1
}
],
"icon": "fa fa-file-text",

View File

@ -420,7 +420,7 @@ class SalesInvoice(SellingController):
if pos:
self.allow_print_before_pay = pos.allow_print_before_pay
if not for_validate:
self.tax_category = pos.get("tax_category")
@ -1243,25 +1243,28 @@ class SalesInvoice(SellingController):
precision = self.precision("outstanding_amount")
outstanding_amount = flt(self.outstanding_amount, precision)
due_date = getdate(self.due_date)
nowdate = getdate()
discountng_status = self.get_discounting_status()
if not status:
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
if outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed':
if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
self.status = "Overdue and Discounted"
elif outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()):
elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed':
elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discountng_status=='Disbursed':
self.status = "Unpaid and Discounted"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()):
elif outstanding_amount > 0 and due_date >= nowdate:
self.status = "Unpaid"
#Check if outstanding amount is 0 due to credit note issued against invoice
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
self.status = "Credit Note Issued"
elif self.is_return == 1:
self.status = "Return"
elif outstanding_amount <=0:
elif outstanding_amount<=0:
self.status = "Paid"
else:
self.status = "Submitted"
@ -1426,23 +1429,42 @@ def set_account_for_mode_of_payment(self):
data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")
def get_inter_company_details(doc, doctype):
if doctype in ["Sales Invoice", "Sales Order"]:
party = frappe.db.get_value("Supplier", {"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}, "name")
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company})
company = frappe.get_cached_value("Customer", doc.customer, "represents_company")
party = get_internal_party(parties, "Supplier", doc)
else:
party = frappe.db.get_value("Customer", {"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}, "name")
parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company})
company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company")
party = get_internal_party(parties, "Customer", doc)
return {
"party": party,
"company": company
}
def get_internal_party(parties, link_doctype, doc):
if len(parties) == 1:
party = parties[0].name
else:
# If more than one Internal Supplier/Customer, get supplier/customer on basis of address
if doc.get('company_address') or doc.get('shipping_address'):
party = frappe.db.get_value("Dynamic Link", {"parent": doc.get('company_address') or doc.get('shipping_address'),
"parenttype": "Address", "link_doctype": link_doctype}, "link_name")
if not party:
party = parties[0].name
else:
party = parties[0].name
return party
def validate_inter_company_transaction(doc, doctype):
details = get_inter_company_details(doc, doctype)
price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order"] else doc.buying_price_list
price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list
valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1})
if not valid_price_list:
frappe.throw(_("Selected Price List should have buying and selling fields checked."))
@ -1526,6 +1548,9 @@ def get_loyalty_programs(customer):
else:
return lp_details
def on_doctype_update():
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
@frappe.whitelist()
def create_invoice_discounting(source_name, target_doc=None):
invoice = frappe.get_doc("Sales Invoice", source_name)

View File

@ -705,6 +705,64 @@ class TestSalesInvoice(unittest.TestCase):
self.pos_gl_entry(si, pos, 50)
def test_pos_returns_without_repayment(self):
pos_profile = make_pos_profile()
pos = create_sales_invoice(qty = 10, do_not_save=True)
pos.is_pos = 1
pos.pos_profile = pos_profile.name
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
pos.insert()
pos.submit()
pos_return = create_sales_invoice(is_return=1,
return_against=pos.name, qty=-5, do_not_save=True)
pos_return.is_pos = 1
pos_return.pos_profile = pos_profile.name
pos_return.insert()
pos_return.submit()
self.assertFalse(pos_return.is_pos)
self.assertFalse(pos_return.get('payments'))
def test_pos_returns_with_repayment(self):
pos_profile = make_pos_profile()
pos_profile.append('payments', {
'default': 1,
'mode_of_payment': 'Cash',
'amount': 0.0
})
pos_profile.save()
pos = create_sales_invoice(qty = 10, do_not_save=True)
pos.is_pos = 1
pos.pos_profile = pos_profile.name
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
pos.insert()
pos.submit()
pos_return = create_sales_invoice(is_return=1,
return_against=pos.name, qty=-5, do_not_save=True)
pos_return.is_pos = 1
pos_return.pos_profile = pos_profile.name
pos_return.insert()
pos_return.submit()
self.assertEqual(pos_return.get('payments')[0].amount, -500)
pos_profile.payments = []
pos_profile.save()
def test_pos_change_amount(self):
make_pos_profile()

View File

@ -35,8 +35,7 @@ def get_party_details(party=None, account=None, party_type="Customer", company=N
def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None,
bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False,
fetch_payment_terms_template=True, party_address=None, company_address=None,shipping_address=None, pos_profile=None):
fetch_payment_terms_template=True, party_address=None, company_address=None, shipping_address=None, pos_profile=None):
party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype))
party = party_details[party_type.lower()]

View File

@ -100,6 +100,11 @@ frappe.query_reports["Accounts Payable"] = {
"fieldtype": "Link",
"options": "Supplier Group"
},
{
"fieldname": "group_by_party",
"label": __("Group By Supplier"),
"fieldtype": "Check"
},
{
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
@ -112,6 +117,16 @@ frappe.query_reports["Accounts Payable"] = {
"hidden": 1
}
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (data && data.bold) {
value = value.bold();
}
return value;
},
onload: function(report) {
report.page.add_inner_button(__("Accounts Payable Summary"), function() {
var filters = report.get_values();

View File

@ -87,7 +87,7 @@ frappe.query_reports["Accounts Receivable"] = {
frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]);
});
frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
["credit_limit"], function(value) {
if (value) {
frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]);
@ -131,6 +131,11 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldtype": "Link",
"options": "Sales Person"
},
{
"fieldname": "group_by_party",
"label": __("Group By Customer"),
"fieldtype": "Check"
},
{
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
@ -177,6 +182,15 @@ frappe.query_reports["Accounts Receivable"] = {
}
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (data && data.bold) {
value = value.bold();
}
return value;
},
onload: function(report) {
report.page.add_inner_button(__("Accounts Receivable Summary"), function() {
var filters = report.get_values();

View File

@ -46,7 +46,7 @@ class ReceivablePayableReport(object):
self.get_columns()
self.get_data()
self.get_chart_data()
return self.columns, self.data, None, self.chart
return self.columns, self.data, None, self.chart, None, self.skip_total_row
def set_defaults(self):
if not self.filters.get("company"):
@ -57,6 +57,12 @@ class ReceivablePayableReport(object):
self.party_type = self.filters.party_type
self.party_details = {}
self.invoices = set()
self.skip_total_row = 0
if self.filters.get('group_by_party'):
self.previous_party=''
self.total_row_map = {}
self.skip_total_row = 1
def get_data(self):
self.get_gl_entries()
@ -102,6 +108,12 @@ class ReceivablePayableReport(object):
)
self.get_invoices(gle)
if self.filters.get('group_by_party'):
self.init_subtotal_row(gle.party)
if self.filters.get('group_by_party'):
self.init_subtotal_row('Total')
def get_invoices(self, gle):
if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'):
if self.filters.get("sales_person"):
@ -111,6 +123,20 @@ class ReceivablePayableReport(object):
else:
self.invoices.add(gle.voucher_no)
def init_subtotal_row(self, party):
if not self.total_row_map.get(party):
self.total_row_map.setdefault(party, {
'party': party,
'bold': 1
})
for field in self.get_currency_fields():
self.total_row_map[party][field] = 0.0
def get_currency_fields(self):
return ['invoiced', 'paid', 'credit_note', 'outstanding', 'range1',
'range2', 'range3', 'range4', 'range5']
def update_voucher_balance(self, gle):
# get the row where this balance needs to be updated
# if its a payment, it will return the linked invoice or will be considered as advance
@ -135,6 +161,18 @@ class ReceivablePayableReport(object):
# advance / unlinked payment or other adjustment
row.paid -= gle_balance
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
for field in self.get_currency_fields():
total_row[field] += row.get(field, 0.0)
def append_subtotal_row(self, party):
sub_total_row = self.total_row_map.get(party)
self.data.append(sub_total_row)
self.data.append({})
self.update_sub_total_row(sub_total_row, 'Total')
def get_voucher_balance(self, gle):
if self.filters.get("sales_person"):
against_voucher = gle.against_voucher or gle.voucher_no
@ -192,11 +230,22 @@ class ReceivablePayableReport(object):
else:
self.append_row(row)
if self.filters.get('group_by_party'):
self.append_subtotal_row(self.previous_party)
self.data.append(self.total_row_map.get('Total'))
def append_row(self, row):
self.allocate_future_payments(row)
self.set_invoice_details(row)
self.set_party_details(row)
self.set_ageing(row)
if self.filters.get('group_by_party'):
self.update_sub_total_row(row, row.party)
if self.previous_party and (self.previous_party != row.party):
self.append_subtotal_row(self.previous_party)
self.previous_party = row.party
self.data.append(row)
def set_invoice_details(self, row):
@ -503,6 +552,7 @@ class ReceivablePayableReport(object):
# get all the GL entries filtered by the given filters
conditions, values = self.prepare_conditions()
order_by = self.get_order_by_condition()
if self.filters.get(scrub(self.party_type)):
select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit"
@ -520,9 +570,8 @@ class ReceivablePayableReport(object):
and party_type=%s
and (party is not null and party != '')
and posting_date <= %s
{1}
order by posting_date, party"""
.format(select_fields, conditions), values, as_dict=True)
{1} {2}"""
.format(select_fields, conditions, order_by), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@ -557,6 +606,12 @@ class ReceivablePayableReport(object):
return " and ".join(conditions), values
def get_order_by_condition(self):
if self.filters.get('group_by_party'):
return "order by party, posting_date"
else:
return "order by posting_date, party"
def add_common_filters(self, conditions, values, party_type_field):
if self.filters.company:
conditions.append("company=%s")
@ -736,11 +791,13 @@ class ReceivablePayableReport(object):
def get_chart_data(self):
rows = []
for row in self.data:
values = [row.range1, row.range2, row.range3, row.range4, row.range5]
precision = cint(frappe.db.get_default("float_precision")) or 2
rows.append({
'values': [flt(val, precision) for val in values]
})
row = frappe._dict(row)
if not cint(row.bold):
values = [row.range1, row.range2, row.range3, row.range4, row.range5]
precision = cint(frappe.db.get_default("float_precision")) or 2
rows.append({
'values': [flt(val, precision) for val in values]
})
self.chart = {
"data": {

View File

@ -180,10 +180,20 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
get_items_from_open_material_requests: function() {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order_based_on_supplier",
args: {
supplier: this.frm.doc.supplier
},
source_doctype: "Material Request",
source_name: this.frm.doc.supplier,
target: this.frm,
setters: {
company: me.frm.doc.company
},
get_query_filters: {
docstatus: ["!=", 2],
}
supplier: this.frm.doc.supplier
},
get_query_method: "erpnext.stock.doctype.material_request.material_request.get_material_requests_based_on_supplier"
});
},

View File

@ -12,8 +12,8 @@
"supplier",
"get_items_from_open_material_requests",
"supplier_name",
"company",
"column_break1",
"company",
"transaction_date",
"schedule_date",
"order_confirmation_no",
@ -170,6 +170,7 @@
"search_index": 1
},
{
"description": "Fetch items based on Default Supplier.",
"depends_on": "eval:doc.supplier && doc.docstatus===0 && (!(doc.items && doc.items.length) || (doc.items.length==1 && !doc.items[0].item_code))",
"fieldname": "get_items_from_open_material_requests",
"fieldtype": "Button",

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,9 @@ class Supplier(TransactionBase):
frappe.db.set(self, "supplier_name", newdn)
def create_onboarding_docs(self, args):
defaults = frappe.defaults.get_defaults()
company = frappe.defaults.get_defaults().get('company') or \
frappe.db.get_single_value('Global Defaults', 'default_company')
for i in range(1, args.get('max_count')):
supplier = args.get('supplier_name_' + str(i))
if supplier:
@ -67,7 +69,7 @@ class Supplier(TransactionBase):
'doctype': self.doctype,
'supplier_name': supplier,
'supplier_group': _('Local'),
'company': defaults.get('company')
'company': company
}).insert()
if args.get('supplier_email_' + str(i)):

View File

@ -9,7 +9,8 @@ def get_data():
'heatmap_message': _('This is based on transactions against this Supplier. See timeline below for details'),
'fieldname': 'supplier',
'non_standard_fieldnames': {
'Payment Entry': 'party_name'
'Payment Entry': 'party_name',
'Bank Account': 'party'
},
'transactions': [
{
@ -24,6 +25,10 @@ def get_data():
'label': _('Payments'),
'items': ['Payment Entry']
},
{
'label': _('Bank'),
'items': ['Bank Account']
},
{
'label': _('Pricing'),
'items': ['Pricing Rule']

View File

@ -80,6 +80,15 @@ def get_data():
"type": "module",
"description": "Sales pipeline, leads, opportunities and customers."
},
{
"module_name": "Loan Management",
"category": "Modules",
"label": _("Loan Management"),
"color": "#EF4DB6",
"icon": "octicon octicon-repo",
"type": "module",
"description": "Loan Management for Customer and Employees"
},
{
"module_name": "Support",
"category": "Modules",

View File

@ -0,0 +1,107 @@
from __future__ import unicode_literals
from frappe import _
import frappe
def get_data():
return [
{
"label": _("Loan"),
"items": [
{
"type": "doctype",
"name": "Loan Type",
"description": _("Loan Type for interest and penalty rates"),
},
{
"type": "doctype",
"name": "Loan Application",
"description": _("Loan Applications from customers and employees."),
},
{
"type": "doctype",
"name": "Loan",
"description": _("Loans provided to customers and employees."),
},
]
},
{
"label": _("Loan Security"),
"items": [
{
"type": "doctype",
"name": "Loan Security Type",
},
{
"type": "doctype",
"name": "Loan Security Price",
},
{
"type": "doctype",
"name": "Loan Security",
},
{
"type": "doctype",
"name": "Loan Security Pledge",
},
{
"type": "doctype",
"name": "Loan Security Unpledge",
},
{
"type": "doctype",
"name": "Loan Security Shortfall",
},
]
},
{
"label": _("Disbursement and Repayment"),
"items": [
{
"type": "doctype",
"name": "Loan Disbursement",
},
{
"type": "doctype",
"name": "Loan Repayment",
},
{
"type": "doctype",
"name": "Loan Interest Accrual"
}
]
},
{
"label": _("Loan Processes"),
"items": [
{
"type": "doctype",
"name": "Process Loan Security Shortfall",
},
{
"type": "doctype",
"name": "Process Loan Interest Accrual",
}
]
},
{
"label": _("Reports"),
"items": [
{
"type": "report",
"is_query_report": True,
"name": "Loan Repayment and Closure",
"route": "#query-report/Loan Repayment and Closure",
"doctype": "Loan Repayment",
},
{
"type": "report",
"is_query_report": True,
"name": "Loan Security Status",
"route": "#query-report/Loan Security Status",
"doctype": "Loan Security Pledge",
}
]
}
]

View File

@ -43,6 +43,7 @@ class BuyingController(StockController):
self.set_qty_as_per_stock_uom()
self.validate_stock_or_nonstock_items()
self.validate_warehouse()
self.validate_from_warehouse()
self.set_supplier_address()
if self.doctype=="Purchase Invoice":
@ -115,6 +116,14 @@ class BuyingController(StockController):
if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]:
d.db_set('cost_center', lc_voucher_data[0][1])
def validate_from_warehouse(self):
for item in self.get('items'):
if item.get('from_warehouse') and (item.get('from_warehouse') == item.get('warehouse')):
frappe.throw(_("Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same").format(item.idx))
if item.get('from_warehouse') and self.get('is_subcontracted') == 'Yes':
frappe.throw(_("Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor").format(item.idx))
def set_supplier_address(self):
address_dict = {
'supplier_address': 'address_display',
@ -521,6 +530,16 @@ class BuyingController(StockController):
pr_qty = flt(d.qty) * flt(d.conversion_factor)
if pr_qty:
if d.from_warehouse and ((not cint(self.is_return) and self.docstatus==1)
or (cint(self.is_return) and self.docstatus==2)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse
})
sl_entries.append(from_warehouse_sle)
sle = self.get_sl_entries(d, {
"actual_qty": flt(pr_qty),
"serial_no": cstr(d.serial_no).strip()
@ -541,6 +560,15 @@ class BuyingController(StockController):
})
sl_entries.append(sle)
if d.from_warehouse and ((not cint(self.is_return) and self.docstatus==2)
or (cint(self.is_return) and self.docstatus==1)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse
})
sl_entries.append(from_warehouse_sle)
if flt(d.rejected_qty) != 0:
sl_entries.append(self.get_sl_entries(d, {
"warehouse": d.rejected_warehouse,

View File

@ -180,7 +180,7 @@ class SellingController(StockController):
last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"])
last_purchase_rate_in_sales_uom = last_purchase_rate / (it.conversion_factor or 1)
if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom):
if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom) and not self.get('is_internal_customer'):
throw_message(it.item_name, last_purchase_rate_in_sales_uom, "last purchase rate")
last_valuation_rate = frappe.db.sql("""
@ -190,7 +190,8 @@ class SellingController(StockController):
""", (it.item_code, it.warehouse))
if last_valuation_rate:
last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] / (it.conversion_factor or 1)
if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom):
if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom) \
and not self.get('is_internal_customer'):
throw_message(it.name, last_valuation_rate_in_sales_uom, "valuation rate")
@ -300,7 +301,7 @@ class SellingController(StockController):
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
return_rate = 0
if cint(self.is_return) and self.return_against and self.docstatus==1:
return_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against)
return_rate = self.get_incoming_rate_for_return(d.item_code, self.return_against)
# On cancellation or if return entry submission, make stock ledger entry for
# target warehouse first, to update serial no values properly

View File

@ -72,7 +72,7 @@ class StockController(AccountsController):
if sle_list:
for sle in sle_list:
if warehouse_account.get(sle.warehouse):
# from warehouse account
# from warehouse account/ target warehouse account
self.check_expense_account(item_row)
@ -96,7 +96,7 @@ class StockController(AccountsController):
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
}, warehouse_account[sle.warehouse]["account_currency"], item=item_row))
# to target warehouse / expense account
# expense account
gl_list.append(self.get_gl_dict({
"account": item_row.expense_account,
"against": warehouse_account[sle.warehouse]["account"],
@ -288,7 +288,7 @@ class StockController(AccountsController):
return serialized_items
def get_incoming_rate_for_sales_return(self, item_code, against_document):
def get_incoming_rate_for_return(self, item_code, against_document):
incoming_rate = 0.0
if against_document and item_code:
incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty)
@ -306,6 +306,16 @@ class StockController(AccountsController):
warehouses = list(set([d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)]))
target_warehouses = list(set([d.target_warehouse for d in
self.get("items") if getattr(d, "target_warehouse", None)]))
warehouses.extend(target_warehouses)
from_warehouse = list(set([d.from_warehouse for d in
self.get("items") if getattr(d, "from_warehouse", None)]))
warehouses.extend(from_warehouse)
for w in warehouses:
validate_warehouse_company(w, self.company)

View File

@ -514,7 +514,7 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype == "Sales Invoice":
self.calculate_paid_amount()
if self.doc.is_return and self.doc.return_against: return
if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
self._set_in_company_currency(self.doc, ['write_off_amount'])
@ -532,7 +532,7 @@ class calculate_taxes_and_totals(object):
self.doc.round_floats_in(self.doc, ["paid_amount"])
change_amount = 0
if self.doc.doctype == "Sales Invoice":
if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'):
self.calculate_write_off_amount()
self.calculate_change_amount()
change_amount = self.doc.change_amount \
@ -544,6 +544,9 @@ class calculate_taxes_and_totals(object):
self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount),
self.doc.precision("outstanding_amount"))
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
self.update_paid_amount_for_return(total_amount_to_pay)
def calculate_paid_amount(self):
paid_amount = base_paid_amount = 0.0
@ -614,6 +617,27 @@ class calculate_taxes_and_totals(object):
def set_item_wise_tax_breakup(self):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
def update_paid_amount_for_return(self, total_amount_to_pay):
default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment',
{'parent': self.doc.pos_profile, 'default': 1},
['mode_of_payment', 'type', 'account'], as_dict=1)
self.doc.payments = []
if default_mode_of_payment:
self.doc.append('payments', {
'mode_of_payment': default_mode_of_payment.mode_of_payment,
'type': default_mode_of_payment.type,
'account': default_mode_of_payment.account,
'amount': total_amount_to_pay
})
else:
self.doc.is_pos = 0
self.doc.pos_profile = ''
self.calculate_paid_amount()
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:
return

View File

@ -59,6 +59,7 @@ frappe.ui.form.on("Opportunity", {
contact_person: erpnext.utils.get_contact_details,
opportunity_from: function(frm) {
frm.trigger('setup_queries');
frm.toggle_reqd("party_name", frm.doc.opportunity_from);
frm.trigger("set_dynamic_field_label");
},

View File

@ -6,21 +6,21 @@ frappe.ui.form.on("Course", "refresh", function(frm) {
}
frappe.set_route("List", "Program");
});
frm.add_custom_button(__("Student Group"), function() {
frappe.route_options = {
course: frm.doc.name
}
frappe.set_route("List", "Student Group");
});
frm.add_custom_button(__("Course Schedule"), function() {
frappe.route_options = {
course: frm.doc.name
}
frappe.set_route("List", "Course Schedule");
});
frm.add_custom_button(__("Assessment Plan"), function() {
frappe.route_options = {
course: frm.doc.name
@ -36,4 +36,17 @@ frappe.ui.form.on("Course", "refresh", function(frm) {
}
}
});
});
});
frappe.ui.form.on('Course Topic', {
topics_add: function(frm){
frm.fields_dict['topics'].grid.get_field('topic').get_query = function(doc){
var topics_list = [];
if(!doc.__islocal) topics_list.push(doc.name);
$.each(doc.topics, function(idx, val){
if (val.topic) topics_list.push(val.topic);
});
return { filters: [['Topic', 'name', 'not in', topics_list]] };
};
}
});

View File

@ -12,7 +12,6 @@ frappe.ui.form.on("Instructor", {
}
};
});
frm.set_query("department", "instructor_log", function() {
return {
"filters": {
@ -49,5 +48,12 @@ frappe.ui.form.on("Instructor", {
frappe.set_route("List", "Assessment Plan");
}, __("Assessment Plan"));
}
frm.set_query("employee", function(doc) {
return {
"filters": {
"department": doc.department,
}
};
});
}
});
});

View File

@ -81,3 +81,16 @@ frappe.ui.form.on("Program Enrollment", {
})
}
});
frappe.ui.form.on('Program Enrollment Course', {
courses_add: function(frm){
frm.fields_dict['courses'].grid.get_field('course').get_query = function(doc){
var course_list = [];
if(!doc.__islocal) course_list.push(doc.name);
$.each(doc.courses, function(idx, val){
if (val.course) course_list.push(val.course);
});
return { filters: [['Course', 'name', 'not in', course_list]] };
};
}
});

View File

@ -4,5 +4,18 @@
frappe.ui.form.on('Quiz', {
refresh: function(frm) {
},
validate: function(frm){
frm.events.check_duplicate_question(frm.doc.question);
},
check_duplicate_question: function(questions_data){
var questions = [];
questions_data.forEach(function(q){
questions.push(q.question_link);
});
var questions_set = new Set(questions);
if (questions.length != questions_set.size) {
frappe.throw(__("The question cannot be duplicate"));
}
}
});
});

View File

@ -22,6 +22,10 @@ class Student(Document):
self.update_student_name_in_linked_doctype()
def validate_dates(self):
for sibling in self.siblings:
if sibling.date_of_birth and getdate(sibling.date_of_birth) > getdate():
frappe.throw(_("Row {0}:Sibling Date of Birth cannot be greater than today.").format(sibling.idx))
if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()):
frappe.throw(_("Date of Birth cannot be greater than today."))

View File

@ -29,10 +29,15 @@ class StudentApplicant(Document):
set_name_by_naming_series(self)
def validate(self):
self.validate_dates()
self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
if self.student_admission and self.program and self.date_of_birth:
self.validation_from_student_admission()
def validate_dates(self):
if self.date_of_birth and getdate(self.date_of_birth) >= getdate():
frappe.throw(_("Date of Birth cannot be greater than today."))
def on_update_after_submit(self):
student = frappe.get_list("Student", filters= {"student_applicant": self.name})
if student:

View File

@ -70,6 +70,16 @@ frappe.ui.form.on("Student Group", {
group_based_on: function(frm) {
if (frm.doc.group_based_on == "Batch") {
frm.doc.course = null;
frm.set_df_property('program', 'reqd', 1);
frm.set_df_property('course', 'reqd', 0);
}
else if (frm.doc.group_based_on == "Course") {
frm.set_df_property('program', 'reqd', 0);
frm.set_df_property('course', 'reqd', 1);
}
else if (frm.doc.group_based_on == "Activity") {
frm.set_df_property('program', 'reqd', 0);
frm.set_df_property('course', 'reqd', 0);
}
},

View File

@ -26,6 +26,8 @@ class StudentGroup(Document):
frappe.throw(_("Please select Program"))
def validate_strength(self):
if cint(self.max_strength) < 0:
frappe.throw(_("""Max strength cannot be less than zero."""))
if self.max_strength and len(self.students) > self.max_strength:
frappe.throw(_("""Cannot enroll more than {0} students for this student group.""").format(self.max_strength))

View File

@ -5,26 +5,30 @@
"program": "_TP1",
"batch": "_Batch 1",
"academic_year": "2014-2015",
"academic_term": "2014-2015 (_Test Academic Term)"
"academic_term": "2014-2015 (_Test Academic Term)",
"max_strength": 0
},
{
"student_group_name": "Course-TC101-2014-2015 (_Test Academic Term)",
"group_based_on": "Course",
"course": "TC101",
"academic_year": "2014-2015",
"academic_term": "2014-2015 (_Test Academic Term)"
"academic_term": "2014-2015 (_Test Academic Term)",
"max_strength": 0
},
{
"student_group_name": "Course-TC102-2014-2015 (_Test Academic Term)",
"group_based_on": "Course",
"course": "TC102",
"academic_year": "2014-2015",
"academic_term": "2014-2015 (_Test Academic Term)"
"academic_term": "2014-2015 (_Test Academic Term)",
"max_strength": 0
},
{
"student_group_name": "Activity-2014-2015 (_Test Academic Term)",
"group_based_on": "Activity",
"academic_year": "2014-2015",
"academic_term": "2014-2015 (_Test Academic Term)"
"academic_term": "2014-2015 (_Test Academic Term)",
"max_strength": 0
}
]

View File

@ -7,9 +7,11 @@ import frappe
from frappe import _
from frappe.utils import get_link_to_form
from frappe.model.document import Document
from frappe import throw, _
class StudentLeaveApplication(Document):
def validate(self):
self.validate_dates()
self.validate_duplicate()
def validate_duplicate(self):
@ -29,4 +31,8 @@ class StudentLeaveApplication(Document):
if data:
link = get_link_to_form("Student Leave Application", data[0].name)
frappe.throw(_("Leave application {0} already exists against the student {1}")
.format(link, self.student))
.format(link, self.student))
def validate_dates(self):
if self.to_date < self.from_date :
throw(_("To Date cannot be less than From Date"))

View File

@ -47,11 +47,12 @@ def _order(*args, **kwargs):
return "success"
if event == "created":
sys_lang = frappe.get_single("System Settings").language or 'en'
raw_billing_data = order.get("billing")
customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name")
link_customer_and_address(raw_billing_data, customer_name)
link_items(order.get("line_items"), woocommerce_settings)
create_sales_order(order, woocommerce_settings, customer_name)
link_items(order.get("line_items"), woocommerce_settings, sys_lang)
create_sales_order(order, woocommerce_settings, customer_name, sys_lang)
def link_customer_and_address(raw_billing_data, customer_name):
customer_woo_com_email = raw_billing_data.get("email")
@ -100,7 +101,7 @@ def link_customer_and_address(raw_billing_data, customer_name):
frappe.rename_doc("Address", old_address_title, new_address_title)
def link_items(items_list, woocommerce_settings):
def link_items(items_list, woocommerce_settings, sys_lang):
for item_data in items_list:
item_woo_com_id = item_data.get("product_id")
@ -112,14 +113,14 @@ def link_items(items_list, woocommerce_settings):
item = frappe.new_doc("Item")
item.item_name = item_data.get("name")
item.item_code = _("woocommerce - {0}").format(item_data.get("product_id"))
item.item_code = _("woocommerce - {0}", sys_lang).format(item_data.get("product_id"))
item.woocommerce_id = item_data.get("product_id")
item.item_group = _("WooCommerce Products")
item.stock_uom = woocommerce_settings.uom or _("Nos")
item.item_group = _("WooCommerce Products", sys_lang)
item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang)
item.flags.ignore_mandatory = True
item.save()
def create_sales_order(order, woocommerce_settings, customer_name):
def create_sales_order(order, woocommerce_settings, customer_name, sys_lang):
new_sales_order = frappe.new_doc("Sales Order")
new_sales_order.customer = customer_name
@ -133,14 +134,14 @@ def create_sales_order(order, woocommerce_settings, customer_name):
new_sales_order.company = woocommerce_settings.company
set_items_in_sales_order(new_sales_order, woocommerce_settings, order)
set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang)
new_sales_order.flags.ignore_mandatory = True
new_sales_order.insert()
new_sales_order.submit()
frappe.db.commit()
def set_items_in_sales_order(new_sales_order, woocommerce_settings, order):
def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang):
company_abbr = frappe.db.get_value('Company', woocommerce_settings.company, 'abbr')
for item in order.get("line_items"):
@ -154,10 +155,10 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order):
"item_name": found_item.item_name,
"description": found_item.item_name,
"delivery_date": new_sales_order.delivery_date,
"uom": woocommerce_settings.uom or _("Nos"),
"uom": woocommerce_settings.uom or _("Nos", sys_lang),
"qty": item.get("quantity"),
"rate": item.get("price"),
"warehouse": woocommerce_settings.warehouse or _("Stores - {0}").format(company_abbr)
"warehouse": woocommerce_settings.warehouse or _("Stores - {0}", sys_lang).format(company_abbr)
})
add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account)

View File

@ -165,6 +165,9 @@ def create_item_code(amazon_item_json, sku):
return item.name
def create_manufacturer(amazon_item_json):
if not amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer:
return None
existing_manufacturer = frappe.db.get_value("Manufacturer",
filters={"short_name":amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer})
@ -177,6 +180,9 @@ def create_manufacturer(amazon_item_json):
return existing_manufacturer
def create_brand(amazon_item_json):
if not amazon_item_json.Product.AttributeSets.ItemAttributes.Brand:
return None
existing_brand = frappe.db.get_value("Brand",
filters={"brand":amazon_item_json.Product.AttributeSets.ItemAttributes.Brand})
if not existing_brand:

View File

@ -7,7 +7,6 @@ import frappe
from frappe.model.document import Document
import dateutil
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods import get_products_details, get_orders
class AmazonMWSSettings(Document):
def validate(self):
@ -19,12 +18,12 @@ class AmazonMWSSettings(Document):
def get_products_details(self):
if self.enable_amazon == 1:
get_products_details()
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details')
def get_order_details(self):
if self.enable_amazon == 1:
after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d")
get_orders(after_date = after_date)
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_orders', after_date=after_date)
def schedule_get_order_details():
mws_settings = frappe.get_doc("Amazon MWS Settings")

View File

@ -33,7 +33,10 @@ class object_dict(dict):
def __getattr__(self, item):
d = self.__getitem__(item)
try:
d = self.__getitem__(item)
except KeyError:
return None
if isinstance(d, dict) and 'value' in d and len(d) == 1:
return d['value']

View File

@ -49,9 +49,10 @@
},
{
"fieldname": "plaid_env",
"fieldtype": "Data",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Plaid Environment"
"label": "Plaid Environment",
"options": "sandbox\ndevelopment\nproduction"
},
{
"fieldname": "column_break_2",
@ -69,7 +70,7 @@
],
"issingle": 1,
"links": [],
"modified": "2020-01-05 10:00:22.137832",
"modified": "2020-02-07 15:21:11.616231",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Plaid Settings",

View File

@ -10,6 +10,7 @@ from frappe.model.document import Document
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
from frappe.utils import getdate, formatdate, today, add_months
from frappe.desk.doctype.tag.tag import add_tag
class PlaidSettings(Document):
pass
@ -133,10 +134,13 @@ def sync_transactions(bank, bank_account):
try:
transactions = get_transactions(bank=bank, bank_account=bank_account, start_date=start_date, end_date=end_date)
result = []
if transactions:
for transaction in transactions:
result.append(new_bank_transaction(transaction))
for transaction in reversed(transactions):
result += new_bank_transaction(transaction)
frappe.logger().info("Plaid added {} new Bank Transactions from '{}' between {} and {}".format(
len(result), bank_account, start_date, end_date))
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", getdate(end_date))
@ -175,6 +179,13 @@ def new_bank_transaction(transaction):
status = "Pending" if transaction["pending"] == "True" else "Settled"
try:
tags = []
tags += transaction["category"]
tags += ["Plaid Cat. {}".format(transaction["category_id"])]
except KeyError:
pass
if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])):
try:
new_transaction = frappe.get_doc({
@ -185,11 +196,16 @@ def new_bank_transaction(transaction):
"debit": debit,
"credit": credit,
"currency": transaction["iso_currency_code"],
"transaction_id": transaction["transaction_id"],
"reference_number": transaction["payment_meta"]["reference_number"],
"description": transaction["name"]
})
new_transaction.insert()
new_transaction.submit()
for tag in tags:
add_tag(tag, "Bank Transaction", new_transaction.name)
result.append(new_transaction.name)
except Exception:
@ -201,7 +217,7 @@ def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
if settings.enabled == 1 and settings.automatic_sync == 1:
plaid_accounts = frappe.get_all("Bank Account", filter={"integration_id": ["!=", ""]}, fields=["name", "bank"])
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
for plaid_account in plaid_accounts:
frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name)

View File

@ -314,12 +314,15 @@ scheduler_events = {
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
"erpnext.hr.utils.generate_leave_encashment"
"erpnext.hr.utils.generate_leave_encashment",
"erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.check_for_ltv_shortfall",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.make_accrual_interest_entry_for_term_loans"
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income",
"erpnext.accounts.deferred_revenue.convert_deferred_expense_to_expense",
"erpnext.hr.utils.allocate_earned_leaves"
"erpnext.hr.utils.allocate_earned_leaves",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual"
]
}

View File

@ -8,7 +8,8 @@ frappe.ui.form.on('Additional Salary', {
frm.set_query("employee", function() {
return {
filters: {
company: frm.doc.company
company: frm.doc.company,
status: "Active"
}
};
});

View File

@ -58,11 +58,12 @@
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Read Only",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Employee Name",
"oldfieldname": "employee_name",
"oldfieldtype": "Data"
"oldfieldtype": "Data",
"read_only": 1
},
{
"depends_on": "working_hours",
@ -174,8 +175,7 @@
"icon": "fa fa-ok",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-01-27 20:25:29.572281",
"modified": "2020-02-19 14:25:32.945842",
"modified_by": "Administrator",
"module": "HR",
"name": "Attendance",

View File

@ -2,6 +2,11 @@
// For license information, please see license.txt
frappe.ui.form.on('Department', {
onload: function(frm) {
frm.set_query("parent_department", function(){
return {"filters": [["Department", "is_group", "=", 1]]};
});
},
refresh: function(frm) {
// read-only for root department
if(!frm.doc.parent_department && !frm.is_new()) {

View File

@ -409,7 +409,7 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
def test_earned_leave(self):
def test_earned_leaves_creation(self):
leave_period = get_leave_period()
employee = get_employee()
leave_type = 'Test Earned Leave Type'
@ -437,6 +437,14 @@ class TestLeaveApplication(unittest.TestCase):
i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
# validate earned leaves creation without maximum leaves
frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0)
i = 0
while(i<6):
allocate_earned_leaves()
i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
# test to not consider current leave in leave balance while submitting
def test_current_leave_on_submit(self):
employee = get_employee()

View File

@ -1,229 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/hr/loan_common.js' %};
frappe.ui.form.on('Loan', {
onload: function (frm) {
frm.set_query("loan_application", function () {
return {
"filters": {
"applicant": frm.doc.applicant,
"docstatus": 1,
"status": "Approved"
}
};
});
frm.set_query("interest_income_account", function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Income",
"is_group": 0
}
};
});
$.each(["payment_account", "loan_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Asset",
"is_group": 0
}
};
});
})
},
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
if (frm.doc.status == "Sanctioned") {
frm.add_custom_button(__('Create Disbursement Entry'), function() {
frm.trigger("make_jv");
}).addClass("btn-primary");
} else if (frm.doc.status == "Disbursed" && frm.doc.repayment_start_date && (frm.doc.applicant_type == 'Member' || frm.doc.repay_from_salary == 0)) {
frm.add_custom_button(__('Create Repayment Entry'), function() {
frm.trigger("make_repayment_entry");
}).addClass("btn-primary");
}
}
frm.trigger("toggle_fields");
},
make_jv: function (frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"company": frm.doc.company,
"loan_account": frm.doc.loan_account,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"loan_amount": frm.doc.loan_amount,
"payment_account": frm.doc.payment_account
},
method: "erpnext.hr.doctype.loan.loan.make_jv_entry",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
make_repayment_entry: function(frm) {
var repayment_schedule = $.map(frm.doc.repayment_schedule, function(d) { return d.paid ? d.payment_date : false; });
if(repayment_schedule.length >= 1){
frm.repayment_data = [];
frm.show_dialog = 1;
let title = "";
let fields = [
{fieldtype:'Section Break', label: __('Repayment Schedule')},
{fieldname: 'payments', fieldtype: 'Table',
fields: [
{
fieldtype:'Data',
fieldname:'payment_date',
label: __('Date'),
read_only:1,
in_list_view: 1,
columns: 2
},
{
fieldtype:'Currency',
fieldname:'principal_amount',
label: __('Principal Amount'),
read_only:1,
in_list_view: 1,
columns: 3
},
{
fieldtype:'Currency',
fieldname:'interest_amount',
label: __('Interest'),
read_only:1,
in_list_view: 1,
columns: 2
},
{
fieldtype:'Currency',
read_only:1,
fieldname:'total_payment',
label: __('Total Payment'),
in_list_view: 1,
columns: 3
},
],
data: frm.repayment_data,
get_data: function() {
return frm.repayment_data;
}
}
]
var dialog = new frappe.ui.Dialog({
title: title, fields: fields,
});
if (frm.doc['repayment_schedule']) {
frm.doc['repayment_schedule'].forEach((payment, index) => {
if (payment.paid == 0 && payment.payment_date <= frappe.datetime.now_date()) {
frm.repayment_data.push ({
'id': index,
'name': payment.name,
'payment_date': payment.payment_date,
'principal_amount': payment.principal_amount,
'interest_amount': payment.interest_amount,
'total_payment': payment.total_payment
});
dialog.fields_dict.payments.grid.refresh();
$(dialog.wrapper.find(".grid-buttons")).hide();
$(`.octicon.octicon-triangle-down`).hide();
}
})
}
dialog.show()
dialog.set_primary_action(__('Create Repayment Entry'), function() {
frm.values = dialog.get_values();
if(frm.values) {
_make_repayment_entry(frm, dialog.fields_dict.payments.grid.get_selected_children());
dialog.hide()
}
});
}
dialog.get_close_btn().on('click', () => {
dialog.hide();
});
},
mode_of_payment: function (frm) {
if (frm.doc.mode_of_payment && frm.doc.company) {
frappe.call({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_bank_cash_account",
args: {
"mode_of_payment": frm.doc.mode_of_payment,
"company": frm.doc.company
},
callback: function (r, rt) {
if (r.message) {
frm.set_value("payment_account", r.message.account);
}
}
});
}
},
loan_application: function (frm) {
if(frm.doc.loan_application){
return frappe.call({
method: "erpnext.hr.doctype.loan.loan.get_loan_application",
args: {
"loan_application": frm.doc.loan_application
},
callback: function (r) {
if (!r.exc && r.message) {
frm.set_value("loan_type", r.message.loan_type);
frm.set_value("loan_amount", r.message.loan_amount);
frm.set_value("repayment_method", r.message.repayment_method);
frm.set_value("monthly_repayment_amount", r.message.repayment_amount);
frm.set_value("repayment_periods", r.message.repayment_periods);
frm.set_value("rate_of_interest", r.message.rate_of_interest);
}
}
});
}
},
repayment_method: function (frm) {
frm.trigger("toggle_fields")
},
toggle_fields: function (frm) {
frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods")
}
});
var _make_repayment_entry = function(frm, payment_rows) {
frappe.call({
method:"erpnext.hr.doctype.loan.loan.make_repayment_entry",
args: {
payment_rows: payment_rows,
"loan": frm.doc.name,
"company": frm.doc.company,
"loan_account": frm.doc.loan_account,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"payment_account": frm.doc.payment_account,
"interest_income_account": frm.doc.interest_income_account
},
callback: function(r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name, {'payment_rows': payment_rows});
}
});
}

View File

@ -1,240 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, math, json
import erpnext
from frappe import _
from frappe.utils import flt, rounded, add_months, nowdate, getdate
from erpnext.controllers.accounts_controller import AccountsController
class Loan(AccountsController):
def validate(self):
validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, self.repayment_periods)
self.set_missing_fields()
self.make_repayment_schedule()
self.set_repayment_period()
self.calculate_totals()
def set_missing_fields(self):
if not self.company:
self.company = erpnext.get_default_company()
if not self.posting_date:
self.posting_date = nowdate()
if self.loan_type and not self.rate_of_interest:
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
if self.repayment_method == "Repay Over Number of Periods":
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.status == "Repaid/Closed":
self.total_amount_paid = self.total_payment
def make_jv_entry(self):
self.check_permission('write')
journal_entry = frappe.new_doc('Journal Entry')
journal_entry.voucher_type = 'Bank Entry'
journal_entry.user_remark = _('Against Loan: {0}').format(self.name)
journal_entry.company = self.company
journal_entry.posting_date = nowdate()
account_amt_list = []
account_amt_list.append({
"account": self.loan_account,
"party_type": self.applicant_type,
"party": self.applicant,
"debit_in_account_currency": self.loan_amount,
"reference_type": "Loan",
"reference_name": self.name,
})
account_amt_list.append({
"account": self.payment_account,
"credit_in_account_currency": self.loan_amount,
"reference_type": "Loan",
"reference_name": self.name,
})
journal_entry.set("accounts", account_amt_list)
return journal_entry.as_dict()
def make_repayment_schedule(self):
self.repayment_schedule = []
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
total_payment = principal_amount + interest_amount
self.append("repayment_schedule", {
"payment_date": payment_date,
"principal_amount": principal_amount,
"interest_amount": interest_amount,
"total_payment": total_payment,
"balance_loan_amount": balance_amount
})
next_payment_date = add_months(payment_date, 1)
payment_date = next_payment_date
def set_repayment_period(self):
if self.repayment_method == "Repay Fixed Amount per Period":
repayment_periods = len(self.repayment_schedule)
self.repayment_periods = repayment_periods
def calculate_totals(self):
self.total_payment = 0
self.total_interest_payable = 0
self.total_amount_paid = 0
for data in self.repayment_schedule:
self.total_payment += data.total_payment
self.total_interest_payable +=data.interest_amount
if data.paid:
self.total_amount_paid += data.total_payment
def update_total_amount_paid(doc):
total_amount_paid = 0
for data in doc.repayment_schedule:
if data.paid:
total_amount_paid += data.total_payment
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def update_disbursement_status(doc):
disbursement = frappe.db.sql("""
select posting_date, ifnull(sum(credit_in_account_currency), 0) as disbursed_amount
from `tabGL Entry`
where account = %s and against_voucher_type = 'Loan' and against_voucher = %s
""", (doc.payment_account, doc.name), as_dict=1)[0]
disbursement_date = None
if not disbursement or disbursement.disbursed_amount == 0:
status = "Sanctioned"
elif disbursement.disbursed_amount == doc.loan_amount:
disbursement_date = disbursement.posting_date
status = "Disbursed"
elif disbursement.disbursed_amount > doc.loan_amount:
frappe.throw(_("Disbursed Amount cannot be greater than Loan Amount {0}").format(doc.loan_amount))
if status == 'Disbursed' and getdate(disbursement_date) > getdate(frappe.db.get_value("Loan", doc.name, "repayment_start_date")):
frappe.throw(_("Disbursement Date cannot be after Loan Repayment Start Date"))
frappe.db.sql("""
update `tabLoan`
set status = %s, disbursement_date = %s
where name = %s
""", (status, disbursement_date, doc.name))
def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods):
if repayment_method == "Repay Over Number of Periods" and not repayment_periods:
frappe.throw(_("Please enter Repayment Periods"))
if repayment_method == "Repay Fixed Amount per Period":
if not monthly_repayment_amount:
frappe.throw(_("Please enter repayment Amount"))
if monthly_repayment_amount > loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))
def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods):
if rate_of_interest:
monthly_interest_rate = flt(rate_of_interest) / (12 *100)
monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *
(1 + monthly_interest_rate)**repayment_periods) \
/ ((1 + monthly_interest_rate)**repayment_periods - 1))
else:
monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
return monthly_repayment_amount
@frappe.whitelist()
def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application)
if loan:
return loan.as_dict()
@frappe.whitelist()
def make_repayment_entry(payment_rows, loan, company, loan_account, applicant_type, applicant, \
payment_account=None, interest_income_account=None):
if isinstance(payment_rows, frappe.string_types):
payment_rows_list = json.loads(payment_rows)
else:
frappe.throw(_("No repayments available for Journal Entry"))
if payment_rows_list:
row_name = list(set(d["name"] for d in payment_rows_list))
else:
frappe.throw(_("No repayments selected for Journal Entry"))
total_payment = 0
principal_amount = 0
interest_amount = 0
for d in payment_rows_list:
total_payment += d["total_payment"]
principal_amount += d["principal_amount"]
interest_amount += d["interest_amount"]
journal_entry = frappe.new_doc('Journal Entry')
journal_entry.voucher_type = 'Bank Entry'
journal_entry.user_remark = _('Against Loan: {0}').format(loan)
journal_entry.company = company
journal_entry.posting_date = nowdate()
journal_entry.paid_loan = json.dumps(row_name)
account_amt_list = []
account_amt_list.append({
"account": payment_account,
"debit_in_account_currency": total_payment,
"reference_type": "Loan",
"reference_name": loan,
})
account_amt_list.append({
"account": loan_account,
"credit_in_account_currency": principal_amount,
"party_type": applicant_type,
"party": applicant,
"reference_type": "Loan",
"reference_name": loan,
})
account_amt_list.append({
"account": interest_income_account,
"credit_in_account_currency": interest_amount,
"reference_type": "Loan",
"reference_name": loan,
})
journal_entry.set("accounts", account_amt_list)
return journal_entry.as_dict()
@frappe.whitelist()
def make_jv_entry(loan, company, loan_account, applicant_type, applicant, loan_amount,payment_account=None):
journal_entry = frappe.new_doc('Journal Entry')
journal_entry.voucher_type = 'Bank Entry'
journal_entry.user_remark = _('Against Loan: {0}').format(loan)
journal_entry.company = company
journal_entry.posting_date = nowdate()
account_amt_list = []
account_amt_list.append({
"account": loan_account,
"debit_in_account_currency": loan_amount,
"party_type": applicant_type,
"party": applicant,
"reference_type": "Loan",
"reference_name": loan,
})
account_amt_list.append({
"account": payment_account,
"credit_in_account_currency": loan_amount,
"reference_type": "Loan",
"reference_name": loan,
})
journal_entry.set("accounts", account_amt_list)
return journal_entry.as_dict()

View File

@ -1,26 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'applicant',
'non_standard_fieldnames': {
'Journal Entry': 'reference_name',
'Salary Slip': 'employee'
},
'transactions': [
{
'label': _('Applicant'),
'items': ['Loan Application']
},
{
'label': _('Account'),
'items': ['Journal Entry']
},
{
'label': _('Employee'),
'items': ['Salary Slip']
}
]
}

View File

@ -1,79 +0,0 @@
QUnit.test("Test Loan [HR]", function(assert) {
assert.expect(8);
let done = assert.async();
let employee_name;
// To create a loan and check principal,interest and balance amount
let loan_creation = (ename,lname) => {
return frappe.run_serially([
() => frappe.db.get_value('Employee', {'employee_name': ename}, 'name'),
(r) => {
employee_name = r.message.name;
},
() => frappe.db.get_value('Loan Application', {'loan_type': lname}, 'name'),
(r) => {
// Creating loan for an employee
return frappe.tests.make('Loan', [
{ company: 'For Testing'},
{ posting_date: '2017-08-26'},
{ applicant: employee_name},
{ loan_application: r.message.name},
{ disbursement_date: '2018-08-26'},
{ mode_of_payment: 'Cash'},
{ loan_account: 'Temporary Opening - FT'},
{ interest_income_account: 'Service - FT'}
]);
},
() => frappe.timeout(3),
() => frappe.click_button('Submit'),
() => frappe.timeout(1),
() => frappe.click_button('Yes'),
() => frappe.timeout(3),
// Checking if all the amounts are correctly calculated
() => {
assert.ok(cur_frm.get_field('applicant_name').value=='Test Employee 1'&&
(cur_frm.get_field('status').value=='Sanctioned'),
'Loan Sanctioned for correct employee');
assert.equal(7270,
cur_frm.get_doc('repayment_schedule').repayment_schedule[0].principal_amount,
'Principal amount for first instalment is correctly calculated');
assert.equal(2333,
cur_frm.get_doc('repayment_schedule').repayment_schedule[0].interest_amount,
'Interest amount for first instalment is correctly calculated');
assert.equal(192730,
cur_frm.get_doc('repayment_schedule').repayment_schedule[0].balance_loan_amount,
'Balance amount after first instalment is correctly calculated');
assert.equal(9479,
cur_frm.get_doc('repayment_schedule').repayment_schedule[23].principal_amount,
'Principal amount for last instalment is correctly calculated');
assert.equal(111,
cur_frm.get_doc('repayment_schedule').repayment_schedule[23].interest_amount,
'Interest amount for last instalment is correctly calculated');
assert.equal(0,
cur_frm.get_doc('repayment_schedule').repayment_schedule[23].balance_loan_amount,
'Balance amount after last instalment is correctly calculated');
},
() => frappe.set_route('List','Loan','List'),
() => frappe.timeout(2),
// Checking the submission of Loan
() => {
assert.ok(cur_list.data[0].docstatus==1,'Loan sanctioned and submitted successfully');
},
]);
};
frappe.run_serially([
// Creating loan
() => loan_creation('Test Employee 1','Test Loan'),
() => done()
]);
});

View File

@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import erpnext
import unittest
from frappe.utils import nowdate, add_days
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee
class TestLoan(unittest.TestCase):
def setUp(self):
create_loan_type("Personal Loan", 500000, 8.4)
self.applicant = make_employee("robert_loan@loan.com")
create_loan(self.applicant, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
def test_loan(self):
loan = frappe.get_doc("Loan", {"applicant":self.applicant})
self.assertEquals(loan.monthly_repayment_amount, 15052)
self.assertEquals(loan.total_interest_payable, 21034)
self.assertEquals(loan.total_payment, 301034)
schedule = loan.repayment_schedule
self.assertEqual(len(schedule), 20)
for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]:
self.assertEqual(schedule[idx].principal_amount, principal_amount)
self.assertEqual(schedule[idx].interest_amount, interest_amount)
self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount)
loan.repayment_method = "Repay Fixed Amount per Period"
loan.monthly_repayment_amount = 14000
loan.save()
self.assertEquals(len(loan.repayment_schedule), 22)
self.assertEquals(loan.total_interest_payable, 22712)
self.assertEquals(loan.total_payment, 302712)
def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest):
if not frappe.db.exists("Loan Type", loan_name):
frappe.get_doc({
"doctype": "Loan Type",
"loan_name": loan_name,
"maximum_loan_amount": maximum_loan_amount,
"rate_of_interest": rate_of_interest
}).insert()
def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods):
create_loan_type(loan_type, 500000, 8.4)
if not frappe.db.get_value("Loan", {"applicant":applicant}):
loan = frappe.new_doc("Loan")
loan.update({
"applicant": applicant,
"loan_type": loan_type,
"loan_amount": loan_amount,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods,
"disbursement_date": nowdate(),
"repayment_start_date": nowdate(),
"status": "Disbursed",
"mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'),
"payment_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"),
"loan_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"),
"interest_income_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name")
})
loan.insert()
return loan
else:
return frappe.get_doc("Loan", {"applicant":applicant})

View File

@ -1,42 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/hr/loan_common.js' %};
frappe.ui.form.on('Loan Application', {
refresh: function(frm) {
frm.trigger("toggle_fields")
frm.trigger("add_toolbar_buttons")
},
repayment_method: function(frm) {
frm.doc.repayment_amount = frm.doc.repayment_periods = ""
frm.trigger("toggle_fields")
frm.trigger("toggle_required")
},
toggle_fields: function(frm) {
frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods")
},
toggle_required: function(frm){
frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period'))
frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods'))
},
add_toolbar_buttons: function(frm) {
if (frm.doc.status == "Approved") {
frm.add_custom_button(__('Create Loan'), function() {
frappe.call({
method: "erpnext.hr.doctype.loan_application.loan_application.make_loan",
args: {
"source_name": frm.doc.name
},
callback: function(r) {
if(!r.exc) {
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
});
}).addClass("btn-primary");
}
}
});

View File

@ -1,840 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "ACC-LOAP-.YYYY.-.#####",
"beta": 0,
"creation": "2016-12-02 12:35:56.046811",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "applicant_type",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Applicant Type",
"length": 0,
"no_copy": 0,
"options": "Employee\nMember",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Applicant",
"length": 0,
"no_copy": 0,
"options": "applicant_type",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "applicant",
"fieldname": "applicant_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Applicant Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Posting Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Status",
"length": 0,
"no_copy": 1,
"options": "Open\nApproved\nRejected",
"permlevel": 1,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Loan Info",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Loan Type",
"length": 0,
"no_copy": 0,
"options": "Loan Type",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Loan Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "required_by_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Required by Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reason",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "repayment_info",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Repayment Info",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "repayment_method",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Repayment Method",
"length": 0,
"no_copy": 0,
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "loan_type.rate_of_interest",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rate of Interest",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_payable_interest",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Payable Interest",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "repayment_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Monthly Repayment Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Repayment Period in Months",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_payable_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Payable Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Loan Application",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:53.688596",
"modified_by": "Administrator",
"module": "HR",
"name": "Loan Application",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "applicant_type, applicant, loan_type, loan_amount",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "applicant",
"title_field": "applicant",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View File

@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, math
from frappe import _
from frappe.utils import flt, rounded
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from erpnext.hr.doctype.loan.loan import get_monthly_repayment_amount, validate_repayment_method
class LoanApplication(Document):
def validate(self):
validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, self.repayment_periods)
self.validate_loan_amount()
self.get_repayment_details()
def validate_loan_amount(self):
maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount')
if maximum_loan_limit and self.loan_amount > maximum_loan_limit:
frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit))
def get_repayment_details(self):
if self.repayment_method == "Repay Over Number of Periods":
self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
if monthly_interest_rate:
min_repayment_amount = self.loan_amount*monthly_interest_rate
if (self.repayment_amount - min_repayment_amount) <= 0:
frappe.throw(_("Repayment Amount must be greater than " \
+ str(flt(min_repayment_amount, 2))))
self.repayment_periods = math.ceil((math.log(self.repayment_amount) -
math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate)))
else:
self.repayment_periods = self.loan_amount / self.repayment_amount
self.calculate_payable_amount()
def calculate_payable_amount(self):
balance_amount = self.loan_amount
self.total_payable_amount = 0
self.total_payable_interest = 0
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
self.total_payable_interest += interest_amount
self.total_payable_amount = self.loan_amount + self.total_payable_interest
@frappe.whitelist()
def make_loan(source_name, target_doc = None):
doclist = get_mapped_doc("Loan Application", source_name, {
"Loan Application": {
"doctype": "Loan",
"field_map": {
"repayment_amount": "monthly_repayment_amount"
},
"validation": {
"docstatus": ["=", 1]
}
}
}, target_doc)
return doclist

View File

@ -1,68 +0,0 @@
QUnit.module('hr');
QUnit.test("Test: Loan Application [HR]", function (assert) {
assert.expect(8);
let done = assert.async();
let employee_name;
frappe.run_serially([
// Creation of Loan Application
() => frappe.db.get_value('Employee', {'employee_name': 'Test Employee 1'}, 'name'),
(r) => {
employee_name = r.message.name;
},
() => {
return frappe.tests.make('Loan Application', [
{ company: 'For Testing'},
{ applicant: employee_name},
{ applicant_name: 'Test Employee 1'},
{ status: 'Approved'},
{ loan_type: 'Test Loan '},
{ loan_amount: 200000},
{ description: 'This is just a test'},
{ repayment_method: 'Repay Over Number of Periods'},
{ repayment_periods: 24},
{ rate_of_interest: 14}
]);
},
() => frappe.timeout(6),
() => frappe.click_button('Submit'),
() => frappe.timeout(1),
() => frappe.click_button('Yes'),
() => frappe.timeout(2),
() => {
// To check if all the amounts are correctly calculated
assert.ok(cur_frm.get_field('applicant_name').value == 'Test Employee 1',
'Application created successfully');
assert.ok(cur_frm.get_field('status').value=='Approved',
'Status of application is correctly set');
assert.ok(cur_frm.get_field('loan_type').value=='Test Loan',
'Application is created for correct Loan Type');
assert.ok(cur_frm.get_field('status').value=='Approved',
'Status of application is correctly set');
assert.ok(cur_frm.get_field('repayment_amount').value==9603,
'Repayment amount is correctly calculated');
assert.ok(cur_frm.get_field('total_payable_interest').value==30459,
'Interest amount is correctly calculated');
assert.ok(cur_frm.get_field('total_payable_amount').value==230459,
'Total payable amount is correctly calculated');
},
() => frappe.set_route('List','Loan Application','List'),
() => frappe.timeout(2),
// Checking the submission of Loan Application
() => {
assert.ok(cur_list.data[0].docstatus==1,'Loan Application submitted successfully');
},
() => frappe.timeout(1),
() => done()
]);
});

View File

@ -1,7 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Type', {
refresh: function(frm) {
}
});

View File

@ -1,259 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:loan_name",
"beta": 0,
"creation": "2016-12-02 10:41:40.732843",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Loan Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Maximum Loan Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rate of Interest (%) Yearly",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "disabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-03-29 21:23:08.665245",
"modified_by": "Administrator",
"module": "HR",
"name": "Loan Type",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Employee",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
}

View File

@ -1,12 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'loan_type',
'transactions': [
{
'items': ['Loan Application']
},
],
}

View File

@ -1,31 +0,0 @@
QUnit.module('hr');
QUnit.test("Test: Loan Type [HR]", function (assert) {
assert.expect(3);
let done = assert.async();
frappe.run_serially([
// Loan Type creation
() => {
frappe.tests.make('Loan Type', [
{ loan_name: 'Test Loan'},
{ maximum_loan_amount: 400000},
{ rate_of_interest: 14},
{ description:
'This is just a test.'}
]);
},
() => frappe.timeout(7),
() => frappe.set_route('List','Loan Type','List'),
() => frappe.timeout(4),
// Checking if the fields are correctly set
() => {
assert.ok(cur_list.data.length==1, 'Loan Type created successfully');
assert.ok(cur_list.data[0].name=='Test Loan', 'Loan title Correctly set');
assert.ok(cur_list.data[0].disabled==0, 'Loan enabled');
},
() => done()
]);
});

View File

@ -157,19 +157,6 @@ class PayrollEntry(Document):
for ss in submitted_ss:
ss.email_salary_slip()
def get_loan_details(self):
"""
Get loan details from submitted salary slip based on selected criteria
"""
cond = self.get_filter_condition()
return frappe.db.sql(""" select eld.loan_account, eld.loan,
eld.interest_income_account, eld.principal_amount, eld.interest_amount, eld.total_payment,t1.employee
from
`tabSalary Slip` t1, `tabSalary Slip Loan` eld
where
t1.docstatus = 1 and t1.name = eld.parent and start_date >= %s and end_date <= %s %s
""" % ('%s', '%s', cond), (self.start_date, self.end_date), as_dict=True) or []
def get_salary_component_account(self, salary_component):
account = frappe.db.get_value("Salary Component Account",
{"parent": salary_component, "company": self.company}, "default_account")
@ -225,7 +212,6 @@ class PayrollEntry(Document):
earnings = self.get_salary_component_total(component_type = "earnings") or {}
deductions = self.get_salary_component_total(component_type = "deductions") or {}
default_payroll_payable_account = self.get_default_payroll_payable_account()
loan_details = self.get_loan_details()
jv_name = ""
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
@ -262,29 +248,6 @@ class PayrollEntry(Document):
"project": self.project
})
# Loan
for data in loan_details:
accounts.append({
"account": data.loan_account,
"credit_in_account_currency": data.principal_amount,
"party_type": "Employee",
"party": data.employee
})
if data.interest_amount and not data.interest_income_account:
frappe.throw(_("Select interest income account in loan {0}").format(data.loan))
if data.interest_income_account and data.interest_amount:
accounts.append({
"account": data.interest_income_account,
"credit_in_account_currency": data.interest_amount,
"cost_center": self.cost_center,
"project": self.project,
"party_type": "Employee",
"party": data.employee
})
payable_amount -= flt(data.total_payment, precision)
# Payable amount
accounts.append({
"account": default_payroll_payable_account,

View File

@ -6,20 +6,22 @@ import erpnext
import frappe
from dateutil.relativedelta import relativedelta
from erpnext.accounts.utils import get_fiscal_year, getdate, nowdate
from frappe.utils import add_months
from erpnext.hr.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.salary_slip.test_salary_slip import get_salary_component_account, \
make_earning_salary_component, make_deduction_salary_component
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.hr.doctype.loan.test_loan import create_loan
from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans
class TestPayrollEntry(unittest.TestCase):
def setUp(self):
for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry", "Loan"]:
for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry"]:
frappe.db.sql("delete from `tab%s`" % dt)
make_earning_salary_component(setup=True)
make_deduction_salary_component(setup=True)
make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0)
@ -49,8 +51,8 @@ class TestPayrollEntry(unittest.TestCase):
def test_loan(self):
branch = "Test Employee Branch"
applicant = make_employee("test_employee@loan.com")
company = erpnext.get_default_company()
applicant = make_employee("test_employee@loan.com", company="_Test Company")
company = "_Test Company"
holiday_list = make_holiday("test holiday for loan")
company_doc = frappe.get_doc('Company', company)
@ -70,16 +72,21 @@ class TestPayrollEntry(unittest.TestCase):
employee_doc.holiday_list = holiday_list
employee_doc.save()
loan = create_loan(applicant,
"Personal Loan", 280000, "Repay Over Number of Periods", 20)
salary_structure = "Test Salary Structure for Loan"
make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company")
loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()
salary_structure = "Test Salary Structure for Loan"
make_salary_structure(salary_structure, "Monthly", employee_doc.name)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
make_accrual_interest_entry_for_term_loans(posting_date=nowdate())
dates = get_start_end_dates('Monthly', nowdate())
make_payroll_entry(start_date=dates.start_date,
end_date=dates.end_date, branch=branch)
make_payroll_entry(company="_Test Company", start_date=dates.start_date,
end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC")
name = frappe.db.get_value('Salary Slip',
{'posting_date': nowdate(), 'employee': applicant}, 'name')
@ -109,6 +116,13 @@ def make_payroll_entry(**args):
payroll_entry.posting_date = nowdate()
payroll_entry.payroll_frequency = "Monthly"
payroll_entry.branch = args.branch or None
if args.cost_center:
payroll_entry.cost_center = args.cost_center
if args.payment_account:
payroll_entry.payment_account = args.payment_account
payroll_entry.save()
payroll_entry.create_salary_slips()
payroll_entry.submit_salary_slips()

View File

@ -17,6 +17,7 @@ from erpnext.hr.doctype.additional_salary.additional_salary import get_additiona
from erpnext.hr.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.hr.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.hr.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
class SalarySlip(TransactionBase):
def __init__(self, *args, **kwargs):
@ -66,6 +67,7 @@ class SalarySlip(TransactionBase):
self.set_status()
self.update_status(self.name)
self.update_salary_slip_in_additional_salary()
self.make_loan_repayment_entry()
if (frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
self.email_salary_slip()
@ -73,6 +75,7 @@ class SalarySlip(TransactionBase):
self.set_status()
self.update_status()
self.update_salary_slip_in_additional_salary()
self.cancel_loan_repayment_entry()
def on_trash(self):
from frappe.model.naming import revert_series_if_last
@ -754,28 +757,35 @@ class SalarySlip(TransactionBase):
self.total_principal_amount = 0
for loan in self.get_loan_details():
self.append('loans', {
'loan': loan.name,
'total_payment': loan.total_payment,
'interest_amount': loan.interest_amount,
'principal_amount': loan.principal_amount,
'loan_account': loan.loan_account,
'interest_income_account': loan.interest_income_account
})
self.total_loan_repayment += loan.total_payment
self.total_interest_amount += loan.interest_amount
self.total_principal_amount += loan.principal_amount
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
total_payment = amounts['interest_amount'] + amounts['payable_principal_amount']
if total_payment:
self.append('loans', {
'loan': loan.name,
'total_payment': total_payment,
'interest_amount': amounts['interest_amount'],
'principal_amount': amounts['payable_principal_amount'],
'loan_account': loan.loan_account,
'interest_income_account': loan.interest_income_account
})
self.total_loan_repayment += total_payment
self.total_interest_amount += amounts['interest_amount']
self.total_principal_amount += amounts['payable_principal_amount']
def get_loan_details(self):
return frappe.db.sql("""select rps.principal_amount, rps.interest_amount, l.name,
rps.total_payment, l.loan_account, l.interest_income_account
from
`tabRepayment Schedule` as rps, `tabLoan` as l
where
l.name = rps.parent and rps.payment_date between %s and %s and
l.repay_from_salary = 1 and l.docstatus = 1 and l.applicant = %s""",
(self.start_date, self.end_date, self.employee), as_dict=True) or []
return frappe.get_all("Loan",
fields=["name", "interest_income_account", "loan_account", "loan_type"],
filters = {
"applicant": self.employee,
"docstatus": 1,
"repay_from_salary": 1,
})
def update_salary_slip_in_additional_salary(self):
salary_slip = self.name if self.docstatus==1 else None
@ -784,6 +794,23 @@ class SalarySlip(TransactionBase):
where employee=%s and payroll_date between %s and %s and docstatus=1
""", (salary_slip, self.employee, self.start_date, self.end_date))
def make_loan_repayment_entry(self):
for loan in self.loans:
repayment_entry = create_repayment_entry(loan.loan, self.employee,
self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
loan.principal_amount, loan.total_payment)
repayment_entry.save()
repayment_entry.submit()
loan.loan_repayment_entry = repayment_entry.name
def cancel_loan_repayment_entry(self):
for loan in self.loans:
if loan.loan_repayment_entry:
repayment_entry = frappe.get_doc("Loan Repayment", loan.loan_repayment_entry)
repayment_entry.cancel()
def email_salary_slip(self):
receiver = frappe.db.get_value("Employee", self.employee, "prefered_email")
hr_settings = frappe.get_single("HR Settings")

View File

@ -18,8 +18,8 @@ from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exe
class TestSalarySlip(unittest.TestCase):
def setUp(self):
make_earning_salary_component(setup=True)
make_deduction_salary_component(setup=True)
make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]:
frappe.db.sql("delete from `tab%s`" % dt)
@ -50,7 +50,7 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.deductions[0].amount, 5000)
self.assertEqual(ss.deductions[1].amount, 5000)
self.assertEqual(ss.gross_pay, 78000)
self.assertEqual(ss.net_pay, 67418.0)
self.assertEqual(ss.net_pay, 68000.0)
def test_salary_slip_with_holidays_excluded(self):
no_of_days = self.get_no_of_days()
@ -70,7 +70,7 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.deductions[0].amount, 5000)
self.assertEqual(ss.deductions[1].amount, 5000)
self.assertEqual(ss.gross_pay, 78000)
self.assertEqual(ss.net_pay, 67418.0)
self.assertEqual(ss.net_pay, 68000.0)
def test_payment_days(self):
no_of_days = self.get_no_of_days()
@ -137,21 +137,41 @@ class TestSalarySlip(unittest.TestCase):
make_employee("test_employee@salary.com")
ss = make_employee_salary_slip("test_employee@salary.com", "Monthly")
ss.company = "_Test Company"
ss.save()
ss.submit()
email_queue = frappe.db.sql("""select name from `tabEmail Queue`""")
self.assertTrue(email_queue)
def test_loan_repayment_salary_slip(self):
from erpnext.hr.doctype.loan.test_loan import create_loan_type, create_loan
applicant = make_employee("test_employee@salary.com")
create_loan_type("Car Loan", 500000, 6.4)
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20)
from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan, make_loan_disbursement_entry, create_loan_accounts
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans
applicant = make_employee("test_loanemployee@salary.com", company="_Test Company")
create_loan_accounts()
create_loan_type("Car Loan", 500000, 8.4,
is_term_loan=1,
mode_of_payment='Cash',
payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC',
penalty_income_account='Penalty Income Account - _TC')
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()
ss = make_employee_salary_slip("test_employee@salary.com", "Monthly")
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
make_accrual_interest_entry_for_term_loans(posting_date=nowdate())
ss = make_employee_salary_slip("test_loanemployee@salary.com", "Monthly")
ss.submit()
self.assertEqual(ss.total_loan_repayment, 582)
self.assertEqual(ss.total_loan_repayment, 592)
self.assertEqual(ss.net_pay, (flt(ss.gross_pay) - (flt(ss.total_deduction) + flt(ss.total_loan_repayment))))
def test_payroll_frequency(self):
@ -321,7 +341,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
return salary_slip
def make_salary_component(salary_components, test_tax):
def make_salary_component(salary_components, test_tax, company_list=None):
for salary_component in salary_components:
if not frappe.db.exists('Salary Component', salary_component["salary_component"]):
if test_tax:
@ -336,17 +356,22 @@ def make_salary_component(salary_components, test_tax):
salary_component["doctype"] = "Salary Component"
salary_component["salary_component_abbr"] = salary_component["abbr"]
frappe.get_doc(salary_component).insert()
get_salary_component_account(salary_component["salary_component"])
get_salary_component_account(salary_component["salary_component"], company_list)
def get_salary_component_account(sal_comp):
def get_salary_component_account(sal_comp, company_list=None):
company = erpnext.get_default_company()
if company_list and company not in company_list:
company_list.append(company)
sal_comp = frappe.get_doc("Salary Component", sal_comp)
if not sal_comp.get("accounts"):
sal_comp.append("accounts", {
"company": company,
"default_account": create_account(company)
})
sal_comp.save()
for d in company_list:
sal_comp.append("accounts", {
"company": d,
"default_account": create_account(d)
})
sal_comp.save()
def create_account(company):
salary_account = frappe.db.get_value("Account", "Salary - " + frappe.get_cached_value('Company', company, 'abbr'))
@ -359,7 +384,7 @@ def create_account(company):
}).insert()
return salary_account
def make_earning_salary_component(setup=False, test_tax=False):
def make_earning_salary_component(setup=False, test_tax=False, company_list=None):
data = [
{
"salary_component": 'Basic Salary',
@ -415,7 +440,7 @@ def make_earning_salary_component(setup=False, test_tax=False):
}
])
if setup or test_tax:
make_salary_component(data, test_tax)
make_salary_component(data, test_tax, company_list)
data.append({
"salary_component": 'Basic Salary',
"abbr":'BS',
@ -426,7 +451,7 @@ def make_earning_salary_component(setup=False, test_tax=False):
})
return data
def make_deduction_salary_component(setup=False, test_tax=False):
def make_deduction_salary_component(setup=False, test_tax=False, company_list=None):
data = [
{
"salary_component": 'Professional Tax',
@ -458,7 +483,7 @@ def make_deduction_salary_component(setup=False, test_tax=False):
"round_to_the_nearest_integer": 1
})
if setup or test_tax:
make_salary_component(data, test_tax)
make_salary_component(data, test_tax, company_list)
return data

View File

@ -1,263 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-11-08 12:51:12.834479",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Loan",
"length": 0,
"no_copy": 0,
"options": "Loan",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "loan_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Loan Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "interest_income_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Interest Income Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "principal_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Principal Amount",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "interest_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Interest Amount",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_payment",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Payment",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-02-26 05:24:31.369630",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Slip Loan",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@ -86,16 +86,17 @@ class TestSalaryStructure(unittest.TestCase):
self.assertEqual(salary_structure_assignment.base, 5000)
self.assertEqual(salary_structure_assignment.variable, 200)
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, test_tax=False):
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None,
test_tax=False, company=None):
if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
if not frappe.db.exists('Salary Structure', salary_structure):
details = {
"doctype": "Salary Structure",
"name": salary_structure,
"company": erpnext.get_default_company(),
"earnings": make_earning_salary_component(test_tax=test_tax),
"deductions": make_deduction_salary_component(test_tax=test_tax),
"company": company or erpnext.get_default_company(),
"earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
"deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
"payroll_frequency": payroll_frequency,
"payment_account": get_random("Account")
}
@ -109,11 +110,11 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do
if employee and not frappe.db.get_value("Salary Structure Assignment",
{'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1:
create_salary_structure_assignment(employee, salary_structure)
create_salary_structure_assignment(employee, salary_structure, company=company)
return salary_structure_doc
def create_salary_structure_assignment(employee, salary_structure, from_date=None):
def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None):
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee))
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
@ -122,7 +123,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non
salary_structure_assignment.variable = 5000
salary_structure_assignment.from_date = from_date or add_months(nowdate(), -1)
salary_structure_assignment.salary_structure = salary_structure
salary_structure_assignment.company = erpnext.get_default_company()
salary_structure_assignment.company = company or erpnext.get_default_company()
salary_structure_assignment.save(ignore_permissions=True)
salary_structure_assignment.submit()
return salary_structure_assignment

View File

@ -8,25 +8,9 @@ import unittest
from frappe.utils import nowdate,flt, cstr,random_string
# test_records = frappe.get_test_records('Vehicle Log')
class TestVehicleLog(unittest.TestCase):
def test_make_vehicle_log(self):
license_plate=random_string(10).upper()
def test_make_vehicle_log_and_syncing_of_odometer_value(self):
employee_id=frappe.db.sql("""select name from `tabEmployee` order by modified desc limit 1""")[0][0]
vehicle = frappe.get_doc({
"doctype": "Vehicle",
"license_plate": cstr(license_plate),
"make": "Maruti",
"model": "PCM",
"last_odometer":5000,
"acquisition_date":frappe.utils.nowdate(),
"location": "Mumbai",
"chassis_no": "1234ABCD",
"uom": "Litre",
"vehicle_value":frappe.utils.flt(500000)
})
try:
vehicle.insert()
except frappe.DuplicateEntryError:
pass
license_plate = get_vehicle(employee_id)
vehicle_log = frappe.get_doc({
"doctype": "Vehicle Log",
"license_plate": cstr(license_plate),
@ -36,5 +20,41 @@ class TestVehicleLog(unittest.TestCase):
"fuel_qty":frappe.utils.flt(50),
"price": frappe.utils.flt(500)
})
vehicle_log.insert()
vehicle_log.submit()
vehicle_log.save()
vehicle_log.submit()
#checking value of vehicle odometer value on submit.
vehicle = frappe.get_doc("Vehicle", license_plate)
self.assertEqual(vehicle.last_odometer, vehicle_log.odometer)
#checking value vehicle odometer on vehicle log cancellation.
last_odometer = vehicle_log.last_odometer
current_odometer = vehicle_log.odometer
distance_travelled = current_odometer - last_odometer
vehicle_log.cancel()
vehicle.reload()
self.assertEqual(vehicle.last_odometer, current_odometer - distance_travelled)
def get_vehicle(employee_id):
license_plate=random_string(10).upper()
vehicle = frappe.get_doc({
"doctype": "Vehicle",
"license_plate": cstr(license_plate),
"make": "Maruti",
"model": "PCM",
"employee": employee_id,
"last_odometer":5000,
"acquisition_date":frappe.utils.nowdate(),
"location": "Mumbai",
"chassis_no": "1234ABCD",
"uom": "Litre",
"vehicle_value":frappe.utils.flt(500000)
})
try:
vehicle.insert()
except frappe.DuplicateEntryError:
pass
return license_plate

View File

@ -2,29 +2,41 @@
// For license information, please see license.txt
frappe.ui.form.on("Vehicle Log", {
refresh: function(frm,cdt,cdn) {
var vehicle_log=frappe.model.get_doc(cdt,cdn);
if (vehicle_log.license_plate) {
frappe.call({
method: "erpnext.hr.doctype.vehicle_log.vehicle_log.get_make_model",
args: {
license_plate: vehicle_log.license_plate
},
callback: function(r) {
frappe.model.set_value(cdt, cdn, ("model"), r.message[0]);
frappe.model.set_value(cdt, cdn, ("make"), r.message[1]);
}
})
refresh: function(frm) {
if(frm.doc.license_plate && frm.doc.__islocal){
frm.events.set_vehicle_details(frm);
}
if(frm.doc.docstatus == 1) {
frm.add_custom_button(__('Expense Claim'), function() {
frm.events.expense_claim(frm)
frm.events.expense_claim(frm);
}, __('Create'));
frm.page.set_inner_btn_group_as_primary(__('Create'));
}
},
license_plate: function(frm) {
if(frm.doc.license_plate){
frm.events.set_vehicle_details(frm);
}
},
set_vehicle_details: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.vehicle_log.vehicle_log.get_make_model",
args: {
license_plate: frm.doc.license_plate
},
callback: function(r) {
frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "make", r.message[0]);
frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "model", r.message[1]);
frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "last_odometer", r.message[2]);
frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "employee", r.message[3]);
}
});
},
expense_claim: function(frm){
frappe.call({
method: "erpnext.hr.doctype.vehicle_log.vehicle_log.make_expense_claim",

View File

@ -1,706 +1,192 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "naming_series:",
"beta": 0,
"creation": "2016-09-03 14:14:51.788550",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"actions": [],
"autoname": "naming_series:",
"creation": "2016-09-03 14:14:51.788550",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"vehicle_section",
"naming_series",
"license_plate",
"employee",
"column_break_4",
"column_break_7",
"model",
"make",
"odometer_reading",
"date",
"odometer",
"column_break_12",
"last_odometer",
"refuelling_details",
"fuel_qty",
"price",
"column_break_15",
"supplier",
"invoice",
"service_details",
"service_detail",
"amended_from"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "vehicle_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"options": "fa fa-user",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "vehicle_section",
"fieldtype": "Section Break",
"options": "fa fa-user"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "naming_series",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Series",
"length": 0,
"no_copy": 1,
"options": "HR-VLOG-.YYYY.-",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 1,
"translatable": 0,
"unique": 0
},
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"no_copy": 1,
"options": "HR-VLOG-.YYYY.-",
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "license_plate",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "License Plate",
"length": 0,
"no_copy": 0,
"options": "Vehicle",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "license_plate",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "License Plate",
"options": "Vehicle",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "employee",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"length": 0,
"no_copy": 0,
"options": "Employee",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "model",
"fieldtype": "Read Only",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Model",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "model",
"fieldtype": "Read Only",
"label": "Model"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "make",
"fieldtype": "Read Only",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Make",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "make",
"fieldtype": "Read Only",
"label": "Make"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "odometer_reading",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Odometer Reading",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "odometer_reading",
"fieldtype": "Section Break",
"label": "Odometer Reading"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "date",
"fieldtype": "Date",
"label": "Date",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "odometer",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Odometer",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "odometer",
"fieldtype": "Int",
"label": "Current Odometer value ",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "refuelling_details",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Refuelling Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"collapsible": 1,
"fieldname": "refuelling_details",
"fieldtype": "Section Break",
"label": "Refuelling Details"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "fuel_qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Fuel Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "fuel_qty",
"fieldtype": "Float",
"label": "Fuel Qty"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "price",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Fuel Price",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "price",
"fieldtype": "Currency",
"label": "Fuel Price"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_15",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "supplier",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Supplier",
"length": 0,
"no_copy": 0,
"options": "Supplier",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
"options": "Supplier"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "invoice",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Invoice Ref",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "invoice",
"fieldtype": "Data",
"label": "Invoice Ref"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "service_details",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Service Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"collapsible": 1,
"fieldname": "service_details",
"fieldtype": "Section Break",
"label": "Service Details"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "service_detail",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Service Detail",
"length": 0,
"no_copy": 0,
"options": "Vehicle Service",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "service_detail",
"fieldtype": "Table",
"label": "Service Detail",
"options": "Vehicle Service"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Vehicle Log",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Vehicle Log",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "last_odometer",
"fieldtype": "Int",
"label": "last Odometer Value ",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 14:44:51.131186",
"modified_by": "Administrator",
"module": "HR",
"name": "Vehicle Log",
"name_case": "",
"owner": "Administrator",
],
"is_submittable": 1,
"links": [],
"modified": "2020-01-28 12:43:34.419647",
"modified_by": "Administrator",
"module": "HR",
"name": "Vehicle Log",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Fleet Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Fleet Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -11,22 +11,33 @@ from frappe.model.document import Document
class VehicleLog(Document):
def validate(self):
last_odometer=frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")
if flt(self.odometer) < flt(last_odometer):
frappe.throw(_("Current Odometer reading entered should be greater than initial Vehicle Odometer {0}").format(last_odometer))
if flt(self.odometer) < flt(self.last_odometer):
frappe.throw(_("Current Odometer reading entered should be greater than initial Vehicle Odometer {0}").format(self.last_odometer))
for service_detail in self.service_detail:
if (service_detail.service_item or service_detail.type or service_detail.frequency or service_detail.expense_amount):
if not (service_detail.service_item and service_detail.type and service_detail.frequency and service_detail.expense_amount):
frappe.throw(_("Service Item,Type,frequency and expense amount are required"))
def before_insert(self):
model_details = get_make_model(self.license_plate)
self.make = model_details[0]
self.model = model_details[1]
self.last_odometer = model_details[2]
self.employee = model_details[3]
def on_submit(self):
frappe.db.sql("update `tabVehicle` set last_odometer=%s where license_plate=%s",
(self.odometer, self.license_plate))
frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", self.odometer)
def on_cancel(self):
distance_travelled = self.odometer - self.last_odometer
if(distance_travelled > 0):
updated_odometer_value = int(frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")) - distance_travelled
frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", updated_odometer_value)
@frappe.whitelist()
def get_make_model(license_plate):
vehicle=frappe.get_doc("Vehicle",license_plate)
return (vehicle.make,vehicle.model)
return (vehicle.make, vehicle.model, vehicle.last_odometer, vehicle.employee)
@frappe.whitelist()
def make_expense_claim(docname):

View File

@ -76,7 +76,7 @@ def get_data(filters, leave_types):
opening = get_leave_balance_on(employee.name, leave_type, filters.from_date)
# closing balance
closing = get_leave_balance_on(employee.name, leave_type, filters.to_date)
closing = max(opening - leaves_taken, 0)
row += [opening, leaves_taken, closing]

View File

@ -1,9 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Loan Repayment"] = {
"filters": [
]
}

View File

@ -1,28 +0,0 @@
{
"add_total_row": 0,
"creation": "2019-03-29 18:58:00.166032",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"letter_head": "",
"modified": "2019-03-29 18:58:00.166032",
"modified_by": "Administrator",
"module": "HR",
"name": "Loan Repayment",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Loan",
"report_name": "Loan Repayment",
"report_type": "Script Report",
"roles": [
{
"role": "HR Manager"
},
{
"role": "Employee"
}
]
}

View File

@ -1,95 +0,0 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
columns = create_columns()
data = get_record()
return columns, data
def create_columns():
return [
{
"label": _("Employee"),
"fieldtype": "Data",
"fieldname": "employee",
"options": "Employee",
"width": 200
},
{
"label": _("Loan"),
"fieldtype": "Link",
"fieldname": "loan_name",
"options": "Loan",
"width": 200
},
{
"label": _("Loan Amount"),
"fieldtype": "Currency",
"fieldname": "loan_amount",
"options": "currency",
"width": 100
},
{
"label": _("Interest"),
"fieldtype": "Data",
"fieldname": "interest",
"width": 100
},
{
"label": _("Payable Amount"),
"fieldtype": "Currency",
"fieldname": "payable_amount",
"options": "currency",
"width": 100
},
{
"label": _("EMI"),
"fieldtype": "Currency",
"fieldname": "emi",
"options": "currency",
"width": 100
},
{
"label": _("Paid Amount"),
"fieldtype": "Currency",
"fieldname": "paid_amount",
"options": "currency",
"width": 100
},
{
"label": _("Outstanding Amount"),
"fieldtype": "Currency",
"fieldname": "out_amt",
"options": "currency",
"width": 100
},
]
def get_record():
data = []
loans = frappe.get_all("Loan",
filters=[("status", "=", "Disbursed")],
fields=["applicant", "applicant_name", "name", "loan_amount", "rate_of_interest",
"total_payment", "monthly_repayment_amount", "total_amount_paid"]
)
for loan in loans:
row = {
"employee": loan.applicant + ": " + loan.applicant_name,
"loan_name": loan.name,
"loan_amount": loan.loan_amount,
"interest": str(loan.rate_of_interest) + "%",
"payable_amount": loan.total_payment,
"emi": loan.monthly_repayment_amount,
"paid_amount": loan.total_amount_paid,
"out_amt": loan.total_payment - loan.total_amount_paid
}
data.append(row)
return data

View File

@ -316,7 +316,9 @@ def allocate_earned_leaves():
allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed
if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
new_allocation = e_leave_type.max_leaves_allowed
if new_allocation == allocation.total_leaves_allocated:
continue

View File

@ -0,0 +1,190 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan', {
setup: function(frm) {
frm.make_methods = {
'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') },
'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') }
}
},
onload: function (frm) {
frm.set_query("loan_application", function () {
return {
"filters": {
"applicant": frm.doc.applicant,
"docstatus": 1,
"status": "Approved"
}
};
});
$.each(["penalty_income_account", "interest_income_account"], function(i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Income",
"is_group": 0
}
};
});
});
$.each(["payment_account", "loan_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Asset",
"is_group": 0
}
};
});
})
frm.set_query('loan_security_pledge', function(doc, cdt, cdn) {
return {
filters: {
applicant: frm.doc.applicant,
docstatus: 1,
loan_application: frm.doc.loan_application || ''
}
};
});
},
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
if (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') {
frm.add_custom_button(__('Loan Disbursement'), function() {
frm.trigger("make_loan_disbursement");
},__('Create'));
}
if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
frm.add_custom_button(__('Loan Repayment'), function() {
frm.trigger("make_repayment_entry");
},__('Create'));
}
if (frm.doc.status == "Loan Closure Requested") {
frm.add_custom_button(__('Loan Security Unpledge'), function() {
frm.trigger("create_loan_security_unpledge");
},__('Create'));
}
}
frm.trigger("toggle_fields");
},
loan_type: function(frm) {
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan);
frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan);
},
is_secured_loan: function(frm) {
frm.toggle_reqd("loan_security_pledge", frm.doc.is_secured_loan);
},
make_loan_disbursement: function (frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"company": frm.doc.company,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"as_dict": 1
},
method: "erpnext.loan_management.doctype.loan.loan.make_loan_disbursement",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
make_repayment_entry: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"loan_type": frm.doc.loan_type,
"company": frm.doc.company,
"as_dict": 1
},
method: "erpnext.loan_management.doctype.loan.loan.make_repayment_entry",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
create_loan_security_unpledge: function(frm) {
frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge",
args : {
"loan": frm.doc.name,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"company": frm.doc.company
},
callback: function(r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
loan_application: function (frm) {
if(frm.doc.loan_application){
return frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.get_loan_application",
args: {
"loan_application": frm.doc.loan_application
},
callback: function (r) {
if (!r.exc && r.message) {
let loan_fields = ["loan_type", "loan_amount", "repayment_method",
"monthly_repayment_amount", "repayment_periods", "rate_of_interest", "is_secured_loan"]
loan_fields.forEach(field => {
frm.set_value(field, r.message[field]);
});
if (frm.doc.is_secured_loan) {
$.each(r.message.proposed_pledges, function(i, d) {
let row = frm.add_child("securities");
row.loan_security = d.loan_security;
row.qty = d.qty;
row.loan_security_price = d.loan_security_price;
row.amount = d.amount;
row.haircut = d.haircut;
});
frm.refresh_fields("securities");
}
}
}
});
}
},
repayment_method: function (frm) {
frm.trigger("toggle_fields")
},
toggle_fields: function (frm) {
frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods")
}
});

View File

@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"autoname": "ACC-LOAN-.YYYY.-.#####",
"creation": "2016-12-02 10:11:49.673604",
"creation": "2019-08-29 17:29:18.176786",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
@ -11,32 +11,41 @@
"applicant_type",
"applicant",
"applicant_name",
"loan_type",
"loan_application",
"column_break_3",
"posting_date",
"company",
"posting_date",
"status",
"repay_from_salary",
"section_break_8",
"loan_type",
"loan_amount",
"is_secured_loan",
"rate_of_interest",
"disbursement_date",
"repayment_start_date",
"disbursed_amount",
"column_break_11",
"is_term_loan",
"repayment_method",
"repayment_periods",
"monthly_repayment_amount",
"repayment_start_date",
"loan_security_details_section",
"loan_security_pledge",
"column_break_25",
"maximum_loan_value",
"account_info",
"mode_of_payment",
"payment_account",
"column_break_9",
"loan_account",
"interest_income_account",
"penalty_income_account",
"section_break_15",
"repayment_schedule",
"section_break_17",
"total_payment",
"total_principal_paid",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
@ -47,7 +56,7 @@
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember",
"options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
@ -75,6 +84,7 @@
"fieldname": "loan_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan Type",
"options": "Loan Type",
"reqd": 1
@ -95,6 +105,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"remember_last_selected_value": 1,
@ -104,9 +115,10 @@
"default": "Sanctioned",
"fieldname": "status",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Sanctioned\nDisbursed\nRepaid/Closed",
"options": "Sanctioned\nPartially Disbursed\nDisbursed\nLoan Closure Requested\nClosed",
"read_only": 1
},
{
@ -125,8 +137,7 @@
"fieldname": "loan_amount",
"fieldtype": "Currency",
"label": "Loan Amount",
"options": "Company:company:default_currency",
"reqd": 1
"options": "Company:company:default_currency"
},
{
"fetch_from": "loan_type.rate_of_interest",
@ -143,29 +154,30 @@
"label": "Disbursement Date"
},
{
"depends_on": "is_term_loan",
"fieldname": "repayment_start_date",
"fieldtype": "Date",
"label": "Repayment Start Date",
"reqd": 1
"label": "Repayment Start Date"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "Repay Over Number of Periods",
"depends_on": "is_term_loan",
"fieldname": "repayment_method",
"fieldtype": "Select",
"label": "Repayment Method",
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods",
"reqd": 1
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
"depends_on": "is_term_loan",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"label": "Repayment Period in Months"
},
{
"depends_on": "is_term_loan",
"fieldname": "monthly_repayment_amount",
"fieldtype": "Currency",
"label": "Monthly Repayment Amount",
@ -178,17 +190,21 @@
"label": "Account Info"
},
{
"fetch_from": "loan_type.mode_of_payment",
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "loan_type.payment_account",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
@ -196,25 +212,31 @@
"fieldtype": "Column Break"
},
{
"fetch_from": "loan_type.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "loan_type.interest_income_account",
"fieldname": "interest_income_account",
"fieldtype": "Link",
"label": "Interest Income Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"depends_on": "is_term_loan",
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Repayment Schedule"
},
{
"depends_on": "eval:doc.is_term_loan == 1",
"fieldname": "repayment_schedule",
"fieldtype": "Table",
"label": "Repayment Schedule",
@ -230,7 +252,7 @@
"default": "0",
"fieldname": "total_payment",
"fieldtype": "Currency",
"label": "Total Payment",
"label": "Total Payable Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
@ -240,6 +262,7 @@
},
{
"default": "0",
"depends_on": "is_term_loan",
"fieldname": "total_interest_payable",
"fieldtype": "Currency",
"label": "Total Interest Payable",
@ -262,13 +285,74 @@
"options": "Loan",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "is_secured_loan",
"fieldtype": "Check",
"label": "Is Secured Loan"
},
{
"depends_on": "is_secured_loan",
"fieldname": "loan_security_details_section",
"fieldtype": "Section Break",
"label": "Loan Security Details"
},
{
"default": "0",
"fetch_from": "loan_type.is_term_loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan",
"read_only": 1
},
{
"fetch_from": "loan_type.penalty_income_account",
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"label": "Penalty Income Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "total_principal_paid",
"fieldtype": "Currency",
"label": "Total Principal Paid",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "loan_security_pledge",
"fieldtype": "Link",
"label": "Loan Security Pledge",
"options": "Loan Security Pledge"
},
{
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fetch_from": "loan_security_pledge.maximum_loan_value",
"fieldname": "maximum_loan_value",
"fieldtype": "Currency",
"label": "Maximum Loan Value",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
"modified": "2019-12-12 14:45:38.823072",
"modified": "2020-02-07 01:31:25.172173",
"modified_by": "Administrator",
"module": "HR",
"module": "Loan Management",
"name": "Loan",
"owner": "Administrator",
"permissions": [
@ -281,7 +365,7 @@
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1

View File

@ -0,0 +1,254 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, math, json
import erpnext
from frappe import _
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
from erpnext.controllers.accounts_controller import AccountsController
class Loan(AccountsController):
def validate(self):
self.set_loan_amount()
self.set_missing_fields()
self.validate_accounts()
self.validate_loan_security_pledge()
self.validate_loan_amount()
self.check_sanctioned_amount_limit()
if self.is_term_loan:
validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount,
self.repayment_periods, self.is_term_loan)
self.make_repayment_schedule()
self.set_repayment_period()
self.calculate_totals()
def validate_accounts(self):
for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']:
company = frappe.get_value("Account", self.get(fieldname), 'company')
if company != self.company:
frappe.throw(_("Account {0} does not belongs to company {1}").format(frappe.bold(self.get(fieldname)),
frappe.bold(self.company)))
def on_submit(self):
self.link_loan_security_pledge()
def on_cancel(self):
self.unlink_loan_security_pledge()
def set_missing_fields(self):
if not self.company:
self.company = erpnext.get_default_company()
if not self.posting_date:
self.posting_date = nowdate()
if self.loan_type and not self.rate_of_interest:
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
if self.repayment_method == "Repay Over Number of Periods":
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
def validate_loan_security_pledge(self):
if self.is_secured_loan and not self.loan_security_pledge:
frappe.throw(_("Loan Security Pledge is mandatory for secured loan"))
if self.loan_security_pledge:
loan_security_details = frappe.db.get_value("Loan Security Pledge", self.loan_security_pledge,
['loan', 'company'], as_dict=1)
if loan_security_details.loan:
frappe.throw(_("Loan Security Pledge already pledged against loan {0}").format(loan_security_details.loan))
if loan_security_details.company != self.company:
frappe.throw(_("Loan Security Pledge Company and Loan Company must be same"))
def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
def make_repayment_schedule(self):
if not self.repayment_start_date:
frappe.throw(_("Repayment Start Date is mandatory for term loans"))
self.repayment_schedule = []
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
total_payment = principal_amount + interest_amount
self.append("repayment_schedule", {
"payment_date": payment_date,
"principal_amount": principal_amount,
"interest_amount": interest_amount,
"total_payment": total_payment,
"balance_loan_amount": balance_amount
})
next_payment_date = add_months(payment_date, 1)
payment_date = next_payment_date
def set_repayment_period(self):
if self.repayment_method == "Repay Fixed Amount per Period":
repayment_periods = len(self.repayment_schedule)
self.repayment_periods = repayment_periods
def calculate_totals(self):
self.total_payment = 0
self.total_interest_payable = 0
self.total_amount_paid = 0
if self.is_term_loan:
for data in self.repayment_schedule:
self.total_payment += data.total_payment
self.total_interest_payable +=data.interest_amount
else:
self.total_payment = self.loan_amount
def set_loan_amount(self):
if not self.loan_amount and self.is_secured_loan and self.loan_security_pledge:
self.loan_amount = self.maximum_loan_value
def validate_loan_amount(self):
if self.is_secured_loan and self.loan_amount > self.maximum_loan_value:
msg = _("Loan amount cannot be greater than {0}").format(self.maximum_loan_value)
frappe.throw(msg)
if not self.loan_amount:
frappe.throw(_("Loan amount is mandatory"))
def link_loan_security_pledge(self):
frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
loan = %s, status = 'Pledged', pledge_time = %s
where name = %s """, (self.name, now_datetime(), self.loan_security_pledge))
def unlink_loan_security_pledge(self):
frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
loan = '', status = 'Unpledged'
where name = %s """, (self.loan_security_pledge))
def update_total_amount_paid(doc):
total_amount_paid = 0
for data in doc.repayment_schedule:
if data.paid:
total_amount_paid += data.total_payment
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def get_total_loan_amount(applicant_type, applicant, company):
return frappe.db.get_value('Loan',
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1},
'sum(loan_amount)')
def get_sanctioned_amount_limit(applicant_type, applicant, company):
return frappe.db.get_value('Sanctioned Loan Amount',
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant},
'sanctioned_amount_limit')
def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan):
if is_term_loan and not repayment_method:
frappe.throw(_("Repayment Method is mandatory for term loans"))
if repayment_method == "Repay Over Number of Periods" and not repayment_periods:
frappe.throw(_("Please enter Repayment Periods"))
if repayment_method == "Repay Fixed Amount per Period":
if not monthly_repayment_amount:
frappe.throw(_("Please enter repayment Amount"))
if monthly_repayment_amount > loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))
def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods):
if rate_of_interest:
monthly_interest_rate = flt(rate_of_interest) / (12 *100)
monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *
(1 + monthly_interest_rate)**repayment_periods) \
/ ((1 + monthly_interest_rate)**repayment_periods - 1))
else:
monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
return monthly_repayment_amount
@frappe.whitelist()
def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application)
if loan:
return loan.as_dict()
def close_loan(loan, total_amount_paid):
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
frappe.db.set_value("Loan", loan, "status", "Closed")
@frappe.whitelist()
def make_loan_disbursement(loan, company, applicant_type, applicant, disbursed_amount=0, as_dict=0):
disbursement_entry = frappe.new_doc("Loan Disbursement")
disbursement_entry.against_loan = loan
disbursement_entry.applicant_type = applicant_type
disbursement_entry.applicant = applicant
disbursement_entry.company = company
disbursement_entry.disbursement_date = nowdate()
if disbursed_amount:
disbursement_entry.disbursed_amount = disbursed_amount
if as_dict:
return disbursement_entry.as_dict()
else:
return disbursement_entry
@frappe.whitelist()
def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as_dict=0):
repayment_entry = frappe.new_doc("Loan Repayment")
repayment_entry.against_loan = loan
repayment_entry.applicant_type = applicant_type
repayment_entry.applicant = applicant
repayment_entry.company = company
repayment_entry.loan_type = loan_type
repayment_entry.posting_date = nowdate()
if as_dict:
return repayment_entry.as_dict()
else:
return repayment_entry
@frappe.whitelist()
def create_loan_security_unpledge(loan, applicant_type, applicant, company):
loan_security_pledge_details = frappe.db.sql("""
SELECT p.parent, p.loan_security, p.qty as qty FROM `tabLoan Security Pledge` lsp , `tabPledge` p
WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1
""",(loan), as_dict=1)
unpledge_request = frappe.new_doc("Loan Security Unpledge")
unpledge_request.applicant_type = applicant_type
unpledge_request.applicant = applicant
unpledge_request.loan = loan
unpledge_request.company = company
for loan_security in loan_security_pledge_details:
unpledge_request.append('securities', {
"loan_security": loan_security.loan_security,
"qty": loan_security.qty,
"against_pledge": loan_security.parent
})
return unpledge_request.as_dict()

View File

@ -0,0 +1,19 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'loan',
'non_standard_fieldnames': {
'Loan Disbursement': 'against_loan',
'Loan Repayment': 'against_loan',
},
'transactions': [
{
'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement']
},
{
'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Security Unpledge']
}
]
}

View File

@ -0,0 +1,559 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import erpnext
import unittest
from frappe.utils import (nowdate, add_days, getdate, now_datetime, add_to_date, get_datetime,
add_months, get_first_day, get_last_day, flt, date_diff)
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year)
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import check_for_ltv_shortfall
class TestLoan(unittest.TestCase):
def setUp(self):
create_loan_accounts()
create_loan_type("Personal Loan", 500000, 8.4,
is_term_loan=1,
mode_of_payment='Cash',
payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC',
penalty_income_account='Penalty Income Account - _TC')
create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type()
create_loan_security()
create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
self.applicant1 = make_employee("robert_loan@loan.com")
if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name')
create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
def test_loan(self):
loan = frappe.get_doc("Loan", {"applicant":self.applicant1})
self.assertEquals(loan.monthly_repayment_amount, 15052)
self.assertEquals(loan.total_interest_payable, 21034)
self.assertEquals(loan.total_payment, 301034)
schedule = loan.repayment_schedule
self.assertEqual(len(schedule), 20)
for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]:
self.assertEqual(schedule[idx].principal_amount, principal_amount)
self.assertEqual(schedule[idx].interest_amount, interest_amount)
self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount)
loan.repayment_method = "Repay Fixed Amount per Period"
loan.monthly_repayment_amount = 14000
loan.save()
self.assertEquals(len(loan.repayment_schedule), 22)
self.assertEquals(loan.total_interest_payable, 22712)
self.assertEquals(loan.total_payment, 302712)
def test_loan_with_security(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50,
"loan_security_price": 500.00
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name)
self.assertEquals(loan.loan_amount, 1000000)
def test_loan_disbursement(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name)
self.assertEquals(loan.loan_amount, 1000000)
loan.submit()
loan_disbursement_entry1 = make_loan_disbursement_entry(loan.name, 500000)
loan_disbursement_entry2 = make_loan_disbursement_entry(loan.name, 500000)
loan = frappe.get_doc("Loan", loan.name)
gl_entries1 = frappe.db.get_all("GL Entry",
fields=["name"],
filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry1.name}
)
gl_entries2 = frappe.db.get_all("GL Entry",
fields=["name"],
filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry2.name}
)
self.assertEquals(loan.status, "Disbursed")
self.assertEquals(loan.disbursed_amount, 1000000)
self.assertTrue(gl_entries1)
self.assertTrue(gl_entries2)
def test_regular_loan_repayment(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68)
repayment_entry.save()
penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year))
self.assertEquals(flt(repayment_entry.interest_payable, 2), flt(accrued_interest_amount, 2))
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
repayment_entry.submit()
def test_loan_closure_repayment(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
# Adding 6 since repayment is made 5 days late after due date
# and since payment type is loan closure so interest should be considered for those
# 6 days as well though in grace period
no_of_days += 6
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
"Loan Closure", 13315.0681)
repayment_entry.save()
repayment_entry.amount_paid = repayment_entry.payable_amount
self.assertEquals(flt(repayment_entry.interest_payable, 3), flt(accrued_interest_amount, 3))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
repayment_entry.submit()
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
def test_loan_repayment_for_term_loan(self):
pledges = []
pledges.append({
"loan_security": "Test Security 2",
"qty": 4000.00,
"haircut": 50
})
pledges.append({
"loan_security": "Test Security 1",
"qty": 2000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12,
loan_security_pledge.name, posting_date=add_months(nowdate(), -1))
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
make_accrual_interest_entry_for_term_loans(posting_date=nowdate())
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(get_last_day(nowdate()), 5),
"Regular Payment", 89768.7534247)
repayment_entry.save()
repayment_entry.submit()
repayment_entry.load_from_db()
self.assertEquals(repayment_entry.interest_payable, 11250.00)
self.assertEquals(repayment_entry.payable_principal_amount, 78303.00)
def test_partial_loan_repayment(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime().year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual(posting_date = add_days(first_date, 15))
process_loan_interest_accrual(posting_date = add_days(first_date, 30))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 1), "Regular Payment", 6500)
repayment_entry.save()
repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
lia = frappe.get_all("Loan Interest Accrual", fields=["is_paid"],
filters={"loan": loan.name}, order_by="posting_date")
self.assertTrue(lia[0].get('is_paid'))
self.assertFalse(lia[1].get('is_paid'))
def test_security_shortfall(self):
pledges = []
pledges.append({
"loan_security": "Test Security 2",
"qty": 8000.00,
"haircut": 50,
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name)
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount)
frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = %s
where loan_security=%s""", (100, 'Test Security 2'))
check_for_ltv_shortfall()
loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name})
self.assertTrue(loan_security_shortfall)
self.assertEquals(loan_security_shortfall.loan_amount, 1000000.00)
self.assertEquals(loan_security_shortfall.security_value, 400000.00)
self.assertEquals(loan_security_shortfall.shortfall_amount, 600000.00)
def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):
frappe.get_doc({
"doctype": "Account",
"account_name": "Loans and Advances (Assets)",
"company": "_Test Company",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Current Assets - _TC",
"account_type": "Bank",
"is_group": 1
}).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Loan Account - _TC"):
frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"account_name": "Loan Account",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Loans and Advances (Assets) - _TC",
"account_type": "Bank",
}).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Payment Account - _TC"):
frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"account_name": "Payment Account",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Bank Accounts - _TC",
"account_type": "Bank",
}).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Interest Income Account - _TC"):
frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"root_type": "Income",
"account_name": "Interest Income Account",
"report_type": "Profit and Loss",
"currency": "INR",
"parent_account": "Direct Income - _TC",
"account_type": "Income Account",
}).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Penalty Income Account - _TC"):
frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"account_name": "Penalty Income Account",
"root_type": "Income",
"report_type": "Profit and Loss",
"currency": "INR",
"parent_account": "Direct Income - _TC",
"account_type": "Income Account",
}).insert(ignore_permissions=True)
def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None,
mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
repayment_method=None, repayment_periods=None):
if not frappe.db.exists("Loan Type", loan_name):
loan_type = frappe.get_doc({
"doctype": "Loan Type",
"company": "_Test Company",
"loan_name": loan_name,
"is_term_loan": is_term_loan,
"maximum_loan_amount": maximum_loan_amount,
"rate_of_interest": rate_of_interest,
"penalty_interest_rate": penalty_interest_rate,
"grace_period_in_days": grace_period_in_days,
"mode_of_payment": mode_of_payment,
"payment_account": payment_account,
"loan_account": loan_account,
"interest_income_account": interest_income_account,
"penalty_income_account": penalty_income_account,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods
}).insert()
loan_type.submit()
def create_loan_security_type():
if not frappe.db.exists("Loan Security Type", "Stock"):
frappe.get_doc({
"doctype": "Loan Security Type",
"loan_security_type": "Stock",
"unit_of_measure": "Nos",
"haircut": 50.00
}).insert(ignore_permissions=True)
def create_loan_security():
if not frappe.db.exists("Loan Security", "Test Security 1"):
frappe.get_doc({
"doctype": "Loan Security",
"loan_security_type": "Stock",
"loan_security_code": "532779",
"loan_security_name": "Test Security 1",
"unit_of_measure": "Nos",
"haircut": 50.00,
}).insert(ignore_permissions=True)
if not frappe.db.exists("Loan Security", "Test Security 2"):
frappe.get_doc({
"doctype": "Loan Security",
"loan_security_type": "Stock",
"loan_security_code": "531335",
"loan_security_name": "Test Security 2",
"unit_of_measure": "Nos",
"haircut": 50.00,
}).insert(ignore_permissions=True)
def create_loan_security_pledge(applicant, pledges):
lsp = frappe.new_doc("Loan Security Pledge")
lsp.applicant_type = 'Customer'
lsp.applicant = applicant
lsp.company = "_Test Company"
for pledge in pledges:
lsp.append('securities', {
"loan_security": pledge['loan_security'],
"qty": pledge['qty'],
"haircut": pledge['haircut']
})
lsp.save()
lsp.submit()
return lsp
def make_loan_disbursement_entry(loan, amount, disbursement_date=None):
loan_disbursement_entry = frappe.get_doc({
"doctype": "Loan Disbursement",
"against_loan": loan,
"disbursement_date": disbursement_date,
"company": "_Test Company",
"disbursed_amount": amount,
"cost_center": 'Main - _TC'
}).insert(ignore_permissions=True)
loan_disbursement_entry.save()
loan_disbursement_entry.submit()
return loan_disbursement_entry
def create_loan_security_price(loan_security, loan_security_price, uom, from_date, to_date):
if not frappe.db.get_value("Loan Security Price",{"loan_security": loan_security,
"valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, 'name'):
lsp = frappe.get_doc({
"doctype": "Loan Security Price",
"loan_security": loan_security,
"loan_security_price": loan_security_price,
"uom": uom,
"valid_from":from_date,
"valid_upto": to_date
}).insert(ignore_permissions=True)
def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amount):
lr = frappe.get_doc({
"doctype": "Loan Repayment",
"against_loan": loan,
"payment_type": payment_type,
"company": "_Test Company",
"posting_date": posting_date or nowdate(),
"applicant": applicant,
"amount_paid": paid_amount,
"loan_type": "Stock Loan"
}).insert(ignore_permissions=True)
return lr
def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods,
repayment_start_date=None, posting_date=None):
loan = frappe.get_doc({
"doctype": "Loan",
"applicant_type": "Employee",
"company": "_Test Company",
"applicant": applicant,
"loan_type": loan_type,
"loan_amount": loan_amount,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods,
"repayment_start_date": nowdate(),
"is_term_loan": 1,
"posting_date": posting_date or nowdate()
})
loan.save()
return loan
def create_loan_with_security(applicant, loan_type, repayment_method, repayment_periods, loan_security_pledge,
posting_date=None, repayment_start_date=None):
loan = frappe.get_doc({
"doctype": "Loan",
"company": "_Test Company",
"applicant_type": "Customer",
"posting_date": posting_date or nowdate(),
"applicant": applicant,
"loan_type": loan_type,
"is_term_loan": 1,
"is_secured_loan": 1,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods,
"repayment_start_date": repayment_start_date or nowdate(),
"mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'),
"loan_security_pledge": loan_security_pledge,
"payment_account": 'Payment Account - _TC',
"loan_account": 'Loan Account - _TC',
"interest_income_account": 'Interest Income Account - _TC',
"penalty_income_account": 'Penalty Income Account - _TC',
})
loan.save()
return loan
def create_demand_loan(applicant, loan_type, loan_security_pledge, posting_date=None):
loan = frappe.get_doc({
"doctype": "Loan",
"company": "_Test Company",
"applicant_type": "Customer",
"posting_date": posting_date or nowdate(),
"applicant": applicant,
"loan_type": loan_type,
"is_term_loan": 0,
"is_secured_loan": 1,
"mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'),
"loan_security_pledge": loan_security_pledge,
"payment_account": 'Payment Account - _TC',
"loan_account": 'Loan Account - _TC',
"interest_income_account": 'Interest Income Account - _TC',
"penalty_income_account": 'Penalty Income Account - _TC',
})
loan.save()
return loan

View File

@ -0,0 +1,127 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan Application', {
setup: function(frm) {
frm.make_methods = {
'Loan': function() { frm.trigger('create_loan') },
'Loan Security Pledge': function() { frm.trigger('create_loan_security_pledge') },
}
},
refresh: function(frm) {
frm.trigger("toggle_fields");
frm.trigger("add_toolbar_buttons");
},
repayment_method: function(frm) {
frm.doc.repayment_amount = frm.doc.repayment_periods = ""
frm.trigger("toggle_fields")
frm.trigger("toggle_required")
},
toggle_fields: function(frm) {
frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods")
},
toggle_required: function(frm){
frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period'))
frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods'))
},
add_toolbar_buttons: function(frm) {
if (frm.doc.status == "Approved") {
frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) {
frm.add_custom_button(__('Loan Security Pledge'), function() {
frm.trigger('create_loan_security_pledge')
},__('Create'))
}
});
frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) {
frm.add_custom_button(__('Loan'), function() {
frm.trigger('create_loan')
},__('Create'))
} else {
frm.set_df_property('status', 'read_only', 1);
}
});
}
},
create_loan: function(frm) {
if (frm.doc.status != "Approved") {
frappe.throw(__("Cannot create loan until application is approved"))
}
frappe.model.open_mapped_doc({
method: 'erpnext.loan_management.doctype.loan_application.loan_application.create_loan',
frm: frm
});
},
create_loan_security_pledge: function(frm) {
frappe.call({
method: "erpnext.loan_management.doctype.loan_application.loan_application.create_pledge",
args: {
loan_application: frm.doc.name
},
callback: function(r) {
frappe.set_route("Form", "Loan Security Pledge", r.message);
}
})
},
is_term_loan: function(frm) {
frm.set_df_property('repayment_method', 'hidden', 1 - frm.doc.is_term_loan);
frm.set_df_property('repayment_method', 'reqd', frm.doc.is_term_loan);
},
is_secured_loan: function(frm) {
frm.set_df_property('proposed_pledges', 'reqd', frm.doc.is_secured_loan);
},
calculate_amounts: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.qty) {
frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price);
frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100)));
} else if (row.amount) {
frappe.model.set_value(cdt, cdn, 'qty', cint(row.amount / row.loan_security_price));
frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price);
frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100)));
}
let maximum_amount = 0;
$.each(frm.doc.proposed_pledges || [], function(i, item){
maximum_amount += item.post_haircut_amount;
});
if (flt(maximum_amount)) {
frm.set_value('maximum_loan_amount', flt(maximum_amount));
}
}
});
frappe.ui.form.on("Proposed Pledge", {
loan_security: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
frappe.call({
method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price",
args: {
loan_security: row.loan_security
},
callback: function(r) {
frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message);
frm.events.calculate_amounts(frm, cdt, cdn);
}
})
},
amount: function(frm, cdt, cdn) {
frm.events.calculate_amounts(frm, cdt, cdn);
},
qty: function(frm, cdt, cdn) {
frm.events.calculate_amounts(frm, cdt, cdn);
},
})

View File

@ -0,0 +1,278 @@
{
"autoname": "ACC-LOAP-.YYYY.-.#####",
"creation": "2019-08-29 17:46:49.201740",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"applicant_type",
"applicant",
"applicant_name",
"column_break_2",
"company",
"posting_date",
"status",
"section_break_4",
"loan_type",
"is_term_loan",
"loan_amount",
"is_secured_loan",
"rate_of_interest",
"column_break_7",
"description",
"loan_security_details_section",
"proposed_pledges",
"maximum_loan_amount",
"repayment_info",
"repayment_method",
"total_payable_amount",
"column_break_11",
"repayment_periods",
"repayment_amount",
"total_payable_interest",
"amended_from"
],
"fields": [
{
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type",
"reqd": 1
},
{
"depends_on": "applicant",
"fieldname": "applicant_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Applicant Name",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date"
},
{
"allow_on_submit": 1,
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
"options": "Open\nApproved\nRejected",
"permlevel": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"label": "Loan Info"
},
{
"fieldname": "loan_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan Type",
"options": "Loan Type",
"reqd": 1
},
{
"bold": 1,
"fieldname": "loan_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Loan Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Reason"
},
{
"depends_on": "eval: doc.is_term_loan == 1",
"fieldname": "repayment_info",
"fieldtype": "Section Break",
"label": "Repayment Info"
},
{
"depends_on": "eval: doc.is_term_loan == 1",
"fetch_from": "loan_type.repayment_method",
"fetch_if_empty": 1,
"fieldname": "repayment_method",
"fieldtype": "Select",
"label": "Repayment Method",
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
"fetch_from": "loan_type.rate_of_interest",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"label": "Rate of Interest",
"read_only": 1
},
{
"depends_on": "is_term_loan",
"fieldname": "total_payable_interest",
"fieldtype": "Currency",
"label": "Total Payable Interest",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"depends_on": "repayment_method",
"fieldname": "repayment_amount",
"fieldtype": "Currency",
"label": "Monthly Repayment Amount",
"options": "Company:company:default_currency"
},
{
"depends_on": "repayment_method",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"label": "Repayment Period in Months"
},
{
"fieldname": "total_payable_amount",
"fieldtype": "Currency",
"label": "Total Payable Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Application",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "is_secured_loan",
"fieldtype": "Check",
"label": "Is Secured Loan"
},
{
"depends_on": "eval:doc.is_secured_loan == 1",
"fieldname": "loan_security_details_section",
"fieldtype": "Section Break",
"label": "Loan Security Details"
},
{
"depends_on": "eval:doc.is_secured_loan == 1",
"fieldname": "proposed_pledges",
"fieldtype": "Table",
"label": "Proposed Pledges",
"options": "Proposed Pledge"
},
{
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"label": "Maximum Loan Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"default": "0",
"fetch_from": "loan_type.is_term_loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan",
"read_only": 1
}
],
"is_submittable": 1,
"modified": "2019-10-24 10:32:03.740558",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Application",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"submit": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1
}
],
"search_fields": "applicant_type, applicant, loan_type, loan_amount",
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "applicant",
"title_field": "applicant",
"track_changes": 1
}

View File

@ -0,0 +1,209 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, math
from frappe import _
from frappe.utils import flt, rounded, cint
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from erpnext.loan_management.doctype.loan.loan import (get_monthly_repayment_amount, validate_repayment_method,
get_total_loan_amount, get_sanctioned_amount_limit)
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
import json
from six import string_types
class LoanApplication(Document):
def validate(self):
validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount,
self.repayment_periods, self.is_term_loan)
self.validate_loan_type()
self.set_pledge_amount()
self.set_loan_amount()
self.validate_loan_amount()
self.get_repayment_details()
self.check_sanctioned_amount_limit()
def validate_loan_type(self):
company = frappe.get_value("Loan Type", self.loan_type, "company")
if company != self.company:
frappe.throw(_("Please select Loan Type for company {0}").format(frappe.bold(self.company)))
def validate_loan_amount(self):
if not self.loan_amount:
frappe.throw(_("Loan Amount is mandatory"))
maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount')
if maximum_loan_limit and self.loan_amount > maximum_loan_limit:
frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit))
if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount:
frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount))
def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
def set_pledge_amount(self):
for proposed_pledge in self.proposed_pledges:
if not proposed_pledge.qty and not proposed_pledge.amount:
frappe.throw(_("Qty or Amount is mandatroy for loan security"))
proposed_pledge.loan_security_price = get_loan_security_price(proposed_pledge.loan_security)
if not proposed_pledge.qty:
proposed_pledge.qty = cint(proposed_pledge.amount/proposed_pledge.loan_security_price)
proposed_pledge.amount = proposed_pledge.qty * proposed_pledge.loan_security_price
proposed_pledge.post_haircut_amount = cint(proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut/100))
def get_repayment_details(self):
if self.is_term_loan:
if self.repayment_method == "Repay Over Number of Periods":
self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
if monthly_interest_rate:
min_repayment_amount = self.loan_amount*monthly_interest_rate
if self.repayment_amount - min_repayment_amount <= 0:
frappe.throw(_("Repayment Amount must be greater than " \
+ str(flt(min_repayment_amount, 2))))
self.repayment_periods = math.ceil((math.log(self.repayment_amount) -
math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate)))
else:
self.repayment_periods = self.loan_amount / self.repayment_amount
self.calculate_payable_amount()
else:
self.total_payable_amount = self.loan_amount
def calculate_payable_amount(self):
balance_amount = self.loan_amount
self.total_payable_amount = 0
self.total_payable_interest = 0
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
self.total_payable_interest += interest_amount
self.total_payable_amount = self.loan_amount + self.total_payable_interest
def set_loan_amount(self):
if self.is_secured_loan and not self.proposed_pledges:
frappe.throw(_("Proposed Pledges are mandatory for secured Loans"))
if not self.loan_amount and self.is_secured_loan and self.proposed_pledges:
self.loan_amount = 0
for security in self.proposed_pledges:
self.loan_amount += security.post_haircut_amount
@frappe.whitelist()
def create_loan(source_name, target_doc=None, submit=0):
def update_accounts(source_doc, target_doc, source_parent):
account_details = frappe.get_all("Loan Type",
fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"],
filters = {'name': source_doc.loan_type}
)[0]
loan_security_pledge = frappe.db.get_value("Loan Security Pledge", {"loan_application": source_name}, 'name')
target_doc.mode_of_payment = account_details.mode_of_payment
target_doc.payment_account = account_details.payment_account
target_doc.loan_account = account_details.loan_account
target_doc.interest_income_account = account_details.interest_income_account
target_doc.penalty_income_account = account_details.penalty_income_account
if loan_security_pledge:
target_doc.is_secured_loan = 1
target_doc.loan_security_pledge = loan_security_pledge
doclist = get_mapped_doc("Loan Application", source_name, {
"Loan Application": {
"doctype": "Loan",
"validation": {
"docstatus": ["=", 1]
},
"postprocess": update_accounts,
"field_no_map": [
"is_secured_loan"
]
}
}, target_doc)
if submit:
doclist.submit()
return doclist
@frappe.whitelist()
def create_pledge(loan_application, loan=None):
loan_application_doc = frappe.get_doc("Loan Application", loan_application)
lsp = frappe.new_doc("Loan Security Pledge")
lsp.applicant_type = loan_application_doc.applicant_type
lsp.applicant = loan_application_doc.applicant
lsp.loan_application = loan_application_doc.name
lsp.company = loan_application_doc.company
if loan:
lsp.loan = loan
for pledge in loan_application_doc.proposed_pledges:
lsp.append('securities', {
"loan_security": pledge.loan_security,
"qty": pledge.qty,
"loan_security_price": pledge.loan_security_price,
"haircut": pledge.haircut
})
lsp.save()
lsp.submit()
message = _("Loan Security Pledge Created : {0}").format(lsp.name)
frappe.msgprint(message)
return lsp.name
#This is a sandbox method to get the proposed pledges
@frappe.whitelist()
def get_proposed_pledge(securities):
if isinstance(securities, string_types):
securities = json.loads(securities)
proposed_pledges = {
'securities': []
}
maximum_loan_amount = 0
for security in securities:
security = frappe._dict(security)
if not security.qty and not security.amount:
frappe.throw(_("Qty or Amount is mandatroy for loan security"))
security.loan_security_price = get_loan_security_price(security.loan_security)
if not security.qty:
security.qty = cint(security.amount/security.loan_security_price)
security.amount = security.qty * security.loan_security_price
security.post_haircut_amount = security.amount - (security.amount * security.haircut/100)
maximum_loan_amount += security.post_haircut_amount
proposed_pledges['securities'].append(security)
proposed_pledges['maximum_loan_amount'] = maximum_loan_amount
return proposed_pledges

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