Merge branch 'develop' into navbar_links

This commit is contained in:
Raffael Meyer 2020-08-19 18:55:44 +02:00 committed by GitHub
commit 585f1f67b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 964 additions and 706 deletions

View File

@ -27,4 +27,4 @@ def get_vouchar_detials(column_list, doctype, docname):
for col in column_list:
sanitize_searchfield(col)
return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s'''
.format(columns=", ".join(json.loads(column_list)), doctype=doctype), docname, as_dict=1)[0]
.format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0]

View File

@ -135,7 +135,7 @@ var create_import_button = function(frm) {
callback: function(r) {
if(!r.exc) {
clearInterval(frm.page["interval"]);
frm.page.set_indicator(__('Import Successfull'), 'blue');
frm.page.set_indicator(__('Import Successful'), 'blue');
create_reset_button(frm);
}
}

View File

@ -13,7 +13,7 @@ def get_data():
},
{
'label': _('References'),
'items': ['Period Closing Voucher', 'Request for Quotation', 'Tax Withholding Category']
'items': ['Period Closing Voucher', 'Tax Withholding Category']
},
{
'label': _('Target Details'),

View File

@ -21,7 +21,7 @@ from six import iteritems
class POSInvoice(SalesInvoice):
def __init__(self, *args, **kwargs):
super(POSInvoice, self).__init__(*args, **kwargs)
def validate(self):
if not cint(self.is_pos):
frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment")))
@ -58,7 +58,7 @@ class POSInvoice(SalesInvoice):
if self.redeem_loyalty_points and self.loyalty_points:
self.apply_loyalty_points()
self.set_status(update=True)
def on_cancel(self):
# run on cancel method of selling controller
super(SalesInvoice, self).on_cancel()
@ -68,10 +68,10 @@ class POSInvoice(SalesInvoice):
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
against_psi_doc.delete_loyalty_point_entry()
against_psi_doc.make_loyalty_point_entry()
def validate_stock_availablility(self):
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
for d in self.get('items'):
if d.serial_no:
filters = {
@ -89,11 +89,11 @@ class POSInvoice(SalesInvoice):
for s in serial_nos:
if s in reserved_serial_nos:
invalid_serial_nos.append(s)
if len(invalid_serial_nos):
multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \
Please select valid serial no.".format(d.idx, multiple_nos,
Please select valid serial no.".format(d.idx, multiple_nos,
frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available"))
else:
if allow_negative_stock:
@ -105,9 +105,9 @@ class POSInvoice(SalesInvoice):
.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available"))
elif flt(available_stock) < flt(d.qty):
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \
Available quantity {}.'.format(d.idx, frappe.bold(d.item_code),
Available quantity {}.'.format(d.idx, frappe.bold(d.item_code),
frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available"))
def validate_serialised_or_batched_item(self):
for d in self.get("items"):
serialized = d.get("has_serial_no")
@ -125,7 +125,7 @@ class POSInvoice(SalesInvoice):
if batched and no_batch_selected:
frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.'
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
def validate_return_items(self):
if not self.get("is_return"): return
@ -158,7 +158,7 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
if self.is_return and entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
def validate_pos_return(self):
if self.is_pos and self.is_return:
total_amount_in_payments = 0
@ -167,12 +167,12 @@ class POSInvoice(SalesInvoice):
invoice_total = self.rounded_total or self.grand_total
if total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total)))
def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"])
if not self.loyalty_redemption_account:
self.loyalty_redemption_account = expense_account
self.loyalty_redemption_account = expense_account
if not self.loyalty_redemption_cost_center:
self.loyalty_redemption_cost_center = cost_center
@ -212,7 +212,7 @@ class POSInvoice(SalesInvoice):
if update:
self.db_set('status', self.status, update_modified = update_modified)
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
@ -315,25 +315,25 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
latest_sle = frappe.db.sql("""select qty_after_transaction
from `tabStock Ledger Entry`
latest_sle = frappe.db.sql("""select qty_after_transaction
from `tabStock Ledger Entry`
where item_code = %s and warehouse = %s
order by posting_date desc, posting_time desc
limit 1""", (item_code, warehouse), as_dict=1)
pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent
and p.consolidated_invoice is NULL
where p.name = p_item.parent
and p.consolidated_invoice is NULL
and p.docstatus = 1
and p_item.docstatus = 1
and p_item.item_code = %s
and p_item.warehouse = %s
""", (item_code, warehouse), as_dict=1)
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty:
return sle_qty - pos_sales_qty
else:
@ -360,14 +360,14 @@ def make_merge_log(invoices):
merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = getdate(nowdate())
for inv in invoices:
inv_data = frappe.db.get_values("POS Invoice", inv.get('name'),
inv_data = frappe.db.get_values("POS Invoice", inv.get('name'),
["customer", "posting_date", "grand_total"], as_dict=1)[0]
merge_log.customer = inv_data.customer
merge_log.append("pos_invoices", {
'pos_invoice': inv.get('name'),
'customer': inv_data.customer,
'posting_date': inv_data.posting_date,
'grand_total': inv_data.grand_total
'grand_total': inv_data.grand_total
})
if merge_log.get('pos_invoices'):

View File

@ -302,10 +302,10 @@
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"mandatory_depends_on": "update_stock",
"oldfieldname": "warehouse",
"oldfieldtype": "Link",
"options": "Warehouse",
"reqd": 1
"options": "Warehouse"
},
{
"default": "0",
@ -350,4 +350,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

@ -1619,22 +1619,23 @@ def update_multi_mode_option(doc, pos_profile):
for pos_payment_method in pos_profile.get('payments'):
pos_payment_method = pos_payment_method.as_dict()
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
payment_mode[0].default = pos_payment_method.default
append_payment(payment_mode[0])
if payment_mode:
payment_mode[0].default = pos_payment_method.default
append_payment(payment_mode[0])
def get_all_mode_of_payments(doc):
return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
select mpa.default_account, mpa.parent, mp.type as type
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
{'company': doc.company}, as_dict=1)
def get_mode_of_payment_info(mode_of_payment, company):
return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
select mpa.default_account, mpa.parent, mp.type as type
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
(company, mode_of_payment), as_dict=1)

View File

@ -14,7 +14,7 @@ import frappe, erpnext
from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency
from erpnext.accounts.utils import get_fiscal_year
from frappe import _
from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr)
from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr, cint)
from six import itervalues
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children
@ -46,7 +46,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_
start_date = year_start_date
months = get_months(year_start_date, year_end_date)
for i in range(math.ceil(months / months_to_add)):
for i in range(cint(math.ceil(months / months_to_add))):
period = frappe._dict({
"from_date": start_date
})

View File

@ -11,7 +11,7 @@ from erpnext.assets.doctype.asset_maintenance.asset_maintenance import calculate
class AssetMaintenanceLog(Document):
def validate(self):
if getdate(self.due_date) < getdate(nowdate()):
if getdate(self.due_date) < getdate(nowdate()) and self.maintenance_status not in ["Completed", "Cancelled"]:
self.maintenance_status = "Overdue"
if self.maintenance_status == "Completed" and not self.completion_date:

View File

@ -1,14 +1,15 @@
frappe.listview_settings['Asset Maintenance Log'] = {
add_fields: ["maintenance_status"],
has_indicator_for_draft: 1,
get_indicator: function(doc) {
if(doc.maintenance_status=="Pending") {
return [__("Pending"), "orange"];
} else if(doc.maintenance_status=="Completed") {
return [__("Completed"), "green"];
} else if(doc.maintenance_status=="Cancelled") {
return [__("Cancelled"), "red"];
} else if(doc.maintenance_status=="Overdue") {
return [__("Overdue"), "red"];
if (doc.maintenance_status=="Planned") {
return [__(doc.maintenance_status), "orange", "status,=," + doc.maintenance_status];
} else if (doc.maintenance_status=="Completed") {
return [__(doc.maintenance_status), "green", "status,=," + doc.maintenance_status];
} else if (doc.maintenance_status=="Cancelled") {
return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status];
} else if (doc.maintenance_status=="Overdue") {
return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status];
}
}
};

View File

@ -173,7 +173,7 @@ class calculate_taxes_and_totals(object):
+ tax.tax_fraction_for_current_item
cumulated_tax_fraction += tax.tax_fraction_for_current_item
total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.stock_qty)
total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.qty)
if not self.discount_amount_applied and item.qty and (cumulated_tax_fraction or total_inclusive_tax_amount_per_qty):
amount = flt(item.amount) - total_inclusive_tax_amount_per_qty

View File

@ -411,7 +411,7 @@
"fieldname": "lost_reasons",
"fieldtype": "Table MultiSelect",
"label": "Lost Reasons",
"options": "Lost Reason Detail",
"options": "Opportunity Lost Reason Detail",
"read_only": 1
},
{

View File

@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2020-07-16 16:11:39.830389",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"lost_reason"
],
"fields": [
{
"fieldname": "lost_reason",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Opportunity Lost Reason",
"options": "Opportunity Lost Reason"
}
],
"istable": 1,
"links": [],
"modified": "2020-07-26 17:58:26.313242",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity Lost Reason Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class OpportunityLostReasonDetail(Document):
pass

View File

@ -17,10 +17,10 @@
"payroll_cost_center",
"column_break_9",
"leave_block_list",
"leave_section",
"approvers",
"leave_approvers",
"expense_section",
"expense_approvers",
"shift_request_approver",
"lft",
"rgt",
"old_parent"
@ -33,14 +33,18 @@
"label": "Department",
"oldfieldname": "department_name",
"oldfieldtype": "Data",
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "parent_department",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Parent Department",
"options": "Department"
"options": "Department",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "company",
@ -48,7 +52,9 @@
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"bold": 1,
@ -56,17 +62,23 @@
"fieldname": "is_group",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Group"
"label": "Is Group",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
"label": "Disabled",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
},
{
"description": "Days for which Holidays are blocked for this department.",
@ -74,31 +86,25 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Leave Block List",
"options": "Leave Block List"
"options": "Leave Block List",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "leave_section",
"fieldtype": "Section Break",
"label": "Leave Approvers"
},
{
"description": "The first Leave Approver in the list will be set as the default Leave Approver.",
"fieldname": "leave_approvers",
"fieldtype": "Table",
"label": "Leave Approver",
"options": "Department Approver"
"options": "Department Approver",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "expense_section",
"fieldtype": "Section Break",
"label": "Expense Approvers"
},
{
"description": "The first Expense Approver in the list will be set as the default Expense Approver.",
"fieldname": "expense_approvers",
"fieldtype": "Table",
"label": "Expense Approver",
"options": "Department Approver"
"options": "Department Approver",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "lft",
@ -106,7 +112,9 @@
"hidden": 1,
"label": "lft",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "rgt",
@ -114,7 +122,9 @@
"hidden": 1,
"label": "rgt",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "old_parent",
@ -122,28 +132,52 @@
"hidden": 1,
"ignore_user_permissions": 1,
"label": "Old Parent",
"print_hide": 1
"print_hide": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "payroll_cost_center",
"fieldtype": "Link",
"label": "Payroll Cost Center",
"options": "Cost Center"
"options": "Cost Center",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"description": "The first Approver in the list will be set as the default Approver.",
"fieldname": "approvers",
"fieldtype": "Section Break",
"label": "Approvers",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "shift_request_approver",
"fieldtype": "Table",
"label": "Shift Request Approver",
"options": "Department Approver",
"show_days": 1,
"show_seconds": 1
}
],
"icon": "fa fa-sitemap",
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2020-05-05 18:49:28.503931",
"modified": "2020-06-23 15:42:00.563272",
"modified_by": "Administrator",
"module": "HR",
"name": "Department",

View File

@ -15,12 +15,12 @@ class DepartmentApprover(Document):
def get_approvers(doctype, txt, searchfield, start, page_len, filters):
if not filters.get("employee"):
frappe.throw(_("Please select Employee Record first."))
frappe.throw(_("Please select Employee first."))
approvers = []
department_details = {}
department_list = []
employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver"], as_dict=True)
employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True)
employee_department = filters.get("department") or employee.department
if employee_department:
@ -37,13 +37,18 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
if filters.get("doctype") == "Expense Claim" and employee.expense_approver:
approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name']))
if filters.get("doctype") == "Shift Request" and employee.shift_request_approver:
approvers.append(frappe.db.get_value("User", employee.shift_request_approver, ['name', 'first_name', 'last_name']))
if filters.get("doctype") == "Leave Application":
parentfield = "leave_approvers"
field_name = "Leave Approver"
else:
elif filters.get("doctype") == "Expense Claim":
parentfield = "expense_approvers"
field_name = "Expense Approver"
elif filters.get("doctype") == "Shift Request":
parentfield = "shift_request_approver"
field_name = "Shift Request Approver"
if department_list:
for d in department_list:
approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from

View File

@ -51,10 +51,14 @@
"column_break_31",
"grade",
"branch",
"approvers_section",
"expense_approver",
"leave_approver",
"column_break_45",
"shift_request_approver",
"attendance_and_leave_details",
"leave_policy",
"attendance_device_id",
"leave_approver",
"column_break_44",
"holiday_list",
"default_shift",
@ -62,7 +66,6 @@
"salary_mode",
"payroll_cost_center",
"column_break_52",
"expense_approver",
"bank_name",
"bank_ac_no",
"health_insurance_section",
@ -806,14 +809,37 @@
"fieldname": "expense_approver",
"fieldtype": "Link",
"label": "Expense Approver",
"options": "User"
"options": "User",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "approvers_section",
"fieldtype": "Section Break",
"label": "Approvers",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_45",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "shift_request_approver",
"fieldtype": "Link",
"label": "Shift Request Approver",
"options": "User",
"show_days": 1,
"show_seconds": 1
}
],
"icon": "fa fa-user",
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2020-07-03 21:28:04.109189",
"modified": "2020-07-28 01:36:04.109189",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",

View File

@ -1,5 +1,5 @@
frappe.listview_settings['Expense Claim'] = {
add_fields: ["total_claimed_amount", "docstatus"],
add_fields: ["total_claimed_amount", "docstatus", "company"],
get_indicator: function(doc) {
if(doc.status == "Paid") {
return [__("Paid"), "green", "status,=,Paid"];

View File

@ -24,8 +24,13 @@ class JobOffer(Document):
check_vacancies = frappe.get_single("HR Settings").check_vacancies
if staffing_plan and check_vacancies:
job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date)
if staffing_plan.vacancies - len(job_offers) <= 0:
frappe.throw(_("There are no vacancies under staffing plan {0}").format(frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent))))
if not staffing_plan.get("vacancies") or staffing_plan.vacancies - len(job_offers) <= 0:
error_variable = 'for ' + frappe.bold(self.designation)
if staffing_plan.get("parent"):
error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent))
frappe.throw(_("There are no vacancies under staffing plan {0}").format(error_variable))
def on_change(self):
update_job_applicant(self.status, self.job_applicant)

View File

@ -5,20 +5,23 @@ cur_frm.add_fetch('employee','employee_name','employee_name');
frappe.ui.form.on("Leave Allocation", {
onload: function(frm) {
// Ignore cancellation of doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
if(!frm.doc.from_date) frm.set_value("from_date", frappe.datetime.get_today());
frm.set_query("employee", function() {
return {
query: "erpnext.controllers.queries.employee_query"
}
};
});
frm.set_query("leave_type", function() {
return {
filters: {
is_lwp: 0
}
}
})
};
});
},
refresh: function(frm) {

View File

@ -19,6 +19,10 @@ frappe.ui.form.on("Leave Application", {
frm.set_query("employee", erpnext.queries.employee);
},
onload: function(frm) {
// Ignore cancellation of doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
if (!frm.doc.posting_date) {
frm.set_value("posting_date", frappe.datetime.get_today());
}

View File

@ -2,6 +2,10 @@
// For license information, please see license.txt
frappe.ui.form.on('Leave Encashment', {
onload: function(frm) {
// Ignore cancellation of doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
},
setup: function(frm) {
frm.set_query("leave_type", function() {
return {
@ -33,7 +37,7 @@ frappe.ui.form.on('Leave Encashment', {
doc: frm.doc,
callback: function(r) {
frm.refresh_fields();
}
}
});
}
}

View File

@ -10,9 +10,11 @@
"employee",
"employee_name",
"shift_type",
"status",
"column_break_3",
"company",
"date",
"start_date",
"end_date",
"shift_request",
"department",
"amended_from"
@ -59,12 +61,6 @@
"options": "Company",
"reqd": 1
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date"
},
{
"fieldname": "shift_request",
"fieldtype": "Link",
@ -80,11 +76,36 @@
"options": "Shift Assignment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Active\nInactive",
"show_days": 1,
"show_seconds": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2019-12-12 15:49:06.956901",
"modified": "2020-06-15 14:27:54.310773",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Assignment",

View File

@ -11,38 +11,63 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from datetime import timedelta, datetime
class OverlapError(frappe.ValidationError): pass
class ShiftAssignment(Document):
def validate(self):
self.validate_overlapping_dates()
if self.end_date and self.end_date <= self.start_date:
frappe.throw(_("End Date must not be lesser than Start Date"))
def validate_overlapping_dates(self):
if not self.name:
self.name = "New Shift Assignment"
if not self.name:
self.name = "New Shift Assignment"
d = frappe.db.sql("""
select
name, shift_type, date
from `tabShift Assignment`
where employee = %(employee)s and docstatus < 2
and date = %(date)s
and name != %(name)s""", {
"employee": self.employee,
"shift_type": self.shift_type,
"date": self.date,
"name": self.name
}, as_dict = 1)
condition = """and (
end_date is null
or
%(start_date)s between start_date and end_date
"""
for date_overlap in d:
if date_overlap['name']:
self.throw_overlap_error(date_overlap)
if self.end_date:
condition += """ or
%(end_date)s between start_date and end_date
or
start_date between %(start_date)s and %(end_date)s
) """
else:
condition += """ ) """
def throw_overlap_error(self, d):
msg = _("Employee {0} has already applied for {1} on {2} : ").format(self.employee,
d['shift_type'], formatdate(d['date'])) \
+ """ <b><a href="#Form/Shift Assignment/{0}">{0}</a></b>""".format(d["name"])
frappe.throw(msg, OverlapError)
assigned_shifts = frappe.db.sql("""
select name, shift_type, start_date ,end_date, docstatus, status
from `tabShift Assignment`
where
employee=%(employee)s and docstatus = 1
and name != %(name)s
and status = "Active"
{0}
""".format(condition), {
"employee": self.employee,
"shift_type": self.shift_type,
"start_date": self.start_date,
"end_date": self.end_date,
"name": self.name
}, as_dict = 1)
if len(assigned_shifts):
self.throw_overlap_error(assigned_shifts[0])
def throw_overlap_error(self, shift_details):
shift_details = frappe._dict(shift_details)
if shift_details.docstatus == 1 and shift_details.status == "Active":
msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name))
if shift_details.start_date:
msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y"))
title = "Ongoing Shift"
if shift_details.end_date:
msg += _(" to {0}").format(getdate(self.end_date).strftime("%d-%m-%Y"))
title = "Active Shift"
if msg:
frappe.throw(msg, title=title)
@frappe.whitelist()
def get_events(start, end, filters=None):
@ -62,19 +87,22 @@ def get_events(start, end, filters=None):
return events
def add_assignments(events, start, end, conditions=None):
query = """select name, date, employee_name,
query = """select name, start_date, end_date, employee_name,
employee, docstatus
from `tabShift Assignment` where
date <= %(date)s
and docstatus < 2"""
start_date >= %(start_date)s
or end_date <= %(end_date)s
or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
and docstatus = 1"""
if conditions:
query += conditions
for d in frappe.db.sql(query, {"date":start, "date":end}, as_dict=True):
for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True):
e = {
"name": d.name,
"doctype": "Shift Assignment",
"date": d.date,
"start_date": d.start_date,
"end_date": d.end_date if d.end_date else nowdate(),
"title": cstr(d.employee_name) + \
cstr(d.shift_type),
"docstatus": d.docstatus
@ -92,7 +120,16 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals
:param next_shift_direction: One of: None, 'forward', 'reverse'. Direction to look for next shift if shift not found on given date.
"""
default_shift = frappe.db.get_value('Employee', employee, 'default_shift')
shift_type_name = frappe.db.get_value('Shift Assignment', {'employee':employee, 'date': for_date, 'docstatus': '1'}, 'shift_type')
shift_type_name = None
shift_assignment_details = frappe.db.get_value('Shift Assignment', {'employee':employee, 'start_date':('<=', for_date), 'docstatus': '1', 'status': "Active"}, ['shift_type', 'end_date'])
if shift_assignment_details:
shift_type_name = shift_assignment_details[0]
# if end_date present means that shift is over after end_date else it is a ongoing shift.
if shift_assignment_details[1] and for_date >= shift_assignment_details[1] :
shift_type_name = None
if not shift_type_name and consider_default_shift:
shift_type_name = default_shift
if shift_type_name:
@ -117,16 +154,20 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals
direction = '<' if next_shift_direction == 'reverse' else '>'
sort_order = 'desc' if next_shift_direction == 'reverse' else 'asc'
dates = frappe.db.get_all('Shift Assignment',
'date',
{'employee':employee, 'date':(direction, for_date), 'docstatus': '1'},
['start_date', 'end_date'],
{'employee':employee, 'start_date':(direction, for_date), 'docstatus': '1', "status": "Active"},
as_list=True,
limit=MAX_DAYS, order_by="date "+sort_order)
for date in dates:
shift_details = get_employee_shift(employee, date[0], consider_default_shift, None)
if shift_details:
shift_type_name = shift_details.shift_type.name
for_date = date[0]
break
limit=MAX_DAYS, order_by="start_date "+sort_order)
if dates:
for date in dates:
if date[1] and date[1] < for_date:
continue
shift_details = get_employee_shift(employee, date[0], consider_default_shift, None)
if shift_details:
shift_type_name = shift_details.shift_type.name
for_date = date[0]
break
return get_shift_details(shift_type_name, for_date)
@ -134,7 +175,7 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals
def get_employee_shift_timings(employee, for_timestamp=now_datetime(), consider_default_shift=False):
"""Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee
"""
# write and verify a test case for midnight shift.
# write and verify a test case for midnight shift.
prev_shift = curr_shift = next_shift = None
curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward')
if curr_shift:

View File

@ -3,8 +3,8 @@
frappe.views.calendar["Shift Assignment"] = {
field_map: {
"start": "date",
"end": "date",
"start": "start_date",
"end": "end_date",
"id": "name",
"docstatus": 1
},

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import nowdate
from frappe.utils import nowdate, add_days
test_dependencies = ["Shift Type"]
@ -20,8 +20,61 @@ class TestShiftAssignment(unittest.TestCase):
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"date": nowdate()
"start_date": nowdate()
}).insert()
shift_assignment.submit()
self.assertEqual(shift_assignment.docstatus, 1)
def test_overlapping_for_ongoing_shift(self):
# shift should be Ongoing if Only start_date is present and status = Active
shift_assignment_1 = frappe.get_doc({
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"start_date": nowdate(),
"status": 'Active'
}).insert()
shift_assignment_1.submit()
self.assertEqual(shift_assignment_1.docstatus, 1)
shift_assignment = frappe.get_doc({
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"start_date": add_days(nowdate(), 2)
})
self.assertRaises(frappe.ValidationError, shift_assignment.save)
def test_overlapping_for_fixed_period_shift(self):
# shift should is for Fixed period if Only start_date and end_date both are present and status = Active
shift_assignment_1 = frappe.get_doc({
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"start_date": nowdate(),
"end_date": add_days(nowdate(), 30),
"status": 'Active'
}).insert()
shift_assignment_1.submit()
# it should not allowed within period of any shift.
shift_assignment_3 = frappe.get_doc({
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"start_date":add_days(nowdate(), 10),
"end_date": add_days(nowdate(), 35),
"status": 'Active'
})
self.assertRaises(frappe.ValidationError, shift_assignment_3.save)

View File

@ -2,7 +2,16 @@
// For license information, please see license.txt
frappe.ui.form.on('Shift Request', {
refresh: function(frm) {
}
setup: function(frm) {
frm.set_query("approver", function() {
return {
query: "erpnext.hr.doctype.department_approver.department_approver.get_approvers",
filters: {
employee: frm.doc.employee,
doctype: frm.doc.doctype
}
};
});
frm.set_query("employee", erpnext.queries.employee);
},
});

View File

@ -1,396 +1,155 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "HR-SHR-.YY.-.MM.-.#####",
"beta": 0,
"creation": "2018-04-13 16:32:27.974273",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"autoname": "HR-SHR-.YY.-.MM.-.#####",
"creation": "2018-04-13 16:32:27.974273",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"shift_type",
"employee",
"employee_name",
"department",
"status",
"column_break_4",
"company",
"approver",
"from_date",
"to_date",
"amended_from"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "shift_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": 0,
"label": "Shift Type",
"length": 0,
"no_copy": 0,
"options": "Shift Type",
"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
},
"fieldname": "shift_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Shift Type",
"options": "Shift Type",
"reqd": 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": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Employee",
"length": 0,
"no_copy": 0,
"options": "Employee",
"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
},
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
"options": "Employee",
"reqd": 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": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Employee Name",
"length": 0,
"no_copy": 0,
"options": "",
"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
},
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 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
},
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"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_4",
"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
},
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"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
},
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 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": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"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": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date"
},
{
"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": 0,
"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,
"options": "Shift Request",
"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
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Shift Request",
"print_hide": 1,
"read_only": 1
},
{
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Draft\nApproved\nRejected",
"reqd": 1
},
{
"fetch_from": "employee.shift_request_approver",
"fetch_if_empty": 1,
"fieldname": "approver",
"fieldtype": "Link",
"label": "Approver",
"options": "User",
"reqd": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:36.577448",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Request",
"name_case": "",
"owner": "Administrator",
],
"is_submittable": 1,
"links": [],
"modified": "2020-08-10 17:59:31.550558",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Request",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"write": 1
},
},
{
"amend": 1,
"cancel": 1,
"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": 1,
"amend": 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
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"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": 1,
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1
}

View File

@ -14,19 +14,26 @@ class ShiftRequest(Document):
def validate(self):
self.validate_dates()
self.validate_shift_request_overlap_dates()
self.validate_approver()
self.validate_default_shift()
def on_submit(self):
date_list = self.get_working_days(self.from_date, self.to_date)
for date in date_list:
if self.status not in ["Approved", "Rejected"]:
frappe.throw(_("Only Shift Request with status 'Approved' and 'Rejected' can be submitted"))
if self.status == "Approved":
assignment_doc = frappe.new_doc("Shift Assignment")
assignment_doc.company = self.company
assignment_doc.shift_type = self.shift_type
assignment_doc.employee = self.employee
assignment_doc.date = date
assignment_doc.start_date = self.from_date
if self.to_date:
assignment_doc.end_date = self.to_date
assignment_doc.shift_request = self.name
assignment_doc.insert()
assignment_doc.submit()
frappe.msgprint(_("Shift Assignment: {0} created for Employee: {1}").format(frappe.bold(assignment_doc.name), frappe.bold(self.employee)))
def on_cancel(self):
shift_assignment_list = frappe.get_list("Shift Assignment", {'employee': self.employee, 'shift_request': self.name})
if shift_assignment_list:
@ -34,6 +41,19 @@ class ShiftRequest(Document):
shift_assignment_doc = frappe.get_doc("Shift Assignment", shift['name'])
shift_assignment_doc.cancel()
def validate_default_shift(self):
default_shift = frappe.get_value("Employee", self.employee, "default_shift")
if self.shift_type == default_shift:
frappe.throw(_("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type)))
def validate_approver(self):
department = frappe.get_value("Employee", self.employee, "department")
shift_approver = frappe.get_value("Employee", self.employee, "shift_request_approver")
approvers = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))
approvers = [approver[0] for approver in approvers]
approvers.append(shift_approver)
if self.approver not in approvers:
frappe.throw(_("Only Approvers can Approve this Request."))
def validate_dates(self):
if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)):
@ -68,28 +88,4 @@ class ShiftRequest(Document):
msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee,
d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \
+ """ <b><a href="#Form/Shift Request/{0}">{0}</a></b>""".format(d["name"])
frappe.throw(msg, OverlapError)
def get_working_days(self, start_date, end_date):
start_date, end_date = getdate(start_date), getdate(end_date)
from datetime import timedelta
date_list = []
employee_holiday_list = []
employee_holidays = frappe.db.sql("""select holiday_date from `tabHoliday`
where parent in (select holiday_list from `tabEmployee`
where name = %s)""",self.employee,as_dict=1)
for d in employee_holidays:
employee_holiday_list.append(d.holiday_date)
reference_date = start_date
while reference_date <= end_date:
if reference_date not in employee_holiday_list:
date_list.append(reference_date)
reference_date += timedelta(days=1)
return date_list
frappe.throw(msg, OverlapError)

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import nowdate
from frappe.utils import nowdate, add_days
class TestShiftRequest(unittest.TestCase):
def setUp(self):
@ -13,14 +13,20 @@ class TestShiftRequest(unittest.TestCase):
frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
def test_make_shift_request(self):
department = frappe.get_value("Employee", "_T-Employee-00001", 'department')
set_shift_approver(department)
approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0]
shift_request = frappe.get_doc({
"doctype": "Shift Request",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"employee_name": "_Test Employee",
"start_date": nowdate(),
"end_date": nowdate()
"from_date": nowdate(),
"to_date": add_days(nowdate(), 10),
"approver": approver,
"status": "Approved"
})
shift_request.insert()
shift_request.submit()
@ -34,4 +40,10 @@ class TestShiftRequest(unittest.TestCase):
self.assertEqual(shift_request.employee, employee)
shift_request.cancel()
shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')})
self.assertEqual(shift_assignment_doc.docstatus, 2)
self.assertEqual(shift_assignment_doc.docstatus, 2)
def set_shift_approver(department):
department_doc = frappe.get_doc("Department", department)
department_doc.append('shift_request_approver',{'approver': "test1@example.com"})
department_doc.save()
department_doc.reload()

View File

@ -4,7 +4,7 @@
frappe.ui.form.on('Shift Type', {
refresh: function(frm) {
frm.add_custom_button(
'Mark Auto Attendance',
'Mark Attendance',
() => frm.call({
doc: frm.doc,
method: 'process_auto_attendance',

View File

@ -79,9 +79,10 @@ class ShiftType(Document):
mark_attendance(employee, date, 'Absent', self.name)
def get_assigned_employee(self, from_date=None, consider_default_shift=False):
filters = {'date':('>=', from_date), 'shift_type': self.name, 'docstatus': '1'}
filters = {'start_date':('>', from_date), 'shift_type': self.name, 'docstatus': '1'}
if not from_date:
del filters['date']
del filters["start_date"]
assigned_employees = frappe.get_all('Shift Assignment', 'employee', filters, as_list=True)
assigned_employees = [x[0] for x in assigned_employees]

View File

@ -16,14 +16,16 @@ from six import string_types
class LoanApplication(Document):
def validate(self):
validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount,
self.repayment_periods, self.is_term_loan)
self.validate_loan_type()
self.set_pledge_amount()
self.set_loan_amount()
self.validate_loan_amount()
if self.is_term_loan:
validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount,
self.repayment_periods, self.is_term_loan)
self.validate_loan_type()
self.get_repayment_details()
self.check_sanctioned_amount_limit()
@ -106,7 +108,7 @@ class LoanApplication(Document):
if self.is_secured_loan and self.proposed_pledges:
self.maximum_loan_amount = 0
for security in self.proposed_pledges:
self.maximum_loan_amount += security.post_haircut_amount
self.maximum_loan_amount += flt(security.post_haircut_amount)
if not self.loan_amount and self.is_secured_loan and self.proposed_pledges:
self.loan_amount = self.maximum_loan_amount
@ -133,10 +135,7 @@ def create_loan(source_name, target_doc=None, submit=0):
"validation": {
"docstatus": ["=", 1]
},
"postprocess": update_accounts,
"field_no_map": [
"is_secured_loan"
]
"postprocess": update_accounts
}
}, target_doc)

View File

@ -10,22 +10,20 @@ from frappe.utils import nowdate, getdate, add_days, flt
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from frappe.utils import get_datetime
class LoanDisbursement(AccountsController):
def validate(self):
self.set_missing_values()
def before_submit(self):
self.set_status_and_amounts()
def before_cancel(self):
self.set_status_and_amounts(cancel=1)
def on_submit(self):
self.set_status_and_amounts()
self.make_gl_entries()
def on_cancel(self):
self.set_status_and_amounts(cancel=1)
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ['GL Entry']
@ -45,29 +43,69 @@ class LoanDisbursement(AccountsController):
def set_status_and_amounts(self, cancel=0):
loan_details = frappe.get_all("Loan",
fields = ["loan_amount", "disbursed_amount", "total_principal_paid", "status", "is_term_loan"],
filters= { "name": self.against_loan }
)[0]
if loan_details.status == "Disbursed" and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
loan=self.against_loan)
fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable",
"status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0]
if cancel:
disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount
total_payment = loan_details.total_payment
if loan_details.disbursed_amount > loan_details.loan_amount:
topup_amount = loan_details.disbursed_amount - loan_details.loan_amount
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment - topup_amount
if disbursed_amount == 0:
status = "Sanctioned"
elif disbursed_amount >= loan_details.disbursed_amount:
elif disbursed_amount >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
else:
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
total_payment = loan_details.total_payment
if flt(disbursed_amount) - flt(loan_details.total_principal_paid) > flt(loan_details.loan_amount):
if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan:
frappe.throw(_("Disbursed Amount cannot be greater than loan amount"))
if flt(disbursed_amount) >= loan_details.disbursed_amount:
if loan_details.status == 'Disbursed':
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid)
else:
pending_principal_amount = loan_details.disbursed_amount
security_value = 0.0
if loan_details.is_secured_loan:
security_value = get_total_pledged_security_value(self.against_loan)
if not security_value:
security_value = loan_details.loan_amount
if pending_principal_amount + self.disbursed_amount > flt(security_value):
allowed_amount = security_value - pending_principal_amount
if allowed_amount < 0:
allowed_amount = 0
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount))
if loan_details.status == "Disbursed" and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
loan=self.against_loan)
if disbursed_amount > loan_details.loan_amount:
topup_amount = disbursed_amount - loan_details.loan_amount
if topup_amount < 0:
topup_amount = 0
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment + topup_amount
if flt(disbursed_amount) >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
@ -75,7 +113,8 @@ class LoanDisbursement(AccountsController):
frappe.db.set_value("Loan", self.against_loan, {
"disbursement_date": self.disbursement_date,
"disbursed_amount": disbursed_amount,
"status": status
"status": status,
"total_payment": total_payment
})
def make_gl_entries(self, cancel=0, adv_adj=0):
@ -116,3 +155,24 @@ class LoanDisbursement(AccountsController):
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
def get_total_pledged_security_value(loan):
update_time = get_datetime()
loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price",
fields=["loan_security", "loan_security_price"],
filters = {
"valid_from": ("<=", update_time),
"valid_upto": (">=", update_time)
}, as_list=1))
hair_cut_map = frappe._dict(frappe.get_all('Loan Security',
fields=["name", "haircut"], as_list=1))
security_value = 0.0
pledged_securities = get_pledged_security_qty(loan)
for security, qty in pledged_securities.items():
security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100
return security_value

View File

@ -85,8 +85,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
if no_of_days <= 0:
return
pending_principal_amount = loan.total_payment - loan.total_interest_payable \
- loan.total_amount_paid
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)
payable_interest = interest_per_day * no_of_days

View File

@ -720,3 +720,6 @@ erpnext.patches.v13_0.move_branch_code_to_bank_account
erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes
erpnext.patches.v13_0.add_standard_navbar_items #4
erpnext.patches.v13_0.stock_entry_enhancements
erpnext.patches.v12_0.update_state_code_for_daman_and_diu
erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment

View File

@ -0,0 +1,17 @@
from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.exists("DocType", "Lost Reason Detail"):
frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail")
frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail")
frappe.db.sql("""INSERT INTO `tabOpportunity Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Opportunity'""")
frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""")
frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`)
SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason`
FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""")
frappe.delete_doc("DocType", "Lost Reason Detail")

View File

@ -0,0 +1,22 @@
import frappe
from erpnext.regional.india import states
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
# Update options in gst_state custom field
gst_state = frappe.get_doc('Custom Field', 'Address-gst_state')
gst_state.options = '\n'.join(states)
gst_state.save()
# Update gst_state and state code in existing address
frappe.db.sql("""
UPDATE `tabAddress`
SET
gst_state = 'Dadra and Nagar Haveli and Daman and Diu',
gst_state_number = 26
WHERE gst_state = 'Daman and Diu'
""")

View File

@ -0,0 +1,10 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc('hr', 'doctype', 'shift_assignment')
frappe.db.sql("update `tabShift Assignment` set end_date=date, start_date=date where date IS NOT NULL and start_date IS NULL and end_date IS NULL;")

View File

@ -7,27 +7,30 @@
"field_order": [
"salary_component",
"abbr",
"statistical_component",
"column_break_3",
"deduct_full_tax_on_selected_payroll_date",
"amount",
"section_break_5",
"additional_salary",
"statistical_component",
"depends_on_payment_days",
"is_tax_applicable",
"exempted_from_income_tax",
"is_tax_applicable",
"column_break_11",
"is_flexible_benefit",
"variable_based_on_taxable_salary",
"do_not_include_in_total",
"deduct_full_tax_on_selected_payroll_date",
"section_break_2",
"condition",
"column_break_18",
"amount_based_on_formula",
"formula",
"amount",
"do_not_include_in_total",
"section_break_19",
"default_amount",
"additional_amount",
"column_break_24",
"tax_on_flexible_benefit",
"tax_on_additional_salary",
"section_break_11",
"additional_salary",
"condition_and_formula_help"
"tax_on_additional_salary"
],
"fields": [
{
@ -110,9 +113,11 @@
"read_only": 1
},
{
"collapsible": 1,
"depends_on": "eval:doc.is_flexible_benefit != 1",
"fieldname": "section_break_2",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Condtion and formula"
},
{
"allow_on_submit": 1,
@ -181,23 +186,12 @@
"label": "Tax on additional salary",
"read_only": 1
},
{
"depends_on": "eval:doc.parenttype=='Salary Structure'",
"fieldname": "section_break_11",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.parenttype=='Salary Structure'",
"fieldname": "condition_and_formula_help",
"fieldtype": "HTML",
"label": "Condition and Formula Help",
"options": "<h3>Condition and Formula Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>Use field <code>base</code> for using base salary of the Employee</li>\n<li>Use Salary Component abbreviations in conditions and formulas. <code>BS = Basic Salary</code></li>\n<li>Use field name for employee details in conditions and formulas. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n<li>Use field name from Salary Slip in conditions and formulas. <code>Payment Days = payment_days</code><code>Leave without pay = leave_without_pay</code></li>\n<li>Direct Amount can also be entered based on Condtion. See example 3</li></ol>\n\n<h4>Examples</h4>\n<ol>\n<li>Calculating Basic Salary based on <code>base</code>\n<pre><code>Condition: base &lt; 10000</code></pre>\n<pre><code>Formula: base * .2</code></pre></li>\n<li>Calculating HRA based on Basic Salary<code>BS</code> \n<pre><code>Condition: BS &gt; 2000</code></pre>\n<pre><code>Formula: BS * .1</code></pre></li>\n<li>Calculating TDS based on Employment Type<code>employment_type</code> \n<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n<pre><code>Amount: 1000</code></pre></li>\n</ol>"
},
{
"fieldname": "additional_salary",
"fieldtype": "Link",
"label": "Additional Salary ",
"options": "Additional Salary"
"options": "Additional Salary",
"read_only": 1
},
{
"default": "0",
@ -207,11 +201,43 @@
"fieldtype": "Check",
"label": "Exempted from Income Tax",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Component properties and references ",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "section_break_19",
"fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_24",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-06-22 23:21:26.300951",
"modified": "2020-07-01 12:13:41.956495",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",

View File

@ -123,13 +123,13 @@ frappe.ui.form.on("Salary Slip", {
doc: frm.doc,
callback: function(r, rt) {
frm.refresh();
if (frm.doc.absent_days){
if (r.message){
frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true));
}
}
});
}
})
});
frappe.ui.form.on('Salary Slip Timesheet', {
time_sheet: function(frm, dt, dn) {

View File

@ -20,15 +20,17 @@
"company",
"letter_head",
"section_break_10",
"salary_slip_based_on_timesheet",
"start_date",
"end_date",
"salary_structure",
"column_break_18",
"salary_slip_based_on_timesheet",
"payroll_frequency",
"column_break_15",
"section_break_20",
"total_working_days",
"unmarked_days",
"leave_without_pay",
"column_break_24",
"absent_days",
"payment_days",
"hourly_wages",
@ -200,10 +202,6 @@
"fieldtype": "Date",
"label": "End Date"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"fieldname": "salary_structure",
"fieldtype": "Link",
@ -490,13 +488,25 @@
"fieldtype": "Float",
"hidden": 1,
"label": "Unmarked days"
},
{
"fieldname": "section_break_20",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_24",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-file-text",
"idx": 9,
"is_submittable": 1,
"links": [],
"modified": "2020-07-22 12:41:03.659422",
"modified": "2020-08-11 17:37:54.274384",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",

View File

@ -94,13 +94,6 @@ frappe.ui.form.on("Timesheet", {
}
},
company: function(frm) {
frappe.db.get_value('Company', { 'company_name' : frm.doc.company }, 'standard_working_hours')
.then(({ message }) => {
(frappe.working_hours = message.standard_working_hours || 0);
});
},
make_invoice: function(frm) {
let dialog = new frappe.ui.Dialog({
title: __("Select Item (optional)"),

View File

@ -10,8 +10,7 @@ states = [
'Bihar',
'Chandigarh',
'Chhattisgarh',
'Dadra and Nagar Haveli',
'Daman and Diu',
'Dadra and Nagar Haveli and Daman and Diu',
'Delhi',
'Goa',
'Gujarat',
@ -50,8 +49,7 @@ state_numbers = {
"Bihar": "10",
"Chandigarh": "04",
"Chhattisgarh": "22",
"Dadra and Nagar Haveli": "26",
"Daman and Diu": "25",
"Dadra and Nagar Haveli and Daman and Diu": "26",
"Delhi": "07",
"Goa": "30",
"Gujarat": "24",

View File

@ -134,15 +134,10 @@
"state_code": "DL",
"state_name": "Delhi"
},
{
"state_number": "25",
"state_code": "DD",
"state_name": "Daman and Diu"
},
{
"state_number": "26",
"state_code": "DN",
"state_name": "Dadra and Nagar Haveli"
"state_name": "Dadra and Nagar Haveli and Daman and Diu"
},
{
"state_number": "22",

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals
import frappe, re, json
from frappe import _
import erpnext
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words
from erpnext.regional.india import states, state_numbers
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
@ -673,25 +674,34 @@ def update_grand_total_for_rcm(doc, method):
if country != 'India':
return
if not doc.total_taxes_and_charges:
return
if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account')
base_gst_tax = 0
gst_tax = 0
for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"):
continue
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
gst_tax += tax.base_tax_amount_after_discount_amount
base_gst_tax += tax.base_tax_amount_after_discount_amount
gst_tax += tax.tax_amount_after_discount_amount
doc.taxes_and_charges_added -= gst_tax
doc.total_taxes_and_charges -= gst_tax
doc.base_taxes_and_charges_added -= base_gst_tax
doc.base_total_taxes_and_charges -= base_gst_tax
update_totals(gst_tax, doc)
update_totals(gst_tax, base_gst_tax, doc)
def update_totals(gst_tax, doc):
def update_totals(gst_tax, base_gst_tax, doc):
doc.base_grand_total -= base_gst_tax
doc.grand_total -= gst_tax
if doc.meta.get_field("rounded_total"):
@ -707,13 +717,17 @@ def update_totals(gst_tax, doc):
doc.outstanding_amount = doc.rounded_total or doc.grand_total
doc.in_words = money_in_words(doc.grand_total, doc.currency)
doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
doc.set_payment_schedule()
def make_regional_gl_entries(gl_entries, doc):
country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India':
return
return gl_entries
if not doc.total_taxes_and_charges:
return gl_entries
if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company)

View File

@ -7,6 +7,8 @@ from frappe import _
from frappe.utils import flt
from frappe.model.meta import get_field_precision
from frappe.utils.xlsxutils import handle_html
from six import iteritems
import json
def execute(filters=None):
return _execute(filters)
@ -21,21 +23,24 @@ def _execute(filters=None):
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
data = []
added_item = []
for d in item_list:
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
total_tax = 0
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
total_tax += flt(item_tax.get("tax_amount"))
if (d.parent, d.item_code) not in added_item:
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
total_tax = 0
for tax in tax_columns:
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
total_tax += flt(item_tax.get("tax_amount", 0))
row += [d.base_net_amount + total_tax]
row += [d.base_net_amount]
row += [d.base_net_amount + total_tax]
row += [d.base_net_amount]
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row += [item_tax.get("tax_amount", 0)]
for tax in tax_columns:
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
row += [item_tax.get("tax_amount", 0)]
data.append(row)
data.append(row)
added_item.append((d.parent, d.item_code))
if data:
data = get_merged_data(columns, data) # merge same hsn code data
return columns, data
@ -103,7 +108,7 @@ def get_items(filters):
match_conditions = " and {0} ".format(match_conditions)
return frappe.db.sql("""
items = frappe.db.sql("""
select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate,
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty,
@ -118,10 +123,9 @@ def get_items(filters):
""" % (conditions, match_conditions), filters, as_dict=1)
return items
def get_tax_accounts(item_list, columns, company_currency,
doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"):
import json
def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"):
item_row_map = {}
tax_columns = []
invoice_item_row = {}
@ -171,7 +175,7 @@ def get_tax_accounts(item_list, columns, company_currency,
for d in item_row_map.get(parent, {}).get(item_code, []):
item_tax_amount = tax_amount
if item_tax_amount:
itemised_tax.setdefault(d.name, {})[description] = frappe._dict({
itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({
"tax_amount": flt(item_tax_amount, tax_amount_precision)
})
except ValueError:
@ -179,42 +183,32 @@ def get_tax_accounts(item_list, columns, company_currency,
tax_columns.sort()
for desc in tax_columns:
columns.append(desc + " Amount:Currency/currency:160")
columns.append({
"label": desc,
"fieldname": frappe.scrub(desc),
"fieldtype": "Float",
"width": 110
})
# columns += ["Total Amount:Currency/currency:110"]
return itemised_tax, tax_columns
def get_merged_data(columns, data):
merged_hsn_dict = {} # to group same hsn under one key and perform row addition
add_column_index = [] # store index of columns that needs to be added
tax_col = len(get_columns())
fields_to_merge = ["stock_qty", "total_amount", "taxable_amount"] # columns for which index needs to be found
for i,d in enumerate(columns):
# check if fieldname in to_merge list and ignore tax-columns
if i < tax_col and d["fieldname"] in fields_to_merge:
add_column_index.append(i)
result = []
for row in data:
if row[0] in merged_hsn_dict:
to_add_row = merged_hsn_dict.get(row[0])
merged_hsn_dict.setdefault(row[0], {})
for i, d in enumerate(columns):
if d['fieldtype'] not in ('Int', 'Float', 'Currency'):
merged_hsn_dict[row[0]][d['fieldname']] = row[i]
else:
if merged_hsn_dict.get(row[0], {}).get(d['fieldname'], ''):
merged_hsn_dict[row[0]][d['fieldname']] += row[i]
else:
merged_hsn_dict[row[0]][d['fieldname']] = row[i]
# add columns from the add_column_index table
for k in add_column_index:
to_add_row[k] += row[k]
for key, value in iteritems(merged_hsn_dict):
result.append(value)
# add tax columns
for k in range(len(columns)):
if tax_col <= k < len(columns):
to_add_row[k] += row[k]
# update hsn dict with the newly added data
merged_hsn_dict[row[0]] = to_add_row
else:
merged_hsn_dict[row[0]] = row
# extract data rows to be displayed in report
data = [merged_hsn_dict[d] for d in merged_hsn_dict]
return data
return result

View File

@ -18,7 +18,7 @@
{
"hidden": 0,
"label": "Key Reports",
"links": "[\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]"
"links": "[\n {\n \n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
@ -44,7 +44,7 @@
"idx": 0,
"is_standard": 1,
"label": "Selling",
"modified": "2020-06-29 19:26:35.139097",
"modified": "2020-08-15 10:12:53.131621",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling",

View File

@ -184,6 +184,14 @@ class Customer(TransactionBase):
def validate_credit_limit_on_change(self):
if self.get("__islocal") or not self.credit_limits:
return
past_credit_limits = [d.credit_limit
for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")]
current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)]
if past_credit_limits == current_credit_limits:
return
company_record = []
for limit in self.credit_limits:

View File

@ -923,7 +923,7 @@
"fieldname": "lost_reasons",
"fieldtype": "Table MultiSelect",
"label": "Lost Reasons",
"options": "Lost Reason Detail",
"options": "Quotation Lost Reason Detail",
"read_only": 1
}
],
@ -932,7 +932,7 @@
"is_submittable": 1,
"links": [],
"max_attachments": 1,
"modified": "2020-07-18 04:59:09.960118",
"modified": "2020-07-26 17:46:19.951223",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",

View File

@ -68,7 +68,7 @@ class Quotation(SellingController):
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order():
get_lost_reasons = frappe.get_list('Opportunity Lost Reason',
get_lost_reasons = frappe.get_list('Quotation Lost Reason',
fields = ["name"])
lost_reasons_lst = [reason.get('name') for reason in get_lost_reasons]
frappe.db.set(self, 'status', 'Lost')

View File

@ -86,7 +86,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.$summary_container.append(
`<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>`
)
this.$summary_btns = this.$summary_container.find('.summary-btns');
}
@ -110,7 +110,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
{fieldname:'print', fieldtype:'Data', label:'Print Preview'}
],
primary_action: () => {
this.events.get_frm().print_preview.printit(true);
const frm = this.events.get_frm();
frm.doc = this.doc;
frm.print_preview.lang_code = frm.doc.language;
frm.print_preview.printit(true);
},
primary_action_label: __('Print'),
});
@ -174,7 +177,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
<div class="flex">
<div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div>
<div class="flex ml-6 text-dark-grey">
${
${
doc.taxes.map((t, i) => {
let margin_left = '';
if (i !== 0) margin_left = 'ml-2';
@ -271,6 +274,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
// this.print_dialog.show();
const frm = this.events.get_frm();
frm.doc = this.doc;
frm.print_preview.lang_code = frm.doc.language;
frm.print_preview.printit(true);
});
}
@ -284,9 +288,9 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.$summary_container.find('.print-btn').click();
});
}
toggle_component(show) {
show ?
show ?
this.$component.removeClass('d-none') :
this.$component.addClass('d-none');
}
@ -372,9 +376,9 @@ erpnext.PointOfSale.PastOrderSummary = class {
}
get_condition_btn_map(after_submission) {
if (after_submission)
if (after_submission)
return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }];
return [
{ condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] },
{ condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']},
@ -384,7 +388,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
load_summary_of(doc, after_submission=false) {
this.$summary_wrapper.removeClass("d-none");
after_submission ?
this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary();

View File

@ -494,13 +494,18 @@ frappe.ui.form.on(cur_frm.doctype, {
var dialog = new frappe.ui.Dialog({
title: __("Set as Lost"),
fields: [
{"fieldtype": "Table MultiSelect",
"label": __("Lost Reasons"),
"fieldname": "lost_reason",
"options": "Lost Reason Detail",
"reqd": 1},
{"fieldtype": "Text", "label": __("Detailed Reason"), "fieldname": "detailed_reason"},
{
"fieldtype": "Table MultiSelect",
"label": __("Lost Reasons"),
"fieldname": "lost_reason",
"options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail',
"reqd": 1
},
{
"fieldtype": "Text",
"label": __("Detailed Reason"),
"fieldname": "detailed_reason"
},
],
primary_action: function() {
var values = dialog.get_values();

View File

@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2020-07-14 09:21:44.057724",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"lost_reason"
],
"fields": [
{
"fieldname": "lost_reason",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Quotation Lost Reason",
"options": "Quotation Lost Reason"
}
],
"istable": 1,
"links": [],
"modified": "2020-07-26 17:58:56.373775",
"modified_by": "Administrator",
"module": "Setup",
"name": "Quotation Lost Reason Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class QuotationLostReasonDetail(Document):
pass

View File

@ -9,6 +9,33 @@
<p class="hero-subtitle">{{ greeting_subtitle }}</p>
{% endif %}
</div>
<div class="search-container">
<div class="website-search" id="search-container">
<div class="dropdown">
<div class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-search">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<input type="search" class="form-control" placeholder="Search the docs (Press ? to focus)" />
<div class="overflow-hidden shadow dropdown-menu w-100">
</div>
</div>
</div>
<button class="navbar-toggler" type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</section>
@ -54,5 +81,21 @@
</div>
</section>
{% endif %}
{% endblock %}
{% endblock %}
{%- block script -%}
<script>
frappe.ready(() => {
frappe.setup_search('#search-container', 'kb');
});
</script>
{%- endblock -%}
{%- block style -%}
<style>
.search-container {
margin-top: 1.2rem;
max-width: 500px;
}
</style>
{%- endblock -%}