diff --git a/__init__.py b/__init__.py index 128fa3f8ff8d7de77d5d026b933d37c8d3b76db1..a453b4b3a3656a314b8ca36c4d0b0f54f491c85e 100755 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,8 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. +# © 2022 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 report from . import wizard from odoo import api, SUPERUSER_ID diff --git a/__manifest__.py b/__manifest__.py index 2f7efcd22c929accdb6d1d82b742201b82fc6f5a..323070940c3d915ecb1cbe547b2bafea7c600e29 100755 --- a/__manifest__.py +++ b/__manifest__.py @@ -1,3 +1,5 @@ +# © 2022 Le Filament (<http://www.le-filament.com>) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { "name": "CG SCOP - Cotisations", "summary": "CG SCOP - Cotisations", @@ -24,6 +26,7 @@ "views/res_config_settings.xml", "views/res_partner.xml", "views/scop_cotisation_task.xml", + "report/scop_contribution_report.xml", ], "qweb": [ "static/src/xml/*.xml", diff --git a/models/__init__.py b/models/__init__.py index 1a0bf4d8dc8f0d671c7079b0fa32e101d572f6f7..419d51378ff1cf86ca82d88e686bdf688a159866 100755 --- a/models/__init__.py +++ b/models/__init__.py @@ -6,6 +6,7 @@ from . import account_payment_order from . import chart_template from . import res_company from . import res_config_settings +from . import res_partner from . import scop_contribution from . import scop_cotisation from . import scop_cotisation_task diff --git a/models/res_partner.py b/models/res_partner.py new file mode 100644 index 0000000000000000000000000000000000000000..67c5ab6ec7046cba252932d4083ecd6b1bdffd43 --- /dev/null +++ b/models/res_partner.py @@ -0,0 +1,14 @@ +# © 2020 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 + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + contribution_report_ids = fields.One2many( + comodel_name='scop.contribution.report', + inverse_name='partner_id', + string='Cotisations', + ) diff --git a/report/__init__.py b/report/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..530b04980c303e369063f0beec2f53cace402497 --- /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 scop_contribution_report diff --git a/report/scop_contribution_report.py b/report/scop_contribution_report.py new file mode 100644 index 0000000000000000000000000000000000000000..23dedb4d5f8fcd3980afcb2b526b9600b172309c --- /dev/null +++ b/report/scop_contribution_report.py @@ -0,0 +1,199 @@ +# © 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 ScopContributionReport(models.Model): + _name = "scop.contribution.report" + _description = "Vue cotisations" + _auto = False + _order = 'year desc, partner_id' + + name = fields.Char(compute='_compute_name') + year = fields.Char('Année de cotisation') + type_contribution_id = fields.Many2one( + comodel_name="scop.contribution.type", + string="Type de cotisation", + readonly=True) + partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) + amount_called = fields.Float('Montant Appelé') + amount_paid = fields.Float('Montant Payé') + amount_due = fields.Float('Montant Restant') + is_loss = fields.Boolean('Exonération/Perte') + payments = fields.Html('Paiements', compute="_compute_payments") + + _depends = { + 'account.invoice': [ + 'year', 'type_contribution_id', 'partner_id', + 'amount_total_signed', 'residual_company_signed', + 'state', 'type', 'is_contribution', 'refund_invoice_id' + ], + } + + # ------------------------------------------------------ + # Sub Query + # ------------------------------------------------------ + def _select_invoice(self): + select_str = """ + SELECT + CAST(i.year AS VARCHAR), + i.type_contribution_id, + i.partner_id, + SUM(i.amount_total_signed) AS amount_called, + SUM(i.amount_total_signed - i.residual_company_signed) AS amount_paid, + SUM(i.residual_company_signed) AS amount_due, + (CASE WHEN max(refund.id) IS NOT NULL THEN true ELSE FALSE END) AS is_loss + """ + return select_str + + def _from_invoice(self): + from_str = """ + FROM + account_invoice i + LEFT JOIN + account_invoice refund ON refund.refund_invoice_id = i.id + """ + return from_str + + def _where_invoice(self): + where_str = """ + WHERE + i.type = 'out_invoice' AND + i.state in ('open', 'paid') AND + i.is_contribution = true + """ + return where_str + + def _groupby_invoice(self): + groupby_str = """ + GROUP BY + i.year, + i.type_contribution_id, + i.partner_id + """ + return groupby_str + + def _query_invoice(self): + query = "(%s %s %s %s)" % ( + self._select_invoice(), self._from_invoice(), + self._where_invoice(), self._groupby_invoice()) + return query + + def _subquery(self): + return self._query_invoice() + + # ------------------------------------------------------ + # Main Query + # ------------------------------------------------------ + def _select(self): + select_str = """ + SELECT + ROW_NUMBER() OVER(ORDER BY c.year, c.partner_id) AS id, + c.year, + c.type_contribution_id, + c.partner_id, + SUM(c.amount_called) AS amount_called, + SUM(c.amount_paid) AS amount_paid, + SUM(c.amount_due) AS amount_due, + BOOL_OR(c.is_loss) AS is_loss + FROM ( + """ + return select_str + + def _query_groupby(self): + return """ + GROUP BY + c.year, + c.type_contribution_id, + c.partner_id + """ + + def _query_order(self): + return "ORDER BY c.year DESC" + + def _query(self): + query = ( + self._select() + self._subquery() + ') c ' + + self._query_groupby() + self._query_order() + ) + 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 + # ------------------------------------------------------ + @api.multi + def _compute_name(self): + for contribution in self: + contribution.name = (contribution.year + ' - ' + + contribution.type_contribution_id.name + + ' - ' + contribution.partner_id.name) + + @api.multi + def _compute_payments(self): + for contribution in self: + contribution.payments = contribution._get_payment() + + # ------------------------------------------------------ + # Business functions + # ------------------------------------------------------ + def _get_payment(self): + self.ensure_one() + invoice_ids = self.env['account.invoice'].search([ + ('year', '=', int(self.year)), + ('partner_id', '=', self.partner_id.id), + ('type_contribution_id', '=', self.type_contribution_id.id), + ('type', '=', 'out_invoice') + ]) + payment_ids = invoice_ids.mapped('payment_move_line_ids') + if payment_ids: + payments = payment_ids.mapped(lambda p: { + 'date': p.date, + 'name': p.name, + 'ref': p.ref, + 'credit': p.credit, + }) + payments_html = self._get_html_table(payments) + else: + payments_html = "<p>Il n'y a pas de paiements associés à cette cotisation</p>" + return payments_html + + def _get_html_table(self, payments): + """ + :param payments: list of dict {date, name, ref, amount} + @return: HTML table with payments + """ + start_html = """ + <table class='table table-sm table-striped table-hover table-bordered'> + <thead><tr> + <th>Date</th> + <th>Libellé</th> + <th>Référence</th> + <th>Montant</th> + </tr></thead> + <tbody> + """ + content_html = "" + for payment in payments: + content_html += """ + <tr> + <td>%s</td> + <td>%s</td> + <td>%s</td> + <td class='text-right'>%.2f €</td> + </tr> + """ % (payment.get('date', '').strftime('%d/%m/%Y'), + payment.get('name', ''), payment.get('ref', ''), + payment.get('credit', 0.0),) + + end_html = "</tbody></table>" + return start_html + content_html + end_html diff --git a/report/scop_contribution_report.xml b/report/scop_contribution_report.xml new file mode 100644 index 0000000000000000000000000000000000000000..6b7fbd72581220dcf5dd99fc7dbb5dce9d511d09 --- /dev/null +++ b/report/scop_contribution_report.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <!-- SEARCH VIEW --> + <record model="ir.ui.view" id="scop_contribution_report_search"> + <field name="name">scop.contribution.report.search</field> + <field name="model">scop.contribution.report</field> + <field name="arch" type="xml"> + <search string="Cotisations"> + <field name='partner_id'/> + <field name="year"/> + <field name="type_contribution_id"/> + <filter name="filter_paid" string="À régler" domain="[('amount_due', '!=', 0)]"/> + <filter name="filter_paid" string="Réglé" domain="[('amount_due', '=', 0)]"/> + <separator></separator> + <filter name="filter_cg" string="Cotisations CG" domain="[('type_contribution_id', '=', 1)]"/> + <filter name="filter_ur" string="Cotisations UR" domain="[('type_contribution_id', '=', 3)]"/> + <filter name="filter_fede" string="Cotisations Fédé" domain="[('type_contribution_id', '=', 2)]"/> + <group expand="0" string="Group By"> + <filter name="group_by_type_contribution_id" string="Type de cotisation" context="{'group_by':'type_contribution_id'}"/> + <filter name="group_by_year" string="Année" context="{'group_by':'year'}"/> + </group> + </search> + </field> + </record> + + <!-- TREE VIEW --> + <record model="ir.ui.view" id="scop_contribution_report_tree"> + <field name="name">scop.contribution.report.tree</field> + <field name="model">scop.contribution.report</field> + <field name="arch" type="xml"> + <tree string="Cotisations"> + <field name="year"/> + <field name="type_contribution_id"/> + <field name='partner_id'/> + <field name='amount_called'/> + <field name='amount_paid'/> + <field name='amount_due'/> + </tree> + </field> + </record> + + <!-- FORM VIEW --> + <record model="ir.ui.view" id="scop_contribution_report_form"> + <field name="name">scop.contribution.report.form</field> + <field name="model">scop.contribution.report</field> + <field name="arch" type="xml"> + <form string="Cotisations"> + <sheet> + <h2><field name="name"/></h2> + <separator></separator> + <table class="table table-striped table-hover"> + <thead> + <tr> + <th>Montant Appelé</th> + <th>Montant payé</th> + <th>Montant restant</th> + </tr> + </thead> + <tbody> + <tr> + <td><field name='amount_called'/> €</td> + <td><field name='amount_paid'/> €</td> + <td><field name='amount_due'/> €</td> + </tr> + </tbody> + </table> + <separator></separator> + <h4>Détail des Paiements</h4> + <field name='payments' widget="html"/> + </sheet> + </form> + </field> + </record> + + <!-- PIVOT VIEW --> + <record model="ir.ui.view" id="scop_contribution_report_pivot"> + <field name="name">scop.contribution.report.pivot</field> + <field name="model">scop.contribution.report</field> + <field name="arch" type="xml"> + <pivot string="Cotisations"> + <field name="year" type="row"/> + <field name="type_contribution_id" type="row"/> + <field name='amount_called' type="measure"/> + <field name='amount_paid' type="measure"/> + <field name='amount_due' type="measure"/> + </pivot> + </field> + </record> + + <!-- GRAPH VIEW --> + <record model="ir.ui.view" id="scop_contribution_report_graph"> + <field name="name">scop.contribution.report.graph</field> + <field name="model">scop.contribution.report</field> + <field name="arch" type="xml"> + <graph string="Cotisations"> + <field name="year"/> + <field name='amount_called' type="measure"/> + <field name='amount_paid' type="measure"/> + <field name='amount_due' type="measure"/> + </graph> + </field> + </record> + + <!-- ACTION --> + <record id="scop_contribution_report_action" model="ir.actions.act_window"> + <field name="name">Vue cotisations</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">scop.contribution.report</field> + <field name="view_mode">tree,pivot,graph,form</field> + </record> + + <!-- Menu --> + <menuitem + id="scop_contribution_report_menu" + name="Cotisations" + parent="account.account_reports_management_menu" + action="scop_contribution_report_action" + sequence="80" + /> + </data> +</odoo> diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv index e20c8308a728efd5f82e307bdc0f250490c415a3..731427df84b1438d79b90c8dc5c23e0953a861b1 100755 --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_scop_cotisation_task,access_scop_cotisation_task,model_scop_cotisation_task,account.group_account_manager,1,1,1,0 admin_access_scop_cotisation_task,admin_access_scop_cotisation_task,model_scop_cotisation_task,cgscop_partner.group_cg_administrator,1,1,1,1 +access_scop_contribution_report_user,access_scop_contribution_report_user,model_scop_contribution_report,base.group_user,1,0,0,0 \ No newline at end of file diff --git a/views/res_partner.xml b/views/res_partner.xml index 3a9946327458e17d45390276491290a35f8cc686..b0a5c4532275eb22b82859dcb5002806ca057788 100644 --- a/views/res_partner.xml +++ b/views/res_partner.xml @@ -9,9 +9,24 @@ <field name="model">res.partner</field> <field name="inherit_id" ref="cgscop_partner.scop_contact_view_form"/> <field name="arch" type="xml"> - <xpath expr="//field[@name='contribution_ids']/tree/field[@name='spreading']" position="before"> - <field name="is_exempt" style="text-align: center;"/> - <button name="view_refund" type="object" icon="fa-eye" attrs="{'invisible': [('is_exempt', '=', False)]}" style="pointer-events: visible;"/> + <xpath expr="//page[@name='scop_membership']" position="after"> + <page name='scop_contribution' string="Cotisations" attrs="{'invisible': ['|', ('is_cooperative', '!=', True), ('project_status', '!=', '6_suivi')]}"> + <notebook> + <page name="contribution" string="Appels de Cotisations"> + <field name="contribution_report_ids" mode="tree,form"> + <tree create="false" edit="false" delete="false" default_order="year desc"> + <field name="year"/> + <field name="type_contribution_id"/> + <field name='partner_id'/> + <field name='amount_called'/> + <field name='amount_paid'/> + <field name='amount_due'/> + <field name='is_loss' style="text-align: center;"/> + </tree> + </field> + </page> + </notebook> + </page> </xpath> </field> </record>