Merge develop into contacts-ref

This commit is contained in:
Himanshu Warekar 2019-08-21 13:19:22 +05:30
commit ea97a6cb5b
156 changed files with 485686 additions and 464390 deletions

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

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

@ -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",

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,83 @@
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
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):
# strip 0 from the start of the number for proper number comparisions
# eg. 07888383332 should match with 7888383332
number = self.get('from').lstrip('0')
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):
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.lstrip('0'))],
'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 hoooks on creation of Lead or Contact'''
if doc.doctype not in ['Lead', 'Contact']: return
numbers = [doc.get('phone'), doc.get('mobile_no')]
for_doc = doc.doctype.lower()
for number in numbers:
if not number: continue
print(number)
filters = frappe._dict({
'from': ['like', '%{}'.format(number.lstrip('0'))],
for_doc: ''
})
logs = frappe.get_all('Call Log', filters=filters)
for log in logs:
call_log = frappe.get_doc('Call Log', log.name)
call_log.set(for_doc, doc.name)
call_log.save(ignore_permissions=True)

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

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

@ -4,82 +4,57 @@ import json
from frappe.contacts.doctype.contact.contact import get_contact_with_phone_number
@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)]
}
contact = get_contact_with_phone_number(number)
def get_last_interaction(contact=None, lead=None):
if not contact and not lead: return
last_communication = None
last_issue = None
if contact:
return frappe.get_doc('Contact', contact)
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 = ''
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)
def get_employee_emails_for_popup(communication_medium):
return issues[0] if issues else None
def get_scheduled_employees_for_popup(communication_medium):
now_time = frappe.utils.nowtime()
weekday = frappe.utils.get_weekday()

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

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

View File

@ -0,0 +1,5 @@
DocType: Fee Structure,Components,Componentes
apps/erpnext/erpnext/hr/doctype/attendance/attendance.py,Employee {0} on Half day on {1},"Empleado {0}, media jornada el día {1}"
DocType: Purchase Invoice Item,Item,Producto
DocType: Payment Entry,Deductions or Loss,Deducciones o Pérdidas
DocType: Cheque Print Template,Cheque Size,Tamaño de Cheque
1 DocType: Fee Structure Components Componentes
1 DocType: Fee Structure Components Componentes
2 apps/erpnext/erpnext/hr/doctype/attendance/attendance.py Employee {0} on Half day on {1} Empleado {0}, media jornada el día {1}
3 DocType: Purchase Invoice Item Item Producto
4 DocType: Payment Entry Deductions or Loss Deducciones o Pérdidas
5 DocType: Cheque Print Template Cheque Size Tamaño de Cheque

View File

