Merge branch 'develop' into fix-timesheet-creation

This commit is contained in:
Suraj Shetty 2019-08-23 11:46:01 +05:30 committed by GitHub
commit 7f94a58bf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
194 changed files with 5664 additions and 8494 deletions

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '11.1.39' __version__ = '12.0.8'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -121,7 +121,11 @@ frappe.treeview_settings["Account"] = {
}, },
onrender: function(node) { onrender: function(node) {
if(frappe.boot.user.can_read.indexOf("GL Entry") !== -1){ if(frappe.boot.user.can_read.indexOf("GL Entry") !== -1){
var dr_or_cr = in_list(["Liability", "Income", "Equity"], node.data.root_type) ? "Cr" : "Dr";
// show Dr if positive since balance is calculated as debit - credit else show Cr
let balance = node.data.balance_in_account_currency || node.data.balance;
let dr_or_cr = balance > 0 ? "Dr": "Cr";
if (node.data && node.data.balance!==undefined) { if (node.data && node.data.balance!==undefined) {
$('<span class="balance-area pull-right text-muted small">' $('<span class="balance-area pull-right text-muted small">'
+ (node.data.balance_in_account_currency ? + (node.data.balance_in_account_currency ?

View File

@ -17,8 +17,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company"
"reqd": 1
}, },
{ {
"fieldname": "reference_document", "fieldname": "reference_document",
@ -34,8 +33,7 @@
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Default Dimension", "label": "Default Dimension",
"options": "reference_document", "options": "reference_document"
"reqd": 1
}, },
{ {
"columns": 3, "columns": 3,
@ -55,7 +53,7 @@
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-07-17 23:34:33.026883", "modified": "2019-08-15 11:59:09.389891",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting Dimension Detail", "name": "Accounting Dimension Detail",

View File

@ -168,38 +168,6 @@
"translatable": 0, "translatable": 0,
"unique": 0 "unique": 0
}, },
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "Open\nClosed",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@ -273,7 +241,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-04-13 19:14:47.593753", "modified": "2019-08-01 19:14:47.593753",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting Period", "name": "Accounting Period",

View File

@ -7,6 +7,8 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
class OverlapError(frappe.ValidationError): pass
class AccountingPeriod(Document): class AccountingPeriod(Document):
def validate(self): def validate(self):
self.validate_overlap() self.validate_overlap()
@ -34,12 +36,13 @@ class AccountingPeriod(Document):
}, as_dict=True) }, as_dict=True)
if len(existing_accounting_period) > 0: if len(existing_accounting_period) > 0:
frappe.throw(_("Accounting Period overlaps with {0}".format(existing_accounting_period[0].get("name")))) frappe.throw(_("Accounting Period overlaps with {0}")
.format(existing_accounting_period[0].get("name")), OverlapError)
def get_doctypes_for_closing(self): def get_doctypes_for_closing(self):
docs_for_closing = [] docs_for_closing = []
#if not self.closed_documents or len(self.closed_documents) == 0: doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", "Bank Reconciliation",
doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", "Bank Reconciliation", "Asset", "Purchase Order", "Sales Order", "Leave Application", "Leave Allocation", "Stock Entry"] "Asset", "Purchase Order", "Sales Order", "Leave Application", "Leave Allocation", "Stock Entry"]
closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes] closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes]
for closed_doctype in closed_doctypes: for closed_doctype in closed_doctypes:
docs_for_closing.append(closed_doctype) docs_for_closing.append(closed_doctype)

View File

@ -5,23 +5,42 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import nowdate, add_months
from erpnext.accounts.general_ledger import ClosedAccountingPeriod
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
# class TestAccountingPeriod(unittest.TestCase): class TestAccountingPeriod(unittest.TestCase):
# def test_overlap(self): def test_overlap(self):
# ap1 = create_accounting_period({"start_date":"2018-04-01", "end_date":"2018-06-30", "company":"Wind Power LLC"}) ap1 = create_accounting_period(start_date = "2018-04-01",
# ap1.save() end_date = "2018-06-30", company = "Wind Power LLC")
# ap2 = create_accounting_period({"start_date":"2018-06-30", "end_date":"2018-07-10", "company":"Wind Power LLC"}) ap1.save()
# self.assertRaises(frappe.OverlapError, accounting_period_2.save())
# ap2 = create_accounting_period(start_date = "2018-06-30",
# def tearDown(self): end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
# pass self.assertRaises(OverlapError, ap2.save)
#
# def test_accounting_period(self):
# def create_accounting_period(**args): ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
# accounting_period = frappe.new_doc("Accounting Period") ap1.save()
# accounting_period.start_date = args.start_date or frappe.utils.datetime.date(2018, 4, 1)
# accounting_period.end_date = args.end_date or frappe.utils.datetime.date(2018, 6, 30) doc = create_sales_invoice(do_not_submit=1, cost_center = "_Test Company - _TC", warehouse = "Stores - _TC")
# accounting_period.company = args.company self.assertRaises(ClosedAccountingPeriod, doc.submit)
# accounting_period.period_name = "_Test_Period_Name_1"
# def tearDown(self):
# return accounting_period for d in frappe.get_all("Accounting Period"):
frappe.delete_doc("Accounting Period", d.name)
def create_accounting_period(**args):
args = frappe._dict(args)
accounting_period = frappe.new_doc("Accounting Period")
accounting_period.start_date = args.start_date or nowdate()
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name =args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", {
"document_type": 'Sales Invoice', "closed": 1
})
return accounting_period

View File

@ -45,12 +45,12 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self): def clear_linked_payment_entries(self):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
allocated_amount = get_total_allocated_amount(payment_entry) allocated_amount = get_total_allocated_amount(payment_entry)
paid_amount = get_paid_amount(payment_entry) paid_amount = get_paid_amount(payment_entry, self.currency)
if paid_amount and allocated_amount: if paid_amount and allocated_amount:
if flt(allocated_amount[0]["allocated_amount"]) > flt(paid_amount): if flt(allocated_amount[0]["allocated_amount"]) > flt(paid_amount):
frappe.throw(_("The total allocated amount ({0}) is greated than the paid amount ({1}).".format(flt(allocated_amount[0]["allocated_amount"]), flt(paid_amount)))) frappe.throw(_("The total allocated amount ({0}) is greated than the paid amount ({1}).".format(flt(allocated_amount[0]["allocated_amount"]), flt(paid_amount))))
elif flt(allocated_amount[0]["allocated_amount"]) == flt(paid_amount): else:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
self.clear_simple_entry(payment_entry) self.clear_simple_entry(payment_entry)
@ -80,9 +80,17 @@ def get_total_allocated_amount(payment_entry):
AND AND
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True) bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
def get_paid_amount(payment_entry): def get_paid_amount(payment_entry, currency):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "paid_amount")
paid_amount_field = "paid_amount"
if payment_entry.payment_document == 'Payment Entry':
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
paid_amount_field = ("base_paid_amount"
if doc.paid_to_account_currency == currency else "paid_amount")
return frappe.db.get_value(payment_entry.payment_document,
payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry": elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit") return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit")

View File

