Merge branch 'place-of-supply-change-issue' of https://github.com/pateljannat/erpnext into place-of-supply-change-issue

This commit is contained in:
pateljannat 2020-11-18 12:52:24 +05:30
commit d0c528143f
212 changed files with 10182 additions and 3059 deletions

View File

@ -5,7 +5,7 @@
"es6": true
},
"parserOptions": {
"ecmaVersion": 6,
"ecmaVersion": 9,
"sourceType": "module"
},
"extends": "eslint:recommended",
@ -15,6 +15,14 @@
"tab",
{ "SwitchCase": 1 }
],
"brace-style": [
"error",
"1tbs"
],
"space-unary-ops": [
"error",
{ "words": true }
],
"linebreak-style": [
"error",
"unix"
@ -44,12 +52,10 @@
"no-control-regex": [
"off"
],
"spaced-comment": [
"warn"
],
"no-trailing-spaces": [
"warn"
]
"space-before-blocks": "warn",
"keyword-spacing": "warn",
"comma-spacing": "warn",
"key-spacing": "warn"
},
"root": true,
"globals": {

View File

@ -23,7 +23,7 @@
{
"hidden": 0,
"label": "Reports",
"links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance for Party\",\n \"name\": \"Trial Balance for Party\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Payment Period Based On Invoice Date\",\n \"name\": \"Payment Period Based On Invoice Date\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Payment Summary\",\n \"name\": \"Sales Payment Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Address And Contacts\",\n \"name\": \"Address And Contacts\",\n \"type\": \"report\"\n }\n]"
"links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance for Party\",\n \"name\": \"Trial Balance for Party\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Payment Period Based On Invoice Date\",\n \"name\": \"Payment Period Based On Invoice Date\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Payment Summary\",\n \"name\": \"Sales Payment Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Address And Contacts\",\n \"name\": \"Address And Contacts\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"DATEV Export\",\n \"name\": \"DATEV\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
@ -43,7 +43,7 @@
{
"hidden": 0,
"label": "Bank Statement",
"links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -98,7 +98,7 @@
"idx": 0,
"is_standard": 1,
"label": "Accounting",
"modified": "2020-10-08 20:31:46.022470",
"modified": "2020-11-11 18:35:11.542909",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@ -108,7 +108,7 @@
"pin_to_top": 0,
"shortcuts": [
{
"label": "Chart of Accounts",
"label": "Chart Of Accounts",
"link_to": "Account",
"type": "DocType"
},

View File

@ -158,8 +158,11 @@ class TestBudget(unittest.TestCase):
set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center")
month = now_datetime().month
if month > 10:
month = 10
for i in range(now_datetime().month):
for i in range(month):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
@ -177,8 +180,11 @@ class TestBudget(unittest.TestCase):
set_total_expense_zero(nowdate(), "project")
budget = make_budget(budget_against="Project")
month = now_datetime().month
if month > 10:
month = 10
for i in range(now_datetime().month):
for i in range(month):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project")

View File

@ -9,11 +9,7 @@ frappe.ui.form.on('Fiscal Year', {
}
},
refresh: function (frm) {
let doc = frm.doc;
frm.toggle_enable('year_start_date', doc.__islocal);
frm.toggle_enable('year_end_date', doc.__islocal);
if (!doc.__islocal && (doc.name != frappe.sys_defaults.fiscal_year)) {
if (!frm.doc.__islocal && (frm.doc.name != frappe.sys_defaults.fiscal_year)) {
frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm));
frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'"));
} else {
@ -24,8 +20,10 @@ frappe.ui.form.on('Fiscal Year', {
return frm.call('set_as_default');
},
year_start_date: function(frm) {
let year_end_date =
frappe.datetime.add_days(frappe.datetime.add_months(frm.doc.year_start_date, 12), -1);
frm.set_value("year_end_date", year_end_date);
if (!frm.doc.is_short_year) {
let year_end_date =
frappe.datetime.add_days(frappe.datetime.add_months(frm.doc.year_start_date, 12), -1);
frm.set_value("year_end_date", year_end_date);
}
},
});

View File

@ -1,347 +1,126 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "field:year",
"beta": 0,
"creation": "2013-01-22 16:50:25",
"custom": 0,
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"actions": [],
"allow_import": 1,
"autoname": "field:year",
"creation": "2013-01-22 16:50:25",
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"year",
"disabled",
"is_short_year",
"year_start_date",
"year_end_date",
"companies",
"auto_created"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "For e.g. 2012, 2012-13",
"fieldname": "year",
"fieldtype": "Data",
"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": "Year Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "year",
"oldfieldtype": "Data",
"permlevel": 0,
"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
},
"description": "For e.g. 2012, 2012-13",
"fieldname": "year",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Year Name",
"oldfieldname": "year",
"oldfieldtype": "Data",
"reqd": 1,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"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,
"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
},
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "year_start_date",
"fieldtype": "Date",
"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": "Year Start Date",
"length": 0,
"no_copy": 1,
"oldfieldname": "year_start_date",
"oldfieldtype": "Date",
"permlevel": 0,
"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": "year_start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Year Start Date",
"no_copy": 1,
"oldfieldname": "year_start_date",
"oldfieldtype": "Date",
"reqd": 1,
"set_only_once": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "year_end_date",
"fieldtype": "Date",
"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": "Year End Date",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"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": "year_end_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Year End Date",
"no_copy": 1,
"reqd": 1,
"set_only_once": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "companies",
"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": "Companies",
"length": 0,
"no_copy": 0,
"options": "Fiscal Year 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": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "companies",
"fieldtype": "Table",
"label": "Companies",
"options": "Fiscal Year Company"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "auto_created",
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Auto Created",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"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
"default": "0",
"fieldname": "auto_created",
"fieldtype": "Check",
"hidden": 1,
"label": "Auto Created",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"description": "Less than 12 months.",
"fieldname": "is_short_year",
"fieldtype": "Check",
"label": "Is Short Year",
"set_only_once": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-calendar",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-25 14:21:41.273354",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
"owner": "Administrator",
],
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2020-11-05 12:16:53.081573",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
},
{
"amend": 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": "Sales User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
"read": 1,
"role": "Sales User"
},
{
"amend": 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": "Purchase User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
"read": 1,
"role": "Purchase User"
},
{
"amend": 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": "Accounts User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
"read": 1,
"role": "Accounts User"
},
{
"amend": 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": "Stock User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
"read": 1,
"role": "Stock User"
},
{
"amend": 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
"read": 1,
"role": "Employee"
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 1,
"sort_field": "name",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
],
"show_name_in_global_search": 1,
"sort_field": "name",
"sort_order": "DESC"
}

View File

@ -36,6 +36,11 @@ class FiscalYear(Document):
frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
def validate_dates(self):
if self.is_short_year:
# Fiscal Year can be shorter than one year, in some jurisdictions
# under certain circumstances. For example, in the USA and Germany.
return
if getdate(self.year_start_date) > getdate(self.year_end_date):
frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
FiscalYearIncorrectDate)
@ -116,12 +121,8 @@ def auto_create_fiscal_year():
pass
def get_from_and_to_date(fiscal_year):
from_and_to_date_tuple = frappe.db.sql("""select year_start_date, year_end_date
from `tabFiscal Year` where name=%s""", (fiscal_year))[0]
from_and_to_date = {
"from_date": from_and_to_date_tuple[0],
"to_date": from_and_to_date_tuple[1]
}
return from_and_to_date
fields = [
"year_start_date as from_date",
"year_end_date as to_date"
]
return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)

View File

@ -11,6 +11,7 @@ test_records = frappe.get_test_records('Fiscal Year')
test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase):
def test_extra_year(self):
if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"):
frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000")

View File

@ -1,4 +1,11 @@
[
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2012",

View File

@ -132,15 +132,19 @@ class POSInvoice(SalesInvoice):
msg = ""
item_code = frappe.bold(d.item_code)
serial_nos = get_serial_nos(d.serial_no)
if serialized and batched and (no_batch_selected or no_serial_selected):
msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.')
.format(d.idx, item_code))
if serialized and no_serial_selected:
elif serialized and no_serial_selected:
msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.')
.format(d.idx, item_code))
if batched and no_batch_selected:
elif batched and no_batch_selected:
msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.')
.format(d.idx, item_code))
elif serialized and not no_serial_selected and len(serial_nos) != d.qty:
msg = (_("Row #{}: You must select {} serial numbers for item {}.").format(d.idx, frappe.bold(cint(d.qty)), item_code))
if msg:
error_msg.append(msg)

View File

@ -352,8 +352,14 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
pricing_rule_rate = 0.0
if pricing_rule.currency == args.currency:
pricing_rule_rate = pricing_rule.rate
if pricing_rule_rate:
# Override already set price list rate (from item price)
# if pricing_rule_rate > 0
item_details.update({
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
})
item_details.update({
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
"discount_percentage": 0.0
})

View File

@ -484,6 +484,43 @@ class TestPricingRule(unittest.TestCase):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
def test_item_price_with_pricing_rule(self):
item = make_item("Water Flask")
make_item_price("Water Flask", "_Test Price List", 100)
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Water Flask Rule",
"apply_on": "Item Code",
"items": [{
"item_code": "Water Flask",
}],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Rate",
"rate": 0,
"margin_type": "Percentage",
"margin_rate_or_amount": 2,
"company": "_Test Company"
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
si.selling_price_list = "_Test Price List"
si.save()
# If rate in Rule is 0, give preference to Item Price if it exists
self.assertEqual(si.items[0].price_list_rate, 100)
self.assertEqual(si.items[0].margin_rate_or_amount, 2)
self.assertEqual(si.items[0].rate_with_margin, 102)
self.assertEqual(si.items[0].rate, 102)
si.delete()
rule.delete()
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
def make_pricing_rule(**args):
args = frappe._dict(args)

View File

@ -99,6 +99,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
target: me.frm,
setters: {
supplier: me.frm.doc.supplier || undefined,
schedule_date: undefined
},
get_query_filters: {
docstatus: 1,
@ -107,16 +108,16 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__('Purchase Receipt'), function() {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice",
source_doctype: "Purchase Receipt",
target: me.frm,
date_field: "posting_date",
setters: {
supplier: me.frm.doc.supplier || undefined,
posting_date: undefined
},
get_query_filters: {
docstatus: 1,
@ -125,7 +126,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
is_return: 0
}
})
}, __("Get items from"));
}, __("Get Items From"));
}
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes");

View File

@ -199,7 +199,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
},
quotation_btn: function() {
@ -223,7 +223,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
},
delivery_note_btn: function() {
@ -251,7 +251,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
};
}
});
}, __("Get items from"));
}, __("Get Items From"));
},
tc_name: function() {
@ -812,10 +812,10 @@ frappe.ui.form.on('Sales Invoice', {
if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) {
frm.add_custom_button(__('Healthcare Services'), function() {
get_healthcare_services_to_invoice(frm);
},"Get items from");
},"Get Items From");
frm.add_custom_button(__('Prescriptions'), function() {
get_drugs_to_invoice(frm);
},"Get items from");
},"Get Items From");
}
}
else {
@ -1080,7 +1080,7 @@ var get_drugs_to_invoice = function(frm) {
description:'Quantity will be calculated only for items which has "Nos" as UoM. You may change as required for each invoice item.',
get_query: function(doc) {
return {
filters: {
filters: {
patient: dialog.get_value("patient"),
company: frm.doc.company,
docstatus: 1

View File

@ -1401,6 +1401,7 @@ def make_delivery_note(source_name, target_doc=None):
def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):

View File

@ -59,7 +59,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
billing_address=party_address, shipping_address=shipping_address)
if fetch_payment_terms_template:
party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company)
party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)
if not party_details.get("currency"):
party_details["currency"] = currency
@ -315,7 +315,7 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
due_date = None
if (bill_date or posting_date) and party:
due_date = bill_date or posting_date
template_name = get_pyt_term_template(party, party_type, company)
template_name = get_payment_terms_template(party, party_type, company)
if template_name:
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
@ -422,7 +422,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup
@frappe.whitelist()
def get_pyt_term_template(party_name, party_type, company=None):
def get_payment_terms_template(party_name, party_type, company=None):
if party_type not in ("Customer", "Supplier"):
return
template = None

View File

@ -19,7 +19,7 @@
</div>
<div class="col-xs-6">
<table>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.formatdate(doc.creation) }}</td></tr>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
</table>
</div>
</div>

View File

@ -17,7 +17,7 @@
</div>
<div class="col-xs-6">
<table>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.formatdate(doc.creation) }}</td></tr>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
</table>
</div>
</div>

View File

@ -5,7 +5,7 @@
{{ add_header(0, 1, doc, letter_head, no_letterhead, print_settings) }}
{%- for label, value in (
(_("Received On"), frappe.utils.formatdate(doc.voucher_date)),
(_("Received On"), frappe.utils.format_date(doc.voucher_date)),
(_("Received From"), doc.pay_to_recd_from),
(_("Amount"), "<strong>" + doc.get_formatted("total_amount") + "</strong><br>" + (doc.total_amount_in_words or "") + "<br>"),
(_("Remarks"), doc.remark)

View File

@ -8,7 +8,7 @@
<div class="col-xs-6">
<table>
<tr><td><strong>Supplier Name: </strong></td><td>{{ doc.supplier }}</td></tr>
<tr><td><strong>Due Date: </strong></td><td>{{ frappe.utils.formatdate(doc.due_date) }}</td></tr>
<tr><td><strong>Due Date: </strong></td><td>{{ frappe.utils.format_date(doc.due_date) }}</td></tr>
<tr><td><strong>Address: </strong></td><td>{{doc.address_display}}</td></tr>
<tr><td><strong>Contact: </strong></td><td>{{doc.contact_display}}</td></tr>
<tr><td><strong>Mobile no: </strong> </td><td>{{doc.contact_mobile}}</td></tr>
@ -17,7 +17,7 @@
<div class="col-xs-6">
<table>
<tr><td><strong>Voucher No: </strong></td><td>{{ doc.name }}</td></tr>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.formatdate(doc.creation) }}</td></tr>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
</table>
</div>
</div>

View File

@ -8,7 +8,7 @@
<div class="col-xs-6">
<table>
<tr><td><strong>Customer Name: </strong></td><td>{{ doc.customer }}</td></tr>
<tr><td><strong>Due Date: </strong></td><td>{{ frappe.utils.formatdate(doc.due_date) }}</td></tr>
<tr><td><strong>Due Date: </strong></td><td>{{ frappe.utils.format_date(doc.due_date) }}</td></tr>
<tr><td><strong>Address: </strong></td><td>{{doc.address_display}}</td></tr>
<tr><td><strong>Contact: </strong></td><td>{{doc.contact_display}}</td></tr>
<tr><td><strong>Mobile no: </strong> </td><td>{{doc.contact_mobile}}</td></tr>
@ -17,7 +17,7 @@
<div class="col-xs-6">
<table>
<tr><td><strong>Voucher No: </strong></td><td>{{ doc.name }}</td></tr>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.formatdate(doc.creation) }}</td></tr>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
</table>
</div>
</div>

