diff --git a/cor_custom/__pycache__/__init__.cpython-36.pyc b/cor_custom/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..a2ca7f5 Binary files /dev/null and b/cor_custom/__pycache__/__init__.cpython-36.pyc differ diff --git a/cor_custom/controllers/__pycache__/__init__.cpython-36.pyc b/cor_custom/controllers/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..a4434d0 Binary files /dev/null and b/cor_custom/controllers/__pycache__/__init__.cpython-36.pyc differ diff --git a/cor_custom/controllers/__pycache__/controllers.cpython-36.pyc b/cor_custom/controllers/__pycache__/controllers.cpython-36.pyc new file mode 100644 index 0000000..38c46b7 Binary files /dev/null and b/cor_custom/controllers/__pycache__/controllers.cpython-36.pyc differ diff --git a/cor_custom/models/__pycache__/__init__.cpython-36.pyc b/cor_custom/models/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..2651ed4 Binary files /dev/null and b/cor_custom/models/__pycache__/__init__.cpython-36.pyc differ diff --git a/cor_custom/models/__pycache__/crm_lead.cpython-36.pyc b/cor_custom/models/__pycache__/crm_lead.cpython-36.pyc new file mode 100644 index 0000000..87c5da1 Binary files /dev/null and b/cor_custom/models/__pycache__/crm_lead.cpython-36.pyc differ diff --git a/cor_custom/models/__pycache__/models.cpython-36.pyc b/cor_custom/models/__pycache__/models.cpython-36.pyc new file mode 100644 index 0000000..18206e2 Binary files /dev/null and b/cor_custom/models/__pycache__/models.cpython-36.pyc differ diff --git a/cor_custom/models/__pycache__/project.cpython-36.pyc b/cor_custom/models/__pycache__/project.cpython-36.pyc new file mode 100644 index 0000000..58c8290 Binary files /dev/null and b/cor_custom/models/__pycache__/project.cpython-36.pyc differ diff --git a/cor_custom/models/__pycache__/project_overview.cpython-36.pyc b/cor_custom/models/__pycache__/project_overview.cpython-36.pyc new file mode 100644 index 0000000..8831012 Binary files /dev/null and b/cor_custom/models/__pycache__/project_overview.cpython-36.pyc differ diff --git a/cor_custom/models/project_overview.py b/cor_custom/models/project_overview.py index e2fb142..f697d5f 100755 --- a/cor_custom/models/project_overview.py +++ b/cor_custom/models/project_overview.py @@ -18,14 +18,7 @@ class Project(models.Model): _inherit = 'project.project' - def _qweb_prepare_qcontext(self, view_id, domain): - values = super()._qweb_prepare_qcontext(view_id, domain) - projects = self.search(domain) - values.update(projects._plan_prepare_values()) - values['actions'] = projects._plan_prepare_actions(values) - - return values def _plan_prepare_values(self): currency = self.env.company.currency_id @@ -181,384 +174,6 @@ class Project(models.Model): values['timesheet_forecast_table'] = timesheet_forecast_table_rows return values - def _table_get_line_values(self, employees=None): - """ return the header and the rows informations of the table """ - if not self: - return False - - uom_hour = self.env.ref('uom.product_uom_hour') - company_uom = self.env.company.timesheet_encode_uom_id - is_uom_day = company_uom and company_uom == self.env.ref('uom.product_uom_day') - - # build SQL query and fetch raw data - query, query_params = self._table_rows_sql_query() - self.env.cr.execute(query, query_params) - raw_data = self.env.cr.dictfetchall() - rows_employee = self._table_rows_get_employee_lines(raw_data) - default_row_vals = self._table_row_default() - - empty_line_ids, empty_order_ids = self._table_get_empty_so_lines() - - # extract row labels - sale_line_ids = set() - sale_order_ids = set() - for key_tuple, row in rows_employee.items(): - if row[0]['sale_line_id']: - sale_line_ids.add(row[0]['sale_line_id']) - if row[0]['sale_order_id']: - sale_order_ids.add(row[0]['sale_order_id']) - - sale_orders = self.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids) - sale_order_lines = self.env['sale.order.line'].sudo().browse(sale_line_ids | empty_line_ids) - map_so_names = {so.id: so.name for so in sale_orders} - map_so_cancel = {so.id: so.state == 'cancel' for so in sale_orders} - map_sol = {sol.id: sol for sol in sale_order_lines} - map_sol_names = {sol.id: sol.name.split('\n')[0] if sol.name else _('No Sales Order Line') for sol in sale_order_lines} - map_sol_so = {sol.id: sol.order_id.id for sol in sale_order_lines} - - rows_sale_line = {} # (so, sol) -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - for sale_line_id in empty_line_ids: # add service SO line having no timesheet - sale_line_row_key = (map_sol_so.get(sale_line_id), sale_line_id) - sale_line = map_sol.get(sale_line_id) - is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False - rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line_id, _('No Sales Order Line')), 'res_id': sale_line_id, 'res_model': 'sale.order.line', 'type': 'sale_order_line', 'is_milestone': is_milestone}] + default_row_vals[:] - if not is_milestone: - rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0 - - rows_sale_line_all_data = {} - if not employees: - employees = self.env['hr.employee'].sudo().search(self.env['account.analytic.line']._domain_employee_id()) - for row_key, row_employee in rows_employee.items(): - sale_order_id, sale_line_id, employee_id = row_key - # sale line row - sale_line_row_key = (sale_order_id, sale_line_id) - if sale_line_row_key not in rows_sale_line: - sale_line = map_sol.get(sale_line_id, self.env['sale.order.line']) - is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False - rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line.id) if sale_line else _('No Sales Order Line'), 'res_id': sale_line_id, 'res_model': 'sale.order.line', 'type': 'sale_order_line', 'is_milestone': is_milestone}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - if not is_milestone: - rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0 - - if sale_line_row_key not in rows_sale_line_all_data: - rows_sale_line_all_data[sale_line_row_key] = [0] * len(row_employee) - for index in range(1, len(row_employee)): - if employee_id in employees.ids: - rows_sale_line[sale_line_row_key][index] += row_employee[index] - rows_sale_line_all_data[sale_line_row_key][index] += row_employee[index] - if not rows_sale_line[sale_line_row_key][0].get('is_milestone'): - rows_sale_line[sale_line_row_key][-1] = rows_sale_line[sale_line_row_key][-2] - rows_sale_line_all_data[sale_line_row_key][5] - else: - rows_sale_line[sale_line_row_key][-1] = 0 - - rows_sale_order = {} # so -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - for row_key, row_sale_line in rows_sale_line.items(): - sale_order_id = row_key[0] - # sale order row - if sale_order_id not in rows_sale_order: - rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), 'canceled': map_so_cancel.get(sale_order_id, False), 'res_id': sale_order_id, 'res_model': 'sale.order', 'type': 'sale_order'}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - - for index in range(1, len(row_sale_line)): - rows_sale_order[sale_order_id][index] += row_sale_line[index] - - # group rows SO, SOL and their related employee rows. - timesheet_forecast_table_rows = [] - for sale_order_id, sale_order_row in rows_sale_order.items(): - timesheet_forecast_table_rows.append(sale_order_row) - for sale_line_row_key, sale_line_row in rows_sale_line.items(): - if sale_order_id == sale_line_row_key[0]: - sale_order_row[0]['has_children'] = True - timesheet_forecast_table_rows.append(sale_line_row) - for employee_row_key, employee_row in rows_employee.items(): - if sale_order_id == employee_row_key[0] and sale_line_row_key[1] == employee_row_key[1] and employee_row_key[2] in employees.ids: - sale_line_row[0]['has_children'] = True - timesheet_forecast_table_rows.append(employee_row) - - if is_uom_day: - # convert all values from hours to days - for row in timesheet_forecast_table_rows: - for index in range(1, len(row)): - row[index] = round(uom_hour._compute_quantity(row[index], company_uom, raise_if_failure=False), 2) - # complete table data - return { - 'header': self._table_header(), - 'rows': timesheet_forecast_table_rows - } - def _table_header(self): - initial_date = fields.Date.from_string(fields.Date.today()) - ts_months = sorted([fields.Date.to_string(initial_date - relativedelta(months=i, day=1)) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 - - def _to_short_month_name(date): - month_index = fields.Date.from_string(date).month - return babel.dates.get_month_names('abbreviated', locale=get_lang(self.env).code)[month_index] - - header_names = [_('Sales Order'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')] - - result = [] - for name in header_names: - result.append({ - 'label': name, - 'tooltip': '', - }) - # add tooltip for reminaing - result[-1]['tooltip'] = _('What is still to deliver based on sold hours and hours already done. Equals to sold hours - done hours.') - return result - - def _table_row_default(self): - lenght = len(self._table_header()) - return [0.0] * (lenght - 1) # before, M1, M2, M3, Done, Sold, Remaining - - def _table_rows_sql_query(self): - initial_date = fields.Date.from_string(fields.Date.today()) - ts_months = sorted([fields.Date.to_string(initial_date - relativedelta(months=i, day=1)) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 - # build query - query = """ - SELECT - 'timesheet' AS type, - date_trunc('month', date)::date AS month_date, - E.id AS employee_id, - S.order_id AS sale_order_id, - A.so_line AS sale_line_id, - SUM(A.unit_amount) AS number_hours - FROM account_analytic_line A - JOIN hr_employee E ON E.id = A.employee_id - LEFT JOIN sale_order_line S ON S.id = A.so_line - WHERE A.project_id IS NOT NULL - AND A.project_id IN %s - AND A.date < %s - GROUP BY date_trunc('month', date)::date, S.order_id, A.so_line, E.id - """ - - last_ts_month = fields.Date.to_string(fields.Date.from_string(ts_months[-1]) + relativedelta(months=1)) - query_params = (tuple(self.ids), last_ts_month) - return query, query_params - - def _table_rows_get_employee_lines(self, data_from_db): - initial_date = fields.Date.today() - ts_months = sorted([initial_date - relativedelta(months=i, day=1) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 - default_row_vals = self._table_row_default() - - # extract employee names - employee_ids = set() - for data in data_from_db: - employee_ids.add(data['employee_id']) - map_empl_names = {empl.id: empl.name for empl in self.env['hr.employee'].sudo().browse(employee_ids)} - - # extract rows data for employee, sol and so rows - rows_employee = {} # (so, sol, employee) -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - for data in data_from_db: - sale_line_id = data['sale_line_id'] - sale_order_id = data['sale_order_id'] - # employee row - row_key = (data['sale_order_id'], sale_line_id, data['employee_id']) - if row_key not in rows_employee: - meta_vals = { - 'label': map_empl_names.get(row_key[2]), - 'sale_line_id': sale_line_id, - 'sale_order_id': sale_order_id, - 'res_id': row_key[2], - 'res_model': 'hr.employee', - 'type': 'hr_employee' - } - rows_employee[row_key] = [meta_vals] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - - index = False - if data['type'] == 'timesheet': - if data['month_date'] in ts_months: - index = ts_months.index(data['month_date']) + 2 - elif data['month_date'] < ts_months[0]: - index = 1 - rows_employee[row_key][index] += data['number_hours'] - rows_employee[row_key][5] += data['number_hours'] - return rows_employee - - def _table_get_empty_so_lines(self): - """ get the Sale Order Lines having no timesheet but having generated a task or a project """ - so_lines = self.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense and not sol.is_downpayment) - # include the service SO line of SO sharing the same project - sale_order = self.env['sale.order'].search([('project_id', 'in', self.ids)]) - return set(so_lines.ids) | set(sale_order.mapped('order_line').filtered(lambda sol: sol.is_service and not sol.is_expense).ids), set(so_lines.mapped('order_id').ids) | set(sale_order.ids) - - # -------------------------------------------------- - # Actions: Stat buttons, ... - # -------------------------------------------------- - - def _plan_prepare_actions(self, values): - actions = [] - if len(self) == 1: - task_order_line_ids = [] - # retrieve all the sale order line that we will need later below - if self.env.user.has_group('sales_team.group_sale_salesman') or self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): - task_order_line_ids = self.env['project.task'].read_group([('project_id', '=', self.id), ('sale_line_id', '!=', False)], ['sale_line_id'], ['sale_line_id']) - task_order_line_ids = [ol['sale_line_id'][0] for ol in task_order_line_ids] - - if self.env.user.has_group('sales_team.group_sale_salesman'): - if self.bill_type == 'customer_project' and self.allow_billable and not self.sale_order_id: - actions.append({ - 'label': _("Create a Sales Order"), - 'type': 'action', - 'action_id': 'sale_timesheet.project_project_action_multi_create_sale_order', - 'context': json.dumps({'active_id': self.id, 'active_model': 'project.project'}), - }) - if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): - to_invoice_amount = values['dashboard']['profit'].get('to_invoice', False) # plan project only takes services SO line with timesheet into account - - sale_order_ids = self.env['sale.order.line'].read_group([('id', 'in', task_order_line_ids)], ['order_id'], ['order_id']) - sale_order_ids = [s['order_id'][0] for s in sale_order_ids] - sale_order_ids = self.env['sale.order'].search_read([('id', 'in', sale_order_ids), ('invoice_status', '=', 'to invoice')], ['id']) - sale_order_ids = list(map(lambda x: x['id'], sale_order_ids)) - - if to_invoice_amount and sale_order_ids: - if len(sale_order_ids) == 1: - actions.append({ - 'label': _("Create Invoice"), - 'type': 'action', - 'action_id': 'sale.action_view_sale_advance_payment_inv', - 'context': json.dumps({'active_ids': sale_order_ids, 'active_model': 'project.project'}), - }) - else: - actions.append({ - 'label': _("Create Invoice"), - 'type': 'action', - 'action_id': 'sale_timesheet.project_project_action_multi_create_invoice', - 'context': json.dumps({'active_id': self.id, 'active_model': 'project.project'}), - }) - return actions - - def _plan_get_stat_button(self): - stat_buttons = [] - num_projects = len(self) - if num_projects == 1: - action_data = _to_action_data('project.project', res_id=self.id, - views=[[self.env.ref('project.edit_project').id, 'form']]) - else: - action_data = _to_action_data(action=self.env.ref('project.open_view_project_all_config').sudo(), - domain=[('id', 'in', self.ids)]) - - stat_buttons.append({ - 'name': _('Project') if num_projects == 1 else _('Projects'), - 'count': num_projects, - 'icon': 'fa fa-puzzle-piece', - 'action': action_data - }) - - # if only one project, add it in the context as default value - tasks_domain = [('project_id', 'in', self.ids)] - tasks_context = self.env.context.copy() - tasks_context.pop('search_default_name', False) - late_tasks_domain = [('project_id', 'in', self.ids), ('date_deadline', '<', fields.Date.to_string(fields.Date.today())), ('date_end', '=', False)] - overtime_tasks_domain = [('project_id', 'in', self.ids), ('overtime', '>', 0), ('planned_hours', '>', 0)] - - # filter out all the projects that have no tasks - task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id']) - task_projects_ids = [p['project_id'][0] for p in task_projects_ids] - - if len(task_projects_ids) == 1: - tasks_context = {**tasks_context, 'default_project_id': task_projects_ids[0]} - stat_buttons.append({ - 'name': _('Tasks'), - 'count': sum(self.mapped('task_count')), - 'icon': 'fa fa-tasks', - 'action': _to_action_data( - action=self.env.ref('project.action_view_task').sudo(), - domain=tasks_domain, - context=tasks_context - ) - }) - stat_buttons.append({ - 'name': [_("Tasks"), _("Late")], - 'count': self.env['project.task'].search_count(late_tasks_domain), - 'icon': 'fa fa-tasks', - 'action': _to_action_data( - action=self.env.ref('project.action_view_task').sudo(), - domain=late_tasks_domain, - context=tasks_context, - ), - }) - stat_buttons.append({ - 'name': [_("Tasks"), _("in Overtime")], - 'count': self.env['project.task'].search_count(overtime_tasks_domain), - 'icon': 'fa fa-tasks', - 'action': _to_action_data( - action=self.env.ref('project.action_view_task').sudo(), - domain=overtime_tasks_domain, - context=tasks_context, - ), - }) - - if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): - # read all the sale orders linked to the projects' tasks - task_so_ids = self.env['project.task'].search_read([ - ('project_id', 'in', self.ids), ('sale_order_id', '!=', False) - ], ['sale_order_id']) - task_so_ids = [o['sale_order_id'][0] for o in task_so_ids] - - sale_orders = self.mapped('sale_line_id.order_id') | self.env['sale.order'].browse(task_so_ids) - if sale_orders: - stat_buttons.append({ - 'name': _('Sales Orders'), - 'count': len(sale_orders), - 'icon': 'fa fa-dollar', - 'action': _to_action_data( - action=self.env.ref('sale.action_orders').sudo(), - domain=[('id', 'in', sale_orders.ids)], - context={'create': False, 'edit': False, 'delete': False} - ) - }) - - invoice_ids = self.env['sale.order'].search_read([('id', 'in', sale_orders.ids)], ['invoice_ids']) - invoice_ids = list(itertools.chain(*[i['invoice_ids'] for i in invoice_ids])) - invoice_ids = self.env['account.move'].search_read([('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')], ['id']) - invoice_ids = list(map(lambda x: x['id'], invoice_ids)) - - if invoice_ids: - stat_buttons.append({ - 'name': _('Invoices'), - 'count': len(invoice_ids), - 'icon': 'fa fa-pencil-square-o', - 'action': _to_action_data( - action=self.env.ref('account.action_move_out_invoice_type').sudo(), - domain=[('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')], - context={'create': False, 'delete': False} - ) - }) - - ts_tree = self.env.ref('hr_timesheet.hr_timesheet_line_tree') - ts_form = self.env.ref('hr_timesheet.hr_timesheet_line_form') - if self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'): - timesheet_label = [_('Days'), _('Recorded')] - else: - timesheet_label = [_('Hours'), _('Recorded')] - - stat_buttons.append({ - 'name': timesheet_label, - 'count': sum(self.mapped('total_timesheet_time')), - 'icon': 'fa fa-calendar', - 'action': _to_action_data( - 'account.analytic.line', - domain=[('project_id', 'in', self.ids)], - views=[(ts_tree.id, 'list'), (ts_form.id, 'form')], - ) - }) - - return stat_buttons -def _to_action_data(model=None, *, action=None, views=None, res_id=None, domain=None, context=None): - # pass in either action or (model, views) - if action: - assert model is None and views is None - act = clean_action(action.read()[0], env=action.env) - model = act['res_model'] - views = act['views'] - # FIXME: search-view-id, possibly help? - descr = { - 'data-model': model, - 'data-views': json.dumps(views), - } - if context is not None: # otherwise copy action's? - descr['data-context'] = json.dumps(context) - if res_id: - descr['data-res-id'] = res_id - elif domain: - descr['data-domain'] = json.dumps(domain) - return descr + diff --git a/cor_custom/views/hr_timesheet_templates.xml b/cor_custom/views/hr_timesheet_templates.xml index f02a7d7..c997bba 100755 --- a/cor_custom/views/hr_timesheet_templates.xml +++ b/cor_custom/views/hr_timesheet_templates.xml @@ -1,467 +1,21 @@ - - + Timesheet Plan qweb project.project + - - -
-
-
-
-
- - - -
-