@ -8,7 +8,8 @@
"customer", "customer",
"column_break_3", "column_break_3",
"posting_date", "posting_date",
"outstanding_amount" "outstanding_amount",
"debit_to"
], ],
"fields": [ "fields": [
{ {
@ -48,10 +49,18 @@
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fetch_from": "sales_invoice.debit_to",
"fieldname": "debit_to",
"fieldtype": "Link",
"label": "Debit to",
"options": "Account",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-05-30 19:27:29.436153", "modified": "2019-08-07 15:13:55.808349",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Discounted Invoice", "name": "Discounted Invoice",

View File

@ -13,41 +13,57 @@ frappe.ui.form.on('Invoice Discounting', {
}; };
}); });
frm.events.filter_accounts("bank_account", frm, {"account_type": "Bank"});
frm.events.filter_accounts("bank_charges_account", frm, {"root_type": "Expense"}); frm.events.filter_accounts("bank_account", frm, [["account_type", "=", "Bank"]]);
frm.events.filter_accounts("short_term_loan", frm, {"root_type": "Liability"}); frm.events.filter_accounts("bank_charges_account", frm, [["root_type", "=", "Expense"]]);
frm.events.filter_accounts("accounts_receivable_credit", frm, {"account_type": "Receivable"}); frm.events.filter_accounts("short_term_loan", frm, [["root_type", "=", "Liability"]]);
frm.events.filter_accounts("accounts_receivable_discounted", frm, {"account_type": "Receivable"}); frm.events.filter_accounts("accounts_receivable_discounted", frm, [["account_type", "=", "Receivable"]]);
frm.events.filter_accounts("accounts_receivable_unpaid", frm, {"account_type": "Receivable"}); frm.events.filter_accounts("accounts_receivable_credit", frm, [["account_type", "=", "Receivable"]]);
frm.events.filter_accounts("accounts_receivable_unpaid", frm, [["account_type", "=", "Receivable"]]);
}, },
filter_accounts: (fieldname, frm, addl_filters) => { filter_accounts: (fieldname, frm, addl_filters) => {
let filters = { let filters = [
"company": frm.doc.company, ["company", "=", frm.doc.company],
"is_group": 0 ["is_group", "=", 0]
}; ];
if(addl_filters) Object.assign(filters, addl_filters); if(addl_filters){
filters = $.merge(filters , addl_filters);
}
frm.set_query(fieldname, () => { return { "filters": filters }; }); frm.set_query(fieldname, () => { return { "filters": filters }; });
}, },
refresh_filters: (frm) =>{
let invoice_accounts = Object.keys(frm.doc.invoices).map(function(key) {
return frm.doc.invoices[key].debit_to;
});
let filters = [
["account_type", "=", "Receivable"],
["name", "not in", invoice_accounts]
];
frm.events.filter_accounts("accounts_receivable_credit", frm, filters);
frm.events.filter_accounts("accounts_receivable_discounted", frm, filters);
frm.events.filter_accounts("accounts_receivable_unpaid", frm, filters);
},
refresh: (frm) => { refresh: (frm) => {
frm.events.show_general_ledger(frm); frm.events.show_general_ledger(frm);
if(frm.doc.docstatus === 0) { if (frm.doc.docstatus === 0) {
frm.add_custom_button(__('Get Invoices'), function() { frm.add_custom_button(__('Get Invoices'), function() {
frm.events.get_invoices(frm); frm.events.get_invoices(frm);
}); });
} }
if(frm.doc.docstatus === 1 && frm.doc.status !== "Settled") { if (frm.doc.docstatus === 1 && frm.doc.status !== "Settled") {
if(frm.doc.status == "Sanctioned") { if (frm.doc.status == "Sanctioned") {
frm.add_custom_button(__('Disburse Loan'), function() { frm.add_custom_button(__('Disburse Loan'), function() {
frm.events.create_disbursement_entry(frm); frm.events.create_disbursement_entry(frm);
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
if(frm.doc.status == "Disbursed") { if (frm.doc.status == "Disbursed") {
frm.add_custom_button(__('Close Loan'), function() { frm.add_custom_button(__('Close Loan'), function() {
frm.events.close_loan(frm); frm.events.close_loan(frm);
}).addClass("btn-primary"); }).addClass("btn-primary");
@ -64,7 +80,7 @@ frappe.ui.form.on('Invoice Discounting', {
}, },
set_end_date: (frm) => { set_end_date: (frm) => {
if(frm.doc.loan_start_date && frm.doc.loan_period) { if (frm.doc.loan_start_date && frm.doc.loan_period) {
let end_date = frappe.datetime.add_days(frm.doc.loan_start_date, frm.doc.loan_period); let end_date = frappe.datetime.add_days(frm.doc.loan_start_date, frm.doc.loan_period);
frm.set_value("loan_end_date", end_date); frm.set_value("loan_end_date", end_date);
} }
@ -132,6 +148,7 @@ frappe.ui.form.on('Invoice Discounting', {
frm.doc.invoices = frm.doc.invoices.filter(row => row.sales_invoice); frm.doc.invoices = frm.doc.invoices.filter(row => row.sales_invoice);
let row = frm.add_child("invoices"); let row = frm.add_child("invoices");
$.extend(row, v); $.extend(row, v);
frm.events.refresh_filters(frm);
}); });
refresh_field("invoices"); refresh_field("invoices");
} }
@ -190,8 +207,10 @@ frappe.ui.form.on('Invoice Discounting', {
frappe.ui.form.on('Discounted Invoice', { frappe.ui.form.on('Discounted Invoice', {
sales_invoice: (frm) => { sales_invoice: (frm) => {
frm.events.calculate_total_amount(frm); frm.events.calculate_total_amount(frm);
frm.events.refresh_filters(frm);
}, },
invoices_remove: (frm) => { invoices_remove: (frm) => {
frm.events.calculate_total_amount(frm); frm.events.calculate_total_amount(frm);
frm.events.refresh_filters(frm);
} }
}); });

View File

@ -12,6 +12,7 @@ from erpnext.accounts.general_ledger import make_gl_entries
class InvoiceDiscounting(AccountsController): class InvoiceDiscounting(AccountsController):
def validate(self): def validate(self):
self.validate_mandatory() self.validate_mandatory()
self.validate_invoices()
self.calculate_total_amount() self.calculate_total_amount()
self.set_status() self.set_status()
self.set_end_date() self.set_end_date()
@ -24,6 +25,15 @@ class InvoiceDiscounting(AccountsController):
if self.docstatus == 1 and not (self.loan_start_date and self.loan_period): if self.docstatus == 1 and not (self.loan_start_date and self.loan_period):
frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting")) frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting"))
def validate_invoices(self):
discounted_invoices = [record.sales_invoice for record in
frappe.get_all("Discounted Invoice",fields = ["sales_invoice"], filters= {"docstatus":1})]
for record in self.invoices:
if record.sales_invoice in discounted_invoices:
frappe.throw("Row({0}): {1} is already discounted in {2}"
.format(record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent)))
def calculate_total_amount(self): def calculate_total_amount(self):
self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices]) self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices])
@ -212,7 +222,8 @@ def get_invoices(filters):
name as sales_invoice, name as sales_invoice,
customer, customer,
posting_date, posting_date,
outstanding_amount outstanding_amount,
debit_to
from `tabSales Invoice` si from `tabSales Invoice` si
where where
docstatus = 1 docstatus = 1

View File

@ -624,8 +624,8 @@ def get_outstanding_reference_documents(args):
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data: if not data:
frappe.msgprint(_("No outstanding invoices found for the {0} <b>{1}</b>.") frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.")
.format(args.get("party_type").lower(), args.get("party"))) .format(args.get("party_type").lower(), frappe.bold(args.get("party"))))
return data return data
@ -648,13 +648,18 @@ def get_orders_to_be_billed(posting_date, party_type, party,
orders = [] orders = []
if voucher_type: if voucher_type:
ref_field = "base_grand_total" if party_account_currency == company_currency else "grand_total" if party_account_currency == company_currency:
grand_total_field = "base_grand_total"
rounded_total_field = "base_rounded_total"
else:
grand_total_field = "grand_total"
rounded_total_field = "rounded_total"
orders = frappe.db.sql(""" orders = frappe.db.sql("""
select select
name as voucher_no, name as voucher_no,
{ref_field} as invoice_amount, if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
({ref_field} - advance_paid) as outstanding_amount, (if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount,
transaction_date as posting_date transaction_date as posting_date
from from
`tab{voucher_type}` `tab{voucher_type}`
@ -663,13 +668,14 @@ def get_orders_to_be_billed(posting_date, party_type, party,
and docstatus = 1 and docstatus = 1
and company = %s and company = %s
and ifnull(status, "") != "Closed" and ifnull(status, "") != "Closed"
and {ref_field} > advance_paid and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid
and abs(100 - per_billed) > 0.01 and abs(100 - per_billed) > 0.01
{condition} {condition}
order by order by
transaction_date, name transaction_date, name
""".format(**{ """.format(**{
"ref_field": ref_field, "rounded_total_field": rounded_total_field,
"grand_total_field": grand_total_field,
"voucher_type": voucher_type, "voucher_type": voucher_type,
"party_type": scrub(party_type), "party_type": scrub(party_type),
"condition": condition "condition": condition
@ -677,8 +683,8 @@ def get_orders_to_be_billed(posting_date, party_type, party,
order_list = [] order_list = []
for d in orders: for d in orders:
if not (d.outstanding_amount >= filters.get("outstanding_amt_greater_than") if not (flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
and d.outstanding_amount <= filters.get("outstanding_amt_less_than")): and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))):
continue continue
d["voucher_type"] = voucher_type d["voucher_type"] = voucher_type
@ -755,9 +761,23 @@ def get_party_details(company, party_type, party, date, cost_center=None):
@frappe.whitelist() @frappe.whitelist()
def get_account_details(account, date, cost_center=None): def get_account_details(account, date, cost_center=None):
frappe.has_permission('Payment Entry', throw=True) frappe.has_permission('Payment Entry', throw=True)
# to check if the passed account is accessible under reference doctype Payment Entry
account_list = frappe.get_list('Account', {
'name': account
}, reference_doctype='Payment Entry', limit=1)
# There might be some user permissions which will allow account under certain doctypes
# except for Payment Entry, only in such case we should throw permission error
if not account_list:
frappe.throw(_('Account: {0} is not permitted under Payment Entry').format(account))
account_balance = get_balance_on(account, date, cost_center=cost_center,
ignore_account_permission=True)
return frappe._dict({ return frappe._dict({
"account_currency": get_account_currency(account), "account_currency": get_account_currency(account),
"account_balance": get_balance_on(account, date, cost_center=cost_center), "account_balance": account_balance,
"account_type": frappe.db.get_value("Account", account, "account_type") "account_type": frappe.db.get_value("Account", account, "account_type")
}) })

View File

@ -66,6 +66,7 @@ frappe.ui.form.on('Payment Order', {
get_query_filters: { get_query_filters: {
bank: frm.doc.bank, bank: frm.doc.bank,
docstatus: 1, docstatus: 1,
payment_type: ("!=", "Receive"),
bank_account: frm.doc.company_bank_account, bank_account: frm.doc.company_bank_account,
paid_from: frm.doc.account, paid_from: frm.doc.account,
payment_order_status: ["=", "Initiated"], payment_order_status: ["=", "Initiated"],

View File

@ -93,7 +93,7 @@ class PaymentReconciliation(Document):
and `tab{doc}`.is_return = 1 and `tabGL Entry`.against_voucher_type = %(voucher_type)s and `tab{doc}`.is_return = 1 and `tabGL Entry`.against_voucher_type = %(voucher_type)s
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s
GROUP BY `tabSales Invoice`.name GROUP BY `tab{doc}`.name
Having Having
amount > 0 amount > 0
""".format(doc=voucher_type, dr_or_cr=dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr), { """.format(doc=voucher_type, dr_or_cr=dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr), {
@ -257,11 +257,8 @@ def reconcile_dr_cr_note(dr_cr_notes):
voucher_type = ('Credit Note' voucher_type = ('Credit Note'
if d.voucher_type == 'Sales Invoice' else 'Debit Note') if d.voucher_type == 'Sales Invoice' else 'Debit Note')
dr_or_cr = ('credit_in_account_currency'
if d.reference_type == 'Sales Invoice' else 'debit_in_account_currency')
reconcile_dr_or_cr = ('debit_in_account_currency' reconcile_dr_or_cr = ('debit_in_account_currency'
if dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') if d.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency')
jv = frappe.get_doc({ jv = frappe.get_doc({
"doctype": "Journal Entry", "doctype": "Journal Entry",
@ -272,8 +269,7 @@ def reconcile_dr_cr_note(dr_cr_notes):
'account': d.account, 'account': d.account,
'party': d.party, 'party': d.party,
'party_type': d.party_type, 'party_type': d.party_type,
reconcile_dr_or_cr: (abs(d.allocated_amount) d.dr_or_cr: abs(d.allocated_amount),
if abs(d.unadjusted_amount) > abs(d.allocated_amount) else abs(d.unadjusted_amount)),
'reference_type': d.against_voucher_type, 'reference_type': d.against_voucher_type,
'reference_name': d.against_voucher 'reference_name': d.against_voucher
}, },
@ -281,7 +277,8 @@ def reconcile_dr_cr_note(dr_cr_notes):
'account': d.account, 'account': d.account,
'party': d.party, 'party': d.party,
'party_type': d.party_type, 'party_type': d.party_type,
dr_or_cr: abs(d.allocated_amount), reconcile_dr_or_cr: (abs(d.allocated_amount)
if abs(d.unadjusted_amount) > abs(d.allocated_amount) else abs(d.unadjusted_amount)),
'reference_type': d.voucher_type, 'reference_type': d.voucher_type,
'reference_name': d.voucher_no 'reference_name': d.voucher_no
} }

View File

@ -307,7 +307,7 @@ def get_item_tax_data():
# example: {'Consulting Services': {'Excise 12 - TS': '12.000'}} # example: {'Consulting Services': {'Excise 12 - TS': '12.000'}}
itemwise_tax = {} itemwise_tax = {}
taxes = frappe.db.sql(""" select parent, tax_type, tax_rate from `tabItem Tax`""", as_dict=1) taxes = frappe.db.sql(""" select parent, tax_type, tax_rate from `tabItem Tax Template Detail`""", as_dict=1)
for tax in taxes: for tax in taxes:
if tax.parent not in itemwise_tax: if tax.parent not in itemwise_tax:
@ -432,7 +432,6 @@ def get_customer_id(doc, customer=None):
return cust_id return cust_id
def make_customer_and_address(customers): def make_customer_and_address(customers):
customers_list = [] customers_list = []
for customer, data in iteritems(customers): for customer, data in iteritems(customers):
@ -449,7 +448,6 @@ def make_customer_and_address(customers):
frappe.db.commit() frappe.db.commit()
return customers_list return customers_list
def add_customer(data): def add_customer(data):
customer = data.get('full_name') or data.get('customer') customer = data.get('full_name') or data.get('customer')
if frappe.db.exists("Customer", customer.strip()): if frappe.db.exists("Customer", customer.strip()):
@ -466,21 +464,18 @@ def add_customer(data):
frappe.db.commit() frappe.db.commit()
return customer_doc.name return customer_doc.name
def get_territory(data): def get_territory(data):
if data.get('territory'): if data.get('territory'):
return data.get('territory') return data.get('territory')
return frappe.db.get_single_value('Selling Settings','territory') or _('All Territories') return frappe.db.get_single_value('Selling Settings','territory') or _('All Territories')
def get_customer_group(data): def get_customer_group(data):
if data.get('customer_group'): if data.get('customer_group'):
return data.get('customer_group') return data.get('customer_group')
return frappe.db.get_single_value('Selling Settings', 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name') return frappe.db.get_single_value('Selling Settings', 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name')
def make_contact(args, customer): def make_contact(args, customer):
if args.get('email_id') or args.get('phone'): if args.get('email_id') or args.get('phone'):
name = frappe.db.get_value('Dynamic Link', name = frappe.db.get_value('Dynamic Link',
@ -506,7 +501,6 @@ def make_contact(args, customer):
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
def make_address(args, customer): def make_address(args, customer):
if not args.get('address_line1'): if not args.get('address_line1'):
return return
@ -521,7 +515,10 @@ def make_address(args, customer):
address = frappe.get_doc('Address', name) address = frappe.get_doc('Address', name)
else: else:
address = frappe.new_doc('Address') address = frappe.new_doc('Address')
address.country = frappe.get_cached_value('Company', args.get('company'), 'country') if args.get('company'):
address.country = frappe.get_cached_value('Company',
args.get('company'), 'country')
address.append('links', { address.append('links', {
'link_doctype': 'Customer', 'link_doctype': 'Customer',
'link_name': customer 'link_name': customer
@ -533,7 +530,6 @@ def make_address(args, customer):
address.flags.ignore_mandatory = True address.flags.ignore_mandatory = True
address.save(ignore_permissions=True) address.save(ignore_permissions=True)
def make_email_queue(email_queue): def make_email_queue(email_queue):
name_list = [] name_list = []
for key, data in iteritems(email_queue): for key, data in iteritems(email_queue):
@ -550,7 +546,6 @@ def make_email_queue(email_queue):
return name_list return name_list
def validate_item(doc): def validate_item(doc):
for item in doc.get('items'): for item in doc.get('items'):
if not frappe.db.exists('Item', item.get('item_code')): if not frappe.db.exists('Item', item.get('item_code')):
@ -569,7 +564,6 @@ def validate_item(doc):
item_doc.save(ignore_permissions=True) item_doc.save(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()
def submit_invoice(si_doc, name, doc, name_list): def submit_invoice(si_doc, name, doc, name_list):
try: try:
si_doc.insert() si_doc.insert()
@ -585,7 +579,6 @@ def submit_invoice(si_doc, name, doc, name_list):
return name_list return name_list
def save_invoice(doc, name, name_list): def save_invoice(doc, name, name_list):
try: try:
if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}): if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}):

View File

@ -78,6 +78,7 @@ class SalesInvoice(SellingController):
self.so_dn_required() self.so_dn_required()
self.validate_proj_cust() self.validate_proj_cust()
self.validate_pos_return()
self.validate_with_previous_doc() self.validate_with_previous_doc()
self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
@ -199,6 +200,16 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains: if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_submit") manage_invoice_submit_cancel(self, "on_submit")
def validate_pos_return(self):
if self.is_pos and self.is_return:
total_amount_in_payments = 0
for payment in self.payments:
total_amount_in_payments += payment.amount
if total_amount_in_payments < self.rounded_total:
frappe.throw(_("Total payments amount can't be greater than {}".format(-self.rounded_total)))
def validate_pos_paid_amount(self): def validate_pos_paid_amount(self):
if len(self.payments) == 0 and self.is_pos: if len(self.payments) == 0 and self.is_pos:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))

View File

@ -10,11 +10,13 @@ from erpnext.accounts.doctype.budget.budget import validate_expense_against_budg
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
class ClosedAccountingPeriod(frappe.ValidationError): pass
class StockAccountInvalidTransaction(frappe.ValidationError): pass class StockAccountInvalidTransaction(frappe.ValidationError): pass
def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False): def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False):
if gl_map: if gl_map:
if not cancel: if not cancel:
validate_accounting_period(gl_map)
gl_map = process_gl_map(gl_map, merge_entries) gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1: if gl_map and len(gl_map) > 1:
save_entries(gl_map, adv_adj, update_outstanding, from_repost) save_entries(gl_map, adv_adj, update_outstanding, from_repost)
@ -23,6 +25,27 @@ def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, upd
else: else:
delete_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) delete_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
def validate_accounting_period(gl_map):
accounting_periods = frappe.db.sql(""" SELECT
ap.name as name
FROM
`tabAccounting Period` ap, `tabClosed Document` cd
WHERE
ap.name = cd.parent
AND ap.company = %(company)s
AND cd.closed = 1
AND cd.document_type = %(voucher_type)s
AND %(date)s between ap.start_date and ap.end_date
""", {
'date': gl_map[0].posting_date,
'company': gl_map[0].company,
'voucher_type': gl_map[0].voucher_type
}, as_dict=1)
if accounting_periods:
frappe.throw(_("You can't create accounting entries in the closed accounting period {0}")
.format(accounting_periods[0].name), ClosedAccountingPeriod)
def process_gl_map(gl_map, merge_entries=True): def process_gl_map(gl_map, merge_entries=True):
if merge_entries: if merge_entries:
gl_map = merge_similar_entries(gl_map) gl_map = merge_similar_entries(gl_map)

View File

@ -124,8 +124,6 @@ def check_matching_amount(bank_account, company, transaction):
'txt': '%%%s%%' % amount 'txt': '%%%s%%' % amount
}, as_dict=True) }, as_dict=True)
frappe.errprint(journal_entries)
if transaction.credit > 0: if transaction.credit > 0:
sales_invoices = frappe.db.sql(""" sales_invoices = frappe.db.sql("""
SELECT SELECT

View File

@ -1762,18 +1762,11 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
this.si_docs = this.get_submitted_invoice() || []; this.si_docs = this.get_submitted_invoice() || [];
this.email_queue_list = this.get_email_queue() || {}; this.email_queue_list = this.get_email_queue() || {};
this.customers_list = this.get_customers_details() || {}; this.customers_list = this.get_customers_details() || {};
if(this.customer_doc) {
this.freeze = this.customer_doc.display
}
freeze_screen = this.freeze_screen || false;
if ((this.si_docs.length || this.email_queue_list || this.customers_list) && !this.freeze) {
this.freeze = true;
if (this.si_docs.length || this.email_queue_list || this.customers_list) {
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.sales_invoice.pos.make_invoice", method: "erpnext.accounts.doctype.sales_invoice.pos.make_invoice",
freeze: freeze_screen, freeze: true,
args: { args: {
doc_list: me.si_docs, doc_list: me.si_docs,
email_queue_list: me.email_queue_list, email_queue_list: me.email_queue_list,

View File

@ -469,7 +469,9 @@ def get_timeline_data(doctype, name):
# fetch and append data from Activity Log # fetch and append data from Activity Log
data += frappe.db.sql("""select {fields} data += frappe.db.sql("""select {fields}
from `tabActivity Log` from `tabActivity Log`
where reference_doctype={doctype} and reference_name={name} where (reference_doctype="{doctype}" and reference_name="{name}")
or (timeline_doctype in ("{doctype}") and timeline_name="{name}")
or (reference_doctype in ("Quotation", "Opportunity") and timeline_name="{name}")
and status!='Success' and creation > {after} and status!='Success' and creation > {after}
{group_by} order by creation desc {group_by} order by creation desc
""".format(doctype=frappe.db.escape(doctype), name=frappe.db.escape(name), fields=fields, """.format(doctype=frappe.db.escape(doctype), name=frappe.db.escape(name), fields=fields,

View File

@ -4,7 +4,7 @@
{%- macro render_currency(df, doc) -%} {%- macro render_currency(df, doc) -%}
<div class="row {% if df.bold %}important{% endif %} data-field"> <div class="row {% if df.bold %}important{% endif %} data-field">
<div class="col-xs-{{ "9" if df.fieldtype=="Check" else "5" }} <div class="col-xs-{{ "9" if df.fieldtype=="Check" else "5" }}
{%- if doc._align_labels_right %} text-right{%- endif -%}"> {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _(df.label) }}</label> <label>{{ _(df.label) }}</label>
</div> </div>
<div class="col-xs-{{ "3" if df.fieldtype=="Check" else "7" }} value"> <div class="col-xs-{{ "3" if df.fieldtype=="Check" else "7" }} value">
@ -23,7 +23,7 @@
{%- for charge in data -%} {%- for charge in data -%}
{%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%} {%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
<div class="row"> <div class="row">
<div class="col-xs-5 {%- if doc._align_labels_right %} text-right{%- endif -%}"> <div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ charge.get_formatted("description") }}</label></div> <label>{{ charge.get_formatted("description") }}</label></div>
<div class="col-xs-7 text-right"> <div class="col-xs-7 text-right">
{{ frappe.utils.fmt_money((charge.tax_amount)|int|abs, currency=doc.currency) }} {{ frappe.utils.fmt_money((charge.tax_amount)|int|abs, currency=doc.currency) }}
@ -103,8 +103,8 @@
{% for section in page %} {% for section in page %}
<div class="row section-break"> <div class="row section-break">
{% if section.columns.fields %} {% if section.columns.fields %}
{%- if doc._line_breaks and loop.index != 1 -%}<hr>{%- endif -%} {%- if doc.print_line_breaks and loop.index != 1 -%}<hr>{%- endif -%}
{%- if doc._show_section_headings and section.label and section.has_data -%} {%- if doc.print_section_headings and section.label and section.has_data -%}
<h4 class='col-sm-12'>{{ _(section.label) }}</h4> <h4 class='col-sm-12'>{{ _(section.label) }}</h4>
{% endif %} {% endif %}
{%- endif -%} {%- endif -%}

View File

@ -33,6 +33,9 @@ class ReceivablePayableReport(object):
columns += [_(args.get("party_type")) + ":Link/" + args.get("party_type") + ":200"] columns += [_(args.get("party_type")) + ":Link/" + args.get("party_type") + ":200"]
if party_naming_by == "Naming Series":
columns += [args.get("party_type") + " Name::110"]
if args.get("party_type") == 'Customer': if args.get("party_type") == 'Customer':
columns.append({ columns.append({
"label": _("Customer Contact"), "label": _("Customer Contact"),
@ -42,9 +45,6 @@ class ReceivablePayableReport(object):
"width": 100 "width": 100
}) })
if party_naming_by == "Naming Series":
columns += [args.get("party_type") + " Name::110"]
columns.append({ columns.append({
"label": _("Voucher Type"), "label": _("Voucher Type"),
"fieldtype": "Data", "fieldtype": "Data",
@ -197,8 +197,10 @@ class ReceivablePayableReport(object):
if self.filters.based_on_payment_terms and gl_entries_data: if self.filters.based_on_payment_terms and gl_entries_data:
self.payment_term_map = self.get_payment_term_detail(voucher_nos) self.payment_term_map = self.get_payment_term_detail(voucher_nos)
self.gle_inclusion_map = {}
for gle in gl_entries_data: for gle in gl_entries_data:
if self.is_receivable_or_payable(gle, self.dr_or_cr, future_vouchers, return_entries): if self.is_receivable_or_payable(gle, self.dr_or_cr, future_vouchers, return_entries):
self.gle_inclusion_map[gle.name] = True
outstanding_amount, credit_note_amount, payment_amount = self.get_outstanding_amount( outstanding_amount, credit_note_amount, payment_amount = self.get_outstanding_amount(
gle,self.filters.report_date, self.dr_or_cr, return_entries) gle,self.filters.report_date, self.dr_or_cr, return_entries)
temp_outstanding_amt = outstanding_amount temp_outstanding_amt = outstanding_amount
@ -409,7 +411,9 @@ class ReceivablePayableReport(object):
for e in self.get_gl_entries_for(gle.party, gle.party_type, gle.voucher_type, gle.voucher_no): for e in self.get_gl_entries_for(gle.party, gle.party_type, gle.voucher_type, gle.voucher_no):
if getdate(e.posting_date) <= report_date \ if getdate(e.posting_date) <= report_date \
and (e.name!=gle.name or (e.voucher_no in return_entries and not return_entries.get(e.voucher_no))): and (e.name!=gle.name or (e.voucher_no in return_entries and not return_entries.get(e.voucher_no))):
if e.name!=gle.name and self.gle_inclusion_map.get(e.name):
continue
self.gle_inclusion_map[e.name] = True
amount = flt(e.get(reverse_dr_or_cr), self.currency_precision) - flt(e.get(dr_or_cr), self.currency_precision) amount = flt(e.get(reverse_dr_or_cr), self.currency_precision) - flt(e.get(dr_or_cr), self.currency_precision)
if e.voucher_no not in return_entries: if e.voucher_no not in return_entries:
payment_amount += amount payment_amount += amount

View File

@ -425,9 +425,12 @@ def get_cost_centers_with_children(cost_centers):
all_cost_centers = [] all_cost_centers = []
for d in cost_centers: for d in cost_centers:
if frappe.db.exists("Cost Center", d):
lft, rgt = frappe.db.get_value("Cost Center", d, ["lft", "rgt"]) lft, rgt = frappe.db.get_value("Cost Center", d, ["lft", "rgt"])
children = frappe.get_all("Cost Center", filters={"lft": [">=", lft], "rgt": ["<=", rgt]}) children = frappe.get_all("Cost Center", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
all_cost_centers += [c.name for c in children] all_cost_centers += [c.name for c in children]
else:
frappe.throw(_("Cost Center: {0} does not exist".format(d)))
return list(set(all_cost_centers)) return list(set(all_cost_centers))

View File

@ -119,19 +119,11 @@ def get_gl_entries(filters):
select_fields = """, debit, credit, debit_in_account_currency, select_fields = """, debit, credit, debit_in_account_currency,
credit_in_account_currency """ credit_in_account_currency """
group_by_statement = ''
order_by_statement = "order by posting_date, account" order_by_statement = "order by posting_date, account"
if filters.get("group_by") == _("Group by Voucher"): if filters.get("group_by") == _("Group by Voucher"):
order_by_statement = "order by posting_date, voucher_type, voucher_no" order_by_statement = "order by posting_date, voucher_type, voucher_no"
if filters.get("group_by") == _("Group by Voucher (Consolidated)"):
group_by_statement = "group by voucher_type, voucher_no, account, cost_center"
select_fields = """, sum(debit) as debit, sum(credit) as credit,
sum(debit_in_account_currency) as debit_in_account_currency,
sum(credit_in_account_currency) as credit_in_account_currency"""
if filters.get("include_default_book_entries"): if filters.get("include_default_book_entries"):
filters['company_fb'] = frappe.db.get_value("Company", filters['company_fb'] = frappe.db.get_value("Company",
filters.get("company"), 'default_finance_book') filters.get("company"), 'default_finance_book')
@ -144,11 +136,10 @@ def get_gl_entries(filters):
against_voucher_type, against_voucher, account_currency, against_voucher_type, against_voucher, account_currency,
remarks, against, is_opening {select_fields} remarks, against, is_opening {select_fields}
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s {conditions} {group_by_statement} where company=%(company)s {conditions}
{order_by_statement} {order_by_statement}
""".format( """.format(
select_fields=select_fields, conditions=get_conditions(filters), select_fields=select_fields, conditions=get_conditions(filters),
group_by_statement=group_by_statement,
order_by_statement=order_by_statement order_by_statement=order_by_statement
), ),
filters, as_dict=1) filters, as_dict=1)
@ -185,7 +176,8 @@ def get_conditions(filters):
if not (filters.get("account") or filters.get("party") or if not (filters.get("account") or filters.get("party") or
filters.get("group_by") in ["Group by Account", "Group by Party"]): filters.get("group_by") in ["Group by Account", "Group by Party"]):
conditions.append("posting_date >=%(from_date)s") conditions.append("posting_date >=%(from_date)s")
conditions.append("posting_date <=%(to_date)s")
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
if filters.get("project"): if filters.get("project"):
conditions.append("project in %(project)s") conditions.append("project in %(project)s")
@ -286,6 +278,7 @@ def initialize_gle_map(gl_entries, filters):
def get_accountwise_gle(filters, gl_entries, gle_map): def get_accountwise_gle(filters, gl_entries, gle_map):
totals = get_totals_dict() totals = get_totals_dict()
entries = [] entries = []
consolidated_gle = OrderedDict()
group_by = group_by_field(filters.get('group_by')) group_by = group_by_field(filters.get('group_by'))
def update_value_in_dict(data, key, gle): def update_value_in_dict(data, key, gle):
@ -310,12 +303,20 @@ def get_accountwise_gle(filters, gl_entries, gle_map):
update_value_in_dict(totals, 'total', gle) update_value_in_dict(totals, 'total', gle)
if filters.get("group_by") != _('Group by Voucher (Consolidated)'): if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
gle_map[gle.get(group_by)].entries.append(gle) gle_map[gle.get(group_by)].entries.append(gle)
elif filters.get("group_by") == _('Group by Voucher (Consolidated)'):
key = (gle.get("voucher_type"), gle.get("voucher_no"),
gle.get("account"), gle.get("cost_center"))
if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle)
else: else:
entries.append(gle) update_value_in_dict(consolidated_gle, key, gle)
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle) update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
update_value_in_dict(totals, 'closing', gle) update_value_in_dict(totals, 'closing', gle)
for key, value in consolidated_gle.items():
entries.append(value)
return totals, entries return totals, entries
def get_result_as_list(data, filters): def get_result_as_list(data, filters):

View File

@ -84,7 +84,8 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year)) throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year))
@frappe.whitelist() @frappe.whitelist()
def get_balance_on(account=None, date=None, party_type=None, party=None, company=None, in_account_currency=True, cost_center=None): def get_balance_on(account=None, date=None, party_type=None, party=None, company=None,
in_account_currency=True, cost_center=None, ignore_account_permission=False):
if not account and frappe.form_dict.get("account"): if not account and frappe.form_dict.get("account"):
account = frappe.form_dict.get("account") account = frappe.form_dict.get("account")
if not date and frappe.form_dict.get("date"): if not date and frappe.form_dict.get("date"):
@ -140,7 +141,8 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company
if account: if account:
if not frappe.flags.ignore_account_permission: if not (frappe.flags.ignore_account_permission
or ignore_account_permission):
acc.check_permission("read") acc.check_permission("read")
if report_type == 'Profit and Loss': if report_type == 'Profit and Loss':

View File

@ -303,14 +303,17 @@ frappe.ui.form.on('Asset', {
}, },
set_depreciation_rate: function(frm, row) { set_depreciation_rate: function(frm, row) {
if (row.total_number_of_depreciations && row.frequency_of_depreciation) { if (row.total_number_of_depreciations && row.frequency_of_depreciation
&& row.expected_value_after_useful_life) {
frappe.call({ frappe.call({
method: "get_depreciation_rate", method: "get_depreciation_rate",
doc: frm.doc, doc: frm.doc,
args: row, args: row,
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
frappe.model.set_value(row.doctype, row.name, "rate_of_depreciation", r.message); frappe.flags.dont_change_rate = true;
frappe.model.set_value(row.doctype, row.name,
"rate_of_depreciation", flt(r.message, precision("rate_of_depreciation", row)));
} }
} }
}); });
@ -338,6 +341,14 @@ frappe.ui.form.on('Asset Finance Book', {
total_number_of_depreciations: function(frm, cdt, cdn) { total_number_of_depreciations: function(frm, cdt, cdn) {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
frm.events.set_depreciation_rate(frm, row); frm.events.set_depreciation_rate(frm, row);
},
rate_of_depreciation: function(frm, cdt, cdn) {
if(!frappe.flags.dont_change_rate) {
frappe.model.set_value(cdt, cdn, "expected_value_after_useful_life", 0);
}
frappe.flags.dont_change_rate = false;
} }
}); });

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, erpnext, math, json import frappe, erpnext, math, json
from frappe import _ from frappe import _
from six import string_types from six import string_types
from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, add_days
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset.depreciation \ from erpnext.assets.doctype.asset.depreciation \
@ -101,16 +101,15 @@ class Asset(AccountsController):
def set_depreciation_rate(self): def set_depreciation_rate(self):
for d in self.get("finance_books"): for d in self.get("finance_books"):
d.rate_of_depreciation = self.get_depreciation_rate(d, on_validate=True) d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True),
d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self): def make_depreciation_schedule(self):
depreciation_method = [d.depreciation_method for d in self.finance_books] if 'Manual' not in [d.depreciation_method for d in self.finance_books]:
if 'Manual' not in depreciation_method:
self.schedules = [] self.schedules = []
if not self.get("schedules") and self.available_for_use_date: if self.get("schedules") or not self.available_for_use_date:
total_depreciations = sum([d.total_number_of_depreciations for d in self.get('finance_books')]) return
for d in self.get('finance_books'): for d in self.get('finance_books'):
self.validate_asset_finance_books(d) self.validate_asset_finance_books(d)
@ -120,71 +119,52 @@ class Asset(AccountsController):
d.value_after_depreciation = value_after_depreciation d.value_after_depreciation = value_after_depreciation
no_of_depreciations = cint(d.total_number_of_depreciations - 1) - cint(self.number_of_depreciations_booked)
end_date = add_months(d.depreciation_start_date,
no_of_depreciations * cint(d.frequency_of_depreciation))
total_days = date_diff(end_date, self.available_for_use_date)
rate_per_day = (value_after_depreciation - d.get("expected_value_after_useful_life")) / total_days
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
cint(self.number_of_depreciations_booked) cint(self.number_of_depreciations_booked)
from_date = self.available_for_use_date has_pro_rata = self.check_is_pro_rata(d)
if number_of_pending_depreciations:
next_depr_date = getdate(add_months(self.available_for_use_date,
number_of_pending_depreciations * 12))
if (cint(frappe.db.get_value("Asset Settings", None, "schedule_based_on_fiscal_year")) == 1
and getdate(d.depreciation_start_date) < next_depr_date):
if has_pro_rata:
number_of_pending_depreciations += 1 number_of_pending_depreciations += 1
skip_row = False
for n in range(number_of_pending_depreciations): for n in range(number_of_pending_depreciations):
if n == list(range(number_of_pending_depreciations))[-1]: # If depreciation is already completed (for double declining balance)
schedule_date = add_months(self.available_for_use_date, n * 12) if skip_row: continue
previous_scheduled_date = add_months(d.depreciation_start_date, (n-1) * 12)
depreciation_amount = \
self.get_depreciation_amount_prorata_temporis(value_after_depreciation,
d, previous_scheduled_date, schedule_date)
elif n == list(range(number_of_pending_depreciations))[0]:
schedule_date = d.depreciation_start_date
depreciation_amount = \
self.get_depreciation_amount_prorata_temporis(value_after_depreciation,
d, self.available_for_use_date, schedule_date)
else:
schedule_date = add_months(d.depreciation_start_date, n * 12)
depreciation_amount = \
self.get_depreciation_amount_prorata_temporis(value_after_depreciation, d)
if value_after_depreciation != 0:
value_after_depreciation -= flt(depreciation_amount)
self.append("schedules", {
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
})
else:
for n in range(number_of_pending_depreciations):
schedule_date = add_months(d.depreciation_start_date,
n * cint(d.frequency_of_depreciation))
if d.depreciation_method in ("Straight Line", "Manual"):
days = date_diff(schedule_date, from_date)
if n == 0: days += 1
depreciation_amount = days * rate_per_day
from_date = schedule_date
else:
depreciation_amount = self.get_depreciation_amount(value_after_depreciation, depreciation_amount = self.get_depreciation_amount(value_after_depreciation,
d.total_number_of_depreciations, d) d.total_number_of_depreciations, d)
if depreciation_amount: if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
value_after_depreciation -= flt(depreciation_amount) schedule_date = add_months(d.depreciation_start_date,
n * cint(d.frequency_of_depreciation))
# For first row
if has_pro_rata and n==0:
depreciation_amount, days = get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation))
depreciation_amount, days = get_pro_rata_amt(d,
depreciation_amount, schedule_date, to_date)
schedule_date = add_days(schedule_date, days)
if not depreciation_amount: continue
value_after_depreciation -= flt(depreciation_amount,
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != d.expected_value_after_useful_life)
or value_after_depreciation < d.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0:
self.append("schedules", { self.append("schedules", {
"schedule_date": schedule_date, "schedule_date": schedule_date,
"depreciation_amount": depreciation_amount, "depreciation_amount": depreciation_amount,
@ -193,6 +173,17 @@ class Asset(AccountsController):
"finance_book_id": d.idx "finance_book_id": d.idx
}) })
def check_is_pro_rata(self, row):
has_pro_rata = False
days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
has_pro_rata = True
return has_pro_rata
def validate_asset_finance_books(self, row): def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
@ -261,31 +252,20 @@ class Asset(AccountsController):
return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation) return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation)
def get_depreciation_amount(self, depreciable_value, total_number_of_depreciations, row): def get_depreciation_amount(self, depreciable_value, total_number_of_depreciations, row):
if row.depreciation_method in ["Straight Line", "Manual"]: precision = self.precision("gross_purchase_amount")
amt = (flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) -
flt(self.opening_accumulated_depreciation))
depreciation_amount = amt * row.rate_of_depreciation
else:
depreciation_amount = flt(depreciable_value) * (flt(row.rate_of_depreciation) / 100)
value_after_depreciation = flt(depreciable_value) - depreciation_amount
if value_after_depreciation < flt(row.expected_value_after_useful_life):
depreciation_amount = flt(depreciable_value) - flt(row.expected_value_after_useful_life)
return depreciation_amount
def get_depreciation_amount_prorata_temporis(self, depreciable_value, row, start_date=None, end_date=None):
if start_date and end_date:
prorata_temporis = min(abs(flt(date_diff(str(end_date), str(start_date)))) / flt(frappe.db.get_value("Asset Settings", None, "number_of_days_in_fiscal_year")), 1)
else:
prorata_temporis = 1
if row.depreciation_method in ("Straight Line", "Manual"): if row.depreciation_method in ("Straight Line", "Manual"):
depreciation_left = (cint(row.total_number_of_depreciations) - cint(self.number_of_depreciations_booked))
if not depreciation_left:
frappe.msgprint(_("All the depreciations has been booked"))
depreciation_amount = flt(row.expected_value_after_useful_life)
return depreciation_amount
depreciation_amount = (flt(row.value_after_depreciation) - depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / (cint(row.total_number_of_depreciations) - flt(row.expected_value_after_useful_life)) / depreciation_left
cint(self.number_of_depreciations_booked)) * prorata_temporis
else: else:
depreciation_amount = self.get_depreciation_amount(depreciable_value, row.total_number_of_depreciations, row) depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100), precision)
return depreciation_amount return depreciation_amount
@ -301,9 +281,12 @@ class Asset(AccountsController):
flt(accumulated_depreciation_after_full_schedule), flt(accumulated_depreciation_after_full_schedule),
self.precision('gross_purchase_amount')) self.precision('gross_purchase_amount'))
if row.expected_value_after_useful_life < asset_value_after_full_schedule: if (row.expected_value_after_useful_life and
row.expected_value_after_useful_life < asset_value_after_full_schedule):
frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}") frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}")
.format(row.idx, asset_value_after_full_schedule)) .format(row.idx, asset_value_after_full_schedule))
elif not row.expected_value_after_useful_life:
row.expected_value_after_useful_life = asset_value_after_full_schedule
def validate_cancellation(self): def validate_cancellation(self):
if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"): if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"):
@ -412,15 +395,7 @@ class Asset(AccountsController):
if isinstance(args, string_types): if isinstance(args, string_types):
args = json.loads(args) args = json.loads(args)
number_of_depreciations_booked = 0
if self.is_existing_asset:
number_of_depreciations_booked = self.number_of_depreciations_booked
float_precision = cint(frappe.db.get_default("float_precision")) or 2 float_precision = cint(frappe.db.get_default("float_precision")) or 2
tot_no_of_depreciation = flt(args.get("total_number_of_depreciations")) - flt(number_of_depreciations_booked)
if args.get("depreciation_method") in ["Straight Line", "Manual"]:
return 1.0 / tot_no_of_depreciation
if args.get("depreciation_method") == 'Double Declining Balance': if args.get("depreciation_method") == 'Double Declining Balance':
return 200.0 / args.get("total_number_of_depreciations") return 200.0 / args.get("total_number_of_depreciations")
@ -600,3 +575,15 @@ def make_journal_entry(asset_name):
def is_cwip_accounting_disabled(): def is_cwip_accounting_disabled():
return cint(frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting")) return cint(frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting"))
def get_pro_rata_amt(row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days
def get_total_days(date, frequency):
period_start_date = add_months(date,
cint(frequency) * -1)
return date_diff(date, period_start_date)

View File

@ -88,23 +88,23 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name) asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2020-06-06' asset.available_for_use_date = '2030-01-01'
asset.purchase_date = '2020-06-06' asset.purchase_date = '2030-01-01'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 12,
"depreciation_start_date": "2020-06-06" "depreciation_start_date": "2030-12-31"
}) })
asset.save() asset.save()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
expected_schedules = [ expected_schedules = [
["2020-06-06", 147.54, 147.54], ["2030-12-31", 30000.00, 30000.00],
["2021-04-06", 44852.46, 45000.0], ["2031-12-31", 30000.00, 60000.00],
["2022-02-06", 45000.0, 90000.00] ["2032-12-31", 30000.00, 90000.00]
] ]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@ -118,20 +118,21 @@ class TestAsset(unittest.TestCase):
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.number_of_depreciations_booked = 1 asset.number_of_depreciations_booked = 1
asset.opening_accumulated_depreciation = 40000 asset.opening_accumulated_depreciation = 40000
asset.available_for_use_date = "2030-06-06"
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 12,
"depreciation_start_date": "2020-06-06" "depreciation_start_date": "2030-12-31"
}) })
asset.insert() asset.insert()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
asset.save() asset.save()
expected_schedules = [ expected_schedules = [
["2020-06-06", 164.47, 40164.47], ["2030-12-31", 14246.58, 54246.58],
["2021-04-06", 49835.53, 90000.00] ["2031-12-31", 25000.00, 79246.58],
["2032-06-06", 10753.42, 90000.00]
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules")] for d in asset.get("schedules")]
@ -145,24 +146,23 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name) asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2020-06-06' asset.available_for_use_date = '2030-01-01'
asset.purchase_date = '2020-06-06' asset.purchase_date = '2030-01-01'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Double Declining Balance", "depreciation_method": "Double Declining Balance",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 12,
"depreciation_start_date": "2020-06-06" "depreciation_start_date": '2030-12-31'
}) })
asset.insert() asset.insert()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
asset.save() asset.save()
expected_schedules = [ expected_schedules = [
["2020-06-06", 66666.67, 66666.67], ['2030-12-31', 66667.00, 66667.00],
["2021-04-06", 22222.22, 88888.89], ['2031-12-31', 22222.11, 88889.11],
["2022-02-06", 1111.11, 90000.0] ['2032-12-31', 1110.89, 90000.0]
] ]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@ -177,23 +177,21 @@ class TestAsset(unittest.TestCase):
asset.is_existing_asset = 1 asset.is_existing_asset = 1
asset.number_of_depreciations_booked = 1 asset.number_of_depreciations_booked = 1
asset.opening_accumulated_depreciation = 50000 asset.opening_accumulated_depreciation = 50000
asset.available_for_use_date = '2030-01-01'
asset.purchase_date = '2029-11-30'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Double Declining Balance", "depreciation_method": "Double Declining Balance",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 12,
"depreciation_start_date": "2020-06-06" "depreciation_start_date": "2030-12-31"
}) })
asset.insert() asset.insert()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
asset.save()
asset.save()
expected_schedules = [ expected_schedules = [
["2020-06-06", 33333.33, 83333.33], ["2030-12-31", 33333.50, 83333.50],
["2021-04-06", 6666.67, 90000.0] ["2031-12-31", 6666.50, 90000.0]
] ]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@ -209,25 +207,25 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name) asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.purchase_date = '2020-01-30' asset.purchase_date = '2030-01-30'
asset.is_existing_asset = 0 asset.is_existing_asset = 0
asset.available_for_use_date = "2020-01-30" asset.available_for_use_date = "2030-01-30"
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 12,
"depreciation_start_date": "2020-12-31" "depreciation_start_date": "2030-12-31"
}) })
asset.insert() asset.insert()
asset.save() asset.save()
expected_schedules = [ expected_schedules = [
["2020-12-31", 28000.0, 28000.0], ["2030-12-31", 27534.25, 27534.25],
["2021-12-31", 30000.0, 58000.0], ["2031-12-31", 30000.0, 57534.25],
["2022-12-31", 30000.0, 88000.0], ["2032-12-31", 30000.0, 87534.25],
["2023-01-30", 2000.0, 90000.0] ["2033-01-30", 2465.75, 90000.0]
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@ -266,8 +264,8 @@ class TestAsset(unittest.TestCase):
self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR")
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 32129.24), ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0),
("_Test Depreciations - _TC", 32129.24, 0.0) ("_Test Depreciations - _TC", 30000.0, 0.0)
) )
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
@ -277,15 +275,15 @@ class TestAsset(unittest.TestCase):
self.assertEqual(gle, expected_gle) self.assertEqual(gle, expected_gle)
self.assertEqual(asset.get("value_after_depreciation"), 0) self.assertEqual(asset.get("value_after_depreciation"), 0)
def test_depreciation_entry_for_wdv(self): def test_depreciation_entry_for_wdv_without_pro_rata(self):
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=8000.0, location="Test Location") qty=1, rate=8000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name) asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2030-06-06' asset.available_for_use_date = '2030-01-01'
asset.purchase_date = '2030-06-06' asset.purchase_date = '2030-01-01'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 1000, "expected_value_after_useful_life": 1000,
"depreciation_method": "Written Down Value", "depreciation_method": "Written Down Value",
@ -298,9 +296,41 @@ class TestAsset(unittest.TestCase):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [ expected_schedules = [
["2030-12-31", 4000.0, 4000.0], ["2030-12-31", 4000.00, 4000.00],
["2031-12-31", 2000.0, 6000.0], ["2031-12-31", 2000.00, 6000.00],
["2032-12-31", 1000.0, 7000.0], ["2032-12-31", 1000.00, 7000.0],
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
for d in asset.get("schedules")]
self.assertEqual(schedules, expected_schedules)
def test_pro_rata_depreciation_entry_for_wdv(self):
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=8000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
asset.available_for_use_date = '2030-06-06'
asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
"expected_value_after_useful_life": 1000,
"depreciation_method": "Written Down Value",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
asset.save(ignore_permissions=True)
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 2279.45, 2279.45],
["2031-12-31", 2860.28, 5139.73],
["2032-12-31", 1430.14, 6569.87],
["2033-06-06", 430.13, 7000.0],
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@ -346,18 +376,19 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name) asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2020-06-06' asset.available_for_use_date = nowdate()
asset.purchase_date = '2020-06-06' asset.purchase_date = nowdate()
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10,
"depreciation_start_date": "2020-06-06" "depreciation_start_date": nowdate()
}) })
asset.insert() asset.insert()
asset.submit() asset.submit()
post_depreciation_entries(date="2021-01-01")
post_depreciation_entries(date=add_months(nowdate(), 10))
scrap_asset(asset.name) scrap_asset(asset.name)
@ -366,9 +397,9 @@ class TestAsset(unittest.TestCase):
self.assertTrue(asset.journal_entry_for_scrap) self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 147.54, 0.0), ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 99852.46, 0.0) ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0)
) )
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
@ -412,9 +443,9 @@ class TestAsset(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 23051.47, 0.0), ("_Test Accumulated Depreciations - _TC", 20392.16, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 51948.53, 0.0), ("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0),
("Debtors - _TC", 25000.0, 0.0) ("Debtors - _TC", 25000.0, 0.0)
) )

