Merge branch 'develop' into version-12

This commit is contained in:
Sahil Khan 2019-08-22 13:10:17 +05:30
commit 7ff82fccf3
162 changed files with 485716 additions and 464417 deletions

View File

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

View File

@ -123,7 +123,8 @@ frappe.treeview_settings["Account"] = {
if(frappe.boot.user.can_read.indexOf("GL Entry") !== -1){
// show Dr if positive since balance is calculated as debit - credit else show Cr
let dr_or_cr = node.data.balance_in_account_currency > 0 ? "Dr": "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) {
$('<span class="balance-area pull-right text-muted small">'

View File

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

View File

@ -50,7 +50,7 @@ class BankTransaction(StatusUpdater):
if paid_amount and allocated_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))))
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"]:
self.clear_simple_entry(payment_entry)

View File

@ -624,8 +624,8 @@ def get_outstanding_reference_documents(args):
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data:
frappe.msgprint(_("No outstanding invoices found for the {0} <b>{1}</b> which qualify the filters you have specified")
.format(args.get("party_type").lower(), args.get("party")))
frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.")
.format(args.get("party_type").lower(), frappe.bold(args.get("party"))))
return data
@ -683,8 +683,8 @@ def get_orders_to_be_billed(posting_date, party_type, party,
order_list = []
for d in orders:
if not (d.outstanding_amount >= filters.get("outstanding_amt_greater_than")
and d.outstanding_amount <= filters.get("outstanding_amt_less_than")):
if not (flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))):
continue
d["voucher_type"] = voucher_type
@ -761,9 +761,23 @@ def get_party_details(company, party_type, party, date, cost_center=None):
@frappe.whitelist()
def get_account_details(account, date, cost_center=None):
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({
"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")
})

View File

@ -257,11 +257,8 @@ def reconcile_dr_cr_note(dr_cr_notes):
voucher_type = ('Credit 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'
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({
"doctype": "Journal Entry",
@ -272,8 +269,7 @@ def reconcile_dr_cr_note(dr_cr_notes):
'account': d.account,
'party': d.party,
'party_type': d.party_type,
reconcile_dr_or_cr: (abs(d.allocated_amount)
if abs(d.unadjusted_amount) > abs(d.allocated_amount) else abs(d.unadjusted_amount)),
d.dr_or_cr: abs(d.allocated_amount),
'reference_type': d.against_voucher_type,
'reference_name': d.against_voucher
},
@ -281,7 +277,8 @@ def reconcile_dr_cr_note(dr_cr_notes):
'account': d.account,
'party': d.party,
'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_name': d.voucher_no
}

View File

@ -307,7 +307,7 @@ def get_item_tax_data():
# example: {'Consulting Services': {'Excise 12 - TS': '12.000'}}
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:
if tax.parent not in itemwise_tax:

View File

@ -469,7 +469,9 @@ def get_timeline_data(doctype, name):
# fetch and append data from Activity Log
data += frappe.db.sql("""select {fields}
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}
{group_by} order by creation desc
""".format(doctype=frappe.db.escape(doctype), name=frappe.db.escape(name), fields=fields,

View File

@ -33,6 +33,9 @@ class ReceivablePayableReport(object):
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':
columns.append({
"label": _("Customer Contact"),
@ -42,9 +45,6 @@ class ReceivablePayableReport(object):
"width": 100
})
if party_naming_by == "Naming Series":
columns += [args.get("party_type") + " Name::110"]
columns.append({
"label": _("Voucher Type"),
"fieldtype": "Data",
@ -197,8 +197,10 @@ class ReceivablePayableReport(object):
if self.filters.based_on_payment_terms and gl_entries_data:
self.payment_term_map = self.get_payment_term_detail(voucher_nos)
self.gle_inclusion_map = {}
for gle in gl_entries_data:
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(
gle,self.filters.report_date, self.dr_or_cr, return_entries)
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):
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))):
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)
if e.voucher_no not in return_entries:
payment_amount += amount

View File

@ -425,9 +425,12 @@ def get_cost_centers_with_children(cost_centers):
all_cost_centers = []
for d in cost_centers:
lft, rgt = frappe.db.get_value("Cost Center", d, ["lft", "rgt"])
children = frappe.get_all("Cost Center", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
all_cost_centers += [c.name for c in children]
if frappe.db.exists("Cost Center", d):
lft, rgt = frappe.db.get_value("Cost Center", d, ["lft", "rgt"])
children = frappe.get_all("Cost Center", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
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))

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))
@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"):
account = frappe.form_dict.get("account")
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 not frappe.flags.ignore_account_permission:
if not (frappe.flags.ignore_account_permission
or ignore_account_permission):
acc.check_permission("read")
if report_type == 'Profit and Loss':

View File

@ -8,12 +8,18 @@
"from",
"to",
"column_break_3",
"received_by",
"medium",
"caller_information",
"contact",
"contact_name",
"column_break_10",
"lead",
"lead_name",
"section_break_5",
"status",
"duration",
"recording_url",
"summary"
"recording_url"
],
"fields": [
{
@ -60,12 +66,6 @@
"label": "Duration",
"read_only": 1
},
{
"fieldname": "summary",
"fieldtype": "Data",
"label": "Summary",
"read_only": 1
},
{
"fieldname": "recording_url",
"fieldtype": "Data",
@ -77,10 +77,58 @@
"fieldtype": "Data",
"label": "Medium",
"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,
"modified": "2019-07-01 09:09:48.516722",
"modified": "2019-08-06 05:46:53.144683",
"modified_by": "Administrator",
"module": "Communication",
"name": "Call Log",
@ -97,10 +145,15 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "Employee"
}
],
"sort_field": "modified",
"sort_order": "ASC",
"title_field": "from",
"track_changes": 1
"track_changes": 1,
"track_views": 1
}

View File

@ -4,16 +4,88 @@
from __future__ import unicode_literals
import frappe
from frappe import _
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):
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):
employee_emails = get_employee_emails_for_popup(self.medium)
for email in employee_emails:
frappe.publish_realtime('show_call_popup', self, user=email)
self.trigger_call_popup()
def on_update(self):
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)
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