@ -0,0 +1,32 @@
DocType: Assessment Plan,Grading Scale,Escala de Calificación
apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py,Guardian1 Mobile No,Número de Móvil de Guardián 1
apps/erpnext/erpnext/accounts/report/profitability_analysis/profitability_analysis.py,Gross Profit / Loss,Ganancia / Pérdida Bruta
apps/erpnext/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py,Cheques and Deposits incorrectly cleared,Los cheques y depósitos resueltos de forma incorrecta
DocType: Assessment Group,Parent Assessment Group,Grupo de Evaluación Padre
DocType: Student,Guardians,Guardianes
DocType: Fee Schedule,Fee Schedule,Programa de Tarifas
apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js,Get Items from Product Bundle,Obtener Ítems de Paquete de Productos
apps/erpnext/erpnext/stock/doctype/material_request/material_request.js,BOM does not contain any stock item,BOM no contiene ningún ítem de stock
DocType: Homepage,Company Tagline for website homepage,Lema de la empresa para la página de inicio del sitio web
DocType: Delivery Note,% Installed,% Instalado
DocType: Student,Guardian Details,Detalles del Guardián
apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py,Guardian1 Name,Nombre de Guardián 1
DocType: Grading Scale Interval,Grade Code,Grado de Código
DocType: Fee Schedule,Fee Structure,Estructura de Tarifas
DocType: Purchase Order,Get Items from Open Material Requests,Obtener Ítems de Solicitudes Abiertas de Materiales
,Batch Item Expiry Status,Estatus de Expiración de Lote de Ítems
DocType: Guardian,Guardian Interests,Intereses del Guardián
DocType: Guardian,Guardian Name,Nombre del Guardián
apps/erpnext/erpnext/selling/doctype/product_bundle/product_bundle.py,Child Item should not be a Product Bundle. Please remove item `{0}` and save,Artículo hijo no debe ser un paquete de productos. Por favor remover el artículo `` {0} y guardar
DocType: BOM Scrap Item,Basic Amount (Company Currency),Monto Base (Divisa de Compañía)
DocType: Grading Scale,Grading Scale Name,Nombre de Escala de Calificación
apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py,Guardian2 Mobile No,Número de Móvil de Guardián 2
apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py,Guardian2 Name,Nombre de Guardián 2
DocType: Stock Entry,Customer or Supplier Details,Detalle de cliente o proveedor
DocType: Course Scheduling Tool,Course Scheduling Tool,Herramienta de Programación de cursos
DocType: Shopping Cart Settings,Checkout Settings,Ajustes de Finalización de Pedido
DocType: Guardian Interest,Guardian Interest,Interés del Guardián
apps/erpnext/erpnext/utilities/user_progress.py,Classrooms/ Laboratories etc where lectures can be scheduled.,"Aulas / laboratorios, etc., donde las lecturas se pueden programar."
apps/erpnext/erpnext/templates/includes/cart/cart_dropdown.html,Checkout,Finalizando pedido
DocType: Guardian Student,Guardian Student,Guardián del Estudiante
DocType: BOM Operation,Base Hour Rate(Company Currency),Tarifa Base por Hora (Divisa de Compañía)
1 DocType: Assessment Plan Grading Scale Escala de Calificación
1 DocType: Assessment Plan Grading Scale Escala de Calificación
2 apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py Guardian1 Mobile No Número de Móvil de Guardián 1
3 apps/erpnext/erpnext/accounts/report/profitability_analysis/profitability_analysis.py Gross Profit / Loss Ganancia / Pérdida Bruta
4 apps/erpnext/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py Cheques and Deposits incorrectly cleared Los cheques y depósitos resueltos de forma incorrecta
5 DocType: Assessment Group Parent Assessment Group Grupo de Evaluación Padre
6 DocType: Student Guardians Guardianes
7 DocType: Fee Schedule Fee Schedule Programa de Tarifas
8 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js Get Items from Product Bundle Obtener Ítems de Paquete de Productos
9 apps/erpnext/erpnext/stock/doctype/material_request/material_request.js BOM does not contain any stock item BOM no contiene ningún ítem de stock
10 DocType: Homepage Company Tagline for website homepage Lema de la empresa para la página de inicio del sitio web
11 DocType: Delivery Note % Installed % Instalado
12 DocType: Student Guardian Details Detalles del Guardián
13 apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py Guardian1 Name Nombre de Guardián 1
14 DocType: Grading Scale Interval Grade Code Grado de Código
15 DocType: Fee Schedule Fee Structure Estructura de Tarifas
16 DocType: Purchase Order Get Items from Open Material Requests Obtener Ítems de Solicitudes Abiertas de Materiales
17 Batch Item Expiry Status Estatus de Expiración de Lote de Ítems
18 DocType: Guardian Guardian Interests Intereses del Guardián
19 DocType: Guardian Guardian Name Nombre del Guardián
20 apps/erpnext/erpnext/selling/doctype/product_bundle/product_bundle.py Child Item should not be a Product Bundle. Please remove item `{0}` and save Artículo hijo no debe ser un paquete de productos. Por favor remover el artículo `` {0} y guardar
21 DocType: BOM Scrap Item Basic Amount (Company Currency) Monto Base (Divisa de Compañía)
22 DocType: Grading Scale Grading Scale Name Nombre de Escala de Calificación
23 apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py Guardian2 Mobile No Número de Móvil de Guardián 2
24 apps/erpnext/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py Guardian2 Name Nombre de Guardián 2
25 DocType: Stock Entry Customer or Supplier Details Detalle de cliente o proveedor
26 DocType: Course Scheduling Tool Course Scheduling Tool Herramienta de Programación de cursos
27 DocType: Shopping Cart Settings Checkout Settings Ajustes de Finalización de Pedido
28 DocType: Guardian Interest Guardian Interest Interés del Guardián
29 apps/erpnext/erpnext/utilities/user_progress.py Classrooms/ Laboratories etc where lectures can be scheduled. Aulas / laboratorios, etc., donde las lecturas se pueden programar.
30 apps/erpnext/erpnext/templates/includes/cart/cart_dropdown.html Checkout Finalizando pedido
31 DocType: Guardian Student Guardian Student Guardián del Estudiante
32 DocType: BOM Operation Base Hour Rate(Company Currency) Tarifa Base por Hora (Divisa de Compañía)