View File

@ -46,75 +46,6 @@
"translatable": 0, "translatable": 0,
"unique": 0 "unique": 0
}, },
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "schedule_based_on_fiscal_year",
"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": "Calculate Prorated Depreciation Schedule Based on Fiscal Year",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "360",
"depends_on": "eval:doc.schedule_based_on_fiscal_year",
"description": "This value is used for pro-rata temporis calculation",
"fetch_if_empty": 0,
"fieldname": "number_of_days_in_fiscal_year",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Number of Days in Fiscal Year",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -159,7 +90,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2019-03-08 10:44:41.924547", "modified": "2019-05-26 18:31:19.930563",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Settings", "name": "Asset Settings",

View File

@ -483,7 +483,7 @@ def make_rm_stock_entry(purchase_order, rm_items):
'from_warehouse': rm_item_data["warehouse"], 'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"], 'stock_uom': rm_item_data["stock_uom"],
'main_item_code': rm_item_data["item_code"], 'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh[rm_item_code].get('allow_alternative_item') 'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
} }
} }
stock_entry.add_to_stock_entry_detail(items_dict) stock_entry.add_to_stock_entry_detail(items_dict)

View File

@ -0,0 +1,41 @@
# Version 12 Release Notes
### Accounting
1. [Accounting Dimensions](https://erpnext.com/docs/user/manual/en/accounts/accounting-dimensions)
1. [Chart of Accounts Importer](https://erpnext.com/docs/user/manual/en/setting-up/chart-of-accounts-importer)
1. [Invoice Discounting](https://erpnext.com/docs/user/manual/en/accounts/invoice_discounting)
1. [Tally Migrator](https://github.com/frappe/erpnext/pull/17405)
### Stock
1. [Serialized & Batched Item Reconciliation](https://erpnext.com/docs/user/manual/en/setting-up/stock-reconciliation#12-for-serialized-items)
1. [Auto Fetch Serialized Items](https://erpnext.com/version-12/release-notes/features#new-upload-dialog)
1. [Item Tax Templates](https://erpnext.com/docs/user/manual/en/accounts/item-tax-template)
### HR
1. [Auto Attendance](https://erpnext.com/docs/user/manual/en/human-resources/auto-attendance)
1. [Employee Skill Map](https://erpnext.com/docs/user/manual/en/human-resources/employee_skill_map)
1. [Encrypted Salary Slips](https://erpnext.com/docs/user/manual/en/human-resources/hr-settings#24-encrypt-salary-slips-in-emails)
1. [Leave Ledger](https://erpnext.com/docs/user/manual/en/human-resources/leave-ledger-entry)
1. [Staffing Plan](https://erpnext.com/docs/user/manual/en/human-resources/staffing-plan)
### CRM
1. [Promotional Scheme](https://erpnext.com/docs/user/manual/en/accounts/promotional-schemes)
1. [SLA](https://erpnext.com/docs/user/manual/en/support/service-level-agreement)
1. [Exotel Call Integration](https://erpnext.com/docs/user/manual/en/erpnext_integration/exotel_integration)
1. [Email Campaign](https://erpnext.com/docs/user/manual/en/CRM/email-campaign)
### Domain Specific Features
1. [Learning Management System](https://erpnext.com/docs/user/manual/en/education/setting-up-lms)
1. [Quality Management System](https://erpnext.com/docs/user/manual/en/quality-management)
1. [Production Planning Enhancements](https://erpnext.com/docs/user/manual/en/manufacturing/production-plan/planning-for-material-requests)
1. [Project Template](https://erpnext.com/docs/user/manual/en/projects/project-template)
### New Reports
1. [Bank Remittance](https://erpnext.com/docs/user/manual/en/human-resources/human-resources-reports#bank-remittance-report)
1. [BOM Explorer](https://erpnext.com/docs/user/manual/en/stock/articles/bom_explorer)
1. [Billing Summary Report](https://erpnext.com/docs/user/manual/en/projects/reports/billing_summary_reports)
1. [Procurement Tracker Report](docs/user/manual/en/buying/articles/procurement-tracker-report)
1. [Loan Repayment](https://erpnext.com/docs/user/manual/en/human-resources/human-resources-reports#loan-repayment-report)
1. [GSTR-3B](https://erpnext.com/docs/user/manual/en/regional/india/gst-3b-report)
1. [Sales Partner](https://erpnext.com/docs/user/manual/en/selling/sales-partner#sales-partner-reports)
1. [Sales Partner Target Variance based on Item Group](https://erpnext.com/docs/user/manual/en/selling/sales-partner#sales-partner-target-variance-based-on-item-group)

View File

@ -8,12 +8,18 @@
"from", "from",
"to", "to",
"column_break_3", "column_break_3",
"received_by",
"medium", "medium",
"caller_information",
"contact",
"contact_name",
"column_break_10",
"lead",
"lead_name",
"section_break_5", "section_break_5",
"status", "status",
"duration", "duration",
"recording_url", "recording_url"
"summary"
], ],
"fields": [ "fields": [
{ {
@ -60,12 +66,6 @@
"label": "Duration", "label": "Duration",
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "summary",
"fieldtype": "Data",
"label": "Summary",
"read_only": 1
},
{ {
"fieldname": "recording_url", "fieldname": "recording_url",
"fieldtype": "Data", "fieldtype": "Data",
@ -77,10 +77,58 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Medium", "label": "Medium",
"read_only": 1 "read_only": 1
},
{
"fieldname": "received_by",
"fieldtype": "Link",
"label": "Received By",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "caller_information",
"fieldtype": "Section Break",
"label": "Caller Information"
},
{
"fieldname": "contact",
"fieldtype": "Link",
"label": "Contact",
"options": "Contact",
"read_only": 1
},
{
"fieldname": "lead",
"fieldtype": "Link",
"label": "Lead ",
"options": "Lead",
"read_only": 1
},
{
"fetch_from": "contact.name",
"fieldname": "contact_name",
"fieldtype": "Data",
"hidden": 1,
"in_list_view": 1,
"label": "Contact Name",
"read_only": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fetch_from": "lead.lead_name",
"fieldname": "lead_name",
"fieldtype": "Data",
"hidden": 1,
"in_list_view": 1,
"label": "Lead Name",
"read_only": 1
} }
], ],
"in_create": 1, "in_create": 1,
"modified": "2019-07-01 09:09:48.516722", "modified": "2019-08-06 05:46:53.144683",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Communication", "module": "Communication",
"name": "Call Log", "name": "Call Log",
@ -97,10 +145,15 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"read": 1,
"role": "Employee"
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"title_field": "from", "title_field": "from",
"track_changes": 1 "track_changes": 1,
"track_views": 1
} }

View File

@ -4,16 +4,88 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.crm.doctype.utils import get_employee_emails_for_popup from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number
from frappe.contacts.doctype.contact.contact import get_contact_with_phone_number
from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number
class CallLog(Document): class CallLog(Document):
def before_insert(self):
number = strip_number(self.get('from'))
self.contact = get_contact_with_phone_number(number)
self.lead = get_lead_with_phone_number(number)
def after_insert(self): def after_insert(self):
employee_emails = get_employee_emails_for_popup(self.medium) self.trigger_call_popup()
for email in employee_emails:
frappe.publish_realtime('show_call_popup', self, user=email)
def on_update(self): def on_update(self):
doc_before_save = self.get_doc_before_save() doc_before_save = self.get_doc_before_save()
if doc_before_save and doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']: if not doc_before_save: return
if doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']:
frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self) frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self)
elif doc_before_save.to != self.to:
self.trigger_call_popup()
def trigger_call_popup(self):
scheduled_employees = get_scheduled_employees_for_popup(self.to)
employee_emails = get_employees_with_number(self.to)
# check if employees with matched number are scheduled to receive popup
emails = set(scheduled_employees).intersection(employee_emails)
# # if no employee found with matching phone number then show popup to scheduled employees
# emails = emails or scheduled_employees if employee_emails
for email in emails:
frappe.publish_realtime('show_call_popup', self, user=email)
@frappe.whitelist()
def add_call_summary(call_log, summary):
doc = frappe.get_doc('Call Log', call_log)
doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '<br><br>' + summary)
def get_employees_with_number(number):
number = strip_number(number)
if not number: return []
employee_emails = frappe.cache().hget('employees_with_number', number)
if employee_emails: return employee_emails
employees = frappe.get_all('Employee', filters={
'cell_number': ['like', '%{}%'.format(number)],
'user_id': ['!=', '']
}, fields=['user_id'])
employee_emails = [employee.user_id for employee in employees]
frappe.cache().hset('employees_with_number', number, employee_emails)
return employee
def set_caller_information(doc, state):
'''Called from hooks on creation of Lead or Contact'''
if doc.doctype not in ['Lead', 'Contact']: return
numbers = [doc.get('phone'), doc.get('mobile_no')]
# contact for Contact and lead for Lead
fieldname = doc.doctype.lower()
# contact_name or lead_name
display_name_field = '{}_name'.format(fieldname)
for number in numbers:
number = strip_number(number)
if not number: continue
filters = frappe._dict({
'from': ['like', '%{}'.format(number)],
fieldname: ''
})
logs = frappe.get_all('Call Log', filters=filters)
for log in logs:
frappe.db.set_value('Call Log', log.name, {
fieldname: doc.name,
display_name_field: doc.get_title()
}, update_modified=False)

View File

@ -41,6 +41,11 @@ def get_data():
"name": "Lead Source", "name": "Lead Source",
"description": _("Track Leads by Lead Source.") "description": _("Track Leads by Lead Source.")
}, },
{
"type": "doctype",
"name": "Contract",
"description": _("Helps you keep tracks of Contracts based on Supplier, Customer and Employee"),
},
] ]
}, },
{ {

View File

@ -134,6 +134,12 @@ def get_data():
"name": "Employee Leave Balance", "name": "Employee Leave Balance",
"doctype": "Leave Application" "doctype": "Leave Application"
}, },
{
"type": "report",
"is_query_report": True,
"name": "Leave Ledger Entry",
"doctype": "Leave Ledger Entry"
},
] ]
}, },
{ {
@ -160,6 +166,10 @@ def get_data():
"name": "Salary Slip", "name": "Salary Slip",
"onboard": 1, "onboard": 1,
}, },
{
"type": "doctype",
"name": "Payroll Period",
},
{ {
"type": "doctype", "type": "doctype",
"name": "Salary Component", "name": "Salary Component",
@ -209,6 +219,16 @@ def get_data():
"name": "Employee Benefit Claim", "name": "Employee Benefit Claim",
"dependencies": ["Employee"] "dependencies": ["Employee"]
}, },
{
"type": "doctype",
"name": "Employee Tax Exemption Category",
"dependencies": ["Employee"]
},
{
"type": "doctype",
"name": "Employee Tax Exemption Sub Category",
"dependencies": ["Employee"]
},
] ]
}, },
{ {

View File

@ -94,6 +94,13 @@ def get_data():
"name": "BOM Update Tool", "name": "BOM Update Tool",
"description": _("Replace BOM and update latest price in all BOMs"), "description": _("Replace BOM and update latest price in all BOMs"),
}, },
{
"type": "page",
"label": _("BOM Comparison Tool"),
"name": "bom-comparison-tool",
"description": _("Compare BOMs for changes in Raw Materials and Operations"),
"data_doctype": "BOM"
},
] ]
}, },
{ {

View File

@ -727,7 +727,7 @@ def get_items_from_bom(item_code, bom, exploded_item=1):
where where
t2.parent = t1.name and t1.item = %s t2.parent = t1.name and t1.item = %s
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
and t2.item_code = t3.name and t3.is_stock_item = 1""".format(doctype), and t2.item_code = t3.name""".format(doctype),
(item_code, bom), as_dict=1) (item_code, bom), as_dict=1)
if not bom_items: if not bom_items:

View File

@ -371,7 +371,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select tabAccount.name from `tabAccount` return frappe.db.sql("""select tabAccount.name from `tabAccount`
where (tabAccount.report_type = "Profit and Loss" where (tabAccount.report_type = "Profit and Loss"
or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed")) or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress"))
and tabAccount.is_group=0 and tabAccount.is_group=0
and tabAccount.docstatus!=2 and tabAccount.docstatus!=2
and tabAccount.{key} LIKE %(txt)s and tabAccount.{key} LIKE %(txt)s

View File

@ -18,18 +18,15 @@ def validate_return(doc):
validate_returned_items(doc) validate_returned_items(doc)
def validate_return_against(doc): def validate_return_against(doc):
filters = {"doctype": doc.doctype, "docstatus": 1, "company": doc.company} if not frappe.db.exists(doc.doctype, doc.return_against):
if doc.meta.get_field("customer") and doc.customer:
filters["customer"] = doc.customer
elif doc.meta.get_field("supplier") and doc.supplier:
filters["supplier"] = doc.supplier
if not frappe.db.exists(filters):
frappe.throw(_("Invalid {0}: {1}") frappe.throw(_("Invalid {0}: {1}")
.format(doc.meta.get_label("return_against"), doc.return_against)) .format(doc.meta.get_label("return_against"), doc.return_against))
else: else:
ref_doc = frappe.get_doc(doc.doctype, doc.return_against) ref_doc = frappe.get_doc(doc.doctype, doc.return_against)
party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier"
if ref_doc.company == doc.company and ref_doc.get(party_type) == doc.get(party_type) and ref_doc.docstatus == 1:
# validate posting date time # validate posting date time
return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00")
ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00") ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00")

View File

@ -45,6 +45,7 @@ class SellingController(StockController):
self.set_gross_profit() self.set_gross_profit()
set_default_income_account_for_item(self) set_default_income_account_for_item(self)
self.set_customer_address() self.set_customer_address()
self.validate_for_duplicate_items()
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
@ -381,6 +382,34 @@ class SellingController(StockController):
if self.get(address_field): if self.get(address_field):
self.set(address_display_field, get_address_display(self.get(address_field))) self.set(address_display_field, get_address_display(self.get(address_field)))
def validate_for_duplicate_items(self):
check_list, chk_dupl_itm = [], []
if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
return
for d in self.get('items'):
if self.doctype == "Sales Invoice":
e = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
f = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note":
e = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or '']
f = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice]
elif self.doctype == "Sales Order":
e = [d.item_code, d.description, d.warehouse, d.batch_no or '']
f = [d.item_code, d.description]
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
if e in check_list:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
else:
check_list.append(e)
else:
if f in chk_dupl_itm:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
else:
chk_dupl_itm.append(f)
def validate_items(self): def validate_items(self):
# validate items to see if they have is_sales_item enabled # validate items to see if they have is_sales_item enabled
from erpnext.controllers.buying_controller import validate_item_type from erpnext.controllers.buying_controller import validate_item_type

View File

@ -21,42 +21,45 @@ def get_list_context(context=None):
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"): def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
user = frappe.session.user user = frappe.session.user
key = None ignore_permissions = False
if not filters: filters = [] if not filters: filters = []
if doctype == 'Supplier Quotation': if doctype == 'Supplier Quotation':
filters.append((doctype, "docstatus", "<", 2)) filters.append((doctype, 'docstatus', '<', 2))
else: else:
filters.append((doctype, "docstatus", "=", 1)) filters.append((doctype, 'docstatus', '=', 1))
if (user != "Guest" and is_website_user()) or doctype == 'Request for Quotation': if (user != 'Guest' and is_website_user()) or doctype == 'Request for Quotation':
parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype
# find party for this contact # find party for this contact
customers, suppliers = get_customers_suppliers(parties_doctype, user) customers, suppliers = get_customers_suppliers(parties_doctype, user)
if not customers and not suppliers: return [] if customers:
if doctype == 'Quotation':
key, parties = get_party_details(customers, suppliers) filters.append(('quotation_to', '=', 'Customer'))
filters.append(('party_name', 'in', customers))
if doctype == 'Request for Quotation': else:
return rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length) filters.append(('customer', 'in', customers))
elif suppliers:
filters.append((doctype, key, "in", parties)) filters.append(('supplier', 'in', suppliers))
if key:
return post_process(doctype, get_list_for_transactions(doctype, txt,
filters=filters, fields="name",limit_start=limit_start,
limit_page_length=limit_page_length,ignore_permissions=True,
order_by="modified desc"))
else: else:
return [] return []
return post_process(doctype, get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length, if doctype == 'Request for Quotation':
fields="name", order_by="modified desc")) parties = customers or suppliers
return rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length)
# Since customers and supplier do not have direct access to internal doctypes
ignore_permissions = True
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
return post_process(doctype, transactions)
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20, def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,
ignore_permissions=False,fields=None, order_by=None): ignore_permissions=False, fields=None, order_by=None):
""" Get List of transactions like Invoices, Orders """ """ Get List of transactions like Invoices, Orders """
from frappe.www.list import get_list from frappe.www.list import get_list
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)
@ -83,16 +86,6 @@ def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_len
return data return data
def get_party_details(customers, suppliers):
if customers:
key, parties = "customer", customers
elif suppliers:
key, parties = "supplier", suppliers
else:
key, parties = "customer", []
return key, parties
def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length): def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length):
data = frappe.db.sql("""select distinct parent as name, supplier from `tab{doctype}` data = frappe.db.sql("""select distinct parent as name, supplier from `tab{doctype}`
where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""". where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""".
@ -130,6 +123,11 @@ def get_customers_suppliers(doctype, user):
suppliers = [] suppliers = []
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)
customer_field_name = get_customer_field_name(doctype)
has_customer_field = meta.has_field(customer_field_name)
has_supplier_field = meta.has_field('supplier')
if has_common(["Supplier", "Customer"], frappe.get_roles(user)): if has_common(["Supplier", "Customer"], frappe.get_roles(user)):
contacts = frappe.db.sql(""" contacts = frappe.db.sql("""
select select
@ -141,27 +139,40 @@ def get_customers_suppliers(doctype, user):
where where
`tabContact`.name=`tabDynamic Link`.parent and `tabContact`.email_id =%s `tabContact`.name=`tabDynamic Link`.parent and `tabContact`.email_id =%s
""", user, as_dict=1) """, user, as_dict=1)
customers = [c.link_name for c in contacts if c.link_doctype == 'Customer'] \ customers = [c.link_name for c in contacts if c.link_doctype == 'Customer']
if meta.get_field("customer") else None suppliers = [c.link_name for c in contacts if c.link_doctype == 'Supplier']
suppliers = [c.link_name for c in contacts if c.link_doctype == 'Supplier'] \
if meta.get_field("supplier") else None
elif frappe.has_permission(doctype, 'read', user=user): elif frappe.has_permission(doctype, 'read', user=user):
customers = [customer.name for customer in frappe.get_list("Customer")] \ customer_list = frappe.get_list("Customer")
if meta.get_field("customer") else None customers = suppliers = [customer.name for customer in customer_list]
suppliers = [supplier.name for supplier in frappe.get_list("Customer")] \
if meta.get_field("supplier") else None
return customers, suppliers return customers if has_customer_field else None, \
suppliers if has_supplier_field else None
def has_website_permission(doc, ptype, user, verbose=False): def has_website_permission(doc, ptype, user, verbose=False):
doctype = doc.doctype doctype = doc.doctype
customers, suppliers = get_customers_suppliers(doctype, user) customers, suppliers = get_customers_suppliers(doctype, user)
if customers: if customers:
return frappe.get_all(doctype, filters=[(doctype, "customer", "in", customers), return frappe.db.exists(doctype, get_customer_filter(doc, customers))
(doctype, "name", "=", doc.name)]) and True or False
elif suppliers: elif suppliers:
fieldname = 'suppliers' if doctype == 'Request for Quotation' else 'supplier' fieldname = 'suppliers' if doctype == 'Request for Quotation' else 'supplier'
return frappe.get_all(doctype, filters=[(doctype, fieldname, "in", suppliers), return frappe.db.exists(doctype, filters={
(doctype, "name", "=", doc.name)]) and True or False 'name': doc.name,
fieldname: ["in", suppliers]
})
else: else:
return False return False
def get_customer_filter(doc, customers):
doctype = doc.doctype
filters = frappe._dict()
filters.name = doc.name
filters[get_customer_field_name(doctype)] = ['in', customers]
if doctype == 'Quotation':
filters.quotation_to = 'Customer'
return filters
def get_customer_field_name(doctype):
if doctype == 'Quotation':
return 'party_name'
else:
return 'customer'

View File

@ -145,6 +145,16 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
@frappe.whitelist() @frappe.whitelist()
def make_opportunity(source_name, target_doc=None): def make_opportunity(source_name, target_doc=None):
def set_missing_values(source, target):
address = frappe.get_all('Dynamic Link', {
'link_doctype': source.doctype,
'link_name': source.name,
'parenttype': 'Address',
}, ['parent'], limit=1)
if address:
target.customer_address = address[0].parent
target_doc = get_mapped_doc("Lead", source_name, target_doc = get_mapped_doc("Lead", source_name,
{"Lead": { {"Lead": {
"doctype": "Opportunity", "doctype": "Opportunity",
@ -157,7 +167,7 @@ def make_opportunity(source_name, target_doc=None):
"email_id": "contact_email", "email_id": "contact_email",
"mobile_no": "contact_mobile" "mobile_no": "contact_mobile"
} }
}}, target_doc) }}, target_doc, set_missing_values)
return target_doc return target_doc
@ -230,3 +240,15 @@ def make_lead_from_communication(communication, ignore_communication_links=False
link_communication_to_document(doc, "Lead", lead_name, ignore_communication_links) link_communication_to_document(doc, "Lead", lead_name, ignore_communication_links)
return lead_name return lead_name
def get_lead_with_phone_number(number):
if not number: return
leads = frappe.get_all('Lead', or_filters={
'phone': ['like', '%{}'.format(number)],
'mobile_no': ['like', '%{}'.format(number)]
}, limit=1)
lead = leads[0].name if leads else None
return lead

View File

@ -31,9 +31,9 @@ frappe.ui.form.on("Opportunity", {
party_name: function(frm) { party_name: function(frm) {
frm.toggle_display("contact_info", frm.doc.party_name); frm.toggle_display("contact_info", frm.doc.party_name);
frm.trigger('set_contact_link');
if (frm.doc.opportunity_from == "Customer") { if (frm.doc.opportunity_from == "Customer") {
frm.trigger('set_contact_link');
erpnext.utils.get_party_details(frm); erpnext.utils.get_party_details(frm);
} else if (frm.doc.opportunity_from == "Lead") { } else if (frm.doc.opportunity_from == "Lead") {
erpnext.utils.map_current_doc({ erpnext.utils.map_current_doc({
@ -48,13 +48,6 @@ frappe.ui.form.on("Opportunity", {
frm.get_field("items").grid.set_multiple_add("item_code", "qty"); frm.get_field("items").grid.set_multiple_add("item_code", "qty");
}, },
party_name: function(frm) {
if (frm.doc.opportunity_from == "Customer") {
frm.trigger('set_contact_link');
erpnext.utils.get_party_details(frm);
}
},
with_items: function(frm) { with_items: function(frm) {
frm.trigger('toggle_mandatory'); frm.trigger('toggle_mandatory');
}, },

View File

@ -3,82 +3,59 @@ from frappe import _
import json import json
@frappe.whitelist() @frappe.whitelist()
def get_document_with_phone_number(number): def get_last_interaction(contact=None, lead=None):
# finds contacts and leads
if not number: return
number = number.lstrip('0')
number_filter = {
'phone': ['like', '%{}'.format(number)],
'mobile_no': ['like', '%{}'.format(number)]
}
contacts = frappe.get_all('Contact', or_filters=number_filter, limit=1)
if contacts: if not contact and not lead: return
return frappe.get_doc('Contact', contacts[0].name)
leads = frappe.get_all('Lead', or_filters=number_filter, limit=1) last_communication = None
last_issue = None
if leads: if contact:
return frappe.get_doc('Lead', leads[0].name)
@frappe.whitelist()
def get_last_interaction(number, reference_doc):
reference_doc = json.loads(reference_doc) if reference_doc else get_document_with_phone_number(number)
if not reference_doc: return
reference_doc = frappe._dict(reference_doc)
last_communication = {}
last_issue = {}
if reference_doc.doctype == 'Contact':
customer_name = ''
query_condition = '' query_condition = ''
for link in reference_doc.links: values = []
link = frappe._dict(link) contact = frappe.get_doc('Contact', contact)
for link in contact.links:
if link.link_doctype == 'Customer': if link.link_doctype == 'Customer':
customer_name = link.link_name last_issue = get_last_issue_from_customer(link.link_name)
query_condition += "(`reference_doctype`='{}' AND `reference_name`='{}') OR".format(link.link_doctype, link.link_name) query_condition += "(`reference_doctype`=%s AND `reference_name`=%s) OR"
values += [link_link_doctype, link_link_name]
if query_condition: if query_condition:
# remove extra appended 'OR'
query_condition = query_condition[:-2] query_condition = query_condition[:-2]
last_communication = frappe.db.sql(""" last_communication = frappe.db.sql("""
SELECT `name`, `content` SELECT `name`, `content`
FROM `tabCommunication` FROM `tabCommunication`
WHERE {} WHERE `sent_or_received`='Received'
AND ({})
ORDER BY `modified` ORDER BY `modified`
LIMIT 1 LIMIT 1
""".format(query_condition)) # nosec """.format(query_condition), values, as_dict=1) # nosec
if customer_name: if lead:
last_issue = frappe.get_all('Issue', {
'customer': customer_name
}, ['name', 'subject', 'customer'], limit=1)
elif reference_doc.doctype == 'Lead':
last_communication = frappe.get_all('Communication', filters={ last_communication = frappe.get_all('Communication', filters={
'reference_doctype': reference_doc.doctype, 'reference_doctype': 'Lead',
'reference_name': reference_doc.name, 'reference_name': lead,
'sent_or_received': 'Received' 'sent_or_received': 'Received'
}, fields=['name', 'content'], limit=1) }, fields=['name', 'content'], order_by='`creation` DESC', limit=1)
last_communication = last_communication[0] if last_communication else None
return { return {
'last_communication': last_communication[0] if last_communication else None, 'last_communication': last_communication,
'last_issue': last_issue[0] if last_issue else None 'last_issue': last_issue
} }
@frappe.whitelist() def get_last_issue_from_customer(customer_name):
def add_call_summary(docname, summary): issues = frappe.get_all('Issue', {
call_log = frappe.get_doc('Call Log', docname) 'customer': customer_name
summary = _('Call Summary by {0}: {1}').format( }, ['name', 'subject', 'customer'], order_by='`creation` DESC', limit=1)
frappe.utils.get_fullname(frappe.session.user), summary)
if not call_log.summary: return issues[0] if issues else None
call_log.summary = summary
else:
call_log.summary += '<br>' + summary def get_scheduled_employees_for_popup(communication_medium):
call_log.save(ignore_permissions=True) if not communication_medium: return []
def get_employee_emails_for_popup(communication_medium):
now_time = frappe.utils.nowtime() now_time = frappe.utils.nowtime()
weekday = frappe.utils.get_weekday() weekday = frappe.utils.get_weekday()
@ -98,3 +75,10 @@ def get_employee_emails_for_popup(communication_medium):
employee_emails = set([employee.user_id for employee in employees]) employee_emails = set([employee.user_id for employee in employees])
return employee_emails return employee_emails
def strip_number(number):
if not number: return
# strip 0 from the start of the number for proper number comparisions
# eg. 07888383332 should match with 7888383332
number = number.lstrip('0')
return number

View File

@ -1,843 +1,213 @@
{ {
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 1, "beta": 1,
"creation": "2018-07-10 14:48:16.757030", "creation": "2018-07-10 14:48:16.757030",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"status",
"application_settings",
"client_id",
"redirect_url",
"token_endpoint",
"application_column_break",
"client_secret",
"scope",
"api_endpoint",
"authorization_settings",
"authorization_endpoint",
"refresh_token",
"code",
"authorization_column_break",
"authorization_url",
"access_token",
"quickbooks_company_id",
"company_settings",
"company",
"default_shipping_account",
"default_warehouse",
"company_column_break",
"default_cost_center",
"undeposited_funds_account"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 1, "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": "Status", "label": "Status",
"length": 0, "options": "Connecting to QuickBooks\nConnected to QuickBooks\nIn Progress\nComplete\nFailed"
"no_copy": 0,
"options": "Connecting to QuickBooks\nConnected to QuickBooks\nIn Progress\nComplete\nFailed",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "eval:doc.client_id && doc.client_secret && doc.redirect_url", "collapsible_depends_on": "eval:doc.client_id && doc.client_secret && doc.redirect_url",
"columns": 0,
"fieldname": "application_settings", "fieldname": "application_settings",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Application Settings"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Application Settings",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "client_id", "fieldname": "client_id",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Client ID", "label": "Client ID",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "redirect_url", "fieldname": "redirect_url",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Redirect URL", "label": "Redirect URL",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer", "default": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
"fieldname": "token_endpoint", "fieldname": "token_endpoint",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Token Endpoint", "label": "Token Endpoint",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "application_column_break", "fieldname": "application_column_break",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "client_secret", "fieldname": "client_secret",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Client Secret", "label": "Client Secret",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "com.intuit.quickbooks.accounting", "default": "com.intuit.quickbooks.accounting",
"fieldname": "scope", "fieldname": "scope",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Scope", "label": "Scope",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "https://quickbooks.api.intuit.com/v3", "default": "https://quickbooks.api.intuit.com/v3",
"fieldname": "api_endpoint", "fieldname": "api_endpoint",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "API Endpoint", "label": "API Endpoint",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0,
"fieldname": "authorization_settings", "fieldname": "authorization_settings",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Authorization Settings"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Authorization Settings",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "https://appcenter.intuit.com/connect/oauth2", "default": "https://appcenter.intuit.com/connect/oauth2",
"fieldname": "authorization_endpoint", "fieldname": "authorization_endpoint",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Authorization Endpoint", "label": "Authorization Endpoint",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "refresh_token", "fieldname": "refresh_token",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0, "label": "Refresh Token"
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Refresh Token",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "code", "fieldname": "code",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0, "label": "Code"
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Code",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "authorization_column_break", "fieldname": "authorization_column_break",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "authorization_url", "fieldname": "authorization_url",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Authorization URL", "label": "Authorization URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "access_token", "fieldname": "access_token",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0, "label": "Access Token"
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Access Token",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "quickbooks_company_id", "fieldname": "quickbooks_company_id",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0, "label": "Quickbooks Company ID"
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Quickbooks Company ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company_settings", "fieldname": "company_settings",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0, "label": "Company Settings"
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Company Settings",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Company", "label": "Company",
"length": 0, "options": "Company"
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_shipping_account", "fieldname": "default_shipping_account",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "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": "Default Shipping Account", "label": "Default Shipping Account",
"length": 0, "options": "Account"
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_warehouse", "fieldname": "default_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "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": "Default Warehouse", "label": "Default Warehouse",
"length": 0, "options": "Warehouse"
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company_column_break", "fieldname": "company_column_break",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_cost_center", "fieldname": "default_cost_center",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "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": "Default Cost Center", "label": "Default Cost Center",
"length": 0, "options": "Cost Center"
"no_copy": 0,
"options": "Cost Center",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "undeposited_funds_account", "fieldname": "undeposited_funds_account",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "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": "Undeposited Funds Account", "label": "Undeposited Funds Account",
"length": 0, "options": "Account"
"no_copy": 0,
"options": "Account",
"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
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "modified": "2019-08-07 15:26:00.653433",
"max_attachments": 0,
"modified": "2018-10-17 03:12:53.506229",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "QuickBooks Migrator", "name": "QuickBooks Migrator",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 0,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC"
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@ -18,6 +18,8 @@ def handle_incoming_call(**kwargs):
call_log = get_call_log(call_payload) call_log = get_call_log(call_payload)
if not call_log: if not call_log:
create_call_log(call_payload) create_call_log(call_payload)
else:
update_call_log(call_payload, call_log=call_log)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def handle_end_call(**kwargs): def handle_end_call(**kwargs):
@ -27,10 +29,11 @@ def handle_end_call(**kwargs):
def handle_missed_call(**kwargs): def handle_missed_call(**kwargs):
update_call_log(kwargs, 'Missed') update_call_log(kwargs, 'Missed')
def update_call_log(call_payload, status): def update_call_log(call_payload, status='Ringing', call_log=None):
call_log = get_call_log(call_payload) call_log = call_log or get_call_log(call_payload)
if call_log: if call_log:
call_log.status = status call_log.status = status
call_log.to = call_payload.get('DialWhomNumber')
call_log.duration = call_payload.get('DialCallDuration') or 0 call_log.duration = call_payload.get('DialCallDuration') or 0
call_log.recording_url = call_payload.get('RecordingUrl') call_log.recording_url = call_payload.get('RecordingUrl')
call_log.save(ignore_permissions=True) call_log.save(ignore_permissions=True)
@ -48,7 +51,7 @@ def get_call_log(call_payload):
def create_call_log(call_payload): def create_call_log(call_payload):
call_log = frappe.new_doc('Call Log') call_log = frappe.new_doc('Call Log')
call_log.id = call_payload.get('CallSid') call_log.id = call_payload.get('CallSid')
call_log.to = call_payload.get('CallTo') call_log.to = call_payload.get('DialWhomNumber')
call_log.medium = call_payload.get('To') call_log.medium = call_payload.get('To')
call_log.status = 'Ringing' call_log.status = 'Ringing'
setattr(call_log, 'from', call_payload.get('CallFrom')) setattr(call_log, 'from', call_payload.get('CallFrom'))

View File

@ -231,8 +231,12 @@ doc_events = {
('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): { ('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): {
'validate': 'erpnext.regional.india.utils.set_place_of_supply' 'validate': 'erpnext.regional.india.utils.set_place_of_supply'
}, },
"Contact":{ "Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue" "on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information"
},
"Lead": {
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information"
}, },
"Email Unsubscribe": { "Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
@ -279,7 +283,9 @@ scheduler_events = {
"erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status" "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status"
], ],
"daily_long": [ "daily_long": [
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
"erpnext.hr.utils.generate_leave_encashment"
], ],
"monthly_long": [ "monthly_long": [
"erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income", "erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income",

View File

@ -4,6 +4,7 @@
"creation": "2013-01-10 16:34:13", "creation": "2013-01-10 16:34:13",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB",
"field_order": [ "field_order": [
"attendance_details", "attendance_details",
"naming_series", "naming_series",
@ -19,7 +20,9 @@
"department", "department",
"shift", "shift",
"attendance_request", "attendance_request",
"amended_from" "amended_from",
"late_entry",
"early_exit"
], ],
"fields": [ "fields": [
{ {
@ -153,12 +156,24 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Shift", "label": "Shift",
"options": "Shift Type" "options": "Shift Type"
},
{
"default": "0",
"fieldname": "late_entry",
"fieldtype": "Check",
"label": "Late Entry"
},
{
"default": "0",
"fieldname": "early_exit",
"fieldtype": "Check",
"label": "Early Exit"
} }
], ],
"icon": "fa fa-ok", "icon": "fa fa-ok",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-06-05 19:37:30.410071", "modified": "2019-07-29 20:35:40.845422",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Attendance", "name": "Attendance",

View File

@ -76,6 +76,7 @@ class Employee(NestedSet):
if self.user_id: if self.user_id:
self.update_user() self.update_user()
self.update_user_permissions() self.update_user_permissions()
self.reset_employee_emails_cache()
def update_user_permissions(self): def update_user_permissions(self):
if not self.create_user_permission: return if not self.create_user_permission: return
@ -214,6 +215,15 @@ class Employee(NestedSet):
doc.validate_employee_creation() doc.validate_employee_creation()
doc.db_set("employee", self.name) doc.db_set("employee", self.name)
def reset_employee_emails_cache(self):
prev_doc = self.get_doc_before_save() or {}
cell_number = self.get('cell_number')
prev_number = prev_doc.get('cell_number')
if (cell_number != prev_number or
self.get('user_id') != prev_doc.get('user_id')):
frappe.cache().hdel('employees_with_number', cell_number)
frappe.cache().hdel('employees_with_number', prev_number)
def get_timeline_data(doctype, name): def get_timeline_data(doctype, name):
'''Return timeline for attendance''' '''Return timeline for attendance'''
return dict(frappe.db.sql('''select unix_timestamp(attendance_date), count(*) return dict(frappe.db.sql('''select unix_timestamp(attendance_date), count(*)

View File

@ -14,8 +14,6 @@
"device_id", "device_id",
"skip_auto_attendance", "skip_auto_attendance",
"attendance", "attendance",
"entry_grace_period_consequence",
"exit_grace_period_consequence",
"shift_start", "shift_start",
"shift_end", "shift_end",
"shift_actual_start", "shift_actual_start",
@ -80,20 +78,6 @@
"options": "Attendance", "options": "Attendance",
"read_only": 1 "read_only": 1
}, },
{
"default": "0",
"fieldname": "entry_grace_period_consequence",
"fieldtype": "Check",
"hidden": 1,
"label": "Entry Grace Period Consequence"
},
{
"default": "0",
"fieldname": "exit_grace_period_consequence",
"fieldtype": "Check",
"hidden": 1,
"label": "Exit Grace Period Consequence"
},
{ {
"fieldname": "shift_start", "fieldname": "shift_start",
"fieldtype": "Datetime", "fieldtype": "Datetime",
@ -119,7 +103,7 @@
"label": "Shift Actual End" "label": "Shift Actual End"
} }
], ],
"modified": "2019-06-10 15:33:22.731697", "modified": "2019-07-23 23:47:33.975263",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Checkin", "name": "Employee Checkin",

View File

@ -72,7 +72,7 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
return doc return doc
def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, shift=None): def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, shift=None):
"""Creates an attendance and links the attendance to the Employee Checkin. """Creates an attendance and links the attendance to the Employee Checkin.
Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown. Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
@ -98,7 +98,9 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
'status': attendance_status, 'status': attendance_status,
'working_hours': working_hours, 'working_hours': working_hours,
'company': employee_doc.company, 'company': employee_doc.company,
'shift': shift 'shift': shift,
'late_entry': late_entry,
'early_exit': early_exit
} }
attendance = frappe.get_doc(doc_dict).insert() attendance = frappe.get_doc(doc_dict).insert()
attendance.submit() attendance.submit()
@ -124,11 +126,16 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out' :param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'
""" """
total_hours = 0 total_hours = 0
in_time = out_time = None
if check_in_out_type == 'Alternating entries as IN and OUT during the same shift': if check_in_out_type == 'Alternating entries as IN and OUT during the same shift':
in_time = logs[0].time
if len(logs) >= 2:
out_time = logs[-1].time
if working_hours_calc_type == 'First Check-in and Last Check-out': if working_hours_calc_type == 'First Check-in and Last Check-out':
# assumption in this case: First log always taken as IN, Last log always taken as OUT # assumption in this case: First log always taken as IN, Last log always taken as OUT
total_hours = time_diff_in_hours(logs[0].time, logs[-1].time) total_hours = time_diff_in_hours(in_time, logs[-1].time)
elif working_hours_calc_type == 'Every Valid Check-in and Check-out': elif working_hours_calc_type == 'Every Valid Check-in and Check-out':
logs = logs[:]
while len(logs) >= 2: while len(logs) >= 2:
total_hours += time_diff_in_hours(logs[0].time, logs[1].time) total_hours += time_diff_in_hours(logs[0].time, logs[1].time)
del logs[:2] del logs[:2]
@ -138,11 +145,15 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
first_in_log = logs[find_index_in_dict(logs, 'log_type', 'IN')] first_in_log = logs[find_index_in_dict(logs, 'log_type', 'IN')]
last_out_log = logs[len(logs)-1-find_index_in_dict(reversed(logs), 'log_type', 'OUT')] last_out_log = logs[len(logs)-1-find_index_in_dict(reversed(logs), 'log_type', 'OUT')]
if first_in_log and last_out_log: if first_in_log and last_out_log:
total_hours = time_diff_in_hours(first_in_log.time, last_out_log.time) in_time, out_time = first_in_log.time, last_out_log.time
total_hours = time_diff_in_hours(in_time, out_time)
elif working_hours_calc_type == 'Every Valid Check-in and Check-out': elif working_hours_calc_type == 'Every Valid Check-in and Check-out':
in_log = out_log = None in_log = out_log = None
for log in logs: for log in logs:
if in_log and out_log: if in_log and out_log:
if not in_time:
in_time = in_log.time
out_time = out_log.time
total_hours += time_diff_in_hours(in_log.time, out_log.time) total_hours += time_diff_in_hours(in_log.time, out_log.time)
in_log = out_log = None in_log = out_log = None
if not in_log: if not in_log:
@ -150,8 +161,9 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
elif not out_log: elif not out_log:
out_log = log if log.log_type == 'OUT' else None out_log = log if log.log_type == 'OUT' else None
if in_log and out_log: if in_log and out_log:
out_time = out_log.time
total_hours += time_diff_in_hours(in_log.time, out_log.time) total_hours += time_diff_in_hours(in_log.time, out_log.time)
return total_hours return total_hours, in_time, out_time
def time_diff_in_hours(start, end): def time_diff_in_hours(start, end):
return round((end-start).total_seconds() / 3600, 1) return round((end-start).total_seconds() / 3600, 1)

View File

@ -70,16 +70,16 @@ class TestEmployeeCheckin(unittest.TestCase):
logs_type_2 = [frappe._dict(x) for x in logs_type_2] logs_type_2 = [frappe._dict(x) for x in logs_type_2]
working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[0]) working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[0])
self.assertEqual(working_hours, 6.5) self.assertEqual(working_hours, (6.5, logs_type_1[0].time, logs_type_1[-1].time))
working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[1]) working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[1])
self.assertEqual(working_hours, 4.5) self.assertEqual(working_hours, (4.5, logs_type_1[0].time, logs_type_1[-1].time))
working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[0]) working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[0])
self.assertEqual(working_hours, 5) self.assertEqual(working_hours, (5, logs_type_2[1].time, logs_type_2[-1].time))
working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1]) working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1])
self.assertEqual(working_hours, 4.5) self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
def make_n_checkins(employee, n, hours_to_reverse=1): def make_n_checkins(employee, n, hours_to_reverse=1):
logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))] logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))]