View File

@ -160,6 +160,8 @@ class ReceivablePayableReport(object):
else:
# advance / unlinked payment or other adjustment
row.paid -= gle_balance
if gle.cost_center:
row.cost_center = str(gle.cost_center)
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
@ -210,7 +212,6 @@ class ReceivablePayableReport(object):
for key, row in self.voucher_balance.items():
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
row.invoice_grand_total = row.invoiced
if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
# non-zero oustanding, we must consider this row
@ -577,7 +578,7 @@ class ReceivablePayableReport(object):
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no,
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, remarks, {0}
from
`tabGL Entry`
@ -741,6 +742,7 @@ class ReceivablePayableReport(object):
self.add_column(_("Customer Contact"), fieldname='customer_primary_contact',
fieldtype='Link', options='Contact')
self.add_column(label=_('Cost Center'), fieldname='cost_center', fieldtype='Data')
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
options='voucher_type', width=180)

View File

@ -50,13 +50,11 @@
"reqd": 1
},
{
"depends_on": "eval:parent.doctype == 'Asset'",
"fieldname": "depreciation_start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Depreciation Posting Date",
"mandatory_depends_on": "eval:parent.doctype == 'Asset'",
"reqd": 1
"mandatory_depends_on": "eval:parent.doctype == 'Asset'"
},
{
"default": "0",
@ -87,7 +85,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-30 15:22:29.119868",
"modified": "2020-11-05 16:30:09.213479",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",

View File

@ -108,7 +108,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_team_members(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") })
return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }, "team_member")
@frappe.whitelist()
def get_maintenance_log(asset_name):

View File

@ -90,6 +90,11 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
this.frm.set_df_property("drop_ship", "hidden", !is_drop_ship);
if(doc.docstatus == 1) {
this.frm.fields_dict.items_section.wrapper.addClass("hide-border");
if(!this.frm.doc.set_warehouse) {
this.frm.fields_dict.items_section.wrapper.removeClass("hide-border");
}
if(!in_list(["Closed", "Delivered"], doc.status)) {
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) {
this.frm.add_custom_button(__('Update Items'), () => {
@ -126,16 +131,25 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
if(doc.status != "Closed") {
if (doc.status != "On Hold") {
if(flt(doc.per_received) < 100 && allow_receipt) {
cur_frm.add_custom_button(__('Receipt'), this.make_purchase_receipt, __('Create'));
cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create'));
if(doc.is_subcontracted==="Yes" && me.has_unsupplied_items()) {
cur_frm.add_custom_button(__('Material to Supplier'),
function() { me.make_stock_entry(); }, __("Transfer"));
}
}
if(flt(doc.per_billed) < 100)
cur_frm.add_custom_button(__('Invoice'),
cur_frm.add_custom_button(__('Purchase Invoice'),
this.make_purchase_invoice, __('Create'));
if(flt(doc.per_billed)==0 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
}
if(flt(doc.per_billed)==0) {
this.frm.add_custom_button(__('Payment Request'),
function() { me.make_payment_request() }, __('Create'));
}
if(!doc.auto_repeat) {
cur_frm.add_custom_button(__('Subscription'), function() {
erpnext.utils.make_subscription(doc.doctype, doc.name)
@ -156,13 +170,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
});
}
}
if(flt(doc.per_billed)==0) {
this.frm.add_custom_button(__('Payment Request'),
function() { me.make_payment_request() }, __('Create'));
}
if(flt(doc.per_billed)==0 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
}
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
}
} else if(doc.docstatus===0) {
@ -299,7 +307,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
if(me.values) {
me.values.sub_con_rm_items.map((row,i) => {
if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
frappe.throw(__("Item Code, warehouse, quantity are required on row" + (i+1)));
frappe.throw(__("Item Code, warehouse, quantity are required on row {0}", [i+1]));
}
})
me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children())
@ -358,15 +366,19 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order",
source_doctype: "Material Request",
target: me.frm,
setters: {},
setters: {
schedule_date: undefined,
status: undefined
},
get_query_filters: {
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99],
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__('Supplier Quotation'),
function() {
@ -375,16 +387,17 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
source_doctype: "Supplier Quotation",
target: me.frm,
setters: {
supplier: me.frm.doc.supplier
supplier: me.frm.doc.supplier,
valid_till: undefined
},
get_query_filters: {
docstatus: 1,
status: ["!=", "Stopped"],
status: ["not in", ["Stopped", "Expired"]],
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__('Update rate as per last purchase'),
this.frm.add_custom_button(__('Update Rate as per Last Purchase'),
function() {
frappe.call({
"method": "get_last_purchase_rate",

View File

@ -30,8 +30,8 @@
"customer_contact_email",
"section_addresses",
"supplier_address",
"contact_person",
"address_display",
"contact_person",
"contact_display",
"contact_mobile",
"contact_email",
@ -49,12 +49,14 @@
"plc_conversion_rate",
"ignore_pricing_rule",
"sec_warehouse",
"set_warehouse",
"col_break_warehouse",
"is_subcontracted",
"col_break_warehouse",
"supplier_warehouse",
"items_section",
"before_items_section",
"scan_barcode",
"items_col_break",
"set_warehouse",
"items_section",
"items",
"sb_last_purchase",
"total_qty",
@ -108,18 +110,13 @@
"payment_terms_template",
"payment_schedule",
"tracking_section",
"per_billed",
"status",
"column_break_75",
"per_billed",
"per_received",
"terms_section_break",
"tc_name",
"terms",
"more_info",
"status",
"ref_sq",
"column_break_74",
"party_account_currency",
"inter_company_order_reference",
"column_break5",
"letter_head",
"select_print_heading",
@ -131,7 +128,12 @@
"to_date",
"column_break_97",
"auto_repeat",
"update_auto_repeat_reference"
"update_auto_repeat_reference",
"more_info",
"ref_sq",
"column_break_74",
"party_account_currency",
"inter_company_order_reference"
],
"fields": [
{
@ -313,34 +315,34 @@
{
"fieldname": "supplier_address",
"fieldtype": "Link",
"label": "Select Supplier Address",
"label": "Supplier Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "contact_person",
"fieldtype": "Link",
"label": "Contact Person",
"label": "Supplier Contact",
"options": "Contact",
"print_hide": 1
},
{
"fieldname": "address_display",
"fieldtype": "Small Text",
"label": "Address",
"label": "Supplier Address Details",
"read_only": 1
},
{
"fieldname": "contact_display",
"fieldtype": "Small Text",
"in_global_search": 1,
"label": "Contact",
"label": "Contact Name",
"read_only": 1
},
{
"fieldname": "contact_mobile",
"fieldtype": "Small Text",
"label": "Mobile No",
"label": "Contact Mobile No",
"read_only": 1
},
{
@ -358,14 +360,14 @@
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Select Shipping Address",
"label": "Company Shipping Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "shipping_address_display",
"fieldtype": "Small Text",
"label": "Shipping Address",
"label": "Shipping Address Details",
"print_hide": 1,
"read_only": 1
},
@ -433,7 +435,8 @@
},
{
"fieldname": "sec_warehouse",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Subcontracting"
},
{
"description": "Sets 'Warehouse' in each row of the Items table.",
@ -466,6 +469,7 @@
{
"fieldname": "items_section",
"fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart"
},
@ -598,7 +602,8 @@
},
{
"fieldname": "section_break_52",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "taxes",
@ -626,10 +631,12 @@
{
"fieldname": "totals",
"fieldtype": "Section Break",
"label": "Taxes and Charges",
"oldfieldtype": "Section Break",
"options": "fa fa-money"
},
{
"depends_on": "base_taxes_and_charges_added",
"fieldname": "base_taxes_and_charges_added",
"fieldtype": "Currency",
"label": "Taxes and Charges Added (Company Currency)",
@ -640,6 +647,7 @@
"read_only": 1
},
{
"depends_on": "base_taxes_and_charges_deducted",
"fieldname": "base_taxes_and_charges_deducted",
"fieldtype": "Currency",
"label": "Taxes and Charges Deducted (Company Currency)",
@ -650,6 +658,7 @@
"read_only": 1
},
{
"depends_on": "base_total_taxes_and_charges",
"fieldname": "base_total_taxes_and_charges",
"fieldtype": "Currency",
"label": "Total Taxes and Charges (Company Currency)",
@ -665,6 +674,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "taxes_and_charges_added",
"fieldname": "taxes_and_charges_added",
"fieldtype": "Currency",
"label": "Taxes and Charges Added",
@ -675,6 +685,7 @@
"read_only": 1
},
{
"depends_on": "taxes_and_charges_deducted",
"fieldname": "taxes_and_charges_deducted",
"fieldtype": "Currency",
"label": "Taxes and Charges Deducted",
@ -685,6 +696,7 @@
"read_only": 1
},
{
"depends_on": "total_taxes_and_charges",
"fieldname": "total_taxes_and_charges",
"fieldtype": "Currency",
"label": "Total Taxes and Charges",
@ -694,7 +706,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "discount_amount",
"collapsible_depends_on": "apply_discount_on",
"fieldname": "discount_section",
"fieldtype": "Section Break",
"label": "Additional Discount"
@ -734,7 +746,8 @@
},
{
"fieldname": "totals_section",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Totals"
},
{
"fieldname": "base_grand_total",
@ -902,12 +915,12 @@
},
{
"fieldname": "ref_sq",
"fieldtype": "Data",
"hidden": 1,
"label": "Ref SQ",
"fieldtype": "Link",
"label": "Supplier Quotation",
"no_copy": 1,
"oldfieldname": "ref_sq",
"oldfieldtype": "Data",
"options": "Supplier Quotation",
"print_hide": 1,
"read_only": 1
},
@ -1061,7 +1074,7 @@
"collapsible": 1,
"fieldname": "tracking_section",
"fieldtype": "Section Break",
"label": "Tracking"
"label": "Order Status"
},
{
"fieldname": "column_break_75",
@ -1070,21 +1083,29 @@
{
"fieldname": "billing_address",
"fieldtype": "Link",
"label": "Select Billing Address",
"label": "Company Billing Address",
"options": "Address"
},
{
"fieldname": "billing_address_display",
"fieldtype": "Small Text",
"label": "Billing Address",
"label": "Billing Address Details",
"read_only": 1
},
{
"fieldname": "before_items_section",
"fieldtype": "Section Break"
},
{
"fieldname": "items_col_break",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2020-10-07 14:31:57.661221",
"modified": "2020-10-30 11:39:37.388249",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@ -24,6 +24,7 @@
"col_break2",
"uom",
"conversion_factor",
"stock_qty",
"sec_break1",
"price_list_rate",
"discount_percentage",
@ -46,11 +47,8 @@
"column_break_32",
"base_net_rate",
"base_net_amount",
"billed_amt",
"warehouse_and_reference",
"warehouse",
"delivered_by_supplier",
"project",
"material_request",
"material_request_item",
"sales_order",
@ -58,36 +56,37 @@
"supplier_quotation",
"supplier_quotation_item",
"col_break5",
"delivered_by_supplier",
"against_blanket_order",
"blanket_order",
"blanket_order_rate",
"item_group",
"brand",
"bom",
"include_exploded_items",
"section_break_56",
"stock_qty",
"column_break_60",
"received_qty",
"returned_qty",
"manufacture_details",
"manufacturer",
"column_break_14",
"manufacturer_part_no",
"more_info_section_break",
"is_fixed_asset",
"item_tax_rate",
"column_break_60",
"billed_amt",
"accounting_details",
"expense_account",
"column_break_68",
"manufacture_details",
"manufacturer",
"manufacturer_part_no",
"column_break_14",
"bom",
"include_exploded_items",
"item_weight_details",
"weight_per_unit",
"total_weight",
"column_break_40",
"weight_uom",
"accounting_dimensions_section",
"cost_center",
"project",
"dimension_col_break",
"cost_center",
"more_info_section_break",
"is_fixed_asset",
"item_tax_rate",
"section_break_72",
"page_break"
],
@ -346,6 +345,7 @@
},
{
"default": "0",
"depends_on": "is_free_item",
"fieldname": "is_free_item",
"fieldtype": "Check",
"label": "Is Free Item",
@ -508,9 +508,10 @@
},
{
"default": "0",
"depends_on": "delivered_by_supplier",
"fieldname": "delivered_by_supplier",
"fieldtype": "Check",
"label": "To be delivered to customer",
"label": "To be Delivered to Customer",
"print_hide": 1,
"read_only": 1
},
@ -558,6 +559,7 @@
"read_only": 1
},
{
"depends_on": "eval:parent.is_subcontracted == 'Yes'",
"fieldname": "bom",
"fieldtype": "Link",
"label": "BOM",
@ -574,21 +576,21 @@
},
{
"fieldname": "section_break_56",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Billed, Received & Returned"
},
{
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Qty as per Stock UOM",
"label": "Qty in Stock UOM",
"no_copy": 1,
"oldfieldname": "stock_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
"print_width": "100px",
"read_only": 1,
"width": "100px"
},
{
"depends_on": "received_qty",
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty",
@ -612,9 +614,10 @@
"fieldtype": "Column Break"
},
{
"depends_on": "billed_amt",
"fieldname": "billed_amt",
"fieldtype": "Currency",
"label": "Billed Amt",
"label": "Billed Amount",
"no_copy": 1,
"options": "currency",
"print_hide": 1,
@ -633,6 +636,7 @@
"report_hide": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details",
"fieldtype": "Section Break",
"label": "Accounting Details"
@ -644,10 +648,6 @@
"options": "Account",
"print_hide": 1
},
{
"fieldname": "column_break_68",
"fieldtype": "Column Break"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
@ -715,6 +715,7 @@
},
{
"default": "0",
"depends_on": "is_fixed_asset",
"fetch_from": "item_code.is_fixed_asset",
"fieldname": "is_fixed_asset",
"fieldtype": "Check",
@ -728,9 +729,10 @@
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-04-21 11:55:58.643393",
"modified": "2020-10-30 11:59:47.670951",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@ -217,13 +217,15 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
source_doctype: "Material Request",
target: me.frm,
setters: {
company: me.frm.doc.company
schedule_date: undefined,
status: undefined
},
get_query_filters: {
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99]
per_ordered: ["<", 99.99],
company: me.frm.doc.company
}
})
}, __("Get Items From"));
@ -236,32 +238,40 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
source_doctype: "Opportunity",
target: me.frm,
setters: {
company: me.frm.doc.company
party_name: undefined,
opportunity_from: undefined,
status: undefined
},
get_query_filters: {
status: ["not in", ["Closed", "Lost"]],
company: me.frm.doc.company
}
})
}, __("Get Items From"));
// Get items from open Material Requests based on supplier
this.frm.add_custom_button(__('Possible Supplier'), function() {
// Create a dialog window for the user to pick their supplier
var d = new frappe.ui.Dialog({
var dialog = new frappe.ui.Dialog({
title: __('Select Possible Supplier'),
fields: [
{fieldname: 'supplier', fieldtype:'Link', options:'Supplier', label:'Supplier', reqd:1},
{fieldname: 'ok_button', fieldtype:'Button', label:'Get Items from Material Requests'},
]
});
// On the user clicking the ok button
d.fields_dict.ok_button.input.onclick = function() {
var btn = d.fields_dict.ok_button.input;
var v = d.get_values();
if(v) {
$(btn).set_working();
{
fieldname: 'supplier',
fieldtype:'Link',
options:'Supplier',
label:'Supplier',
reqd:1,
description: __("Get Items from Material Requests against this Supplier")
}
],
primary_action_label: __("Get Items"),
primary_action: (args) => {
if(!args) return;
dialog.hide();
erpnext.utils.map_current_doc({
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_item_from_material_requests_based_on_supplier",
source_name: v.supplier,
source_name: args.supplier,
target: me.frm,
setters: {
company: me.frm.doc.company
@ -273,11 +283,11 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
per_ordered: ["<", 99.99]
}
});
$(btn).done_working();
d.hide();
dialog.hide();
}
}
d.show();
});
dialog.show();
}, __("Get Items From"));
// Get Suppliers
@ -304,7 +314,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
{
"fieldtype": "Select", "label": __("Get Suppliers By"),
"fieldname": "search_type",
"options": ["Tag","Supplier Group"],
"options": ["Supplier Group", "Tag"],
"reqd": 1,
onchange() {
if(dialog.get_value('search_type') == 'Tag'){

View File

@ -21,9 +21,9 @@
"link_to_mrs",
"supplier_response_section",
"salutation",
"email_template",
"col_break_email_1",
"subject",
"col_break_email_1",
"email_template",
"preview",
"sec_break_email_2",
"message_for_supplier",
@ -260,7 +260,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-10-16 17:49:09.561929",
"modified": "2020-11-04 22:04:29.017134",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@ -5,14 +5,14 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"send_email",
"email_sent",
"supplier",
"contact",
"quote_status",
"column_break_3",
"supplier_name",
"email_id"
"email_id",
"send_email",
"email_sent"
],
"fields": [
{
@ -87,7 +87,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-16 12:23:41.769820",
"modified": "2020-11-04 22:01:43.832942",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation Supplier",

View File

@ -37,16 +37,18 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
source_doctype: "Material Request",
target: me.frm,
setters: {
company: me.frm.doc.company
schedule_date: undefined,
status: undefined
},
get_query_filters: {
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99]
per_ordered: ["<", 99.99],
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__("Request for Quotation"),
function() {
@ -58,16 +60,16 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
source_doctype: "Request for Quotation",
target: me.frm,
setters: {
company: me.frm.doc.company,
transaction_date: null
},
get_query_filters: {
supplier: me.frm.doc.supplier
supplier: me.frm.doc.supplier,
company: me.frm.doc.company
},
get_query_method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_rfq_containing_supplier"
})
}, __("Get items from"));
}, __("Get Items From"));
}
},