View File

@ -0,0 +1,3 @@
apps/erpnext/erpnext/stock/doctype/item/item.py,Barcode {0} is not a valid {1} code,El código de barras {0} no es un código válido {1}
apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py,{0} on Half day Leave on {1},{0} Ausente medio día en {1}
apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py,{0} does not have a Healthcare Practitioner Schedule. Add it in Healthcare Practitioner master,{0} no tiene agenda del profesional médico . Añádelo al médico correspondiente.
1 apps/erpnext/erpnext/stock/doctype/item/item.py Barcode {0} is not a valid {1} code El código de barras {0} no es un código válido {1}
1 apps/erpnext/erpnext/stock/doctype/item/item.py Barcode {0} is not a valid {1} code El código de barras {0} no es un código válido {1}
2 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py {0} on Half day Leave on {1} {0} Ausente medio día en {1}
3 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py {0} does not have a Healthcare Practitioner Schedule. Add it in Healthcare Practitioner master {0} no tiene agenda del profesional médico . Añádelo al médico correspondiente.

View File

@ -0,0 +1,10 @@
DocType: Supplier,Block Supplier,Bloque de Proveedor
apps/erpnext/erpnext/hr/doctype/retention_bonus/retention_bonus.py,Cannot create Retention Bonus for left Employees,No se puede crear una bonificación de retención para los empleados que se han marchado
apps/erpnext/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py,Cancel the journal entry {0} first,Cancelar el ingreso diario {0} primero
apps/erpnext/erpnext/hr/doctype/employee_transfer/employee_transfer.py,Cannot transfer Employee with status Left,No se puede transferir Empleado con estado ah salido
DocType: Employee Benefit Claim,Benefit Type and Amount,Tipo de beneficio y monto
apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js,Block Invoice,Bloque de Factura
DocType: Item,Asset Naming Series,Series de Nombres de Activos
,BOM Variance Report,Informe de varianza BOM(Lista de Materiales)
apps/erpnext/erpnext/hr/doctype/employee_promotion/employee_promotion.py,Cannot promote Employee with status Left,No se puede promover Empleado con estado ha salido
apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js,Auto repeat document updated,Repetición automática del documento actualizado
1 DocType: Supplier Block Supplier Bloque de Proveedor
1 DocType: Supplier Block Supplier Bloque de Proveedor
2 apps/erpnext/erpnext/hr/doctype/retention_bonus/retention_bonus.py Cannot create Retention Bonus for left Employees No se puede crear una bonificación de retención para los empleados que se han marchado
3 apps/erpnext/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py Cancel the journal entry {0} first Cancelar el ingreso diario {0} primero
4 apps/erpnext/erpnext/hr/doctype/employee_transfer/employee_transfer.py Cannot transfer Employee with status Left No se puede transferir Empleado con estado ah salido
5 DocType: Employee Benefit Claim Benefit Type and Amount Tipo de beneficio y monto
6 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js Block Invoice Bloque de Factura
7 DocType: Item Asset Naming Series Series de Nombres de Activos
8 BOM Variance Report Informe de varianza BOM(Lista de Materiales)
9 apps/erpnext/erpnext/hr/doctype/employee_promotion/employee_promotion.py Cannot promote Employee with status Left No se puede promover Empleado con estado ha salido
10 apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js Auto repeat document updated Repetición automática del documento actualizado

View File