View File

@ -24,6 +24,7 @@
"column_break_18", "column_break_18",
"leave_approver_mandatory_in_leave_application", "leave_approver_mandatory_in_leave_application",
"show_leaves_of_all_department_members_in_calendar", "show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
"hiring_settings", "hiring_settings",
"check_vacancies" "check_vacancies"
], ],
@ -153,12 +154,18 @@
"fieldname": "check_vacancies", "fieldname": "check_vacancies",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation" "label": "Check Vacancies On Job Offer Creation"
},
{
"default": "0",
"fieldname": "auto_leave_encashment",
"fieldtype": "Check",
"label": "Auto Leave Encashment"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"modified": "2019-07-01 18:59:55.256878", "modified": "2019-08-05 13:07:17.993968",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR Settings", "name": "HR Settings",

View File

@ -21,11 +21,41 @@ frappe.ui.form.on("Leave Allocation", {
}) })
}, },
refresh: function(frm) {
if(frm.doc.docstatus === 1 && frm.doc.expired) {
var valid_expiry = moment(frappe.datetime.get_today()).isBetween(frm.doc.from_date, frm.doc.to_date);
if(valid_expiry) {
// expire current allocation
frm.add_custom_button(__('Expire Allocation'), function() {
frm.trigger("expire_allocation");
});
}
}
},
expire_allocation: function(frm) {
frappe.call({
method: 'erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.expire_allocation',
args: {
'allocation': frm.doc,
'expiry_date': frappe.datetime.get_today()
},
freeze: true,
callback: function(r){
if(!r.exc){
frappe.msgprint(__("Allocation Expired!"));
}
frm.refresh();
}
});
},
employee: function(frm) { employee: function(frm) {
frm.trigger("calculate_total_leaves_allocated"); frm.trigger("calculate_total_leaves_allocated");
}, },
leave_type: function(frm) { leave_type: function(frm) {
frm.trigger("leave_policy");
frm.trigger("calculate_total_leaves_allocated"); frm.trigger("calculate_total_leaves_allocated");
}, },
@ -33,37 +63,38 @@ frappe.ui.form.on("Leave Allocation", {
frm.trigger("calculate_total_leaves_allocated"); frm.trigger("calculate_total_leaves_allocated");
}, },
carry_forwarded_leaves: function(frm) { unused_leaves: function(frm) {
frm.set_value("total_leaves_allocated", frm.set_value("total_leaves_allocated",
flt(frm.doc.carry_forwarded_leaves) + flt(frm.doc.new_leaves_allocated)); flt(frm.doc.unused_leaves) + flt(frm.doc.new_leaves_allocated));
}, },
new_leaves_allocated: function(frm) { new_leaves_allocated: function(frm) {
frm.set_value("total_leaves_allocated", frm.set_value("total_leaves_allocated",
flt(frm.doc.carry_forwarded_leaves) + flt(frm.doc.new_leaves_allocated)); flt(frm.doc.unused_leaves) + flt(frm.doc.new_leaves_allocated));
}, },
leave_policy: function(frm) {
if(frm.doc.leave_policy && frm.doc.leave_type) {
frappe.db.get_value("Leave Policy Detail",{
'parent': frm.doc.leave_policy,
'leave_type': frm.doc.leave_type
}, 'annual_allocation', (r) => {
if (r && !r.exc) frm.set_value("new_leaves_allocated", flt(r.annual_allocation));
}, "Leave Policy");
}
},
calculate_total_leaves_allocated: function(frm) { calculate_total_leaves_allocated: function(frm) {
if (cint(frm.doc.carry_forward) == 1 && frm.doc.leave_type && frm.doc.employee) { if (cint(frm.doc.carry_forward) == 1 && frm.doc.leave_type && frm.doc.employee) {
return frappe.call({ return frappe.call({
method: "erpnext.hr.doctype.leave_allocation.leave_allocation.get_carry_forwarded_leaves", method: "set_total_leaves_allocated",
args: { doc: frm.doc,
"employee": frm.doc.employee,
"date": frm.doc.from_date,
"leave_type": frm.doc.leave_type,
"carry_forward": frm.doc.carry_forward
},
callback: function(r) { callback: function(r) {
if (!r.exc && r.message) { frm.refresh_fields();
frm.set_value('carry_forwarded_leaves', r.message);
frm.set_value("total_leaves_allocated",
flt(r.message) + flt(frm.doc.new_leaves_allocated));
}
} }
}) })
} else if (cint(frm.doc.carry_forward) == 0) { } else if (cint(frm.doc.carry_forward) == 0) {
frm.set_value("carry_forwarded_leaves", 0); frm.set_value("unused_leaves", 0);
frm.set_value("total_leaves_allocated", flt(frm.doc.new_leaves_allocated)); frm.set_value("total_leaves_allocated", flt(frm.doc.new_leaves_allocated));
} }
} }
}) });

View File