View File

@ -1,12 +1,14 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2019-06-05 11:48:30.572795",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"communication_channel",
"communication_medium_type",
"catch_all",
"column_break_3",
"catch_all",
"provider",
"disabled",
"timeslots_section",
@ -54,9 +56,16 @@
"fieldtype": "Table",
"label": "Timeslots",
"options": "Communication Medium Timeslot"
},
{
"fieldname": "communication_channel",
"fieldtype": "Select",
"label": "Communication Channel",
"options": "\nExotel"
}
],
"modified": "2019-06-05 11:49:30.769006",
"links": [],
"modified": "2020-10-27 16:22:08.068542",
"modified_by": "Administrator",
"module": "Communication",
"name": "Communication Medium",

View File

@ -371,13 +371,27 @@ class SellingController(StockController):
self.make_sl_entries(sl_entries)
def set_po_nos(self):
if self.doctype in ("Delivery Note", "Sales Invoice") and hasattr(self, "items"):
ref_fieldname = "against_sales_order" if self.doctype == "Delivery Note" else "sales_order"
sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
if sales_orders:
po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)})
if po_nos and po_nos[0].get('po_no'):
self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no])))
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
self.set_pos_for_sales_invoice()
if self.doctype == 'Delivery Note' and hasattr(self, "items"):
self.set_pos_for_delivery_note()
def set_pos_for_sales_invoice(self):
po_nos = []
self.get_po_nos('Sales Order', 'sales_order', po_nos)
self.get_po_nos('Delivery Note', 'delivery_note', po_nos)
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
def set_pos_for_delivery_note(self):
po_nos = []
self.get_po_nos('Sales Order', 'against_sales_order', po_nos)
self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos)
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
def get_po_nos(self, ref_doctype, ref_fieldname, po_nos):
doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
if doc_list:
po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')]
def set_gross_profit(self):
if self.doctype in ["Sales Order", "Quotation"]:

View File

@ -23,8 +23,7 @@
{
"fieldname": "contract_terms",
"fieldtype": "Text Editor",
"label": "Contract Terms and Conditions",
"read_only": 1
"label": "Contract Terms and Conditions"
},
{
"fieldname": "sb_fulfilment",
@ -45,7 +44,7 @@
}
],
"links": [],
"modified": "2020-06-03 00:24:58.179816",
"modified": "2020-11-11 17:49:44.879363",
"modified_by": "Administrator",
"module": "CRM",
"name": "Contract Template",

View File

@ -6,8 +6,10 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_link_to_form, getdate
from frappe.utils import get_link_to_form, getdate, formatdate
from erpnext import get_default_company
from erpnext.education.api import get_student_group_students
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
class StudentAttendance(Document):
def validate(self):
@ -17,6 +19,7 @@ class StudentAttendance(Document):
self.set_student_group()
self.validate_student()
self.validate_duplication()
self.validate_is_holiday()
def set_date(self):
if self.course_schedule:
@ -78,3 +81,18 @@ class StudentAttendance(Document):
record = get_link_to_form('Student Attendance', attendance_record)
frappe.throw(_('Student Attendance record {0} already exists against the Student {1}')
.format(record, frappe.bold(self.student)), title=_('Duplicate Entry'))
def validate_is_holiday(self):
holiday_list = get_holiday_list()
if is_holiday(holiday_list, self.date):
frappe.throw(_('Attendance cannot be marked for {0} as it is a holiday.').format(
frappe.bold(formatdate(self.date))))
def get_holiday_list(company=None):
if not company:
company = get_default_company() or frappe.get_all('Company')[0].name
holiday_list = frappe.get_cached_value('Company', company, 'default_holiday_list')
if not holiday_list:
frappe.throw(_('Please set a default Holiday List for Company {0}').format(frappe.bold(get_default_company())))
return holiday_list

View File

@ -20,10 +20,10 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour
student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , \
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
if not student_list:
student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] ,
if not student_list:
student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] ,
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
if course_schedule:
student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \
course_schedule= %s''', (course_schedule), as_dict=1)
@ -32,7 +32,7 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour
student_group= %s and date= %s and \
(course_schedule is Null or course_schedule='')''',
(student_group, date), as_dict=1)
for attendance in student_attendance_list:
for student in student_list:
if student.student == attendance.student:

View File

@ -11,6 +11,7 @@
"column_break_3",
"from_date",
"to_date",
"total_leave_days",
"section_break_5",
"attendance_based_on",
"student_group",
@ -110,11 +111,17 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "total_leave_days",
"fieldtype": "Float",
"label": "Total Leave Days",
"read_only": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-07-08 13:22:38.329002",
"modified": "2020-09-21 18:10:24.440669",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Leave Application",

View File

