From fd065647f458f09b25164e7fb071466080a10fa9 Mon Sep 17 00:00:00 2001 From: Denis Leemann Date: Tue, 30 May 2017 14:25:37 +0200 Subject: [PATCH 1/5] Add sale_project_fixed_price_task_completed_invoicing --- README.md | 56 +++++++++++++++++++++++++++++++++++++++ __init__.py | 4 +++ __manifest__.py | 21 +++++++++++++++ models/__init__.py | 8 ++++++ models/procurement.py | 19 +++++++++++++ models/product.py | 25 +++++++++++++++++ models/project_task.py | 55 ++++++++++++++++++++++++++++++++++++++ models/sale_order.py | 25 +++++++++++++++++ models/sale_order_line.py | 35 ++++++++++++++++++++++++ views/project_views.xml | 24 +++++++++++++++++ 10 files changed, 272 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 models/__init__.py create mode 100644 models/procurement.py create mode 100644 models/product.py create mode 100644 models/project_task.py create mode 100644 models/sale_order.py create mode 100644 models/sale_order_line.py create mode 100644 views/project_views.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..176d13c --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +Sale Project Fixed Price Task Completed Invoicing +================================================= + +The main goal of this module is to add the possibility to link a sale.order.line +to a project.task considering the delivery. +Unless sale_timesheet, the quantity shipped won't be linked to a Timesheet nor +to the time spent on the task. The price is fixed on the sale.order.line and it +will be considered as shipped once the task is accomplished. + +Usage +===== + +Create a product with product.type 'Service' and track_service 'Completed Task'. + +Use it in a Sale Order. Once you validate the Sale Order, it will create a linked +project and task. + +Once the task is finished, on the form view of the corrisponding task click on the +button 'Invoiceable'. The linked sale.order.line will be considered as shipped. + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback. + + +Credits +======= + +Contributors +------------ + +* Denis Leemann + + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a77a6fc --- /dev/null +++ b/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..42f806f --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Sale project fixed price task completed invoicing", + "version": "10.0.1.0.0", + "depends": [ + 'product', + 'project', + 'sale', + 'sale_timesheet', + ], + "author": "Camptocamp,Odoo Community Association (OCA)", + "website": "http://www.camptocamp.com", + "license": "AGPL-3", + "category": "Sale", + "data": [ + 'views/project_views.xml', + ], + 'installable': True, +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..00edd76 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import product +from . import project_task +from . import sale_order +from . import sale_order_line +from . import procurement diff --git a/models/procurement.py b/models/procurement.py new file mode 100644 index 0000000..e83ad6e --- /dev/null +++ b/models/procurement.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class ProcurementOrder(models.Model): + _inherit = 'procurement.order' + + def _is_procurement_task(self): + return (self.product_id.type == 'service' and + self.product_id.track_service in ('task', 'completed_task')) + + def _create_service_task(self): + task = super(ProcurementOrder, self)._create_service_task() + if self.product_id.track_service == 'completed_task': + task.fixed_price = True + return task diff --git a/models/product.py b/models/product.py new file mode 100644 index 0000000..37d2ece --- /dev/null +++ b/models/product.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + track_service = fields.Selection(selection_add=[ + ('completed_task', 'Completed Task')] + ) + + +class ProductProduct(models.Model): + _inherit = 'product.product' + + @api.multi + def _need_procurement(self): + for product in self: + if (product.type == 'service' and + product.track_service == 'completed_task'): + return True + return super(ProductProduct, self)._need_procurement() diff --git a/models/project_task.py b/models/project_task.py new file mode 100644 index 0000000..e7524e2 --- /dev/null +++ b/models/project_task.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError, UserError + + +class ProjectTask(models.Model): + _inherit = 'project.task' + + invoiceable = fields.Boolean( + string='Invoiceable', + ) + fixed_price = fields.Boolean( + string='Fixed Price', + ) + + @api.multi + def toggle_invoiceable(self): + for task in self: + # We dont' want to modify when the related SOLine is invoiced + if (not task.sale_line_id or + task.sale_line_id.state in ('done', 'cancel')): + raise UserError(_("You cannot modify the status if there is " + "no Sale Order Line or if it has been " + "invoiced.")) + task.invoiceable = not task.invoiceable + task.sale_line_id._check_delivered_qty() + + @api.multi + def write(self, vals): + for task in self: + if (vals.get('sale_line_id') and + task.sale_line_id.state in ('done', 'cancel')): + raise ValidationError(_('You cannot modify the Sale Order ' + 'Line of the task once it is invoiced') + ) + return super(ProjectTask, self).write(vals) + + def create(self, vals): + SOLine = self.env['sale.order.line'] + so_line = SOLine.browse(vals.get('sale_line_id')) + # We don't want to add a project.task to an already invoiced line + if so_line and so_line.state in ('done', 'cancel'): + raise ValidationError(_('You cannot add a task to and invoiced ' + 'Sale Order Line')) + return super(ProjectTask, self).create(vals) + + @api.onchange('invoiceable') + def _onchange_invoiceable(self): + for task in self: + if not task.invoiceable: + continue + task.sale_line_id._check_delivered_qty() diff --git a/models/sale_order.py b/models/sale_order.py new file mode 100644 index 0000000..f1fa88c --- /dev/null +++ b/models/sale_order.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + @api.multi + def action_confirm(self): + res = super(SaleOrder, self).action_confirm() + for order in self: + if not order.project_project_id: + for line in order.order_line: + if (line.product_id.track_service in ('completed_task', + 'timesheet')): + if not order.project_id: + order._create_analytic_account( + prefix=line.product_id.default_code or None) + order.project_id.project_create( + {'name': order.project_id.name, + 'use_tasks': True}) + return res diff --git a/models/sale_order_line.py b/models/sale_order_line.py new file mode 100644 index 0000000..ad58d66 --- /dev/null +++ b/models/sale_order_line.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, models, _ +from odoo.exceptions import ValidationError + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + @api.model + def create(self, vals): + line = super(SaleOrderLine, self).create(vals) + if (line.state == 'sale' and not line.order_id.project_id and + line.product_id.track_service in ('completed_task', )): + line.order_id._create_analytic_account() + + @api.constrains('product_id') + def _onchange_product_id(self): + for line in self: + if ('completed_task' == line.product_id.track_service and + line.product_uom_qty != 1.0): + raise ValidationError(_("The quantity for 'Complete Task' " + "products must be exactly one")) + + @api.multi + def _check_delivered_qty(self): + for line in self: + tasks_count = self.env['project.task'].search_count( + [('sale_line_id', '=', line.id)]) + task_invoiced_count = self.env['project.task'].search_count( + [('sale_line_id', '=', line.id), ('invoiceable', '=', True)]) + if tasks_count == task_invoiced_count: + line.qty_delivered = 1.0 diff --git a/views/project_views.xml b/views/project_views.xml new file mode 100644 index 0000000..155d4ca --- /dev/null +++ b/views/project_views.xml @@ -0,0 +1,24 @@ + + + + + project.task.form.track + project.task + + + + + + + + +
+ +
+ +
+
+ +
From a85f4b6536e367e99a05f2ec4fe83c070ece8aab Mon Sep 17 00:00:00 2001 From: Denis Leemann Date: Wed, 31 May 2017 15:30:51 +0200 Subject: [PATCH 2/5] fix indentation --- models/product.py | 2 +- models/project_task.py | 4 ++-- models/sale_order_line.py | 4 ++-- tests/__init__.py | 0 4 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 tests/__init__.py diff --git a/models/product.py b/models/product.py index 37d2ece..2a6718b 100644 --- a/models/product.py +++ b/models/product.py @@ -20,6 +20,6 @@ class ProductProduct(models.Model): def _need_procurement(self): for product in self: if (product.type == 'service' and - product.track_service == 'completed_task'): + product.track_service == 'completed_task'): return True return super(ProductProduct, self)._need_procurement() diff --git a/models/project_task.py b/models/project_task.py index e7524e2..f9b4a53 100644 --- a/models/project_task.py +++ b/models/project_task.py @@ -21,7 +21,7 @@ def toggle_invoiceable(self): for task in self: # We dont' want to modify when the related SOLine is invoiced if (not task.sale_line_id or - task.sale_line_id.state in ('done', 'cancel')): + task.sale_line_id.state in ('done', 'cancel')): raise UserError(_("You cannot modify the status if there is " "no Sale Order Line or if it has been " "invoiced.")) @@ -32,7 +32,7 @@ def toggle_invoiceable(self): def write(self, vals): for task in self: if (vals.get('sale_line_id') and - task.sale_line_id.state in ('done', 'cancel')): + task.sale_line_id.state in ('done', 'cancel')): raise ValidationError(_('You cannot modify the Sale Order ' 'Line of the task once it is invoiced') ) diff --git a/models/sale_order_line.py b/models/sale_order_line.py index ad58d66..abfeac6 100644 --- a/models/sale_order_line.py +++ b/models/sale_order_line.py @@ -13,14 +13,14 @@ class SaleOrderLine(models.Model): def create(self, vals): line = super(SaleOrderLine, self).create(vals) if (line.state == 'sale' and not line.order_id.project_id and - line.product_id.track_service in ('completed_task', )): + line.product_id.track_service in ('completed_task', )): line.order_id._create_analytic_account() @api.constrains('product_id') def _onchange_product_id(self): for line in self: if ('completed_task' == line.product_id.track_service and - line.product_uom_qty != 1.0): + line.product_uom_qty != 1.0): raise ValidationError(_("The quantity for 'Complete Task' " "products must be exactly one")) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From cec424b402a9cc35170d7cb3be0225c1bd79cf0c Mon Sep 17 00:00:00 2001 From: Denis Leemann Date: Wed, 31 May 2017 15:57:43 +0200 Subject: [PATCH 3/5] Add tests --- models/project_task.py | 5 +- models/sale_order_line.py | 1 + tests/__init__.py | 2 + ...ct_fixed_price_task_completed_invoicing.py | 56 +++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/test_sale_project_fixed_price_task_completed_invoicing.py diff --git a/models/project_task.py b/models/project_task.py index f9b4a53..507bb04 100644 --- a/models/project_task.py +++ b/models/project_task.py @@ -21,7 +21,9 @@ def toggle_invoiceable(self): for task in self: # We dont' want to modify when the related SOLine is invoiced if (not task.sale_line_id or - task.sale_line_id.state in ('done', 'cancel')): + task.sale_line_id.state in ('done', 'cancel') or + task.sale_line_id.invoice_status in ('to invoice', + 'invoiced')): raise UserError(_("You cannot modify the status if there is " "no Sale Order Line or if it has been " "invoiced.")) @@ -38,6 +40,7 @@ def write(self, vals): ) return super(ProjectTask, self).write(vals) + @api.model def create(self, vals): SOLine = self.env['sale.order.line'] so_line = SOLine.browse(vals.get('sale_line_id')) diff --git a/models/sale_order_line.py b/models/sale_order_line.py index abfeac6..bc57524 100644 --- a/models/sale_order_line.py +++ b/models/sale_order_line.py @@ -15,6 +15,7 @@ def create(self, vals): if (line.state == 'sale' and not line.order_id.project_id and line.product_id.track_service in ('completed_task', )): line.order_id._create_analytic_account() + return line @api.constrains('product_id') def _onchange_product_id(self): diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..6524ec6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_sale_project_fixed_price_task_completed_invoicing diff --git a/tests/test_sale_project_fixed_price_task_completed_invoicing.py b/tests/test_sale_project_fixed_price_task_completed_invoicing.py new file mode 100644 index 0000000..f46d5ca --- /dev/null +++ b/tests/test_sale_project_fixed_price_task_completed_invoicing.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.addons.sale.tests.test_sale_common import TestSale +from odoo.exceptions import UserError # , ValidationError + + +class TestSaleProjectFixedPrice(TestSale): + + def test_sale_project_fixed_price(self): + # The product comes from 'sale_timesheet' the only modification to add + # is for the track_service + prod_task = self.env.ref('product.product_product_1') + prd_vals = { + 'track_service': 'completed_task', + } + prod_task.write(prd_vals) + so_vals = { + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, {'name': prod_task.name, + 'product_id': prod_task.id, + 'product_uom_qty': 1, + 'product_uom': prod_task.uom_id.id, + 'price_unit': prod_task.list_price})], + 'pricelist_id': self.env.ref('product.list0').id, + } + so = self.env['sale.order'].create(so_vals) + so.action_confirm() + + # check task creation + project = self.env.ref('sale_timesheet.project_GAP') + task = project.task_ids.filtered( + lambda t: t.name == '%s:%s' % (so.name, prod_task.name)) + self.assertTrue(task, 'Sale Service: task is not created') + self.assertEqual(task.partner_id, so.partner_id, + 'Sale Service: customer should be the same on task ' + 'and on SO') + self.assertTrue(task.fixed_price) + + # check Task validation. It should update the delivered quantity + line = so.order_line + self.assertFalse(line.product_uom_qty == line.qty_delivered, + 'Sale Service: line should be invoiced completely') + task.toggle_invoiceable() + self.assertTrue(task.invoiceable, 'The task should be invoiceable') + self.assertTrue(line.product_uom_qty == line.qty_delivered, + 'Sale Service: line should be invoiced completely') + + # Impossible to change task invoicable after validation of soline + so.action_invoice_create() + self.assertEqual(so.invoice_status, + 'invoiced', 'SO should be invoiced') + with self.assertRaises(UserError): + task.toggle_invoiceable() From 979cde6147eea790537b02b1bb7ad260ce48d424 Mon Sep 17 00:00:00 2001 From: Denis Leemann Date: Tue, 6 Jun 2017 13:30:39 +0200 Subject: [PATCH 4/5] Remove qty_delivered constraint & method _create_analytic_and_tasks for better reusability --- README.md | 8 ++++---- models/project_task.py | 10 +--------- models/sale_order.py | 23 ++++++++++++++--------- models/sale_order_line.py | 19 ++++--------------- 4 files changed, 23 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 176d13c..fc49df4 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ Sale Project Fixed Price Task Completed Invoicing The main goal of this module is to add the possibility to link a sale.order.line to a project.task considering the delivery. -Unless sale_timesheet, the quantity shipped won't be linked to a Timesheet nor -to the time spent on the task. The price is fixed on the sale.order.line and it -will be considered as shipped once the task is accomplished. +The difference with sale_timesheet is that the quantity shipped won't be linked +to a Timesheet nor to the time spent on the task. The price is fixed on the +sale.order.line and it will be considered as shipped once the task is accomplished. Usage ===== @@ -19,7 +19,7 @@ Create a product with product.type 'Service' and track_service 'Completed Task'. Use it in a Sale Order. Once you validate the Sale Order, it will create a linked project and task. -Once the task is finished, on the form view of the corrisponding task click on the +Once the task is finished, on the form view of the corresponding task click on the button 'Invoiceable'. The linked sale.order.line will be considered as shipped. diff --git a/models/project_task.py b/models/project_task.py index 507bb04..6274f4c 100644 --- a/models/project_task.py +++ b/models/project_task.py @@ -22,8 +22,7 @@ def toggle_invoiceable(self): # We dont' want to modify when the related SOLine is invoiced if (not task.sale_line_id or task.sale_line_id.state in ('done', 'cancel') or - task.sale_line_id.invoice_status in ('to invoice', - 'invoiced')): + task.sale_line_id.invoice_status in ('invoiced',)): raise UserError(_("You cannot modify the status if there is " "no Sale Order Line or if it has been " "invoiced.")) @@ -49,10 +48,3 @@ def create(self, vals): raise ValidationError(_('You cannot add a task to and invoiced ' 'Sale Order Line')) return super(ProjectTask, self).create(vals) - - @api.onchange('invoiceable') - def _onchange_invoiceable(self): - for task in self: - if not task.invoiceable: - continue - task.sale_line_id._check_delivered_qty() diff --git a/models/sale_order.py b/models/sale_order.py index f1fa88c..f641975 100644 --- a/models/sale_order.py +++ b/models/sale_order.py @@ -13,13 +13,18 @@ def action_confirm(self): res = super(SaleOrder, self).action_confirm() for order in self: if not order.project_project_id: - for line in order.order_line: - if (line.product_id.track_service in ('completed_task', - 'timesheet')): - if not order.project_id: - order._create_analytic_account( - prefix=line.product_id.default_code or None) - order.project_id.project_create( - {'name': order.project_id.name, - 'use_tasks': True}) + order._create_analytic_and_tasks() return res + + @api.multi + def _create_analytic_and_tasks(self): + for order in self: + for line in order.order_line: + if (line.product_id.track_service in ('completed_task', + 'timesheet')): + if not order.project_id: + order._create_analytic_account( + prefix=line.product_id.default_code or None) + order.project_id.project_create( + {'name': order.project_id.name, + 'use_tasks': True}) diff --git a/models/sale_order_line.py b/models/sale_order_line.py index bc57524..2f434c2 100644 --- a/models/sale_order_line.py +++ b/models/sale_order_line.py @@ -2,8 +2,7 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, models, _ -from odoo.exceptions import ValidationError +from odoo import api, models class SaleOrderLine(models.Model): @@ -17,20 +16,10 @@ def create(self, vals): line.order_id._create_analytic_account() return line - @api.constrains('product_id') - def _onchange_product_id(self): - for line in self: - if ('completed_task' == line.product_id.track_service and - line.product_uom_qty != 1.0): - raise ValidationError(_("The quantity for 'Complete Task' " - "products must be exactly one")) - @api.multi def _check_delivered_qty(self): for line in self: - tasks_count = self.env['project.task'].search_count( + tasks = self.env['project.task'].search( [('sale_line_id', '=', line.id)]) - task_invoiced_count = self.env['project.task'].search_count( - [('sale_line_id', '=', line.id), ('invoiceable', '=', True)]) - if tasks_count == task_invoiced_count: - line.qty_delivered = 1.0 + if len(tasks) == len(tasks.filtered('invoiceable')): + line.qty_delivered = line.product_uom_qty From 436f51ed00eb4486cd630dbbdfe3c56bf484f625 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Fri, 9 Mar 2018 14:59:17 +0100 Subject: [PATCH 5/5] Isolate sale_project_fixed_price_task_completed_invoicing module --- .../README.md | 0 .../__init__.py | 0 .../__manifest__.py | 0 .../models}/__init__.py | 0 .../models}/procurement.py | 0 .../models}/product.py | 0 .../models}/project_task.py | 0 .../models}/sale_order.py | 0 .../models}/sale_order_line.py | 0 .../tests}/__init__.py | 0 .../test_sale_project_fixed_price_task_completed_invoicing.py | 0 .../views}/project_views.xml | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename README.md => sale_project_fixed_price_task_completed_invoicing/README.md (100%) rename __init__.py => sale_project_fixed_price_task_completed_invoicing/__init__.py (100%) rename __manifest__.py => sale_project_fixed_price_task_completed_invoicing/__manifest__.py (100%) rename {models => sale_project_fixed_price_task_completed_invoicing/models}/__init__.py (100%) rename {models => sale_project_fixed_price_task_completed_invoicing/models}/procurement.py (100%) rename {models => sale_project_fixed_price_task_completed_invoicing/models}/product.py (100%) rename {models => sale_project_fixed_price_task_completed_invoicing/models}/project_task.py (100%) rename {models => sale_project_fixed_price_task_completed_invoicing/models}/sale_order.py (100%) rename {models => sale_project_fixed_price_task_completed_invoicing/models}/sale_order_line.py (100%) rename {tests => sale_project_fixed_price_task_completed_invoicing/tests}/__init__.py (100%) rename {tests => sale_project_fixed_price_task_completed_invoicing/tests}/test_sale_project_fixed_price_task_completed_invoicing.py (100%) rename {views => sale_project_fixed_price_task_completed_invoicing/views}/project_views.xml (100%) diff --git a/README.md b/sale_project_fixed_price_task_completed_invoicing/README.md similarity index 100% rename from README.md rename to sale_project_fixed_price_task_completed_invoicing/README.md diff --git a/__init__.py b/sale_project_fixed_price_task_completed_invoicing/__init__.py similarity index 100% rename from __init__.py rename to sale_project_fixed_price_task_completed_invoicing/__init__.py diff --git a/__manifest__.py b/sale_project_fixed_price_task_completed_invoicing/__manifest__.py similarity index 100% rename from __manifest__.py rename to sale_project_fixed_price_task_completed_invoicing/__manifest__.py diff --git a/models/__init__.py b/sale_project_fixed_price_task_completed_invoicing/models/__init__.py similarity index 100% rename from models/__init__.py rename to sale_project_fixed_price_task_completed_invoicing/models/__init__.py diff --git a/models/procurement.py b/sale_project_fixed_price_task_completed_invoicing/models/procurement.py similarity index 100% rename from models/procurement.py rename to sale_project_fixed_price_task_completed_invoicing/models/procurement.py diff --git a/models/product.py b/sale_project_fixed_price_task_completed_invoicing/models/product.py similarity index 100% rename from models/product.py rename to sale_project_fixed_price_task_completed_invoicing/models/product.py diff --git a/models/project_task.py b/sale_project_fixed_price_task_completed_invoicing/models/project_task.py similarity index 100% rename from models/project_task.py rename to sale_project_fixed_price_task_completed_invoicing/models/project_task.py diff --git a/models/sale_order.py b/sale_project_fixed_price_task_completed_invoicing/models/sale_order.py similarity index 100% rename from models/sale_order.py rename to sale_project_fixed_price_task_completed_invoicing/models/sale_order.py diff --git a/models/sale_order_line.py b/sale_project_fixed_price_task_completed_invoicing/models/sale_order_line.py similarity index 100% rename from models/sale_order_line.py rename to sale_project_fixed_price_task_completed_invoicing/models/sale_order_line.py diff --git a/tests/__init__.py b/sale_project_fixed_price_task_completed_invoicing/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to sale_project_fixed_price_task_completed_invoicing/tests/__init__.py diff --git a/tests/test_sale_project_fixed_price_task_completed_invoicing.py b/sale_project_fixed_price_task_completed_invoicing/tests/test_sale_project_fixed_price_task_completed_invoicing.py similarity index 100% rename from tests/test_sale_project_fixed_price_task_completed_invoicing.py rename to sale_project_fixed_price_task_completed_invoicing/tests/test_sale_project_fixed_price_task_completed_invoicing.py diff --git a/views/project_views.xml b/sale_project_fixed_price_task_completed_invoicing/views/project_views.xml similarity index 100% rename from views/project_views.xml rename to sale_project_fixed_price_task_completed_invoicing/views/project_views.xml