@ -1,683 +1,220 @@
{ {
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 0,
"autoname": "naming_series:", "autoname": "naming_series:",
"beta": 0,
"creation": "2013-02-20 19:10:38", "creation": "2013-02-20 19:10:38",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"naming_series",
"employee",
"employee_name",
"department",
"column_break1",
"leave_type",
"from_date",
"to_date",
"section_break_6",
"new_leaves_allocated",
"carry_forward",
"unused_leaves",
"total_leaves_allocated",
"total_leaves_encashed",
"column_break_10",
"compensatory_request",
"leave_period",
"leave_policy",
"carry_forwarded_leaves_count",
"expired",
"amended_from",
"notes",
"description"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Series", "label": "Series",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "HR-LAL-.YYYY.-", "options": "HR-LAL-.YYYY.-",
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "set_only_once": 1
"set_only_once": 1,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "employee", "fieldname": "employee",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Employee", "label": "Employee",
"length": 0,
"no_copy": 0,
"oldfieldname": "employee", "oldfieldname": "employee",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Employee", "options": "Employee",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.employee_name", "fetch_from": "employee.employee_name",
"fieldname": "employee_name", "fieldname": "employee_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Employee Name", "label": "Employee Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "search_index": 1
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.department", "fetch_from": "employee.department",
"fieldname": "department", "fieldname": "department",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Department", "label": "Department",
"length": 0,
"no_copy": 0,
"options": "Department", "options": "Department",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break1", "fieldname": "column_break1",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"width": "50%" "width": "50%"
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "leave_type", "fieldname": "leave_type",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Leave Type", "label": "Leave Type",
"length": 0,
"no_copy": 0,
"oldfieldname": "leave_type", "oldfieldname": "leave_type",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Leave Type", "options": "Leave Type",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_date", "fieldname": "from_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "From Date", "label": "From Date",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_date", "fieldname": "to_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "To Date", "label": "To Date",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Allocation"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allocation",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 1, "bold": 1,
"collapsible": 0,
"columns": 0,
"fieldname": "new_leaves_allocated", "fieldname": "new_leaves_allocated",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "New Leaves Allocated"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "New Leaves Allocated",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "carry_forward", "fieldname": "carry_forward",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Add unused leaves from previous allocations"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Add unused leaves from previous allocations",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "carry_forward", "depends_on": "carry_forward",
"fieldname": "carry_forwarded_leaves", "fieldname": "unused_leaves",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Unused leaves", "label": "Unused leaves",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_leaves_allocated", "fieldname": "total_leaves_allocated",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Leaves Allocated", "label": "Total Leaves Allocated",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.total_leaves_encashed>0", "depends_on": "eval:doc.total_leaves_encashed>0",
"fieldname": "total_leaves_encashed", "fieldname": "total_leaves_encashed",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Leaves Encashed", "label": "Total Leaves Encashed",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "compensatory_request", "fieldname": "compensatory_request",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Compensatory Leave Request", "label": "Compensatory Leave Request",
"length": 0,
"no_copy": 0,
"options": "Compensatory Leave Request", "options": "Compensatory Leave Request",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "leave_period", "fieldname": "leave_period",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Leave Period", "label": "Leave Period",
"length": 0,
"no_copy": 0,
"options": "Leave Period", "options": "Leave Period",
"permlevel": 0, "read_only": 1
"precision": "", },
"print_hide": 0, {
"print_hide_if_no_value": 0, "fetch_from": "employee.leave_policy",
"read_only": 1, "fieldname": "leave_policy",
"remember_last_selected_value": 0, "fieldtype": "Link",
"report_hide": 0, "in_standard_filter": 1,
"reqd": 0, "label": "Leave Policy",
"search_index": 0, "options": "Leave Policy",
"set_only_once": 0, "read_only": 1
"translatable": 0, },
"unique": 0 {
"default": "0",
"fieldname": "expired",
"fieldtype": "Check",
"hidden": 1,
"in_standard_filter": 1,
"label": "Expired",
"read_only": 1
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From", "label": "Amended From",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "amended_from", "oldfieldname": "amended_from",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Leave Allocation", "options": "Leave Allocation",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0,
"fieldname": "notes", "fieldname": "notes",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Notes"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Notes",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description", "label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "reason", "oldfieldname": "reason",
"oldfieldtype": "Small Text", "oldfieldtype": "Small Text",
"permlevel": 0,
"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,
"width": "300px" "width": "300px"
},
{
"depends_on": "carry_forwarded_leaves_count",
"fieldname": "carry_forwarded_leaves_count",
"fieldtype": "Float",
"label": "Carry Forwarded Leaves",
"read_only": 1
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-ok", "icon": "fa fa-ok",
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"issingle": 0, "modified": "2019-08-08 15:08:42.440909",
"istable": 0,
"max_attachments": 0,
"modified": "2019-01-30 11:28:09.360525",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Allocation", "name": "Leave Allocation",
@ -689,15 +226,10 @@
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR User", "role": "HR User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
@ -709,28 +241,19 @@
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 1, "import": 1,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "employee,employee_name,leave_type,total_leaves_allocated", "search_fields": "employee,employee_name,leave_type,total_leaves_allocated",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"timeline_field": "employee", "timeline_field": "employee"
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@ -3,11 +3,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import flt, date_diff, formatdate from frappe.utils import flt, date_diff, formatdate, add_days, today, getdate
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.utils import set_employee_name, get_leave_period
from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class BackDatedAllocationError(frappe.ValidationError): pass class BackDatedAllocationError(frappe.ValidationError): pass
@ -40,14 +40,18 @@ class LeaveAllocation(Document):
frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period")\ frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period")\
.format(self.leave_type, self.employee)) .format(self.leave_type, self.employee))
def on_update_after_submit(self): def on_submit(self):
self.validate_new_leaves_allocated_value() self.create_leave_ledger_entry()
self.set_total_leaves_allocated()
frappe.db.set(self,'carry_forwarded_leaves', flt(self.carry_forwarded_leaves)) # expire all unused leaves in the ledger on creation of carry forward allocation
frappe.db.set(self,'total_leaves_allocated',flt(self.total_leaves_allocated)) allocation = get_previous_allocation(self.from_date, self.leave_type, self.employee)
if self.carry_forward and allocation:
expire_allocation(allocation)
self.validate_against_leave_applications() def on_cancel(self):
self.create_leave_ledger_entry(submit=False)
if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
def validate_period(self): def validate_period(self):
if date_diff(self.to_date, self.from_date) <= 0: if date_diff(self.to_date, self.from_date) <= 0:
@ -87,13 +91,32 @@ class LeaveAllocation(Document):
BackDatedAllocationError) BackDatedAllocationError)
def set_total_leaves_allocated(self): def set_total_leaves_allocated(self):
self.carry_forwarded_leaves = get_carry_forwarded_leaves(self.employee, self.unused_leaves = get_carry_forwarded_leaves(self.employee,
self.leave_type, self.from_date, self.carry_forward) self.leave_type, self.from_date, self.carry_forward)
self.total_leaves_allocated = flt(self.carry_forwarded_leaves) + flt(self.new_leaves_allocated) self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated)
if self.carry_forward:
self.maintain_carry_forwarded_leaves()
self.set_carry_forwarded_leaves_in_previous_allocation()
if not self.total_leaves_allocated and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave") and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory"): if not self.total_leaves_allocated and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave") and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory"):
frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}".format(self.leave_type))) frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}").format(self.leave_type))
def maintain_carry_forwarded_leaves(self):
''' Reduce the carry forwarded leaves to be within the maximum allowed leaves '''
max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")
if self.new_leaves_allocated <= max_leaves_allowed <= self.total_leaves_allocated:
self.unused_leaves = max_leaves_allowed - flt(self.new_leaves_allocated)
self.total_leaves_allocated = flt(max_leaves_allowed)
def set_carry_forwarded_leaves_in_previous_allocation(self, on_cancel=False):
''' Set carry forwarded leaves in previous allocation '''
previous_allocation = get_previous_allocation(self.from_date, self.leave_type, self.employee)
if on_cancel:
self.unused_leaves = 0.0
frappe.db.set_value("Leave Allocation", previous_allocation.name, 'carry_forwarded_leaves_count', self.unused_leaves)
def validate_total_leaves_allocated(self): def validate_total_leaves_allocated(self):
# Adding a day to include To Date in the difference # Adding a day to include To Date in the difference
@ -101,15 +124,37 @@ class LeaveAllocation(Document):
if date_difference < self.total_leaves_allocated: if date_difference < self.total_leaves_allocated:
frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError) frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError)
def validate_against_leave_applications(self): def create_leave_ledger_entry(self, submit=True):
leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, if self.unused_leaves:
self.from_date, self.to_date) expiry_days = frappe.db.get_value("Leave Type", self.leave_type, "expire_carry_forwarded_leaves_after_days")
end_date = add_days(self.from_date, expiry_days - 1) if expiry_days else self.to_date
args = dict(
leaves=self.unused_leaves,
from_date=self.from_date,
to_date= min(getdate(end_date), getdate(self.to_date)),
is_carry_forward=1
)
create_leave_ledger_entry(self, args, submit)
if flt(leaves_taken) > flt(self.total_leaves_allocated): args = dict(
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): leaves=self.new_leaves_allocated,
frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) from_date=self.from_date,
else: to_date=self.to_date,
frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError) is_carry_forward=0
)
create_leave_ledger_entry(self, args, submit)
def get_previous_allocation(from_date, leave_type, employee):
''' Returns document properties of previous allocation '''
return frappe.db.get_value("Leave Allocation",
filters={
'to_date': ("<", from_date),
'leave_type': leave_type,
'employee': employee,
'docstatus': 1
},
order_by='to_date DESC',
fieldname=['name', 'from_date', 'to_date', 'employee', 'leave_type'], as_dict=1)
def get_leave_allocation_for_period(employee, leave_type, from_date, to_date): def get_leave_allocation_for_period(employee, leave_type, from_date, to_date):
leave_allocated = 0 leave_allocated = 0
@ -136,24 +181,27 @@ def get_leave_allocation_for_period(employee, leave_type, from_date, to_date):
@frappe.whitelist() @frappe.whitelist()
def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None): def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None):
carry_forwarded_leaves = 0 ''' Returns carry forwarded leaves for the given employee '''
unused_leaves = 0.0
if carry_forward: previous_allocation = get_previous_allocation(date, leave_type, employee)
if carry_forward and previous_allocation:
validate_carry_forward(leave_type) validate_carry_forward(leave_type)
unused_leaves = get_unused_leaves(employee, leave_type, previous_allocation.from_date, previous_allocation.to_date)
previous_allocation = frappe.db.sql(""" return unused_leaves
select name, from_date, to_date, total_leaves_allocated
from `tabLeave Allocation`
where employee=%s and leave_type=%s and docstatus=1 and to_date < %s
order by to_date desc limit 1
""", (employee, leave_type, date), as_dict=1)
if previous_allocation:
leaves_taken = get_approved_leaves_for_period(employee, leave_type,
previous_allocation[0].from_date, previous_allocation[0].to_date)
carry_forwarded_leaves = flt(previous_allocation[0].total_leaves_allocated) - flt(leaves_taken) def get_unused_leaves(employee, leave_type, from_date, to_date):
''' Returns unused leaves between the given period while skipping leave allocation expiry '''
return carry_forwarded_leaves leaves = frappe.get_all("Leave Ledger Entry", filters={
'employee': employee,
'leave_type': leave_type,
'from_date': ('>=', from_date),
'to_date': ('<=', to_date)
}, or_filters={
'is_expired': 0,
'is_carry_forward': 1
}, fields=['sum(leaves) as leaves'])
return flt(leaves[0]['leaves'])
def validate_carry_forward(leave_type): def validate_carry_forward(leave_type):
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):

View File

@ -12,4 +12,9 @@ def get_data():
'items': ['Leave Encashment'] 'items': ['Leave Encashment']
} }
], ],
'reports': [
{
'items': ['Employee Leave Balance']
}
]
} }

View File

@ -0,0 +1,11 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// render
frappe.listview_settings['Leave Allocation'] = {
get_indicator: function(doc) {
if(doc.status==="Expired") {
return [__("Expired"), "darkgrey", "expired, =, 1"];
}
},
};

View File

@ -34,7 +34,7 @@ QUnit.test("Test: Leave allocation [HR]", function (assert) {
() => assert.equal(today_date, cur_frm.doc.from_date, () => assert.equal(today_date, cur_frm.doc.from_date,
"from date correctly set"), "from date correctly set"),
// check for total leaves // check for total leaves
() => assert.equal(cur_frm.doc.carry_forwarded_leaves + 2, cur_frm.doc.total_leaves_allocated, () => assert.equal(cur_frm.doc.unused_leaves + 2, cur_frm.doc.total_leaves_allocated,
"total leave calculation is correctly set"), "total leave calculation is correctly set"),
() => done() () => done()
]); ]);

View File

@ -1,7 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import getdate from frappe.utils import nowdate, add_months, getdate, add_days
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation
class TestLeaveAllocation(unittest.TestCase): class TestLeaveAllocation(unittest.TestCase):
def test_overlapping_allocation(self): def test_overlapping_allocation(self):
@ -38,7 +40,7 @@ class TestLeaveAllocation(unittest.TestCase):
def test_invalid_period(self): def test_invalid_period(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
d = frappe.get_doc({ doc = frappe.get_doc({
"doctype": "Leave Allocation", "doctype": "Leave Allocation",
"__islocal": 1, "__islocal": 1,
"employee": employee.name, "employee": employee.name,
@ -50,11 +52,11 @@ class TestLeaveAllocation(unittest.TestCase):
}) })
#invalid period #invalid period
self.assertRaises(frappe.ValidationError, d.save) self.assertRaises(frappe.ValidationError, doc.save)
def test_allocated_leave_days_over_period(self): def test_allocated_leave_days_over_period(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
d = frappe.get_doc({ doc = frappe.get_doc({
"doctype": "Leave Allocation", "doctype": "Leave Allocation",
"__islocal": 1, "__islocal": 1,
"employee": employee.name, "employee": employee.name,
@ -64,8 +66,100 @@ class TestLeaveAllocation(unittest.TestCase):
"to_date": getdate("2015-09-30"), "to_date": getdate("2015-09-30"),
"new_leaves_allocated": 35 "new_leaves_allocated": 35
}) })
#allocated leave more than period #allocated leave more than period
self.assertRaises(frappe.ValidationError, d.save) self.assertRaises(frappe.ValidationError, doc.save)
def test_carry_forward_calculation(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
leave_type.submit()
# initial leave allocation
leave_allocation = create_leave_allocation(
leave_type="_Test_CF_leave",
from_date=add_months(nowdate(), -12),
to_date=add_months(nowdate(), -1),
carry_forward=0)
leave_allocation.submit()
# leave allocation with carry forward from previous allocation
leave_allocation_1 = create_leave_allocation(
leave_type="_Test_CF_leave",
carry_forward=1)
leave_allocation_1.submit()
self.assertEquals(leave_allocation.total_leaves_allocated, leave_allocation_1.unused_leaves)
def test_carry_forward_leaves_expiry(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90)
leave_type.submit()
# initial leave allocation
leave_allocation = create_leave_allocation(
leave_type="_Test_CF_leave_expiry",
from_date=add_months(nowdate(), -24),
to_date=add_months(nowdate(), -12),
carry_forward=0)
leave_allocation.submit()
leave_allocation = create_leave_allocation(
leave_type="_Test_CF_leave_expiry",
from_date=add_days(nowdate(), -90),
to_date=add_days(nowdate(), 100),
carry_forward=1)
leave_allocation.submit()
# expires all the carry forwarded leaves after 90 days
process_expired_allocation()
# leave allocation with carry forward of only new leaves allocated
leave_allocation_1 = create_leave_allocation(
leave_type="_Test_CF_leave_expiry",
carry_forward=1,
from_date=add_months(nowdate(), 6),
to_date=add_months(nowdate(), 12))
leave_allocation_1.submit()
self.assertEquals(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated)
def test_creation_of_leave_ledger_entry_on_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name))
self.assertEquals(len(leave_ledger_entry), 1)
self.assertEquals(leave_ledger_entry[0].employee, leave_allocation.employee)
self.assertEquals(leave_ledger_entry[0].leave_type, leave_allocation.leave_type)
self.assertEquals(leave_ledger_entry[0].leaves, leave_allocation.new_leaves_allocated)
# check if leave ledger entry is deleted on cancellation
leave_allocation.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
def create_leave_allocation(**args):
args = frappe._dict(args)
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
leave_allocation = frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": args.employee or employee.name,
"employee_name": args.employee_name or employee.employee_name,
"leave_type": args.leave_type or "_Test Leave Type",
"from_date": args.from_date or nowdate(),
"new_leaves_allocated": args.new_leaves_created or 15,
"carry_forward": args.carry_forward or 0,
"to_date": args.to_date or add_months(nowdate(), 12)
})
return leave_allocation
test_dependencies = ["Employee", "Leave Type"] test_dependencies = ["Employee", "Leave Type"]

View File

@ -49,7 +49,7 @@ frappe.ui.form.on("Leave Application", {
async: false, async: false,
args: { args: {
employee: frm.doc.employee, employee: frm.doc.employee,
date: frm.doc.posting_date date: frm.doc.from_date || frm.doc.posting_date
}, },
callback: function(r) { callback: function(r) {
if (!r.exc && r.message['leave_allocation']) { if (!r.exc && r.message['leave_allocation']) {
@ -60,9 +60,8 @@ frappe.ui.form.on("Leave Application", {
} }
} }
}); });
$("div").remove(".form-dashboard-section"); $("div").remove(".form-dashboard-section");
let section = frm.dashboard.add_section( frm.dashboard.add_section(
frappe.render_template('leave_application_dashboard', { frappe.render_template('leave_application_dashboard', {
data: leave_details data: leave_details
}) })
@ -115,6 +114,7 @@ frappe.ui.form.on("Leave Application", {
}, },
from_date: function(frm) { from_date: function(frm) {
frm.trigger("make_dashboard");
frm.trigger("half_day_datepicker"); frm.trigger("half_day_datepicker");
frm.trigger("calculate_total_days"); frm.trigger("calculate_total_days");
}, },
@ -138,12 +138,13 @@ frappe.ui.form.on("Leave Application", {
}, },
get_leave_balance: function(frm) { get_leave_balance: function(frm) {
if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date) { if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) {
return frappe.call({ return frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on", method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on",
args: { args: {
employee: frm.doc.employee, employee: frm.doc.employee,
date: frm.doc.from_date, date: frm.doc.from_date,
to_date: frm.doc.to_date,
leave_type: frm.doc.leave_type, leave_type: frm.doc.leave_type,
consider_all_leaves_in_the_allocation_period: true consider_all_leaves_in_the_allocation_period: true
}, },

File diff suppressed because it is too large Load Diff

View File

@ -5,11 +5,12 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \ from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \
comma_or, get_fullname, add_days, nowdate comma_or, get_fullname, add_days, nowdate, get_datetime_str
from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.utils import set_employee_name, get_leave_period
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
class LeaveDayBlockedError(frappe.ValidationError): pass class LeaveDayBlockedError(frappe.ValidationError): pass
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
@ -50,6 +51,7 @@ class LeaveApplication(Document):
# notify leave applier about approval # notify leave applier about approval
self.notify_employee() self.notify_employee()
self.create_leave_ledger_entry()
self.reload() self.reload()
def on_cancel(self): def on_cancel(self):
@ -57,6 +59,7 @@ class LeaveApplication(Document):
# notify leave applier about cancellation # notify leave applier about cancellation
self.notify_employee() self.notify_employee()
self.cancel_attendance() self.cancel_attendance()
self.create_leave_ledger_entry(submit=False)
def validate_applicable_after(self): def validate_applicable_after(self):
if self.leave_type: if self.leave_type:
@ -193,9 +196,9 @@ class LeaveApplication(Document):
frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave.")) frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave."))
if not is_lwp(self.leave_type): if not is_lwp(self.leave_type):
self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, docname=self.name, self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
consider_all_leaves_in_the_allocation_period=True) consider_all_leaves_in_the_allocation_period=True)
if self.status != "Rejected" and self.leave_balance < self.total_leave_days: if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}") frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}")
.format(self.leave_type)) .format(self.leave_type))
@ -347,6 +350,54 @@ class LeaveApplication(Document):
except frappe.OutgoingEmailError: except frappe.OutgoingEmailError:
pass pass
def create_leave_ledger_entry(self, submit=True):
expiry_date = get_allocation_expiry(self.employee, self.leave_type,
self.to_date, self.from_date)
lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp")
if expiry_date:
self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
else:
args = dict(
leaves=self.total_leave_days * -1,
from_date=self.from_date,
to_date=self.to_date,
is_lwp=lwp
)
create_leave_ledger_entry(self, args, submit)
def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
''' splits leave application into two ledger entries to consider expiry of allocation '''
args = dict(
from_date=self.from_date,
to_date=expiry_date,
leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
is_lwp=lwp
)
create_leave_ledger_entry(self, args, submit)
if getdate(expiry_date) != getdate(self.to_date):
start_date = add_days(expiry_date, 1)
args.update(dict(
from_date=start_date,
to_date=self.to_date,
leaves=date_diff(self.to_date, expiry_date) * -1
))
create_leave_ledger_entry(self, args, submit)
def get_allocation_expiry(employee, leave_type, to_date, from_date):
''' Returns expiry of carry forward allocation in leave ledger entry '''
expiry = frappe.get_all("Leave Ledger Entry",
filters={
'employee': employee,
'leave_type': leave_type,
'is_carry_forward': 1,
'transaction_type': 'Leave Allocation',
'to_date': ['between', (from_date, to_date)]
},fields=['to_date'])
return expiry[0]['to_date'] if expiry else None
@frappe.whitelist() @frappe.whitelist()
def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None): def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None):
number_of_days = 0 number_of_days = 0
@ -364,14 +415,16 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day
@frappe.whitelist() @frappe.whitelist()
def get_leave_details(employee, date): def get_leave_details(employee, date):
allocation_records = get_leave_allocation_records(date, employee).get(employee, frappe._dict()) allocation_records = get_leave_allocation_records(employee, date)
leave_allocation = {} leave_allocation = {}
for d in allocation_records: for d in allocation_records:
allocation = allocation_records.get(d, frappe._dict()) allocation = allocation_records.get(d, frappe._dict())
date = allocation.to_date remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, date, status="Approved") consider_all_leaves_in_the_allocation_period=True)
leaves_pending = get_leaves_for_period(employee, d, allocation.from_date, date, status="Open") end_date = allocation.to_date
remaining_leaves = allocation.total_leaves_allocated - leaves_taken - leaves_pending leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1
leaves_pending = get_pending_leaves_for_period(employee, d, allocation.from_date, end_date)
leave_allocation[d] = { leave_allocation[d] = {
"total_leaves": allocation.total_leaves_allocated, "total_leaves": allocation.total_leaves_allocated,
"leaves_taken": leaves_taken, "leaves_taken": leaves_taken,
@ -386,27 +439,131 @@ def get_leave_details(employee, date):
return ret return ret
@frappe.whitelist() @frappe.whitelist()
def get_leave_balance_on(employee, leave_type, date, allocation_records=None, docname=None, def get_leave_balance_on(employee, leave_type, date, to_date=nowdate(), consider_all_leaves_in_the_allocation_period=False):
consider_all_leaves_in_the_allocation_period=False, consider_encashed_leaves=True): '''
Returns leave balance till date
:param employee: employee name
:param leave_type: leave type
:param date: date to check balance on
:param to_date: future date to check for allocation expiry
:param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date
'''
if allocation_records == None: allocation_records = get_leave_allocation_records(employee, date, leave_type)
allocation_records = get_leave_allocation_records(date, employee).get(employee, frappe._dict())
allocation = allocation_records.get(leave_type, frappe._dict()) allocation = allocation_records.get(leave_type, frappe._dict())
if consider_all_leaves_in_the_allocation_period:
date = allocation.to_date
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, date, status="Approved", docname=docname)
leaves_encashed = 0
if frappe.db.get_value("Leave Type", leave_type, 'allow_encashment') and consider_encashed_leaves:
leaves_encashed = flt(allocation.total_leaves_encashed)
return flt(allocation.total_leaves_allocated) - (flt(leaves_taken) + flt(leaves_encashed)) end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
expiry = get_allocation_expiry(employee, leave_type, to_date, date)
def get_leaves_for_period(employee, leave_type, from_date, to_date, status, docname=None): leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
leave_applications = frappe.db.sql("""
select name, employee, leave_type, from_date, to_date, total_leave_days return get_remaining_leaves(allocation, leaves_taken, date, expiry)
from `tabLeave Application`
def get_leave_allocation_records(employee, date, leave_type=None):
''' returns the total allocated leaves and carry forwarded leaves based on ledger entries '''
conditions = ("and leave_type='%s'" % leave_type) if leave_type else ""
allocation_details = frappe.db.sql("""
SELECT
SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves,
SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves,
MIN(from_date) as from_date,
MAX(to_date) as to_date,
leave_type
FROM `tabLeave Ledger Entry`
WHERE
from_date <= %(date)s
AND to_date >= %(date)s
AND docstatus=1
AND transaction_type="Leave Allocation"
AND employee=%(employee)s
AND is_expired=0
AND is_lwp=0
{0}
GROUP BY employee, leave_type
""".format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec
allocated_leaves = frappe._dict()
for d in allocation_details:
allocated_leaves.setdefault(d.leave_type, frappe._dict({
"from_date": d.from_date,
"to_date": d.to_date,
"total_leaves_allocated": flt(d.cf_leaves) + flt(d.new_leaves),
"unused_leaves": d.cf_leaves,
"new_leaves_allocated": d.new_leaves,
"leave_type": d.leave_type
}))
return allocated_leaves
def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
''' Returns leaves that are pending approval '''
return frappe.db.get_value("Leave Application",
filters={
"employee": employee,
"leave_type": leave_type,
"from_date": ("<=", from_date),
"to_date": (">=", to_date),
"status": "Open"
}, fieldname=['SUM(total_leave_days)']) or flt(0)
def get_remaining_leaves(allocation, leaves_taken, date, expiry):
''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
def _get_remaining_leaves(allocated_leaves, end_date):
remaining_leaves = flt(allocated_leaves) + flt(leaves_taken)
if remaining_leaves > 0:
remaining_days = date_diff(end_date, date) + 1
remaining_leaves = min(remaining_days, remaining_leaves)
return remaining_leaves
total_leaves = allocation.total_leaves_allocated
if expiry and allocation.unused_leaves:
remaining_leaves = _get_remaining_leaves(allocation.unused_leaves, expiry)
total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves)
return _get_remaining_leaves(total_leaves, allocation.to_date)
def get_leaves_for_period(employee, leave_type, from_date, to_date):
leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
leave_days = 0
for leave_entry in leave_entries:
inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date)
if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
leave_days += leave_entry.leaves
elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \
and not skip_expiry_leaves(leave_entry, to_date):
leave_days += leave_entry.leaves
else:
if leave_entry.from_date < getdate(from_date):
leave_entry.from_date = from_date
if leave_entry.to_date > getdate(to_date):
leave_entry.to_date = to_date
leave_days += get_number_of_leave_days(employee, leave_type,
leave_entry.from_date, leave_entry.to_date) * -1
return leave_days
def skip_expiry_leaves(leave_entry, date):
''' Checks whether the expired leaves coincide with the to_date of leave balance check '''
end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date'])
return True if end_date == date and not leave_entry.is_carry_forward else False
def get_leave_entries(employee, leave_type, from_date, to_date):
''' Returns leave entries between from_date and to_date '''
return frappe.db.sql("""
select employee, leave_type, from_date, to_date, leaves, transaction_type, is_carry_forward
from `tabLeave Ledger Entry`
where employee=%(employee)s and leave_type=%(leave_type)s where employee=%(employee)s and leave_type=%(leave_type)s
and status = %(status)s and docstatus != 2 and docstatus=1
and leaves<0
and (from_date between %(from_date)s and %(to_date)s and (from_date between %(from_date)s and %(to_date)s
or to_date between %(from_date)s and %(to_date)s or to_date between %(from_date)s and %(to_date)s
or (from_date < %(from_date)s and to_date > %(to_date)s)) or (from_date < %(from_date)s and to_date > %(to_date)s))
@ -414,43 +571,8 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, status, docn
"from_date": from_date, "from_date": from_date,
"to_date": to_date, "to_date": to_date,
"employee": employee, "employee": employee,
"status": status,
"leave_type": leave_type "leave_type": leave_type
}, as_dict=1) }, as_dict=1)
leave_days = 0
for leave_app in leave_applications:
if docname and leave_app.name == docname:
continue
if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
leave_days += leave_app.total_leave_days
else:
if leave_app.from_date < getdate(from_date):
leave_app.from_date = from_date
if leave_app.to_date > getdate(to_date):
leave_app.to_date = to_date
leave_days += get_number_of_leave_days(employee, leave_type,
leave_app.from_date, leave_app.to_date)
return leave_days
def get_leave_allocation_records(date, employee=None):
conditions = (" and employee='%s'" % employee) if employee else ""
leave_allocation_records = frappe.db.sql("""
select employee, leave_type, total_leaves_allocated, total_leaves_encashed, from_date, to_date
from `tabLeave Allocation`
where %s between from_date and to_date and docstatus=1 {0}""".format(conditions), (date), as_dict=1)
allocated_leaves = frappe._dict()
for d in leave_allocation_records:
allocated_leaves.setdefault(d.employee, frappe._dict()).setdefault(d.leave_type, frappe._dict({
"from_date": d.from_date,
"to_date": d.to_date,
"total_leaves_allocated": d.total_leaves_allocated,
"total_leaves_encashed":d.total_leaves_encashed
}))
return allocated_leaves
@frappe.whitelist() @frappe.whitelist()
def get_holidays(employee, from_date, to_date): def get_holidays(employee, from_date, to_date):