@ -6,11 +6,14 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from datetime import timedelta
from frappe.utils import get_link_to_form, getdate
from frappe.utils import get_link_to_form, getdate, date_diff, flt
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from frappe.model.document import Document
class StudentLeaveApplication(Document):
def validate(self):
self.validate_holiday_list()
self.validate_duplicate()
self.validate_from_to_dates('from_date', 'to_date')
@ -39,10 +42,19 @@ class StudentLeaveApplication(Document):
frappe.throw(_('Leave application {0} already exists against the student {1}')
.format(link, frappe.bold(self.student)), title=_('Duplicate Entry'))
def validate_holiday_list(self):
holiday_list = get_holiday_list()
self.total_leave_days = get_number_of_leave_days(self.from_date, self.to_date, holiday_list)
def update_attendance(self):
holiday_list = get_holiday_list()
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime('%Y-%m-%d')
if is_holiday(holiday_list, date):
continue
attendance = frappe.db.exists('Student Attendance', {
'student': self.student,
'date': date,
@ -89,3 +101,19 @@ class StudentLeaveApplication(Document):
def daterange(start_date, end_date):
for n in range(int ((end_date - start_date).days)+1):
yield start_date + timedelta(n)
def get_number_of_leave_days(from_date, to_date, holiday_list):
number_of_days = date_diff(to_date, from_date) + 1
holidays = frappe.db.sql("""
SELECT
COUNT(DISTINCT holiday_date)
FROM `tabHoliday` h1,`tabHoliday List` h2
WHERE
h1.parent = h2.name and
h1.holiday_date between %s and %s and
h2.name = %s""", (from_date, to_date, holiday_list))[0][0]
number_of_days = flt(number_of_days) - flt(holidays)
return number_of_days

View File

@ -5,13 +5,15 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import getdate, add_days
from frappe.utils import getdate, add_days, add_months
from erpnext import get_default_company
from erpnext.education.doctype.student_group.test_student_group import get_random_group
from erpnext.education.doctype.student.test_student import create_student
class TestStudentLeaveApplication(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabStudent Leave Application`""")
create_holiday_list()
def test_attendance_record_creation(self):
leave_application = create_leave_application()
@ -35,20 +37,45 @@ class TestStudentLeaveApplication(unittest.TestCase):
attendance_status = frappe.db.get_value('Student Attendance', {'leave_application': leave_application.name}, 'docstatus')
self.assertTrue(attendance_status, 2)
def test_holiday(self):
today = getdate()
leave_application = create_leave_application(from_date=today, to_date= add_days(today, 1), submit=0)
def create_leave_application(from_date=None, to_date=None, mark_as_present=0):
# holiday list validation
company = get_default_company() or frappe.get_all('Company')[0].name
frappe.db.set_value('Company', company, 'default_holiday_list', '')
self.assertRaises(frappe.ValidationError, leave_application.save)
frappe.db.set_value('Company', company, 'default_holiday_list', 'Test Holiday List for Student')
leave_application.save()
leave_application.reload()
self.assertEqual(leave_application.total_leave_days, 1)
# check no attendance record created for a holiday
leave_application.submit()
self.assertIsNone(frappe.db.exists('Student Attendance', {'leave_application': leave_application.name, 'date': add_days(today, 1)}))
def tearDown(self):
company = get_default_company() or frappe.get_all('Company')[0].name
frappe.db.set_value('Company', company, 'default_holiday_list', '_Test Holiday List')
def create_leave_application(from_date=None, to_date=None, mark_as_present=0, submit=1):
student = get_student()
leave_application = frappe.get_doc({
'doctype': 'Student Leave Application',
'student': student.name,
'attendance_based_on': 'Student Group',
'student_group': get_random_group().name,
'from_date': from_date if from_date else getdate(),
'to_date': from_date if from_date else getdate(),
'mark_as_present': mark_as_present
}).insert()
leave_application.submit()
leave_application = frappe.new_doc('Student Leave Application')
leave_application.student = student.name
leave_application.attendance_based_on = 'Student Group'
leave_application.student_group = get_random_group().name
leave_application.from_date = from_date if from_date else getdate()
leave_application.to_date = from_date if from_date else getdate()
leave_application.mark_as_present = mark_as_present
if submit:
leave_application.insert()
leave_application.submit()
return leave_application
def create_student_attendance(date=None, status=None):
@ -67,4 +94,22 @@ def get_student():
email='test_student@gmail.com',
first_name='Test',
last_name='Student'
))
))
def create_holiday_list():
holiday_list = 'Test Holiday List for Student'
today = getdate()
if not frappe.db.exists('Holiday List', holiday_list):
frappe.get_doc(dict(
doctype = 'Holiday List',
holiday_list_name = holiday_list,
from_date = add_months(today, -6),
to_date = add_months(today, 6),
holidays = [
dict(holiday_date=add_days(today, 1), description = 'Test')
]
)).insert()
company = get_default_company() or frappe.get_all('Company')[0].name
frappe.db.set_value('Company', company, 'default_holiday_list', holiday_list)
return holiday_list

View File

@ -3,8 +3,10 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cstr, cint, getdate
from frappe.utils import formatdate
from frappe import msgprint, _
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
def execute(filters=None):
if not filters: filters = {}
@ -15,6 +17,11 @@ def execute(filters=None):
columns = get_columns(filters)
date = filters.get("date")
holiday_list = get_holiday_list()
if is_holiday(holiday_list, filters.get("date")):
msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date")))))
absent_students = get_absent_students(date)
leave_applicants = get_leave_applications(date)
if absent_students:

View File

@ -3,8 +3,10 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cstr, cint, getdate
from frappe.utils import formatdate
from frappe import msgprint, _
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
def execute(filters=None):
if not filters: filters = {}
@ -12,6 +14,10 @@ def execute(filters=None):
if not filters.get("date"):
msgprint(_("Please select date"), raise_exception=1)
holiday_list = get_holiday_list()
if is_holiday(holiday_list, filters.get("date")):
msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date")))))
columns = get_columns(filters)
active_student_group = get_active_student_group()

View File

@ -7,6 +7,8 @@ from frappe.utils import cstr, cint, getdate, get_first_day, get_last_day, date_
from frappe import msgprint, _
from calendar import monthrange
from erpnext.education.api import get_student_group_students
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from erpnext.support.doctype.issue.issue import get_holidays
def execute(filters=None):
if not filters: filters = {}
@ -19,26 +21,32 @@ def execute(filters=None):
students_list = get_students_list(students)
att_map = get_attendance_list(from_date, to_date, filters.get("student_group"), students_list)
data = []
for stud in students:
row = [stud.student, stud.student_name]
student_status = frappe.db.get_value("Student", stud.student, "enabled")
date = from_date
total_p = total_a = 0.0
for day in range(total_days_in_month):
status="None"
if att_map.get(stud.student):
status = att_map.get(stud.student).get(date, "None")
elif not student_status:
status = "Inactive"
else:
status = "None"
status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive":"-"}
status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive":"-", "Holiday":"H"}
row.append(status_map[status])
if status == "Present":
total_p += 1
elif status == "Absent":
total_a += 1
date = add_days(date, 1)
row += [total_p, total_a]
data.append(row)
return columns, data
@ -63,14 +71,19 @@ def get_attendance_list(from_date, to_date, student_group, students_list):
and date between %s and %s
order by student, date''',
(student_group, from_date, to_date), as_dict=1)
att_map = {}
students_with_leave_application = get_students_with_leave_application(from_date, to_date, students_list)
for d in attendance_list:
att_map.setdefault(d.student, frappe._dict()).setdefault(d.date, "")
if students_with_leave_application.get(d.date) and d.student in students_with_leave_application.get(d.date):
att_map[d.student][d.date] = "Present"
else:
att_map[d.student][d.date] = d.status
att_map = mark_holidays(att_map, from_date, to_date, students_list)
return att_map
def get_students_with_leave_application(from_date, to_date, students_list):
@ -108,3 +121,14 @@ def get_attendance_years():
if not year_list:
year_list = [getdate().year]
return "\n".join(str(year) for year in year_list)
def mark_holidays(att_map, from_date, to_date, students_list):
holiday_list = get_holiday_list()
holidays = get_holidays(holiday_list)
for dt in daterange(getdate(from_date), getdate(to_date)):
if dt in holidays:
for student in students_list:
att_map.setdefault(student, frappe._dict()).setdefault(dt, "Holiday")
return att_map

View File

@ -2,12 +2,13 @@ from __future__ import unicode_literals
import frappe
from frappe import _
import json
from frappe.utils import cstr, cint, nowdate, flt
from frappe.utils import cstr, cint, nowdate, getdate, flt, get_request_session, get_datetime
from erpnext.erpnext_integrations.utils import validate_webhooks_request
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import sync_item_from_shopify
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer
from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log, dump_request_data
from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import get_shopify_url, get_header
@frappe.whitelist(allow_guest=True)
@validate_webhooks_request("Shopify Settings", 'X-Shopify-Hmac-Sha256', secret_key='shared_secret')
@ -18,7 +19,7 @@ def store_request_data(order=None, event=None):
dump_request_data(order, event)
def sync_sales_order(order, request_id=None):
def sync_sales_order(order, request_id=None, old_order_sync=False):
frappe.set_user('Administrator')
shopify_settings = frappe.get_doc("Shopify Settings")
frappe.flags.request_id = request_id
@ -27,7 +28,7 @@ def sync_sales_order(order, request_id=None):
try:
validate_customer(order, shopify_settings)
validate_item(order, shopify_settings)
create_order(order, shopify_settings)
create_order(order, shopify_settings, old_order_sync=old_order_sync)
except Exception as e:
make_shopify_log(status="Error", exception=e)
@ -77,13 +78,13 @@ def validate_item(order, shopify_settings):
if item.get("product_id") and not frappe.db.get_value("Item", {"shopify_product_id": item.get("product_id")}, "name"):
sync_item_from_shopify(shopify_settings, item)
def create_order(order, shopify_settings, company=None):
def create_order(order, shopify_settings, old_order_sync=False, company=None):
so = create_sales_order(order, shopify_settings, company)
if so:
if order.get("financial_status") == "paid":
create_sales_invoice(order, shopify_settings, so)
create_sales_invoice(order, shopify_settings, so, old_order_sync=old_order_sync)
if order.get("fulfillments"):
if order.get("fulfillments") and not old_order_sync:
create_delivery_note(order, shopify_settings, so)
def create_sales_order(shopify_order, shopify_settings, company=None):
@ -92,7 +93,7 @@ def create_sales_order(shopify_order, shopify_settings, company=None):
so = frappe.db.get_value("Sales Order", {"shopify_order_id": shopify_order.get("id")}, "name")
if not so:
items = get_order_items(shopify_order.get("line_items"), shopify_settings)
items = get_order_items(shopify_order.get("line_items"), shopify_settings, getdate(shopify_order.get('created_at')))
if not items:
message = 'Following items exists in the shopify order but relevant records were not found in the shopify Product master'
@ -106,8 +107,10 @@ def create_sales_order(shopify_order, shopify_settings, company=None):
"doctype": "Sales Order",
"naming_series": shopify_settings.sales_order_series or "SO-Shopify-",
"shopify_order_id": shopify_order.get("id"),
"shopify_order_number": shopify_order.get("name"),
"customer": customer or shopify_settings.default_customer,
"delivery_date": nowdate(),
"transaction_date": getdate(shopify_order.get("created_at")) or nowdate(),
"delivery_date": getdate(shopify_order.get("created_at")) or nowdate(),
"company": shopify_settings.company,
"selling_price_list": shopify_settings.price_list,
"ignore_pricing_rule": 1,
@ -132,12 +135,20 @@ def create_sales_order(shopify_order, shopify_settings, company=None):
frappe.db.commit()
return so
def create_sales_invoice(shopify_order, shopify_settings, so):
def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=False):
if not frappe.db.get_value("Sales Invoice", {"shopify_order_id": shopify_order.get("id")}, "name")\
and so.docstatus==1 and not so.per_billed and cint(shopify_settings.sync_sales_invoice):
if old_order_sync:
posting_date = getdate(shopify_order.get('created_at'))
else:
posting_date = nowdate()
si = make_sales_invoice(so.name, ignore_permissions=True)
si.shopify_order_id = shopify_order.get("id")
si.shopify_order_number = shopify_order.get("name")
si.set_posting_time = 1
si.posting_date = posting_date
si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-"
si.flags.ignore_mandatory = True
set_cost_center(si.items, shopify_settings.cost_center)
@ -169,6 +180,9 @@ def create_delivery_note(shopify_order, shopify_settings, so):
dn = make_delivery_note(so.name)
dn.shopify_order_id = fulfillment.get("order_id")
dn.shopify_order_number = shopify_order.get("name")
dn.set_posting_time = 1
dn.posting_date = getdate(fulfillment.get("created_at"))
dn.shopify_fulfillment_id = fulfillment.get("id")
dn.naming_series = shopify_settings.delivery_note_series or "DN-Shopify-"
dn.items = get_fulfillment_items(dn.items, fulfillment.get("line_items"), shopify_settings)
@ -187,7 +201,7 @@ def get_discounted_amount(order):
discounted_amount += flt(discount.get("amount"))
return discounted_amount
def get_order_items(order_items, shopify_settings):
def get_order_items(order_items, shopify_settings, delivery_date):
items = []
all_product_exists = True
product_not_exists = []
@ -205,7 +219,7 @@ def get_order_items(order_items, shopify_settings):
"item_code": item_code,
"item_name": shopify_item.get("name"),
"rate": shopify_item.get("price"),
"delivery_date": nowdate(),
"delivery_date": delivery_date,
"qty": shopify_item.get("quantity"),
"stock_uom": shopify_item.get("uom") or _("Nos"),
"warehouse": shopify_settings.warehouse
@ -265,3 +279,64 @@ def get_tax_account_head(tax):
frappe.throw(_("Tax Account not specified for Shopify Tax {0}").format(tax.get("title")))
return tax_account
@frappe.whitelist(allow_guest=True)
def sync_old_orders():
frappe.set_user('Administrator')
shopify_settings = frappe.get_doc('Shopify Settings')
if not shopify_settings.sync_missing_orders:
return
url = get_url(shopify_settings)
session = get_request_session()
try:
res = session.get(url, headers=get_header(shopify_settings))
res.raise_for_status()
orders = res.json()["orders"]
for order in orders:
if is_sync_complete(shopify_settings, order):
stop_sync(shopify_settings)
return
sync_sales_order(order=order, old_order_sync=True)
last_order_id = order.get('id')
if last_order_id:
shopify_settings.load_from_db()
shopify_settings.last_order_id = last_order_id
shopify_settings.save()
frappe.db.commit()
except Exception as e:
raise e
def stop_sync(shopify_settings):
shopify_settings.sync_missing_orders = 0
shopify_settings.last_order_id = ''
shopify_settings.save()
frappe.db.commit()
def get_url(shopify_settings):
last_order_id = shopify_settings.last_order_id
if not last_order_id:
if shopify_settings.sync_based_on == 'Date':
url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&created_at_min={0}&since_id=0".format(
get_datetime(shopify_settings.from_date)), shopify_settings)
else:
url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&since_id={0}".format(
shopify_settings.from_order_id), shopify_settings)
else:
url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings)
return url
def is_sync_complete(shopify_settings, order):
if shopify_settings.sync_based_on == 'Date':
return getdate(shopify_settings.to_date) < getdate(order.get('created_at'))
else:
return cstr(order.get('id')) == cstr(shopify_settings.to_order_id)

View File

@ -8,7 +8,7 @@
{
"hidden": 0,
"label": "Payments",
"links": "[\n {\n \"description\": \"GoCardless payment gateway settings\",\n \"label\": \"GoCardless Settings\",\n \"name\": \"GoCardless Settings\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"description\": \"GoCardless payment gateway settings\",\n \"label\": \"GoCardless Settings\",\n \"name\": \"GoCardless Settings\",\n \"type\": \"doctype\"\n }, {\n \"description\": \"M-Pesa payment gateway settings\",\n \"label\": \"M-Pesa Settings\",\n \"name\": \"Mpesa Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -29,7 +29,7 @@
"idx": 0,
"is_standard": 1,
"label": "ERPNext Integrations",
"modified": "2020-08-23 16:30:51.494655",
"modified": "2020-10-29 19:54:46.228222",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",

View File

@ -9,11 +9,12 @@ frappe.ui.form.on('Mpesa Settings', {
refresh: function(frm) {
frappe.realtime.on("refresh_mpesa_dashboard", function(){
frm.reload_doc();
frm.events.setup_account_balance_html(frm);
});
},
get_account_balance: function(frm) {
if (!frm.initiator_name && !frm.security_credentials) {
if (!frm.doc.initiator_name && !frm.doc.security_credential) {
frappe.throw(__("Please set the initiator name and the security credential"));
}
frappe.call({

View File

@ -147,7 +147,7 @@ def get_account_balance(request_payload):
return response
except Exception:
frappe.log_error(title=_("Account Balance Processing Error"))
frappe.throw(title=_("Error"), message=_("Please check your configuration and try again"))
frappe.throw(_("Please check your configuration and try again"), title=_("Error"))
@frappe.whitelist(allow_guest=True)
def process_balance_info(**kwargs):
@ -173,7 +173,8 @@ def process_balance_info(**kwargs):
ref_doc.db_set("account_balance", balance_info)
request.handle_success(account_balance_response)
frappe.publish_realtime("refresh_mpesa_dashboard")
frappe.publish_realtime("refresh_mpesa_dashboard", doctype="Mpesa Settings",
docname=transaction_data.reference_docname, user=transaction_data.owner)
except Exception:
request.handle_failure(account_balance_response)
frappe.log_error(title=_("Mpesa Account Balance Processing Error"), message=account_balance_response)

View File

@ -31,6 +31,7 @@ class PlaidConnector():
return access_token
def get_link_token(self):
country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"]
token_request = {
"client_name": self.client_name,
"client_id": self.settings.plaid_client_id,
@ -38,7 +39,7 @@ class PlaidConnector():
"products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"],
"country_codes": country_codes,
"user": {
"client_user_id": frappe.generate_hash(frappe.session.user, length=32)
}

View File

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2018-10-25 10:02:48.656165",
"doctype": "DocType",
"editable_grid": 1,
@ -11,7 +12,8 @@
"plaid_client_id",
"plaid_secret",
"column_break_7",
"plaid_env"
"plaid_env",
"enable_european_access"
],
"fields": [
{
@ -58,10 +60,17 @@
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "enable_european_access",
"fieldtype": "Check",
"label": "Enable European Access"
}
],
"issingle": 1,
"modified": "2020-09-12 02:31:44.542385",
"links": [],
"modified": "2020-10-29 20:24:56.916104",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Plaid Settings",

View File

@ -1,7 +1,9 @@
{
"actions": [],
"creation": "2015-05-18 05:21:07.270859",
"doctype": "DocType",
"document_type": "System",
"engine": "InnoDB",
"field_order": [
"status_html",
"enable_shopify",
@ -40,7 +42,16 @@
"sales_invoice_series",
"section_break_22",
"html_16",
"taxes"
"taxes",
"syncing_details_section",
"sync_missing_orders",
"sync_based_on",
"column_break_41",
"from_date",
"to_date",
"from_order_id",
"to_order_id",
"last_order_id"
],
"fields": [
{
@ -255,10 +266,71 @@
"fieldtype": "Table",
"label": "Shopify Tax Account",
"options": "Shopify Tax Account"
},
{
"collapsible": 1,
"fieldname": "syncing_details_section",
"fieldtype": "Section Break",
"label": "Syncing Missing Orders"
},
{
"depends_on": "eval:doc.sync_missing_orders",
"fieldname": "last_order_id",
"fieldtype": "Data",
"label": "Last Order Id",
"read_only": 1
},
{
"fieldname": "column_break_41",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "On checking this Order from the ",
"fieldname": "sync_missing_orders",
"fieldtype": "Check",
"label": "Sync Missing Old Shopify Orders"
},
{
"depends_on": "eval:doc.sync_missing_orders",
"fieldname": "sync_based_on",
"fieldtype": "Select",
"label": "Sync Based On",
"mandatory_depends_on": "eval:doc.sync_missing_orders",
"options": "\nDate\nShopify Order Id"
},
{
"depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders"
},
{
"depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders",
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders"
},
{
"depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders",
"fieldname": "from_order_id",
"fieldtype": "Data",
"label": "From Order Id",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders"
},
{
"depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders",
"fieldname": "to_order_id",
"fieldtype": "Data",
"label": "To Order Id",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders"
}
],
"issingle": 1,
"modified": "2020-09-18 17:26:09.703215",
"links": [],
"modified": "2020-11-05 20:44:03.664891",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Shopify Settings",
@ -277,4 +349,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

@ -87,7 +87,7 @@ def get_shopify_url(path, settings):
def get_header(settings):
header = {'Content-Type': 'application/json'}
return header;
return header
@frappe.whitelist()
def get_series():
@ -121,17 +121,23 @@ def setup_custom_fields():
],
"Sales Order": [
dict(fieldname='shopify_order_id', label='Shopify Order Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1)
fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
dict(fieldname='shopify_order_number', label='Shopify Order Number',
fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1)
],
"Delivery Note":[
dict(fieldname='shopify_order_id', label='Shopify Order Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
dict(fieldname='shopify_order_number', label='Shopify Order Number',
fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1),
dict(fieldname='shopify_fulfillment_id', label='Shopify Fulfillment Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1)
],
"Sales Invoice": [
dict(fieldname='shopify_order_id', label='Shopify Order Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1)
fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
dict(fieldname='shopify_order_number', label='Shopify Order Number',
fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1)
]
}

View File

@ -58,7 +58,7 @@ class ShopifySettings(unittest.TestCase):
}).save(ignore_permissions=True)
self.shopify_settings = shopify_settings
def test_order(self):
### Create Customer ###
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer:
@ -75,7 +75,7 @@ class ShopifySettings(unittest.TestCase):
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order:
shopify_order = json.load(shopify_order)
create_order(shopify_order.get("order"), self.shopify_settings, "_Test Company")
create_order(shopify_order.get("order"), self.shopify_settings, False, company="_Test Company")
sales_order = frappe.get_doc("Sales Order", {"shopify_order_id": cstr(shopify_order.get("order").get("id"))})

View File

@ -37,7 +37,8 @@
"depends_on": "eval:doc.parenttype==\"Therapy\";",
"fieldname": "counts_completed",
"fieldtype": "Int",
"label": "Counts Completed"
"label": "Counts Completed",
"no_copy": 1
},
{
"fieldname": "assistance_level",
@ -48,7 +49,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-04-10 13:41:06.662351",
"modified": "2020-11-04 18:20:25.583491",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Exercise",

View File

@ -21,6 +21,19 @@ frappe.ui.form.on('Inpatient Medication Entry', {
}
};
});
frm.set_query('warehouse', () => {
return {
filters: {
company: frm.doc.company
}
};
});
},
patient: function(frm) {
if (frm.doc.patient)
frm.set_value('service_unit', '');
},
get_medication_orders: function(frm) {

View File

@ -67,6 +67,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.__islocal",
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
@ -93,6 +94,7 @@
"options": "Patient"
},
{
"depends_on": "eval:!doc.patient",
"fieldname": "service_unit",
"fieldtype": "Link",
"label": "Healthcare Service Unit",
@ -178,7 +180,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-30 23:40:45.528715",
"modified": "2020-11-03 13:22:37.820707",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Entry",

View File

@ -199,6 +199,7 @@ class InpatientMedicationEntry(Document):
def get_pending_medication_orders(entry):
filters, values = get_filters(entry)
to_remove = []
data = frappe.db.sql("""
SELECT
@ -225,7 +226,10 @@ def get_pending_medication_orders(entry):
doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record)
if entry.service_unit and doc.service_unit != entry.service_unit:
data.remove(doc)
to_remove.append(doc)
for doc in to_remove:
data.remove(doc)
return data

View File

@ -12,7 +12,8 @@ frappe.ui.form.on('Inpatient Medication Order', {
frm.set_query('patient', () => {
return {
filters: {
'inpatient_record': ['!=', '']
'inpatient_record': ['!=', ''],
'inpatient_status': 'Admitted'
}
};
});

View File

@ -13,43 +13,42 @@ frappe.ui.form.on('Therapy Plan', {
refresh: function(frm) {
if (!frm.doc.__islocal) {
frm.trigger('show_progress_for_therapies');
}
if (!frm.doc.__islocal && frm.doc.status != 'Completed') {
let therapy_types = (frm.doc.therapy_plan_details || []).map(function(d){ return d.therapy_type });
const fields = [{
fieldtype: 'Link',
label: __('Therapy Type'),
fieldname: 'therapy_type',
options: 'Therapy Type',
reqd: 1,
get_query: function() {
return {
filters: { 'therapy_type': ['in', therapy_types]}
if (frm.doc.status != 'Completed') {
let therapy_types = (frm.doc.therapy_plan_details || []).map(function(d){ return d.therapy_type; });
const fields = [{
fieldtype: 'Link',
label: __('Therapy Type'),
fieldname: 'therapy_type',
options: 'Therapy Type',
reqd: 1,
get_query: function() {
return {
filters: { 'therapy_type': ['in', therapy_types]}
};
}
}
}];
}];
frm.add_custom_button(__('Therapy Session'), function() {
frappe.prompt(fields, data => {
frappe.call({
method: 'erpnext.healthcare.doctype.therapy_plan.therapy_plan.make_therapy_session',
args: {
therapy_plan: frm.doc.name,
patient: frm.doc.patient,
therapy_type: data.therapy_type,
company: frm.doc.company
},
freeze: true,
callback: function(r) {
if (r.message) {
frappe.model.sync(r.message);
frappe.set_route('Form', r.message.doctype, r.message.name);
frm.add_custom_button(__('Therapy Session'), function() {
frappe.prompt(fields, data => {
frappe.call({
method: 'erpnext.healthcare.doctype.therapy_plan.therapy_plan.make_therapy_session',
args: {
therapy_plan: frm.doc.name,
patient: frm.doc.patient,
therapy_type: data.therapy_type,
company: frm.doc.company
},
freeze: true,
callback: function(r) {
if (r.message) {
frappe.model.sync(r.message);
frappe.set_route('Form', r.message.doctype, r.message.name);
}
}
}
});
}, __('Select Therapy Type'), __('Create'));
}, __('Create'));
});
}, __('Select Therapy Type'), __('Create'));
}, __('Create'));
}
if (frm.doc.therapy_plan_template && !frm.doc.invoiced) {
frm.add_custom_button(__('Sales Invoice'), function() {

View File

@ -115,7 +115,8 @@
"fieldname": "therapy_plan_template",
"fieldtype": "Link",
"label": "Therapy Plan Template",
"options": "Therapy Plan Template"
"options": "Therapy Plan Template",
"set_only_once": 1
},
{
"default": "0",
@ -128,7 +129,7 @@
}
],
"links": [],
"modified": "2020-10-23 01:27:42.128855",
"modified": "2020-11-04 18:13:13.564999",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Plan",

View File

@ -30,12 +30,13 @@
"fieldname": "sessions_completed",
"fieldtype": "Int",
"label": "Sessions Completed",
"no_copy": 1,
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-10-08 01:17:34.778028",
"modified": "2020-11-04 18:15:52.173450",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Plan Detail",

View File

@ -22,6 +22,10 @@ frappe.ui.form.on('Therapy Session', {
},
refresh: function(frm) {
if (frm.doc.therapy_plan) {
frm.trigger('filter_therapy_types');
}
if (!frm.doc.__islocal) {
frm.dashboard.add_indicator(__('Counts Targeted: {0}', [frm.doc.total_counts_targeted]), 'blue');
frm.dashboard.add_indicator(__('Counts Completed: {0}', [frm.doc.total_counts_completed]),
@ -36,15 +40,43 @@ frappe.ui.form.on('Therapy Session', {
})
}, 'Create');
frm.add_custom_button(__('Sales Invoice'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.invoice_therapy_session',
frm: frm,
})
}, 'Create');
frappe.db.get_value('Therapy Plan', {'name': frm.doc.therapy_plan}, 'therapy_plan_template', (r) => {
if (r && !r.therapy_plan_template) {
frm.add_custom_button(__('Sales Invoice'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.invoice_therapy_session',
frm: frm,
});
}, 'Create');
}
});
}
},
therapy_plan: function(frm) {
if (frm.doc.therapy_plan) {
frm.trigger('filter_therapy_types');
}
},
filter_therapy_types: function(frm) {
frappe.call({
'method': 'frappe.client.get',
args: {
doctype: 'Therapy Plan',
name: frm.doc.therapy_plan
},
callback: function(data) {
let therapy_types = (data.message.therapy_plan_details || []).map(function(d){ return d.therapy_type; });
frm.set_query('therapy_type', function() {
return {
filters: { 'therapy_type': ['in', therapy_types]}
};
});
}
});
},
patient: function(frm) {
if (frm.doc.patient) {
frappe.call({
@ -98,19 +130,6 @@ frappe.ui.form.on('Therapy Session', {
frm.set_value(values);
}
});
} else {
let values = {
'patient': '',
'therapy_type': '',
'therapy_plan': '',
'practitioner': '',
'department': '',
'start_date': '',
'start_time': '',
'service_unit': '',
'duration': ''
};
frm.set_value(values);
}
},

View File

@ -194,6 +194,7 @@
"fieldname": "total_counts_completed",
"fieldtype": "Int",
"label": "Total Counts Completed",
"no_copy": 1,
"read_only": 1
},
{
@ -222,7 +223,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-10-22 23:10:21.178644",
"modified": "2020-11-04 18:14:25.999939",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Session",

View File

@ -15,10 +15,10 @@ app_logo_url = '/assets/erpnext/images/erp-icon.svg'
develop_version = '13.x.x-develop'
app_include_js = "assets/js/erpnext.min.js"
app_include_css = "assets/css/erpnext.css"
web_include_js = "assets/js/erpnext-web.min.js"
web_include_css = "assets/css/erpnext-web.css"
app_include_js = "/assets/js/erpnext.min.js"
app_include_css = "/assets/css/erpnext.css"
web_include_js = "/assets/js/erpnext-web.min.js"
web_include_css = "/assets/css/erpnext-web.css"
doctype_js = {
"Address": "public/js/address.js",
@ -307,6 +307,7 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
],
"daily": [
"erpnext.stock.reorder_item.reorder_item",

View File

@ -32,7 +32,7 @@ class LeaveEncashment(Document):
additional_salary.employee = self.employee
earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component")
if not earning_component:
frappe.throw(_("Please set Earning Component for Leave type: {0}.".format(self.leave_type)))
frappe.throw(_("Please set Earning Component for Leave type: {0}.").format(self.leave_type))
additional_salary.salary_component = earning_component
additional_salary.payroll_date = self.encashment_date
additional_salary.amount = self.encashment_amount
@ -98,7 +98,11 @@ class LeaveEncashment(Document):
create_leave_ledger_entry(self, args, submit)
# create reverse entry for expired leaves
to_date = self.get_leave_allocation().get('to_date')
leave_allocation = self.get_leave_allocation()
if not leave_allocation:
return
to_date = leave_allocation.get('to_date')
if to_date < getdate(nowdate()):
args = frappe._dict(
leaves=self.encashable_days,

View File

@ -24,10 +24,10 @@ erpnext.hr.AttendanceControlPanel = frappe.ui.form.Controller.extend({
}
window.location.href = repl(frappe.request.url +
'?cmd=%(cmd)s&from_date=%(from_date)s&to_date=%(to_date)s', {
cmd: "erpnext.hr.doctype.upload_attendance.upload_attendance.get_template",
from_date: this.frm.doc.att_fr_date,
to_date: this.frm.doc.att_to_date,
});
cmd: "erpnext.hr.doctype.upload_attendance.upload_attendance.get_template",
from_date: this.frm.doc.att_fr_date,
to_date: this.frm.doc.att_to_date,
});
},
show_upload() {

View File

@ -28,7 +28,12 @@ def get_template():
w = UnicodeWriter()
w = add_header(w)
w = add_data(w, args)
try:
w = add_data(w, args)
except Exception as e:
frappe.clear_messages()
frappe.respond_as_web_page("Holiday List Missing", html=e)
return
# write out response as a type csv
frappe.response['result'] = cstr(w.getvalue())

View File

@ -3,7 +3,7 @@
{
"hidden": 0,
"label": "Loan",
"links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n { \"dependencies\": [\n \"Loan Type\"\n ],\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -13,7 +13,7 @@
{
"hidden": 0,
"label": "Disbursement and Repayment",
"links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Write Off\",\n \"name\": \"Loan Write Off\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -34,10 +34,11 @@
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
"hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Loan",
"modified": "2020-06-07 19:42:14.947902",
"modified": "2020-10-17 12:59:50.336085",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@ -7,10 +7,14 @@ 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') }
'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') },
'Loan Write Off': function() { frm.trigger('make_loan_write_off_entry') }
}
},
onload: function (frm) {
// Ignore loan security pledge on cancel of loan
frm.ignore_doctypes_on_cancel_all = ["Loan Security Pledge"];
frm.set_query("loan_application", function () {
return {
"filters": {
@ -21,6 +25,14 @@ frappe.ui.form.on('Loan', {
};
});
frm.set_query("loan_type", function () {
return {
"filters": {
"docstatus": 1
}
};
});
$.each(["penalty_income_account", "interest_income_account"], function(i, field) {
frm.set_query(field, function () {
return {
@ -49,24 +61,33 @@ frappe.ui.form.on('Loan', {
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
if (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') {
if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
frm.add_custom_button(__('Request Loan Closure'), function() {
frm.trigger("request_loan_closure");
},__('Status'));
frm.add_custom_button(__('Loan Repayment'), function() {
frm.trigger("make_repayment_entry");
},__('Create'));
}
if (["Sanctioned", "Partially Disbursed"].includes(frm.doc.status)) {
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'));
}
if (["Loan Closure Requested", "Disbursed", "Partially Disbursed"].includes(frm.doc.status)) {
frm.add_custom_button(__('Loan Write Off'), function() {
frm.trigger("make_loan_write_off_entry");
},__('Create'));
}
}
frm.trigger("toggle_fields");
},
@ -117,6 +138,38 @@ frappe.ui.form.on('Loan', {
})
},
make_loan_write_off_entry: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"company": frm.doc.company,
"as_dict": 1
},
method: "erpnext.loan_management.doctype.loan.loan.make_loan_write_off",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
request_loan_closure: function(frm) {
frappe.confirm(__("Do you really want to close this loan"),
function() {
frappe.call({
args: {
'loan': frm.doc.name
},
method: "erpnext.loan_management.doctype.loan.loan.request_loan_closure",
callback: function() {
frm.reload_doc();
}
});
}
);
},
create_loan_security_unpledge: function(frm) {
frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.unpledge_security",

View File

@ -43,6 +43,7 @@
"section_break_17",
"total_payment",
"total_principal_paid",
"written_off_amount",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
@ -75,6 +76,7 @@
"fieldname": "loan_application",
"fieldtype": "Link",
"label": "Loan Application",
"no_copy": 1,
"options": "Loan Application"
},
{
@ -134,6 +136,7 @@
"fieldname": "loan_amount",
"fieldtype": "Currency",
"label": "Loan Amount",
"non_negative": 1,
"options": "Company:company:default_currency"
},
{
@ -148,7 +151,8 @@
"depends_on": "eval:doc.status==\"Disbursed\"",
"fieldname": "disbursement_date",
"fieldtype": "Date",
"label": "Disbursement Date"
"label": "Disbursement Date",
"no_copy": 1
},
{
"depends_on": "is_term_loan",
@ -252,6 +256,7 @@
"fieldname": "total_payment",
"fieldtype": "Currency",
"label": "Total Payable Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@ -265,6 +270,7 @@
"fieldname": "total_interest_payable",
"fieldtype": "Currency",
"label": "Total Interest Payable",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@ -273,6 +279,7 @@
"fieldname": "total_amount_paid",
"fieldtype": "Currency",
"label": "Total Amount Paid",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@ -289,8 +296,7 @@
"default": "0",
"fieldname": "is_secured_loan",
"fieldtype": "Check",
"label": "Is Secured Loan",
"read_only": 1
"label": "Is Secured Loan"
},
{
"default": "0",
@ -313,6 +319,7 @@
"fieldname": "total_principal_paid",
"fieldtype": "Currency",
"label": "Total Principal Paid",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@ -320,6 +327,7 @@
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@ -328,13 +336,23 @@
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"label": "Maximum Loan Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "written_off_amount",
"fieldtype": "Currency",
"label": "Written Off Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-08-01 12:36:11.255233",
"modified": "2020-11-05 10:04:00.762975",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@ -9,6 +9,7 @@ from frappe import _
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
class Loan(AccountsController):
def validate(self):
@ -137,9 +138,12 @@ class Loan(AccountsController):
})
def unlink_loan_security_pledge(self):
frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
loan = '', status = 'Unpledged'
where name = %s """, (self.loan_security_pledge))
pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name})
pledge_list = [d.name for d in pledges]
if pledge_list:
frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
loan = '', status = 'Unpledged'
where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) #nosec
def update_total_amount_paid(doc):
total_amount_paid = 0
@ -182,6 +186,24 @@ def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest
monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
return monthly_repayment_amount
@frappe.whitelist()
def request_loan_closure(loan, posting_date=None):
if not posting_date:
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
pending_amount = amounts['payable_amount'] + amounts['unaccrued_interest']
loan_type = frappe.get_value('Loan', loan, 'loan_type')
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
# checking greater than 0 as there may be some minor precision error
if pending_amount < write_off_limit:
# update status as loan closure requested
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
else:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
@frappe.whitelist()
def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application)
@ -200,6 +222,7 @@ def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amo
disbursement_entry.applicant = applicant
disbursement_entry.company = company
disbursement_entry.disbursement_date = nowdate()
disbursement_entry.posting_date = nowdate()
disbursement_entry.disbursed_amount = pending_amount
if as_dict:
@ -222,6 +245,38 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as
else:
return repayment_entry
@frappe.whitelist()
def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict=0):
if not company:
company = frappe.get_value('Loan', loan, 'company')
if not posting_date:
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
pending_amount = amounts['pending_principal_amount']
if amount and (amount > pending_amount):
frappe.throw('Write Off amount cannot be greater than pending loan amount')
if not amount:
amount = pending_amount
# get default write off account from company master
write_off_account = frappe.get_value('Company', company, 'write_off_account')
write_off = frappe.new_doc('Loan Write Off')
write_off.loan = loan
write_off.posting_date = posting_date
write_off.write_off_account = write_off_account
write_off.write_off_amount = amount
write_off.save()
if as_dict:
return write_off.as_dict()
else:
return write_off
@frappe.whitelist()
def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0):
# if loan is passed it will be considered as full unpledge

View File

@ -13,7 +13,7 @@ def get_data():
'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement']
},
{
'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Security Unpledge']
'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off', 'Loan Security Unpledge']
}
]
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.listview_settings['Loan'] = {
get_indicator: function(doc) {
var status_color = {
"Draft": "red",
"Sanctioned": "blue",
"Disbursed": "orange",
"Partially Disbursed": "yellow",
"Loan Closure Requested": "green",
"Closed": "green"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
},
};

View File

@ -14,7 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_
process_loan_interest_accrual_for_term_loans)
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall
from erpnext.loan_management.doctype.loan.loan import unpledge_security
from erpnext.loan_management.doctype.loan.loan import unpledge_security, request_loan_closure, make_loan_write_off
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount
@ -132,7 +132,7 @@ class TestLoan(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
@ -142,30 +142,30 @@ class TestLoan(unittest.TestCase):
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)
accrued_interest_amount = flt((loan.loan_amount * loan.rate_of_interest * no_of_days)
/ (days_in_year(get_datetime(first_date).year) * 100), 2)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111119)
repayment_entry.save()
repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
penalty_amount = (accrued_interest_amount * 5 * 25) / 100
self.assertEquals(flt(repayment_entry.penalty_amount,0), flt(penalty_amount, 0))
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount'])
loan.load_from_db()
self.assertEquals(amounts[0], repayment_entry.interest_payable)
self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid -
penalty_amount - amounts[0], 2))
total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount']
self.assertEquals(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable)
self.assertEquals(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid -
penalty_amount - total_interest_paid, 0))
def test_loan_closure_repayment(self):
def test_loan_closure(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
@ -174,7 +174,7 @@ class TestLoan(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
@ -184,10 +184,10 @@ class TestLoan(unittest.TestCase):
no_of_days = date_diff(last_date, first_date) + 1
# Adding 6 since repayment is made 5 days late after due date
# Adding 5 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
# 5 days as well though in grace period
no_of_days += 5
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
@ -195,15 +195,17 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2))
self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
request_loan_closure(loan.name)
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
@ -230,8 +232,7 @@ class TestLoan(unittest.TestCase):
process_loan_interest_accrual_for_term_loans(posting_date=nowdate())
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5),
"Regular Payment", 89768.75)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75)
repayment_entry.submit()
@ -281,7 +282,7 @@ class TestLoan(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
@ -291,7 +292,7 @@ class TestLoan(unittest.TestCase):
no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 6
no_of_days += 5
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
@ -299,10 +300,10 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
request_loan_closure(loan.name)
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
@ -317,9 +318,9 @@ class TestLoan(unittest.TestCase):
self.assertEqual(loan.status, 'Closed')
self.assertEquals(sum(pledged_qty.values()), 0)
amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
self.assertEqual(amounts['pending_principal_amount'], 0)
self.assertEqual(amounts['payable_principal_amount'], 0)
amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertTrue(amounts['pending_principal_amount'] < 0)
self.assertEquals(amounts['payable_principal_amount'], 0.0)
self.assertEqual(amounts['interest_amount'], 0)
def test_disbursal_check_with_shortfall(self):
@ -381,7 +382,7 @@ class TestLoan(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
@ -391,7 +392,7 @@ class TestLoan(unittest.TestCase):
no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 6
no_of_days += 5
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
@ -399,20 +400,192 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
request_loan_closure(loan.name)
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
self.assertEquals(amounts['pending_principal_amount'], 0.0)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertTrue(amounts['pending_principal_amount'] < 0.0)
def test_partial_unaccrued_interest_payment(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
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
no_of_days += 5.5
# get partial unaccrued interest amount
paid_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_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
paid_amount)
repayment_entry.submit()
repayment_entry.load_from_db()
partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) \
/ (days_in_year(get_datetime(first_date).year) * 100)
interest_amount = flt(amounts['interest_amount'] + partial_accrued_interest_amount, 2)
self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0))
def test_penalty(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
paid_amount = amounts['interest_amount']/2
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
paid_amount)
repayment_entry.submit()
# 30 days - grace period
penalty_days = 30 - 4
penalty_applicable_amount = flt(amounts['interest_amount']/2, 2)
penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2)
process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30')
calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual',
{'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount')
self.assertEquals(calculated_penalty_amount, penalty_amount)
def test_loan_write_off_limit(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
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
no_of_days += 5
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_for_demand_loans(posting_date = last_date)
# repay 50 less so that it can be automatically written off
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
flt(loan.loan_amount + accrued_interest_amount - 50))
repayment_entry.submit()
amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEquals(flt(amounts['pending_principal_amount'], 0), 50)
request_loan_closure(loan.name)
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
def test_loan_amount_write_off(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
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
no_of_days += 5
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_for_demand_loans(posting_date = last_date)
# repay 100 less so that it can be automatically written off
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
flt(loan.loan_amount + accrued_interest_amount - 100))
repayment_entry.submit()
amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEquals(flt(amounts['pending_principal_amount'], 0), 100)
we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount'])
we.submit()
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0)
def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):
@ -496,7 +669,8 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i
"interest_income_account": interest_income_account,
"penalty_income_account": penalty_income_account,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods
"repayment_periods": repayment_periods,
"write_off_amount": 100
}).insert()
loan_type.submit()
@ -532,7 +706,7 @@ def create_loan_security():
"haircut": 50.00,
}).insert(ignore_permissions=True)
def create_loan_security_pledge(applicant, pledges, loan_application):
def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=None):
lsp = frappe.new_doc("Loan Security Pledge")
lsp.applicant_type = 'Customer'
@ -540,11 +714,13 @@ def create_loan_security_pledge(applicant, pledges, loan_application):
lsp.company = "_Test Company"
lsp.loan_application = loan_application
if loan:
lsp.loan = loan
for pledge in pledges:
lsp.append('securities', {
"loan_security": pledge['loan_security'],
"qty": pledge['qty'],
"haircut": pledge['haircut']
"qty": pledge['qty']
})
lsp.save()
@ -582,12 +758,11 @@ def create_loan_security_price(loan_security, loan_security_price, uom, from_dat
"valid_upto": to_date
}).insert(ignore_permissions=True)
def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amount):
def create_repayment_entry(loan, applicant, posting_date, 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,

View File

@ -127,6 +127,7 @@ def create_loan(source_name, target_doc=None, submit=0):
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
target_doc.loan_application = source_name
doclist = get_mapped_doc("Loan Application", source_name, {

View File

@ -26,19 +26,24 @@
{
"fieldname": "against_loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Against Loan ",
"options": "Loan"
"options": "Loan",
"reqd": 1
},
{
"fieldname": "disbursement_date",
"fieldtype": "Date",
"label": "Disbursement Date"
"label": "Disbursement Date",
"reqd": 1
},
{
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
"options": "Company:company:default_currency"
"non_negative": 1,
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "amended_from",
@ -53,17 +58,21 @@
"fetch_from": "against_loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "against_loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Applicant",
"options": "applicant_type",
"read_only": 1
"read_only": 1,
"reqd": 1
},
{
"collapsible": 1,
@ -102,9 +111,11 @@
"fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
"read_only": 1,
"reqd": 1
},
{
"fieldname": "bank_account",
@ -117,9 +128,10 @@
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-04-29 05:20:41.629911",
"modified": "2020-11-06 10:04:30.882322",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",

View File

@ -17,6 +17,7 @@ class LoanDisbursement(AccountsController):
def validate(self):
self.set_missing_values()
self.validate_disbursal_amount()
def on_submit(self):
self.set_status_and_amounts()
@ -40,57 +41,21 @@ class LoanDisbursement(AccountsController):
if not self.bank_account and self.applicant_type == "Customer":
self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
def set_status_and_amounts(self, cancel=0):
def validate_disbursal_amount(self):
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
if self.disbursed_amount > possible_disbursal_amount:
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
def set_status_and_amounts(self, cancel=0):
loan_details = frappe.get_all("Loan",
fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable",
"status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0]
if cancel:
disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount
total_payment = loan_details.total_payment
if loan_details.disbursed_amount > loan_details.loan_amount:
topup_amount = loan_details.disbursed_amount - loan_details.loan_amount
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment - topup_amount
if disbursed_amount == 0:
status = "Sanctioned"
elif disbursed_amount >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
disbursed_amount, status, total_payment = self.get_values_on_cancel(loan_details)
else:
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
total_payment = loan_details.total_payment
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
if self.disbursed_amount > possible_disbursal_amount:
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
if loan_details.status == "Disbursed" and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
loan=self.against_loan)
if disbursed_amount > loan_details.loan_amount:
topup_amount = disbursed_amount - loan_details.loan_amount
if topup_amount < 0:
topup_amount = 0
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment + topup_amount
if flt(disbursed_amount) >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
disbursed_amount, status, total_payment = self.get_values_on_submit(loan_details)
frappe.db.set_value("Loan", self.against_loan, {
"disbursement_date": self.disbursement_date,
@ -99,6 +64,53 @@ class LoanDisbursement(AccountsController):
"total_payment": total_payment
})
def get_values_on_cancel(self, loan_details):
disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount
total_payment = loan_details.total_payment
if loan_details.disbursed_amount > loan_details.loan_amount:
topup_amount = loan_details.disbursed_amount - loan_details.loan_amount
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment - topup_amount
if disbursed_amount == 0:
status = "Sanctioned"
elif disbursed_amount >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
return disbursed_amount, status, total_payment
def get_values_on_submit(self, loan_details):
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
total_payment = loan_details.total_payment
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
loan=self.against_loan, accrual_type="Disbursement")
if disbursed_amount > loan_details.loan_amount:
topup_amount = disbursed_amount - loan_details.loan_amount
if topup_amount < 0:
topup_amount = 0
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment + topup_amount
if flt(disbursed_amount) >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
return disbursed_amount, status, total_payment
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
@ -111,7 +123,7 @@ class LoanDisbursement(AccountsController):
"debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": "Against Loan:" + self.against_loan,
"remarks": _("Disbursement against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
@ -127,10 +139,8 @@ class LoanDisbursement(AccountsController):
"credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": "Against Loan:" + self.against_loan,
"remarks": _("Disbursement against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": self.disbursement_date
})
)
@ -155,7 +165,8 @@ def get_total_pledged_security_value(loan):
pledged_securities = get_pledged_security_qty(loan)
for security, qty in pledged_securities.items():
security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100
after_haircut_percentage = 100 - hair_cut_map.get(security)
security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage)/100
return security_value
@ -173,7 +184,8 @@ def get_disbursal_amount(loan):
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid)
else:
pending_principal_amount = flt(loan_details.disbursed_amount)
pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid)
security_value = 0.0
if loan_details.is_secured_loan:
@ -184,6 +196,9 @@ def get_disbursal_amount(loan):
disbursal_amount = flt(security_value) - flt(pending_principal_amount)
if loan_details.is_term_loan and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount:
disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount
return disbursal_amount

View File

@ -8,9 +8,10 @@ from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_la
from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, create_loan_application,
make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_security_price)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year, get_per_day_interest
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
class TestLoanDisbursement(unittest.TestCase):
@ -60,8 +61,7 @@ class TestLoanDisbursement(unittest.TestCase):
self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name,
500000, first_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5),
"Regular Payment", 611095.89)
repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89)
repayment_entry.submit()
loan.reload()
@ -69,3 +69,50 @@ class TestLoanDisbursement(unittest.TestCase):
# After repayment loan disbursement entry should go through
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16))
# check for disbursement accrual
loan_interest_accrual = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name,
'accrual_type': 'Disbursement'})
self.assertTrue(loan_interest_accrual)
def test_loan_topup_with_additional_pledge(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
# Disbursed 10,00,000 amount
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
previous_interest = amounts['interest_amount']
pledge1 = [{
"loan_security": "Test Security 1",
"qty": 2000.00
}]
create_loan_security_pledge(self.applicant, pledge1, loan=loan.name)
# Topup 500000
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 1))
process_loan_interest_accrual_for_demand_loans(posting_date = add_days(last_date, 15))
amounts = calculate_amounts(loan.name, add_days(last_date, 15))
per_day_interest = get_per_day_interest(1500000, 13.5, '2019-10-30')
interest = per_day_interest * 15
self.assertEquals(amounts['pending_principal_amount'], 1500000)
self.assertEquals(amounts['interest_amount'], flt(interest + previous_interest, 2))

View File

@ -14,6 +14,7 @@
"column_break_4",
"company",
"posting_date",
"accrual_type",
"is_term_loan",
"section_break_7",
"pending_principal_amount",
@ -22,9 +23,11 @@
"column_break_14",
"interest_amount",
"paid_interest_amount",
"penalty_amount",
"section_break_15",
"process_loan_interest_accrual",
"repayment_schedule_name",
"last_accrual_date",
"amended_from"
],
"fields": [
@ -139,6 +142,7 @@
"read_only": 1
},
{
"depends_on": "eval:doc.is_term_loan",
"fieldname": "paid_principal_amount",
"fieldtype": "Currency",
"label": "Paid Principal Amount",
@ -149,12 +153,32 @@
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "accrual_type",
"fieldtype": "Select",
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement"
},
{
"fieldname": "penalty_amount",
"fieldtype": "Currency",
"label": "Penalty Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "last_accrual_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Last Accrual Date",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-04-16 11:24:23.258404",
"modified": "2020-11-07 05:49:25.448875",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",

View File

@ -22,6 +22,8 @@ class LoanInterestAccrual(AccountsController):
if not self.interest_amount and not self.payable_principal_amount:
frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
if not self.last_accrual_date:
self.last_accrual_date = get_last_accrual_date(self.loan)
def on_submit(self):
self.make_gl_entries()
@ -50,7 +52,8 @@ class LoanInterestAccrual(AccountsController):
"debit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
"cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date
})
@ -59,14 +62,13 @@ class LoanInterestAccrual(AccountsController):
gle_map.append(
self.get_gl_dict({
"account": self.interest_income_account,
"party_type": self.applicant_type,
"party": self.applicant,
"against": self.loan_account,
"credit": self.interest_amount,
"credit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
"cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date
})
@ -79,19 +81,23 @@ class LoanInterestAccrual(AccountsController):
# For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and
# rate of interest is 13.5 then first loan interest accural will be on '01-10-2019'
# which means interest will be accrued for 30 days which should be equal to 11095.89
def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest):
def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type):
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
no_of_days = get_no_of_days_for_interest_accural(loan, posting_date)
precision = cint(frappe.db.get_default("currency_precision")) or 2
if no_of_days <= 0:
return
if loan.status == 'Disbursed':
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
else:
pending_principal_amount = loan.disbursed_amount
pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)
interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
payable_interest = interest_per_day * no_of_days
args = frappe._dict({
@ -102,13 +108,16 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
'loan_account': loan.loan_account,
'pending_principal_amount': pending_principal_amount,
'interest_amount': payable_interest,
'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'],
'process_loan_interest': process_loan_interest,
'posting_date': posting_date
'posting_date': posting_date,
'accrual_type': accrual_type
})
make_loan_interest_accrual_entry(args)
if flt(payable_interest, precision) > 0.0:
make_loan_interest_accrual_entry(args)
def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None):
def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"):
query_filters = {
"status": ('in', ['Disbursed', 'Partially Disbursed']),
"docstatus": 1
@ -123,13 +132,13 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte
open_loans = frappe.get_all("Loan",
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account",
"is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
"rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"],
"rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"],
filters=query_filters)
for loan in open_loans:
calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest)
calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type)
def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None):
def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular"):
curr_date = posting_date or add_days(nowdate(), 1)
term_loans = get_term_loans(curr_date, term_loan, loan_type)
@ -148,7 +157,8 @@ def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_intere
'payable_principal': loan.principal_amount,
'process_loan_interest': process_loan_interest,
'repayment_schedule_name': loan.payment_entry,
'posting_date': posting_date
'posting_date': posting_date,
'accrual_type': accrual_type
})
make_loan_interest_accrual_entry(args)
@ -192,31 +202,33 @@ def make_loan_interest_accrual_entry(args):
loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision)
loan_interest_accrual.interest_amount = flt(args.interest_amount, precision)
loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest
loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name
loan_interest_accrual.payable_principal_amount = args.payable_principal
loan_interest_accrual.accrual_type = args.accrual_type
loan_interest_accrual.save()
loan_interest_accrual.submit()
def get_no_of_days_for_interest_accural(loan, posting_date):
last_interest_accrual_date = get_last_accural_date_in_current_month(loan)
last_interest_accrual_date = get_last_accrual_date(loan.name)
no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
return no_of_days
def get_last_accural_date_in_current_month(loan):
def get_last_accrual_date(loan):
last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
WHERE loan = %s""", (loan.name))
WHERE loan = %s and docstatus = 1""", (loan))
if last_posting_date[0][0]:
# interest for last interest accrual date is already booked, so add 1 day
return add_days(last_posting_date[0][0], 1)
else:
return loan.disbursement_date
return frappe.db.get_value('Loan', loan, 'disbursement_date')
def days_in_year(year):
days = 365
@ -226,3 +238,11 @@ def days_in_year(year):
return days
def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None):
if not posting_date:
posting_date = getdate()
precision = cint(frappe.db.get_default("currency_precision")) or 2
return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), precision)

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date)
from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_loan_security_price,
from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_price,
make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_application)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
@ -57,4 +57,4 @@ class TestLoanInterestAccrual(unittest.TestCase):
loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
self.assertEquals(flt(loan_interest_accural.interest_amount, 2), flt(accrued_interest_amount, 2))
self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0))

View File

@ -10,11 +10,11 @@
"applicant_type",
"applicant",
"loan_type",
"payment_type",
"column_break_3",
"company",
"posting_date",
"is_term_loan",
"rate_of_interest",
"payment_details_section",
"due_date",
"pending_principal_amount",
@ -31,6 +31,7 @@
"column_break_21",
"reference_date",
"principal_amount_paid",
"total_interest_paid",
"repayment_details",
"amended_from"
],
@ -95,15 +96,6 @@
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"default": "Regular Payment",
"fieldname": "payment_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Payment Type",
"options": "\nRegular Payment\nLoan Closure",
"reqd": 1
},
{
"fieldname": "payable_amount",
"fieldtype": "Currency",
@ -116,6 +108,7 @@
"fieldname": "amount_paid",
"fieldtype": "Currency",
"label": "Amount Paid",
"non_negative": 1,
"options": "Company:company:default_currency",
"reqd": 1
},
@ -195,6 +188,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Principal Amount Paid",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@ -217,11 +211,27 @@
"hidden": 1,
"label": "Repayment Details",
"options": "Loan Repayment Detail"
},
{
"fieldname": "total_interest_paid",
"fieldtype": "Currency",
"hidden": 1,
"label": "Total Interest Paid",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fetch_from": "loan_type.rate_of_interest",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"label": "Rate Of Interest",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-05-16 09:40:15.581165",
"modified": "2020-11-05 10:06:58.792841",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",

View File

@ -14,14 +14,15 @@ from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest, get_last_accrual_date
class LoanRepayment(AccountsController):
def validate(self):
amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type)
amounts = calculate_amounts(self.against_loan, self.posting_date)
self.set_missing_values(amounts)
self.validate_amount()
self.allocate_amounts(amounts['pending_accrual_entries'])
self.allocate_amounts(amounts)
def before_submit(self):
self.book_unaccrued_interest()
@ -32,8 +33,8 @@ class LoanRepayment(AccountsController):
def on_cancel(self):
self.mark_as_unpaid()
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ['GL Entry']
self.make_gl_entries(cancel=1)
def set_missing_values(self, amounts):
precision = cint(frappe.db.get_default("currency_precision")) or 2
@ -72,29 +73,36 @@ class LoanRepayment(AccountsController):
msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount)
frappe.throw(msg)
if self.payment_type == "Loan Closure" and flt(self.amount_paid, precision) < flt(self.payable_amount, precision):
msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount)
frappe.throw(msg)
def book_unaccrued_interest(self):
if self.payment_type == 'Loan Closure':
total_interest_paid = 0
for payment in self.repayment_details:
total_interest_paid += payment.paid_interest_amount
precision = cint(frappe.db.get_default("currency_precision")) or 2
if self.total_interest_paid > self.interest_payable:
if not self.is_term_loan:
# get last loan interest accrual date
last_accrual_date = get_last_accrual_date(self.against_loan)
if total_interest_paid < self.interest_payable:
if not self.is_term_loan:
process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date,
loan=self.against_loan)
# get posting date upto which interest has to be accrued
per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
self.rate_of_interest, self.posting_date), 2)
lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual':
process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1)
no_of_days = flt(flt(self.total_interest_paid - self.interest_payable,
precision)/per_day_interest, 0) - 1
self.append('repayment_details', {
'loan_interest_accrual': lia.name,
'paid_interest_amount': lia.interest_amount,
'paid_principal_amount': lia.payable_principal_amount
})
posting_date = add_days(last_accrual_date, no_of_days)
# book excess interest paid
process = process_loan_interest_accrual_for_demand_loans(posting_date=posting_date,
loan=self.against_loan, accrual_type="Repayment")
# get loan interest accrual to update paid amount
lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual':
process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1)
self.append('repayment_details', {
'loan_interest_accrual': lia.name,
'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision),
'paid_principal_amount': 0.0,
'accrual_type': 'Repayment'
})
def update_paid_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
@ -108,12 +116,6 @@ class LoanRepayment(AccountsController):
WHERE name = %s""",
(flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual))
if flt(loan.total_principal_paid + self.principal_amount_paid, precision) >= flt(loan.total_payment, precision):
if loan.is_secured_loan:
frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested")
else:
frappe.db.set_value("Loan", self.against_loan, "status", "Closed")
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid + self.amount_paid,
loan.total_principal_paid + self.principal_amount_paid, self.against_loan))
@ -123,6 +125,8 @@ class LoanRepayment(AccountsController):
def mark_as_unpaid(self):
loan = frappe.get_doc("Loan", self.against_loan)
no_of_repayments = len(self.repayment_details)
for payment in self.repayment_details:
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` - %s,
@ -130,6 +134,12 @@ class LoanRepayment(AccountsController):
WHERE name = %s""",
(payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual))
# Cancel repayment interest accrual
# checking idx as a preventive measure, repayment accrual will always be the last entry
if payment.accrual_type == 'Repayment' and payment.idx == no_of_repayments:
lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual)
lia_doc.cancel()
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid - self.amount_paid,
loan.total_principal_paid - self.principal_amount_paid, self.against_loan))
@ -137,15 +147,17 @@ class LoanRepayment(AccountsController):
if loan.status == "Loan Closure Requested":
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
def allocate_amounts(self, paid_entries):
def allocate_amounts(self, repayment_details):
precision = cint(frappe.db.get_default("currency_precision")) or 2
self.set('repayment_details', [])
self.principal_amount_paid = 0
total_interest_paid = 0
interest_paid = self.amount_paid - self.penalty_amount
if self.amount_paid - self.penalty_amount > 0 and paid_entries:
if self.amount_paid - self.penalty_amount > 0:
interest_paid = self.amount_paid - self.penalty_amount
for lia, amounts in iteritems(paid_entries):
for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])):
if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
interest_amount = amounts['interest_amount']
paid_principal = amounts['payable_principal_amount']
@ -169,9 +181,24 @@ class LoanRepayment(AccountsController):
'paid_principal_amount': paid_principal
})
if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable:
unaccrued_interest = self.interest_payable - total_interest_paid
interest_paid -= unaccrued_interest
if repayment_details['unaccrued_interest'] and interest_paid:
# no of days for which to accrue interest
# Interest can only be accrued for an entire day and not partial
if interest_paid > repayment_details['unaccrued_interest']:
per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
self.rate_of_interest, self.posting_date), precision)
interest_paid -= repayment_details['unaccrued_interest']
total_interest_paid += repayment_details['unaccrued_interest']
else:
# get no of days for which interest can be paid
per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
self.rate_of_interest, self.posting_date), precision)
no_of_days = cint(interest_paid/per_day_interest)
total_interest_paid += no_of_days * per_day_interest
interest_paid -= no_of_days * per_day_interest
self.total_interest_paid = total_interest_paid
if interest_paid:
self.principal_amount_paid += interest_paid
@ -189,7 +216,7 @@ class LoanRepayment(AccountsController):
"debit_in_account_currency": self.penalty_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Against Loan:") + self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
@ -205,10 +232,8 @@ class LoanRepayment(AccountsController):
"credit_in_account_currency": self.penalty_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Against Loan:") + self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
@ -219,13 +244,11 @@ class LoanRepayment(AccountsController):
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid ,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Against Loan:") + self.against_loan,
"remarks": _("Repayment against Loan: ") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
@ -240,7 +263,7 @@ class LoanRepayment(AccountsController):
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Against Loan:") + self.against_loan,
"remarks": _("Repayment against Loan: ") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
@ -273,7 +296,8 @@ def get_accrued_interest_entries(against_loan):
unpaid_accrued_entries = frappe.db.sql(
"""
SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount,
payable_principal_amount - paid_principal_amount as payable_principal_amount
payable_principal_amount - paid_principal_amount as payable_principal_amount,
accrual_type
FROM
`tabLoan Interest Accrual`
WHERE
@ -282,6 +306,7 @@ def get_accrued_interest_entries(against_loan):
payable_principal_amount - paid_principal_amount > 0)
AND
docstatus = 1
ORDER BY posting_date
""", (against_loan), as_dict=1)
return unpaid_accrued_entries
@ -289,7 +314,7 @@ def get_accrued_interest_entries(against_loan):
# This function returns the amounts that are payable at the time of loan repayment based on posting date
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
def get_amounts(amounts, against_loan, posting_date, payment_type):
def get_amounts(amounts, against_loan, posting_date):
precision = cint(frappe.db.get_default("currency_precision")) or 2
against_loan_doc = frappe.get_doc("Loan", against_loan)
@ -311,10 +336,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
due_date = add_days(entry.posting_date, 1)
no_of_late_days = date_diff(posting_date,
add_days(due_date, loan_type_details.grace_period_in_days))
add_days(due_date, loan_type_details.grace_period_in_days)) + 1
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary):
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular':
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)
total_pending_interest += entry.interest_amount
payable_principal_amount += entry.payable_principal_amount
@ -324,23 +349,27 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
'payable_principal_amount': flt(entry.payable_principal_amount, precision)
})
if not final_due_date:
if due_date and not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'):
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
else:
pending_principal_amount = against_loan_doc.disbursed_amount
pending_principal_amount = against_loan_doc.disbursed_amount - against_loan_doc.total_principal_paid \
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
if payment_type == "Loan Closure":
if due_date:
pending_days = date_diff(posting_date, due_date) + 1
else:
pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1
unaccrued_interest = 0
if due_date:
pending_days = date_diff(posting_date, due_date) + 1
else:
last_accrual_date = get_last_accrual_date(against_loan_doc.name)
pending_days = date_diff(posting_date, last_accrual_date) + 1
payable_principal_amount = pending_principal_amount
per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365
total_pending_interest += (pending_days * per_day_interest)
if pending_days > 0:
principal_amount = flt(pending_principal_amount, precision)
per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date)
unaccrued_interest += (pending_days * flt(per_day_interest, precision))
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
@ -348,6 +377,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
amounts["penalty_amount"] = flt(penalty_amount, precision)
amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = unaccrued_interest
if final_due_date:
amounts["due_date"] = final_due_date
@ -355,7 +385,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
return amounts
@frappe.whitelist()
def calculate_amounts(against_loan, posting_date, payment_type):
def calculate_amounts(against_loan, posting_date, payment_type=''):
amounts = {
'penalty_amount': 0.0,
@ -363,10 +393,17 @@ def calculate_amounts(against_loan, posting_date, payment_type):
'pending_principal_amount': 0.0,
'payable_principal_amount': 0.0,
'payable_amount': 0.0,
'unaccrued_interest': 0.0,
'due_date': ''
}
amounts = get_amounts(amounts, against_loan, posting_date, payment_type)
amounts = get_amounts(amounts, against_loan, posting_date)
# update values for closure
if payment_type == 'Loan Closure':
amounts['payable_principal_amount'] = amounts['pending_principal_amount']
amounts['interest_amount'] += amounts['unaccrued_interest']
amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount']
return amounts

View File

@ -7,7 +7,8 @@
"field_order": [
"loan_interest_accrual",
"paid_principal_amount",
"paid_interest_amount"
"paid_interest_amount",
"accrual_type"
],
"fields": [
{
@ -27,11 +28,20 @@
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
},
{
"fetch_from": "loan_interest_accrual.accrual_type",
"fetch_if_empty": 1,
"fieldname": "accrual_type",
"fieldtype": "Select",
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-04-15 21:50:03.837019",
"modified": "2020-10-23 08:09:18.267030",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment Detail",

View File

@ -25,6 +25,7 @@
},
{
"fetch_from": "loan_security_type.haircut",
"fetch_if_empty": 1,
"fieldname": "haircut",
"fieldtype": "Percent",
"label": "Haircut %"
@ -64,8 +65,9 @@
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-04-29 13:21:26.043492",
"modified": "2020-10-26 07:34:48.601766",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security",

View File

@ -78,7 +78,7 @@ class LoanSecurityPledge(Document):
self.maximum_loan_value = maximum_loan_value
def update_loan(loan, maximum_value_against_pledge):
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_value'])
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_value=%s, is_secured_loan=1
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))

View File

@ -22,7 +22,7 @@ def update_shortfall_status(loan, security_value):
if security_value >= loan_security_shortfall.shortfall_amount:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, {
"status": "Completed",
"shortfall_value": loan_security_shortfall.shortfall_amount})
"shortfall_amount": loan_security_shortfall.shortfall_amount})
else:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name,
"shortfall_amount", loan_security_shortfall.shortfall_amount - security_value)

View File

@ -42,18 +42,20 @@ class LoanSecurityUnpledge(Document):
"valid_upto": (">=", get_datetime())
}, as_list=1))
total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
'total_interest_payable'])
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
'total_interest_payable', 'written_off_amount'])
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid)
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
security_value = 0
for security in self.securities:
pledged_qty = pledge_qty_map.get(security.loan_security, 0)
if security.qty > pledged_qty:
frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}.
You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom,
frappe.bold(security.loan_security), frappe.bold(self.loan)))
msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format(security.idx, pledged_qty, security.uom,
frappe.bold(security.loan_security), frappe.bold(self.loan))
msg += "<br>"
msg += _("You are trying to unpledge more.")
frappe.throw(msg, title=_("Loan Security Unpledge Error"))
qty_after_unpledge = pledged_qty - security.qty
ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
@ -65,10 +67,18 @@ class LoanSecurityUnpledge(Document):
security_value += qty_after_unpledge * current_price
if not security_value and flt(pending_principal_amount, 2) > 0:
frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
self._throw(security_value, pending_principal_amount, ltv_ratio)
if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio:
frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
self._throw(security_value, pending_principal_amount, ltv_ratio)
def _throw(self, security_value, pending_principal_amount, ltv_ratio):
msg = _("Loan Security Value after unpledge is {0}").format(frappe.bold(security_value))
msg += '<br>'
msg += _("Pending principal amount is {0}").format(frappe.bold(flt(pending_principal_amount, 2)))
msg += '<br>'
msg += _("Loan To Security Value ratio must always be {0}").format(frappe.bold(ltv_ratio))
frappe.throw(msg, title=_("Loan To Value ratio breach"))
def on_update_after_submit(self):
self.approve()

View File

@ -11,6 +11,7 @@
"rate_of_interest",
"penalty_interest_rate",
"grace_period_in_days",
"write_off_amount",
"column_break_2",
"company",
"is_term_loan",
@ -76,7 +77,6 @@
"reqd": 1
},
{
"description": "This account is used for booking loan repayments from the borrower and also disbursing loans to the borrower",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment Account",
@ -84,7 +84,6 @@
"reqd": 1
},
{
"description": "This account is capital account which is used to allocate capital for loan disbursal account ",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
@ -96,7 +95,6 @@
"fieldtype": "Column Break"
},
{
"description": "This account will be used for booking loan interest accruals",
"fieldname": "interest_income_account",
"fieldtype": "Link",
"label": "Interest Income Account",
@ -104,7 +102,6 @@
"reqd": 1
},
{
"description": "This account will be used for booking penalties levied due to delayed repayments",
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"label": "Penalty Income Account",
@ -113,7 +110,6 @@
},
{
"default": "0",
"description": "If this is not checked the loan by default will be considered as a Demand Loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan"
@ -145,17 +141,27 @@
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"allow_on_submit": 1,
"description": "Pending amount that will be automatically ignored on loan closure request ",
"fieldname": "write_off_amount",
"fieldtype": "Currency",
"label": "Write Off Amount ",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-06-07 18:55:59.346292",
"modified": "2020-10-26 07:13:55.029811",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@ -165,6 +171,7 @@
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
},
{

View File

@ -0,0 +1,36 @@
// Copyright (c) 2020, 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 Write Off', {
loan: function(frm) {
frm.trigger('show_pending_principal_amount');
},
onload: function(frm) {
frm.trigger('show_pending_principal_amount');
},
refresh: function(frm) {
frm.set_query('write_off_account', function(){
return {
filters: {
'company': frm.doc.company,
'root_type': 'Expense',
'is_group': 0
}
}
});
},
show_pending_principal_amount: function(frm) {
if (frm.doc.loan && frm.doc.docstatus === 0) {
frappe.db.get_value('Loan', frm.doc.loan, ['total_payment', 'total_interest_payable',
'total_principal_paid', 'written_off_amount'], function(values) {
frm.set_df_property('write_off_amount', 'description',
"Pending principal amount is " + cstr(flt(values.total_payment - values.total_interest_payable
- values.total_principal_paid - values.written_off_amount, 2)));
frm.refresh_field('write_off_amount');
});
}
}
});

View File

@ -0,0 +1,157 @@
{
"actions": [],
"autoname": "LM-WO-.#####",
"creation": "2020-10-16 11:09:14.495066",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"write_off_account",
"column_break_11",
"write_off_amount",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Write Off Details"
},
{
"fieldname": "write_off_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Write Off Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "write_off_amount",
"fieldtype": "Currency",
"label": "Write Off Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Write Off",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-10-26 07:13:43.663924",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Write Off",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"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
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, erpnext
from frappe import _
from frappe.utils import getdate, flt, cint
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries
class LoanWriteOff(AccountsController):
def validate(self):
self.set_missing_values()
self.validate_write_off_amount()
def set_missing_values(self):
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def validate_write_off_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan,
['total_payment', 'total_principal_paid','total_interest_payable', 'written_off_amount'])
pending_principal_amount = flt(flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount),
precision)
if self.write_off_amount > pending_principal_amount:
frappe.throw(_("Write off amount cannot be greater than pending principal amount"))
def on_submit(self):
self.update_outstanding_amount()
self.make_gl_entries()
def on_cancel(self):
self.update_outstanding_amount(cancel=1)
self.ignore_linked_doctypes = ['GL Entry']
self.make_gl_entries(cancel=1)
def update_outstanding_amount(self, cancel=0):
written_off_amount = frappe.db.get_value('Loan', self.loan, 'written_off_amount')
if cancel:
written_off_amount -= self.write_off_amount
else:
written_off_amount += self.write_off_amount
frappe.db.set_value('Loan', self.loan, 'written_off_amount', written_off_amount)
def make_gl_entries(self, cancel=0):
gl_entries = []
loan_details = frappe.get_doc("Loan", self.loan)
gl_entries.append(
self.get_gl_dict({
"account": self.write_off_account,
"against": loan_details.loan_account,
"debit": self.write_off_amount,
"debit_in_account_currency": self.write_off_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gl_entries.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": self.write_off_account,
"credit": self.write_off_amount,
"credit_in_account_currency": self.write_off_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
make_gl_entries(gl_entries, cancel=cancel, merge_entries=False)

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestLoanWriteOff(unittest.TestCase):
pass

View File

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2019-09-09 17:06:16.756573",
"doctype": "DocType",
"editable_grid": 1,
@ -49,7 +50,8 @@
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity"
"label": "Quantity",
"non_negative": 1
},
{
"fieldname": "loan_security_price",
@ -86,7 +88,8 @@
}
],
"istable": 1,
"modified": "2019-12-03 10:59:58.001421",
"links": [],
"modified": "2020-11-05 10:07:15.424937",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Pledge",

View File

@ -10,6 +10,7 @@
"loan_type",
"loan",
"process_type",
"accrual_type",
"amended_from"
],
"fields": [
@ -47,17 +48,27 @@
"hidden": 1,
"label": "Process Type",
"read_only": 1
},
{
"fieldname": "accrual_type",
"fieldtype": "Select",
"hidden": 1,
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-04-09 22:52:53.911416",
"modified": "2020-11-06 13:28:51.478909",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Process Loan Interest Accrual",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@ -67,9 +78,11 @@
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@ -79,6 +92,7 @@
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],

View File

@ -20,19 +20,20 @@ class ProcessLoanInterestAccrual(Document):
if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans':
make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name,
open_loans = open_loans, loan_type = self.loan_type)
open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type)
if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans':
make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan,
loan_type=self.loan_type)
loan_type=self.loan_type, accrual_type=self.accrual_type)
def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None):
def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"):
loan_process = frappe.new_doc('Process Loan Interest Accrual')
loan_process.posting_date = posting_date or nowdate()
loan_process.loan_type = loan_type
loan_process.process_type = 'Demand Loans'
loan_process.loan = loan
loan_process.accrual_type = accrual_type
loan_process.submit()

View File

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2019-08-29 22:29:37.628178",
"doctype": "DocType",
"editable_grid": 1,
@ -39,7 +40,8 @@
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity"
"label": "Quantity",
"non_negative": 1
},
{
"fieldname": "loan_security",
@ -56,8 +58,10 @@
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"modified": "2019-12-02 10:23:11.498308",
"links": [],
"modified": "2020-11-05 10:07:37.542344",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Proposed Pledge",

View File

@ -52,6 +52,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
"non_negative": 1,
"reqd": 1
},
{
@ -62,9 +63,10 @@
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-05-06 10:50:18.448552",
"modified": "2020-11-05 10:07:28.106961",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Unpledge",

View File

@ -8,14 +8,14 @@ frappe.ui.form.on(cur_frm.doctype, {
frm.refresh_field('applicant_type');
}
if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual'].includes(frm.doc.doctype)
if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off'].includes(frm.doc.doctype)
&& frm.doc.docstatus > 0) {
frm.add_custom_button(__("Accounting Ledger"), function() {
frappe.route_options = {
voucher_no: frm.doc.name,
company: frm.doc.company,
from_date: frm.doc.posting_date,
from_date: moment(frm.doc.posting_date).format('YYYY-MM-DD'),
to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
show_cancelled_entries: frm.doc.docstatus === 2
};

View File

@ -66,7 +66,7 @@ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({
company: me.frm.doc.company
}
});
}, __("Get items from"));
}, __("Get Items From"));
} else if (this.frm.doc.docstatus === 1) {
this.frm.add_custom_button(__('Create Maintenance Visit'), function() {
frappe.model.open_mapped_doc({

View File

@ -62,7 +62,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__('Warranty Claim'),
function() {
erpnext.utils.map_current_doc({
@ -78,7 +78,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__('Sales Order'),
function() {
erpnext.utils.map_current_doc({
@ -94,7 +94,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
order_type: me.frm.doc.order_type,
}
})
}, __("Get items from"));
}, __("Get Items From"));
}
},
});

View File

@ -76,6 +76,7 @@ class BOM(WebsiteGenerator):
self.set_routing_operations()
self.validate_operations()
self.calculate_cost()
self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, save=False)
def get_context(self, context):
@ -84,8 +85,6 @@ class BOM(WebsiteGenerator):
def on_update(self):
frappe.cache().hdel('bom_children', self.name)
self.check_recursion()
self.update_stock_qty()
self.update_exploded_items()
def on_submit(self):
self.manage_default_bom()
@ -237,7 +236,8 @@ class BOM(WebsiteGenerator):
self.calculate_cost()
if save:
self.db_update()
self.update_exploded_items()
self.update_exploded_items(save=save)
# update parent BOMs
if self.total_cost != existing_bom_cost and update_parent:
@ -318,8 +318,6 @@ class BOM(WebsiteGenerator):
m.uom = m.stock_uom
m.qty = m.stock_qty
m.db_update()
def validate_uom_is_interger(self):
from erpnext.utilities.transaction_base import validate_uom_is_integer
validate_uom_is_integer(self, "uom", "qty", "BOM Item")
@ -372,15 +370,6 @@ class BOM(WebsiteGenerator):
if raise_exception:
frappe.throw(_("BOM recursion: {0} cannot be parent or child of {1}").format(self.name, self.name))
def update_cost_and_exploded_items(self, bom_list=[]):
bom_list = self.traverse_tree(bom_list)
for bom in bom_list:
bom_obj = frappe.get_doc("BOM", bom)
bom_obj.check_recursion(bom_list=bom_list)
bom_obj.update_exploded_items()
return bom_list
def traverse_tree(self, bom_list=None):
def _get_children(bom_no):
children = frappe.cache().hget('bom_children', bom_no)
@ -472,10 +461,10 @@ class BOM(WebsiteGenerator):
d.rate = rate
d.amount = (d.stock_qty or d.qty) * rate
def update_exploded_items(self):
def update_exploded_items(self, save=True):
""" Update Flat BOM, following will be correct data"""
self.get_exploded_items()
self.add_exploded_items()
self.add_exploded_items(save=save)
def get_exploded_items(self):
""" Get all raw materials including items from child bom"""
@ -544,11 +533,13 @@ class BOM(WebsiteGenerator):
'sourced_by_supplier': d.get('sourced_by_supplier', 0)
}))
def add_exploded_items(self):
def add_exploded_items(self, save=True):
"Add items to Flat BOM table"
frappe.db.sql("""delete from `tabBOM Explosion Item` where parent=%s""", self.name)
self.set('exploded_items', [])
if save:
frappe.db.sql("""delete from `tabBOM Explosion Item` where parent=%s""", self.name)
for d in sorted(self.cur_exploded_items, key=itemgetter(0)):
ch = self.append('exploded_items', {})
for i in self.cur_exploded_items[d].keys():
@ -556,7 +547,9 @@ class BOM(WebsiteGenerator):
ch.amount = flt(ch.stock_qty) * flt(ch.rate)
ch.qty_consumed_per_unit = flt(ch.stock_qty) / flt(self.quantity)
ch.docstatus = self.docstatus
ch.db_insert()
if save:
ch.db_insert()
def validate_bom_links(self):
if not self.is_active:

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