diff --git a/__init__.py b/__init__.py index 09f29e14370809193737242f352f70c82640b943..db3c96a20d0d825c802d0635aabb49823593e8b8 100644 --- a/__init__.py +++ b/__init__.py @@ -2,4 +2,3 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import models -from . import controllers diff --git a/__manifest__.py b/__manifest__.py index d212f929fbb69051930f9467a743515b48be2a60..fea4a3ce315df71933d95c388be815516d382654 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -3,7 +3,7 @@ 'summary': """Filament - Lien entre commandes et projets""", 'author': "Le Filament", 'website': "https://www.le-filament.com", - 'version': '12.0.1.0.1', + 'version': '13.0.1.0.1', 'license': "AGPL-3", 'category': 'Sale Management', 'depends': ['sale_timesheet', 'project'], diff --git a/models/__init__.py b/models/__init__.py index a3fa0c885925e7c0806469a9bd665d66775ee386..eaa1c10c07b7acadcdd8d69f4618975156fc1f54 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,7 +1,8 @@ # Copyright 2019 Le Filament (<http://www.le-filament.com>) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from . import sale_order from . import res_company from . import res_config_settings from . import product_template +from . import project_overview +from . import sale_order diff --git a/models/__pycache__/__init__.cpython-36.pyc b/models/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index f3dd9c2928ea769333c10a98dcae3dab63e3dfc6..0000000000000000000000000000000000000000 Binary files a/models/__pycache__/__init__.cpython-36.pyc and /dev/null differ diff --git a/models/__pycache__/product_template.cpython-36.pyc b/models/__pycache__/product_template.cpython-36.pyc deleted file mode 100644 index b540924c439385ccaf2b7d8c3adedcaa94b79c0f..0000000000000000000000000000000000000000 Binary files a/models/__pycache__/product_template.cpython-36.pyc and /dev/null differ diff --git a/models/__pycache__/res_company.cpython-36.pyc b/models/__pycache__/res_company.cpython-36.pyc deleted file mode 100644 index 11f9a18f35dc764f26ec0cb4c2b3d96049554cc9..0000000000000000000000000000000000000000 Binary files a/models/__pycache__/res_company.cpython-36.pyc and /dev/null differ diff --git a/models/__pycache__/res_config_settings.cpython-36.pyc b/models/__pycache__/res_config_settings.cpython-36.pyc deleted file mode 100644 index 4c248db163efc306fe2c0f5125066e7b4d418b4b..0000000000000000000000000000000000000000 Binary files a/models/__pycache__/res_config_settings.cpython-36.pyc and /dev/null differ diff --git a/models/__pycache__/sale_order.cpython-36.pyc b/models/__pycache__/sale_order.cpython-36.pyc deleted file mode 100644 index d030919e351b2b5206f75ccee555db1128fc88cf..0000000000000000000000000000000000000000 Binary files a/models/__pycache__/sale_order.cpython-36.pyc and /dev/null differ diff --git a/models/project_overview.py b/models/project_overview.py new file mode 100644 index 0000000000000000000000000000000000000000..fe1dcde48a72c36021b7e8ee61afa809cb42fbaa --- /dev/null +++ b/models/project_overview.py @@ -0,0 +1,161 @@ +# Copyright 2021 Le Filament (<http://www.le-filament.com>) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models, api, _ + + +class Project(models.Model): + _inherit = 'project.project' + + # ------------------------------------------------------ + # Fields declaration + # ------------------------------------------------------ + + # ------------------------------------------------------ + # SQL Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Default methods + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Computed fields / Search Fields + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Onchange / Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # CRUD methods (ORM overrides) + # ------------------------------------------------------ + def _table_get_line_values(self): + """ return the header and the rows informations of the table """ + if not self: + return False + + uom_hour = self.env.ref('uom.product_uom_hour') + + # 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: + # ***** Modif Filament ***** + rows_sale_line[sale_line_row_key][ + -2] = sale_line.product_uom_qty * sale_line.price_unit / sale_line.order_id.taux_horaire if sale_line else 0.0 + # 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 + + for row_key, row_employee in rows_employee.items(): + sale_line_id = row_key[1] + sale_order_id = row_key[0] + # 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: + # ***** Modif Filament ***** + rows_sale_line[sale_line_row_key][ + -2] = sale_line.product_uom_qty * sale_line.price_unit / sale_line.order_id.taux_horaire if sale_line else 0.0 + # 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 + + for index in range(len(rows_employee[row_key])): + if index != 0: + rows_sale_line[sale_line_row_key][index] += rows_employee[row_key][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[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] + rows_sale_order_done_sold = {key: dict(sold=0.0, done=0.0) for key in + set(map_sol_so.values()) | set([None])} # SO id -> {'sold':0.0, 'done': 0.0} + 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(len(rows_sale_line[row_key])): + if index != 0: + rows_sale_order[sale_order_id][index] += rows_sale_line[row_key][index] + + # do not sum the milestone SO line for sold and done (for remaining computation) + if not rows_sale_line[row_key][0].get('is_milestone'): + rows_sale_order_done_sold[sale_order_id]['sold'] += rows_sale_line[row_key][-2] + rows_sale_order_done_sold[sale_order_id]['done'] += rows_sale_line[row_key][5] + + # remaining computation of SO row, as Sold - Done (timesheet total) + for sale_order_id, done_sold_vals in rows_sale_order_done_sold.items(): + if sale_order_id in rows_sale_order: + rows_sale_order[sale_order_id][-1] = done_sold_vals['sold'] - done_sold_vals['done'] + + # 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]: + 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]: + timesheet_forecast_table_rows.append(employee_row) + + # complete table data + return { + 'header': self._table_header(), + 'rows': timesheet_forecast_table_rows + } + # ------------------------------------------------------ + # Actions + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Business methods + # ------------------------------------------------------ diff --git a/models/sale_order.py b/models/sale_order.py index 24323b35404a1ea5a542d4be44e80ca7460b7240..8c78f1bcff65e8c98cbea8c521659fbfb7e9c8b9 100644 --- a/models/sale_order.py +++ b/models/sale_order.py @@ -21,7 +21,7 @@ class SaleOrder(models.Model): taux_horaire = fields.Integer( 'Taux horaire', - default=lambda self: self.env.user.company_id.taux_horaire) + default=lambda self: self.env.company.taux_horaire) @api.onchange("partner_id", "order_line") def _project_name_to_create(self): @@ -49,7 +49,6 @@ class SaleOrder(models.Model): if so_line_new_project_with_tasks and self.partner_id: self.project_name_to_create = self.partner_id.name + str(' - ') - @api.multi def action_confirm(self): # on différencie so_line_new_project (dans _timesheet_service_generation) de so_line_new_project_with_tasks # car on laisse le fonctionnement natif pour les articles où on crée le projet sans les tâches @@ -74,7 +73,6 @@ class SaleOrder(models.Model): class SaleOrderLine(models.Model): _inherit = "sale.order.line" - @api.multi def _convert_qty_company_hours(self): """ Reprise de la fonction native pour changer le mode de calcul des heures planifiées dans timesheet """ @@ -87,7 +85,6 @@ class SaleOrderLine(models.Model): planned_hours = (self.product_uom_qty * self.price_unit) / taux_horaire return planned_hours - @api.multi def _timesheet_create_task(self, project): """ Pour gérer le stage_id et le nom des tâches pour les projets maintenance et support """ @@ -103,7 +100,6 @@ class SaleOrderLine(models.Model): task.write({'name': client_name}) return task - @api.multi def _timesheet_create_project(self, name_project): """ Genère le projet de la même manière mais lui donne le nom choisi """ @@ -111,7 +107,6 @@ class SaleOrderLine(models.Model): project.name = name_project # on réécrit le nom du projet return project - @api.multi def _timesheet_service_generation(self): """ Réécriture de la fonction native de manière quasi-identique mais qui permet d'associer chaque ligne du devis à un projet