diff --git a/sale_project_fixed_price_task_completed_invoicing/README.md b/sale_project_fixed_price_task_completed_invoicing/README.md new file mode 100644 index 0000000..fc49df4 --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/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. +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 +===== + +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 corresponding 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/sale_project_fixed_price_task_completed_invoicing/__init__.py b/sale_project_fixed_price_task_completed_invoicing/__init__.py new file mode 100644 index 0000000..a77a6fc --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/__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/sale_project_fixed_price_task_completed_invoicing/__manifest__.py b/sale_project_fixed_price_task_completed_invoicing/__manifest__.py new file mode 100644 index 0000000..42f806f --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/__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/sale_project_fixed_price_task_completed_invoicing/models/__init__.py b/sale_project_fixed_price_task_completed_invoicing/models/__init__.py new file mode 100644 index 0000000..00edd76 --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/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/sale_project_fixed_price_task_completed_invoicing/models/procurement.py b/sale_project_fixed_price_task_completed_invoicing/models/procurement.py new file mode 100644 index 0000000..e83ad6e --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/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/sale_project_fixed_price_task_completed_invoicing/models/product.py b/sale_project_fixed_price_task_completed_invoicing/models/product.py new file mode 100644 index 0000000..2a6718b --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/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/sale_project_fixed_price_task_completed_invoicing/models/project_task.py b/sale_project_fixed_price_task_completed_invoicing/models/project_task.py new file mode 100644 index 0000000..6274f4c --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/models/project_task.py @@ -0,0 +1,50 @@ +# -*- 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') or + 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.")) + 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) + + @api.model + 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) diff --git a/sale_project_fixed_price_task_completed_invoicing/models/sale_order.py b/sale_project_fixed_price_task_completed_invoicing/models/sale_order.py new file mode 100644 index 0000000..f641975 --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/models/sale_order.py @@ -0,0 +1,30 @@ +# -*- 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: + 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/sale_project_fixed_price_task_completed_invoicing/models/sale_order_line.py b/sale_project_fixed_price_task_completed_invoicing/models/sale_order_line.py new file mode 100644 index 0000000..2f434c2 --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/models/sale_order_line.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 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() + return line + + @api.multi + def _check_delivered_qty(self): + for line in self: + tasks = self.env['project.task'].search( + [('sale_line_id', '=', line.id)]) + if len(tasks) == len(tasks.filtered('invoiceable')): + line.qty_delivered = line.product_uom_qty diff --git a/sale_project_fixed_price_task_completed_invoicing/tests/__init__.py b/sale_project_fixed_price_task_completed_invoicing/tests/__init__.py new file mode 100644 index 0000000..6524ec6 --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_sale_project_fixed_price_task_completed_invoicing diff --git a/sale_project_fixed_price_task_completed_invoicing/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 new file mode 100644 index 0000000..f46d5ca --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/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() diff --git a/sale_project_fixed_price_task_completed_invoicing/views/project_views.xml b/sale_project_fixed_price_task_completed_invoicing/views/project_views.xml new file mode 100644 index 0000000..155d4ca --- /dev/null +++ b/sale_project_fixed_price_task_completed_invoicing/views/project_views.xml @@ -0,0 +1,24 @@ + + + + + project.task.form.track + project.task + + + + + + + + +
+ +
+ +
+
+ +