diff --git a/__init__.py b/__init__.py index 06b0d57daf0829dd014edb9a1b58189e3935f3cf..bab6ee4efb0b22a7440dd3c6864664d23a615fe0 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,4 @@ # Copyright 2020 Le Filament (<http://www.le-filament.com>) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from . import models +from . import models, report diff --git a/__manifest__.py b/__manifest__.py index 81abb86a32ece31abe43f82bad2635b2edadc57d..c917ebb6d27e42763c6442a6423734e126f0af70 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -7,7 +7,13 @@ "license": "AGPL-3", "depends": ["account"], "data": [ - "views/account_invoice_views.xml", + # security + "security/ir.model.access.csv", + # templates "templates/account_invoice_templates.xml", + # templates + "report/account_invoice_employee_report.xml", + # views + "views/account_invoice_views.xml", ], } diff --git a/models/__init__.py b/models/__init__.py index 0acdefe9301960227e81143824c837e4d994683d..731c3ba29060505ce7f20c40e4b8064bc8d2d6fc 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -2,4 +2,5 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import account_invoice +from . import account_invoice_employee_assign from . import account_journal_dashboard diff --git a/models/account_invoice.py b/models/account_invoice.py index 2b2843d2ed3282f684a0dd109e23fd1f8e9e545e..fa7aaf95ea210793d93afa8dde183c6bc7bf5fe2 100644 --- a/models/account_invoice.py +++ b/models/account_invoice.py @@ -1,10 +1,44 @@ # Copyright 2022 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 +from odoo import api, fields, models class AccountInvoice(models.Model): _inherit = "account.invoice" with_detail = fields.Boolean("Facture avec détail", default=False) + employee_allocation_ids = fields.One2many( + comodel_name="account.invoice.employee.assign", + inverse_name="invoice_id", + string="Ventilation facture", + ) + is_allocated = fields.Boolean( + string="Facture ventilée", + compute="_compute_allocation", + store=True, + default=False, + ) + is_allocation_error = fields.Boolean( + string="Erreur de ventilation", + compute="_compute_allocation", + store=True, + default=False, + ) + + # ------------------------------------------------------ + # Computed field + # ------------------------------------------------------ + @api.depends("employee_allocation_ids", "employee_allocation_ids.percentage") + @api.multi + def _compute_allocation(self): + for invoice in self: + if invoice.employee_allocation_ids: + invoice.is_allocated = True + if sum(invoice.employee_allocation_ids.mapped("percentage")) != 100: + invoice.is_allocation_error = True + else: + invoice.is_allocation_error = False + else: + invoice.is_allocated = False + invoice.is_allocation_error = False diff --git a/models/account_invoice_employee_assign.py b/models/account_invoice_employee_assign.py new file mode 100644 index 0000000000000000000000000000000000000000..a1cb877bf33a33e9802a15f6be30913d1ec9fb58 --- /dev/null +++ b/models/account_invoice_employee_assign.py @@ -0,0 +1,41 @@ +# Copyright 2022 Le Filament (<http://www.le-filament.com>) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class AccountInvoiceEmployeeAssign(models.Model): + _name = "account.invoice.employee.assign" + _description = "Ventilation des factures par employé" + + @api.model + def _get_partner_domain(self): + partner_ids = self.env.ref("base.main_partner") + partner_ids += self.env["hr.employee"].search( + []).mapped("user_id").mapped("partner_id") + return [("id", "in", partner_ids.ids)] + + invoice_id = fields.Many2one( + comodel_name="account.invoice", + string="Facture", + required=True + ) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Employé", + domain=_get_partner_domain, + required=True + ) + percentage = fields.Float("Pourcentage", default=0.0, required=True) + + # ------------------------------------------------------ + # Constrains + # ------------------------------------------------------ + @api.constrains("percentage") + def _check_percentage(self): + for record in self: + if record.percentage < 0 or record.percentage > 100: + raise UserError(_( + "Le pourcentage doit être entre 0 et 100" + )) diff --git a/report/__init__.py b/report/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..34523d96ed133c76f046953430ec6287d6302b77 --- /dev/null +++ b/report/__init__.py @@ -0,0 +1,4 @@ +# © 2020 Le Filament (<http://www.le-filament.com>) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import account_invoice_employee_report diff --git a/report/account_invoice_employee_report.py b/report/account_invoice_employee_report.py new file mode 100644 index 0000000000000000000000000000000000000000..481137a26c5ceacdd0aa9fe2d7130f09e2b84b34 --- /dev/null +++ b/report/account_invoice_employee_report.py @@ -0,0 +1,91 @@ +# © 2022 Le Filament (<http://www.le-filament.com>) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import tools +from odoo import models, fields, api + + +class AccountInvoiceEmployeeReport(models.Model): + _name = "account.invoice.employee.report" + _description = "Vue facturation par employé" + _auto = False + + # invoice fields + date_invoice = fields.Date("Date", readonly=True) + invoice_id = fields.Many2one( + comodel_name="account.invoice", + string="Facture", + readonly=True, + ) + amount_untaxed_signed = fields.Monetary("Montant H.T", readonly=True) + state = fields.Selection( + [ + ("open", "Ouverte"), + ("in_payment", "En paiement"), + ("paid", "Payée"), + ], + string="Statut", + readonly=True, + ) + user_id = fields.Many2one( + comodel_name="res.users", + string="Vendeur", + readonly=True, + ) + currency_id = fields.Many2one("res.currency", string='Currency', readonly=True) + # employee fields + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Employé", + readonly=True, + ) + percentage = fields.Float("Pourcentage", readonly=True) + revenue = fields.Monetary("Affectation CA", readonly=True) + + _depends = { + "account.invoice": [ + "state", "user_id", "amount_untaxed_signed", "date_invoice", + ], + "account.invoice.employee.assign": [ + "partner_id", "percentage", + ], + } + + # ------------------------------------------------------ + # Main Query + # ------------------------------------------------------ + def _query(self): + query = """ + SELECT + e.id, + i.id as "invoice_id", + i.date_invoice, + i.amount_untaxed_signed, + i.state, + i.user_id, + i.currency_id, + e.partner_id, + e.percentage, + i.amount_untaxed_signed * e.percentage / 100 as "revenue" + FROM + account_invoice_employee_assign e + LEFT JOIN + account_invoice i on i.id = e.invoice_id + WHERE + i.date_invoice is not null + and i.state in ('open', 'in_payment', 'paid') + and i.type in ('out_invoice', 'out_refund') + """ + return query + + @api.model_cr + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + self.env.cr.execute("""CREATE or REPLACE VIEW %s as ( + %s + )""" % ( + self._table, self._query())) + + # ------------------------------------------------------ + # Computed fields + # ------------------------------------------------------ diff --git a/report/account_invoice_employee_report.xml b/report/account_invoice_employee_report.xml new file mode 100644 index 0000000000000000000000000000000000000000..d8599ea7e5c75216a8b8d40e2e0222e712145c0d --- /dev/null +++ b/report/account_invoice_employee_report.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <!-- SEARCH VIEW --> + <record model="ir.ui.view" id="account_invoice_employee_report_search"> + <field name="name">account.invoice.employee.report.search</field> + <field name="model">account.invoice.employee.report</field> + <field name="arch" type="xml"> + <search string="Facturation par employé"> + <field name="partner_id"/> + <field name="invoice_id"/> + <field name="user_id"/> + <group expand="0" string="Group By"> + <filter name="group_by_date" string="Date de facturation" context="{'group_by':'date_invoice'}"/> + <filter name="group_by_employee" string="Employé" context="{'group_by':'partner_id'}"/> + <filter name="group_by_state" string="Statut" context="{'group_by':'state'}"/> + </group> + </search> + </field> + </record> + + <!-- TREE VIEW --> + <record model="ir.ui.view" id="account_invoice_employee_report_tree"> + <field name="name">account.invoice.employee.report.tree</field> + <field name="model">account.invoice.employee.report</field> + <field name="arch" type="xml"> + <tree string="Facturation par employé"> + <field name="currency_id" invisible="1" /> + <field name="date_invoice"/> + <field name="partner_id"/> + <field name="percentage"/> + <field name="revenue" widget="monetary" /> + <field name="user_id"/> + <field name="invoice_id"/> + <field name="amount_untaxed_signed"/> + <field name="state"/> + </tree> + </field> + </record> + + <!-- PIVOT VIEW --> + <record model="ir.ui.view" id="account_invoice_employee_report_pivot"> + <field name="name">account.invoice.employee.report.pivot</field> + <field name="model">account.invoice.employee.report</field> + <field name="arch" type="xml"> + <pivot string="Facturation par employé"> + <field name="currency_id" invisible="1" /> + <field name="partner_id" type="row"/> + <field name="date_invoice" type="col"/> + <field name="revenue" type="measure"/> + </pivot> + </field> + </record> + + <!-- GRAPH VIEW --> + <record model="ir.ui.view" id="account_invoice_employee_report_graph"> + <field name="name">account.invoice.employee.report.graph</field> + <field name="model">account.invoice.employee.report</field> + <field name="arch" type="xml"> + <graph string="Facturation par employé"> + <field name="partner_id" /> + <field name="revenue" type="measure"/> + </graph> + </field> + </record> + + <!-- ACTION --> + <record id="account_invoice_employee_report_action" model="ir.actions.act_window"> + <field name="name">Rapports Legicoop</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">account.invoice.employee.report</field> + <field name="view_mode">pivot,graph,tree</field> + </record> + + <!-- Menu --> + <menuitem + id="account_invoice_employee_report_menu" + name="CA par employé" + parent="account.account_reports_management_menu" + action="legicoop_account.account_invoice_employee_report_action" + sequence="50" + /> + + </data> +</odoo> diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000000000000000000000000000000000000..62f4e98040551fa163bbc84fed97f96a8d73363f --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_invoice_employee_assign,access_account_invoice_employee_assign,model_account_invoice_employee_assign,account.group_account_invoice,1,1,1,1 +access_account_invoice_employee_report,access_account_invoice_employee_report,model_account_invoice_employee_report,account.group_account_invoice,1,1,1,1 \ No newline at end of file diff --git a/views/account_invoice_views.xml b/views/account_invoice_views.xml index 72b6d20cf19cc8b028a4b2ddb2e64bd8e132f583..33d8f7b13585c7ed09ce74ebb9fc766413c31957 100644 --- a/views/account_invoice_views.xml +++ b/views/account_invoice_views.xml @@ -8,6 +8,7 @@ <field name="model">account.invoice</field> <field name="inherit_id" ref="account.invoice_form" /> <field name="arch" type="xml"> + <!-- Détail de la facture --> <xpath expr="//field[@name='payment_term_id']" position="after"> <field name="with_detail" /> </xpath> @@ -17,9 +18,32 @@ > <attribute name="attrs">{}</attribute> </xpath> + <!-- Ventilation de la facture --> + <xpath expr="//notebook" position="inside"> + <page name="employee_assign" string="Répartition par employé"> + <field name="is_allocation_error" invisible="1" /> + <div + class="alert alert-danger" + role="alert" + attrs="{'invisible': [('is_allocation_error', '!=', True)]}" + > + La répartition pour cette facture n'est pas égale à 100%. + </div> + <field name="employee_allocation_ids"> + <tree editable="top" create="1" edit="1" delete="1"> + <field + name="partner_id" + options="{'no_create': 1, 'no_edit': 1}" + /> + <field name="percentage" /> + </tree> + </field> + </page> + </xpath> </field> </record> + <!-- Tree view --> <record id="legicoop_invoice_tree" model="ir.ui.view"> <field name="name">legicoop.account.invoice.tree</field> <field name="model">account.invoice</field> @@ -44,5 +68,27 @@ </field> </record> + <!-- Search view --> + <record id="legicoop_invoice_search" model="ir.ui.view"> + <field name="name">legicoop.account.invoice.search</field> + <field name="model">account.invoice</field> + <field name="inherit_id" ref="account.view_account_invoice_filter" /> + <field name="arch" type="xml"> + <xpath expr="//filter[@name='late']" position="after"> + <separator /> + <filter + string="Factures à répartir" + name="assign_ok" + domain="[('is_allocated', '=', False)]" + /> + <filter + string="Répartition en erreur" + name="assign_error" + domain="[('is_allocation_error', '=', True)]" + /> + </xpath> + </field> + </record> + </data> </odoo>