View File

@ -0,0 +1,14 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'reports': [
{
'label': _('Reports'),
'items': ['Employee Leave Balance']
}
]
}

View File

@ -5,6 +5,8 @@ frappe.listview_settings['Leave Application'] = {
return [__("Approved"), "green", "status,=,Approved"]; return [__("Approved"), "green", "status,=,Approved"];
} else if (doc.status === "Rejected") { } else if (doc.status === "Rejected") {
return [__("Rejected"), "red", "status,=,Rejected"]; return [__("Rejected"), "red", "status,=,Rejected"];
} else {
return [__("Open"), "red", "status,=,Open"];
} }
} }
}; };

View File

@ -7,7 +7,9 @@ import unittest
from erpnext.hr.doctype.leave_application.leave_application import LeaveDayBlockedError, OverlapError, NotAnOptionalHoliday, get_leave_balance_on from erpnext.hr.doctype.leave_application.leave_application import LeaveDayBlockedError, OverlapError, NotAnOptionalHoliday, get_leave_balance_on
from frappe.permissions import clear_user_permissions_for_doctype from frappe.permissions import clear_user_permissions_for_doctype
from frappe.utils import add_days, nowdate, now_datetime, getdate from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
test_dependencies = ["Leave Allocation", "Leave Block List"] test_dependencies = ["Leave Allocation", "Leave Block List"]
@ -17,6 +19,7 @@ _test_records = [
"doctype": "Leave Application", "doctype": "Leave Application",
"employee": "_T-Employee-00001", "employee": "_T-Employee-00001",
"from_date": "2013-05-01", "from_date": "2013-05-01",
"description": "_Test Reason",
"leave_type": "_Test Leave Type", "leave_type": "_Test Leave Type",
"posting_date": "2013-01-02", "posting_date": "2013-01-02",
"to_date": "2013-05-05" "to_date": "2013-05-05"
@ -26,6 +29,7 @@ _test_records = [
"doctype": "Leave Application", "doctype": "Leave Application",
"employee": "_T-Employee-00002", "employee": "_T-Employee-00002",
"from_date": "2013-05-01", "from_date": "2013-05-01",
"description": "_Test Reason",
"leave_type": "_Test Leave Type", "leave_type": "_Test Leave Type",
"posting_date": "2013-01-02", "posting_date": "2013-01-02",
"to_date": "2013-05-05" "to_date": "2013-05-05"
@ -35,6 +39,7 @@ _test_records = [
"doctype": "Leave Application", "doctype": "Leave Application",
"employee": "_T-Employee-00001", "employee": "_T-Employee-00001",
"from_date": "2013-01-15", "from_date": "2013-01-15",
"description": "_Test Reason",
"leave_type": "_Test Leave Type LWP", "leave_type": "_Test Leave Type LWP",
"posting_date": "2013-01-02", "posting_date": "2013-01-02",
"to_date": "2013-01-15" "to_date": "2013-01-15"
@ -44,8 +49,8 @@ _test_records = [
class TestLeaveApplication(unittest.TestCase): class TestLeaveApplication(unittest.TestCase):
def setUp(self): def setUp(self):
for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]: for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab%s`" % dt) frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -268,13 +273,14 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application', doctype = 'Leave Application',
employee = employee.name, employee = employee.name,
company = '_Test Company', company = '_Test Company',
description = "_Test Reason",
leave_type = leave_type, leave_type = leave_type,
from_date = date, from_date = date,
to_date = date, to_date = date,
)) ))
# can only apply on optional holidays # can only apply on optional holidays
self.assertTrue(NotAnOptionalHoliday, leave_application.insert) self.assertRaises(NotAnOptionalHoliday, leave_application.insert)
leave_application.from_date = today leave_application.from_date = today
leave_application.to_date = today leave_application.to_date = today
@ -285,7 +291,6 @@ class TestLeaveApplication(unittest.TestCase):
# check leave balance is reduced # check leave balance is reduced
self.assertEqual(get_leave_balance_on(employee.name, leave_type, today), 9) self.assertEqual(get_leave_balance_on(employee.name, leave_type, today), 9)
def test_leaves_allowed(self): def test_leaves_allowed(self):
employee = get_employee() employee = get_employee()
leave_period = get_leave_period() leave_period = get_leave_period()
@ -304,21 +309,22 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application', doctype = 'Leave Application',
employee = employee.name, employee = employee.name,
leave_type = leave_type.name, leave_type = leave_type.name,
description = "_Test Reason",
from_date = date, from_date = date,
to_date = add_days(date, 2), to_date = add_days(date, 2),
company = "_Test Company", company = "_Test Company",
docstatus = 1, docstatus = 1,
status = "Approved" status = "Approved"
)) ))
leave_application.submit()
self.assertTrue(leave_application.insert())
leave_application = frappe.get_doc(dict( leave_application = frappe.get_doc(dict(
doctype = 'Leave Application', doctype = 'Leave Application',
employee = employee.name, employee = employee.name,
leave_type = leave_type.name, leave_type = leave_type.name,
description = "_Test Reason",
from_date = add_days(date, 4), from_date = add_days(date, 4),
to_date = add_days(date, 7), to_date = add_days(date, 8),
company = "_Test Company", company = "_Test Company",
docstatus = 1, docstatus = 1,
status = "Approved" status = "Approved"
@ -342,6 +348,7 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application', doctype = 'Leave Application',
employee = employee.name, employee = employee.name,
leave_type = leave_type.name, leave_type = leave_type.name,
description = "_Test Reason",
from_date = date, from_date = date,
to_date = add_days(date, 4), to_date = add_days(date, 4),
company = "_Test Company", company = "_Test Company",
@ -363,6 +370,7 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application', doctype = 'Leave Application',
employee = employee.name, employee = employee.name,
leave_type = leave_type_1.name, leave_type = leave_type_1.name,
description = "_Test Reason",
from_date = date, from_date = date,
to_date = add_days(date, 4), to_date = add_days(date, 4),
company = "_Test Company", company = "_Test Company",
@ -392,6 +400,7 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application', doctype = 'Leave Application',
employee = employee.name, employee = employee.name,
leave_type = leave_type.name, leave_type = leave_type.name,
description = "_Test Reason",
from_date = date, from_date = date,
to_date = add_days(date, 4), to_date = add_days(date, 4),
company = "_Test Company", company = "_Test Company",
@ -401,6 +410,18 @@ class TestLeaveApplication(unittest.TestCase):
self.assertRaises(frappe.ValidationError, leave_application.insert) self.assertRaises(frappe.ValidationError, leave_application.insert)
def test_leave_balance_near_allocaton_expiry(self):
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90)
leave_type.submit()
create_carry_forwarded_allocation(employee, leave_type)
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
def test_earned_leave(self): def test_earned_leave(self):
leave_period = get_leave_period() leave_period = get_leave_period()
employee = get_employee() employee = get_employee()
@ -447,6 +468,7 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application', doctype = 'Leave Application',
employee = employee.name, employee = employee.name,
leave_type = leave_type, leave_type = leave_type,
description = "_Test Reason",
from_date = '2018-10-02', from_date = '2018-10-02',
to_date = '2018-10-02', to_date = '2018-10-02',
company = '_Test Company', company = '_Test Company',
@ -457,9 +479,103 @@ class TestLeaveApplication(unittest.TestCase):
leave_application.submit() leave_application.submit()
self.assertEqual(leave_application.docstatus, 1) self.assertEqual(leave_application.docstatus, 1)
def make_allocation_record(employee=None, leave_type=None): def test_creation_of_leave_ledger_entry_on_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`") employee = get_employee()
leave_type = create_leave_type(leave_type_name = 'Test Leave Type 1')
leave_type.save()
leave_allocation = create_leave_allocation(employee=employee.name, employee_name=employee.employee_name,
leave_type=leave_type.name)
leave_allocation.submit()
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
from_date = add_days(nowdate(), 1),
to_date = add_days(nowdate(), 4),
description = "_Test Reason",
company = "_Test Company",
docstatus = 1,
status = "Approved"
))
leave_application.submit()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name))
self.assertEquals(leave_ledger_entry[0].employee, leave_application.employee)
self.assertEquals(leave_ledger_entry[0].leave_type, leave_application.leave_type)
self.assertEquals(leave_ledger_entry[0].leaves, leave_application.total_leave_days * -1)
# check if leave ledger entry is deleted on cancellation
leave_application.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_application.name}))
def test_ledger_entry_creation_on_intermediate_allocation_expiry(self):
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90)
leave_type.submit()
create_carry_forwarded_allocation(employee, leave_type)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
from_date = add_days(nowdate(), -3),
to_date = add_days(nowdate(), 7),
description = "_Test Reason",
company = "_Test Company",
docstatus = 1,
status = "Approved"
))
leave_application.submit()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', '*', filters=dict(transaction_name=leave_application.name))
self.assertEquals(len(leave_ledger_entry), 2)
self.assertEquals(leave_ledger_entry[0].employee, leave_application.employee)
self.assertEquals(leave_ledger_entry[0].leave_type, leave_application.leave_type)
self.assertEquals(leave_ledger_entry[0].leaves, -9)
self.assertEquals(leave_ledger_entry[1].leaves, -2)
def test_leave_application_creation_after_expiry(self):
# test leave balance for carry forwarded allocation
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90)
leave_type.submit()
create_carry_forwarded_allocation(employee, leave_type)
self.assertEquals(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0)
def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation
leave_allocation = create_leave_allocation(
leave_type="_Test_CF_leave_expiry",
employee=employee.name,
employee_name=employee.employee_name,
from_date=add_months(nowdate(), -24),
to_date=add_months(nowdate(), -12),
carry_forward=0)
leave_allocation.submit()
leave_allocation = create_leave_allocation(
leave_type="_Test_CF_leave_expiry",
employee=employee.name,
employee_name=employee.employee_name,
from_date=add_days(nowdate(), -84),
to_date=add_days(nowdate(), 100),
carry_forward=1)
leave_allocation.submit()
def make_allocation_record(employee=None, leave_type=None):
allocation = frappe.get_doc({ allocation = frappe.get_doc({
"doctype": "Leave Allocation", "doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001", "employee": employee or "_T-Employee-00001",

View File

@ -10,6 +10,8 @@ from frappe.utils import getdate, nowdate, flt
from erpnext.hr.utils import set_employee_name from erpnext.hr.utils import set_employee_name
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
class LeaveEncashment(Document): class LeaveEncashment(Document):
def validate(self): def validate(self):
@ -25,7 +27,7 @@ class LeaveEncashment(Document):
def on_submit(self): def on_submit(self):
if not self.leave_allocation: if not self.leave_allocation:
self.leave_allocation = self.get_leave_allocation() self.leave_allocation = self.get_leave_allocation().get('name')
additional_salary = frappe.new_doc("Additional Salary") additional_salary = frappe.new_doc("Additional Salary")
additional_salary.company = frappe.get_value("Employee", self.employee, "company") additional_salary.company = frappe.get_value("Employee", self.employee, "company")
additional_salary.employee = self.employee additional_salary.employee = self.employee
@ -40,6 +42,8 @@ class LeaveEncashment(Document):
frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed",
frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') + self.encashable_days) frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') + self.encashable_days)
self.create_leave_ledger_entry()
def on_cancel(self): def on_cancel(self):
if self.additional_salary: if self.additional_salary:
frappe.get_doc("Additional Salary", self.additional_salary).cancel() frappe.get_doc("Additional Salary", self.additional_salary).cancel()
@ -48,6 +52,7 @@ class LeaveEncashment(Document):
if self.leave_allocation: if self.leave_allocation:
frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed",
frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days) frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days)
self.create_leave_ledger_entry(submit=False)
def get_leave_details_for_encashment(self): def get_leave_details_for_encashment(self):
salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate())) salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate()))
@ -57,8 +62,10 @@ class LeaveEncashment(Document):
if not frappe.db.get_value("Leave Type", self.leave_type, 'allow_encashment'): if not frappe.db.get_value("Leave Type", self.leave_type, 'allow_encashment'):
frappe.throw(_("Leave Type {0} is not encashable").format(self.leave_type)) frappe.throw(_("Leave Type {0} is not encashable").format(self.leave_type))
self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, allocation = self.get_leave_allocation()
self.encashment_date or getdate(nowdate()), consider_all_leaves_in_the_allocation_period=True)
self.leave_balance = allocation.total_leaves_allocated - allocation.carry_forwarded_leaves_count\
- get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date)
encashable_days = self.leave_balance - frappe.db.get_value('Leave Type', self.leave_type, 'encashment_threshold_days') encashable_days = self.leave_balance - frappe.db.get_value('Leave Type', self.leave_type, 'encashment_threshold_days')
self.encashable_days = encashable_days if encashable_days > 0 else 0 self.encashable_days = encashable_days if encashable_days > 0 else 0
@ -66,12 +73,47 @@ class LeaveEncashment(Document):
per_day_encashment = frappe.db.get_value('Salary Structure', salary_structure , 'leave_encashment_amount_per_day') per_day_encashment = frappe.db.get_value('Salary Structure', salary_structure , 'leave_encashment_amount_per_day')
self.encashment_amount = self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0 self.encashment_amount = self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0
self.leave_allocation = self.get_leave_allocation() self.leave_allocation = allocation.name
return True return True
def get_leave_allocation(self): def get_leave_allocation(self):
leave_allocation = frappe.db.sql("""select name from `tabLeave Allocation` where '{0}' leave_allocation = frappe.db.sql("""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}'
between from_date and to_date and docstatus=1 and leave_type='{1}' between from_date and to_date and docstatus=1 and leave_type='{1}'
and employee= '{2}'""".format(self.encashment_date or getdate(nowdate()), self.leave_type, self.employee)) and employee= '{2}'""".format(self.encashment_date or getdate(nowdate()), self.leave_type, self.employee), as_dict=1) #nosec
return leave_allocation[0][0] if leave_allocation else None return leave_allocation[0] if leave_allocation else None
def create_leave_ledger_entry(self, submit=True):
args = frappe._dict(
leaves=self.encashable_days * -1,
from_date=self.encashment_date,
to_date=self.encashment_date,
is_carry_forward=0
)
create_leave_ledger_entry(self, args, submit)
# create reverse entry for expired leaves
to_date = self.get_leave_allocation().get('to_date')
if to_date < getdate(nowdate()):
args = frappe._dict(
leaves=self.encashable_days,
from_date=to_date,
to_date=to_date,
is_carry_forward=0
)
create_leave_ledger_entry(self, args, submit)
def create_leave_encashment(leave_allocation):
''' Creates leave encashment for the given allocations '''
for allocation in leave_allocation:
if not get_assigned_salary_structure(allocation.employee, allocation.to_date):
continue
leave_encashment = frappe.get_doc(dict(
doctype="Leave Encashment",
leave_period=allocation.leave_period,
employee=allocation.employee,
leave_type=allocation.leave_type,
encashment_date=allocation.to_date
))
leave_encashment.insert(ignore_permissions=True)

View File

@ -9,42 +9,43 @@ from frappe.utils import today, add_months
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\
test_dependencies = ["Leave Type"] test_dependencies = ["Leave Type"]
class TestLeaveEncashment(unittest.TestCase): class TestLeaveEncashment(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.db.sql('''delete from `tabLeave Period`''') frappe.db.sql('''delete from `tabLeave Period`''')
def test_leave_balance_value_and_amount(self): frappe.db.sql('''delete from `tabLeave Allocation`''')
employee = "test_employee_encashment@salary.com" frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
leave_type = "_Test Leave Type Encashment" frappe.db.sql('''delete from `tabAdditional Salary`''')
# create the leave policy # create the leave policy
leave_policy = frappe.get_doc({ leave_policy = create_leave_policy(
"doctype": "Leave Policy", leave_type="_Test Leave Type Encashment",
"leave_policy_details": [{ annual_allocation=10)
"leave_type": leave_type,
"annual_allocation": 10
}]
}).insert()
leave_policy.submit() leave_policy.submit()
# create employee, salary structure and assignment # create employee, salary structure and assignment
employee = make_employee(employee) self.employee = make_employee("test_employee_encashment@example.com")
frappe.db.set_value("Employee", employee, "leave_policy", leave_policy.name)
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", employee, frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name)
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
other_details={"leave_encashment_amount_per_day": 50}) other_details={"leave_encashment_amount_per_day": 50})
# create the leave period and assign the leaves # create the leave period and assign the leaves
leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
leave_period.grant_leave_allocation(employee=employee) self.leave_period.grant_leave_allocation(employee=self.employee)
def test_leave_balance_value_and_amount(self):
frappe.db.sql('''delete from `tabLeave Encashment`''')
leave_encashment = frappe.get_doc(dict( leave_encashment = frappe.get_doc(dict(
doctype = 'Leave Encashment', doctype='Leave Encashment',
employee = employee, employee=self.employee,
leave_type = leave_type, leave_type="_Test Leave Type Encashment",
leave_period = leave_period.name, leave_period=self.leave_period.name,
payroll_date = today() payroll_date=today()
)).insert() )).insert()
self.assertEqual(leave_encashment.leave_balance, 10) self.assertEqual(leave_encashment.leave_balance, 10)
@ -53,3 +54,26 @@ class TestLeaveEncashment(unittest.TestCase):
leave_encashment.submit() leave_encashment.submit()
self.assertTrue(frappe.db.get_value("Leave Encashment", leave_encashment.name, "additional_salary")) self.assertTrue(frappe.db.get_value("Leave Encashment", leave_encashment.name, "additional_salary"))
def test_creation_of_leave_ledger_entry_on_submit(self):
frappe.db.sql('''delete from `tabLeave Encashment`''')
leave_encashment = frappe.get_doc(dict(
doctype='Leave Encashment',
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
payroll_date=today()
)).insert()
leave_encashment.submit()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_encashment.name))
self.assertEquals(len(leave_ledger_entry), 1)
self.assertEquals(leave_ledger_entry[0].employee, leave_encashment.employee)
self.assertEquals(leave_ledger_entry[0].leave_type, leave_encashment.leave_type)
self.assertEquals(leave_ledger_entry[0].leaves, leave_encashment.encashable_days * -1)
# check if leave ledger entry is deleted on cancellation
leave_encashment.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_encashment.name}))

View File

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

View File

@ -0,0 +1,169 @@
{
"creation": "2019-05-09 15:47:39.760406",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"employee",
"employee_name",
"leave_type",
"transaction_type",
"transaction_name",
"leaves",
"column_break_7",
"from_date",
"to_date",
"is_carry_forward",
"is_expired",
"is_lwp",
"amended_from"
],
"fields": [
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee"
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name"
},
{
"fieldname": "leave_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Leave Type",
"options": "Leave Type"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Leave Ledger Entry",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "transaction_type",
"fieldtype": "Link",
"label": "Transaction Type",
"options": "DocType"
},
{
"fieldname": "transaction_name",
"fieldtype": "Dynamic Link",
"label": "Transaction Name",
"options": "transaction_type"
},
{
"fieldname": "leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Leaves"
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date"
},
{
"default": "0",
"fieldname": "is_carry_forward",
"fieldtype": "Check",
"label": "Is Carry Forward"
},
{
"default": "0",
"fieldname": "is_expired",
"fieldtype": "Check",
"label": "Is Expired"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "is_lwp",
"fieldtype": "Check",
"label": "Is Leave Without Pay"
}
],
"in_create": 1,
"is_submittable": 1,
"modified": "2019-08-20 14:40:04.130799",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Ledger Entry",
"owner": "Administrator",
"permissions": [
{
"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": "HR Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "ASC",
"title_field": "employee"
}

View File

@ -0,0 +1,174 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import add_days, today, flt, DATE_FORMAT, getdate
class LeaveLedgerEntry(Document):
def validate(self):
if getdate(self.from_date) > getdate(self.to_date):
frappe.throw(_("To date needs to be before from date"))
def on_cancel(self):
# allow cancellation of expiry leaves
if self.is_expired:
frappe.db.set_value("Leave Allocation", self.transaction_name, "expired", 0)
else:
frappe.throw(_("Only expired allocation can be cancelled"))
def validate_leave_allocation_against_leave_application(ledger):
''' Checks that leave allocation has no leave application against it '''
leave_application_records = frappe.db.sql_list("""
SELECT transaction_name
FROM `tabLeave Ledger Entry`
WHERE
employee=%s
AND leave_type=%s
AND transaction_type='Leave Application'
AND from_date>=%s
AND to_date<=%s
""", (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date))
if leave_application_records:
frappe.throw(_("Leave allocation %s is linked with leave application %s"
% (ledger.transaction_name, ', '.join(leave_application_records))))
def create_leave_ledger_entry(ref_doc, args, submit=True):
ledger = frappe._dict(
doctype='Leave Ledger Entry',
employee=ref_doc.employee,
employee_name=ref_doc.employee_name,
leave_type=ref_doc.leave_type,
transaction_type=ref_doc.doctype,
transaction_name=ref_doc.name,
is_carry_forward=0,
is_expired=0,
is_lwp=0
)
ledger.update(args)
if submit:
frappe.get_doc(ledger).submit()
else:
delete_ledger_entry(ledger)
def delete_ledger_entry(ledger):
''' Delete ledger entry on cancel of leave application/allocation/encashment '''
if ledger.transaction_type == "Leave Allocation":
validate_leave_allocation_against_leave_application(ledger)
expired_entry = get_previous_expiry_ledger_entry(ledger)
frappe.db.sql("""DELETE
FROM `tabLeave Ledger Entry`
WHERE
`transaction_name`=%s
OR `name`=%s""", (ledger.transaction_name, expired_entry))
def get_previous_expiry_ledger_entry(ledger):
''' Returns the expiry ledger entry having same creation date as the ledger entry to be cancelled '''
creation_date = frappe.db.get_value("Leave Ledger Entry", filters={
'transaction_name': ledger.transaction_name,
'is_expired': 0,
'transaction_type': 'Leave Allocation'
}, fieldname=['creation'])
creation_date = creation_date.strftime(DATE_FORMAT) if creation_date else ''
return frappe.db.get_value("Leave Ledger Entry", filters={
'creation': ('like', creation_date+"%"),
'employee': ledger.employee,
'leave_type': ledger.leave_type,
'is_expired': 1,
'docstatus': 1,
'is_carry_forward': 0
}, fieldname=['name'])
def process_expired_allocation():
''' Check if a carry forwarded allocation has expired and create a expiry ledger entry '''
# fetch leave type records that has carry forwarded leaves expiry
leave_type_records = frappe.db.get_values("Leave Type", filters={
'expire_carry_forwarded_leaves_after_days': (">", 0)
}, fieldname=['name'])
if leave_type_records:
leave_type = [record[0] for record in leave_type_records]
expired_allocation = frappe.db.sql_list("""SELECT name
FROM `tabLeave Ledger Entry`
WHERE
`transaction_type`='Leave Allocation'
AND `is_expired`=1""")
expire_allocation = frappe.get_all("Leave Ledger Entry",
fields=['leaves', 'to_date', 'employee', 'leave_type', 'is_carry_forward', 'transaction_name as name', 'transaction_type'],
filters={
'to_date': ("<", today()),
'transaction_type': 'Leave Allocation',
'transaction_name': ('not in', expired_allocation)
},
or_filters={
'is_carry_forward': 0,
'leave_type': ('in', leave_type)
})
if expire_allocation:
create_expiry_ledger_entry(expire_allocation)
def create_expiry_ledger_entry(allocations):
''' Create ledger entry for expired allocation '''
for allocation in allocations:
if allocation.is_carry_forward:
expire_carried_forward_allocation(allocation)
else:
expire_allocation(allocation)
def get_remaining_leaves(allocation):
''' Returns remaining leaves from the given allocation '''
return frappe.db.get_value("Leave Ledger Entry",
filters={
'employee': allocation.employee,
'leave_type': allocation.leave_type,
'to_date': ('<=', allocation.to_date),
}, fieldname=['SUM(leaves)'])
@frappe.whitelist()
def expire_allocation(allocation, expiry_date=None):
''' expires non-carry forwarded allocation '''
leaves = get_remaining_leaves(allocation)
expiry_date = expiry_date if expiry_date else allocation.to_date
if leaves:
args = dict(
leaves=flt(leaves) * -1,
transaction_name=allocation.name,
transaction_type='Leave Allocation',
from_date=expiry_date,
to_date=expiry_date,
is_carry_forward=0,
is_expired=1
)
create_leave_ledger_entry(allocation, args)
frappe.db.set_value("Leave Allocation", allocation.name, "expired", 1)
def expire_carried_forward_allocation(allocation):
''' Expires remaining leaves in the on carried forward allocation '''
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, allocation.from_date, allocation.to_date)
leaves = flt(allocation.leaves) + flt(leaves_taken)
if leaves > 0:
args = frappe._dict(
transaction_name=allocation.name,
transaction_type="Leave Allocation",
leaves=allocation.leaves * -1,
is_carry_forward=allocation.is_carry_forward,
is_expired=1,
from_date=allocation.to_date,
to_date=allocation.to_date
)
create_leave_ledger_entry(allocation, args)

