From 73099f6e26623000080585716ca87d8aca1bbe49 Mon Sep 17 00:00:00 2001
From: Juliana <juliana@le-filament.com>
Date: Mon, 16 Aug 2021 14:57:28 +0200
Subject: [PATCH] [MIG] Migration 14.0

---
 __init__.py                                   |   1 -
 __manifest__.py                               |   2 +-
 models/__init__.py                            |   3 +-
 models/__pycache__/__init__.cpython-36.pyc    | Bin 301 -> 0 bytes
 .../product_template.cpython-36.pyc           | Bin 680 -> 0 bytes
 models/__pycache__/res_company.cpython-36.pyc | Bin 483 -> 0 bytes
 .../res_config_settings.cpython-36.pyc        | Bin 556 -> 0 bytes
 models/__pycache__/sale_order.cpython-36.pyc  | Bin 7121 -> 0 bytes
 models/project_overview.py                    | 161 ++++++++++++++++++
 models/sale_order.py                          |   7 +-
 10 files changed, 165 insertions(+), 9 deletions(-)
 delete mode 100644 models/__pycache__/__init__.cpython-36.pyc
 delete mode 100644 models/__pycache__/product_template.cpython-36.pyc
 delete mode 100644 models/__pycache__/res_company.cpython-36.pyc
 delete mode 100644 models/__pycache__/res_config_settings.cpython-36.pyc
 delete mode 100644 models/__pycache__/sale_order.cpython-36.pyc
 create mode 100644 models/project_overview.py

diff --git a/__init__.py b/__init__.py
index 09f29e1..db3c96a 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 d212f92..fea4a3c 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 a3fa0c8..eaa1c10 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
GIT binary patch
literal 0
HcmV?d00001

literal 301
zcmXr!<>hjpaXRiB0|Ucj1|-15z`)?Zz`#&!!oa|g!jQt4!;s4u#mER^GvzSkGDR_g
z*~~c%xy(__U^Yt*LoQ1c3nN1cYcPW*+e=0U1_n*WTU^D7IjQmaMJcI8nvA!&i&Bf@
zlk;;667woG8E*+gxOr)r>G8#>B_)}8>BX9iw*(4`@>5EaOX5pXa|?13OH%zbS#L2E
zfm~I@%)r1<!~!B%85kH=G8D0c*dXGUi+)CaZmNEMN`5|=Ff`InE-fy}&(%*%Ny*PE
z*3Zez%Z>-Tr=Tc5D>b=9KQ})mHK$lVK0Y%qvm`!Vub}c4hfQvNN@-529mtYmHU<U;
I9!3xZ0P+-15C8xG

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
GIT binary patch
literal 0
HcmV?d00001

literal 680
zcmXr!<>e}yc{=VVBLl-@1|-13z`)?Zz`#(f#lXOj!jQt4!;s4u#hA+!#gxk&#mvay
z&XB^C!ra1;!ko&O#nQ|i#hStr%%I8o5@d)b^DVaA{FKz3;#+KKnW;G`#kZIf3o?^I
zaxlz=Y(pwT6k`fQ6jKUg6mu#|7Awf?6qXd$6t)!h6pj?m6s}&5D7F;tU<OT|Tl@h<
z`6;EzB_XM~1v!Z&sZ|065COdsn22COQGQlxa*19^VsW-!No7H56sv1VVnOOHj-u4!
zlA_Gyl3N`4dC3`xdFiPkw?yNMQWI0+(~I&;3*w7Q64O)TGgFGIl%h4%p$38Y>N*PQ
zw(2?x2qDc_H%<0iY#;;k(m@8Mq~@fSq}~!JDN0PvjxWnB&P>Y8$t<b7#hRIyl3H<#
z2VzBPML}X-$}P5({M^LMyjv{E`304Jn(VhY;^XrYb5rBvZ*j%P=jNxB=788d@$rSF
zi8)Xij`+;HjMSpck|JgX28LT4zKMC2M)`TEx5Q!Y%E`>jPECRPV<kh87y|=@_~oOY
zk)NBYpP!PS4<-za^pi`AOY(E|6H`+1^NRIBn&XQTb5i4>#_2;MQXd+|@vtz~E2u1D
zWnf@n2c;5FWO6XFFmW-mFfuWM#r-tdZn1#uDPjWI4svV}C{^8J^#!F2uo{HJAS`jP
Rk8B`5w*#pz25A;y1^^;&$S(i@

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
GIT binary patch
literal 0
HcmV?d00001

literal 483
zcmXr!<>k6Bcs9<Ek%8ec0}^0iU|?`yU|=ZLVqjoMVMt-jVaR2SV$5ZVV#;NXVrFD;
zXGmd6VQyhaVNPYtVrgcMVohNQX3%7P2{J^J`4(GleoAUi@h!Hr%+#Ee;#<s#1)0ep
zIT(i9AjiPKkjfCnn8Fanl)@OroXV2L3Nkx|rI#s+Erm6hL6hwkyK{bSL1JEI6?ai;
zv0gHS6Y|oVfq_Aj=@v&(YGG++QEJL9_LS7L#L}D+KTY;q9P#maiMgrq@wd3*<8$*<
zN^?MLp7{8}(!?C73`cxsUPfwBW=Rn<0|Ub?cF(+$)b!M%TRbI+r4{iR`9+DDMX4(p
ziuf5AAjB_s{fzwFRQ>#v{CqHBXr!N9T3nK!tDl&XlAl+spOcxF9bcT7lNw)8l%JKF
zT%r#N3jLzg;&`ab^$IGBSQ!`?*g!$d!N9=4!N|hI!pOu37WLC)yTt;spoj@%Ey!g>
ipy0a2>I(`~uo{GmAS?lp^EqrF-n0X$E(U29VFCa@ntO@>

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
GIT binary patch
literal 0
HcmV?d00001

literal 556
zcmXr!<>k6Bcs8zvk%8ec0}^0iU|?`yU|=ZLVqjoMVMt-jVaR2SV$5ZVV#;NXVrFD;
zXGmd6VQyhaVNPYtVrgcMVohNQX3%7P2{J^J`4(GleoAUi@h!Hr%+#Ee;#<s#1)0ep
zIT&Vz*`UC{z>vxi#hAhn#gxJr#hl8L#R@Vzg(ZcxmpO_pg)Nvtll_)pP-?MreqLH;
zdT?q<NoHPpag}gUYO!82SWK@NCdd<#SX!ZwkzbUUS(I8Oo}8askeF8)pP8apl2}?1
z50!A!WV*#(l$w)RlA3ahqbM~oB|k5x(od897Ds%1USe))eEco0`1suXl+qj!n<qZL
zurx6TD#H<<nU|4Tlvz^5%)r2Ki`_G?BsD#?=oSyM6)PEv_!t-<#4msSjQreG{rr^t
zd@x~Xq@P?`T#}!wpO})8pI5A(lbM$tU!0he8edS9pOu<iq7Ml`{i4+3c!(e4VSdyr
zs4M~n9~&rwxEL51I2c)&SQwcYnHa(1ewu8zSU^@4F@YQaa$^xFLT~Yf6eZ>rXQt+r
e_<~{wtQg@)2#X)&BMuvg&+S0kib1A`FaZGUgP2<Y

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
GIT binary patch
literal 0
HcmV?d00001

literal 7121
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
Y-r}%<jL+DCTwDxl6N@nNFbmlM0EAL5&Hw-a

diff --git a/models/project_overview.py b/models/project_overview.py
new file mode 100644
index 0000000..fe1dcde
--- /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 24323b3..8c78f1b 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
-- 
GitLab