[Enhancement] Time log wages and multiple active salary structure

This commit is contained in:
Rohit Waghchaure 2016-06-03 14:44:35 +05:30
parent 18357664cb
commit d6c986da8f
97 changed files with 3112 additions and 2670 deletions

View File

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

View File

@ -2,11 +2,13 @@
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-06-24 15:49:57",
"custom": 0,
"description": "Settings for Accounts",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Other",
"fields": [
{
"allow_on_submit": 0,
@ -18,6 +20,7 @@
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Make Accounting Entry For Every Stock Movement",
@ -42,6 +45,7 @@
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Accounts Frozen Upto",
@ -66,6 +70,7 @@
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Role Allowed to Set Frozen Accounts & Edit Frozen Entries",
@ -91,6 +96,7 @@
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Credit Controller",
@ -115,6 +121,7 @@
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Check Supplier Invoice Number Uniqueness",
@ -136,13 +143,14 @@
"hide_toolbar": 0,
"icon": "icon-cog",
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2015-12-24 21:42:01.274459",
"modified": "2016-06-27 15:18:27.566087",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@ -169,6 +177,9 @@
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0
"read_only_onload": 0,
"sort_order": "ASC",
"track_seen": 0
}

View File

@ -429,4 +429,30 @@ cur_frm.set_query("asset", "items", function(doc, cdt, cdn) {
["Asset", "company", "=", doc.company]
]
}
});
});
frappe.ui.form.on('Sales Invoice', {
setup: function(frm){
frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function(doc, cdt, cdn){
return {
filters: [
["Time Sheet", "status", "in", ["Submitted", "Payslip"]]
]
}
}
}
})
frappe.ui.form.on('Sales Invoice Timesheet', {
time_sheet: function(frm){
frm.call({
method: "calculate_billing_amount_from_timesheet",
doc: frm.doc,
callback: function(r, rt) {
refresh_field('total_billing_amount')
}
})
}
})
cur_frm.add_fetch("time_sheet", "total_billing_amount", "billing_amount");

View File