View File

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

View File

@ -68,7 +68,7 @@ frappe.ui.form.on('Leave Period', {
}, },
{ {
"label": "Add unused leaves from previous allocations", "label": "Add unused leaves from previous allocations",
"fieldname": "carry_forward_leaves", "fieldname": "carry_forward",
"fieldtype": "Check" "fieldtype": "Check"
} }
], ],

View File

@ -77,6 +77,38 @@
"translatable": 0, "translatable": 0,
"unique": 0 "unique": 0
}, },
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_active",
"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": "Is Active",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -141,38 +173,6 @@
"translatable": 0, "translatable": 0,
"unique": 0 "unique": 0
}, },
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_active",
"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": "Is Active",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -217,7 +217,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-08-21 16:15:43.305502", "modified": "2019-05-30 16:15:43.305502",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Period", "name": "Leave Period",

View File

@ -5,9 +5,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import getdate, cstr from frappe.utils import getdate, cstr, add_days, date_diff, getdate, ceil
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_overlap, get_employee_leave_policy from erpnext.hr.utils import validate_overlap, get_employee_leave_policy
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_carry_forwarded_leaves
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from six import iteritems from six import iteritems
@ -21,8 +22,8 @@ class LeavePeriod(Document):
condition_str = " and " + " and ".join(conditions) if len(conditions) else "" condition_str = " and " + " and ".join(conditions) if len(conditions) else ""
employees = frappe.db.sql_list("select name from tabEmployee where status='Active' {condition}" employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec
.format(condition=condition_str), tuple(values)) .format(condition=condition_str), tuple(values)))
return employees return employees
@ -36,29 +37,29 @@ class LeavePeriod(Document):
def grant_leave_allocation(self, grade=None, department=None, designation=None, def grant_leave_allocation(self, grade=None, department=None, designation=None,
employee=None, carry_forward_leaves=0): employee=None, carry_forward=0):
employees = self.get_employees({ employee_records = self.get_employees({
"grade": grade, "grade": grade,
"department": department, "department": department,
"designation": designation, "designation": designation,
"name": employee "name": employee
}) })
if employees: if employee_records:
if len(employees) > 20: if len(employee_records) > 20:
frappe.enqueue(grant_leave_alloc_for_employees, timeout=600, frappe.enqueue(grant_leave_alloc_for_employees, timeout=600,
employees=employees, leave_period=self, carry_forward_leaves=carry_forward_leaves) employee_records=employee_records, leave_period=self, carry_forward=carry_forward)
else: else:
grant_leave_alloc_for_employees(employees, self, carry_forward_leaves) grant_leave_alloc_for_employees(employee_records, self, carry_forward)
else: else:
frappe.msgprint(_("No Employee Found")) frappe.msgprint(_("No Employee Found"))
def grant_leave_alloc_for_employees(employees, leave_period, carry_forward_leaves=0): def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0):
leave_allocations = [] leave_allocations = []
existing_allocations_for = get_existing_allocations(employees, leave_period.name) existing_allocations_for = get_existing_allocations(list(employee_records.keys()), leave_period.name)
leave_type_details = get_leave_type_details() leave_type_details = get_leave_type_details()
count=0 count = 0
for employee in employees: for employee in employee_records.keys():
if employee in existing_allocations_for: if employee in existing_allocations_for:
continue continue
count +=1 count +=1
@ -67,18 +68,24 @@ def grant_leave_alloc_for_employees(employees, leave_period, carry_forward_leave
for leave_policy_detail in leave_policy.leave_policy_details: for leave_policy_detail in leave_policy.leave_policy_details:
if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp: if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type, leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type,
leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward_leaves) leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward, employee_records.get(employee))
leave_allocations.append(leave_allocation) leave_allocations.append(leave_allocation)
frappe.db.commit() frappe.db.commit()
frappe.publish_progress(count*100/len(set(employees) - set(existing_allocations_for)), title = _("Allocating leaves...")) frappe.publish_progress(count*100/len(set(employee_records.keys()) - set(existing_allocations_for)), title = _("Allocating leaves..."))
if leave_allocations: if leave_allocations:
frappe.msgprint(_("Leaves has been granted sucessfully")) frappe.msgprint(_("Leaves has been granted sucessfully"))
def get_existing_allocations(employees, leave_period): def get_existing_allocations(employees, leave_period):
leave_allocations = frappe.db.sql_list(""" leave_allocations = frappe.db.sql_list("""
select distinct employee from `tabLeave Allocation` SELECT DISTINCT
where leave_period=%s and employee in (%s) and docstatus=1 employee
FROM `tabLeave Allocation`
WHERE
leave_period=%s
AND employee in (%s)
AND carry_forward=0
AND docstatus=1
""" % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees) """ % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees)
if leave_allocations: if leave_allocations:
frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}") frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}")
@ -87,28 +94,36 @@ def get_existing_allocations(employees, leave_period):
def get_leave_type_details(): def get_leave_type_details():
leave_type_details = frappe._dict() leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type", fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward"]) leave_types = frappe.get_all("Leave Type",
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
for d in leave_types: for d in leave_types:
leave_type_details.setdefault(d.name, d) leave_type_details.setdefault(d.name, d)
return leave_type_details return leave_type_details
def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward_leaves): def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining):
allocation = frappe.new_doc("Leave Allocation") ''' Creates leave allocation for the given employee in the provided leave period '''
allocation.employee = employee if carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
allocation.leave_type = leave_type carry_forward = 0
allocation.from_date = leave_period.from_date
allocation.to_date = leave_period.to_date # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
if getdate(date_of_joining) > getdate(leave_period.from_date):
remaining_period = ((date_diff(leave_period.to_date, date_of_joining) + 1) / (date_diff(leave_period.to_date, leave_period.from_date) + 1))
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0 # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1: if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
new_leaves_allocated = 0 new_leaves_allocated = 0
allocation.new_leaves_allocated = new_leaves_allocated allocation = frappe.get_doc(dict(
allocation.leave_period = leave_period.name doctype="Leave Allocation",
if carry_forward_leaves: employee=employee,
if leave_type_details.get(leave_type).is_carry_forward: leave_type=leave_type,
allocation.carry_forward = carry_forward_leaves from_date=leave_period.from_date,
to_date=leave_period.to_date,
new_leaves_allocated=new_leaves_allocated,
leave_period=leave_period.name,
carry_forward=carry_forward
))
allocation.save(ignore_permissions = True) allocation.save(ignore_permissions = True)
allocation.submit() allocation.submit()
return allocation.name return allocation.name

View File

@ -12,6 +12,9 @@ def get_data():
}, },
{ {
'items': ['Employee Grade'] 'items': ['Employee Grade']
} },
{
'items': ['Leave Allocation']
},
] ]
} }

View File

@ -12,16 +12,20 @@ class TestLeavePolicy(unittest.TestCase):
if random_leave_type: if random_leave_type:
random_leave_type = random_leave_type[0] random_leave_type = random_leave_type[0]
leave_type = frappe.get_doc("Leave Type", random_leave_type.name) leave_type = frappe.get_doc("Leave Type", random_leave_type.name)
old_max_leaves_allowed = leave_type.max_leaves_allowed
leave_type.max_leaves_allowed = 2 leave_type.max_leaves_allowed = 2
leave_type.save() leave_type.save()
leave_policy_details = { leave_policy = create_leave_policy(leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1)
self.assertRaises(frappe.ValidationError, leave_policy.insert)
def create_leave_policy(**args):
''' Returns an object of leave policy '''
args = frappe._dict(args)
return frappe.get_doc({
"doctype": "Leave Policy", "doctype": "Leave Policy",
"leave_policy_details": [{ "leave_policy_details": [{
"leave_type": leave_type.name, "leave_type": args.leave_type or "_Test Leave Type",
"annual_allocation": leave_type.max_leaves_allowed + 1 "annual_allocation": args.annual_allocation or 10
}] }]
} })
self.assertRaises(frappe.ValidationError, frappe.get_doc(leave_policy_details).insert)

View File

@ -1,5 +1,6 @@
{ {
"allow_copy": 0, "allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0, "allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
@ -14,10 +15,12 @@
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "leave_type_name", "fieldname": "leave_type_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
@ -42,15 +45,17 @@
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0, "translatable": 0,
"unique": 0 "unique": 1
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "", "depends_on": "",
"fetch_if_empty": 0,
"fieldname": "max_leaves_allowed", "fieldname": "max_leaves_allowed",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "hidden": 0,
@ -78,10 +83,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "applicable_after", "fieldname": "applicable_after",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "hidden": 0,
@ -109,10 +116,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "max_continuous_days_allowed", "fieldname": "max_continuous_days_allowed",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "hidden": 0,
@ -141,10 +150,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0, "hidden": 0,
@ -171,10 +182,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_carry_forward", "fieldname": "is_carry_forward",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
@ -203,10 +216,13 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "",
"fetch_if_empty": 0,
"fieldname": "is_lwp", "fieldname": "is_lwp",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
@ -233,10 +249,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_optional_leave", "fieldname": "is_optional_leave",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
@ -264,10 +282,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "allow_negative", "fieldname": "allow_negative",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
@ -294,10 +314,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "include_holiday", "fieldname": "include_holiday",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
@ -324,10 +346,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_compensatory", "fieldname": "is_compensatory",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
@ -355,10 +379,81 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0, "columns": 0,
"depends_on": "eval: doc.is_carry_forward == 1",
"fetch_if_empty": 0,
"fieldname": "carry_forward_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Carry Forward",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"description": "Calculated in days",
"fetch_if_empty": 0,
"fieldname": "expire_carry_forwarded_leaves_after_days",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Expire Carry Forwarded Leaves (Days)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "encashment", "fieldname": "encashment",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
@ -386,10 +481,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "allow_encashment", "fieldname": "allow_encashment",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
@ -417,11 +514,13 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "allow_encashment", "depends_on": "allow_encashment",
"fetch_if_empty": 0,
"fieldname": "encashment_threshold_days", "fieldname": "encashment_threshold_days",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "hidden": 0,
@ -449,11 +548,13 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "allow_encashment", "depends_on": "allow_encashment",
"fetch_if_empty": 0,
"fieldname": "earning_component", "fieldname": "earning_component",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
@ -482,10 +583,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "earned_leave", "fieldname": "earned_leave",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
@ -513,10 +616,12 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_earned_leave", "fieldname": "is_earned_leave",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
@ -544,11 +649,13 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "is_earned_leave", "depends_on": "is_earned_leave",
"fetch_if_empty": 0,
"fieldname": "earned_leave_frequency", "fieldname": "earned_leave_frequency",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0, "hidden": 0,
@ -577,12 +684,14 @@
}, },
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "0.5", "default": "0.5",
"depends_on": "is_earned_leave", "depends_on": "is_earned_leave",
"fetch_if_empty": 0,
"fieldname": "rounding", "fieldname": "rounding",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0, "hidden": 0,
@ -611,17 +720,15 @@
} }
], ],
"has_web_view": 0, "has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0, "hide_toolbar": 0,
"icon": "fa fa-flag", "icon": "fa fa-flag",
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0, "in_create": 0,
"is_submittable": 0, "is_submittable": 0,
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-06-03 18:32:51.803472", "modified": "2019-08-02 15:38:39.334283",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Type", "name": "Leave Type",
@ -687,8 +794,8 @@
], ],
"quick_entry": 0, "quick_entry": 0,
"read_only": 0, "read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0, "show_name_in_global_search": 0,
"track_changes": 0, "track_changes": 0,
"track_seen": 0 "track_seen": 0,
"track_views": 0
} }

View File

@ -2,9 +2,22 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import calendar
import frappe import frappe
from datetime import datetime
from frappe.utils import today
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
class LeaveType(Document): class LeaveType(Document):
pass def validate(self):
if self.is_lwp:
leave_allocation = frappe.get_all("Leave Allocation", filters={
'leave_type': self.name,
'from_date': ("<=", today()),
'to_date': (">=", today())
}, fields=['name'])
leave_allocation = [l['name'] for l in leave_allocation]
if leave_allocation:
frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec

View File

@ -2,6 +2,25 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _
test_records = frappe.get_test_records('Leave Type') test_records = frappe.get_test_records('Leave Type')
def create_leave_type(**args):
args = frappe._dict(args)
if frappe.db.exists("Leave Type", args.leave_type_name):
return frappe.get_doc("Leave Type", args.leave_type_name)
leave_type = frappe.get_doc({
"doctype": "Leave Type",
"leave_type_name": args.leave_type_name or "_Test Leave Type",
"include_holiday": args.include_holidays or 1,
"allow_encashment": args.allow_encashment or 0,
"is_earned_leave": args.is_earned_leave or 0,
"is_lwp": args.is_lwp or 0,
"is_carry_forward": args.is_carry_forward or 0,
"expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
"encashment_threshold_days": args.encashment_threshold_days or 5,
"earning_component": "Leave Encashment"
})
return leave_type

View File

@ -69,7 +69,7 @@ frappe.ui.form.on('Payroll Entry', {
}, },
add_context_buttons: function(frm) { add_context_buttons: function(frm) {
if(frm.doc.salary_slips_submitted) { if(frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
frm.events.add_bank_entry_button(frm); frm.events.add_bank_entry_button(frm);
} else if(frm.doc.salary_slips_created) { } else if(frm.doc.salary_slips_created) {
frm.add_custom_button(__("Submit Salary Slip"), function() { frm.add_custom_button(__("Submit Salary Slip"), function() {

View File

@ -18,9 +18,8 @@ class PayrollEntry(Document):
# check if salary slips were manually submitted # check if salary slips were manually submitted
entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name'])
if cint(entries) == len(self.employees) and not self.salary_slips_submitted: if cint(entries) == len(self.employees):
self.db_set("salary_slips_submitted", 1) self.set_onload("submitted_ss", True)
self.reload()
def on_submit(self): def on_submit(self):
self.create_salary_slips() self.create_salary_slips()
@ -429,7 +428,6 @@ def get_start_end_dates(payroll_frequency, start_date=None, company=None):
'start_date': start_date, 'end_date': end_date 'start_date': start_date, 'end_date': end_date
}) })
def get_frequency_kwargs(frequency_name): def get_frequency_kwargs(frequency_name):
frequency_dict = { frequency_dict = {
'monthly': {'months': 1}, 'monthly': {'months': 1},

View File

@ -23,14 +23,9 @@
"grace_period_settings_auto_attendance_section", "grace_period_settings_auto_attendance_section",
"enable_entry_grace_period", "enable_entry_grace_period",
"late_entry_grace_period", "late_entry_grace_period",
"consequence_after",
"consequence",
"column_break_18", "column_break_18",
"enable_exit_grace_period", "enable_exit_grace_period",
"enable_different_consequence_for_early_exit", "early_exit_grace_period"
"early_exit_grace_period",
"early_exit_consequence_after",
"early_exit_consequence"
], ],
"fields": [ "fields": [
{ {
@ -107,21 +102,6 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Late Entry Grace Period" "label": "Late Entry Grace Period"
}, },
{
"depends_on": "enable_entry_grace_period",
"description": "The number of occurrence after which the consequence is executed.",
"fieldname": "consequence_after",
"fieldtype": "Int",
"label": "Consequence after"
},
{
"default": "Half Day",
"depends_on": "enable_entry_grace_period",
"fieldname": "consequence",
"fieldtype": "Select",
"label": "Consequence",
"options": "Half Day\nAbsent"
},
{ {
"fieldname": "column_break_18", "fieldname": "column_break_18",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -132,13 +112,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Exit Grace Period" "label": "Enable Exit Grace Period"
}, },
{
"default": "0",
"depends_on": "enable_exit_grace_period",
"fieldname": "enable_different_consequence_for_early_exit",
"fieldtype": "Check",
"label": "Enable Different Consequence for Early Exit"
},
{ {
"depends_on": "eval:doc.enable_exit_grace_period", "depends_on": "eval:doc.enable_exit_grace_period",
"description": "The time before the shift end time when check-out is considered as early (in minutes).", "description": "The time before the shift end time when check-out is considered as early (in minutes).",
@ -146,21 +119,6 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Early Exit Grace Period" "label": "Early Exit Grace Period"
}, },
{
"depends_on": "eval:doc.enable_exit_grace_period && doc.enable_different_consequence_for_early_exit",
"description": "The number of occurrence after which the consequence is executed.",
"fieldname": "early_exit_consequence_after",
"fieldtype": "Int",
"label": "Early Exit Consequence after"
},
{
"default": "Half Day",
"depends_on": "eval:doc.enable_exit_grace_period && doc.enable_different_consequence_for_early_exit",
"fieldname": "early_exit_consequence",
"fieldtype": "Select",
"label": "Early Exit Consequence",
"options": "Half Day\nAbsent"
},
{ {
"default": "60", "default": "60",
"description": "Time after the end of shift during which check-out is considered for attendance.", "description": "Time after the end of shift during which check-out is considered for attendance.",
@ -178,7 +136,6 @@
"depends_on": "enable_auto_attendance", "depends_on": "enable_auto_attendance",
"fieldname": "grace_period_settings_auto_attendance_section", "fieldname": "grace_period_settings_auto_attendance_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 1,
"label": "Grace Period Settings For Auto Attendance" "label": "Grace Period Settings For Auto Attendance"
}, },
{ {
@ -201,7 +158,7 @@
"label": "Last Sync of Checkin" "label": "Last Sync of Checkin"
} }
], ],
"modified": "2019-06-10 06:02:44.272036", "modified": "2019-07-30 01:05:24.660666",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Shift Type", "name": "Shift Type",

View File

@ -28,8 +28,8 @@ class ShiftType(Document):
logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time") logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time")
for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])): for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
single_shift_logs = list(group) single_shift_logs = list(group)
attendance_status, working_hours = self.get_attendance(single_shift_logs) attendance_status, working_hours, late_entry, early_exit = self.get_attendance(single_shift_logs)
mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, self.name) mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, self.name)
for employee in self.get_assigned_employee(self.process_attendance_after, True): for employee in self.get_assigned_employee(self.process_attendance_after, True):
self.mark_absent_for_dates_with_no_attendance(employee) self.mark_absent_for_dates_with_no_attendance(employee)
@ -39,12 +39,19 @@ class ShiftType(Document):
1. These logs belongs to an single shift, single employee and is not in a holiday date. 1. These logs belongs to an single shift, single employee and is not in a holiday date.
2. Logs are in chronological order 2. Logs are in chronological order
""" """
total_working_hours = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on) late_entry = early_exit = False
total_working_hours, in_time, out_time = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on)
if cint(self.enable_entry_grace_period) and in_time and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)):
late_entry = True
if cint(self.enable_exit_grace_period) and out_time and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)):
early_exit = True
if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent: if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent:
return 'Absent', total_working_hours return 'Absent', total_working_hours, late_entry, early_exit
if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day: if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day:
return 'Half Day', total_working_hours return 'Half Day', total_working_hours, late_entry, early_exit
return 'Present', total_working_hours return 'Present', total_working_hours, late_entry, early_exit
def mark_absent_for_dates_with_no_attendance(self, employee): def mark_absent_for_dates_with_no_attendance(self, employee):
"""Marks Absents for the given employee on working days in this shift which have no attendance marked. """Marks Absents for the given employee on working days in this shift which have no attendance marked.

View File

@ -24,6 +24,18 @@ frappe.query_reports["Employee Leave Balance"] = {
"options": "Company", "options": "Company",
"reqd": 1, "reqd": 1,
"default": frappe.defaults.get_user_default("Company") "default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"department",
"label": __("Department"),
"fieldtype": "Link",
"options": "Department",
},
{
"fieldname":"employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee",
} }
] ]
} }

View File

@ -4,8 +4,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt
from erpnext.hr.doctype.leave_application.leave_application \ from erpnext.hr.doctype.leave_application.leave_application \
import get_leave_allocation_records, get_leave_balance_on, get_approved_leaves_for_period import get_leave_balance_on, get_leaves_for_period
def execute(filters=None): def execute(filters=None):
@ -30,17 +31,28 @@ def get_columns(leave_types):
return columns return columns
def get_conditions(filters):
conditions = {
"status": "Active",
"company": filters.company,
}
if filters.get("department"):
conditions.update({"department": filters.get("department")})
if filters.get("employee"):
conditions.update({"employee": filters.get("employee")})
return conditions
def get_data(filters, leave_types): def get_data(filters, leave_types):
user = frappe.session.user user = frappe.session.user
allocation_records_based_on_to_date = get_leave_allocation_records(filters.to_date) conditions = get_conditions(filters)
allocation_records_based_on_from_date = get_leave_allocation_records(filters.from_date)
if filters.to_date <= filters.from_date: if filters.to_date <= filters.from_date:
frappe.throw(_("From date can not be greater than than To date")) frappe.throw(_("From date can not be greater than than To date"))
active_employees = frappe.get_all("Employee", active_employees = frappe.get_all("Employee",
filters = { "status": "Active", "company": filters.company}, filters=conditions,
fields = ["name", "employee_name", "department", "user_id"]) fields=["name", "employee_name", "department", "user_id"])
data = [] data = []
for employee in active_employees: for employee in active_employees:
@ -50,16 +62,14 @@ def get_data(filters, leave_types):
for leave_type in leave_types: for leave_type in leave_types:
# leaves taken # leaves taken
leaves_taken = get_approved_leaves_for_period(employee.name, leave_type, leaves_taken = get_leaves_for_period(employee.name, leave_type,
filters.from_date, filters.to_date) filters.from_date, filters.to_date) * -1
# opening balance # opening balance
opening = get_leave_balance_on(employee.name, leave_type, filters.from_date, opening = get_total_allocated_leaves(employee.name, leave_type, filters.from_date, filters.to_date)
allocation_records_based_on_to_date.get(employee.name, frappe._dict()))
# closing balance # closing balance
closing = get_leave_balance_on(employee.name, leave_type, filters.to_date, closing = flt(opening) - flt(leaves_taken)
allocation_records_based_on_to_date.get(employee.name, frappe._dict()))
row += [opening, leaves_taken, closing] row += [opening, leaves_taken, closing]
@ -84,3 +94,18 @@ def get_approvers(department):
where parent = %s and parentfield = 'leave_approvers'""", (d), as_dict=True)]) where parent = %s and parentfield = 'leave_approvers'""", (d), as_dict=True)])
return approvers return approvers
def get_total_allocated_leaves(employee, leave_type, from_date, to_date):
''' Returns leave allocation between from date and to date '''
leave_allocation_records = frappe.db.get_all('Leave Ledger Entry', filters={
'docstatus': 1,
'is_expired': 0,
'leave_type': leave_type,
'employee': employee,
'transaction_type': 'Leave Allocation'
}, or_filters={
'from_date': ['between', (from_date, to_date)],
'to_date': ['between', (from_date, to_date)]
}, fields=['SUM(leaves) as leaves'])
return flt(leave_allocation_records[0].get('leaves')) if leave_allocation_records else flt(0)

View File