@ -0,0 +1,9 @@
DocType: Instructor Log,Other Details,Otros Detalles
apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py,{0} does not have a Healthcare Practitioner Schedule. Add it in Healthcare Practitioner master,{0} no tiene agenda del profesional médico . Asígnele al médico
DocType: Material Request Item,Lead Time Date,Fecha de la Iniciativa
apps/erpnext/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py,Lead Time Days,Tiempo de ejecución en días
apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py,{0} on Half day Leave on {1},{0} en Medio día Permiso en {1}
apps/erpnext/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py,Outstanding Amt,Saldo Pendiente
DocType: Bank Statement Transaction Invoice Item,Outstanding Amount,Saldo Pendiente
DocType: Payment Entry Reference,Outstanding,Pendiente
apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py,{0} on Leave on {1},{0} De permiso De permiso {1}
1 DocType: Instructor Log Other Details Otros Detalles
1 DocType: Instructor Log Other Details Otros Detalles
2 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py {0} does not have a Healthcare Practitioner Schedule. Add it in Healthcare Practitioner master {0} no tiene agenda del profesional médico . Asígnele al médico
3 DocType: Material Request Item Lead Time Date Fecha de la Iniciativa
4 apps/erpnext/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py Lead Time Days Tiempo de ejecución en días
5 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py {0} on Half day Leave on {1} {0} en Medio día Permiso en {1}
6 apps/erpnext/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py Outstanding Amt Saldo Pendiente
7 DocType: Bank Statement Transaction Invoice Item Outstanding Amount Saldo Pendiente
8 DocType: Payment Entry Reference Outstanding Pendiente
9 apps/erpnext/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py {0} on Leave on {1} {0} De permiso De permiso {1}

View File