@ -134,6 +134,12 @@ def get_data():
"name": "Employee Leave Balance",
"doctype": "Leave Application"
},
{
"type": "report",
"is_query_report": True,
"name": "Leave Ledger Entry",
"doctype": "Leave Ledger Entry"
},
]
},
{
@ -213,6 +219,16 @@ def get_data():
"name": "Employee Benefit Claim",
"dependencies": ["Employee"]
},
{
"type": "doctype",
"name": "Employee Tax Exemption Category",
"dependencies": ["Employee"]
},
{
"type": "doctype",
"name": "Employee Tax Exemption Sub Category",
"dependencies": ["Employee"]
},
]
},
{

View File

@ -1192,7 +1192,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
.format(child_item.idx, child_item.item_code))
else:
child_item.rate = flt(d.get("rate"))
if child_item.price_list_rate:
if flt(child_item.price_list_rate):
child_item.discount_percentage = flt((1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0, \
child_item.precision("discount_percentage"))

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`
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.docstatus!=2
and tabAccount.{key} LIKE %(txt)s

View File

@ -18,34 +18,31 @@ def validate_return(doc):
validate_returned_items(doc)
def validate_return_against(doc):
filters = {"doctype": doc.doctype, "docstatus": 1, "company": doc.company}
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):
if not frappe.db.exists(doc.doctype, doc.return_against):
frappe.throw(_("Invalid {0}: {1}")
.format(doc.meta.get_label("return_against"), doc.return_against))
else:
ref_doc = frappe.get_doc(doc.doctype, doc.return_against)
# validate posting date time
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")
party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier"
if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime):
frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime)))
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
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")
# validate same exchange rate
if doc.conversion_rate != ref_doc.conversion_rate:
frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})")
.format(doc.doctype, doc.return_against, ref_doc.conversion_rate))
if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime):
frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime)))
# validate update stock
if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock:
frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}")
.format(doc.return_against))
# validate same exchange rate
if doc.conversion_rate != ref_doc.conversion_rate:
frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})")
.format(doc.doctype, doc.return_against, ref_doc.conversion_rate))
# validate update stock
if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock:
frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}")
.format(doc.return_against))
def validate_returned_items(doc):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos

View File

@ -145,6 +145,16 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
@frappe.whitelist()
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,
{"Lead": {
"doctype": "Opportunity",
@ -157,7 +167,7 @@ def make_opportunity(source_name, target_doc=None):
"email_id": "contact_email",
"mobile_no": "contact_mobile"
}
}}, target_doc)
}}, target_doc, set_missing_values)
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)
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) {
frm.toggle_display("contact_info", frm.doc.party_name);
frm.trigger('set_contact_link');
if (frm.doc.opportunity_from == "Customer") {
frm.trigger('set_contact_link');
erpnext.utils.get_party_details(frm);
} else if (frm.doc.opportunity_from == "Lead") {
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");
},
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) {
frm.trigger('toggle_mandatory');
},

View File

@ -3,82 +3,59 @@ from frappe import _
import json
@frappe.whitelist()
def get_document_with_phone_number(number):
# 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)
def get_last_interaction(contact=None, lead=None):
if contacts:
return frappe.get_doc('Contact', contacts[0].name)
if not contact and not lead: return
leads = frappe.get_all('Lead', or_filters=number_filter, limit=1)
if leads:
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 = ''
last_communication = None
last_issue = None
if contact:
query_condition = ''
for link in reference_doc.links:
link = frappe._dict(link)
values = []
contact = frappe.get_doc('Contact', contact)
for link in contact.links:
if link.link_doctype == 'Customer':
customer_name = link.link_name
query_condition += "(`reference_doctype`='{}' AND `reference_name`='{}') OR".format(link.link_doctype, link.link_name)
last_issue = get_last_issue_from_customer(link.link_name)
query_condition += "(`reference_doctype`=%s AND `reference_name`=%s) OR"
values += [link_link_doctype, link_link_name]
if query_condition:
# remove extra appended 'OR'
query_condition = query_condition[:-2]
last_communication = frappe.db.sql("""
SELECT `name`, `content`
FROM `tabCommunication`
WHERE {}
WHERE `sent_or_received`='Received'
AND ({})
ORDER BY `modified`
LIMIT 1
""".format(query_condition)) # nosec
""".format(query_condition), values, as_dict=1) # nosec
if customer_name:
last_issue = frappe.get_all('Issue', {
'customer': customer_name
}, ['name', 'subject', 'customer'], limit=1)
elif reference_doc.doctype == 'Lead':
if lead:
last_communication = frappe.get_all('Communication', filters={
'reference_doctype': reference_doc.doctype,
'reference_name': reference_doc.name,
'reference_doctype': 'Lead',
'reference_name': lead,
'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 {
'last_communication': last_communication[0] if last_communication else None,
'last_issue': last_issue[0] if last_issue else None
'last_communication': last_communication,
'last_issue': last_issue
}
@frappe.whitelist()
def add_call_summary(docname, summary):
call_log = frappe.get_doc('Call Log', docname)
summary = _('Call Summary by {0}: {1}').format(
frappe.utils.get_fullname(frappe.session.user), summary)
if not call_log.summary:
call_log.summary = summary
else:
call_log.summary += '<br>' + summary
call_log.save(ignore_permissions=True)
def get_last_issue_from_customer(customer_name):
issues = frappe.get_all('Issue', {
'customer': customer_name
}, ['name', 'subject', 'customer'], order_by='`creation` DESC', limit=1)
return issues[0] if issues else None
def get_scheduled_employees_for_popup(communication_medium):
if not communication_medium: return []
def get_employee_emails_for_popup(communication_medium):
now_time = frappe.utils.nowtime()
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])
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

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

View File

@ -231,8 +231,12 @@ doc_events = {
('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): {
'validate': 'erpnext.regional.india.utils.set_place_of_supply'
},
"Contact":{
"on_trash": "erpnext.support.doctype.issue.issue.update_issue"
"Contact": {
"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": {
"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"
],
"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": [
"erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income",

View File

@ -4,6 +4,7 @@
"creation": "2013-01-10 16:34:13",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"attendance_details",
"naming_series",
@ -19,7 +20,9 @@
"department",
"shift",
"attendance_request",
"amended_from"
"amended_from",
"late_entry",
"early_exit"
],
"fields": [
{
@ -153,12 +156,24 @@
"fieldtype": "Link",
"label": "Shift",
"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",
"idx": 1,
"is_submittable": 1,
"modified": "2019-06-05 19:37:30.410071",
"modified": "2019-07-29 20:35:40.845422",
"modified_by": "Administrator",
"module": "HR",
"name": "Attendance",

View File

@ -76,6 +76,7 @@ class Employee(NestedSet):
if self.user_id:
self.update_user()
self.update_user_permissions()
self.reset_employee_emails_cache()
def update_user_permissions(self):
if not self.create_user_permission: return
@ -214,6 +215,15 @@ class Employee(NestedSet):
doc.validate_employee_creation()
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):
'''Return timeline for attendance'''
return dict(frappe.db.sql('''select unix_timestamp(attendance_date), count(*)

View File

@ -14,8 +14,6 @@
"device_id",
"skip_auto_attendance",
"attendance",
"entry_grace_period_consequence",
"exit_grace_period_consequence",
"shift_start",
"shift_end",
"shift_actual_start",
@ -80,20 +78,6 @@
"options": "Attendance",
"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",
"fieldtype": "Datetime",
@ -119,7 +103,7 @@
"label": "Shift Actual End"
}
],
"modified": "2019-06-10 15:33:22.731697",
"modified": "2019-07-23 23:47:33.975263",
"modified_by": "Administrator",
"module": "HR",
"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
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.
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,
'working_hours': working_hours,
'company': employee_doc.company,
'shift': shift
'shift': shift,
'late_entry': late_entry,
'early_exit': early_exit
}
attendance = frappe.get_doc(doc_dict).insert()
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'
"""
total_hours = 0
in_time = out_time = None
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':
# 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':
logs = logs[:]
while len(logs) >= 2:
total_hours += time_diff_in_hours(logs[0].time, logs[1].time)
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')]
last_out_log = logs[len(logs)-1-find_index_in_dict(reversed(logs), 'log_type', 'OUT')]
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':
in_log = out_log = None
for log in logs:
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)
in_log = out_log = None
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:
out_log = log if log.log_type == 'OUT' else None
if in_log and out_log:
out_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):
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]
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])
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])
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])
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):
logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))]

View File

@ -210,10 +210,42 @@
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "notify_users_by_email",
"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": "Notify users by email",
"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,
@ -548,7 +580,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:55.968224",
"modified": "2019-08-01 16:15:55.968224",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Onboarding",

View File

@ -29,6 +29,9 @@ class EmployeeOnboarding(EmployeeBoardingController):
def on_submit(self):
super(EmployeeOnboarding, self).on_submit()
def on_update_after_submit(self):
self.create_task_and_notify_user()
def on_cancel(self):
super(EmployeeOnboarding, self).on_cancel()

View File

@ -145,40 +145,43 @@
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_bulk_edit": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "project",
"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": "Project",
"length": 0,
"no_copy": 0,
"options": "Project",
"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_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "notify_users_by_email",
"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": "Notify users by email",
"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,
@ -276,7 +279,40 @@
"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": "project",
"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": "Project",
"length": 0,
"no_copy": 0,
"options": "Project",
"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,
@ -550,7 +586,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:39.025898",
"modified": "2019-08-03 16:15:39.025898",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Separation",

View File

@ -11,6 +11,9 @@ class EmployeeSeparation(EmployeeBoardingController):
def on_submit(self):
super(EmployeeSeparation, self).on_submit()
def on_update_after_submit(self):
self.create_task_and_notify_user()
def on_cancel(self):
super(EmployeeSeparation, self).on_cancel()

View File

@ -24,6 +24,7 @@
"column_break_18",
"leave_approver_mandatory_in_leave_application",
"show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
"hiring_settings",
"check_vacancies"
],
@ -153,12 +154,18 @@
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
},
{
"default": "0",
"fieldname": "auto_leave_encashment",
"fieldtype": "Check",
"label": "Auto Leave Encashment"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"modified": "2019-07-01 18:59:55.256878",
"modified": "2019-08-05 13:07:17.993968",
"modified_by": "Administrator",
"module": "HR",
"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) {
frm.trigger("calculate_total_leaves_allocated");
},
leave_type: function(frm) {
frm.trigger("leave_policy");
frm.trigger("calculate_total_leaves_allocated");
},
@ -33,37 +63,38 @@ frappe.ui.form.on("Leave Allocation", {
frm.trigger("calculate_total_leaves_allocated");
},
carry_forwarded_leaves: function(frm) {
unused_leaves: function(frm) {
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) {
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) {
if (cint(frm.doc.carry_forward) == 1 && frm.doc.leave_type && frm.doc.employee) {
return frappe.call({
method: "erpnext.hr.doctype.leave_allocation.leave_allocation.get_carry_forwarded_leaves",
args: {
"employee": frm.doc.employee,
"date": frm.doc.from_date,
"leave_type": frm.doc.leave_type,
"carry_forward": frm.doc.carry_forward
},
method: "set_total_leaves_allocated",
doc: frm.doc,
callback: function(r) {
if (!r.exc && r.message) {
frm.set_value('carry_forwarded_leaves', r.message);
frm.set_value("total_leaves_allocated",
flt(r.message) + flt(frm.doc.new_leaves_allocated));
}
frm.refresh_fields();
}
})
} 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));
}
}
})
});

View File

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

View File

@ -3,11 +3,11 @@
from __future__ import unicode_literals
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.model.document import Document
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 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")\
.format(self.leave_type, self.employee))
def on_update_after_submit(self):
self.validate_new_leaves_allocated_value()
self.set_total_leaves_allocated()
def on_submit(self):
self.create_leave_ledger_entry()
frappe.db.set(self,'carry_forwarded_leaves', flt(self.carry_forwarded_leaves))
frappe.db.set(self,'total_leaves_allocated',flt(self.total_leaves_allocated))
# expire all unused leaves in the ledger on creation of carry forward allocation
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):
if date_diff(self.to_date, self.from_date) <= 0:
@ -87,13 +91,32 @@ class LeaveAllocation(Document):
BackDatedAllocationError)
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.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"):
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):
# Adding a day to include To Date in the difference
@ -101,15 +124,37 @@ class LeaveAllocation(Document):
if date_difference < self.total_leaves_allocated:
frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError)
def validate_against_leave_applications(self):
leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
self.from_date, self.to_date)
def create_leave_ledger_entry(self, submit=True):
if self.unused_leaves:
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):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
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))
else:
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)
args = dict(
leaves=self.new_leaves_allocated,
from_date=self.from_date,
to_date=self.to_date,
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):
leave_allocated = 0
@ -136,25 +181,28 @@ def get_leave_allocation_for_period(employee, leave_type, from_date, to_date):
@frappe.whitelist()
def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None):
carry_forwarded_leaves = 0
if carry_forward:
''' Returns carry forwarded leaves for the given employee '''
unused_leaves = 0.0
previous_allocation = get_previous_allocation(date, leave_type, employee)
if carry_forward and previous_allocation:
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("""
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)
return unused_leaves
carry_forwarded_leaves = flt(previous_allocation[0].total_leaves_allocated) - flt(leaves_taken)
return carry_forwarded_leaves
def get_unused_leaves(employee, leave_type, from_date, to_date):
''' Returns unused leaves between the given period while skipping leave allocation expiry '''
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):
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))

View File

@ -12,4 +12,9 @@ def get_data():
'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,
"from date correctly set"),
// 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"),
() => done()
]);

View File

@ -1,12 +1,14 @@
from __future__ import unicode_literals
import frappe
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):
def test_overlapping_allocation(self):
frappe.db.sql("delete from `tabLeave Allocation`")
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
leaves = [
{
@ -18,7 +20,7 @@ class TestLeaveAllocation(unittest.TestCase):
"from_date": getdate("2015-10-01"),
"to_date": getdate("2015-10-31"),
"new_leaves_allocated": 5,
"docstatus": 1
"docstatus": 1
},
{
"doctype": "Leave Allocation",
@ -28,17 +30,17 @@ class TestLeaveAllocation(unittest.TestCase):
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-01"),
"to_date": getdate("2015-11-30"),
"new_leaves_allocated": 5
"new_leaves_allocated": 5
}
]
frappe.get_doc(leaves[0]).save()
self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save)
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])
d = frappe.get_doc({
doc = frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": employee.name,
@ -46,15 +48,15 @@ class TestLeaveAllocation(unittest.TestCase):
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-30"),
"to_date": getdate("2015-09-1"),
"new_leaves_allocated": 5
"new_leaves_allocated": 5
})
#invalid period
self.assertRaises(frappe.ValidationError, d.save)
self.assertRaises(frappe.ValidationError, doc.save)
def test_allocated_leave_days_over_period(self):
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",
"__islocal": 1,
"employee": employee.name,
@ -62,10 +64,102 @@ class TestLeaveAllocation(unittest.TestCase):
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-1"),
"to_date": getdate("2015-09-30"),
"new_leaves_allocated": 35
"new_leaves_allocated": 35
})
#allocated leave more than period
self.assertRaises(frappe.ValidationError, d.save)
#allocated leave more than period
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"]

View File

@ -49,7 +49,7 @@ frappe.ui.form.on("Leave Application", {
async: false,
args: {
employee: frm.doc.employee,
date: frm.doc.posting_date
date: frm.doc.from_date || frm.doc.posting_date
},
callback: function(r) {
if (!r.exc && r.message['leave_allocation']) {
@ -60,9 +60,8 @@ frappe.ui.form.on("Leave Application", {
}
}
});
$("div").remove(".form-dashboard-section");
let section = frm.dashboard.add_section(
frm.dashboard.add_section(
frappe.render_template('leave_application_dashboard', {
data: leave_details
})
@ -115,6 +114,7 @@ frappe.ui.form.on("Leave Application", {
},
from_date: function(frm) {
frm.trigger("make_dashboard");
frm.trigger("half_day_datepicker");
frm.trigger("calculate_total_days");
},
@ -138,12 +138,13 @@ frappe.ui.form.on("Leave Application", {
},
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({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on",
args: {
employee: frm.doc.employee,
date: frm.doc.from_date,
to_date: frm.doc.to_date,
leave_type: frm.doc.leave_type,
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
from frappe import _
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.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.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 OverlapError(frappe.ValidationError): pass
@ -50,6 +51,7 @@ class LeaveApplication(Document):
# notify leave applier about approval
self.notify_employee()
self.create_leave_ledger_entry()
self.reload()
def on_cancel(self):
@ -57,6 +59,7 @@ class LeaveApplication(Document):
# notify leave applier about cancellation
self.notify_employee()
self.cancel_attendance()
self.create_leave_ledger_entry(submit=False)
def validate_applicable_after(self):
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."))
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)
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"):
frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}")
.format(self.leave_type))
@ -347,6 +350,54 @@ class LeaveApplication(Document):
except frappe.OutgoingEmailError:
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()
def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None):
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()
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 = {}
for d in allocation_records:
allocation = allocation_records.get(d, frappe._dict())
date = allocation.to_date
leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, date, status="Approved")
leaves_pending = get_leaves_for_period(employee, d, allocation.from_date, date, status="Open")
remaining_leaves = allocation.total_leaves_allocated - leaves_taken - leaves_pending
remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
consider_all_leaves_in_the_allocation_period=True)
end_date = allocation.to_date
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] = {
"total_leaves": allocation.total_leaves_allocated,
"leaves_taken": leaves_taken,
@ -386,27 +439,131 @@ def get_leave_details(employee, date):
return ret
@frappe.whitelist()
def get_leave_balance_on(employee, leave_type, date, allocation_records=None, docname=None,
consider_all_leaves_in_the_allocation_period=False, consider_encashed_leaves=True):
def get_leave_balance_on(employee, leave_type, date, to_date=nowdate(), consider_all_leaves_in_the_allocation_period=False):
'''
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(date, employee).get(employee, frappe._dict())
allocation_records = get_leave_allocation_records(employee, date, leave_type)
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):
leave_applications = frappe.db.sql("""
select name, employee, leave_type, from_date, to_date, total_leave_days
from `tabLeave Application`
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
return get_remaining_leaves(allocation, leaves_taken, date, expiry)
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
and status = %(status)s and docstatus != 2
and docstatus=1
and leaves<0
and (from_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))
@ -414,43 +571,8 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, status, docn
"from_date": from_date,
"to_date": to_date,
"employee": employee,
"status": status,
"leave_type": leave_type
}, 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()
def get_holidays(employee, from_date, to_date):
@ -629,4 +751,4 @@ def get_leave_approver(employee, department=None):
if department:
return frappe.db.get_value('Department Approver', {'parent': department,
'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
'parentfield': 'leave_approvers', 'idx': 1}, 'approver')

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

@ -7,7 +7,9 @@ import unittest
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.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"]
@ -17,6 +19,7 @@ _test_records = [
"doctype": "Leave Application",
"employee": "_T-Employee-00001",
"from_date": "2013-05-01",
"description": "_Test Reason",
"leave_type": "_Test Leave Type",
"posting_date": "2013-01-02",
"to_date": "2013-05-05"
@ -26,6 +29,7 @@ _test_records = [
"doctype": "Leave Application",
"employee": "_T-Employee-00002",
"from_date": "2013-05-01",
"description": "_Test Reason",
"leave_type": "_Test Leave Type",
"posting_date": "2013-01-02",
"to_date": "2013-05-05"
@ -35,6 +39,7 @@ _test_records = [
"doctype": "Leave Application",
"employee": "_T-Employee-00001",
"from_date": "2013-01-15",
"description": "_Test Reason",
"leave_type": "_Test Leave Type LWP",
"posting_date": "2013-01-02",
"to_date": "2013-01-15"
@ -44,8 +49,8 @@ _test_records = [
class TestLeaveApplication(unittest.TestCase):
def setUp(self):
for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]:
frappe.db.sql("delete from `tab%s`" % dt)
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
@classmethod
def setUpClass(cls):
@ -268,13 +273,14 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application',
employee = employee.name,
company = '_Test Company',
description = "_Test Reason",
leave_type = leave_type,
from_date = date,
to_date = date,
))
# 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.to_date = today
@ -285,7 +291,6 @@ class TestLeaveApplication(unittest.TestCase):
# check leave balance is reduced
self.assertEqual(get_leave_balance_on(employee.name, leave_type, today), 9)
def test_leaves_allowed(self):
employee = get_employee()
leave_period = get_leave_period()
@ -301,24 +306,25 @@ class TestLeaveApplication(unittest.TestCase):
allocate_leaves(employee, leave_period, leave_type.name, 5)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
description = "_Test Reason",
from_date = date,
to_date = add_days(date, 2),
company = "_Test Company",
docstatus = 1,
status = "Approved"
))
self.assertTrue(leave_application.insert())
leave_application.submit()
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
description = "_Test Reason",
from_date = add_days(date, 4),
to_date = add_days(date, 7),
to_date = add_days(date, 8),
company = "_Test Company",
docstatus = 1,
status = "Approved"
@ -342,6 +348,7 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
description = "_Test Reason",
from_date = date,
to_date = add_days(date, 4),
company = "_Test Company",
@ -363,6 +370,7 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type_1.name,
description = "_Test Reason",
from_date = date,
to_date = add_days(date, 4),
company = "_Test Company",
@ -392,6 +400,7 @@ class TestLeaveApplication(unittest.TestCase):
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
description = "_Test Reason",
from_date = date,
to_date = add_days(date, 4),
company = "_Test Company",
@ -401,6 +410,18 @@ class TestLeaveApplication(unittest.TestCase):
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):
leave_period = get_leave_period()
employee = get_employee()
@ -444,9 +465,10 @@ class TestLeaveApplication(unittest.TestCase):
allocation.insert(ignore_permissions=True)
allocation.submit()
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type,
description = "_Test Reason",
from_date = '2018-10-02',
to_date = '2018-10-02',
company = '_Test Company',
@ -457,9 +479,103 @@ class TestLeaveApplication(unittest.TestCase):
leave_application.submit()
self.assertEqual(leave_application.docstatus, 1)
def make_allocation_record(employee=None, leave_type=None):
frappe.db.sql("delete from `tabLeave Allocation`")
def test_creation_of_leave_ledger_entry_on_submit(self):
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({
"doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001",
@ -513,4 +629,4 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
"docstatus": 1
}).insert()
allocate_leave.submit()
allocate_leave.submit()

View File

@ -10,6 +10,8 @@ from frappe.utils import getdate, nowdate, flt
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.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):
def validate(self):
@ -25,7 +27,7 @@ class LeaveEncashment(Document):
def on_submit(self):
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.company = frappe.get_value("Employee", self.employee, "company")
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.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') + self.encashable_days)
self.create_leave_ledger_entry()
def on_cancel(self):
if self.additional_salary:
frappe.get_doc("Additional Salary", self.additional_salary).cancel()
@ -48,6 +52,7 @@ class LeaveEncashment(Document):
if self.leave_allocation:
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)
self.create_leave_ledger_entry(submit=False)
def get_leave_details_for_encashment(self):
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'):
frappe.throw(_("Leave Type {0} is not encashable").format(self.leave_type))
self.leave_balance = get_leave_balance_on(self.employee, self.leave_type,
self.encashment_date or getdate(nowdate()), consider_all_leaves_in_the_allocation_period=True)
allocation = self.get_leave_allocation()
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')
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')
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
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}'
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.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_policy.test_leave_policy import create_leave_policy\
test_dependencies = ["Leave Type"]
class TestLeaveEncashment(unittest.TestCase):
def setUp(self):
frappe.db.sql('''delete from `tabLeave Period`''')
def test_leave_balance_value_and_amount(self):
employee = "test_employee_encashment@salary.com"
leave_type = "_Test Leave Type Encashment"
frappe.db.sql('''delete from `tabLeave Allocation`''')
frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
frappe.db.sql('''delete from `tabAdditional Salary`''')
# create the leave policy
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"leave_policy_details": [{
"leave_type": leave_type,
"annual_allocation": 10
}]
}).insert()
leave_policy = create_leave_policy(
leave_type="_Test Leave Type Encashment",
annual_allocation=10)
leave_policy.submit()
# create employee, salary structure and assignment
employee = make_employee(employee)
frappe.db.set_value("Employee", employee, "leave_policy", leave_policy.name)
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", employee,
self.employee = make_employee("test_employee_encashment@example.com")
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})
# create the leave period and assign the leaves
leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
leave_period.grant_leave_allocation(employee=employee)
self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
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(
doctype = 'Leave Encashment',
employee = employee,
leave_type = leave_type,
leave_period = leave_period.name,
payroll_date = today()
doctype='Leave Encashment',
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
payroll_date=today()
)).insert()
self.assertEqual(leave_encashment.leave_balance, 10)
@ -53,3 +54,26 @@ class TestLeaveEncashment(unittest.TestCase):
leave_encashment.submit()
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",
"fieldname": "carry_forward_leaves",
"fieldname": "carry_forward",
"fieldtype": "Check"
}
],

View File

@ -1,294 +1,294 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "HR-LPR-.YYYY.-.#####",
"beta": 0,
"creation": "2018-04-13 15:20:52.864288",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "HR-LPR-.YYYY.-.#####",
"beta": 0,
"creation": "2018-04-13 15:20:52.864288",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "From Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "From Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "To Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "To Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"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,
"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_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"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": "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,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "optional_holiday_list",
"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": "Holiday List for Optional Leave",
"length": 0,
"no_copy": 0,
"options": "Holiday List",
"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,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "optional_holiday_list",
"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": "Holiday List for Optional Leave",
"length": 0,
"no_copy": 0,
"options": "Holiday List",
"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": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:43.305502",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Period",
"name_case": "",
"owner": "Administrator",
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-05-30 16:15:43.305502",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Period",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View File

@ -5,9 +5,10 @@
from __future__ import unicode_literals
import frappe
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 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 six import iteritems
@ -21,8 +22,8 @@ class LeavePeriod(Document):
condition_str = " and " + " and ".join(conditions) if len(conditions) else ""
employees = frappe.db.sql_list("select name from tabEmployee where status='Active' {condition}"
.format(condition=condition_str), tuple(values))
employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec
.format(condition=condition_str), tuple(values)))
return employees
@ -36,29 +37,29 @@ class LeavePeriod(Document):
def grant_leave_allocation(self, grade=None, department=None, designation=None,
employee=None, carry_forward_leaves=0):
employees = self.get_employees({
employee=None, carry_forward=0):
employee_records = self.get_employees({
"grade": grade,
"department": department,
"designation": designation,
"department": department,
"designation": designation,
"name": employee
})
if employees:
if len(employees) > 20:
if employee_records:
if len(employee_records) > 20:
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:
grant_leave_alloc_for_employees(employees, self, carry_forward_leaves)
grant_leave_alloc_for_employees(employee_records, self, carry_forward)
else:
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 = []
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()
count=0
for employee in employees:
count = 0
for employee in employee_records.keys():
if employee in existing_allocations_for:
continue
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:
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_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)
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:
frappe.msgprint(_("Leaves has been granted sucessfully"))
def get_existing_allocations(employees, leave_period):
leave_allocations = frappe.db.sql_list("""
select distinct employee from `tabLeave Allocation`
where leave_period=%s and employee in (%s) and docstatus=1
SELECT DISTINCT
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)
if leave_allocations:
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():
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:
leave_type_details.setdefault(d.name, d)
return leave_type_details
def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward_leaves):
allocation = frappe.new_doc("Leave Allocation")
allocation.employee = employee
allocation.leave_type = leave_type
allocation.from_date = leave_period.from_date
allocation.to_date = leave_period.to_date
def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining):
''' Creates leave allocation for the given employee in the provided leave period '''
if carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
carry_forward = 0
# 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
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
allocation.new_leaves_allocated = new_leaves_allocated
allocation.leave_period = leave_period.name
if carry_forward_leaves:
if leave_type_details.get(leave_type).is_carry_forward:
allocation.carry_forward = carry_forward_leaves
allocation = frappe.get_doc(dict(
doctype="Leave Allocation",
employee=employee,
leave_type=leave_type,
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.submit()
return allocation.name
return allocation.name

View File

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

View File

@ -12,16 +12,20 @@ class TestLeavePolicy(unittest.TestCase):
if random_leave_type:
random_leave_type = random_leave_type[0]
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.save()
leave_policy_details = {
"doctype": "Leave Policy",
"leave_policy_details": [{
"leave_type": leave_type.name,
"annual_allocation": leave_type.max_leaves_allowed + 1
}]
}
leave_policy = create_leave_policy(leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1)
self.assertRaises(frappe.ValidationError, frappe.get_doc(leave_policy_details).insert)
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",
"leave_policy_details": [{
"leave_type": args.leave_type or "_Test Leave Type",
"annual_allocation": args.annual_allocation or 10
}]
})

View File

@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
@ -14,10 +15,12 @@
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "leave_type_name",
"fieldtype": "Data",
"hidden": 0,
@ -42,15 +45,17 @@
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fetch_if_empty": 0,
"fieldname": "max_leaves_allowed",
"fieldtype": "Int",
"hidden": 0,
@ -78,10 +83,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "applicable_after",
"fieldtype": "Int",
"hidden": 0,
@ -109,10 +116,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "max_continuous_days_allowed",
"fieldtype": "Int",
"hidden": 0,
@ -141,10 +150,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
@ -171,10 +182,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_carry_forward",
"fieldtype": "Check",
"hidden": 0,
@ -203,10 +216,13 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fetch_if_empty": 0,
"fieldname": "is_lwp",
"fieldtype": "Check",
"hidden": 0,
@ -233,10 +249,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_optional_leave",
"fieldtype": "Check",
"hidden": 0,
@ -264,10 +282,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "allow_negative",
"fieldtype": "Check",
"hidden": 0,
@ -294,10 +314,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "include_holiday",
"fieldtype": "Check",
"hidden": 0,
@ -324,10 +346,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_compensatory",
"fieldtype": "Check",
"hidden": 0,
@ -355,10 +379,81 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"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",
"fieldtype": "Section Break",
"hidden": 0,
@ -386,10 +481,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "allow_encashment",
"fieldtype": "Check",
"hidden": 0,
@ -417,11 +514,13 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "allow_encashment",
"fetch_if_empty": 0,
"fieldname": "encashment_threshold_days",
"fieldtype": "Int",
"hidden": 0,
@ -449,11 +548,13 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "allow_encashment",
"fetch_if_empty": 0,
"fieldname": "earning_component",
"fieldtype": "Link",
"hidden": 0,
@ -482,10 +583,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "earned_leave",
"fieldtype": "Section Break",
"hidden": 0,
@ -513,10 +616,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_earned_leave",
"fieldtype": "Check",
"hidden": 0,
@ -544,11 +649,13 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "is_earned_leave",
"fetch_if_empty": 0,
"fieldname": "earned_leave_frequency",
"fieldtype": "Select",
"hidden": 0,
@ -577,12 +684,14 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.5",
"depends_on": "is_earned_leave",
"fetch_if_empty": 0,
"fieldname": "rounding",
"fieldtype": "Select",
"hidden": 0,
@ -611,17 +720,15 @@
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-flag",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-06-03 18:32:51.803472",
"modified": "2019-08-02 15:38:39.334283",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
@ -687,8 +794,8 @@
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 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
from __future__ import unicode_literals
import calendar
import frappe
from datetime import datetime
from frappe.utils import today
from frappe import _
from frappe.model.document import 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
from __future__ import unicode_literals
import frappe
test_records = frappe.get_test_records('Leave Type')
from frappe import _
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) {
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);
} else if(frm.doc.salary_slips_created) {
frm.add_custom_button(__("Submit Salary Slip"), function() {

View File

@ -13,14 +13,13 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
class PayrollEntry(Document):
def onload(self):
if not self.docstatus==1:
return
if not self.docstatus==1 or self.salary_slips_submitted:
return
# check if salary slips were manually submitted
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:
self.db_set("salary_slips_submitted", 1)
self.reload()
if cint(entries) == len(self.employees):
self.set_onload("submitted_ss", True)
def on_submit(self):
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
})
def get_frequency_kwargs(frequency_name):
frequency_dict = {
'monthly': {'months': 1},

View File

@ -23,14 +23,9 @@
"grace_period_settings_auto_attendance_section",
"enable_entry_grace_period",
"late_entry_grace_period",
"consequence_after",
"consequence",
"column_break_18",
"enable_exit_grace_period",
"enable_different_consequence_for_early_exit",
"early_exit_grace_period",
"early_exit_consequence_after",
"early_exit_consequence"
"early_exit_grace_period"
],
"fields": [
{
@ -107,21 +102,6 @@
"fieldtype": "Int",
"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",
"fieldtype": "Column Break"
@ -132,13 +112,6 @@
"fieldtype": "Check",
"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",
"description": "The time before the shift end time when check-out is considered as early (in minutes).",
@ -146,21 +119,6 @@
"fieldtype": "Int",
"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",
"description": "Time after the end of shift during which check-out is considered for attendance.",
@ -178,7 +136,6 @@
"depends_on": "enable_auto_attendance",
"fieldname": "grace_period_settings_auto_attendance_section",
"fieldtype": "Section Break",
"hidden": 1,
"label": "Grace Period Settings For Auto Attendance"
},
{
@ -201,7 +158,7 @@
"label": "Last Sync of Checkin"
}
],
"modified": "2019-06-10 06:02:44.272036",
"modified": "2019-07-30 01:05:24.660666",
"modified_by": "Administrator",
"module": "HR",
"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")
for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
single_shift_logs = list(group)
attendance_status, working_hours = self.get_attendance(single_shift_logs)
mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, self.name)
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, late_entry, early_exit, self.name)
for employee in self.get_assigned_employee(self.process_attendance_after, True):
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.
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:
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:
return 'Half Day', total_working_hours
return 'Present', total_working_hours
return 'Half Day', total_working_hours, late_entry, early_exit
return 'Present', total_working_hours, late_entry, early_exit
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.

View File

@ -24,6 +24,18 @@ frappe.query_reports["Employee Leave Balance"] = {
"options": "Company",
"reqd": 1,
"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
import frappe
from frappe import _
from frappe.utils import flt
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):
@ -30,17 +31,28 @@ def get_columns(leave_types):
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):
user = frappe.session.user
allocation_records_based_on_to_date = get_leave_allocation_records(filters.to_date)
allocation_records_based_on_from_date = get_leave_allocation_records(filters.from_date)
conditions = get_conditions(filters)
if filters.to_date <= filters.from_date:
frappe.throw(_("From date can not be greater than than To date"))
active_employees = frappe.get_all("Employee",
filters = { "status": "Active", "company": filters.company},
fields = ["name", "employee_name", "department", "user_id"])
filters=conditions,
fields=["name", "employee_name", "department", "user_id"])
data = []
for employee in active_employees:
@ -50,16 +62,14 @@ def get_data(filters, leave_types):
for leave_type in leave_types:
# leaves taken
leaves_taken = get_approved_leaves_for_period(employee.name, leave_type,
filters.from_date, filters.to_date)
leaves_taken = get_leaves_for_period(employee.name, leave_type,
filters.from_date, filters.to_date) * -1
# opening balance
opening = get_leave_balance_on(employee.name, leave_type, filters.from_date,
allocation_records_based_on_to_date.get(employee.name, frappe._dict()))
opening = get_total_allocated_leaves(employee.name, leave_type, filters.from_date, filters.to_date)
# closing balance
closing = get_leave_balance_on(employee.name, leave_type, filters.to_date,
allocation_records_based_on_to_date.get(employee.name, frappe._dict()))
closing = flt(opening) - flt(leaves_taken)
row += [opening, leaves_taken, closing]
@ -84,3 +94,18 @@ def get_approvers(department):
where parent = %s and parentfield = 'leave_approvers'""", (d), as_dict=True)])
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_list = [d[0] for d in leave_types]
columns.extend(leave_list)
columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"])
for emp in sorted(att_map):
emp_det = emp_map.get(emp)
@ -65,6 +66,10 @@ def execute(filters=None):
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)
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 = {}
for d in leave_details:
@ -80,7 +85,8 @@ def execute(filters=None):
row.append(leaves[d])
else:
row.append("0.0")
row.extend([time_default_counts[0][0],time_default_counts[0][1]])
data.append(row)
return columns, data

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, erpnext
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.desk.form import assign_to
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
@ -36,12 +36,18 @@ class EmployeeBoardingController(Document):
}).insert(ignore_permissions=True)
self.db_set("project", project.name)
self.db_set("boarding_status", "Pending")
self.reload()
self.create_task_and_notify_user()
def create_task_and_notify_user(self):
# create the task for the given project and assign to the concerned person
for activity in self.activities:
if activity.task:
continue
task = frappe.get_doc({
"doctype": "Task",
"project": project.name,
"project": self.project,
"subject": activity.activity_name + " : " + self.employee_name,
"description": activity.description,
"department": self.department,
@ -69,6 +75,7 @@ class EmployeeBoardingController(Document):
'doctype' : task.doctype,
'name' : task.name,
'description' : task.description or task.subject,
'notify': self.notify_users_by_email
}
assign_to.add(args)
@ -263,6 +270,21 @@ def get_leave_period(from_date, to_date, company):
if 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():
'''Allocate earned leaves to Employees'''
e_leave_types = frappe.get_all("Leave Type",
@ -270,31 +292,43 @@ def allocate_earned_leaves():
filters={'is_earned_leave' : 1})
today = getdate()
divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
if 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}'
between from_date and to_date and docstatus=1 and leave_type='{1}'"""
.format(today, e_leave_type.name), as_dict=1)
for allocation in leave_allocations:
leave_policy = get_employee_leave_policy(allocation.employee)
if not leave_policy:
continue
if not e_leave_type.earned_leave_frequency == "Monthly":
if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency):
continue
annual_allocation = frappe.db.sql("""select annual_allocation from `tabLeave Policy Detail`
where parent=%s and leave_type=%s""", (leave_policy.name, e_leave_type.name))
if annual_allocation and annual_allocation[0]:
earned_leaves = flt(annual_allocation[0][0]) / divide_by_frequency[e_leave_type.earned_leave_frequency]
if e_leave_type.rounding == "0.5":
earned_leaves = round(earned_leaves * 2) / 2
else:
earned_leaves = round(earned_leaves)
allocated_leaves = frappe.db.get_value('Leave Allocation', allocation.name, 'total_leaves_allocated')
new_allocation = flt(allocated_leaves) + flt(earned_leaves)
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)
for e_leave_type in e_leave_types:
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=%s""", (today, e_leave_type.name), as_dict=1)
for allocation in leave_allocations:
leave_policy = get_employee_leave_policy(allocation.employee)
if not leave_policy:
continue
if not e_leave_type.earned_leave_frequency == "Monthly":
if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency):
continue
annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={
'parent': leave_policy.name,
'leave_type': e_leave_type.name
}, 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":
earned_leaves = round(earned_leaves * 2) / 2
else:
earned_leaves = round(earned_leaves)
allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed
if new_allocation == 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):
'''Return True if current date matches frequency'''

View File

@ -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.create_accounting_dimensions_in_missing_doctypes
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

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

View File

@ -11,12 +11,51 @@ class CallPopup {
'static': true,
'minimizable': true,
'fields': [{
'fieldname': 'caller_info',
'fieldtype': 'HTML'
'fieldname': 'name',
'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',
'fieldtype': 'Section Break',
'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',
'label': __('Last Communication'),
@ -24,13 +63,7 @@ class CallPopup {
'read_only': true,
'default': `<i class="text-muted">${__('No communication found.')}<i>`
}, {
'fieldtype': 'Small Text',
'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': 'Section Break',
}, {
'fieldtype': 'Small Text',
'label': __('Call Summary'),
@ -41,13 +74,21 @@ class CallPopup {
'click': () => {
const call_summary = this.dialog.get_value('call_summary');
if (!call_summary) return;
frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', {
'docname': this.call_log.id,
frappe.xcall('erpnext.communication.doctype.call_log.call_log.add_call_summary', {
'call_log': this.call_log.name,
'summary': call_summary,
}).then(() => {
this.close_modal();
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'
});
});
@ -55,71 +96,14 @@ class CallPopup {
}],
});
this.set_call_status();
this.make_caller_info_section();
this.dialog.get_close_btn().show();
this.make_last_interaction_section();
this.dialog.$body.addClass('call-popup');
this.dialog.set_secondary_action(this.close_modal.bind(this));
frappe.utils.play_sound('incoming-call');
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) {
let classes = `indicator ${color} ${blink ? 'blink': ''}`;
this.dialog.header.find('.indicator').attr('class', classes);
@ -129,7 +113,7 @@ class CallPopup {
let title = '';
call_status = call_status || this.call_log.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);
} else if (call_status === 'In Progress') {
title = __('Call Connected');
@ -164,13 +148,13 @@ class CallPopup {
if (!this.dialog.get_value('call_summary')) {
this.close_modal();
}
}, 10000);
}, 30000);
}
make_last_interaction_section() {
frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', {
'number': this.caller_number,
'reference_doc': this.contact
'contact': this.call_log.contact,
'lead': this.call_log.lead
}).then(data => {
const comm_field = this.dialog.get_field('last_communication');
if (data.last_communication) {
@ -182,15 +166,20 @@ class CallPopup {
const issue = data.last_issue;
const issue_field = this.dialog.get_field("last_issue");
issue_field.set_value(issue.subject);
issue_field.$wrapper.append(`<a class="text-medium" href="#List/Issue?customer=${issue.customer}">
${__('View all issues from {0}', [issue.customer])}
</a>`);
issue_field.$wrapper.append(`
<a class="text-medium" href="#List/Issue?customer=${issue.customer}">
${__('View all issues from {0}', [issue.customer])}
</a>
`);
}
});
}
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() {
frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => {
this.call_disconnected(call_log);

View File

@ -1,12 +1,13 @@
frappe.provide('frappe.ui.form');
erpnext.doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
"Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Sales Invoice Item", "Purchase Invoice Item",
"Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", "Purchase Receipt Item",
"Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
"Subscription Plan"];
"Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Shipping Rule", "Loyalty Program",
"Fee Schedule", "Fee Structure", "Stock Reconciliation", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool",
"Subscription", "Purchase Order", "Journal Entry", "Material Request", "Purchase Receipt", "Landed Cost Item", "Asset"];
erpnext.child_docs = ["Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account",
"Material Request Item", "Delivery Note Item", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction",
"Landed Cost Item", "Asset Value Adjustment", "Opening Invoice Creation Tool Item", "Subscription Plan"];
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimension_filters",
@ -26,10 +27,51 @@ erpnext.doctypes_with_dimensions.forEach((doctype) => {
"is_group": 0
});
}
if (frm.is_new() && frappe.meta.has_field(doctype, 'company') && frm.doc.company) {
if (Object.keys(erpnext.default_dimensions).length > 0) {
if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
if (frm.is_new() && frappe.meta.has_field(doctype, 'company') && frm.doc.company) {
frm.set_value(dimension['fieldname'], erpnext.default_dimensions[frm.doc.company][dimension['document_type']]);
}
}
if (frm.doc.items && frm.doc.items.length) {
frm.doc.items[0][dimension['fieldname']] = erpnext.default_dimensions[frm.doc.company][dimension['document_type']];
}
if (frm.doc.accounts && frm.doc.accounts.length) {
frm.doc.accounts[0][dimension['fieldname']] = erpnext.default_dimensions[frm.doc.company][dimension['document_type']];
}
}
});
});
},
company: function(frm) {
if(frm.doc.company && (Object.keys(erpnext.default_dimensions).length > 0)) {
erpnext.dimension_filters.forEach((dimension) => {
if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
frm.set_value(dimension['fieldname'], erpnext.default_dimensions[frm.doc.company][dimension['document_type']]);
}
});
}
},
});
});
erpnext.child_docs.forEach((doctype) => {
frappe.ui.form.on(doctype, {
items_add: function(frm, cdt, cdn) {
erpnext.dimension_filters.forEach((dimension) => {
var row = frappe.get_doc(cdt, cdn);
frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]);
});
},
accounts_add: function(frm, cdt, cdn) {
erpnext.dimension_filters.forEach((dimension) => {
var row = frappe.get_doc(cdt, cdn);
frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]);
});
},

View File

@ -156,33 +156,21 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2B":
customers = frappe.get_all("Customer",
filters={
"gst_category": ["in", ["Registered Regular", "Deemed Export", "SEZ"]]
})
if customers:
conditions += """ and ifnull(gst_category, '') != 'Overseas' and is_return != 1
and customer in ({0})""".format(", ".join([frappe.db.escape(c.name) for c in customers]))
conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1"
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
if not b2c_limit:
frappe.throw(_("Please set B2C Limit in GST Settings."))
customers = frappe.get_all("Customer",
filters={
"gst_category": ["in", ["Unregistered"]]
})
if self.filters.get("type_of_business") == "B2C Large" and customers:
conditions += """ and SUBSTR(place_of_supply, 1, 2) != SUBSTR(company_gstin, 1, 2)
and grand_total > {0} and is_return != 1 and customer in ({1})""".\
and grand_total > {0} and is_return != 1 and gst_category ='Unregistered' """.\
format(flt(b2c_limit), ", ".join([frappe.db.escape(c.name) for c in customers]))
elif self.filters.get("type_of_business") == "B2C Small" and customers:
conditions += """ and (
SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2)
or grand_total <= {0}) and is_return != 1 and customer in ({1})""".\
or grand_total <= {0}) and is_return != 1 and gst_category ='Unregistered' """.\
format(flt(b2c_limit), ", ".join([frappe.db.escape(c.name) for c in customers]))
elif self.filters.get("type_of_business") == "CDNR":

View File

@ -0,0 +1,27 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Customer-wise Item Price"] = {
"filters": [
{
"label": __("Customer"),
"fieldname": "customer",
"fieldtype": "Link",
"options": "Customer",
"reqd": 1
},
{
"label": __("Item"),
"fieldname": "item",
"fieldtype": "Link",
"options": "Item",
"get_query": () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: { 'is_sales_item': 1 }
}
}
}
]
}

View File

@ -0,0 +1,43 @@
{
"add_total_row": 0,
"creation": "2019-06-12 03:25:36.263179",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"letter_head": "Delta9",
"modified": "2019-06-12 03:25:36.263179",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer-wise Item Price",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Customer",
"report_name": "Customer-wise Item Price",
"report_type": "Script Report",
"roles": [
{
"role": "Sales User"
},
{
"role": "Stock Manager"
},
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Sales Manager"
},
{
"role": "Sales Master Manager"
},
{
"role": "Stock User"
}
]
}

View File

@ -0,0 +1,103 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from erpnext import get_default_company
from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_price_list_rate_for
from frappe import _
def execute(filters=None):
if not filters:
filters = {}
if not filters.get("customer"):
frappe.throw(_("Please select a Customer"))
columns = get_columns(filters)
data = get_data(filters)
return columns, data
def get_columns(filters=None):
return [
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 150
},
{
"label": _("Item Name"),
"fieldname": "item_name",
"fieldtype": "Data",
"width": 200
},
{
"label": _("Selling Rate"),
"fieldname": "selling_rate",
"fieldtype": "Currency"
},
{
"label": _("Available Stock"),
"fieldname": "available_stock",
"fieldtype": "Float",
"width": 150
},
{
"label": _("Price List"),
"fieldname": "price_list",
"fieldtype": "Link",
"options": "Price List",
"width": 120
}
]
def get_data(filters=None):
data = []
customer_details = get_customer_details(filters)
items = get_selling_items(filters)
item_stock_map = frappe.get_all("Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code")
item_stock_map = {item.item_code: item.available for item in item_stock_map}
for item in items:
price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0
available_stock = item_stock_map.get(item.item_code)
data.append({
"item_code": item.item_code,
"item_name": item.item_name,
"selling_rate": price_list_rate,
"price_list": customer_details.get("price_list"),
"available_stock": available_stock,
})
return data
def get_customer_details(filters):
customer_details = get_party_details(party=filters.get("customer"), party_type="Customer")
customer_details.update({
"company": get_default_company(),
"price_list": customer_details.get("selling_price_list")
})
return customer_details
def get_selling_items(filters):
if filters.get("item"):
item_filters = {"item_code": filters.get("item"), "is_sales_item": 1, "disabled": 0}
else:
item_filters = {"is_sales_item": 1, "disabled": 0}
items = frappe.get_all("Item", filters=item_filters, fields=["item_code", "item_name"], order_by="item_name")
return items

View File

@ -701,6 +701,7 @@ def create_delivery_note(**args):
"qty": args.qty or 1,
"rate": args.rate or 100,
"conversion_factor": 1.0,
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1,
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,

View File

@ -39,11 +39,12 @@ cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
filters: {
"from": doctype,
"parent": doc.reference_name
"parent": doc.reference_name,
"inspection_type": doc.inspection_type
}
}
};
}
}
},
// Serial No based on item_code
cur_frm.fields_dict['item_serial_no'].get_query = function(doc, cdt, cdn) {

View File

@ -63,10 +63,12 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
mcond = get_match_cond(filters["from"])
cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
if filters.get('from') in ['Purchase Invoice Item', 'Purchase Receipt Item']:
if filters.get('from') in ['Purchase Invoice Item', 'Purchase Receipt Item']\
and filters.get("inspection_type") != "In Process":
cond = """and item_code in (select name from `tabItem` where
inspection_required_before_purchase = 1)"""
elif filters.get('from') in ['Sales Invoice Item', 'Delivery Note Item']:
elif filters.get('from') in ['Sales Invoice Item', 'Delivery Note Item']\
and filters.get("inspection_type") != "In Process":
cond = """and item_code in (select name from `tabItem` where
inspection_required_before_delivery = 1)"""
elif filters.get('from') == 'Stock Entry Detail':

View File

@ -464,16 +464,22 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
where item_code = %s and warehouse = %s
and valuation_rate >= 0
order by posting_date desc, posting_time desc, creation desc limit 1""", (item_code, warehouse))
where
item_code = %s
AND warehouse = %s
AND valuation_rate >= 0
AND NOT (voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
if not last_valuation_rate:
# Get valuation rate from last sle for the item against any warehouse
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
where item_code = %s and valuation_rate > 0
order by posting_date desc, posting_time desc, creation desc limit 1""", item_code)
where
item_code = %s
AND valuation_rate > 0
AND NOT(voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
if last_valuation_rate:
return flt(last_valuation_rate[0][0]) # as there is previous records, it might come with zero rate

View File

@ -32,7 +32,7 @@ frappe.ready(function() {
if(r.message.product_info.in_stock===0) {
$(".item-stock").html("<div style='color: red'> <i class='fa fa-close'></i> {{ _("Not in stock") }}</div>");
}
else if(r.message.product_info.in_stock===1) {
else if(r.message.product_info.in_stock===1 && r.message.cart_settings.show_stock_availability) {
var qty_display = "{{ _("In stock") }}";
if (r.message.product_info.show_stock_qty) {
qty_display += " ("+r.message.product_info.stock_qty+")";

View File

@ -7,7 +7,7 @@ class TestSearch(unittest.TestCase):
#Search for the word "cond", part of the word "conduire" (Lead) in french.
def test_contact_search_in_foreign_language(self):
frappe.local.lang = 'fr'
output = filter_dynamic_link_doctypes("DocType", "cond", "name", 0, 20, {'fieldtype': 'HTML', 'fieldname': 'contact_html'})
output = filter_dynamic_link_doctypes("DocType", "prospect", "name", 0, 20, {'fieldtype': 'HTML', 'fieldname': 'contact_html'})
result = [['found' for x in y if x=="Lead"] for y in output]
self.assertTrue(['found'] in result)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
DocType: Account,Accounts,དངུལ་རྩིས།
DocType: Pricing Rule,Buying,ཉོ་བ།
1 DocType: Account Accounts དངུལ་རྩིས།
1 DocType: Account Accounts དངུལ་རྩིས།
2 DocType: Pricing Rule Buying ཉོ་བ།

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
apps/erpnext/erpnext/stock/report/stock_ledger/stock_ledger.py,'Opening','Åbning'
DocType: Email Campaign,Lead,Bly
apps/erpnext/erpnext/config/selling.py,Default settings for selling transactions.,Standardindstillinger for at sælge transaktioner.
DocType: Timesheet,% Amount Billed,% Beløb Billed
DocType: Purchase Order,% Billed,% Billed
,Lead Id,Bly Id
apps/erpnext/erpnext/accounts/doctype/payment_order/payment_order.py,{0} {1} created,{0} {1} creado
apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html,'Total','Total'
DocType: Selling Settings,Selling Settings,Salg af indstillinger
apps/erpnext/erpnext/accounts/report/gross_profit/gross_profit.py,Selling Amount,Selling Beløb
DocType: Item Default,Default Selling Cost Center,Standard Selling Cost center
apps/erpnext/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py,90-Above,90-Above
DocType: Pricing Rule,Selling,Selling
DocType: Sales Order,% Delivered,% Leveres
DocType: Lead,Lead Owner,Bly Owner
apps/erpnext/erpnext/controllers/stock_controller.py,{0} {1}: Cost Center is mandatory for Item {2},{0} {1}: Udgiftområde er obligatorisk for varen {2}
apps/erpnext/erpnext/config/accounting.py,Tax template for selling transactions.,Skat skabelon til at sælge transaktioner.
apps/erpnext/erpnext/controllers/accounts_controller.py, or ,o
DocType: Sales Order,% of materials billed against this Sales Order,% Af materialer faktureret mod denne Sales Order
DocType: SMS Center,All Lead (Open),Alle Bly (Open)
apps/erpnext/erpnext/templates/includes/footer/footer_extension.html,Get Updates,Hent opdateringer
apps/erpnext/erpnext/controllers/sales_and_purchase_return.py,'Update Stock' can not be checked because items are not delivered via {0},"'Opdater lager' kan ikke markeres, varerne ikke leveres via {0}"
apps/erpnext/erpnext/patches/v4_0/create_price_list_if_missing.py,Standard Selling,Standard Selling
,Lead Details,Bly Detaljer
DocType: Selling Settings,Settings for Selling Module,Indstillinger for Selling modul
,Lead Name,Bly navn
DocType: Rename Tool,"Attach .csv file with two columns, one for the old name and one for the new name","Vedhæfte .csv fil med to kolonner, en for det gamle navn og et til det nye navn"
1 apps/erpnext/erpnext/stock/report/stock_ledger/stock_ledger.py 'Opening' 'Åbning'
1 apps/erpnext/erpnext/stock/report/stock_ledger/stock_ledger.py 'Opening' 'Åbning'
2 DocType: Email Campaign Lead Bly
3 apps/erpnext/erpnext/config/selling.py Default settings for selling transactions. Standardindstillinger for at sælge transaktioner.
4 DocType: Timesheet % Amount Billed % Beløb Billed
5 DocType: Purchase Order % Billed % Billed
6 Lead Id Bly Id
7 apps/erpnext/erpnext/accounts/doctype/payment_order/payment_order.py {0} {1} created {0} {1} creado
8 apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html 'Total' 'Total'
9 DocType: Selling Settings Selling Settings Salg af indstillinger
10 apps/erpnext/erpnext/accounts/report/gross_profit/gross_profit.py Selling Amount Selling Beløb
11 DocType: Item Default Default Selling Cost Center Standard Selling Cost center
12 apps/erpnext/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py 90-Above 90-Above
13 DocType: Pricing Rule Selling Selling
14 DocType: Sales Order % Delivered % Leveres
15 DocType: Lead Lead Owner Bly Owner
16 apps/erpnext/erpnext/controllers/stock_controller.py {0} {1}: Cost Center is mandatory for Item {2} {0} {1}: Udgiftområde er obligatorisk for varen {2}
17 apps/erpnext/erpnext/config/accounting.py Tax template for selling transactions. Skat skabelon til at sælge transaktioner.
18 apps/erpnext/erpnext/controllers/accounts_controller.py or o
19 DocType: Sales Order % of materials billed against this Sales Order % Af materialer faktureret mod denne Sales Order
20 DocType: SMS Center All Lead (Open) Alle Bly (Open)
21 apps/erpnext/erpnext/templates/includes/footer/footer_extension.html Get Updates Hent opdateringer
22 apps/erpnext/erpnext/controllers/sales_and_purchase_return.py 'Update Stock' can not be checked because items are not delivered via {0} 'Opdater lager' kan ikke markeres, varerne ikke leveres via {0}
23 apps/erpnext/erpnext/patches/v4_0/create_price_list_if_missing.py Standard Selling Standard Selling
24 Lead Details Bly Detaljer
25 DocType: Selling Settings Settings for Selling Module Indstillinger for Selling modul
26 Lead Name Bly navn
27 DocType: Rename Tool Attach .csv file with two columns, one for the old name and one for the new name Vedhæfte .csv fil med to kolonner, en for det gamle navn og et til det nye navn

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
DocType: Patient,Married,既婚
1 DocType: Patient Married 既婚
1 DocType: Patient Married 既婚

View File

@ -0,0 +1,2 @@
apps/erpnext/erpnext/controllers/accounts_controller.py, or ,uor
apps/erpnext/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py,90-Above,90-Above
1 apps/erpnext/erpnext/controllers/accounts_controller.py or uor
1 apps/erpnext/erpnext/controllers/accounts_controller.py or uor
2 apps/erpnext/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py 90-Above 90-Above

View File

@ -0,0 +1,46 @@
apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html,Cheques Required,Checks Required
apps/erpnext/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py,Row #{0}: Clearance date {1} cannot be before Cheque Date {2},Row #{0}: Clearance date {1} cannot be before Check Date {2}
apps/erpnext/erpnext/utilities/user_progress.py,People who teach at your organisation,People who teach at your organization
apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py,"Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}","Leave cannot be applied/canceled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}"
apps/erpnext/erpnext/support/doctype/warranty_claim/warranty_claim.py,Cancel Material Visit {0} before cancelling this Warranty Claim,Cancel Material Visit {0} before canceling this Warranty Claim
apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py,"Appointment cancelled, Please review and cancel the invoice {0}","Appointment canceled, Please review and cancel the invoice {0}"
apps/erpnext/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py,Outstanding Cheques and Deposits to clear,Outstanding Checks and Deposits to clear
apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py,Appointment cancelled,Appointment canceled
DocType: Payment Entry,Cheque/Reference Date,Check/Reference Date
DocType: Cheque Print Template,Scanned Cheque,Scanned Check
DocType: Cheque Print Template,Cheque Size,Check Size
apps/erpnext/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py,Maintenance Status has to be Cancelled or Completed to Submit,Maintenance Status has to be Canceled or Completed to Submit
apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py,'Entries' cannot be empty,'Entries' can not be empty
apps/erpnext/erpnext/setup/doctype/company/company.py,"Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency.","Cannot change company's default currency, because there are existing transactions. Transactions must be canceled to change the default currency."
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,Maintenance Visit {0} must be cancelled before cancelling this Sales Order,Maintenance Visit {0} must be canceled before cancelling this Sales Order
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,Sales Invoice {0} must be cancelled before cancelling this Sales Order,Sales Invoice {0} must be canceled before cancelling this Sales Order
DocType: Bank Reconciliation Detail,Cheque Date,Check Date
apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order.py,"Stopped Work Order cannot be cancelled, Unstop it first to cancel","Stopped Work Order cannot be canceled, Unstop it first to cancel"
apps/erpnext/erpnext/buying/doctype/purchase_order/purchase_order.py,Material Request {0} is cancelled or stopped,Material Request {0} is canceled or stopped
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,Closed order cannot be cancelled. Unclose to cancel.,Closed order cannot be canceled. Unclose to cancel.
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,Maintenance Schedule {0} must be cancelled before cancelling this Sales Order,Maintenance Schedule {0} must be canceled before cancelling this Sales Order
DocType: Accounts Settings,Unlink Payment on Cancellation of Invoice,Unlink Payment on Cancelation of Invoice
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,Delivery Notes {0} must be cancelled before cancelling this Sales Order,Delivery Notes {0} must be canceled before cancelling this Sales Order
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,Work Order {0} must be cancelled before cancelling this Sales Order,Work Order {0} must be canceled before cancelling this Sales Order
apps/erpnext/erpnext/config/accounting.py,Setup cheque dimensions for printing,Setup check dimensions for printing
apps/erpnext/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py,Cheques and Deposits incorrectly cleared,Checks and Deposits incorrectly cleared
apps/erpnext/erpnext/stock/doctype/material_request/material_request.py,{0} {1} is cancelled so the action cannot be completed,{0} {1} is canceled so the action cannot be completed
apps/erpnext/erpnext/stock/doctype/delivery_note/delivery_note.py,Packing Slip(s) cancelled,Packing Slip(s) canceled
DocType: Payment Entry,Cheque/Reference No,Check/Reference No
apps/erpnext/erpnext/assets/doctype/asset/asset.py,"Asset cannot be cancelled, as it is already {0}","Asset cannot be canceled, as it is already {0}"
DocType: Bank Reconciliation,Select account head of the bank where cheque was deposited.,Select account head of the bank where check was deposited.
DocType: Cheque Print Template,Cheque Print Template,Check Print Template
apps/erpnext/erpnext/controllers/selling_controller.py,{0} {1} is cancelled or closed,{0} {1} is canceled or closed
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,Quotation {0} is cancelled,Quotation {0} is canceled
apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py,Timesheet {0} is already completed or cancelled,Timesheet {0} is already completed or canceled
apps/erpnext/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py,Payment Cancelled. Please check your GoCardless Account for more details,Payment Canceled. Please check your GoCardless Account for more details
apps/erpnext/erpnext/stock/doctype/item/item.py,Item {0} is cancelled,Item {0} is canceled
DocType: Serial No,Is Cancelled,Is Canceled
apps/erpnext/erpnext/stock/doctype/material_request/material_request.py,{0} {1} is cancelled or stopped,{0} {1} is canceled or stopped
apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py,Colour,Color
DocType: Bank Reconciliation Detail,Cheque Number,Check Number
apps/erpnext/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py,Cancel Material Visits {0} before cancelling this Maintenance Visit,Cancel Material Visits {0} before canceling this Maintenance Visit
DocType: Employee,Cheque,Check
DocType: Cheque Print Template,Cheque Height,Check Height
DocType: Cheque Print Template,Cheque Width,Check Width
apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py,Wire Transfer,Wire Transfer
1 apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html Cheques Required Checks Required
1 apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html Cheques Required Checks Required
2 apps/erpnext/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py Row #{0}: Clearance date {1} cannot be before Cheque Date {2} Row #{0}: Clearance date {1} cannot be before Check Date {2}
3 apps/erpnext/erpnext/utilities/user_progress.py People who teach at your organisation People who teach at your organization
4 apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1} Leave cannot be applied/canceled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}
5 apps/erpnext/erpnext/support/doctype/warranty_claim/warranty_claim.py Cancel Material Visit {0} before cancelling this Warranty Claim Cancel Material Visit {0} before canceling this Warranty Claim
6 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py Appointment cancelled, Please review and cancel the invoice {0} Appointment canceled, Please review and cancel the invoice {0}
7 apps/erpnext/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py Outstanding Cheques and Deposits to clear Outstanding Checks and Deposits to clear
8 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py Appointment cancelled Appointment canceled
9 DocType: Payment Entry Cheque/Reference Date Check/Reference Date
10 DocType: Cheque Print Template Scanned Cheque Scanned Check
11 DocType: Cheque Print Template Cheque Size Check Size
12 apps/erpnext/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py Maintenance Status has to be Cancelled or Completed to Submit Maintenance Status has to be Canceled or Completed to Submit
13 apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py 'Entries' cannot be empty 'Entries' can not be empty
14 apps/erpnext/erpnext/setup/doctype/company/company.py Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency. Cannot change company's default currency, because there are existing transactions. Transactions must be canceled to change the default currency.
15 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py Maintenance Visit {0} must be cancelled before cancelling this Sales Order Maintenance Visit {0} must be canceled before cancelling this Sales Order
16 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py Sales Invoice {0} must be cancelled before cancelling this Sales Order Sales Invoice {0} must be canceled before cancelling this Sales Order
17 DocType: Bank Reconciliation Detail Cheque Date Check Date
18 apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order.py Stopped Work Order cannot be cancelled, Unstop it first to cancel Stopped Work Order cannot be canceled, Unstop it first to cancel
19 apps/erpnext/erpnext/buying/doctype/purchase_order/purchase_order.py Material Request {0} is cancelled or stopped Material Request {0} is canceled or stopped
20 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py Closed order cannot be cancelled. Unclose to cancel. Closed order cannot be canceled. Unclose to cancel.
21 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py Maintenance Schedule {0} must be cancelled before cancelling this Sales Order Maintenance Schedule {0} must be canceled before cancelling this Sales Order
22 DocType: Accounts Settings Unlink Payment on Cancellation of Invoice Unlink Payment on Cancelation of Invoice
23 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py Delivery Notes {0} must be cancelled before cancelling this Sales Order Delivery Notes {0} must be canceled before cancelling this Sales Order
24 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py Work Order {0} must be cancelled before cancelling this Sales Order Work Order {0} must be canceled before cancelling this Sales Order
25 apps/erpnext/erpnext/config/accounting.py Setup cheque dimensions for printing Setup check dimensions for printing
26 apps/erpnext/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py Cheques and Deposits incorrectly cleared Checks and Deposits incorrectly cleared
27 apps/erpnext/erpnext/stock/doctype/material_request/material_request.py {0} {1} is cancelled so the action cannot be completed {0} {1} is canceled so the action cannot be completed
28 apps/erpnext/erpnext/stock/doctype/delivery_note/delivery_note.py Packing Slip(s) cancelled Packing Slip(s) canceled
29 DocType: Payment Entry Cheque/Reference No Check/Reference No
30 apps/erpnext/erpnext/assets/doctype/asset/asset.py Asset cannot be cancelled, as it is already {0} Asset cannot be canceled, as it is already {0}
31 DocType: Bank Reconciliation Select account head of the bank where cheque was deposited. Select account head of the bank where check was deposited.
32 DocType: Cheque Print Template Cheque Print Template Check Print Template
33 apps/erpnext/erpnext/controllers/selling_controller.py {0} {1} is cancelled or closed {0} {1} is canceled or closed
34 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py Quotation {0} is cancelled Quotation {0} is canceled
35 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py Timesheet {0} is already completed or cancelled Timesheet {0} is already completed or canceled
36 apps/erpnext/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py Payment Cancelled. Please check your GoCardless Account for more details Payment Canceled. Please check your GoCardless Account for more details
37 apps/erpnext/erpnext/stock/doctype/item/item.py Item {0} is cancelled Item {0} is canceled
38 DocType: Serial No Is Cancelled Is Canceled
39 apps/erpnext/erpnext/stock/doctype/material_request/material_request.py {0} {1} is cancelled or stopped {0} {1} is canceled or stopped
40 apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py Colour Color
41 DocType: Bank Reconciliation Detail Cheque Number Check Number
42 apps/erpnext/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py Cancel Material Visits {0} before cancelling this Maintenance Visit Cancel Material Visits {0} before canceling this Maintenance Visit
43 DocType: Employee Cheque Check
44 DocType: Cheque Print Template Cheque Height Check Height
45 DocType: Cheque Print Template Cheque Width Check Width
46 apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py Wire Transfer Wire Transfer

File diff suppressed because it is too large Load Diff

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