@ -25,6 +25,7 @@ def execute(filters=None):
leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True) leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True)
leave_list = [d[0] for d in leave_types] leave_list = [d[0] for d in leave_types]
columns.extend(leave_list) columns.extend(leave_list)
columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"])
for emp in sorted(att_map): for emp in sorted(att_map):
emp_det = emp_map.get(emp) emp_det = emp_map.get(emp)
@ -66,6 +67,10 @@ def execute(filters=None):
leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\ leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\
where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1) where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1)
time_default_counts = frappe.db.sql("""select (select count(*) from `tabAttendance` where \
late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \
early_exit = 1 %s) as early_exit_count""" % (conditions, conditions), filters)
leaves = {} leaves = {}
for d in leave_details: for d in leave_details:
if d.status == "Half Day": if d.status == "Half Day":
@ -81,6 +86,7 @@ def execute(filters=None):
else: else:
row.append("0.0") row.append("0.0")
row.extend([time_default_counts[0][0],time_default_counts[0][1]])
data.append(row) data.append(row)
return columns, data return columns, data

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe import _ from frappe import _
from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr, add_days, today
from frappe.model.document import Document from frappe.model.document import Document
from frappe.desk.form import assign_to from frappe.desk.form import assign_to
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
@ -270,6 +270,21 @@ def get_leave_period(from_date, to_date, company):
if leave_period: if leave_period:
return leave_period return leave_period
def generate_leave_encashment():
''' Generates a draft leave encashment on allocation expiry '''
from erpnext.hr.doctype.leave_encashment.leave_encashment import create_leave_encashment
if frappe.db.get_single_value('HR Settings', 'auto_leave_encashment'):
leave_type = frappe.get_all('Leave Type', filters={'allow_encashment': 1}, fields=['name'])
leave_type=[l['name'] for l in leave_type]
leave_allocation = frappe.get_all("Leave Allocation", filters={
'to_date': add_days(today(), -1),
'leave_type': ('in', leave_type)
}, fields=['employee', 'leave_period', 'leave_type', 'to_date', 'total_leaves_allocated', 'new_leaves_allocated'])
create_leave_encashment(leave_allocation=leave_allocation)
def allocate_earned_leaves(): def allocate_earned_leaves():
'''Allocate earned leaves to Employees''' '''Allocate earned leaves to Employees'''
e_leave_types = frappe.get_all("Leave Type", e_leave_types = frappe.get_all("Leave Type",
@ -277,11 +292,10 @@ def allocate_earned_leaves():
filters={'is_earned_leave' : 1}) filters={'is_earned_leave' : 1})
today = getdate() today = getdate()
divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12} divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
if e_leave_types:
for e_leave_type in e_leave_types: for e_leave_type in e_leave_types:
leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where '{0}' leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where %s
between from_date and to_date and docstatus=1 and leave_type='{1}'""" between from_date and to_date and docstatus=1 and leave_type=%s""", (today, e_leave_type.name), as_dict=1)
.format(today, e_leave_type.name), as_dict=1)
for allocation in leave_allocations: for allocation in leave_allocations:
leave_policy = get_employee_leave_policy(allocation.employee) leave_policy = get_employee_leave_policy(allocation.employee)
if not leave_policy: if not leave_policy:
@ -289,19 +303,32 @@ def allocate_earned_leaves():
if not e_leave_type.earned_leave_frequency == "Monthly": if not e_leave_type.earned_leave_frequency == "Monthly":
if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency): if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency):
continue continue
annual_allocation = frappe.db.sql("""select annual_allocation from `tabLeave Policy Detail` annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={
where parent=%s and leave_type=%s""", (leave_policy.name, e_leave_type.name)) 'parent': leave_policy.name,
if annual_allocation and annual_allocation[0]: 'leave_type': e_leave_type.name
earned_leaves = flt(annual_allocation[0][0]) / divide_by_frequency[e_leave_type.earned_leave_frequency] }, fieldname=['annual_allocation'])
if annual_allocation:
earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
if e_leave_type.rounding == "0.5": if e_leave_type.rounding == "0.5":
earned_leaves = round(earned_leaves * 2) / 2 earned_leaves = round(earned_leaves * 2) / 2
else: else:
earned_leaves = round(earned_leaves) earned_leaves = round(earned_leaves)
allocated_leaves = frappe.db.get_value('Leave Allocation', allocation.name, 'total_leaves_allocated') allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocated_leaves) + flt(earned_leaves) new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed
frappe.db.set_value('Leave Allocation', allocation.name, 'total_leaves_allocated', new_allocation)
if new_allocation == allocation.total_leaves_allocated:
continue
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_earned_leave_ledger_entry(allocation, earned_leaves, today)
def create_earned_leave_ledger_entry(allocation, earned_leaves, date):
''' Create leave ledger entry based on the earned leave frequency '''
allocation.new_leaves_allocated = earned_leaves
allocation.from_date = date
allocation.unused_leaves = 0
allocation.create_leave_ledger_entry()
def check_frequency_hit(from_date, to_date, frequency): def check_frequency_hit(from_date, to_date, frequency):
'''Return True if current date matches frequency''' '''Return True if current date matches frequency'''

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ from erpnext.setup.utils import get_exchange_rate
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.get_item_details import get_price_list_rate from erpnext.stock.get_item_details import get_price_list_rate
from frappe.core.doctype.version.version import get_diff
import functools import functools
@ -763,3 +764,52 @@ def add_additional_cost(stock_entry, work_order):
'description': name[0], 'description': name[0],
'amount': items.get(name[0]) 'amount': items.get(name[0])
}) })
@frappe.whitelist()
def get_bom_diff(bom1, bom2):
from frappe.model import table_fields
doc1 = frappe.get_doc('BOM', bom1)
doc2 = frappe.get_doc('BOM', bom2)
out = get_diff(doc1, doc2)
out.row_changed = []
out.added = []
out.removed = []
meta = doc1.meta
identifiers = {
'operations': 'operation',
'items': 'item_code',
'scrap_items': 'item_code',
'exploded_items': 'item_code'
}
for df in meta.fields:
old_value, new_value = doc1.get(df.fieldname), doc2.get(df.fieldname)
if df.fieldtype in table_fields:
identifier = identifiers[df.fieldname]
# make maps
old_row_by_identifier, new_row_by_identifier = {}, {}
for d in old_value:
old_row_by_identifier[d.get(identifier)] = d
for d in new_value:
new_row_by_identifier[d.get(identifier)] = d
# check rows for additions, changes
for i, d in enumerate(new_value):
if d.get(identifier) in old_row_by_identifier:
diff = get_diff(old_row_by_identifier[d.get(identifier)], d, for_child=True)
if diff and diff.changed:
out.row_changed.append((df.fieldname, i, d.get(identifier), diff.changed))
else:
out.added.append([df.fieldname, d.as_dict()])
# check for deletions
for d in old_value:
if not d.get(identifier) in new_row_by_identifier:
out.removed.append([df.fieldname, d.as_dict()])
return out

View File

@ -320,7 +320,8 @@ class ProductionPlan(Document):
'qty': data.get("stock_qty") * item.get("qty"), 'qty': data.get("stock_qty") * item.get("qty"),
'production_plan': self.name, 'production_plan': self.name,
'company': self.company, 'company': self.company,
'fg_warehouse': item.get("fg_warehouse") 'fg_warehouse': item.get("fg_warehouse"),
'update_consumed_material_cost_in_project': 0
}) })
work_order = self.create_work_order(data) work_order = self.create_work_order(data)

View File

@ -4,6 +4,7 @@
"creation": "2013-01-10 16:34:16", "creation": "2013-01-10 16:34:16",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB",
"field_order": [ "field_order": [
"item", "item",
"naming_series", "naming_series",
@ -12,9 +13,6 @@
"item_name", "item_name",
"image", "image",
"bom_no", "bom_no",
"allow_alternative_item",
"use_multi_level_bom",
"skip_transfer",
"column_break1", "column_break1",
"company", "company",
"qty", "qty",
@ -22,7 +20,13 @@
"produced_qty", "produced_qty",
"sales_order", "sales_order",
"project", "project",
"settings_section",
"allow_alternative_item",
"use_multi_level_bom",
"column_break_18",
"skip_transfer",
"from_wip_warehouse", "from_wip_warehouse",
"update_consumed_material_cost_in_project",
"warehouses", "warehouses",
"wip_warehouse", "wip_warehouse",
"fg_warehouse", "fg_warehouse",
@ -442,13 +446,28 @@
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Work Order", "options": "Work Order",
"read_only": 1 "read_only": 1
},
{
"fieldname": "settings_section",
"fieldtype": "Section Break",
"label": "Settings"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"default": "1",
"fieldname": "update_consumed_material_cost_in_project",
"fieldtype": "Check",
"label": "Update Consumed Material Cost In Project"
} }
], ],
"icon": "fa fa-cogs", "icon": "fa fa-cogs",
"idx": 1, "idx": 1,
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-05-27 09:36:16.707719", "modified": "2019-07-31 00:13:38.218277",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",
@ -477,8 +496,9 @@
"role": "Stock User" "role": "Stock User"
} }
], ],
"sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"title_field": "production_item", "title_field": "production_item",
"track_changes": 1, "track_changes": 1,
"track_seen": 1 "track_seen": 1
} }

View File

@ -0,0 +1,213 @@
frappe.pages['bom-comparison-tool'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __('BOM Comparison Tool'),
single_column: true
});
new erpnext.BOMComparisonTool(page);
}
erpnext.BOMComparisonTool = class BOMComparisonTool {
constructor(page) {
this.page = page;
this.make_form();
}
make_form() {
this.form = new frappe.ui.FieldGroup({
fields: [
{
label: __('BOM 1'),
fieldname: 'name1',
fieldtype: 'Link',
options: 'BOM',
change: () => this.fetch_and_render()
},
{
fieldtype: 'Column Break'
},
{
label: __('BOM 2'),
fieldname: 'name2',
fieldtype: 'Link',
options: 'BOM',
change: () => this.fetch_and_render()
},
{
fieldtype: 'Section Break'
},
{
fieldtype: 'HTML',
fieldname: 'preview'
}
],
body: this.page.body
});
this.form.make();
}
fetch_and_render() {
let { name1, name2 } = this.form.get_values();
if (!(name1 && name2)) {
this.form.get_field('preview').html('');
return;
}
// set working state
this.form.get_field('preview').html(`
<div class="text-muted margin-top">
${__("Fetching...")}
</div>
`);
frappe.call('erpnext.manufacturing.doctype.bom.bom.get_bom_diff', {
bom1: name1,
bom2: name2
}).then(r => {
let diff = r.message;
frappe.model.with_doctype('BOM', () => {
this.render('BOM', name1, name2, diff);
});
});
}
render(doctype, name1, name2, diff) {
let change_html = (title, doctype, changed) => {
let values_changed = this.get_changed_values(doctype, changed)
.map(change => {
let [fieldname, value1, value2] = change;
return `
<tr>
<td>${frappe.meta.get_label(doctype, fieldname)}</td>
<td>${value1}</td>
<td>${value2}</td>
</tr>
`;
})
.join('');
return `
<h4 class="margin-top">${title}</h4>
<div>
<table class="table table-bordered">
<tr>
<th width="33%">${__('Field')}</th>
<th width="33%">${name1}</th>
<th width="33%">${name2}</th>
</tr>
${values_changed}
</table>
</div>
`;
}
let value_changes = change_html(__('Values Changed'), doctype, diff.changed);
let row_changes_by_fieldname = group_items(diff.row_changed, change => change[0]);
let table_changes = Object.keys(row_changes_by_fieldname).map(fieldname => {
let changes = row_changes_by_fieldname[fieldname];
let df = frappe.meta.get_docfield(doctype, fieldname);
let html = changes.map(change => {
let [fieldname,, item_code, changes] = change;
let df = frappe.meta.get_docfield(doctype, fieldname);
let child_doctype = df.options;
let values_changed = this.get_changed_values(child_doctype, changes);
return values_changed.map((change, i) => {
let [fieldname, value1, value2] = change;
let th = i === 0
? `<th rowspan="${values_changed.length}">${item_code}</th>`
: '';
return `
<tr>
${th}
<td>${frappe.meta.get_label(child_doctype, fieldname)}</td>
<td>${value1}</td>
<td>${value2}</td>
</tr>
`;
}).join('');
}).join('');
return `
<h4 class="margin-top">${__('Changes in {0}', [df.label])}</h4>
<table class="table table-bordered">
<tr>
<th width="25%">${__('Item Code')}</th>
<th width="25%">${__('Field')}</th>
<th width="25%">${name1}</th>
<th width="25%">${name2}</th>
</tr>
${html}
</table>
`;
}).join('');
let get_added_removed_html = (title, grouped_items) => {
return Object.keys(grouped_items).map(fieldname => {
let rows = grouped_items[fieldname];
let df = frappe.meta.get_docfield(doctype, fieldname);
let fields = frappe.meta.get_docfields(df.options)
.filter(df => df.in_list_view);
let html = rows.map(row => {
let [, doc] = row;
let cells = fields
.map(df => `<td>${doc[df.fieldname]}</td>`)
.join('');
return `<tr>${cells}</tr>`;
}).join('');
let header = fields.map(df => `<th>${df.label}</th>`).join('');
return `
<h4 class="margin-top">${$.format(title, [df.label])}</h4>
<table class="table table-bordered">
<tr>${header}</tr>
${html}
</table>
`;
}).join('');
};
let added_by_fieldname = group_items(diff.added, change => change[0]);
let removed_by_fieldname = group_items(diff.removed, change => change[0]);
let added_html = get_added_removed_html(__('Rows Added in {0}'), added_by_fieldname);
let removed_html = get_added_removed_html(__('Rows Removed in {0}'), removed_by_fieldname);
let html = `
${value_changes}
${table_changes}
${added_html}
${removed_html}
`;
this.form.get_field('preview').html(html);
}
get_changed_values(doctype, changed) {
return changed.filter(change => {
let [fieldname, value1, value2] = change;
if (!value1) value1 = '';
if (!value2) value2 = '';
if (value1 === value2) return false;
let df = frappe.meta.get_docfield(doctype, fieldname);
if (!df) return false;
if (df.hidden) return false;
return true;
});
}
};
function group_items(array, fn) {
return array.reduce((acc, item) => {
let key = fn(item);
acc[key] = acc[key] || [];
acc[key].push(item);
return acc;
}, {});
}

View File

@ -0,0 +1,30 @@
{
"content": null,
"creation": "2019-07-29 13:24:38.201981",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2019-07-29 13:24:38.201981",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "bom-comparison-tool",
"owner": "Administrator",
"page_name": "BOM Comparison Tool",
"restrict_to_domain": "Manufacturing",
"roles": [
{
"role": "System Manager"
},
{
"role": "Manufacturing User"
},
{
"role": "Manufacturing Manager"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "BOM Comparison Tool"
}

View File

@ -596,6 +596,7 @@ erpnext.patches.v12_0.rename_pricing_rule_child_doctypes
erpnext.patches.v12_0.move_target_distribution_from_parent_to_child erpnext.patches.v12_0.move_target_distribution_from_parent_to_child
erpnext.patches.v12_0.stock_entry_enhancements erpnext.patches.v12_0.stock_entry_enhancements
erpnext.patches.v10_0.item_barcode_childtable_migrate # 16-02-2019 #25-06-2019 erpnext.patches.v10_0.item_barcode_childtable_migrate # 16-02-2019 #25-06-2019
erpnext.patches.v12_0.make_item_manufacturer
erpnext.patches.v12_0.move_item_tax_to_item_tax_template erpnext.patches.v12_0.move_item_tax_to_item_tax_template
erpnext.patches.v11_1.set_variant_based_on erpnext.patches.v11_1.set_variant_based_on
erpnext.patches.v11_1.woocommerce_set_creation_user erpnext.patches.v11_1.woocommerce_set_creation_user
@ -606,7 +607,6 @@ erpnext.patches.v11_1.delete_scheduling_tool
erpnext.patches.v12_0.rename_tolerance_fields erpnext.patches.v12_0.rename_tolerance_fields
erpnext.patches.v12_0.make_custom_fields_for_bank_remittance #14-06-2019 erpnext.patches.v12_0.make_custom_fields_for_bank_remittance #14-06-2019
execute:frappe.delete_doc_if_exists("Page", "support-analytics") execute:frappe.delete_doc_if_exists("Page", "support-analytics")
erpnext.patches.v12_0.make_item_manufacturer
erpnext.patches.v12_0.remove_patient_medical_record_page erpnext.patches.v12_0.remove_patient_medical_record_page
erpnext.patches.v11_1.move_customer_lead_to_dynamic_column erpnext.patches.v11_1.move_customer_lead_to_dynamic_column
erpnext.patches.v11_1.set_default_action_for_quality_inspection erpnext.patches.v11_1.set_default_action_for_quality_inspection
@ -626,3 +626,4 @@ erpnext.patches.v12_0.add_default_buying_selling_terms_in_company
erpnext.patches.v12_0.update_ewaybill_field_position erpnext.patches.v12_0.update_ewaybill_field_position
erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes
erpnext.patches.v11_1.set_status_for_material_request_type_manufacture erpnext.patches.v11_1.set_status_for_material_request_type_manufacture
erpnext.patches.v12_0.generate_leave_ledger_entries

View File

@ -0,0 +1,87 @@
# Copyright (c) 2018, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import getdate, today
def execute():
""" Generates leave ledger entries for leave allocation/application/encashment
for last allocation """
frappe.reload_doc("HR", "doctype", "Leave Ledger Entry")
frappe.reload_doc("HR", "doctype", "Leave Encashment")
if frappe.db.a_row_exists("Leave Ledger Entry"):
return
if not frappe.get_meta("Leave Allocation").has_field("unused_leaves"):
frappe.reload_doc("HR", "doctype", "Leave Allocation")
update_leave_allocation_fieldname()
generate_allocation_ledger_entries()
generate_application_leave_ledger_entries()
generate_encashment_leave_ledger_entries()
generate_expiry_allocation_ledger_entries()
def update_leave_allocation_fieldname():
''' maps data from old field to the new field '''
frappe.db.sql("""
UPDATE `tabLeave Allocation`
SET `unused_leaves` = `carry_forwarded_leaves`
""")
def generate_allocation_ledger_entries():
''' fix ledger entries for missing leave allocation transaction '''
allocation_list = get_allocation_records()
for allocation in allocation_list:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}):
allocation.update(dict(doctype="Leave Allocation"))
allocation_obj = frappe.get_doc(allocation)
allocation_obj.create_leave_ledger_entry()
def generate_application_leave_ledger_entries():
''' fix ledger entries for missing leave application transaction '''
leave_applications = get_leaves_application_records()
for application in leave_applications:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}):
application.update(dict(doctype="Leave Application"))
frappe.get_doc(application).create_leave_ledger_entry()
def generate_encashment_leave_ledger_entries():
''' fix ledger entries for missing leave encashment transaction '''
leave_encashments = get_leave_encashment_records()
for encashment in leave_encashments:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}):
encashment.update(dict(doctype="Leave Encashment"))
frappe.get_doc(encashment).create_leave_ledger_entry()
def generate_expiry_allocation_ledger_entries():
''' fix ledger entries for missing leave allocation transaction '''
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation
allocation_list = get_allocation_records()
for allocation in allocation_list:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}):
allocation.update(dict(doctype="Leave Allocation"))
allocation_obj = frappe.get_doc(allocation)
if allocation_obj.to_date <= getdate(today()):
expire_allocation(allocation_obj)
def get_allocation_records():
return frappe.get_all("Leave Allocation", filters={
"docstatus": 1
}, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated',
'unused_leaves', 'from_date', 'to_date', 'carry_forward'
], order_by='to_date ASC')
def get_leaves_application_records():
return frappe.get_all("Leave Application", filters={
"docstatus": 1
}, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date'])
def get_leave_encashment_records():
return frappe.get_all("Leave Encashment", filters={
"docstatus": 1
}, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date'])

View File

@ -24,7 +24,7 @@ def execute():
frappe.reload_doc(module, 'doctype', frappe.scrub(doctype)) frappe.reload_doc(module, 'doctype', frappe.scrub(doctype))
child_doc = frappe.scrub(doctype) + '_item' child_doc = frappe.scrub(doctype) + '_item'
frappe.reload_doc(module, 'doctype', child_doc) frappe.reload_doc(module, 'doctype', child_doc, force=True)
child_doctype = doctype + ' Item' child_doctype = doctype + ' Item'

View File

@ -164,7 +164,7 @@ class Task(NestedSet):
if self.status not in ('Cancelled', 'Completed') and self.exp_end_date: if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
from datetime import datetime from datetime import datetime
if self.exp_end_date < datetime.now().date(): if self.exp_end_date < datetime.now().date():
self.db_set('status', 'Overdue') self.db_set('status', 'Overdue', update_modified=False)
self.update_project() self.update_project()
@frappe.whitelist() @frappe.whitelist()

View File

@ -11,12 +11,51 @@ class CallPopup {
'static': true, 'static': true,
'minimizable': true, 'minimizable': true,
'fields': [{ 'fields': [{
'fieldname': 'caller_info', 'fieldname': 'name',
'fieldtype': 'HTML' 'label': 'Name',
'default': this.get_caller_name() || __('Unknown Caller'),
'fieldtype': 'Data',
'read_only': 1
}, {
'fieldtype': 'Button',
'label': __('Open Contact'),
'click': () => frappe.set_route('Form', 'Contact', this.call_log.contact),
'depends_on': () => this.call_log.contact
}, {
'fieldtype': 'Button',
'label': __('Open Lead'),
'click': () => frappe.set_route('Form', 'Lead', this.call_log.lead),
'depends_on': () => this.call_log.lead
}, {
'fieldtype': 'Button',
'label': __('Make New Contact'),
'click': () => frappe.new_doc('Contact', { 'mobile_no': this.caller_number }),
'depends_on': () => !this.get_caller_name()
}, {
'fieldtype': 'Button',
'label': __('Make New Lead'),
'click': () => frappe.new_doc('Lead', { 'mobile_no': this.caller_number }),
'depends_on': () => !this.get_caller_name()
}, {
'fieldtype': 'Column Break',
}, {
'fieldname': 'number',
'label': 'Phone Number',
'fieldtype': 'Data',
'default': this.caller_number,
'read_only': 1
}, { }, {
'fielname': 'last_interaction', 'fielname': 'last_interaction',
'fieldtype': 'Section Break', 'fieldtype': 'Section Break',
'label': __('Activity'), 'label': __('Activity'),
'depends_on': () => this.get_caller_name()
}, {
'fieldtype': 'Small Text',
'label': __('Last Issue'),
'fieldname': 'last_issue',
'read_only': true,
'depends_on': () => this.call_log.contact,
'default': `<i class="text-muted">${__('No issue has been raised by the caller.')}<i>`
}, { }, {
'fieldtype': 'Small Text', 'fieldtype': 'Small Text',
'label': __('Last Communication'), 'label': __('Last Communication'),
@ -24,13 +63,7 @@ class CallPopup {
'read_only': true, 'read_only': true,
'default': `<i class="text-muted">${__('No communication found.')}<i>` 'default': `<i class="text-muted">${__('No communication found.')}<i>`
}, { }, {
'fieldtype': 'Small Text', 'fieldtype': 'Section Break',
'label': __('Last Issue'),
'fieldname': 'last_issue',
'read_only': true,
'default': `<i class="text-muted">${__('No issue raised by the customer.')}<i>`
}, {
'fieldtype': 'Column Break',
}, { }, {
'fieldtype': 'Small Text', 'fieldtype': 'Small Text',
'label': __('Call Summary'), 'label': __('Call Summary'),
@ -41,13 +74,21 @@ class CallPopup {
'click': () => { 'click': () => {
const call_summary = this.dialog.get_value('call_summary'); const call_summary = this.dialog.get_value('call_summary');
if (!call_summary) return; if (!call_summary) return;
frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', { frappe.xcall('erpnext.communication.doctype.call_log.call_log.add_call_summary', {
'docname': this.call_log.id, 'call_log': this.call_log.name,
'summary': call_summary, 'summary': call_summary,
}).then(() => { }).then(() => {
this.close_modal(); this.close_modal();
frappe.show_alert({ frappe.show_alert({
message: `${__('Call Summary Saved')}<br><a class="text-small text-muted" href="#Form/Call Log/${this.call_log.name}">${__('View call log')}</a>`, message: `
${__('Call Summary Saved')}
<br>
<a
class="text-small text-muted"
href="#Form/Call Log/${this.call_log.name}">
${__('View call log')}
</a>
`,
indicator: 'green' indicator: 'green'
}); });
}); });
@ -55,71 +96,14 @@ class CallPopup {
}], }],
}); });
this.set_call_status(); this.set_call_status();
this.make_caller_info_section();
this.dialog.get_close_btn().show(); this.dialog.get_close_btn().show();
this.make_last_interaction_section();
this.dialog.$body.addClass('call-popup'); this.dialog.$body.addClass('call-popup');
this.dialog.set_secondary_action(this.close_modal.bind(this)); this.dialog.set_secondary_action(this.close_modal.bind(this));
frappe.utils.play_sound('incoming-call'); frappe.utils.play_sound('incoming-call');
this.dialog.show(); this.dialog.show();
} }
make_caller_info_section() {
const wrapper = this.dialog.get_field('caller_info').$wrapper;
wrapper.append(`<div class="text-muted"> ${__("Loading...")} </div>`);
frappe.xcall('erpnext.crm.doctype.utils.get_document_with_phone_number', {
'number': this.caller_number
}).then(contact_doc => {
wrapper.empty();
const contact = this.contact = contact_doc;
if (!contact) {
this.setup_unknown_caller(wrapper);
} else {
this.setup_known_caller(wrapper);
this.set_call_status();
this.make_last_interaction_section();
}
});
}
setup_unknown_caller(wrapper) {
wrapper.append(`
<div class="caller-info">
<b>${__('Unknown Number')}:</b> ${this.caller_number}
<button
class="margin-left btn btn-new btn-default btn-xs"
data-doctype="Contact"
title=${__("Make New Contact")}>
<i class="octicon octicon-plus text-medium"></i>
</button>
</div>
`).find('button').click(
() => frappe.set_route(`Form/Contact/New Contact?phone=${this.caller_number}`)
);
}
setup_known_caller(wrapper) {
const contact = this.contact;
const contact_name = frappe.utils.get_form_link(contact.doctype, contact.name, true, this.get_caller_name());
const links = contact.links ? contact.links : [];
let contact_links = '';
links.forEach(link => {
contact_links += `<div>${link.link_doctype}: ${frappe.utils.get_form_link(link.link_doctype, link.link_name, true)}</div>`;
});
wrapper.append(`
<div class="caller-info flex">
${frappe.avatar(null, 'avatar-xl', contact.name, contact.image || '')}
<div>
<h5>${contact_name}</h5>
<div>${contact.mobile_no || ''}</div>
<div>${contact.phone_no || ''}</div>
${contact_links}
</div>
</div>
`);
}
set_indicator(color, blink=false) { set_indicator(color, blink=false) {
let classes = `indicator ${color} ${blink ? 'blink': ''}`; let classes = `indicator ${color} ${blink ? 'blink': ''}`;
this.dialog.header.find('.indicator').attr('class', classes); this.dialog.header.find('.indicator').attr('class', classes);
@ -129,7 +113,7 @@ class CallPopup {
let title = ''; let title = '';
call_status = call_status || this.call_log.status; call_status = call_status || this.call_log.status;
if (['Ringing'].includes(call_status) || !call_status) { if (['Ringing'].includes(call_status) || !call_status) {
title = __('Incoming call from {0}', [this.get_caller_name()]); title = __('Incoming call from {0}', [this.get_caller_name() || this.caller_number]);
this.set_indicator('blue', true); this.set_indicator('blue', true);
} else if (call_status === 'In Progress') { } else if (call_status === 'In Progress') {
title = __('Call Connected'); title = __('Call Connected');
@ -164,13 +148,13 @@ class CallPopup {
if (!this.dialog.get_value('call_summary')) { if (!this.dialog.get_value('call_summary')) {
this.close_modal(); this.close_modal();
} }
}, 10000); }, 30000);
} }
make_last_interaction_section() { make_last_interaction_section() {
frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', { frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', {
'number': this.caller_number, 'contact': this.call_log.contact,
'reference_doc': this.contact 'lead': this.call_log.lead
}).then(data => { }).then(data => {
const comm_field = this.dialog.get_field('last_communication'); const comm_field = this.dialog.get_field('last_communication');
if (data.last_communication) { if (data.last_communication) {
@ -182,15 +166,20 @@ class CallPopup {
const issue = data.last_issue; const issue = data.last_issue;
const issue_field = this.dialog.get_field("last_issue"); const issue_field = this.dialog.get_field("last_issue");
issue_field.set_value(issue.subject); issue_field.set_value(issue.subject);
issue_field.$wrapper.append(`<a class="text-medium" href="#List/Issue?customer=${issue.customer}"> issue_field.$wrapper.append(`
<a class="text-medium" href="#List/Issue?customer=${issue.customer}">
${__('View all issues from {0}', [issue.customer])} ${__('View all issues from {0}', [issue.customer])}
</a>`); </a>
`);
} }
}); });
} }
get_caller_name() { get_caller_name() {
return this.contact ? this.contact.lead_name || this.contact.name || '' : this.caller_number; let log = this.call_log;
return log.contact_name || log.lead_name;
} }
setup_listener() { setup_listener() {
frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => { frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => {
this.call_disconnected(call_log); this.call_disconnected(call_log);

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