@ -0,0 +1,87 @@
apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order.js,Please set the Item Code first,"Por favor, primero define el Código del Artículo"
DocType: Program Enrollment,School House,Casa Escuela
apps/erpnext/erpnext/hr/doctype/vehicle_log/vehicle_log.py,"Service Item,Type,frequency and expense amount are required","El Artículo de Servico, el Tipo, la Frecuencia y la Cantidad de Gasto son requeridos"
DocType: Delivery Note,% Installed,% Instalado
apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py,Only Leave Applications with status 'Approved' and 'Rejected' can be submitted,"Sólo Solicitudes de Permiso con estado ""Aprobado"" y ""Rechazado"" puede ser presentado"
DocType: Salary Structure,Leave Encashment Amount Per Day,Cantidad por día para pago por Ausencia
apps/erpnext/erpnext/stock/dashboard/item_dashboard.js,Source and target warehouse must be different,El almacén de origen y el de destino deben ser diferentes
DocType: Item,"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Ejemplo: ABCD.#####. Si se establece una serie y no se especifica un número de lote en las transacciones, se creará un número de lote automático basado en esta serie. Si quiere especificar el número de lote para este artículo en cada transacción, déjelo en blanco. Nota: esta configuración tendrá prioridad sobre el prefijo de la Serie en Configuración de Inventario."
apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.py,The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.,La cantidad de {0} establecida en esta solicitud de pago es diferente de la cantidad calculada para todos los planes de pago: {1}. Verifique que esto sea correcto antes de enviar el documento.
DocType: Lab Test Template,Standard Selling Rate,Tarifa de Venta Estándar
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,"Material Request not created, as quantity for Raw Materials already available.","La Requisición de Material no fue creada, debido a que hay suficiente materia prima disponible."
DocType: Subscription Plan,Payment Plan,Plan de pago
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.js,Select Items based on Delivery Date,Seleccionar Artículos según la fecha de entrega
apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,PO already created for all sales order items,Ya existe una Orden de Compra para todos los artículos de la Órden de Venta
DocType: Currency Exchange,Specify Exchange Rate to convert one currency into another,Especificar el Tipo de Cambio para convertir de una divisa a otra
DocType: Education Settings,"For Batch based Student Group, the Student Batch will be validated for every Student from the Program Enrollment.","Para grupo de estudiantes por lotes, el lote de estudiantes se validará para cada estudiante de la inscripción del programa."
DocType: Timesheet,Total Costing Amount,Monto Total Calculado
DocType: Education Settings,"For Course based Student Group, the Course will be validated for every Student from the enrolled Courses in Program Enrollment.","Para Grupo de Estudiantes por Curso, el Curso será validado para cada Estudiante de los Cursos inscritos en la Inscripción del Programa."
apps/erpnext/erpnext/hr/doctype/salary_structure/salary_structure.js,Show Salary Slip,Mostrar Recibo de Nómina
apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.py,The payment gateway account in plan {0} is different from the payment gateway account in this payment request,La cuenta para pasarela de pago en el plan {0} es diferente de la cuenta de pasarela de pago en en esta petición de pago
apps/erpnext/erpnext/selling/doctype/pos_closing_voucher/closing_voucher_details.html,Mode of Payments,Forma de pago
DocType: Loyalty Point Entry,Loyalty Point Entry,Entrada de Punto de Lealtad
DocType: Leave Policy Detail,Leave Policy Detail,Detalles de política de Licencia
apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py,Please enter Account for Change Amount,"Por favor, introduzca la Vuenta para el Cambio Monto"
DocType: Purchase Taxes and Charges Template,"Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like ""Shipping"", ""Insurance"", ""Handling"" etc.
#### Note
The tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.
#### Description of Columns
1. Calculation Type:
- This can be on **Net Total** (that is the sum of basic amount).
- **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.
- **Actual** (as mentioned).
2. Account Head: The Account ledger under which this tax will be booked
3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.
4. Description: Description of the tax (that will be printed in invoices / quotes).
5. Rate: Tax rate.
6. Amount: Tax amount.
7. Total: Cumulative total to this point.
8. Enter Row: If based on ""Previous Row Total"" you can select the row number which will be taken as a base for this calculation (default is the previous row).
9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.
10. Add or Deduct: Whether you want to add or deduct the tax.","Plantilla de impuestos que puede aplicarse a todas las operaciones de compra. Esta plantilla puede contener un listado de cuentas de impuestos así como también de otras cuentas de gastos como ""Envío"", ""Seguros"", ""Manejo"", etc.
#### Nota
La tasa impositiva que se defina aquí será la tasa de gravamen predeterminada para todos los **Productos**. Si existen **Productos** con diferentes tasas, estas deben ser añadidas a la tabla de **Impuestos del Producto ** dentro del maestro del **Producto**.
#### Descripción de las Columnas
1. Tipo de Cálculo:
- Este puede ser **Sobre el total neto** (que es la suma de la cantidad básica).
- **Sobre la línea anterior total / importe** (para impuestos o cargos acumulados). Si selecciona esta opción, el impuesto se aplica como un porcentaje de la cantidad o total de la fila anterior (de la tabla de impuestos).
- **Actual** (como se haya capturado).
2. Encabezado de cuenta: La cuenta mayor sobre la que se registrara este gravamen.
3. Centro de Costo: Si el impuesto / cargo es un ingreso (como en un envío) o un gasto, debe ser registrado contra un centro de costos.
4. Descripción: Descripción del impuesto (que se imprimirán en facturas / cotizaciones).
5. Rate: Tasa de impuesto.
6. Monto: Monto de impuesto.
7. Total: Total acumulado hasta este punto.
8. Línea de referencia: Si se basa en ""Línea anterior al total"" se puede seleccionar el número de la fila que será tomado como base para este cálculo (por defecto es la fila anterior).
9. Considerar impuesto o cargo para: En esta sección se puede especificar si el impuesto / cargo es sólo para la valoración (no una parte del total) o sólo para el total (no agrega valor al elemento) o para ambos.
10. Añadir o deducir: Si usted quiere añadir o deducir el impuesto."
DocType: Company,Gain/Loss Account on Asset Disposal,Cuenta de ganancia/pérdida en la disposición de activos
apps/erpnext/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js,Stock quantity to start procedure is not available in the warehouse. Do you want to record a Stock Transfer,La cantidad de existencias para comenzar el procedimiento no está disponible en el almacén. ¿Desea registrar una transferencia de inventario?
apps/erpnext/erpnext/stock/report/total_stock_summary/total_stock_summary.py,Please set Company filter blank if Group By is 'Company',"Por favor, establezca el filtro de Compañía en blanco si Agrupar Por es 'Compañía'"
,Support Hour Distribution,Distribución de Hora de Soporte
apps/erpnext/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py,Student Group Strength,Fortaleza de Grupo Estudiante
DocType: Leave Encashment,Leave Encashment,Cobro de Permiso
DocType: Student Group Student,Student Group Student,Alumno de Grupo de Estudiantes
apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py,Gain/Loss on Asset Disposal,Ganancia/Pérdida por la venta de activos
apps/erpnext/erpnext/education/doctype/assessment_result/assessment_result.js,Score cannot be greater than Maximum Score,Los resultados no puede ser mayor que la Puntuación Máxima
apps/erpnext/erpnext/stock/doctype/item/item.py,Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.,No se pueden cambiar las propiedades de Variantes después de la transacción de inventario. Deberá crear un nuevo artículo para hacer esto.
apps/erpnext/erpnext/hr/doctype/leave_allocation/leave_allocation.py,Leave Type {0} cannot be carry-forwarded,Tipo de Permiso {0} no se puede arrastar o trasladar
apps/erpnext/erpnext/hr/doctype/expense_claim/expense_claim.py,Please set default account in Expense Claim Type {0},"Por favor, establezca la Cuenta predeterminada en el Tipo de Reembolso de Gastos {0}"
DocType: Leave Policy,Leave Policy Details,Detalles de Política de Licencia
apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.js,"Company, Payment Account, From Date and To Date is mandatory","Empresa, cuenta de pago, fecha de inicio y fecha final son obligatorios"
DocType: Loyalty Point Entry,Loyalty Program,Programa de Lealtad
DocType: Stock Entry,Customer or Supplier Details,Detalle de cliente o proveedor
apps/erpnext/erpnext/assets/doctype/asset/asset.py,Gross Purchase Amount is mandatory,El Importe Bruto de Compra es obligatorio
apps/erpnext/erpnext/hr/doctype/leave_allocation/leave_allocation.py,Leave Type {0} cannot be allocated since it is leave without pay,Tipo de Permiso {0} no puede ser asignado ya que es un Permiso sin paga
apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py,Leave Without Pay does not match with approved Leave Application records,Permiso sin sueldo no coincide con los registros de Solicitud de Permiso aprobadas
DocType: Item,Asset Naming Series,Numeración para Activos
apps/erpnext/erpnext/assets/doctype/asset/depreciation.py,Please set 'Gain/Loss Account on Asset Disposal' in Company {0},Por favor defina la 'Cuenta de Ganacia/Pérdida por Ventas de Activos' en la empresa {0}
apps/erpnext/erpnext/accounts/report/trial_balance/trial_balance.js,Show unclosed fiscal year's P&L balances,Mostrar saldos de Ganancias y Perdidas de año fiscal sin cerrar
1 apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order.js Please set the Item Code first Por favor, primero define el Código del Artículo
1 apps/erpnext/erpnext/manufacturing/doctype/work_order/work_order.js Please set the Item Code first Por favor, primero define el Código del Artículo
2 DocType: Program Enrollment School House Casa Escuela
3 apps/erpnext/erpnext/hr/doctype/vehicle_log/vehicle_log.py Service Item,Type,frequency and expense amount are required El Artículo de Servico, el Tipo, la Frecuencia y la Cantidad de Gasto son requeridos
4 DocType: Delivery Note % Installed % Instalado
5 apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py Only Leave Applications with status 'Approved' and 'Rejected' can be submitted Sólo Solicitudes de Permiso con estado "Aprobado" y "Rechazado" puede ser presentado
6 DocType: Salary Structure Leave Encashment Amount Per Day Cantidad por día para pago por Ausencia
7 apps/erpnext/erpnext/stock/dashboard/item_dashboard.js Source and target warehouse must be different El almacén de origen y el de destino deben ser diferentes
8 DocType: Item Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings. Ejemplo: ABCD.#####. Si se establece una serie y no se especifica un número de lote en las transacciones, se creará un número de lote automático basado en esta serie. Si quiere especificar el número de lote para este artículo en cada transacción, déjelo en blanco. Nota: esta configuración tendrá prioridad sobre el prefijo de la Serie en Configuración de Inventario.
9 apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.py The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document. La cantidad de {0} establecida en esta solicitud de pago es diferente de la cantidad calculada para todos los planes de pago: {1}. Verifique que esto sea correcto antes de enviar el documento.
10 DocType: Lab Test Template Standard Selling Rate Tarifa de Venta Estándar
11 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py Material Request not created, as quantity for Raw Materials already available. La Requisición de Material no fue creada, debido a que hay suficiente materia prima disponible.
12 DocType: Subscription Plan Payment Plan Plan de pago
13 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.js Select Items based on Delivery Date Seleccionar Artículos según la fecha de entrega
14 apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py PO already created for all sales order items Ya existe una Orden de Compra para todos los artículos de la Órden de Venta
15 DocType: Currency Exchange Specify Exchange Rate to convert one currency into another Especificar el Tipo de Cambio para convertir de una divisa a otra
16 DocType: Education Settings For Batch based Student Group, the Student Batch will be validated for every Student from the Program Enrollment. Para grupo de estudiantes por lotes, el lote de estudiantes se validará para cada estudiante de la inscripción del programa.
17 DocType: Timesheet Total Costing Amount Monto Total Calculado
18 DocType: Education Settings For Course based Student Group, the Course will be validated for every Student from the enrolled Courses in Program Enrollment. Para Grupo de Estudiantes por Curso, el Curso será validado para cada Estudiante de los Cursos inscritos en la Inscripción del Programa.
19 apps/erpnext/erpnext/hr/doctype/salary_structure/salary_structure.js Show Salary Slip Mostrar Recibo de Nómina
20 apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.py The payment gateway account in plan {0} is different from the payment gateway account in this payment request La cuenta para pasarela de pago en el plan {0} es diferente de la cuenta de pasarela de pago en en esta petición de pago
21 apps/erpnext/erpnext/selling/doctype/pos_closing_voucher/closing_voucher_details.html Mode of Payments Forma de pago
22 DocType: Loyalty Point Entry Loyalty Point Entry Entrada de Punto de Lealtad
23 DocType: Leave Policy Detail Leave Policy Detail Detalles de política de Licencia
24 apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py Please enter Account for Change Amount Por favor, introduzca la Vuenta para el Cambio Monto
25 DocType: Purchase Taxes and Charges Template Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like "Shipping", "Insurance", "Handling" etc. #### Note The tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master. #### Description of Columns 1. Calculation Type: - This can be on **Net Total** (that is the sum of basic amount). - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total. - **Actual** (as mentioned). 2. Account Head: The Account ledger under which this tax will be booked 3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center. 4. Description: Description of the tax (that will be printed in invoices / quotes). 5. Rate: Tax rate. 6. Amount: Tax amount. 7. Total: Cumulative total to this point. 8. Enter Row: If based on "Previous Row Total" you can select the row number which will be taken as a base for this calculation (default is the previous row). 9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both. 10. Add or Deduct: Whether you want to add or deduct the tax. Plantilla de impuestos que puede aplicarse a todas las operaciones de compra. Esta plantilla puede contener un listado de cuentas de impuestos así como también de otras cuentas de gastos como "Envío", "Seguros", "Manejo", etc. #### Nota La tasa impositiva que se defina aquí será la tasa de gravamen predeterminada para todos los **Productos**. Si existen **Productos** con diferentes tasas, estas deben ser añadidas a la tabla de **Impuestos del Producto ** dentro del maestro del **Producto**. #### Descripción de las Columnas 1. Tipo de Cálculo: - Este puede ser **Sobre el total neto** (que es la suma de la cantidad básica). - **Sobre la línea anterior total / importe** (para impuestos o cargos acumulados). Si selecciona esta opción, el impuesto se aplica como un porcentaje de la cantidad o total de la fila anterior (de la tabla de impuestos). - **Actual** (como se haya capturado). 2. Encabezado de cuenta: La cuenta mayor sobre la que se registrara este gravamen. 3. Centro de Costo: Si el impuesto / cargo es un ingreso (como en un envío) o un gasto, debe ser registrado contra un centro de costos. 4. Descripción: Descripción del impuesto (que se imprimirán en facturas / cotizaciones). 5. Rate: Tasa de impuesto. 6. Monto: Monto de impuesto. 7. Total: Total acumulado hasta este punto. 8. Línea de referencia: Si se basa en "Línea anterior al total" se puede seleccionar el número de la fila que será tomado como base para este cálculo (por defecto es la fila anterior). 9. Considerar impuesto o cargo para: En esta sección se puede especificar si el impuesto / cargo es sólo para la valoración (no una parte del total) o sólo para el total (no agrega valor al elemento) o para ambos. 10. Añadir o deducir: Si usted quiere añadir o deducir el impuesto.
26 DocType: Company Gain/Loss Account on Asset Disposal Cuenta de ganancia/pérdida en la disposición de activos
27 apps/erpnext/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js Stock quantity to start procedure is not available in the warehouse. Do you want to record a Stock Transfer La cantidad de existencias para comenzar el procedimiento no está disponible en el almacén. ¿Desea registrar una transferencia de inventario?
28 apps/erpnext/erpnext/stock/report/total_stock_summary/total_stock_summary.py Please set Company filter blank if Group By is 'Company' Por favor, establezca el filtro de Compañía en blanco si Agrupar Por es 'Compañía'
29 Support Hour Distribution Distribución de Hora de Soporte
30 apps/erpnext/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py Student Group Strength Fortaleza de Grupo Estudiante
31 DocType: Leave Encashment Leave Encashment Cobro de Permiso
32 DocType: Student Group Student Student Group Student Alumno de Grupo de Estudiantes
33 apps/erpnext/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py Gain/Loss on Asset Disposal Ganancia/Pérdida por la venta de activos
34 apps/erpnext/erpnext/education/doctype/assessment_result/assessment_result.js Score cannot be greater than Maximum Score Los resultados no puede ser mayor que la Puntuación Máxima
35 apps/erpnext/erpnext/stock/doctype/item/item.py Cannot change Variant properties after stock transaction. You will have to make a new Item to do this. No se pueden cambiar las propiedades de Variantes después de la transacción de inventario. Deberá crear un nuevo artículo para hacer esto.
36 apps/erpnext/erpnext/hr/doctype/leave_allocation/leave_allocation.py Leave Type {0} cannot be carry-forwarded Tipo de Permiso {0} no se puede arrastar o trasladar
37 apps/erpnext/erpnext/hr/doctype/expense_claim/expense_claim.py Please set default account in Expense Claim Type {0} Por favor, establezca la Cuenta predeterminada en el Tipo de Reembolso de Gastos {0}
38 DocType: Leave Policy Leave Policy Details Detalles de Política de Licencia
39 apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.js Company, Payment Account, From Date and To Date is mandatory Empresa, cuenta de pago, fecha de inicio y fecha final son obligatorios
40 DocType: Loyalty Point Entry Loyalty Program Programa de Lealtad
41 DocType: Stock Entry Customer or Supplier Details Detalle de cliente o proveedor
42 apps/erpnext/erpnext/assets/doctype/asset/asset.py Gross Purchase Amount is mandatory El Importe Bruto de Compra es obligatorio
43 apps/erpnext/erpnext/hr/doctype/leave_allocation/leave_allocation.py Leave Type {0} cannot be allocated since it is leave without pay Tipo de Permiso {0} no puede ser asignado ya que es un Permiso sin paga
44 apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py Leave Without Pay does not match with approved Leave Application records Permiso sin sueldo no coincide con los registros de Solicitud de Permiso aprobadas
45 DocType: Item Asset Naming Series Numeración para Activos
46 apps/erpnext/erpnext/assets/doctype/asset/depreciation.py Please set 'Gain/Loss Account on Asset Disposal' in Company {0} Por favor defina la 'Cuenta de Ganacia/Pérdida por Ventas de Activos' en la empresa {0}
47 apps/erpnext/erpnext/accounts/report/trial_balance/trial_balance.js Show unclosed fiscal year's P&L balances Mostrar saldos de Ganancias y Perdidas de año fiscal sin cerrar
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

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