diff --git a/project_sale_usability/README.rst b/project_sale_usability/README.rst new file mode 100644 index 00000000..89ba4999 --- /dev/null +++ b/project_sale_usability/README.rst @@ -0,0 +1,28 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +====================== +Project Sale Usability +====================== + +With this module you can access to customer invoices related to the project. + +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 smash it by providing detailed and welcomed feedback. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Contributors +------------ + +* Oihane Crucelaegui +* Ana Juaristi diff --git a/project_sale_usability/__init__.py b/project_sale_usability/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/project_sale_usability/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_sale_usability/__manifest__.py b/project_sale_usability/__manifest__.py new file mode 100644 index 00000000..68c246e5 --- /dev/null +++ b/project_sale_usability/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2020 Oihane Crucelaegui - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +{ + "name": "Project Sale Usability", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "depends": [ + "project", + "sale", + "sales_team", + "sale_project", + ], + "author": "AvanzOSC", + "website": "https://github.com/avanzosc/project-addons", + "category": "Hidden", + "data": [ + "views/project_project_view.xml", + ], + "installable": True, +} diff --git a/project_sale_usability/i18n/es.po b/project_sale_usability/i18n/es.po new file mode 100644 index 00000000..4a68ea9e --- /dev/null +++ b/project_sale_usability/i18n/es.po @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_sale_usability +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-09 12:11+0000\n" +"PO-Revision-Date: 2023-10-09 12:11+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: project_sale_usability +#: model:ir.model.fields,field_description:project_sale_usability.field_project_project__out_invoice_count +msgid "# Sale Invoice" +msgstr "N.º de facturas de venta" + +#. module: project_sale_usability +#: model:ir.model.fields,field_description:project_sale_usability.field_project_project__out_refund_count +msgid "# Sale Refund" +msgstr "N.º de rectificativas de venta" + +#. module: project_sale_usability +#. odoo-python +#: code:addons/project_sale_usability/models/project_project.py:0 +#: model_terms:ir.ui.view,arch_db:project_sale_usability.project_project_view_form +#, python-format +msgid "Out Invoice Lines" +msgstr "Lineas de facturas de venta" + +#. module: project_sale_usability +#: model:ir.model.fields,field_description:project_sale_usability.field_project_project__out_invoiced +msgid "Out Invoiced" +msgstr "Facturado en ventas" + +#. module: project_sale_usability +#: model_terms:ir.ui.view,arch_db:project_sale_usability.project_project_view_form +msgid "Out Invoices" +msgstr "Facturas de venta" + +#. module: project_sale_usability +#: model:ir.model.fields,field_description:project_sale_usability.field_project_project__out_refund +#: model_terms:ir.ui.view,arch_db:project_sale_usability.project_project_view_form +msgid "Out Refund" +msgstr "Rectificado en venta" + +#. module: project_sale_usability +#. odoo-python +#: code:addons/project_sale_usability/models/project_project.py:0 +#: model_terms:ir.ui.view,arch_db:project_sale_usability.project_project_view_form +#, python-format +msgid "Out Refund Lines" +msgstr "Lineas de rectificadas de venta" + +#. module: project_sale_usability +#: model:ir.model,name:project_sale_usability.model_project_project +msgid "Project" +msgstr "Proyecto" diff --git a/project_sale_usability/i18n/project_sale_usability.pot b/project_sale_usability/i18n/project_sale_usability.pot new file mode 100644 index 00000000..9dba2984 --- /dev/null +++ b/project_sale_usability/i18n/project_sale_usability.pot @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_sale_usability +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-09 12:10+0000\n" +"PO-Revision-Date: 2023-10-09 12:10+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: project_sale_usability +#: model:ir.model.fields,field_description:project_sale_usability.field_project_project__out_invoice_count +msgid "# Sale Invoice" +msgstr "" + +#. module: project_sale_usability +#: model:ir.model.fields,field_description:project_sale_usability.field_project_project__out_refund_count +msgid "# Sale Refund" +msgstr "" + +#. module: project_sale_usability +#. odoo-python +#: code:addons/project_sale_usability/models/project_project.py:0 +#: model_terms:ir.ui.view,arch_db:project_sale_usability.project_project_view_form +#, python-format +msgid "Out Invoice Lines" +msgstr "" + +#. module: project_sale_usability +#: model:ir.model.fields,field_description:project_sale_usability.field_project_project__out_invoiced +msgid "Out Invoiced" +msgstr "" + +#. module: project_sale_usability +#: model_terms:ir.ui.view,arch_db:project_sale_usability.project_project_view_form +msgid "Out Invoices" +msgstr "" + +#. module: project_sale_usability +#: model:ir.model.fields,field_description:project_sale_usability.field_project_project__out_refund +#: model_terms:ir.ui.view,arch_db:project_sale_usability.project_project_view_form +msgid "Out Refund" +msgstr "" + +#. module: project_sale_usability +#. odoo-python +#: code:addons/project_sale_usability/models/project_project.py:0 +#: model_terms:ir.ui.view,arch_db:project_sale_usability.project_project_view_form +#, python-format +msgid "Out Refund Lines" +msgstr "" + +#. module: project_sale_usability +#: model:ir.model,name:project_sale_usability.model_project_project +msgid "Project" +msgstr "" diff --git a/project_sale_usability/models/__init__.py b/project_sale_usability/models/__init__.py new file mode 100644 index 00000000..56545d0d --- /dev/null +++ b/project_sale_usability/models/__init__.py @@ -0,0 +1 @@ +from . import project_project diff --git a/project_sale_usability/models/project_project.py b/project_sale_usability/models/project_project.py new file mode 100644 index 00000000..74baa16a --- /dev/null +++ b/project_sale_usability/models/project_project.py @@ -0,0 +1,144 @@ +# Copyright 2020 Oihane Crucelaegui - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import _, fields, models +from odoo.osv import expression +from odoo.tools.safe_eval import safe_eval + + +class ProjectProject(models.Model): + _inherit = "project.project" + + out_invoice_count = fields.Integer( + compute="_compute_out_invoiced", string="# Sale Invoice" + ) + out_invoiced = fields.Monetary(compute="_compute_out_invoiced") + out_refund_count = fields.Integer( + compute="_compute_out_refund", string="# Sale Refund" + ) + out_refund = fields.Monetary(compute="_compute_out_refund") + + def _domain_sale_invoice_line(self): + query = self.env["account.move.line"]._search( + [ + ("move_id.state", "!=", "cancel"), + ("move_id.move_type", "in", ["out_invoice", "out_refund"]), + ] + ) + # check if analytic_distribution contains id of analytic account + query.add_where( + "account_move_line.analytic_distribution ?| array[%s]", + [str(project.account_id.id) for project in self if project.account_id], + ) + query.order = None + query_string, query_param = query.select( + "account_move_line.id as id", + ) + self._cr.execute(query_string, query_param) + purchase_invoice_lines_ids = [ + int(record.get("id")) for record in self._cr.dictfetchall() + ] + domain = [("id", "in", purchase_invoice_lines_ids)] + return domain + + def _get_out_invoiced(self, domain=False): + filter_domain = expression.AND( + [ + [("move_id.move_type", "=", "out_invoice")], + self._domain_sale_invoice_line(), + ] + ) + if domain: + return filter_domain + invoice_lines = self.env["account.move.line"].search(filter_domain) + return invoice_lines + + def _get_out_refund(self, domain=False): + filter_domain = expression.AND( + [ + [("move_id.move_type", "=", "out_refund")], + self._domain_sale_invoice_line(), + ] + ) + if domain: + return filter_domain + invoice_lines = self.env["account.move.line"].search(filter_domain) + return invoice_lines + + def _compute_out_invoiced(self): + for project in self: + lines = project._get_out_invoiced() + project.out_invoiced = sum(lines.mapped("price_subtotal")) + project.out_invoice_count = len(lines.mapped("move_id")) + + def _compute_out_refund(self): + for project in self: + lines = project._get_out_refund() + project.out_refund = sum(lines.mapped("price_subtotal")) + project.out_refund_count = len(lines.mapped("move_id")) + + def button_open_out_invoice(self): + self.ensure_one() + lines = self._get_out_invoiced() + action = self.env["ir.actions.actions"]._for_xml_id( + "account.action_move_out_invoice_type" + ) + domain = expression.AND( + [ + [("id", "in", lines.mapped("move_id").ids)], + safe_eval(action.get("domain") or "[]"), + ] + ) + context = safe_eval(action.get("context") or "{}") + context.update({"group_by": ["payment_state"]}) + action.update( + { + "domain": domain, + "context": context, + } + ) + return action + + def button_open_out_refund(self): + self.ensure_one() + lines = self._get_out_refund() + action = self.env["ir.actions.actions"]._for_xml_id( + "account.action_move_out_refund_type" + ) + domain = expression.AND( + [ + [("id", "in", lines.mapped("move_id").ids)], + safe_eval(action.get("domain") or "[]"), + ] + ) + context = safe_eval(action.get("context") or "{}") + context.update({"group_by": ["payment_state"]}) + action.update( + { + "domain": domain, + "context": context, + } + ) + return action + + def button_open_out_invoice_line(self): + self.ensure_one() + domain = self._get_out_invoiced(domain=True) + return { + "name": _("Out Invoice Lines"), + "domain": domain, + "type": "ir.actions.act_window", + "view_mode": "tree", + "res_model": "account.move.line", + } + + def button_open_out_refund_line(self): + self.ensure_one() + domain = self._get_out_refund(domain=True) + return { + "name": _("Out Refund Lines"), + "domain": domain, + "type": "ir.actions.act_window", + "view_mode": "tree", + "res_model": "account.move.line", + } diff --git a/project_sale_usability/pyproject.toml b/project_sale_usability/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/project_sale_usability/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/project_sale_usability/tests/__init__.py b/project_sale_usability/tests/__init__.py new file mode 100644 index 00000000..f2d533d1 --- /dev/null +++ b/project_sale_usability/tests/__init__.py @@ -0,0 +1 @@ +from . import test_project_sale_utilities diff --git a/project_sale_usability/tests/test_project_sale_utilities.py b/project_sale_usability/tests/test_project_sale_utilities.py new file mode 100644 index 00000000..aac62bc3 --- /dev/null +++ b/project_sale_usability/tests/test_project_sale_utilities.py @@ -0,0 +1,98 @@ +# Copyright 2020 Oihane Crucelaegui - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from collections import OrderedDict + +from odoo.osv import expression +from odoo.tests import common, tagged +from odoo.tools.safe_eval import safe_eval + + +@tagged("post_install", "-at_install") +class TestProjectSaleUtilities(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.products = OrderedDict( + [ + ("prod_order", cls.env.ref("product.product_order_01")), + ("prod_del", cls.env.ref("product.product_delivery_01")), + ] + ) + cls.partner = cls.env["res.partner"].create( + { + "name": "Test Customer", + } + ) + cls.project = cls.env["project.project"].create( + { + "name": "Test Project", + "partner_id": cls.partner.id, + } + ) + cls.project._create_analytic_account() + cls.sale_model = cls.env["sale.order"] + cls.sale = cls.sale_model.create( + { + "partner_id": cls.partner.id, + "analytic_account_id": cls.project.analytic_account_id.id, + "order_line": [ + ( + 0, + 0, + { + "name": p.name, + "product_id": p.id, + "product_uom_qty": 2, + "product_uom": p.uom_id.id, + "price_unit": p.list_price, + }, + ) + for p in cls.products.values() + ], + } + ) + cls.invoice_model = cls.env["account.move"] + + def test_project_sale_out_invoice(self): + inv = self.create_invoice_from_sale_order() + self.assertEqual(self.project.out_invoice_count, 1.0) + self.assertEqual( + self.project.out_invoiced, + sum(inv.mapped("invoice_line_ids.price_subtotal")), + ) + line_domain = [ + ( + "analytic_distribution", + "in", + self.project.mapped("analytic_account_id").ids, + ), + ("move_id.move_type", "=", "out_invoice"), + ] + invoice_lines = self.env["account.move.line"].search(line_domain) + invoice_action = self.browse_ref("account.action_move_out_invoice_type") + invoice_domain = expression.AND( + [ + [("id", "in", invoice_lines.mapped("move_id").ids)], + safe_eval(invoice_action.domain or "[]"), + ] + ) + invoice_dict = self.project.button_open_out_invoice() + self.assertEqual(invoice_dict.get("domain"), invoice_domain) + invoice_line_domain = self.project._get_out_invoiced(domain=True) + invoice_line_dict = self.project.button_open_out_invoice_line() + self.assertEqual(invoice_line_dict.get("domain"), invoice_line_domain) + + def create_invoice_from_sale_order(self): + self.assertTrue(self.sale.state == "draft") + self.sale.action_confirm() + self.assertTrue(self.sale.state == "sale") + self.assertTrue(self.sale.invoice_status == "to invoice") + inv = self.sale._create_invoices() + self.assertTrue( + self.sale.invoice_status == "no", + 'Sale: SO status after invoicing should be "nothing to invoice"', + ) + self.assertEqual(len(inv.invoice_line_ids), 1, "Sale: invoice is missing lines") + self.assertTrue(len(self.sale.invoice_ids) == 1, "Sale: invoice is missing") + return inv diff --git a/project_sale_usability/views/project_project_view.xml b/project_sale_usability/views/project_project_view.xml new file mode 100644 index 00000000..8ad73aa4 --- /dev/null +++ b/project_sale_usability/views/project_project_view.xml @@ -0,0 +1,47 @@ + + + + project.project + + +
+ + + + +
+
+
+