@ -1065,6 +1065,85 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": "",
"depends_on": "eval:doc.total_billing_amount > 0",
"fieldname": "time_sheet_list",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Time Sheet List",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "timesheets",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Time Sheets",
"length": 0,
"no_copy": 0,
"options": "Sales Invoice Timesheet",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "0",
"fieldname": "total_billing_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Total Billing Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -3597,7 +3676,7 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2016-06-10 12:57:08.818701",
"modified": "2016-06-27 15:13:36.523798",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -79,9 +79,10 @@ class SalesInvoice(SellingController):
self.set_against_income_account()
self.validate_c_form()
self.validate_time_logs_are_submitted()
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
self.update_packing_list()
self.calculate_billing_amount_from_timesheet()
def before_save(self):
set_account_for_mode_of_payment(self)
@ -125,11 +126,10 @@ class SalesInvoice(SellingController):
if not cint(self.is_pos) == 1 and not self.is_return:
self.update_against_document_in_jv()
self.update_time_log_batch(self.name)
self.update_time_sheet(self.name)
def before_cancel(self):
self.update_time_log_batch(None)
self.update_time_sheet(None)
def on_cancel(self):
self.check_close_sales_order("sales_order")
@ -217,20 +217,21 @@ class SalesInvoice(SellingController):
if pos:
return {"print_format": pos.get("print_format") }
def update_time_log_batch(self, sales_invoice):
for d in self.get("items"):
if d.time_log_batch:
tlb = frappe.get_doc("Time Log Batch", d.time_log_batch)
tlb.sales_invoice = sales_invoice
tlb.flags.ignore_validate_update_after_submit = True
tlb.save()
def update_time_sheet(self, sales_invoice):
for d in self.get("timesheets"):
if d.time_sheet:
timesheet = frappe.get_doc("Time Sheet", d.time_sheet)
timesheet.sales_invoice = sales_invoice
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status()
timesheet.save()
def validate_time_logs_are_submitted(self):
for d in self.get("items"):
if d.time_log_batch:
docstatus = frappe.db.get_value("Time Log Batch", d.time_log_batch, "docstatus")
if docstatus!=1:
frappe.throw(_("Time Log Batch {0} must be 'Submitted'").format(d.time_log_batch))
def validate_time_sheets_are_submitted(self):
for data in self.get("timesheets"):
if data.time_sheet:
status = frappe.db.get_value("Time Sheet", data.time_sheet, "status")
if status not in ['Submitted', 'Payslip']:
frappe.throw(_("Time Sheet {0} is already completed or cancelled").format(data.time_sheet))
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
@ -450,6 +451,13 @@ class SalesInvoice(SellingController):
else:
self.set('packed_items', [])
def calculate_billing_amount_from_timesheet(self):
total_billing_amount = 0.0
for data in self.timesheets:
if data.billing_amount:
total_billing_amount += data.billing_amount
self.total_billing_amount = total_billing_amount
def get_warehouse(self):
user_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile`

View File

@ -3,6 +3,7 @@
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
"beta": 0,
"creation": "2013-06-04 11:02:19",
"custom": 0,
"docstatus": 0,
@ -1417,31 +1418,6 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "time_log_batch",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Time Log Batch",
"length": 0,
"no_copy": 0,
"options": "Time Log Batch",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -1676,18 +1652,20 @@
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-04-06 12:26:19.052860",
"modified": "2016-06-24 16:01:59.719026",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",

View File

@ -0,0 +1,87 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-06-14 19:21:34.321662",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "time_sheet",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Time Sheet",
"length": 0,
"no_copy": 0,
"options": "Time Sheet",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "billing_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Billing Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-06-15 23:56:06.131202",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
}

View File

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

View File

@ -3,4 +3,4 @@
Now additional costs like shipping charges, operating costs etc can be added in Stock Entry in item valuation
- **Update Finished Goods** in Production Order can now use the items from **Transfer Materials for Manufacture** step instead of items from the Bill of Materials. This can be configured in Manufacturing Settings
- Added field **Tax ID** in Customer
- Bug fixes in Item, Time Log Batch, Pricing Rule, Salary Slip, Address and Stock Entry
- Bug fixes in Item, Time Sheet, Pricing Rule, Salary Slip, Address and Stock Entry

View File

@ -23,8 +23,8 @@ def get_data():
},
{
"type": "doctype",
"name": "Time Log",
"description": _("Time Logs for manufacturing."),
"name": "Time Sheet",
"description": _("Time Sheet for manufacturing."),
},
]

View File

@ -31,13 +31,8 @@ def get_data():
"items": [
{
"type": "doctype",
"name": "Time Log",
"description": _("Time Log for tasks."),
},
{
"type": "doctype",
"name": "Time Log Batch",
"description": _("Batch Time Logs for billing."),
"name": "Time Sheet",
"description": _("Time Sheet for tasks."),
},
{
"type": "doctype",
@ -58,8 +53,8 @@ def get_data():
{
"type": "report",
"is_query_report": True,
"name": "Daily Time Log Summary",
"doctype": "Time Log"
"name": "Daily Time Sheet Summary",
"doctype": "Time Sheet"
},
{
"type": "report",

View File

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -9,17 +9,17 @@ ODER
Öffnen Sie einfach Ihre Übersicht der Zeitprotokolle und kreuzen Sie die Artikel an, die Sie dem Zeitprotokollstapel hinzufügen möchten. Klicken Sie dann auf die Schaltfläche "Zeitprotokollstapel erstellen" und diese Zeitprotokolle werden ausgewählt.
<img class="screenshot" alt="Zeitprotokoll - Kalender aufziehen" src="{{docs_base_url}}/assets/img/project/time_log_batch.gif">
<img class="screenshot" alt="Zeitprotokoll - Kalender aufziehen" src="{{docs_base_url}}/assets/img/project/time_sheet.gif">
### Ausgangsrechnungen erstellen
* Sobald Sie einen Zeitprotokollstapel übertragen haben, sollte die Schaltfläche "Rechnung erstellen" erscheinen.
<img class="screenshot" alt="Zeitprotokoll - Kalender aufziehen" src="{{docs_base_url}}/assets/img/project/time_log_batch_make_invoice.png">
<img class="screenshot" alt="Zeitprotokoll - Kalender aufziehen" src="{{docs_base_url}}/assets/img/project/time_sheet_make_invoice.png">
* Klicken Sie auf diese Schaltfläche und erstellen Sie eine Ausgangsrechnung zu einem Zeitprotokollstapel.
<img class="screenshot" alt="Zeitprotokoll - Kalender aufziehen" src="{{docs_base_url}}/assets/img/project/time_log_batch_sales_invoice.png">
<img class="screenshot" alt="Zeitprotokoll - Kalender aufziehen" src="{{docs_base_url}}/assets/img/project/time_sheet_sales_invoice.png">
* Wenn Sie die Ausgangsrechnung "übertragen", wird die Nummer der Ausgangsrechnung in den Zeitprotokollen und im Zeitprotokollstapel aktualisiert und ihr Status wird auf "abgerechnet" geändert.

View File

@ -1,23 +1,23 @@
You can bill Time Logs by batching them together. This gives you the flexiblity to manage your customer billing in the way you want. To create a new Time Log Batch, go to
You can bill Time Logs by batching them together. This gives you the flexiblity to manage your customer billing in the way you want. To create a new Time Sheet, go to
> Projects > Time Log Batch > New Time Log Batch
> Projects > Time Sheet > New Time Sheet
OR
Just open your Time Log list and check the Items to you want to add to the Time Log. Then click on "Make Time Log Batch" button and these Time Logs will be selected.
Just open your Time Log list and check the Items to you want to add to the Time Log. Then click on "Make Time Sheet" button and these Time Logs will be selected.
<img class="screenshot" alt="Time Log - Drag Calender" src="{{docs_base_url}}/assets/img/project/time_log_batch.gif">
<img class="screenshot" alt="Time Log - Drag Calender" src="{{docs_base_url}}/assets/img/project/time_sheet.gif">
###Making Sales Invoice
* After submitting the Time Log Batch, "Make Invoice" button shall appear.
* After submitting the Time Sheet, "Make Invoice" button shall appear.
<img class="screenshot" alt="Time Log - Drag Calender" src="{{docs_base_url}}/assets/img/project/time_log_batch_make_invoice.png">
<img class="screenshot" alt="Time Log - Drag Calender" src="{{docs_base_url}}/assets/img/project/time_sheet_make_invoice.png">
* Click on that button to raise a Sales Invoice against the Time Log Batch.
* Click on that button to raise a Sales Invoice against the Time Sheet.
<img class="screenshot" alt="Time Log - Drag Calender" src="{{docs_base_url}}/assets/img/project/time_log_batch_sales_invoice.png">
<img class="screenshot" alt="Time Log - Drag Calender" src="{{docs_base_url}}/assets/img/project/time_sheet_sales_invoice.png">
* When you "Submit" the Sales Invoice, the Sales Invoice number will get updated in the Time Logs and Time Log Batch and their status will change to "Billed".
* When you "Submit" the Sales Invoice, the Sales Invoice number will get updated in the Time Logs and Time Sheet and their status will change to "Billed".
{next}

View File

@ -41,7 +41,7 @@ my_account_context = "erpnext.shopping_cart.utils.update_my_account_context"
email_append_to = ["Job Applicant", "Opportunity", "Issue"]
calendars = ["Task", "Production Order", "Time Log", "Leave Application", "Sales Order", "Holiday List"]
calendars = ["Task", "Production Order", "Leave Application", "Sales Order", "Holiday List"]
fixtures = ["Web Form"]

View File

@ -9,7 +9,7 @@ links = {
},
{
'label': _('Payroll'),
'items': ['Salary Structure', 'Salary Slip']
'items': ['Salary Structure', 'Salary Slip', 'Time Sheet']
},
{
'label': _('Expense'),

View File

@ -166,7 +166,7 @@
"bold": 0,
"collapsible": 0,
"default": "1",
"description": "Check if you want to send salary slip in mail to each employee while submitting salary slip",
"description": "",
"fieldname": "email_salary_slip_to_employee",
"fieldtype": "Check",
"hidden": 0,
@ -200,7 +200,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2016-06-25 17:43:06.643469",
"modified": "2016-06-27 16:20:59.737869",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
@ -220,8 +220,6 @@
"print": 1,
"read": 1,
"report": 0,
"restrict": 0,
"restricted": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,

View File

@ -58,7 +58,8 @@ cur_frm.cscript.make_jv = function(doc, dt, dn) {
});
}
frappe.ui.form.on("Process Payroll", "refresh", function(frm) {
frm.disable_save();
});
frappe.ui.form.on("Process Payroll", {
refresh: function(frm) {
frm.disable_save();
}
})

View File

@ -2,6 +2,7 @@
"allow_copy": 1,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2012-03-27 14:35:59",
"custom": 0,
"docstatus": 0,
@ -180,6 +181,55 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "salary_slip_based_on_timesheet",
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Salary Slip Based on Timesheet",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -503,13 +553,14 @@
"hide_toolbar": 1,
"icon": "icon-cog",
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2016-04-26 07:22:41.792785",
"modified": "2016-06-22 18:14:02.418857",
"modified_by": "Administrator",
"module": "HR",
"name": "Process Payroll",
@ -536,8 +587,10 @@
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"track_seen": 0
}

View File

@ -15,20 +15,18 @@ class ProcessPayroll(Document):
Returns list of active employees based on selected criteria
and for which salary structure exists
"""
cond = self.get_filter_condition()
cond += self.get_joining_releiving_condition()
emp_list = frappe.db.sql("""
select t1.name
from `tabEmployee` t1, `tabSalary Structure` t2
where t1.docstatus!=2 and t2.docstatus != 2
and t1.name = t2.employee
where t1.docstatus!=2 and t2.docstatus != 2 and
ifnull(t2.salary_slip_based_on_timesheet,0) = 0 and t1.name = t2.employee
%s """% cond)
return emp_list
def get_filter_condition(self):
self.check_mandatory()
@ -67,6 +65,7 @@ class ProcessPayroll(Document):
""", (emp[0], self.month, self.fiscal_year, self.company)):
ss = frappe.get_doc({
"doctype": "Salary Slip",
"salary_slip_based_on_timesheet": 0,
"fiscal_year": self.fiscal_year,
"employee": emp[0],
"month": self.month,

View File

@ -2,13 +2,51 @@
// License: GNU General Public License v3. See license.txt
cur_frm.add_fetch('employee', 'company', 'company');
cur_frm.add_fetch('time_sheet', 'total_hours', 'working_hours');
frappe.ui.form.on("Salary Slip", {
setup: function(frm) {
frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function(){
return {
filters: {
employee: frm.doc.employee,
make_for: 'Salary Slip'
}
}
}
},
company: function(frm) {
var company = locals[':Company'][frm.doc.company];
if(!frm.doc.letter_head && company.default_letter_head) {
frm.set_value('letter_head', company.default_letter_head);
}
},
refresh: function(frm) {
frm.trigger("toggle_fields")
},
salary_slip_based_on_timesheet: function(frm) {
frm.trigger("toggle_fields")
},
toggle_fields: function(frm) {
if(frm.doc.salary_slip_based_on_timesheet) {
hide_field(['fiscal_year', 'month', 'total_days_in_month', 'leave_without_pay', 'payment_days'])
unhide_field(['start_date', 'end_date', 'hourly_wages', 'timesheets'])
}else {
unhide_field(['fiscal_year', 'month', 'total_days_in_month', 'leave_without_pay', 'payment_days'])
hide_field(['start_date', 'end_date', 'hourly_wages', 'timesheets'])
}
}
})
frappe.ui.form.on("Salary Slip Timesheet", {
time_sheet: function(frm, cdt, cdn) {
doc = frm.doc;
cur_frm.cscript.fiscal_year(doc, cdt, cdn)
}
})
@ -38,7 +76,13 @@ cur_frm.cscript.fiscal_year = function(doc,dt,dn){
});
}
cur_frm.cscript.month = cur_frm.cscript.employee = cur_frm.cscript.fiscal_year;
cur_frm.cscript.month = cur_frm.cscript.salary_slip_based_on_timesheet = cur_frm.cscript.fiscal_year;
cur_frm.cscript.start_date = cur_frm.cscript.end_date = cur_frm.cscript.fiscal_year;
cur_frm.cscript.employee = function(doc,dt,dn){
doc.salary_structure = ''
cur_frm.cscript.fiscal_year(doc, dt, dn)
}
cur_frm.cscript.leave_without_pay = function(doc,dt,dn){
if (doc.employee && doc.fiscal_year && doc.month) {
@ -56,7 +100,7 @@ var calculate_all = function(doc, dt, dn) {
calculate_net_pay(doc, dt, dn);
}
cur_frm.cscript.e_modified_amount = function(doc,dt,dn){
cur_frm.cscript.earning_amount = function(doc,dt,dn){
calculate_earning_total(doc, dt, dn);
calculate_net_pay(doc, dt, dn);
}
@ -67,7 +111,7 @@ cur_frm.cscript.e_depends_on_lwp = function(doc,dt,dn){
}
// Trigger on earning modified amount and depends on lwp
// ------------------------------------------------------------------------
cur_frm.cscript.d_modified_amount = function(doc,dt,dn){
cur_frm.cscript.deduction_amount = function(doc,dt,dn){
calculate_ded_total(doc, dt, dn);
calculate_net_pay(doc, dt, dn);
}
@ -85,17 +129,17 @@ var calculate_earning_total = function(doc, dt, dn, reset_amount) {
var total_earn = 0;
for(var i = 0; i < tbl.length; i++){
if(cint(tbl[i].e_depends_on_lwp) == 1) {
tbl[i].e_modified_amount = Math.round(tbl[i].e_amount)*(flt(doc.payment_days) /
tbl[i].earning_amount = Math.round(tbl[i].e_amount)*(flt(doc.payment_days) /
cint(doc.total_days_in_month)*100)/100;
refresh_field('e_modified_amount', tbl[i].name, 'earnings');
refresh_field('earning_amount', tbl[i].name, 'earnings');
} else if(reset_amount) {
tbl[i].e_modified_amount = tbl[i].e_amount;
refresh_field('e_modified_amount', tbl[i].name, 'earnings');
tbl[i].earning_amount = tbl[i].e_amount;
refresh_field('earning_amount', tbl[i].name, 'earnings');
}
total_earn += flt(tbl[i].e_modified_amount);
total_earn += flt(tbl[i].earning_amount);
}
doc.gross_pay = total_earn + flt(doc.arrear_amount) + flt(doc.leave_encashment_amount);
refresh_many(['e_modified_amount', 'gross_pay']);
refresh_many(['earning_amount', 'gross_pay']);
}
// Calculate deduction total
@ -106,13 +150,13 @@ var calculate_ded_total = function(doc, dt, dn, reset_amount) {
var total_ded = 0;
for(var i = 0; i < tbl.length; i++){
if(cint(tbl[i].d_depends_on_lwp) == 1) {
tbl[i].d_modified_amount = Math.round(tbl[i].d_amount)*(flt(doc.payment_days)/cint(doc.total_days_in_month)*100)/100;
refresh_field('d_modified_amount', tbl[i].name, 'deductions');
tbl[i].deduction_amount = Math.round(tbl[i].d_amount)*(flt(doc.payment_days)/cint(doc.total_days_in_month)*100)/100;
refresh_field('deduction_amount', tbl[i].name, 'deductions');
} else if(reset_amount) {
tbl[i].d_modified_amount = tbl[i].d_amount;
refresh_field('d_modified_amount', tbl[i].name, 'earnings');
tbl[i].deduction_amount = tbl[i].d_amount;
refresh_field('deduction_amount', tbl[i].name, 'earnings');
}
total_ded += flt(tbl[i].d_modified_amount);
total_ded += flt(tbl[i].deduction_amount);
}
doc.total_deduction = total_ded;
refresh_field('total_deduction');

View File

@ -2,6 +2,7 @@
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-01-10 16:34:15",
"custom": 0,
"docstatus": 0,
@ -65,7 +66,7 @@
"bold": 0,
"collapsible": 0,
"fieldname": "employee_name",
"fieldtype": "Data",
"fieldtype": "Read Only",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@ -76,10 +77,11 @@
"no_copy": 0,
"oldfieldname": "employee_name",
"oldfieldtype": "Data",
"options": "employee.employee_name",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
@ -167,6 +169,31 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"oldfieldtype": "Column Break",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "50%"
},
{
"allow_on_submit": 0,
"bold": 0,
@ -221,8 +248,8 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break1",
"fieldtype": "Column Break",
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@ -230,8 +257,8 @@
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"oldfieldtype": "Column Break",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
@ -239,41 +266,40 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "50%"
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "month",
"fieldtype": "Select",
"depends_on": "",
"fieldname": "salary_slip_based_on_timesheet",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 1,
"label": "Month",
"in_filter": 0,
"in_list_view": 0,
"label": "Salary Slip Based on Timesheet",
"length": 0,
"no_copy": 0,
"oldfieldname": "month",
"oldfieldtype": "Select",
"options": "\n01\n02\n03\n04\n05\n06\n07\n08\n09\n10\n11\n12",
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "37%"
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "fiscal_year",
"fieldtype": "Link",
"hidden": 0,
@ -301,6 +327,139 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "month",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 1,
"label": "Month",
"length": 0,
"no_copy": 0,
"oldfieldname": "month",
"oldfieldtype": "Select",
"options": "\n01\n02\n03\n04\n05\n06\n07\n08\n09\n10\n11\n12",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0,
"width": "37%"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "start_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Start Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "Today",
"depends_on": "",
"fieldname": "end_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "End Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_15",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "salary_structure",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Salary Structure",
"length": 0,
"no_copy": 0,
"options": "Salary Structure",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "total_days_in_month",
"fieldtype": "Float",
"hidden": 0,
@ -327,6 +486,7 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "leave_without_pay",
"fieldtype": "Float",
"hidden": 0,
@ -353,6 +513,7 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "payment_days",
"fieldtype": "Float",
"hidden": 0,
@ -375,6 +536,159 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "hourly_wages",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "timesheets",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Salary Slip Timesheet",
"length": 0,
"no_copy": 0,
"options": "Salary Slip Timesheet",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_20",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "total_working_hours",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Total Working Hours",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "hour_rate",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Hour Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "section_break_26",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -427,6 +741,30 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_01",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -610,52 +948,6 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_25",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_26",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -737,6 +1029,29 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_25",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -849,13 +1164,14 @@
"hide_toolbar": 0,
"icon": "icon-file-text",
"idx": 9,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-04-26 06:02:06.940543",
"max_attachments": 0,
"modified": "2016-06-27 16:22:46.063078",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Slip",
@ -923,6 +1239,7 @@
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",

View File

@ -20,14 +20,16 @@ class SalarySlip(TransactionBase):
self.name = make_autoname('Sal Slip/' +self.employee + '/.#####')
def validate(self):
self.validate_dates()
self.check_existing()
self.set_month_dates()
if not (len(self.get("earnings")) or len(self.get("deductions"))):
self.get_emp_and_leave_details()
else:
self.get_leave_details(lwp = self.leave_without_pay)
if not self.net_pay:
if self.salary_slip_based_on_timesheet or not self.net_pay:
self.calculate_net_pay()
company_currency = get_company_currency(self.company)
@ -35,38 +37,85 @@ class SalarySlip(TransactionBase):
set_employee_name(self)
def validate_dates(self):
if date_diff(self.end_date, self.start_date) < 0:
frappe.throw(_("To date cannot be before From date"))
def get_emp_and_leave_details(self):
if self.employee:
self.set("earnings", [])
self.set("deductions", [])
self.set_month_dates()
self.validate_dates()
joining_date, relieving_date = frappe.db.get_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
self.get_leave_details(joining_date, relieving_date)
struct = self.check_sal_struct(joining_date, relieving_date)
if struct:
self.set("earnings", [])
self.set("deduction", [])
self.pull_sal_struct(struct)
ss_doc = frappe.get_doc('Salary Structure', struct)
self.salary_slip_based_on_timesheet = ss_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet()
self.pull_sal_struct(ss_doc)
def set_time_sheet(self):
if self.salary_slip_based_on_timesheet and not self.get('timesheets'):
self.set("timesheets", [])
timesheets = frappe.db.sql(""" select * from `tabTime Sheet` where employee = %(employee)s and (status = 'Submitted' or
status = 'Billed')""", {'employee': self.employee}, as_dict=1)
for data in timesheets:
self.append('timesheets', {
'time_sheet': data.name,
'working_hours': data.total_hours
})
def set_month_dates(self):
if self.month and not self.salary_slip_based_on_timesheet:
m = get_month_details(self.fiscal_year, self.month)
self.start_date = m['month_start_date']
self.end_date = m['month_end_date']
def check_sal_struct(self, joining_date, relieving_date):
m = get_month_details(self.fiscal_year, self.month)
timesheet = 1 if self.salary_slip_based_on_timesheet else 0
struct = frappe.db.sql("""select name from `tabSalary Structure`
where employee=%s and is_active = 'Yes'
and (from_date <= %s or from_date <= %s)
and (to_date is null or to_date >= %s or to_date >= %s)""",
(self.employee, m.month_start_date, joining_date, m.month_end_date, relieving_date))
and (to_date is null or to_date >= %s or to_date >= %s) and salary_slip_based_on_timesheet=%s""",
(self.employee, self.start_date, joining_date, self.end_date, relieving_date, timesheet))
if not struct:
msgprint(_("No active Salary Structure found for employee {0} and the month")
self.salary_structure = None
msgprint(_("No active or default Salary Structure found for employee {0} and the month")
.format(self.employee))
self.employee = None
return struct and struct[0][0] or ''
def pull_sal_struct(self, struct):
def pull_sal_struct(self, ss_doc):
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
make_salary_slip(struct, self)
make_salary_slip(ss_doc.name, self)
if self.salary_slip_based_on_timesheet:
self.salary_structure = ss_doc.name
self.hour_rate = ss_doc.hour_rate
self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0
self.add_earning_for_hourly_wages(ss_doc.earning_type)
def add_earning_for_hourly_wages(self, earning_type):
default_type = False
for data in self.earnings:
if data.earning_type == earning_type:
data.earning_amount = self.hour_rate * self.total_working_hours
default_type = True
break
if not default_type:
earnings = self.append('earnings', {})
earnings.earning_type = earning_type
earnings.earning_amount = self.hour_rate * self.total_working_hours
def pull_emp_details(self):
emp = frappe.db.get_value("Employee", self.employee, ["bank_name", "bank_ac_no"], as_dict=1)
@ -81,40 +130,41 @@ class SalarySlip(TransactionBase):
if not self.month:
self.month = "%02d" % getdate(nowdate()).month
self.set_month_dates()
if not joining_date:
joining_date, relieving_date = frappe.db.get_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
m = get_month_details(self.fiscal_year, self.month)
holidays = self.get_holidays_for_employee(m['month_start_date'], m['month_end_date'])
holidays = self.get_holidays_for_employee(self.start_date, self.end_date)
working_days = m["month_days"]
working_days = date_diff(self.end_date, self.start_date) + 1
if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")):
working_days -= len(holidays)
if working_days < 0:
frappe.throw(_("There are more holidays than working days this month."))
if not lwp:
lwp = self.calculate_lwp(holidays, m)
lwp = self.calculate_lwp(holidays, working_days)
self.total_days_in_month = working_days
self.leave_without_pay = lwp
payment_days = flt(self.get_payment_days(m, joining_date, relieving_date)) - flt(lwp)
payment_days = flt(self.get_payment_days(joining_date, relieving_date)) - flt(lwp)
self.payment_days = payment_days > 0 and payment_days or 0
def get_payment_days(self, month, joining_date, relieving_date):
start_date = month['month_start_date']
def get_payment_days(self, joining_date, relieving_date):
start_date = getdate(self.start_date)
if joining_date:
if joining_date > month['month_start_date']:
if joining_date > getdate(self.start_date):
start_date = joining_date
elif joining_date > month['month_end_date']:
elif joining_date > getdate(self.end_date):
return
end_date = month['month_end_date']
end_date = getdate(self.end_date)
if relieving_date:
if relieving_date > start_date and relieving_date < month['month_end_date']:
if relieving_date > start_date and relieving_date < getdate(self.end_date):
end_date = relieving_date
elif relieving_date < month['month_start_date']:
elif relieving_date < getdate(self.start_date):
frappe.throw(_("Employee relieved on {0} must be set as 'Left'")
.format(relieving_date))
@ -142,10 +192,10 @@ class SalarySlip(TransactionBase):
return holidays
def calculate_lwp(self, holidays, m):
def calculate_lwp(self, holidays, working_days):
lwp = 0
for d in range(m['month_days']):
dt = add_days(cstr(m['month_start_date']), d)
for d in range(working_days):
dt = add_days(cstr(self.start_date), d)
if dt not in holidays:
leave = frappe.db.sql("""
select t1.name, t1.half_day
@ -161,38 +211,43 @@ class SalarySlip(TransactionBase):
return lwp
def check_existing(self):
ret_exist = frappe.db.sql("""select name from `tabSalary Slip`
where month = %s and fiscal_year = %s and docstatus != 2
and employee = %s and name != %s""",
(self.month, self.fiscal_year, self.employee, self.name))
if ret_exist:
self.employee = ''
frappe.throw(_("Salary Slip of employee {0} already created for this month").format(self.employee))
if not self.salary_slip_based_on_timesheet:
ret_exist = frappe.db.sql("""select name from `tabSalary Slip`
where month = %s and fiscal_year = %s and docstatus != 2
and employee = %s and name != %s""",
(self.month, self.fiscal_year, self.employee, self.name))
if ret_exist:
self.employee = ''
frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee))
else:
for data in self.timesheets:
if frappe.db.get_value('Time Sheet', data.time_sheet, 'status') == 'Payrolled':
frappe.throw(_("Salary Slip of employee {0} already created for time sheet {1}").format(self.employee, data.time_sheet))
def calculate_earning_total(self):
self.gross_pay = flt(self.arrear_amount) + flt(self.leave_encashment_amount)
for d in self.get("earnings"):
if cint(d.e_depends_on_lwp) == 1:
d.e_modified_amount = rounded((flt(d.e_amount) * flt(self.payment_days)
/ cint(self.total_days_in_month)), self.precision("e_modified_amount", "earnings"))
d.earning_amount = rounded((flt(d.e_amount) * flt(self.payment_days)
/ cint(self.total_days_in_month)), self.precision("earning_amount", "earnings"))
elif not self.payment_days:
d.e_modified_amount = 0
elif not d.e_modified_amount:
d.e_modified_amount = d.e_amount
self.gross_pay += flt(d.e_modified_amount)
d.earning_amount = 0
elif not d.earning_amount:
d.earning_amount = d.e_amount
self.gross_pay += flt(d.earning_amount)
def calculate_ded_total(self):
self.total_deduction = 0
for d in self.get('deductions'):
if cint(d.d_depends_on_lwp) == 1:
d.d_modified_amount = rounded((flt(d.d_amount) * flt(self.payment_days)
/ cint(self.total_days_in_month)), self.precision("d_modified_amount", "deductions"))
d.deduction_amount = rounded((flt(d.d_amount) * flt(self.payment_days)
/ cint(self.total_days_in_month)), self.precision("deduction_amount", "deductions"))
elif not self.payment_days:
d.d_modified_amount = 0
elif not d.d_modified_amount:
d.d_modified_amount = d.d_amount
d.deduction_amount = 0
elif not d.deduction_amount:
d.deduction_amount = d.d_amount
self.total_deduction += flt(d.d_modified_amount)
self.total_deduction += flt(d.deduction_amount)
def calculate_net_pay(self):
disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None, "disable_rounded_total"))
@ -204,16 +259,28 @@ class SalarySlip(TransactionBase):
self.precision("net_pay") if disable_rounded_total else 0)
def on_submit(self):
self.update_status(self.name)
if(frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")):
self.email_salary_slip()
def on_cancel(self):
self.update_status()
def email_salary_slip(self):
receiver = frappe.db.get_value("Employee", self.employee, "company_email") or \
frappe.db.get_value("Employee", self.employee, "personal_email")
if receiver:
subj = 'Salary Slip - ' + cstr(self.month) +'/'+cstr(self.fiscal_year)
subj = 'Salary Slip - from {0} to {1}, fiscal year {2}'.format(self.start_date, self.end_date, self.fiscal_year)
frappe.sendmail([receiver], subject=subj, message = _("Please see attachment"),
attachments=[frappe.attach_print(self.doctype, self.name, file_name=self.name)], reference_doctype= self.doctype, reference_name= self.name)
else:
msgprint(_("{0}: Employee email not found, hence email not sent").format(self.employee_name))
def update_status(self, salary_slip=None):
for data in self.timesheets:
if data.time_sheet:
timesheet = frappe.get_doc('Time Sheet', data.time_sheet)
timesheet.salary_slip = salary_slip
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status()
timesheet.save()

View File

@ -5,14 +5,14 @@
{
"d_amount": 100,
"d_depends_on_lwp": 0,
"d_type": "_Test Professional Tax",
"deduction_type": "_Test Professional Tax",
"doctype": "Salary Slip Deduction",
"parentfield": "deductions"
},
{
"d_amount": 50,
"d_depends_on_lwp": 1,
"d_type": "_Test TDS",
"deduction_type": "_Test TDS",
"doctype": "Salary Slip Deduction",
"parentfield": "deductions"
}
@ -23,14 +23,14 @@
"doctype": "Salary Slip Earning",
"e_amount": 15000,
"e_depends_on_lwp": 1,
"e_type": "_Test Basic Salary",
"earning_type": "_Test Basic Salary",
"parentfield": "earnings"
},
{
"doctype": "Salary Slip Earning",
"e_amount": 500,
"e_depends_on_lwp": 0,
"e_type": "_Test Allowance",
"earning_type": "_Test Allowance",
"parentfield": "earnings"
}
],

View File

@ -4,7 +4,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import today
from frappe.utils import today, now_datetime, getdate, cstr
from erpnext.hr.doctype.employee.employee import make_salary_structure
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.hr.doctype.leave_application.test_leave_application import make_allocation_record
@ -35,10 +35,10 @@ class TestSalarySlip(unittest.TestCase):
self.assertEquals(ss.total_days_in_month, 31)
self.assertEquals(ss.payment_days, 30)
self.assertEquals(ss.earnings[0].e_modified_amount, 14516.13)
self.assertEquals(ss.earnings[1].e_modified_amount, 500)
self.assertEquals(ss.deductions[0].d_modified_amount, 100)
self.assertEquals(ss.deductions[1].d_modified_amount, 48.39)
self.assertEquals(ss.earnings[0].earning_amount, 14516.13)
self.assertEquals(ss.earnings[1].earning_amount, 500)
self.assertEquals(ss.deductions[0].deduction_amount, 100)
self.assertEquals(ss.deductions[1].deduction_amount, 48.39)
self.assertEquals(ss.gross_pay, 15016.13)
self.assertEquals(ss.net_pay, 14867.74)
@ -49,10 +49,10 @@ class TestSalarySlip(unittest.TestCase):
self.assertEquals(ss.total_days_in_month, 29)
self.assertEquals(ss.payment_days, 28)
self.assertEquals(ss.earnings[0].e_modified_amount, 14482.76)
self.assertEquals(ss.earnings[1].e_modified_amount, 500)
self.assertEquals(ss.deductions[0].d_modified_amount, 100)
self.assertEquals(ss.deductions[1].d_modified_amount, 48.28)
self.assertEquals(ss.earnings[0].earning_amount, 14482.76)
self.assertEquals(ss.earnings[1].earning_amount, 500)
self.assertEquals(ss.deductions[0].deduction_amount, 100)
self.assertEquals(ss.deductions[1].deduction_amount, 48.28)
self.assertEquals(ss.gross_pay, 14982.76)
self.assertEquals(ss.net_pay, 14834.48)
@ -153,6 +153,13 @@ class TestSalarySlip(unittest.TestCase):
return salary_slip
def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50
activity_type.costing_rate = 20
activity_type.wage_rate = 25
activity_type.save()
test_dependencies = ["Leave Application", "Holiday List"]
test_records = frappe.get_test_records('Salary Slip')

View File

@ -2,19 +2,22 @@
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-02-22 01:27:48",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "d_type",
"fieldname": "deduction_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Type",
@ -25,6 +28,7 @@
"options": "Deduction Type",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "200px",
"read_only": 0,
"report_hide": 0,
@ -42,6 +46,7 @@
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Default Amount",
@ -52,6 +57,7 @@
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -67,6 +73,7 @@
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Depends on Leave Without Pay",
@ -74,6 +81,7 @@
"no_copy": 0,
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -85,10 +93,11 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "d_modified_amount",
"fieldname": "deduction_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Amount",
@ -97,6 +106,7 @@
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -108,18 +118,22 @@
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2015-11-16 06:29:55.610195",
"modified": "2016-06-27 16:02:25.613138",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Slip Deduction",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0
"read_only_onload": 0,
"sort_order": "ASC",
"track_seen": 0
}

View File

@ -2,19 +2,22 @@
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-02-22 01:27:48",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "e_type",
"fieldname": "earning_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Type",
@ -25,6 +28,7 @@
"options": "Earning Type",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "",
"read_only": 0,
"report_hide": 0,
@ -42,6 +46,7 @@
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Default Amount",
@ -52,6 +57,7 @@
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -67,6 +73,7 @@
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Depends on Leave Without Pay",
@ -74,6 +81,7 @@
"no_copy": 0,
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -85,10 +93,11 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "e_modified_amount",
"fieldname": "earning_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Amount",
@ -97,6 +106,7 @@
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -108,18 +118,22 @@
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2015-11-16 06:29:55.649418",
"modified": "2016-06-27 15:56:04.146109",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Slip Earning",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0
"read_only_onload": 0,
"sort_order": "ASC",
"track_seen": 0
}

View File

@ -0,0 +1,87 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-06-14 19:22:29.811658",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "time_sheet",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Time Sheet",
"length": 0,
"no_copy": 0,
"options": "Time Sheet",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "working_hours",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Working Hours",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "3",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-06-15 17:59:57.876373",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Slip Timesheet",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
}

View File

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

View File

@ -12,7 +12,7 @@ cur_frm.cscript.onload = function(doc, dt, dn){
}
cur_frm.cscript.refresh = function(doc, dt, dn){
if((!doc.__islocal) && (doc.is_active == 'Yes')){
if((!doc.__islocal) && (doc.is_active == 'Yes') && (doc.salary_slip_based_on_timesheet == 0)){
cur_frm.add_custom_button(__('Salary Slip'),
cur_frm.cscript['Make Salary Slip'], __("Make"));
cur_frm.page.set_inner_btn_group_as_primary(__("Make"));
@ -52,7 +52,11 @@ var calculate_totals = function(doc, cdt, cdn) {
}
doc.total_earning = total_earn;
doc.total_deduction = total_ded;
doc.net_pay = flt(total_earn) - flt(total_ded);
doc.net_pay = 0.0
if(doc.salary_slip_based_on_timesheet == 0){
doc.net_pay = flt(total_earn) - flt(total_ded);
}
refresh_many(['total_earning', 'total_deduction', 'net_pay']);
}

View File

@ -2,6 +2,7 @@
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"beta": 0,
"creation": "2013-03-07 18:50:29",
"custom": 0,
"docstatus": 0,
@ -269,6 +270,33 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "No",
"fieldname": "is_default",
"fieldtype": "Select",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Is Default",
"length": 0,
"no_copy": 1,
"options": "Yes\nNo",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -325,6 +353,136 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:!doc.__islocal",
"fieldname": "salary_slip_based_on_timesheet",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Salary Slip Based on Timesheet",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:doc.salary_slip_based_on_timesheet",
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"description": "Earning type for timesheet based payroll.",
"fieldname": "earning_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Earning Type",
"length": 0,
"no_copy": 0,
"options": "Earning Type",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_17",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "hour_rate",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Hour Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"description": "Salary breakup based on Earning and Deduction.",
"fieldname": "earning_deduction",
"fieldtype": "Section Break",
@ -333,7 +491,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Monthly Earning & Deduction",
"label": "",
"length": 0,
"no_copy": 0,
"oldfieldname": "earning_deduction",
@ -379,6 +537,7 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "earnings",
"fieldtype": "Table",
"hidden": 0,
@ -397,7 +556,7 @@
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
@ -460,7 +619,8 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break0",
"depends_on": "",
"fieldname": "net_pay_detail",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
@ -612,13 +772,14 @@
"hide_toolbar": 0,
"icon": "icon-file-text",
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-03-18 16:10:30.031811",
"modified": "2016-06-27 16:23:16.856237",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Structure",
@ -649,7 +810,7 @@
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
@ -665,10 +826,12 @@
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "employee",
"title_field": "employee_name"
"title_field": "employee_name",
"track_seen": 0
}

View File

@ -16,7 +16,7 @@ class SalaryStructure(Document):
self.name = make_autoname(self.employee + '/.SST' + '/.#####')
def validate(self):
self.check_existing()
self.check_overlap()
self.validate_amount()
self.validate_employee()
self.validate_joining_date()
@ -48,43 +48,44 @@ class SalaryStructure(Document):
for li in list1:
child = self.append(tab_fname, {})
if(tab_fname == 'earnings'):
child.e_type = cstr(li[0])
child.earning_type = cstr(li[0])
child.modified_value = 0
elif(tab_fname == 'deductions'):
child.d_type = cstr(li[0])
child.deduction_type = cstr(li[0])
child.d_modified_amt = 0
def make_earn_ded_table(self):
self.make_table('Earning Type','earnings','Salary Structure Earning')
self.make_table('Deduction Type','deductions', 'Salary Structure Deduction')
def check_existing(self):
ret = self.get_other_active_salary_structure()
if ret and self.is_active=='Yes':
frappe.throw(_("Another Salary Structure {0} is active for employee {1}. Please make its status 'Inactive' to proceed.").format(ret, self.employee))
def get_other_active_salary_structure(self):
ret = frappe.db.sql("""select name from `tabSalary Structure` where is_active = 'Yes'
and employee = %s and name!=%s""", (self.employee,self.name))
return ret[0][0] if ret else None
def before_test_insert(self):
"""Make any existing salary structure for employee inactive."""
ret = self.get_other_active_salary_structure()
if ret:
frappe.db.set_value("Salary Structure", ret, "is_active", "No")
def check_overlap(self):
existing = frappe.db.sql("""select name from `tabSalary Structure`
where employee = %(employee)s and
(
(%(from_date)s > from_date and %(from_date)s < to_date) or
(%(to_date)s > from_date and %(to_date)s < to_date) or
(%(from_date)s <= from_date and %(to_date)s >= to_date))
and name!=%(name)s
and docstatus < 2""",
{
"employee": self.employee,
"from_date": self.from_date,
"to_date": self.to_date,
"name": self.name or "No Name"
}, as_dict=True)
if existing:
frappe.throw(_("Salary structure {0} already exist, more than one salary structure for same period is not allowed").format(existing[0].name))
def validate_amount(self):
if flt(self.net_pay) < 0:
if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet:
frappe.throw(_("Net pay cannot be negative"))
def validate_employee(self):
old_employee = frappe.db.get_value("Salary Structure", self.name, "employee")
if old_employee and self.employee != old_employee:
frappe.throw(_("Employee can not be changed"))
def validate_joining_date(self):
joining_date = getdate(frappe.db.get_value("Employee", self.employee, "date_of_joining"))
if getdate(self.from_date) < joining_date:
@ -93,6 +94,7 @@ class SalaryStructure(Document):
@frappe.whitelist()
def make_salary_slip(source_name, target_doc=None):
def postprocess(source, target):
target.salary_structure = source.name
target.run_method("pull_emp_details")
target.run_method("get_leave_details")
target.run_method("calculate_net_pay")
@ -109,7 +111,7 @@ def make_salary_slip(source_name, target_doc=None):
"field_map": [
["depend_on_lwp", "d_depends_on_lwp"],
["d_modified_amt", "d_amount"],
["d_modified_amt", "d_modified_amount"]
["d_modified_amt", "deduction_amount"]
],
"add_if_empty": True
},
@ -117,8 +119,8 @@ def make_salary_slip(source_name, target_doc=None):
"doctype": "Salary Slip Earning",
"field_map": [
["depend_on_lwp", "e_depends_on_lwp"],
["modified_value", "e_modified_amount"],
["modified_value", "e_amount"]
["modified_value", "e_amount"],
["modified_value", "earning_amount"]
],
"add_if_empty": True
}

View File

@ -6,10 +6,10 @@
"from_date": "2014-02-01",
"earnings": [
{
"e_type": "_Test Basic Salary"
"earning_type": "_Test Basic Salary"
},
{
"e_type": "_Test Allowance"
"earning_type": "_Test Allowance"
}
]
}

View File

@ -2,19 +2,22 @@
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-02-22 01:27:48",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "d_type",
"fieldname": "deduction_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Type",
@ -25,6 +28,7 @@
"options": "Deduction Type",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "200px",
"read_only": 0,
"report_hide": 0,
@ -42,6 +46,7 @@
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Amount",
@ -52,6 +57,7 @@
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -67,6 +73,7 @@
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reduce Deduction for Leave Without Pay (LWP)",
@ -76,6 +83,7 @@
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -87,18 +95,21 @@
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2015-11-16 06:29:55.793274",
"modified": "2016-06-27 15:36:26.491643",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Structure Deduction",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0
"read_only_onload": 0,
"track_seen": 0
}

View File

@ -2,6 +2,7 @@
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-02-22 01:27:48",
"custom": 0,
"docstatus": 0,
@ -11,10 +12,11 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "e_type",
"fieldname": "earning_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Type",
@ -25,6 +27,7 @@
"options": "Earning Type",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "200px",
"read_only": 0,
"report_hide": 0,
@ -42,6 +45,7 @@
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Amount",
@ -52,6 +56,7 @@
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -67,6 +72,7 @@
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reduce Earning for Leave Without Pay (LWP)",
@ -76,6 +82,7 @@
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -87,18 +94,22 @@
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2015-11-16 06:29:55.830492",
"modified": "2016-06-27 15:28:58.884891",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Structure Earning",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0
"read_only_onload": 0,
"sort_order": "ASC",
"track_seen": 0
}

View File

@ -42,12 +42,12 @@ def get_columns(salary_slips):
_("Payment Days") + ":Float:120"
]
earning_types = frappe.db.sql_list("""select distinct e_type from `tabSalary Slip Earning`
where e_modified_amount != 0 and parent in (%s)""" %
earning_types = frappe.db.sql_list("""select distinct earning_type from `tabSalary Slip Earning`
where earning_amount != 0 and parent in (%s)""" %
(', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]))
ded_types = frappe.db.sql_list("""select distinct d_type from `tabSalary Slip Deduction`
where d_modified_amount != 0 and parent in (%s)""" %
ded_types = frappe.db.sql_list("""select distinct deduction_type from `tabSalary Slip Deduction`
where deduction_amount != 0 and parent in (%s)""" %
(', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]))
columns = columns + [(e + ":Currency:120") for e in earning_types] + \
@ -83,25 +83,25 @@ def get_conditions(filters):
return conditions, filters
def get_ss_earning_map(salary_slips):
ss_earnings = frappe.db.sql("""select parent, e_type, e_modified_amount
ss_earnings = frappe.db.sql("""select parent, earning_type, earning_amount
from `tabSalary Slip Earning` where parent in (%s)""" %
(', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1)
ss_earning_map = {}
for d in ss_earnings:
ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.e_type, [])
ss_earning_map[d.parent][d.e_type] = flt(d.e_modified_amount)
ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.earning_type, [])
ss_earning_map[d.parent][d.earning_type] = flt(d.earning_amount)
return ss_earning_map
def get_ss_ded_map(salary_slips):
ss_deductions = frappe.db.sql("""select parent, d_type, d_modified_amount
ss_deductions = frappe.db.sql("""select parent, deduction_type, deduction_amount
from `tabSalary Slip Deduction` where parent in (%s)""" %
(', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1)
ss_ded_map = {}
for d in ss_deductions:
ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.d_type, [])
ss_ded_map[d.parent][d.d_type] = flt(d.d_modified_amount)
ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.deduction_type, [])
ss_ded_map[d.parent][d.deduction_type] = flt(d.deduction_amount)
return ss_ded_map

View File

@ -16,6 +16,10 @@ frappe.ui.form.on("Production Order", {
});
erpnext.production_order.set_default_warehouse(frm);
}
// formatter for production order operation
frm.set_indicator_formatter('operation',
function(doc) { return (frm.doc.qty==doc.completed_qty) ? "green" : "orange" })
erpnext.production_order.set_custom_buttons(frm);
erpnext.production_order.setup_company_filter(frm);
@ -33,6 +37,15 @@ frappe.ui.form.on("Production Order", {
if (frm.doc.docstatus===1) {
frm.trigger('show_progress');
}
if(frm.doc.docstatus == 1){
frm.add_custom_button(__('Make Timesheet'), function(){
frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.production_order.production_order.make_timesheet",
frm: cur_frm
})
})
}
},
show_progress: function(frm) {
var bars = [];
@ -90,7 +103,7 @@ frappe.ui.form.on("Production Order Operation", {
time_in_mins: function(frm, cdt, cdn) {
erpnext.production_order.calculate_cost(frm.doc);
erpnext.production_order.calculate_total_cost(frm);
}
},
});
erpnext.production_order = {
@ -112,9 +125,9 @@ erpnext.production_order = {
// opertions
if ((doc.operations || []).length) {
frm.add_custom_button(__('Time Logs'), function() {
frm.add_custom_button(__('Time Sheet'), function() {
frappe.route_options = {"production_order": frm.doc.name};
frappe.set_route("List", "Time Log");
frappe.set_route("List", "Time Sheet");
}, __("View"));
}
@ -252,32 +265,6 @@ $.extend(cur_frm.cscript, {
qty: function() {
frappe.ui.form.trigger("Production Order", 'bom_no')
},
show_time_logs: function(doc, cdt, cdn) {
var child = locals[cdt][cdn]
frappe.route_options = {"operation_id": child.name};
frappe.set_route("List", "Time Log");
},
make_time_log: function(doc, cdt, cdn){
var child = locals[cdt][cdn]
frappe.call({
method:"erpnext.manufacturing.doctype.production_order.production_order.make_time_log",
args: {
"name": doc.name,
"operation": child.operation,
"from_time": child.planned_start_time,
"to_time": child.planned_end_time,
"project": doc.project,
"workstation": child.workstation,
"qty": flt(doc.qty) - flt(child.completed_qty),
"operation_id": child.name
},
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
}
});
cur_frm.cscript['Stop Production Order'] = function() {

View File

@ -1084,13 +1084,14 @@
"hide_toolbar": 0,
"icon": "icon-cogs",
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-05-11 12:17:29.480533",
"modified": "2016-06-23 10:20:22.075476",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Order",
@ -1115,6 +1116,26 @@
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 1,
"role": "Stock User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"quick_entry": 1,

View File

@ -4,14 +4,16 @@
from __future__ import unicode_literals
import frappe
import json
from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from dateutil.relativedelta import relativedelta
from erpnext.stock.doctype.item.item import validate_end_of_life
from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError, NotInWorkingHoursError
from erpnext.projects.doctype.time_log.time_log import OverlapError
from erpnext.projects.doctype.time_sheet.time_sheet import OverlapError
from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
@ -158,6 +160,11 @@ class ProductionOrder(Document):
def before_submit(self):
self.set_required_items()
self.make_timesheets()
def before_cancel(self):
for data in self.operations:
data.time_sheet = None
def on_submit(self):
if not self.wip_warehouse:
@ -166,7 +173,6 @@ class ProductionOrder(Document):
frappe.throw(_("For Warehouse is required before Submit"))
self.update_reserved_qty_for_production()
self.make_time_logs()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
@ -175,7 +181,7 @@ class ProductionOrder(Document):
frappe.db.set(self,'status', 'Cancelled')
self.clear_required_items()
self.delete_time_logs()
self.delete_time_sheet()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
@ -233,8 +239,8 @@ class ProductionOrder(Document):
holidays[holiday_list] = holiday_list_days
return holidays[holiday_list]
def make_time_logs(self):
def make_timesheets(self):
"""Capacity Planning. Plan time logs based on earliest availablity of workstation after
Planned Start Date. Time logs will be created and remain in Draft mode and must be submitted
before manufacturing entry can be made."""
@ -242,72 +248,80 @@ class ProductionOrder(Document):
if not self.operations:
return
time_logs = []
time_sheets = []
plan_days = frappe.db.get_single_value("Manufacturing Settings", "capacity_planning_for_days") or 30
time_sheet = make_time_sheet(self.name)
workstation_list = []
last_workstation_idx = {}
for i, d in enumerate(self.operations):
self.set_operation_start_end_time(i, d)
time_log = make_time_log(self.name, d.operation, d.planned_start_time, d.planned_end_time,
flt(self.qty) - flt(d.completed_qty), self.project, d.workstation, operation_id=d.name)
if d.workstation:
last_workstation_idx[d.workstation] = i # set last row index of workstation
self.set_start_end_time_for_workstation(d, workstation_list, last_workstation_idx.get(d.workstation))
args = self.get_operations_data(d)
add_timesheet_detail(time_sheet, args)
original_start_time = d.planned_start_time
# validate operating hours if workstation [not mandatory] is specified
self.check_operation_fits_in_working_hours(d)
original_start_time = time_log.from_time
while True:
_from_time = time_log.from_time
try:
time_log.save()
break
except WorkstationHolidayError:
time_log.move_to_next_day()
except NotInWorkingHoursError:
time_log.move_to_next_working_slot()
time_sheet.validate_timesheets()
except OverlapError:
time_log.move_to_next_non_overlapping_slot()
time_sheet.move_to_next_non_overlapping_slot(d.idx)
# reset end time
time_log.to_time = get_datetime(time_log.from_time) + relativedelta(minutes=d.time_in_mins)
from_time, to_time = self.get_start_end_time(time_sheet, d.name)
if date_diff(time_log.from_time, original_start_time) > plan_days:
frappe.msgprint(_("Unable to find Time Slot in the next {0} days for Operation {1}").format(plan_days, d.operation))
if date_diff(from_time, original_start_time) > plan_days:
frappe.throw(_("Unable to find Time Slot in the next {0} days for Operation {1}").format(plan_days, d.operation))
break
# if time log needs to be moved, make sure that the from time is not the same
if _from_time == time_log.from_time:
frappe.throw(_("Capacity Planning Error"))
d.planned_start_time = from_time
d.planned_end_time = to_time
d.db_update()
d.planned_start_time = time_log.from_time
d.planned_end_time = time_log.to_time
d.db_update()
if time_log.name:
time_logs.append(time_log.name)
if time_sheet:
time_sheet.save()
time_sheets.append(time_sheet.name)
self.planned_end_date = self.operations[-1].planned_end_time
if time_logs:
if time_sheets:
frappe.local.message_log = []
frappe.msgprint(_("Time Logs created:") + "\n" + "\n".join(time_logs))
frappe.msgprint(_("Time Sheet created:") + "\n" + "\n".join(time_sheets))
def set_operation_start_end_time(self, i, d):
def get_operations_data(self, data):
return {
'from_time': data.planned_start_time,
'hours': data.time_in_mins / 60,
'to_time': data.planned_end_time,
'project': self.project,
'operation': data.operation,
'operation_id': data.name,
'workstation': data.workstation,
'completed_qty': flt(self.qty) - flt(data.completed_qty)
}
def set_start_end_time_for_workstation(self, data, workstation_list, index):
"""Set start and end time for given operation. If first operation, set start as
`planned_start_date`, else add time diff to end time of earlier operation."""
if self.planned_start_date:
if i==0:
# first operation at planned_start date
d.planned_start_time = self.planned_start_date
else:
d.planned_start_time = get_datetime(self.operations[i-1].planned_end_time)\
+ get_mins_between_operations()
d.planned_end_time = get_datetime(d.planned_start_time) + relativedelta(minutes = d.time_in_mins)
if data.workstation not in workstation_list:
data.planned_start_time = self.planned_start_date
workstation_list.append(data.workstation)
else:
data.planned_start_time = get_datetime(self.operations[index-1].planned_end_time)\
+ get_mins_between_operations()
if d.planned_start_time == d.planned_end_time:
frappe.throw(_("Capacity Planning Error"))
data.planned_end_time = get_datetime(data.planned_start_time) + relativedelta(minutes = data.time_in_mins)
if data.planned_start_time == data.planned_end_time:
frappe.throw(_("Capacity Planning Error"))
def get_start_end_time(self, time_sheet, operation_id):
for data in time_sheet.timesheets:
if data.operation_id == operation_id:
return data.from_time, data.to_time
def check_operation_fits_in_working_hours(self, d):
"""Raises expection if operation is longer than working hours in the given workstation."""
@ -336,9 +350,9 @@ class ProductionOrder(Document):
self.actual_start_date = None
self.actual_end_date = None
def delete_time_logs(self):
for time_log in frappe.get_all("Time Log", ["name"], {"production_order": self.name}):
frappe.delete_doc("Time Log", time_log.name)
def delete_time_sheet(self):
for time_sheet in frappe.get_all("Time Sheet", ["name"], {"production_order": self.name}):
frappe.delete_doc("Time Sheet", time_sheet.name)
def validate_production_item(self):
if frappe.db.get_value("Item", self.production_item, "has_variants"):
@ -490,22 +504,22 @@ def get_events(start, end, filters=None):
return data
@frappe.whitelist()
def make_time_log(name, operation, from_time=None, to_time=None, qty=None, project=None, workstation=None, operation_id=None):
time_log = frappe.new_doc("Time Log")
time_log.for_manufacturing = 1
time_log.from_time = from_time
time_log.to_time = to_time
time_log.production_order = name
time_log.project = project
time_log.operation_id = operation_id
time_log.operation = operation
time_log.workstation= workstation
time_log.activity_type= "Manufacturing"
time_log.completed_qty = flt(qty)
def make_time_sheet(production_order):
time_sheet = frappe.new_doc("Time Sheet")
time_sheet.employee = ""
time_sheet.production_order = production_order
return time_sheet
if from_time and to_time :
time_log.calculate_total_hours()
return time_log
@frappe.whitelist()
def add_timesheet_detail(time_sheet, args):
if isinstance(time_sheet, unicode):
time_sheet = frappe.get_doc('Time Sheet', time_sheet)
if isinstance(args, unicode):
args = json.loads(args)
time_sheet.append('timesheets', args)
return time_sheet
@frappe.whitelist()
def get_default_warehouse():
@ -514,3 +528,33 @@ def get_default_warehouse():
fg_warehouse = frappe.db.get_single_value("Manufacturing Settings",
"default_fg_warehouse")
return {"wip_warehouse": wip_warehouse, "fg_warehouse": fg_warehouse}
@frappe.whitelist()
def make_timesheet(source_name, target_doc=None):
def postprocess(source, target):
target.production_order = source.name
target.naming_series = 'TS-'
def update_item(source, target, source_parent):
target.completed_qty = source_parent.qty - source.completed_qty
doc = get_mapped_doc("Production Order", source_name, {
"Production Order": {
"doctype": "Time Sheet",
"validation": {
"docstatus": ["=", 1]
}
},
"Production Order Operation": {
"doctype": "Time Sheet Detail",
"field_map": {
"name": "operation_id",
"operation": "operation",
"workstation": "workstation"
},
"postprocess": update_item,
"condition": lambda doc: doc.status != 'Completed'
}
}, target_doc, postprocess)
return doc

View File

@ -10,7 +10,6 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_per
from erpnext.manufacturing.doctype.production_order.production_order \
import make_stock_entry, ItemHasVariantError
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.projects.doctype.time_log.time_log import OverProductionLoggedError
from erpnext.stock.utils import get_bin
class TestProductionOrder(unittest.TestCase):
@ -72,25 +71,25 @@ class TestProductionOrder(unittest.TestCase):
self.assertRaises(StockOverProductionError, s.submit)
def test_make_time_log(self):
from erpnext.projects.doctype.time_log.test_time_log import make_time_log_test_record
def test_make_time_sheet(self):
from erpnext.manufacturing.doctype.production_order.production_order import make_timesheet
prod_order = make_prod_order_test_record(item="_Test FG Item 2",
planned_start_date=now(), qty=1, do_not_save=True)
prod_order.set_production_order_operations()
prod_order.insert()
prod_order.submit()
d = prod_order.operations[0]
d.completed_qty = flt(d.completed_qty)
time_log = make_time_log_test_record(hours=1, production_order= prod_order.name, operation= d.operation,
completed_qty= prod_order.qty - d.completed_qty, operation_id=d.name, for_manufacturing=1, simulate=True)
name = frappe.db.get_value('Time Sheet', {'production_order': prod_order.name}, 'name')
time_sheet_doc = frappe.get_doc('Time Sheet', name)
time_sheet_doc.submit()
self.assertEqual(prod_order.name, time_log.production_order)
self.assertEqual((prod_order.qty - d.completed_qty), time_log.completed_qty)
self.assertEqual(time_diff_in_hours(d.planned_end_time, d.planned_start_time),time_log.hours)
self.assertEqual(prod_order.name, time_sheet_doc.production_order)
self.assertEqual((prod_order.qty - d.completed_qty), sum([d.completed_qty for d in time_sheet_doc.timesheets]))
manufacturing_settings = frappe.get_doc({
"doctype": "Manufacturing Settings",
@ -105,8 +104,11 @@ class TestProductionOrder(unittest.TestCase):
self.assertEqual(prod_order.operations[0].actual_operation_time, 60)
self.assertEqual(prod_order.operations[0].actual_operating_cost, 100)
time_sheet_doc1 = make_timesheet(prod_order.name)
self.assertEqual(len(time_sheet_doc1.get('timesheets')), 0)
time_log.cancel()
time_sheet_doc.cancel()
prod_order.load_from_db()
self.assertEqual(prod_order.operations[0].status, "Pending")
@ -115,11 +117,6 @@ class TestProductionOrder(unittest.TestCase):
self.assertEqual(flt(prod_order.operations[0].actual_operation_time), 0)
self.assertEqual(flt(prod_order.operations[0].actual_operating_cost), 0)
time_log2 = make_time_log_test_record(from_time= add_days(time_log.to_time, 1) ,production_order= prod_order.name, operation= d.operation,
completed_qty= 5, operation_id=d.name, for_manufacturing=1, do_not_save=True)
self.assertRaises(OverProductionLoggedError, time_log2.save)
def test_planned_operating_cost(self):
prod_order = make_prod_order_test_record(item="_Test FG Item 2",
planned_start_date=now(), qty=1, do_not_save=True)

View File

@ -2,6 +2,7 @@
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2014-10-16 14:35:41.950175",
"custom": 0,
"docstatus": 0,
@ -16,6 +17,7 @@
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "",
@ -24,6 +26,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -39,6 +42,7 @@
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Operation",
@ -50,6 +54,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -65,6 +70,7 @@
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Operation Description",
@ -75,6 +81,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 1,
@ -90,6 +97,7 @@
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
@ -97,6 +105,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -113,6 +122,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Completed Qty",
@ -121,6 +131,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -137,6 +148,7 @@
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Status",
@ -146,6 +158,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -161,6 +174,7 @@
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Workstation",
@ -172,30 +186,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:(doc.docstatus==1)",
"fieldname": "show_time_logs",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Show Time Logs",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -211,6 +202,7 @@
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Estimated Time and Cost",
@ -219,6 +211,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -234,6 +227,7 @@
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Planned Start Time",
@ -242,6 +236,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -257,6 +252,7 @@
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Planned End Time",
@ -265,6 +261,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -280,6 +277,7 @@
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
@ -287,6 +285,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -303,6 +302,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Operation Time",
@ -313,6 +313,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
@ -328,6 +329,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Hour Rate",
@ -338,6 +340,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -353,6 +356,7 @@
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Planned Operating Cost",
@ -362,6 +366,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -377,6 +382,7 @@
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Actual Time and Cost",
@ -385,6 +391,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -400,6 +407,7 @@
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Actual Start Time",
@ -408,6 +416,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -424,6 +433,7 @@
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Actual End Time",
@ -432,6 +442,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -447,6 +458,7 @@
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
@ -454,6 +466,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
@ -470,6 +483,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Actual Operation Time",
@ -478,6 +492,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
@ -494,6 +509,7 @@
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Actual Operating Cost",
@ -503,55 +519,36 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:(doc.docstatus==1 && doc.status!=\"Completed\")",
"fieldname": "make_time_log",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Make Time Log",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2015-11-16 06:29:52.476364",
"modified": "2016-06-24 16:12:08.380635",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Order Operation",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"track_seen": 0
}

View File

@ -276,3 +276,8 @@ erpnext.patches.v7_0.re_route #2016-06-27
erpnext.patches.v7_0.create_warehouse_nestedset
erpnext.patches.v7_0.system_settings_setup_complete
erpnext.patches.v7_0.merge_account_type_stock_and_warehouse_to_stock
erpnext.patches.v7_0.set_naming_series_for_timesheet
erpnext.patches.v7_0.convert_timelogbatch_to_timesheet
erpnext.patches.v7_0.convert_timelog_to_timesheet
erpnext.patches.v7_0.move_timelogbatch_from_salesinvoiceitem_to_salesinvoicetimesheet
erpnext.patches.v7_0.remove_doctypes_and_reports

View File

@ -182,8 +182,8 @@ rename_map = {
"Territory": [
["target_details", "targets"]
],
"Time Log Batch": [
["time_log_batch_details", "time_logs"]
"Time Sheet": [
["time_sheet_details", "time_logs"]
],
"Workstation": [
["workstation_operation_hours", "working_hours"]

View File

@ -0,0 +1,26 @@
import frappe
from erpnext.manufacturing.doctype.production_order.production_order import make_time_sheet, add_timesheet_detail
def execute():
for data in frappe.get_all('Time Log', fields=["*"],
filters = [["docstatus", "<", "2"]]):
time_sheet = make_time_sheet(data.production_order)
args = get_timesheet_data(data)
add_timesheet_detail(time_sheet, args)
time_sheet.docstatus = data.docstatus
time_sheet.company = frappe.db.get_single_value('Global Defaults', 'default_company')
time_sheet.save(ignore_permissions=True)
def get_timesheet_data(data):
return {
'from_time': data.from_time,
'hours': data.hours,
'to_time': data.to_time,
'project': data.project,
'activity_type': data.activity_type or "Planning",
'operation': data.operation,
'operation_id': data.operation_id,
'workstation': data.workstation,
'completed_qty': data.completed_qty
}

View File

@ -0,0 +1,32 @@
import frappe
from frappe.utils import cint
from erpnext.manufacturing.doctype.production_order.production_order import add_timesheet_detail
def execute():
for tlb in frappe.get_all('Time Log Batch', fields=["*"],
filters = [["docstatus", "<", "2"]]):
time_sheet = frappe.new_doc('Time Sheet')
time_sheet.employee= ""
time_sheet.company = frappe.db.get_single_value('Global Defaults', 'default_company')
time_sheet.sales_invoice = tlb.sales_invoice
for data in tlb.time_logs:
args = get_timesheet_data(data)
add_timesheet_detail(time_sheet, args)
time_sheet.docstatus = tlb.docstatus
time_sheet.save(ignore_permissions=True)
def get_timesheet_data(data):
time_log = frappe.get_doc('Time Log', data.time_log)
return {
'from_time': time_log.from_time,
'hours': time_log.hours,
'to_time': time_log.to_time,
'project': time_log.project,
'activity_type': time_log.activity_type,
'operation': time_log.operation,
'operation_id': time_log.operation_id,
'workstation': time_log.workstation,
'completed_qty': time_log.completed_qty
}

View File

@ -0,0 +1,16 @@
import frappe
from erpnext.manufacturing.doctype.production_order.production_order import add_timesheet_detail
def execute():
for si in frappe.db.sql(""" select sales_invoice as name from `tabTime Sheet`
where sales_invoice is not null and docstatus < 2""", as_dict=True):
si_doc = frappe.get_doc('Sales Invoice', si.name)
for item in si_doc.items:
if item.time_log_batch:
ts = si_doc.append('timesheets',{})
ts.time_sheet = item.time_log_batch
ts.billing_amount = frappe.db.get_value('Time Log Batch', item.time_log_batch, 'total_billing_amount')
si_doc.ignore_validate_update_after_submit = True
si_doc.update_time_sheet(ts.time_sheet)
si_doc.save()

View File

@ -0,0 +1,9 @@
import frappe
def execute():
for doctype in ['Time Log Batch', 'Time Log Batch Detail', 'Time Log']:
frappe.delete_doc('DocType', doctype)
report = "Daily Time Log Summary"
if frappe.db.exists("Report", report):
frappe.delete_doc('Report', report)

View File

@ -0,0 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
def execute():
frappe.reload_doctype('Time Sheet')
frappe.reload_doctype('Time Sheet Detail')
make_property_setter('Time Sheet', "naming_series", "options", 'TS-', "Text")
make_property_setter('Time Sheet', "naming_series", "default", 'TS-', "Text")

View File

@ -67,7 +67,7 @@ frappe.ui.form.on("Project", {
},
show_dashboard: function(frm) {
frm.dashboard.show_heatmap = true;
frm.dashboard.heatmap_message = __('This is based on the Time Logs created against this project');
frm.dashboard.heatmap_message = __('This is based on the Time Sheet created against this project');
frm.dashboard.show_dashboard();
if(frm.doc.__onload.activity_summary.length) {
@ -82,9 +82,9 @@ frappe.ui.form.on("Project", {
sum: sum
}));
section.on('click', '.time-log-link', function() {
section.on('click', '.time-sheet-link', function() {
var activity_type = $(this).attr('data-activity_type');
frappe.set_route('List', 'Time Log',
frappe.set_route('List', 'Time Sheet',
{'activity_type': activity_type, 'project': frm.doc.name});
});
}

View File

@ -28,7 +28,7 @@ class Project(Document):
self.set_onload('links', self.meta.get_links_setup())
self.set_onload('activity_summary', frappe.db.sql('''select activity_type, sum(hours) as total_hours
from `tabTime Log` where project=%s group by activity_type order by total_hours desc''', self.name, as_dict=True))
from `tabTime Sheet Detail` where project=%s group by activity_type order by total_hours desc''', self.name, as_dict=True))
def __setup__(self):
self.onload()
@ -95,13 +95,13 @@ class Project(Document):
self.percent_complete = flt(flt(completed) / total * 100, 2)
def update_costing(self):
from_time_log = frappe.db.sql("""select
from_time_sheet = frappe.db.sql("""select
sum(costing_amount) as costing_amount,
sum(billing_amount) as billing_amount,
min(from_time) as start_date,
max(to_time) as end_date,
sum(hours) as time
from `tabTime Log` where project = %s and docstatus = 1""", self.name, as_dict=1)[0]
from `tabTime Sheet Detail` where project = %s and docstatus = 1""", self.name, as_dict=1)[0]
from_expense_claim = frappe.db.sql("""select
sum(total_sanctioned_amount) as total_sanctioned_amount
@ -109,12 +109,12 @@ class Project(Document):
and docstatus = 1""",
self.name, as_dict=1)[0]
self.actual_start_date = from_time_log.start_date
self.actual_end_date = from_time_log.end_date
self.actual_start_date = from_time_sheet.start_date
self.actual_end_date = from_time_sheet.end_date
self.total_costing_amount = from_time_log.costing_amount
self.total_billing_amount = from_time_log.billing_amount
self.actual_time = from_time_log.time
self.total_costing_amount = from_time_sheet.costing_amount
self.total_billing_amount = from_time_sheet.billing_amount
self.actual_time = from_time_sheet.time
self.total_expense_claim = from_expense_claim.total_sanctioned_amount
@ -162,7 +162,7 @@ def get_dashboard_data(name):
def get_timeline_data(name):
'''Return timeline for attendance'''
return dict(frappe.db.sql('''select unix_timestamp(from_time), count(*)
from `tabTime Log` where project=%s
from `tabTime Sheet Detail` where project=%s
and from_time > date_sub(curdate(), interval 1 year)
and docstatus < 2
group by date(from_time)''', name))

View File

@ -3,7 +3,7 @@
{% for d in data %}
<div class="row">
<div class="col-xs-4">
<a class="small time-log-link" data-activity_type="{{ d.activity_type || "" }}">
<a class="small time-sheet-link" data-activity_type="{{ d.activity_type || "" }}">
{{ d.activity_type || __("Unknown") }}</a>
</div>
<div class="col-xs-8">

View File

@ -5,7 +5,7 @@ links = {
'transactions': [
{
'label': _('Project'),
'items': ['Task', 'Time Log', 'Expense Claim', 'Issue']
'items': ['Task', 'Time Sheet', 'Expense Claim', 'Issue']
},
{
'label': _('Material'),

View File

@ -16,10 +16,10 @@ frappe.ui.form.on("Task", {
if(!doc.__islocal) {
if(frappe.model.can_read("Time Log")) {
frm.add_custom_button(__("Time Logs"), function() {
if(frappe.model.can_read("Time Sheet")) {
frm.add_custom_button(__("Time Sheet"), function() {
frappe.route_options = {"project": doc.project, "task": doc.name}
frappe.set_route("List", "Time Log");
frappe.set_route("List", "Time Sheet");
}, __("View"), true);
}
if(frappe.model.can_read("Expense Claim")) {

View File

@ -3,6 +3,7 @@
"allow_import": 1,
"allow_rename": 0,
"autoname": "TASK.#####",
"beta": 0,
"creation": "2013-01-29 19:25:50",
"custom": 0,
"docstatus": 0,
@ -413,7 +414,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Actual Start Date (via Time Logs)",
"label": "Actual Start Date (via Time Sheet)",
"length": 0,
"no_copy": 0,
"oldfieldname": "act_start_date",
@ -491,7 +492,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Actual End Date (via Time Logs)",
"label": "Actual End Date (via Time Sheet)",
"length": 0,
"no_copy": 0,
"oldfieldname": "act_end_date",
@ -541,7 +542,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Total Costing Amount (via Time Logs)",
"label": "Total Costing Amount (via Time Sheet)",
"length": 0,
"no_copy": 0,
"oldfieldname": "actual_budget",
@ -618,7 +619,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Total Billing Amount (via Time Logs)",
"label": "Total Billing Amount (via Time Sheet)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -763,6 +764,7 @@
"hide_toolbar": 0,
"icon": "icon-check",
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
@ -770,7 +772,7 @@
"istable": 0,
"max_attachments": 5,
"menu_index": 0,
"modified": "2016-03-29 01:01:50.074252",
"modified": "2016-06-20 17:31:00.798534",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
@ -797,6 +799,7 @@
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "subject",

View File

@ -58,7 +58,7 @@ class Task(Document):
def update_time_and_costing(self):
tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
sum(hours) as time from `tabTime Log` where task = %s and docstatus=1"""
sum(hours) as time from `tabTime Sheet Detail` where task = %s and docstatus=1"""
,self.name, as_dict=1)[0]
if self.status == "Open":
self.status = "Working"

View File

@ -124,29 +124,6 @@ class TestTask(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate('2015-1-26'))
self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate('2015-1-28'))
time_log = frappe.new_doc('Time Log')
time_log.update({
"from_time": "2015-1-1",
"to_time": "2015-1-20",
"task": task1.name
})
time_log.submit()
self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_start_date"), getdate('2015-1-21'))
self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_end_date"), getdate('2015-1-25'))
self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate('2015-1-26'))
self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate('2015-1-28'))
time_log.cancel()
self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_start_date"), getdate('2015-1-21'))
self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_end_date"), getdate('2015-1-25'))
self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate('2015-1-26'))
self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate('2015-1-28'))
def test_close_assignment(self):
task = frappe.new_doc("Task")
task.subject = "Test Close Assignment"

View File

@ -1 +0,0 @@
Log of activity done by a user in a particular period.

View File

@ -1,182 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
import unittest
import datetime
from frappe.utils import now_datetime, now
from erpnext.projects.doctype.time_log.time_log import OverlapError, NotSubmittedError, NegativeHoursError
from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError, NotInWorkingHoursError
from erpnext.manufacturing.doctype.production_order.test_production_order import make_prod_order_test_record
class TestTimeLog(unittest.TestCase):
def test_duplication(self):
tl1 = make_time_log_test_record(user= "test@example.com", employee= "_T-Employee-0002", simulate= True)
tl2 = make_time_log_test_record(user= "test@example.com", employee= "_T-Employee-0002",
from_time= tl1.from_time, to_time= tl1.to_time, do_not_save= 1)
self.assertRaises(OverlapError, tl2.insert)
tl3 = make_time_log_test_record(user= "test@example.com", employee= "_T-Employee-0002",
from_time= tl1.from_time - datetime.timedelta(hours=1),
to_time= tl1.to_time + datetime.timedelta(hours=1), do_not_save= 1)
self.assertRaises(OverlapError, tl3.insert)
tl4 = make_time_log_test_record(user= "test@example.com", employee= "_T-Employee-0002",
from_time= tl1.from_time + datetime.timedelta(minutes=20),
to_time= tl1.to_time + datetime.timedelta(minutes=30), do_not_save= 1)
self.assertRaises(OverlapError, tl4.insert)
make_time_log_test_record(user= "test@example.com", employee= "_T-Employee-0002",
from_time= tl1.to_time,
to_time= tl1.to_time + datetime.timedelta(hours=1))
def test_production_order_status(self):
prod_order = make_prod_order_test_record(item= "_Test FG Item 2", qty= 1, do_not_submit= True)
prod_order.set_production_order_operations()
prod_order.save()
time_log = make_time_log_test_record(for_manufacturing= 1, production_order= prod_order.name, qty= 1,
employee= "_T-Employee-0003", do_not_save= True, simulate=1)
self.assertRaises(NotSubmittedError, time_log.save)
def test_time_log_on_holiday(self):
prod_order = make_prod_order_test_record(item= "_Test FG Item 2", qty= 1,
planned_start_date= now(), do_not_save= True)
prod_order.set_production_order_operations()
prod_order.save()
prod_order.submit()
time_log = make_time_log_test_record(from_time= "2013-02-01 10:00:00", to_time= "2013-02-01 20:00:00",
for_manufacturing= 1, production_order= prod_order.name, qty= 1,
operation= prod_order.operations[0].operation, operation_id= prod_order.operations[0].name,
workstation= "_Test Workstation 1", do_not_save= True)
self.assertRaises(WorkstationHolidayError , time_log.save)
time_log.update({
"from_time": "2013-02-02 09:00:00",
"to_time": "2013-02-02 20:00:00"
})
self.assertRaises(NotInWorkingHoursError , time_log.save)
time_log.from_time= "2013-02-02 10:30:00"
time_log.save()
time_log.submit()
time_log.cancel()
def test_negative_hours(self):
time_log = make_time_log_test_record(to_time= now_datetime() + datetime.timedelta(minutes=-1),
employee="_T-Employee-0006",do_not_save= True)
self.assertRaises(NegativeHoursError, time_log.save)
def test_default_activity_cost(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 20
activity_type.costing_rate = 15
activity_type.save()
project = "_Test Project for Activity Type"
frappe.db.sql("delete from `tabTime Log` where project=%s or employee='_T-Employee-0002'", project)
frappe.delete_doc("Project", project)
project = frappe.get_doc({"doctype": "Project", "project_name": project}).insert()
make_time_log_test_record(employee="_T-Employee-0002", hours=2,
activity_type = "_Test Activity Type", project = project.name)
project = frappe.get_doc("Project", project.name)
self.assertTrue(project.total_costing_amount, 30)
self.assertTrue(project.total_billing_amount, 40)
def test_total_activity_cost_for_project(self):
frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """)
frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
frappe.db.sql("""delete from `tabTime Log` where name = "_Test Project 1" """)
if not frappe.db.exists('Activity Cost', {"activity_type": "_Test Activity Type"}):
activity_cost = frappe.get_doc({
"doctype": "Activity Cost",
"employee": "_T-Employee-0002",
"activity_type": "_Test Activity Type",
"billing_rate": 100,
"costing_rate": 50
})
activity_cost.insert()
frappe.get_doc({
"project_name": "_Test Project 1",
"doctype": "Project",
"tasks" :
[{ "title": "_Test Project Task 1", "status": "Open" }]
}).save()
task_name = frappe.db.get_value("Task",{"project": "_Test Project 1"})
time_log = make_time_log_test_record(employee="_T-Employee-0002", hours=2,
task=task_name, simulate=1)
self.assertEqual(time_log.costing_rate, 50)
self.assertEqual(time_log.costing_amount, 100)
self.assertEqual(time_log.billing_rate, 100)
self.assertEqual(time_log.billing_amount, 200)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_billing_amount"), 200)
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_billing_amount"), 200)
time_log2 = make_time_log_test_record(employee="_T-Employee-0002",
hours=2, task= task_name, from_time = now_datetime() + datetime.timedelta(hours= 3), simulate=1)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_billing_amount"), 400)
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_billing_amount"), 400)
time_log2.cancel()
self.assertEqual(frappe.db.get_value("Task", task_name, "total_billing_amount"), 200)
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_billing_amount"), 200)
time_log.cancel()
test_ignore = ["Time Log Batch", "Sales Invoice"]
def make_time_log_test_record(**args):
args = frappe._dict(args)
time_log = frappe.new_doc("Time Log")
time_log.from_time = args.from_time or now_datetime()
time_log.hours = args.hours or 1
time_log.to_time = args.to_time or time_log.from_time + datetime.timedelta(hours= time_log.hours)
time_log.project = args.project
time_log.task = args.task
time_log.for_manufacturing = args.for_manufacturing
time_log.production_order = args.production_order
time_log.operation = args.operation
time_log.operation_id = args.operation_id
time_log.workstation = args.workstation
time_log.completed_qty = args.completed_qty
time_log.activity_type = args.activity_type or "_Test Activity Type"
time_log.billable = args.billable or 1
time_log.employee = args.employee
time_log.user = args.user
if not args.do_not_save:
if args.simulate:
while True:
try:
time_log.save()
break
except OverlapError:
time_log.from_time = time_log.from_time + datetime.timedelta(minutes=10)
time_log.to_time = time_log.from_time + datetime.timedelta(hours= time_log.hours)
else:
time_log.save()
if not args.do_not_submit:
time_log.submit()
return time_log

View File

@ -1,105 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Time Log", {
onload: function(frm) {
if (frm.doc.__islocal) {
if (frm.doc.for_manufacturing) {
frappe.ui.form.trigger("Time Log", "production_order");
}
if (frm.doc.from_time && frm.doc.to_time) {
frappe.ui.form.trigger("Time Log", "to_time");
}
}
frm.set_query('task', function() {
return {
filters:{
'project': frm.doc.project
}
}
});
},
refresh: function(frm) {
// set default user if created
if (frm.doc.__islocal && !frm.doc.user) {
frm.set_value("user", user);
}
if (frm.doc.status==='In Progress' && !frm.is_new()) {
frm.add_custom_button(__('Finish'), function() {
frappe.prompt({
fieldtype: 'Datetime',
fieldname: 'to_time',
label: __('End Time'),
'default': dateutil.now_datetime()
}, function(value) {
frm.set_value('to_time', value.to_time);
frm.save();
});
}).addClass('btn-primary');
}
frm.toggle_reqd("activity_type", !frm.doc.for_manufacturing);
},
hours: function(frm) {
if(!frm.doc.from_time) {
frm.set_value("from_time", frappe.datetime.now_datetime());
}
var d = moment(frm.doc.from_time);
d.add(frm.doc.hours, "hours");
frm._setting_hours = true;
frm.set_value("to_time", d.format(moment.defaultDatetimeFormat));
frm._setting_hours = false;
frm.trigger('calculate_cost');
},
before_save: function(frm) {
frm.doc.production_order && frappe.model.remove_from_locals("Production Order",
frm.doc.production_order);
},
to_time: function(frm) {
if(frm._setting_hours) return;
frm.set_value("hours", moment(cur_frm.doc.to_time).diff(moment(cur_frm.doc.from_time),
"seconds") / 3600);
},
calculate_cost: function(frm) {
frm.set_value("costing_amount", frm.doc.costing_rate * frm.doc.hours);
if (frm.doc.billable==1){
frm.set_value("billing_amount", (frm.doc.billing_rate * frm.doc.hours) + frm.doc.additional_cost);
}
},
additional_cost: function(frm) {
frm.trigger('calculate_cost');
},
activity_type: function(frm) {
if (frm.doc.activity_type){
return frappe.call({
method: "erpnext.projects.doctype.time_log.time_log.get_activity_cost",
args: {
"employee": frm.doc.employee,
"activity_type": frm.doc.activity_type
},
callback: function(r) {
if(!r.exc && r.message) {
frm.set_value("costing_rate", r.message.costing_rate);
frm.set_value("billing_rate", r.message.billing_rate);
frm.trigger('calculate_cost');
}
}
});
}
},
employee: function(frm) {
frm.trigger('activity_type');
},
billable: function(frm) {
if (frm.doc.billable==1) {
frm.trigger('calculate_cost');
}
else {
frm.set_value("billing_amount", 0);
frm.set_value("additional_cost", 0);
}
}
});

View File

@ -1,296 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt, get_datetime, get_time, getdate
from dateutil.relativedelta import relativedelta
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
class OverlapError(frappe.ValidationError): pass
class OverProductionLoggedError(frappe.ValidationError): pass
class NotSubmittedError(frappe.ValidationError): pass
class NegativeHoursError(frappe.ValidationError): pass
from frappe.model.document import Document
class TimeLog(Document):
def validate(self):
self.set_status()
self.set_title()
self.validate_overlap()
self.validate_timings()
self.calculate_total_hours()
self.validate_time_log_for()
self.check_workstation_timings()
self.validate_production_order()
self.validate_manufacturing()
self.set_project_if_missing()
self.update_cost()
def on_submit(self):
self.update_production_order()
self.update_task_and_project()
def on_cancel(self):
self.update_production_order()
self.update_task_and_project()
def before_update_after_submit(self):
self.set_status()
def before_cancel(self):
self.set_status()
def set_status(self):
self.status = {
0: "To Submit",
1: "Submitted",
2: "Cancelled"
}[self.docstatus or 0]
if not self.to_time:
self.status = 'In Progress'
if self.time_log_batch:
self.status= "Batched for Billing"
if self.sales_invoice:
self.status= "Billed"
def set_title(self):
"""Set default title for the Time Log"""
if self.title:
return
from frappe.utils import get_fullname
if self.production_order:
self.title = _("{0} for {1}").format(self.operation, self.production_order)
elif self.activity_type and (self.task or self.project):
self.title = _("{0} for {1}").format(self.activity_type, self.task or self.project)
else:
self.title = self.task or self.project or get_fullname(frappe.session.user)
def validate_overlap(self):
"""Checks if 'Time Log' entries overlap for a user, workstation. """
self.validate_overlap_for("user")
self.validate_overlap_for("employee")
self.validate_overlap_for("workstation")
def validate_overlap_for(self, fieldname):
existing = self.get_overlap_for(fieldname)
if existing:
frappe.throw(_("This Time Log conflicts with {0} for {1} {2}").format(existing.name,
self.meta.get_label(fieldname), self.get(fieldname)), OverlapError)
def get_overlap_for(self, fieldname):
if not self.get(fieldname):
return
existing = frappe.db.sql("""select name, from_time, to_time from `tabTime Log`
where `{0}`=%(val)s and
(
(%(from_time)s > from_time and %(from_time)s < to_time) or
(%(to_time)s > from_time and %(to_time)s < to_time) or
(%(from_time)s <= from_time and %(to_time)s >= to_time))
and name!=%(name)s
and docstatus < 2""".format(fieldname),
{
"val": self.get(fieldname),
"from_time": self.from_time,
"to_time": self.to_time,
"name": self.name or "No Name"
}, as_dict=True)
return existing[0] if existing else None
def validate_timings(self):
if self.to_time and self.from_time and get_datetime(self.to_time) <= get_datetime(self.from_time):
frappe.throw(_("To Time must be greater than From Time"), NegativeHoursError)
def calculate_total_hours(self):
if self.to_time and self.from_time:
from frappe.utils import time_diff_in_seconds
self.hours = flt(time_diff_in_seconds(self.to_time, self.from_time)) / 3600
def set_project_if_missing(self):
"""Set project if task is set"""
if self.task and not self.project:
self.project = frappe.db.get_value("Task", self.task, "project")
def validate_time_log_for(self):
if not self.for_manufacturing:
for fld in ["production_order", "operation", "workstation", "completed_qty"]:
self.set(fld, None)
else:
self.activity_type=None
def check_workstation_timings(self):
"""Checks if **Time Log** is between operating hours of the **Workstation**."""
if self.workstation and self.from_time and self.to_time:
from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours
check_if_within_operating_hours(self.workstation, self.operation, self.from_time, self.to_time)
def validate_production_order(self):
"""Throws 'NotSubmittedError' if **production order** is not submitted. """
if self.production_order:
if frappe.db.get_value("Production Order", self.production_order, "docstatus") != 1 :
frappe.throw(_("You can make a time log only against a submitted production order"), NotSubmittedError)
def update_production_order(self):
"""Updates `start_date`, `end_date`, `status` for operation in Production Order."""
if self.production_order and self.for_manufacturing:
if not self.operation_id:
frappe.throw(_("Operation ID not set"))
dates = self.get_operation_start_end_time()
summary = self.get_time_log_summary()
pro = frappe.get_doc("Production Order", self.production_order)
for o in pro.operations:
if o.name == self.operation_id:
o.actual_start_time = dates.start_date
o.actual_end_time = dates.end_date
o.completed_qty = summary.completed_qty
o.actual_operation_time = summary.mins
break
pro.flags.ignore_validate_update_after_submit = True
pro.update_operation_status()
pro.calculate_operating_cost()
pro.set_actual_dates()
pro.save()
def get_operation_start_end_time(self):
"""Returns Min From and Max To Dates of Time Logs against a specific Operation. """
return frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date from `tabTime Log`
where production_order = %s and operation = %s and docstatus=1""",
(self.production_order, self.operation), as_dict=1)[0]
def move_to_next_day(self):
"""Move start and end time one day forward"""
self.from_time = get_datetime(self.from_time) + relativedelta(day=1)
def move_to_next_working_slot(self):
"""Move to next working slot from workstation"""
workstation = frappe.get_doc("Workstation", self.workstation)
slot_found = False
for working_hour in workstation.working_hours:
if get_datetime(self.from_time).time() < get_time(working_hour.start_time):
self.from_time = getdate(self.from_time).strftime("%Y-%m-%d") + " " + working_hour.start_time
slot_found = True
break
if not slot_found:
# later than last time
self.from_time = getdate(self.from_time).strftime("%Y-%m-%d") + " " + workstation.working_hours[0].start_time
self.move_to_next_day()
def move_to_next_non_overlapping_slot(self):
"""If in overlap, set start as the end point of the overlapping time log"""
overlapping = self.get_overlap_for("workstation") \
or self.get_overlap_for("employee") \
or self.get_overlap_for("user")
if not overlapping:
frappe.throw(_("Logical error: Must find overlapping"))
self.from_time = get_datetime(overlapping.to_time) + get_mins_between_operations()
def get_time_log_summary(self):
"""Returns 'Actual Operating Time'. """
return frappe.db.sql("""select
sum(hours*60) as mins, sum(completed_qty) as completed_qty
from `tabTime Log`
where production_order = %s and operation_id = %s and docstatus=1""",
(self.production_order, self.operation_id), as_dict=1)[0]
def validate_manufacturing(self):
if self.for_manufacturing:
if not self.production_order:
frappe.throw(_("Production Order is Mandatory"))
if not self.completed_qty:
self.completed_qty = 0
production_order = frappe.get_doc("Production Order", self.production_order)
pending_qty = flt(production_order.qty) - flt(production_order.produced_qty)
if flt(self.completed_qty) > pending_qty:
frappe.throw(_("Completed Qty cannot be more than {0} for operation {1}").format(pending_qty, self.operation),
OverProductionLoggedError)
else:
self.production_order = None
self.operation = None
self.quantity = None
def update_cost(self):
rate = get_activity_cost(self.employee, self.activity_type)
if rate:
self.costing_rate = flt(rate.get('costing_rate'))
self.billing_rate = flt(rate.get('billing_rate'))
self.costing_amount = self.costing_rate * self.hours
if self.billable:
self.billing_amount = self.billing_rate * self.hours
else:
self.billing_amount = 0
if self.additional_cost and self.billable:
self.billing_amount += self.additional_cost
def update_task_and_project(self):
"""Update costing rate in Task or Project if either is set"""
if self.task:
task = frappe.get_doc("Task", self.task)
task.update_time_and_costing()
task.save()
elif self.project:
frappe.get_doc("Project", self.project).update_project()
def has_webform_permission(doc):
project_user = frappe.db.get_value("Project User", {"parent": doc.project, "user":frappe.session.user} , "user")
if project_user:
return True
@frappe.whitelist()
def get_events(start, end, filters=None):
"""Returns events for Gantt / Calendar view rendering.
:param start: Start date-time.
:param end: End date-time.
:param filters: Filters like workstation, project etc.
"""
from frappe.desk.calendar import get_event_conditions
conditions = get_event_conditions("Time Log", filters)
data = frappe.db.sql("""select name, from_time, to_time,
activity_type, task, project, production_order, workstation from `tabTime Log`
where docstatus < 2 and ( from_time between %(start)s and %(end)s or to_time between %(start)s and %(end)s )
{conditions}""".format(conditions=conditions), {
"start": start,
"end": end
}, as_dict=True, update={"allDay": 0})
for d in data:
d.title = d.name + ": " + (d.activity_type or d.production_order or "")
if d.task:
d.title += " for Task: " + d.task
if d.project:
d.title += " for Project: " + d.project
return data
@frappe.whitelist()
def get_activity_cost(employee=None, activity_type=None):
rate = frappe.db.get_values("Activity Cost", {"employee": employee,
"activity_type": activity_type}, ["costing_rate", "billing_rate"], as_dict=True)
if not rate:
rate = frappe.db.get_values("Activity Type", {"activity_type": activity_type},
["costing_rate", "billing_rate"], as_dict=True)
return rate[0] if rate else {}

View File

@ -1,29 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.views.calendar["Time Log"] = {
field_map: {
"start": "from_time",
"end": "to_time",
"id": "name",
"title": "title",
"allDay": "allDay"
},
gantt: true,
gantt_scale: "hours",
filters: [
{
"fieldtype": "Link",
"fieldname": "workstation",
"options": "Workstation",
"label": __("Workstation")
},
{
"fieldtype": "Link",
"fieldname": "employee",
"options": "Employee",
"label": __("Employee")
},
],
get_events_method: "erpnext.projects.doctype.time_log.time_log.get_events"
}

View File

@ -1,70 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// render
frappe.listview_settings['Time Log'] = {
add_fields: ["status", "billable", "activity_type", "task", "project", "hours", "for_manufacturing", "billing_amount"],
has_indicator_for_draft: true,
get_indicator: function(doc) {
if (doc.status== "Draft") {
return [__("Draft"), "red", "status,=,Draft"]
} else if (doc.status== "In Progress") {
return [__("In Progress"), "orange", "status,=,In Progress"]
} else if (doc.status== "Batched for Billing") {
return [__("Batched for Billing"), "darkgrey", "status,=,Batched for Billing"]
} else if (doc.status== "Billed") {
return [__("Billed"), "green", "status,=,Billed"]
} else if (doc.status== "Submitted" && doc.billable) {
return [__("Billable"), "orange", "billable,=,1"]
}
},
selectable: true,
onload: function(me) {
me.page.add_menu_item(__("Make Time Log Batch"), function() {
var selected = me.get_checked_items() || [];
if(!selected.length) {
msgprint(__("Please select Time Logs."));
return;
}
// select only billable time logs
for(var i in selected) {
var d = selected[i];
if(d.status=="Batched for Billing") {
msgprint(__("Time Log has been Batched for Billing") + ": " + d.name + " - " + d.title);
return;
}
if(d.status=="Billed") {
msgprint(__("Time Log has been Billed") + ": " + d.name + " - " + d.title);
return;
}
if(d.status!="Submitted") {
msgprint(__("Time Log Status must be Submitted.") + ": " + d.name + " - " + d.title);
return;
}
}
// make batch
frappe.model.with_doctype("Time Log Batch", function() {
var tlb = frappe.model.get_new_doc("Time Log Batch");
$.each(selected, function(i, d) {
var detail = frappe.model.get_new_doc("Time Log Batch Detail", tlb,
"time_logs");
$.extend(detail, {
"time_log": d.name,
"activity_type": d.activity_type,
"billing_amount": d.billing_amount,
"hours": d.hours
});
})
frappe.set_route("Form", "Time Log Batch", tlb.name);
})
}, "icon-file-alt");
}
};

View File

@ -1 +0,0 @@
Group of Time Logs that can be batched for billing.

View File

@ -1,37 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe, unittest
from erpnext.projects.doctype.time_log.test_time_log import make_time_log_test_record
class TimeLogBatchTest(unittest.TestCase):
def test_time_log_status(self):
time_log = make_time_log_test_record(simulate=True)
self.assertEquals(frappe.db.get_value("Time Log", time_log.name, "status"), "Submitted")
tlb = create_time_log_batch(time_log.name)
self.assertEquals(frappe.db.get_value("Time Log", time_log.name, "status"), "Batched for Billing")
tlb.cancel()
self.assertEquals(frappe.db.get_value("Time Log", time_log.name, "status"), "Submitted")
def create_time_log_batch(time_log):
tlb = frappe.get_doc({
"doctype": "Time Log Batch",
"time_logs": [
{
"doctype": "Time Log Batch Detail",
"parentfield": "time_logs",
"parenttype": "Time Log Batch",
"time_log": time_log
}
]
})
tlb.insert()
tlb.submit()
return tlb
test_ignore = ["Sales Invoice"]

View File

@ -1,67 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.add_fetch("time_log", "activity_type", "activity_type");
cur_frm.add_fetch("time_log", "billing_amount", "billing_amount");
cur_frm.add_fetch("time_log", "hours", "hours");
cur_frm.set_query("time_log", "time_logs", function(doc) {
return {
query: "erpnext.projects.utils.get_time_log_list",
filters: {
"billable": 1,
"status": "Submitted"
}
}
});
$.extend(cur_frm.cscript, {
refresh: function(doc) {
cur_frm.set_intro({
"Draft": __("Select Time Logs and Submit to create a new Sales Invoice."),
"Submitted": __("Click on 'Make Sales Invoice' button to create a new Sales Invoice."),
"Billed": __("This Time Log Batch has been billed."),
"Cancelled": __("This Time Log Batch has been cancelled.")
}[doc.status]);
if(doc.status=="Submitted") {
cur_frm.add_custom_button(__("Make Sales Invoice"), function() { cur_frm.cscript.make_invoice() },
"icon-file-alt");
}
},
make_invoice: function() {
frappe.model.open_mapped_doc({
method: "erpnext.projects.doctype.time_log_batch.time_log_batch.make_sales_invoice",
frm: cur_frm
});
}
});
frappe.ui.form.on("Time Log Batch Detail", "time_log", function(frm) {
calculate_time_and_amount(frm);
});
frappe.ui.form.on("Time Log Batch Detail", "time_logs_remove", function(frm) {
calculate_time_and_amount(frm);
});
frappe.ui.form.on("Time Log Batch", "onload", function(frm) {
if (frm.doc.__islocal && frm.doc.time_logs) {
calculate_time_and_amount(frm);
}
});
var calculate_time_and_amount = function(frm) {
var tl = frm.doc.time_logs || [];
total_hr = 0;
total_amt = 0;
for(var i=0; i<tl.length; i++) {
if (tl[i].time_log) {
total_hr += tl[i].hours;
total_amt += tl[i].billing_amount;
}
}
cur_frm.set_value("total_hours", total_hr);
cur_frm.set_value("total_billing_amount", total_amt);
}

View File

@ -1,305 +0,0 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "naming_series:",
"creation": "2013-02-28 17:57:33",
"custom": 0,
"description": "Batch Time Logs for Billing.",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "naming_series",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Series",
"length": 0,
"no_copy": 0,
"options": "TLB-",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Status",
"length": 0,
"no_copy": 1,
"options": "Draft\nSubmitted\nBilled\nCancelled",
"permlevel": 0,
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Will be updated after Sales Invoice is Submitted.",
"fieldname": "sales_invoice",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Sales Invoice",
"length": 0,
"no_copy": 0,
"options": "Sales Invoice",
"permlevel": 0,
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "time_logs",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Time Logs",
"length": 0,
"no_copy": 0,
"options": "Time Log Batch Detail",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "0",
"description": "updated via Time Logs",
"fieldname": "total_hours",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Total Hours",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_10",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "0",
"description": "updated via Time Logs",
"fieldname": "total_billing_amount",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Total Billing Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"in_filter": 0,
"in_list_view": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Time Log Batch",
"permlevel": 0,
"print_hide": 1,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-time",
"idx": 1,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2015-11-16 06:29:59.535984",
"modified_by": "Administrator",
"module": "Projects",
"name": "Time Log Batch",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"apply_user_permissions": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Projects User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
"read_only": 0,
"read_only_onload": 0
}

View File

@ -1,88 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
class TimeLogBatch(Document):
def validate(self):
self.set_status()
self.total_hours = 0.0
self.total_billing_amount = 0.0
for d in self.get("time_logs"):
tl = frappe.get_doc("Time Log", d.time_log)
self.update_time_log_values(d, tl)
self.validate_time_log_is_submitted(tl)
self.total_hours += flt(tl.hours)
self.total_billing_amount += flt(tl.billing_amount)
def update_time_log_values(self, d, tl):
d.update({
"hours": tl.hours,
"activity_type": tl.activity_type,
"billing_amount": tl.billing_amount,
"note": tl.note
})
def validate_time_log_is_submitted(self, tl):
if tl.status == "Batched for Billing":
frappe.throw(_("Time Log {0} already billed").format(tl.name))
elif tl.docstatus != 1:
frappe.throw(_("Time Log {0} must be 'Submitted'").format(tl.name))
def set_status(self):
self.status = {
"0": "Draft",
"1": "Submitted",
"2": "Cancelled"
}[str(self.docstatus or 0)]
if self.sales_invoice:
self.status = "Billed"
def on_submit(self):
self.update_status(self.name)
def before_cancel(self):
self.update_status(None)
def before_update_after_submit(self):
self.update_status(self.name)
def update_status(self, time_log_batch):
self.set_status()
for d in self.get("time_logs"):
tl = frappe.get_doc("Time Log", d.time_log)
tl.time_log_batch = time_log_batch
tl.sales_invoice = self.sales_invoice
tl.flags.ignore_validate_update_after_submit = True
tl.save()
@frappe.whitelist()
def make_sales_invoice(source_name, target=None):
def update_item(source_doc, target_doc, source_parent):
target_doc.stock_uom = "Hour"
target_doc.description = "via Time Logs"
target_doc.qty = 1
target = frappe.new_doc("Sales Invoice")
target.append("items", get_mapped_doc("Time Log Batch", source_name, {
"Time Log Batch": {
"doctype": "Sales Invoice Item",
"field_map": {
"total_billing_amount": "rate",
"name": "time_log_batch"
},
"postprocess": update_item
}
}))
return target

View File

@ -1,8 +0,0 @@
frappe.listview_settings['Time Log Batch'] = {
add_fields: ["status", "total_hours"],
get_indicator: function(doc) {
if (doc.status== "Billed") {
return [__("Billed"), "green", "status,=," + "Billed"]
}
}
};

View File

@ -1 +0,0 @@
Time Log detail for parent Time Log Batch.

View File

@ -1,142 +0,0 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"creation": "2013-03-05 09:11:06",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "time_log",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Time Log",
"length": 0,
"no_copy": 0,
"options": "Time Log",
"permlevel": 0,
"print_hide": 0,
"print_width": "200px",
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "200px"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "hours",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Hours",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "activity_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Activity Type",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "billing_amount",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Billing Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2015-11-16 06:29:59.590738",
"modified_by": "Administrator",
"module": "Projects",
"name": "Time Log Batch Detail",
"owner": "Administrator",
"permissions": [],
"read_only": 0,
"read_only_onload": 0
}

View File

@ -1,12 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class TimeLogBatchDetail(Document):
pass

View File

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
import datetime
from frappe.utils import now_datetime, nowdate
from erpnext.projects.doctype.time_sheet.time_sheet import OverlapError
from erpnext.projects.doctype.time_sheet.time_sheet import make_salary_slip, make_sales_invoice
class TestTimeSheet(unittest.TestCase):
def test_time_sheet_billing_amount(self):
salary_structure = make_salary_structure("_T-Employee-0001")
time_sheet = make_time_sheet("_T-Employee-0001", True)
self.assertEquals(time_sheet.total_hours, 2)
self.assertEquals(time_sheet.timesheets[0].billing_rate, 50)
self.assertEquals(time_sheet.timesheets[0].billing_amount, 100)
def test_salary_slip_from_timesheet(self):
salary_structure = make_salary_structure("_T-Employee-0001")
time_sheet = make_time_sheet("_T-Employee-0001", simulate = True)
salary_slip = make_salary_slip(time_sheet.name)
salary_slip.submit()
self.assertEquals(salary_slip.total_working_hours, 2)
self.assertEquals(salary_slip.hour_rate, 50)
self.assertEquals(salary_slip.net_pay, 150)
self.assertEquals(salary_slip.timesheets[0].time_sheet, time_sheet.name)
self.assertEquals(salary_slip.timesheets[0].working_hours, 2)
time_sheet = frappe.get_doc('Time Sheet', time_sheet.name)
self.assertEquals(time_sheet.status, 'Payslip')
salary_slip.cancel()
time_sheet = frappe.get_doc('Time Sheet', time_sheet.name)
self.assertEquals(time_sheet.status, 'Submitted')
def test_sales_invoice_from_timesheet(self):
time_sheet = make_time_sheet("_T-Employee-0001", simulate = True, billable = 1)
sales_invoice = make_sales_invoice(time_sheet.name)
sales_invoice.customer = "_Test Customer"
sales_invoice.due_date = nowdate()
item = sales_invoice.append('items', {})
item.item_code = '_Test Item'
item.qty = 2
item.rate = 100
sales_invoice.submit()
time_sheet = frappe.get_doc('Time Sheet', time_sheet.name)
self.assertEquals(sales_invoice.total_billing_amount, 100)
self.assertEquals(time_sheet.status, 'Billed')
def make_salary_structure(employee):
name = frappe.db.get_value('Salary Structure', {'employee': employee, 'salary_slip_based_on_timesheet': 1}, 'name')
if name:
frappe.delete_doc('Salary Structure', name)
salary_structure = frappe.new_doc("Salary Structure")
salary_structure.salary_slip_based_on_timesheet = 1
salary_structure.employee = employee
salary_structure.from_date = nowdate()
salary_structure.earning_type = "Basic"
salary_structure.hour_rate = 50.0
salary_structure.company= "_Test Company"
salary_structure.set('earnings', [])
salary_structure.set('deductions', [])
es = salary_structure.append('earnings', {
"e_type": "_Test Allowance",
"modified_value": 100
})
ds = salary_structure.append('deductions', {
"d_type": "_Test Professional Tax",
"d_modified_amt": 50
})
salary_structure.save(ignore_permissions=True)
return salary_structure
def make_time_sheet(employee, simulate=False, billable = 0):
update_activity_type("_Test Activity Type")
time_sheet = frappe.new_doc("Time Sheet")
time_sheet.employee = employee
time_sheet_detail = time_sheet.append('timesheets', {})
time_sheet_detail.billable = billable
time_sheet_detail.activity_type = "_Test Activity Type"
time_sheet_detail.from_time = now_datetime()
time_sheet_detail.hours = 2
time_sheet_detail.to_time = time_sheet_detail.from_time + datetime.timedelta(hours= time_sheet_detail.hours)
for data in time_sheet.get('timesheets'):
if simulate:
while True:
try:
time_sheet.save()
break
except OverlapError:
data.from_time = data.from_time + datetime.timedelta(minutes=10)
data.to_time = data.from_time + datetime.timedelta(hours= data.hours)
else:
time_sheet.save()
time_sheet.submit()
return time_sheet
def update_activity_type(activity_type):
activity_type = frappe.get_doc('Activity Type',activity_type)
activity_type.billing_rate = 50.0
activity_type.save(ignore_permissions=True)

View File

@ -0,0 +1,156 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.add_fetch("activity_type", "billing_rate", "billing_rate");
frappe.ui.form.on("Time Sheet", {
setup: function(frm) {
frm.get_field('timesheets').grid.editable_fields = [
{fieldname: 'activity_type', columns: 2},
{fieldname: 'from_time', columns: 2},
{fieldname: 'hours', columns: 2},
{fieldname: 'to_time', columns: 2},
];
frm.fields_dict.employee.get_query = function() {
return {
query:"erpnext.projects.doctype.time_sheet.time_sheet.get_employee_list"
}
}
frm.fields_dict['timesheets'].grid.get_field('task').get_query = function(frm, cdt, cdn) {
child = locals[cdt][cdn];
return{
filters: {
'project': child.project
}
}
}
},
onload: function(frm){
if (frm.doc.__islocal && frm.doc.timesheets) {
frm.set_value("employee", "")
calculate_time_and_amount(frm);
}
},
refresh: function(frm) {
if(frm.doc.docstatus==1) {
if(!frm.doc.sales_invoice && frm.doc.total_billing_amount > 0
&& frm.doc.employee){
frm.add_custom_button(__("Make Sales Invoice"), function() { frm.trigger("make_invoice") },
"icon-file-alt");
}
if(!frm.doc.salary_slip && frm.doc.employee){
frm.add_custom_button(__("Make Salary Slip"), function() { frm.trigger("make_salary_slip") },
"icon-file-alt");
}
}
},
make_invoice: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.projects.doctype.time_sheet.time_sheet.make_sales_invoice",
frm: frm
});
},
make_salary_slip: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.projects.doctype.time_sheet.time_sheet.make_salary_slip",
frm: frm
});
},
})
frappe.ui.form.on("Time Sheet Detail", {
timesheets_remove: function(frm) {
calculate_time_and_amount(frm);
},
from_time: function(frm, cdt, cdn) {
calculate_end_time(frm, cdt, cdn)
},
to_time: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if(frm._setting_hours) return;
if(flt(child.hours) == 0.0){
frappe.model.set_value(cdt, cdn, "hours", moment(child.to_time).diff(moment(child.from_time),
"seconds") / 3600);
}else{
var d = moment(child.to_time);
d.add(child.hours, "hours");
frappe.model.set_value(cdt, cdn, "from_time", d.format(moment.defaultDatetimeFormat));
}
},
hours: function(frm, cdt, cdn) {
calculate_end_time(frm, cdt, cdn)
},
billing_rate: function(frm, cdt, cdn) {
var child = locals[cdt][cdn]
if(child.hours && child.billing_rate){
frappe.mode.set_value(cdt, cdn, 'total_billing_amount', flt(child.billing_rate * child.hours))
}
calculate_billing_amount(frm, cdt, cdn)
},
costing_rate: function(frm, cdt, cdn) {
var child = locals[cdt][cdn]
frappe.mode.set_value(cdt, cdn, 'total_costing_amount', flt(child.costing_rate * child.hours))
calculate_billing_amount(frm, cdt, cdn)
},
billable: function(frm, cdt, cdn) {
calculate_billing_amount(frm, cdt, cdn)
}
});
calculate_end_time = function(frm, cdt, cdn){
var child = locals[cdt][cdn];
if(!child.from_time) {
frappe.model.set_value(cdt, cdn, "from_time", frappe.datetime.now_datetime());
}
var d = moment(child.from_time);
d.add(child.hours, "hours");
frm._setting_hours = true;
frappe.model.set_value(cdt, cdn, "to_time", d.format(moment.defaultDatetimeFormat));
frm._setting_hours = false;
calculate_billing_amount(frm, cdt, cdn)
}
var calculate_billing_amount = function(frm, cdt, cdn){
child = locals[cdt][cdn]
billing_amount = 0.0
if(child.hours && child.billable){
billing_amount = (child.hours * child.billing_rate)
}
frappe.model.set_value(cdt, cdn, 'billing_amount', billing_amount)
calculate_time_and_amount(frm)
}
var calculate_time_and_amount = function(frm) {
var tl = frm.doc.timesheets || [];
total_hr = 0;
total_billing_amount = 0;
for(var i=0; i<tl.length; i++) {
if (tl[i].hours) {
total_hr += tl[i].hours;
total_billing_amount += tl[i].billing_amount;
}
}
cur_frm.set_value("total_hours", total_hr);
cur_frm.set_value("total_billing_amount", total_billing_amount);
}

View File

@ -0,0 +1,272 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt, time_diff_in_hours, get_datetime, getdate, cint, get_datetime_str
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
class OverlapError(frappe.ValidationError): pass
class OverProductionLoggedError(frappe.ValidationError): pass
class TimeSheet(Document):
def validate(self):
self.set_status()
self.total_hours = 0.0
self.total_billing_amount = 0.0
self.validate_dates()
self.validate_timesheets()
self.update_cost()
for d in self.get("timesheets"):
self.total_hours += flt(d.hours)
if d.billable: self.total_billing_amount += flt(d.billing_amount)
def set_status(self):
self.status = {
"0": "Draft",
"1": "Submitted",
"2": "Cancelled"
}[str(self.docstatus or 0)]
if self.sales_invoice:
self.status = "Billed"
if self.salary_slip:
self.status = "Payslip"
if self.sales_invoice and self.salary_slip:
self.status = "Completed"
def set_dates(self):
if self.docstatus < 2:
start_date = min([d.from_time for d in self.timesheets])
end_date = max([d.to_time for d in self.timesheets])
if start_date and end_date:
self.start_date = getdate(start_date)
self.end_date = getdate(end_date)
def before_submit(self):
self.set_dates()
def before_cancel(self):
self.set_status()
def on_cancel(self):
self.update_production_order(None)
self.update_task_and_project()
def on_submit(self):
self.validate_mandatory_fields()
self.update_production_order(self.name)
self.update_task_and_project()
def validate_mandatory_fields(self):
if self.production_order:
production_order = frappe.get_doc("Production Order", self.production_order)
pending_qty = flt(production_order.qty) - flt(production_order.produced_qty)
for data in self.timesheets:
if not data.from_time and not data.to_time:
frappe.throw(_("Row {0}: From Time and To Time is mandatory.").format(data.idx))
if not data.activity_type and self.employee:
frappe.throw(_("Row {0}: Activity Type is mandatory.").format(data.idx))
if flt(data.hours) == 0.0:
frappe.throw(_("Row {0}: Hours value must be greater than zero.").format(data.idx))
if self.production_order and flt(data.completed_qty) == 0:
frappe.throw(_("Row {0}: Completed Qty must be greater than zero.").format(data.idx))
if self.production_order and flt(pending_qty) < flt(data.completed_qty):
frappe.throw(_("Row {0}: Completed Qty cannot be more than {0} for operation {1}").format(data.idx, pending_qty, self.operation),
OverProductionLoggedError)
def update_production_order(self, time_sheet):
if self.production_order:
pro = frappe.get_doc('Production Order', self.production_order)
for timesheet in self.timesheets:
for data in pro.operations:
if data.name == timesheet.operation_id:
summary = self.get_actual_timesheet_summary(timesheet.operation_id)
data.time_sheet = time_sheet
data.completed_qty = summary.completed_qty
data.actual_operation_time = summary.mins
data.actual_start_time = summary.from_time
data.actual_end_time = summary.to_time
pro.flags.ignore_validate_update_after_submit = True
pro.update_operation_status()
pro.calculate_operating_cost()
pro.set_actual_dates()
pro.save()
def get_actual_timesheet_summary(self, operation_id):
"""Returns 'Actual Operating Time'. """
return frappe.db.sql("""select
sum(tsd.hours*60) as mins, sum(tsd.completed_qty) as completed_qty, min(tsd.from_time) as from_time,
max(tsd.to_time) as to_time from `tabTime Sheet Detail` as tsd, `tabTime Sheet` as ts where
ts.production_order = %s and tsd.operation_id = %s and ts.docstatus=1 and ts.name = tsd.parent""",
(self.production_order, operation_id), as_dict=1)[0]
def update_task_and_project(self):
for data in self.timesheets:
if data.task:
task = frappe.get_doc("Task", data.task)
task.update_time_and_costing()
task.save()
elif data.project:
frappe.get_doc("Project", data.project).update_project()
def validate_dates(self):
for data in self.timesheets:
if time_diff_in_hours(data.to_time, data.from_time) < 0:
frappe.throw(_("To date cannot be before from date"))
def validate_timesheets(self):
for data in self.get('timesheets'):
self.validate_overlap(data)
self.check_workstation_timings(data)
def validate_overlap(self, data):
if self.production_order:
self.validate_overlap_for("workstation", data)
else:
self.validate_overlap_for("user", data)
self.validate_overlap_for("employee", data)
def validate_overlap_for(self, fieldname, args):
existing = self.get_overlap_for(fieldname, args)
if existing:
frappe.throw(_("Row {0}: From Time and To Time overlap with existing from and to time").format(args.idx),
OverlapError)
def get_overlap_for(self, fieldname, args):
if not args.get(fieldname):
return
existing = frappe.db.sql("""select ts.name as name, tsd.from_time as from_time, tsd.to_time as to_time from
`tabTime Sheet Detail` tsd, `tabTime Sheet` ts where tsd.`{0}`=%(val)s and tsd.parent = ts.name and
(
(%(from_time)s > tsd.from_time and %(from_time)s < tsd.to_time) or
(%(to_time)s > tsd.from_time and %(to_time)s < tsd.to_time) or
(%(from_time)s <= tsd.from_time and %(to_time)s >= tsd.to_time))
and tsd.name!=%(name)s
and ts.docstatus < 2""".format(fieldname),
{
"val": args.get(fieldname),
"from_time": args.from_time,
"to_time": args.to_time,
"name": args.name or "No Name"
}, as_dict=True)
return existing[0] if existing else None
def check_workstation_timings(self, args):
"""Checks if **Time Log** is between operating hours of the **Workstation**."""
if args.workstation and args.from_time and args.to_time:
from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours
check_if_within_operating_hours(args.workstation, args.operation, args.from_time, args.to_time)
def move_to_next_non_overlapping_slot(self, index):
from datetime import timedelta
if self.timesheets:
for data in self.timesheets:
if data.idx == index:
overlapping = self.get_overlap_for("workstation", data)
if not overlapping:
frappe.throw(_("Logical error: Must find overlapping"))
if overlapping:
time_sheet = self.get_last_working_slot(overlapping.name, data.workstation)
data.from_time = get_datetime(time_sheet.to_time) + get_mins_between_operations()
data.to_time = get_datetime(data.from_time) + timedelta(hours=data.hours)
break
def get_last_working_slot(self, time_sheet, workstation):
return frappe.db.sql(""" select max(from_time) as from_time, max(to_time) as to_time
from `tabTime Sheet Detail` where workstation = %(workstation)s""",
{'workstation': workstation}, as_dict=True)[0]
def update_cost(self):
for data in self.timesheets:
if data.activity_type and not data.billing_amount:
rate = get_activity_cost(self.employee, data.activity_type)
hours = data.hours or 0
if rate:
data.billing_rate = flt(rate.get('billing_rate'))
data.costing_rate = flt(rate.get('costing_rate'))
data.billing_amount = data.billing_rate * hours
data.costing_amount = data.costing_rate * hours
@frappe.whitelist()
def make_sales_invoice(source_name, target=None):
target = frappe.new_doc("Sales Invoice")
target.append("timesheets", get_mapped_doc("Time Sheet", source_name, {
"Time Sheet": {
"doctype": "Sales Invoice Timesheet",
"field_map": {
"total_billing_amount": "billing_amount",
"name": "time_sheet"
},
}
}))
target.run_method("calculate_billing_amount_from_timesheet")
return target
@frappe.whitelist()
def make_salary_slip(source_name, target_doc=None):
target = frappe.new_doc("Salary Slip")
set_missing_values(source_name, target)
target.append("timesheets", get_mapped_doc("Time Sheet", source_name, {
"Time Sheet": {
"doctype": "Salary Slip Timesheet",
"field_map": {
"total_hours": "working_hours",
"name": "time_sheet"
},
}
}))
target.run_method("get_emp_and_leave_details")
return target
def set_missing_values(time_sheet, target):
doc = frappe.get_doc('Time Sheet', time_sheet)
target.employee = doc.employee
target.employee_name = doc.employee_name
target.salary_slip_based_on_timesheet = 1
target.start_date = doc.start_date
target.end_date = doc.end_date
@frappe.whitelist()
def get_activity_cost(employee=None, activity_type=None):
rate = frappe.db.get_values("Activity Cost", {"employee": employee,
"activity_type": activity_type}, ["costing_rate", "billing_rate"], as_dict=True)
if not rate:
rate = frappe.db.get_values("Activity Type", {"activity_type": activity_type},
["costing_rate", "billing_rate"], as_dict=True)
return rate[0] if rate else {}
@frappe.whitelist()
def get_employee_list(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select distinct(employee) as employee
from `tabSalary Structure` where salary_slip_based_on_timesheet=1
and employee like %(txt)s limit %(start)s, %(page_len)s""",
{'txt': "%%%s%%"%(txt), 'start': start, 'page_len': page_len})

View File

@ -0,0 +1,16 @@
frappe.listview_settings['Time Sheet'] = {
add_fields: ["status", "total_hours"],
get_indicator: function(doc) {
if (doc.status== "Billed") {
return [__("Billed"), "green", "status,=," + "Billed"]
}
if (doc.status== "Payslip") {
return [__("Payslip"), "green", "status,=," + "Payslip"]
}
if (doc.status== "Completed") {
return [__("Completed"), "green", "status,=," + "Completed"]
}
}
};

View File

@ -0,0 +1,493 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-03-05 09:11:06",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "billable",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Billable",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "activity_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Activity Type",
"length": 0,
"no_copy": 0,
"options": "Activity Type",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "from_time",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "From Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "hours",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Hours",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "to_time",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "To Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "project",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Project",
"length": 0,
"no_copy": 0,
"options": "Project",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "project",
"fieldname": "task",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Task",
"length": 0,
"no_copy": 0,
"options": "Task",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:parent.production_order",
"fieldname": "workstation",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Workstation",
"length": 0,
"no_copy": 0,
"options": "Workstation",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:parent.production_order",
"fieldname": "operation",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Operation",
"length": 0,
"no_copy": 0,
"options": "Operation",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:parent.production_order",
"fieldname": "operation_id",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Operation Id",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "completed_qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Completed Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_11",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "billing_rate",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Billing Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "costing_rate",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Costing Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_14",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "0",
"depends_on": "",
"fieldname": "billing_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Billing Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "0",
"fieldname": "costing_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Costing Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-06-24 15:55:30.746870",
"modified_by": "Administrator",
"module": "Projects",
"name": "Time Sheet Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "ASC",
"track_seen": 0
}

View File

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

View File

@ -1,16 +0,0 @@
{
"apply_user_permissions": 1,
"creation": "2013-04-03 11:27:52",
"docstatus": 0,
"doctype": "Report",
"idx": 1,
"is_standard": "Yes",
"modified": "2014-06-03 07:18:17.019543",
"modified_by": "Administrator",
"module": "Projects",
"name": "Daily Time Log Summary",
"owner": "Administrator",
"ref_doctype": "Time Log",
"report_name": "Daily Time Log Summary",
"report_type": "Script Report"
}

View File

@ -1,98 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
def execute(filters=None):
if not filters:
filters = {}
elif filters.get("from_date") or filters.get("to_date"):
filters["from_time"] = "00:00:00"
filters["to_time"] = "24:00:00"
columns = [_("Time Log") + ":Link/Time Log:120", _("Employee") + "::150", _("From Datetime") + "::140",
_("To Datetime") + "::140", _("Hours") + "::70", _("Activity Type") + "::120", _("Task") + ":Link/Task:150",
_("Task Subject") + "::180", _("Project") + ":Link/Project:120", _("Status") + "::70"]
user_map = get_user_map()
employee_map = get_employee_map()
task_map = get_task_map()
conditions = build_conditions(filters)
time_logs = frappe.db.sql("""select * from `tabTime Log`
where docstatus < 2 %s order by owner asc""" % (conditions, ), filters, as_dict=1)
if time_logs:
users = [time_logs[0].owner]
data = []
total_hours = total_employee_hours = count = 0
for tl in time_logs:
if tl.employee:
employee=employee_map[tl.employee]
else:
employee=user_map[tl.owner]
if tl.owner not in users:
users.append(tl.owner)
data.append(["", "", "", "Total", total_employee_hours, "", "", "", "", ""])
total_employee_hours = 0
data.append([tl.name, employee, tl.from_time, tl.to_time, tl.hours,
tl.activity_type, tl.task, task_map.get(tl.task), tl.project, tl.status])
count += 1
total_hours += flt(tl.hours)
total_employee_hours += flt(tl.hours)
if count == len(time_logs):
data.append(["", "", "", "Total Hours", total_employee_hours, "", "", "", "", ""])
if total_hours:
data.append(["", "", "", "Grand Total", total_hours, "", "", "", "", ""])
return columns, data
def get_user_map():
users = frappe.db.sql("""select name,
concat(first_name, if(last_name, (' ' + last_name), '')) as fullname
from tabUser""", as_dict=1)
user_map = {}
for p in users:
user_map.setdefault(p.name, []).append(p.fullname)
return user_map
def get_employee_map():
employees = frappe.db.sql("""select name,
employee_name as fullname
from tabEmployee""", as_dict=1)
employee_map = {}
for p in employees:
employee_map.setdefault(p.name, []).append(p.fullname)
return employee_map
def get_task_map():
tasks = frappe.db.sql("""select name, subject from tabTask""", as_dict=1)
task_map = {}
for t in tasks:
task_map.setdefault(t.name, []).append(t.subject)
return task_map
def build_conditions(filters):
conditions = ""
if filters.get("from_date"):
conditions += " and from_time >= timestamp(%(from_date)s, %(from_time)s)"
if filters.get("to_date"):
conditions += " and to_time <= timestamp(%(to_date)s, %(to_time)s)"
from frappe.desk.reportview import build_match_conditions
match_conditions = build_match_conditions("Time Log")
if match_conditions:
conditions += " and %s" % match_conditions
return conditions

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Daily Time Log Summary"] = {
frappe.query_reports["Daily Time Sheet Summary"] = {
"filters": [
{
"fieldname":"from_date",

View File

@ -0,0 +1,18 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2016-06-25 01:52:25.911238",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2016-06-25 01:52:31.214122",
"modified_by": "Administrator",
"module": "Projects",
"name": "Daily Time Sheet Summary",
"owner": "Administrator",
"ref_doctype": "Time Sheet",
"report_name": "Daily Time Sheet Summary",
"report_type": "Script Report"
}

View File

@ -0,0 +1,34 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
if not filters:
filters = {}
elif filters.get("from_date") or filters.get("to_date"):
filters["from_time"] = "00:00:00"
filters["to_time"] = "24:00:00"
columns = [_("Time Sheet") + ":Link/Time Sheet:120", _("Employee") + "::150", _("From Datetime") + "::140",
_("To Datetime") + "::140", _("Hours") + "::70", _("Activity Type") + "::120", _("Task") + ":Link/Task:150",
_("Project") + ":Link/Project:120", _("Status") + "::70"]
conditions = "ts.docstatus = 1"
if filters.get("from_date"):
conditions += " and tsd.from_time >= timestamp(%(from_date)s, %(from_time)s)"
if filters.get("to_date"):
conditions += " and tsd.to_time <= timestamp(%(to_date)s, %(to_time)s)"
data = get_data(conditions, filters)
return columns, data
def get_data(conditions, filters):
time_sheet = frappe.db.sql(""" select ts.name, ts.employee, tsd.from_time, tsd.to_time, tsd.hours,
tsd.activity_type, tsd.task, tsd.project, ts.status from `tabTime Sheet Detail` tsd,
`tabTime Sheet` ts where ts.name = tsd.parent and %s order by ts.name"""%(conditions), filters, as_list=1)
return time_sheet

View File

@ -6,10 +6,6 @@
from __future__ import unicode_literals
import frappe
@frappe.whitelist()
def get_time_log_list(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.get_values("Time Log", filters, ["name", "activity_type", "owner"])
@frappe.whitelist()
def query_task(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import build_match_conditions

View File

@ -18,7 +18,7 @@ def delete_company_transactions(company_name):
frappe.PermissionError)
delete_bins(company_name)
delete_time_logs(company_name)
delete_time_sheets(company_name)
delete_lead_addresses(company_name)
for doctype in frappe.db.sql_list("""select parent from
@ -70,21 +70,12 @@ def delete_bins(company_name):
frappe.db.sql("""delete from tabBin where warehouse in
(select name from tabWarehouse where company=%s)""", company_name)
def delete_time_logs(company_name):
def delete_time_sheets(company_name):
# Delete Time Logs as it is linked to Production Order / Project / Task, which are linked to company
frappe.db.sql("""
delete from `tabTime Log`
delete from `tabTime Sheet`
where
(ifnull(project, '') != ''
and exists(select name from `tabProject` where name=`tabTime Log`.project and company=%(company)s))
or (ifnull(task, '') != ''
and exists(select name from `tabTask` where name=`tabTime Log`.task and company=%(company)s))
or (ifnull(production_order, '') != ''
and exists(select name from `tabProduction Order`
where name=`tabTime Log`.production_order and company=%(company)s))
or (ifnull(sales_invoice, '') != ''
and exists(select name from `tabSales Invoice`
where name=`tabTime Log`.sales_invoice and company=%(company)s))
company=%(company)s
""", {"company": company_name})
def delete_lead_addresses(company_name):

View File

@ -43,7 +43,7 @@ domains = {
},
'Services': {
'desktop_icons': ['Project', 'Time Log', 'Customer', 'Sales Order', 'Sales Invoice', 'Lead', 'Opportunity',
'desktop_icons': ['Project', 'Time Sheet', 'Customer', 'Sales Order', 'Sales Invoice', 'Lead', 'Opportunity',
'Expense Claim', 'Employee', 'HR', 'ToDo'],
'remove_roles': ['Manufacturing User', 'Manufacturing Manager'],
'properties': [

View File

@ -43,8 +43,6 @@ def get_notification_config():
"Purchase Receipt": {"docstatus": 0},
"Production Order": { "status": ("in", ("Draft", "Not Started", "In Process")) },
"BOM": {"docstatus": 0},
"Timesheet": {"docstatus": 0},
"Time Log": {"status": "Draft"},
"Time Log Batch": {"status": "Draft"}
"Time Sheet": {"status": "Draft"}
}
}

View File

@ -178,7 +178,7 @@ class StockEntry(StockController):
self.production_order = None
def check_if_operations_completed(self):
"""Check if Time Logs are completed against before manufacturing to capture operating costs."""
"""Check if Time Sheets are completed against before manufacturing to capture operating costs."""
prod_order = frappe.get_doc("Production Order", self.production_order)
for d in prod_order.get("operations"):

View File

@ -20,8 +20,9 @@ def get_context(context):
project.tasks = get_tasks(project.name, start=0, item_status='open',
search=frappe.form_dict.get("search"))
project.timelogs = get_timelogs(project.name, start=0,
search=frappe.form_dict.get("search"))
project.timelogs = []
# project.timelogs = get_timelogs(project.name, start=0,
# search=frappe.form_dict.get("search"))
context.doc = project