Recorded Days and Profitability

-

Recorded Hours and Profitability

-
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Days recorded - Hours recorded - -
- - - - ( %) - - Billed on Timesheets -
- - - - ( %) - - Billed at a Fixed price -
- - - - ( %) - - No task found -
- - - - ( %) - - - - Non Billable Tasks - - -
- - - - ( %) - - - - Non Billable Timesheets - - -
- - - - ( %) - - Cancelled -
- - - - - Total
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Profitability -
- - - Invoiced -
- - - To invoice -
- - Other Revenues
- - - Timesheet costs -
- - - Other costs -
- - - Re-invoiced costs -
- - - - Total
-
-
-
-
- -
-

Time by people

-
- -
- -

There are no timesheets for now.

-
- -
- - - - - - - - - - - - - - - - - - - - - - - -
EmployeeHourly RateDays SpentHours Spent - -
- - - - - - - - - - - -
-
- - - - Billed on Timesheets - billable_time - - - Billed at a Fixed price - billable_fixed - - - No task found - non_billable_project - - - Non billable timesheets - non_billable_timesheet - - - Non billable tasks - non_billable - - - Cancelled - canceled - -
-
-
-
-
-
- -
-

Timesheets

-
- - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Timesheets

- -
- - - - - - - - Cancelled - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- -
-
-
-
-
+ + Hourly Rate + + + + + +
-