diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 732d0c4a644eb444d6b4385643ff32fab19fab52..4acca684ec8c5cda7b3ecd21b9e568ab03d57e8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,8 @@ exclude: | ^setup/|/static/description/index\.html$| # We don't want to mess with tool-generated files .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.md$| # Library files can have extraneous formatting (even minimized) /static/(src/)?lib/| # Repos using Sphinx to generate docs don't need prettying @@ -25,8 +27,13 @@ repos: entry: found forbidden files; remove them language: fail files: "\\.rej$" + - id: en-po-files + name: en.po files cannot exist + entry: found a en.po file + language: fail + files: '[a-zA-Z0-9_]*/i18n/en\.po$' - repo: https://github.com/oca/maintainer-tools - rev: 7d8a9f9ad73db0976fb03cbee43d953bc29b89e9 + rev: ab1d7f6 hooks: # update the NOT INSTALLABLE ADDONS section above - id: oca-update-pre-commit-excluded-addons @@ -48,7 +55,7 @@ repos: hooks: - id: black - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.6.2 + rev: v2.1.2 hooks: - id: prettier name: prettier (with plugin-xml) @@ -59,7 +66,7 @@ repos: - --plugin=@prettier/plugin-xml files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$ - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.15.0 + rev: v7.8.1 hooks: - id: eslint verbose: true @@ -67,7 +74,7 @@ repos: - --color - --fix - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v3.2.0 hooks: - id: trailing-whitespace # exclude autogenerated files @@ -89,37 +96,33 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v2.7.2 hooks: - id: pyupgrade args: ["--keep-percent-format"] - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort name: isort except __init__.py args: - --settings=. exclude: /__init__\.py$ - - repo: https://gitlab.com/PyCQA/flake8 - rev: 3.9.2 + - repo: https://github.com/PyCQA/flake8 + rev: 3.8.3 hooks: - id: flake8 name: flake8 additional_dependencies: ["flake8-bugbear==20.1.4"] - - repo: https://github.com/PyCQA/pylint - rev: v2.11.1 + - repo: https://github.com/OCA/pylint-odoo + rev: 7.0.2 hooks: - - id: pylint + - id: pylint_odoo name: pylint with optional checks args: - --rcfile=.pylintrc - --exit-zero verbose: true - additional_dependencies: &pylint_deps - - pylint-odoo==5.0.5 - - id: pylint - name: pylint with mandatory checks + - id: pylint_odoo args: - --rcfile=.pylintrc-mandatory - additional_dependencies: *pylint_deps diff --git a/models/scop_cotisation_cg_followup.py b/models/scop_cotisation_cg_followup.py index ccf2e7f40c65aa81e5f8fde730a478660185809a..f100779e713ee5bcab4db21c08747f2a8be7c634 100644 --- a/models/scop_cotisation_cg_followup.py +++ b/models/scop_cotisation_cg_followup.py @@ -17,6 +17,7 @@ class ScopCotisationCgFollowup(models.Model): date = fields.Date( "Date Limite de la Relance", states={"done": [("readonly", True)]} ) + date_validation = fields.Date("Date de validation de la Relance") threshold_amount = fields.Monetary( "Seuil de relance", default=0, @@ -34,7 +35,7 @@ class ScopCotisationCgFollowup(models.Model): string="Année minimale", ) state = fields.Selection( - [("draft", "Brouillon"), ("done", "Validé"), ("cancel", "Annulé")], + [("draft", "Brouillon"), ("done", "Validé")], string="Statut", default="draft", tracking=2, @@ -49,7 +50,7 @@ class ScopCotisationCgFollowup(models.Model): string="Nombre de coopératives", compute="_compute_partner_count" ) total_amount = fields.Monetary( - string="Montant à recouvrir", + string="Montant recouvré", compute="_compute_total_amount", currency_field="company_currency_id", ) @@ -135,10 +136,6 @@ class ScopCotisationCgFollowup(models.Model): for line in followup_line_ids: # update partner followup_level line.partner_id.followup_level = line.followup_level - # TODO: - # - création courrier + chargement sur Alfresco - # - envouer un mail (contact COTI ?) - # - autres actions à définir en fonction du niveau # Store invoices history in JSON amount_history = {} @@ -162,10 +159,11 @@ class ScopCotisationCgFollowup(models.Model): ) line.amount_history = amount_history - self.state = "done" + self.update({"date_validation": fields.Date.today(), "state": "done"}) - def cancel_followup(self): - self.state = "cancel" + def reset_to_draft(self): + self.ensure_one() + self.update({"date_validation": False, "state": "draft"}) def create_followup(self): """ diff --git a/models/scop_cotisation_cg_followup_line.py b/models/scop_cotisation_cg_followup_line.py index 99c737563257f770cd011470f57a207adce7c041..66d3fa5282f13951970429224e31e3bbfd07f013 100644 --- a/models/scop_cotisation_cg_followup_line.py +++ b/models/scop_cotisation_cg_followup_line.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import ast +from datetime import datetime from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -16,6 +17,7 @@ class ScopCotisationCgFollowupLine(models.Model): comodel_name="scop.cotisation.cg.followup", string="Relance", required=True, + ondelete="cascade", index=True, ) followup_date = fields.Date(related="followup_id.date", string="Date") @@ -157,11 +159,16 @@ class ScopCotisationCgFollowupLine(models.Model): # ------------------------------------------------------ def print_followup(self): return self.env.ref( - "cgscop_cotisation_cg_followup.cgscop_followup_report").report_action(self) + "cgscop_cotisation_cg_followup.cgscop_followup_report" + ).report_action(self) def delete_followup_line(self): self.unlink() + def action_exclude_line(self): + for line in self.browse(self.env.context["active_ids"]): + line.update({"is_excluded": True}) + # ------------------------------------------------------ # Override ORM # ------------------------------------------------------ @@ -195,3 +202,35 @@ class ScopCotisationCgFollowupLine(models.Model): ) ) return super(ScopCotisationCgFollowupLine, self).unlink() + + # ------------------------------------------------------ + # Report functions + # ------------------------------------------------------ + def get_report_invoices(self): + """ + Retournes les impayés depuis la date de validation + """ + self.ensure_one() + domain_odoo = [ + ("invoice_date_due", "<=", self.followup_id.date_validation), + ("is_contribution", "=", True), + ("state", "=", "posted"), + ("move_type", "=", "out_invoice"), + ("bordereau_id", "!=", False), + ("bordereau_id.state", "!=", "cancelled"), + ("partner_id", "=", self.partner_id.id), + ("amount_residual_signed", ">", 0), + ("id", "not in", self.invoice_ids.ids), + ] + if self.followup_id.threshold_year: + domain_odoo.append( + ( + "invoice_date_due", + ">", + datetime(int(self.followup_id.threshold_year), 1, 1), + ) + ) + + # Get Odoo move line & partners + move_ids = self.env["account.move"].search(domain_odoo) + return move_ids + self.invoice_ids diff --git a/templates/report_scop_followup.xml b/templates/report_scop_followup.xml index 79cae31b1a55baa561af06dc85c95dd82c4b52a8..e507983264aeca7544053c22d5ae436f35dd39a1 100644 --- a/templates/report_scop_followup.xml +++ b/templates/report_scop_followup.xml @@ -5,6 +5,12 @@ <template id="report_scop_followup_document"> <t t-call="web.external_layout"> <t t-set="o" t-value="o.with_context(lang='fr')" /> + <t t-set="all_invoices" t-value="o.get_report_invoices()" /> + <t + t-set="total_amount" + t-value="sum(all_invoices.mapped('amount_residual_signed')) + sum(o.riga_contribution_ids.mapped('amount_due'))" + /> + <div class="page" style="font-size: 16px;"> <!-- Adresse --> <div class="row"> @@ -43,8 +49,7 @@ <div class="row pt32"> <div class="col-12" style="text-align: justify;"> <p> - Objet : Relance pour cotisation impayée<br - /> + Objet : Relance pour cotisation impayée<br /> N° adhérent : <t t-esc="str(o.partner_id.member_number_int)" /><br /> @@ -61,15 +66,20 @@ </p> <p> Sauf erreur ou omission de notre part, votre relevé de compte cotisant à la date du - <span t-field="o.followup_id.date" /> présente un solde débiteur de - <span t-field="o.amount_due" t-options='{"widget": "monetary","display_currency": company.currency_id}' /> + <span + t-field="o.followup_id.date" + /> présente un solde débiteur de + <span + t-esc="total_amount" + t-options='{"widget": "monetary","display_currency": company.currency_id}' + />. </p> <table class="table table-sm table-stripped"> <thead> <tr> <th>Année</th> - <th>Trimestre</th> + <th>Trimestre(s)</th> <th>Type de cotisation</th> <th>Montant total</th> <th>Montant réglé</th> @@ -77,41 +87,109 @@ </tr> </thead> <tbody> - <tr t-foreach="o.invoice_ids" t-as="line"> - <td><span t-esc="line.year" /></td> - <td><span t-field="line.cotiz_quarter" /></td> - <td><span t-field="line.type_contribution_id.name" /></td> - <td class="text-right"><span t-field="line.amount_called" /></td> - <td class="text-right"><span t-esc="line.amount_called - line.amount_residual_signed" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td> - <td class="text-right"><span t-field="line.amount_residual_signed" /></td> - </tr> - <tr t-foreach="o.riga_contribution_ids.sorted(key='year', reverse=True)" t-as="line"> - <td><span t-field="line.year" /></td> - <td><span t-field="line.quarter" /></td> - <td><span t-field="line.type_contribution_id.name" /></td> - <td class="text-right"><span t-field="line.amount" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td> - <td class="text-right"><span t-esc="line.amount_paid" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td> - <td class="text-right"><span t-field="line.amount_due" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td> - </tr> + <t + t-set="year_invoices" + t-value="sorted(list(set(all_invoices.mapped('year'))), reverse=True)" + /> + + <t + t-set="type_contrib_invoices" + t-value="list(set(all_invoices.mapped('type_contribution_id')))" + /> + <t t-foreach="year_invoices" t-as="year"> + <tr + t-foreach="type_contrib_invoices" + t-as="contrib" + > + <t + t-set="line" + t-value="all_invoices.filtered(lambda c: c.year == year and c.type_contribution_id == contrib)" + /> + <t t-if="line"> + <td><span t-esc="year" /></td> + <td><span + t-esc="', '.join(sorted(line.mapped(lambda c: c.cotiz_quarter or '')))" + /></td> + <td><span t-esc="contrib.name" /></td> + <td class="text-right"><span + t-esc="sum(line.mapped('amount_called'))" + t-options='{"widget": "monetary","display_currency": company.currency_id}' + /></td> + <td class="text-right"><span + t-esc="sum(line.mapped(lambda c: c.amount_called - c.amount_residual_signed))" + t-options='{"widget": "monetary","display_currency": company.currency_id}' + /></td> + <td class="text-right"><span + t-esc="sum(line.mapped('amount_residual_signed'))" + t-options='{"widget": "monetary","display_currency": company.currency_id}' + /></td> + </t> + </tr> + </t> + + <t + t-set="year_riga" + t-value="list(set(o.riga_contribution_ids.sorted(key='year', reverse=True).mapped('year')))" + /> + <t + t-set="type_contrib_riga" + t-value="list(set(o.riga_contribution_ids.mapped('type_contribution_id')))" + /> + <t t-foreach="year_riga" t-as="year"> + <tr + t-foreach="type_contrib_riga" + t-as="contrib" + > + <t + t-set="line" + t-value="o.riga_contribution_ids.filtered(lambda c: c.year == year and c.type_contribution_id == contrib)" + /> + <t t-if="line"> + <td><span t-esc="year" /></td> + <td><span + t-esc="', '.join(sorted(line.mapped('quarter')))" + /></td> + <td><span t-esc="contrib.name" /></td> + <td class="text-right"><span + t-esc="sum(line.mapped('amount'))" + t-options='{"widget": "monetary","display_currency": company.currency_id}' + /></td> + <td class="text-right"><span + t-esc="sum(line.mapped('amount_paid'))" + t-options='{"widget": "monetary","display_currency": company.currency_id}' + /></td> + <td class="text-right"><span + t-esc="sum(line.mapped('amount_due'))" + t-options='{"widget": "monetary","display_currency": company.currency_id}' + /></td> + + </t> + </tr> + </t> </tbody> <tfoot> <th colspan="5" class="text-right">Total impayé</th> - <td class="text-right"><span t-field="o.amount_due" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td> + <td class="text-right"><span + t-esc="total_amount" + t-options='{"widget": "monetary","display_currency": company.currency_id}' + /></td> </tfoot> </table> - <p class="pt32 font-italic"> + <p class="pt32"> Ces cotisations sont à régler par virement à l’ordre de la CGSCOP (IBAN : FR76 4255 9100 0008 0024 8837 710). Il convient de préciser dans le libellé votre numéro d’adhérent et/ou le nom de votre coopérative ainsi que la période réglée. </p> - <p class="font-italic"> + <p class=""> Pour une gestion simplifiée de vos cotisations, nous pouvons mettre en place le prélèvement automatique trimestriel. Vous êtes informés avant chaque prélèvement par courriel. <br /> - Vous pouvez faire une demande de mandat SEPA à <a href="mailto:administratif.cg@scop.coop">administratif.cg@scop.coop</a>. + Vous pouvez faire une demande de mandat SEPA à <a + href="mailto:administratif.cg@scop.coop" + >administratif.cg@scop.coop</a>. </p> - <p class="font-italic"> + <p class=""> Si vous rencontrez des difficultés ou si vous souhaitez des renseignements complémentaires sur le calcul de ces cotisations, nous vous invitons à contacter votre délégué.e au sein de votre Union Régionale. </p> - <p class="font-italic"> + <p class=""> Nous vous rappelons que les cotisations constituent les principales ressources de notre mouvement, et lui donnent les moyens d’agir en toute indépendance au service de ses adhérents, notamment au travers des outils financiers et des équipes de délégués de votre Union Régionale. </p> <p> diff --git a/views/scop_cotisation_cg_followup.xml b/views/scop_cotisation_cg_followup.xml index fedf9e6d705d890c69c3bb3b88a290365fd88443..e268c91cec38c92bf6fd018d2761bd9c16a975b1 100644 --- a/views/scop_cotisation_cg_followup.xml +++ b/views/scop_cotisation_cg_followup.xml @@ -17,11 +17,11 @@ attrs="{'invisible': [('state', '!=', 'draft')]}" /> <button - name="cancel_followup" + name="reset_to_draft" type="object" - string="Annuler la relance" - confirm="Êtes-vous certain(e) de vouloir annuler cette relance ?" - attrs="{'invisible': [('state', '!=', 'draft')]}" + string="Remettre en brouillon" + confirm="Êtes-vous certain(e) de vouloir remettre en brouillon cette relance ?" + attrs="{'invisible': [('state', '!=', 'done')]}" /> <button name="action_report_followup_per_ur" @@ -77,6 +77,7 @@ <group string="Critères de la relance"> <group> <field name="date" required="1" /> + <field name="date_validation" readonly="1" /> <field name="threshold_year" /> <field name="threshold_amount" widget="monetary" /> <div class="text-muted" colspan="2"> diff --git a/views/scop_cotisation_cg_followup_line.xml b/views/scop_cotisation_cg_followup_line.xml index eccce18a69f9308d1c2017e0093ca66ec0aaa255..aa7d7751ffa84e1b0e6bb54383555a473b93c9eb 100644 --- a/views/scop_cotisation_cg_followup_line.xml +++ b/views/scop_cotisation_cg_followup_line.xml @@ -174,13 +174,18 @@ <field name="model">scop.cotisation.cg.followup.line</field> <field name="arch" type="xml"> <tree string="Détail Relances"> + <field name="followup_state" invisible="1" /> <field name="followup_id" /> <field name="member_number" /> <field name="partner_id" /> <field name="ur_id" /> <field name="followup_level" /> <field name="amount_due" /> - <field name="is_excluded" widget="boolean_toggle" /> + <field + name="is_excluded" + widget="boolean_toggle" + attrs="{'readonly': [('followup_state', '=', 'done')]}" + /> <button name="print_followup" type="object" @@ -225,6 +230,21 @@ <field name="view_mode">tree,pivot,graph,form</field> </record> + <!-- Action server --> + <record id="action_exclude_followup_line" model="ir.actions.server"> + <field name="name">Exclure les relances</field> + <field name="model_id" ref="model_scop_cotisation_cg_followup_line" /> + <field + name="binding_model_id" + ref="cgscop_cotisation_cg_followup.model_scop_cotisation_cg_followup_line" + /> + <field name="binding_view_types">list</field> + <field name="state">code</field> + <field name="code"> + action = model.action_exclude_line() + </field> + </record> + <!-- Menu --> <menuitem name="Détail des Relances"