diff --git a/erpnext/__init__.py b/erpnext/__init__.py index d234e1ed4ba..90e5821029b 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '10.1.46' +__version__ = '10.1.47' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/hr/doctype/employee_loan/employee_loan.js b/erpnext/hr/doctype/employee_loan/employee_loan.js new file mode 100644 index 00000000000..e089e29911f --- /dev/null +++ b/erpnext/hr/doctype/employee_loan/employee_loan.js @@ -0,0 +1,121 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Employee Loan', { + onload: function (frm) { + frm.set_query("employee_loan_application", function () { + return { + "filters": { + "employee": frm.doc.employee, + "docstatus": 1, + "status": "Approved" + } + }; + }); + + frm.set_query("interest_income_account", function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Income", + "is_group": 0 + } + }; + }); + + frm.set_query("employee", function() { + return { + "filters": { + "company": frm.doc.company, + } + }; + }); + + $.each(["payment_account", "employee_loan_account"], function (i, field) { + frm.set_query(field, function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Asset", + "is_group": 0 + } + }; + }); + }) + }, + + refresh: function (frm) { + if (frm.doc.docstatus == 1 && (frm.doc.status == "Sanctioned" || frm.doc.status == "Partially Disbursed")) { + frm.add_custom_button(__('Make Disbursement Entry'), function () { + frm.trigger("make_jv"); + }) + } + frm.trigger("toggle_fields"); + }, + + make_jv: function (frm) { + frappe.call({ + args: { + "employee_loan": frm.doc.name, + "company": frm.doc.company, + "employee_loan_account": frm.doc.employee_loan_account, + "employee": frm.doc.employee, + "loan_amount": frm.doc.loan_amount, + "payment_account": frm.doc.payment_account + }, + method: "erpnext.hr.doctype.employee_loan.employee_loan.make_jv_entry", + callback: function (r) { + if (r.message) + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }) + }, + + mode_of_payment: function (frm) { + if (frm.doc.mode_of_payment && frm.doc.company) { + frappe.call({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_bank_cash_account", + args: { + "mode_of_payment": frm.doc.mode_of_payment, + "company": frm.doc.company + }, + callback: function (r, rt) { + if (r.message) { + frm.set_value("payment_account", r.message.account); + } + } + }); + } + }, + + employee_loan_application: function (frm) { + if(frm.doc.employee_loan_application){ + return frappe.call({ + method: "erpnext.hr.doctype.employee_loan.employee_loan.get_employee_loan_application", + args: { + "employee_loan_application": frm.doc.employee_loan_application + }, + callback: function (r) { + if (!r.exc && r.message) { + frm.set_value("loan_type", r.message.loan_type); + frm.set_value("loan_amount", r.message.loan_amount); + frm.set_value("repayment_method", r.message.repayment_method); + frm.set_value("monthly_repayment_amount", r.message.repayment_amount); + frm.set_value("repayment_periods", r.message.repayment_periods); + frm.set_value("rate_of_interest", r.message.rate_of_interest); + } + } + }); + } + }, + + repayment_method: function (frm) { + frm.trigger("toggle_fields") + }, + + toggle_fields: function (frm) { + frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period") + frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods") + } +}); diff --git a/erpnext/hr/doctype/employee_loan_application/employee_loan_application.py b/erpnext/hr/doctype/employee_loan_application/employee_loan_application.py new file mode 100644 index 00000000000..b6c650207f0 --- /dev/null +++ b/erpnext/hr/doctype/employee_loan_application/employee_loan_application.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, math +from frappe import _ +from frappe.utils import flt, rounded +from frappe.model.mapper import get_mapped_doc +from frappe.model.document import Document + +from erpnext.hr.doctype.employee_loan.employee_loan import get_monthly_repayment_amount, check_repayment_method + +class EmployeeLoanApplication(Document): + def validate(self): + check_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, self.repayment_periods) + self.validate_loan_amount() + self.get_repayment_details() + + def validate_loan_amount(self): + maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount') + if maximum_loan_limit and self.loan_amount > maximum_loan_limit: + frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)) + + def get_repayment_details(self): + if self.repayment_method == "Repay Over Number of Periods": + self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) + + if self.repayment_method == "Repay Fixed Amount per Period": + monthly_interest_rate = flt(self.rate_of_interest) / (12 *100) + if monthly_interest_rate: + monthly_interest_amount = self.loan_amount * monthly_interest_rate + if monthly_interest_amount >= self.repayment_amount: + frappe.throw(_("Repayment amount {} should be greater than monthly interest amount {}"). + format(self.repayment_amount, monthly_interest_amount)) + + self.repayment_periods = math.ceil((math.log(self.repayment_amount) - + math.log(self.repayment_amount - (monthly_interest_amount))) / + (math.log(1 + monthly_interest_rate))) + else: + self.repayment_periods = self.loan_amount / self.repayment_amount + + self.calculate_payable_amount() + + def calculate_payable_amount(self): + balance_amount = self.loan_amount + self.total_payable_amount = 0 + self.total_payable_interest = 0 + + while(balance_amount > 0): + interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) + balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount) + + self.total_payable_interest += interest_amount + + self.total_payable_amount = self.loan_amount + self.total_payable_interest + +@frappe.whitelist() +def make_employee_loan(source_name, target_doc = None): + doclist = get_mapped_doc("Employee Loan Application", source_name, { + "Employee Loan Application": { + "doctype": "Employee Loan", + "validation": { + "docstatus": ["=", 1] + } + } + }, target_doc) + + return doclist \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 23838040381..d4beec39ce0 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -63,7 +63,6 @@ class Project(Document): self.validate_weights() self.sync_tasks() self.tasks = [] - self.load_tasks() self.send_welcome_email() def validate_project_name(self): @@ -86,6 +85,9 @@ class Project(Document): def sync_tasks(self): """sync tasks and remove table""" + if not hasattr(self, "deleted_task_list"): + self.set("deleted_task_list", []) + if self.flags.dont_sync_tasks: return task_names = [] @@ -134,7 +136,7 @@ class Project(Document): # delete for t in frappe.get_all("Task", ["name"], {"project": self.name, "name": ("not in", task_names)}): - frappe.delete_doc("Task", t.name) + self.deleted_task_list.append(t.name) def update_costing_and_percentage_complete(self): self.update_percent_complete() @@ -143,8 +145,14 @@ class Project(Document): def is_row_updated(self, row, existing_task_data): if self.get("__islocal") or not existing_task_data: return True + project_task_custom_fields = frappe.get_all("Custom Field", {"dt": "Project Task"}, "fieldname") + d = existing_task_data.get(row.task_id) + for field in project_task_custom_fields: + if row.get(field) != d.get(field): + return True + if (d and (row.title != d.title or row.status != d.status or getdate(row.start_date) != getdate(d.start_date) or getdate(row.end_date) != getdate(d.end_date) or row.description != d.description or row.task_weight != d.task_weight)): @@ -272,9 +280,19 @@ class Project(Document): user.welcome_email_sent = 1 def on_update(self): + self.delete_task() + self.load_tasks() self.update_costing_and_percentage_complete() self.update_dependencies_on_duplicated_project() + def delete_task(self): + if not self.get('deleted_task_list'): return + + for d in self.get('deleted_task_list'): + frappe.delete_doc("Task", d) + + self.deleted_task_list = [] + def update_dependencies_on_duplicated_project(self): if self.flags.dont_sync_tasks: return if not self.copied_from: diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 2822ae850a0..0ecf6e15f3d 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -26,7 +26,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ }; }); - if (this.frm.doc.__islocal) { + if (this.frm.doc.__islocal + && frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) { this.frm.set_value("disable_rounded_total", cint(frappe.sys_defaults.disable_rounded_total)); }