diff --git a/__manifest__.py b/__manifest__.py index 8517ac47410592aa4c65f7f4f3a631a4d04e5c9f..cf14a9d19730747516e4c473c734518b16f03ab5 100755 --- a/__manifest__.py +++ b/__manifest__.py @@ -17,6 +17,7 @@ "views/res_config_settings.xml", "views/scop_cotisation_aura.xml", "views/account_invoice.xml", + "views/account_payment_term.xml", "views/report_cotisation_aura.xml", "views/report_cotisation_aura_refund.xml", "wizard/scop_cotisation_aura_wizard.xml", diff --git a/models/__init__.py b/models/__init__.py index 2370a971631a05ebb76c2dc9b5f6607687c3111c..424e32226cc200b16e886ae22f2a1035703021d9 100755 --- a/models/__init__.py +++ b/models/__init__.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import account_invoice +from . import account_payment_term from . import res_company from . import res_config_settings from . import scop_cotisation_aura diff --git a/models/account_invoice.py b/models/account_invoice.py index 010da669b8467a28146b0ecd50dbea9f9291a222..6f4500fded4ee7792ec0bf846057a8b084c940b0 100755 --- a/models/account_invoice.py +++ b/models/account_invoice.py @@ -1,7 +1,8 @@ # © 2020 Le Filament (<http://www.le-filament.com>) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import models, fields, api +from odoo import models, fields, api, _ +from odoo.exceptions import UserError class ScopAuraAccountInvoice(models.Model): @@ -25,6 +26,10 @@ class ScopAuraAccountInvoice(models.Model): string="Montant calculé annuel", currency_field='company_currency_id', compute='_compute_amount_aura_calculated', store=True, readonly=True) + nb_quarter_aura = fields.Selection( + string='Nombre de trimestres de cotisation', + selection=[(1, 1), (2, 2), (3, 3), (4, 4)], + default=4, required=True) amount_aura_prorata = fields.Monetary( string="Montant calculé proratisé", currency_field='company_currency_id', @@ -50,14 +55,16 @@ class ScopAuraAccountInvoice(models.Model): 'scop.cotisation.aura'].get_cotiz_aura( partner_id=i.partner_id, wage_cg=i.wage_cg_retenu) - @api.depends('amount_aura_calculated', 'nb_quarter') + @api.depends('amount_aura_calculated', 'nb_quarter_aura') @api.multi def _compute_amount_aura_prorata(self): for i in self: - prorata = int(i.nb_quarter) / 4 - i.amount_aura_prorata = i.cotisation_aura_id.\ - round_to_closest_multiple( - i.amount_aura_calculated * prorata, int(i.nb_quarter)) + if i.cotisation_aura_id \ + and i.amount_aura_calculated and i.nb_quarter_aura: + prorata = i.nb_quarter_aura / 4 + i.amount_aura_prorata = i.cotisation_aura_id.\ + round_to_closest_multiple( + i.amount_aura_calculated * prorata, i.nb_quarter_aura) # ------------------------------------------------------ # Onchange @@ -82,22 +89,109 @@ class ScopAuraAccountInvoice(models.Model): # Override parent # ------------------------------------------------------ @api.multi - def set_scop_contribution(self): - contrib_id = super(ScopAuraAccountInvoice, self).set_scop_contribution() - if self.cotisation_aura_id: - i = 4 - self.nb_quarter - schedule = { - 'quarter_1': self.cotisation_aura_id.trimester_1, - 'quarter_2': self.cotisation_aura_id.trimester_2, - 'quarter_3': self.cotisation_aura_id.trimester_3, - 'quarter_4': self.cotisation_aura_id.trimester_4, + def action_move_create(self): + """ + Complete override parent + Pass invoice in payment_term.compute function to generate payment + schedule + :return: True + """ + account_move = self.env['account.move'] + + for inv in self: + if not inv.journal_id.sequence_id: + raise UserError( + _('Please define sequence on the journal related to this invoice.')) + if not inv.invoice_line_ids.filtered(lambda line: line.account_id): + raise UserError(_('Please add at least one invoice line.')) + if inv.move_id: + continue + + if not inv.date_invoice: + inv.write({'date_invoice': fields.Date.context_today(self)}) + if not inv.date_due: + inv.write({'date_due': inv.date_invoice}) + company_currency = inv.company_id.currency_id + + # create move lines + # (one per invoice line + eventual taxes and analytic lines) + iml = inv.invoice_line_move_line_get() + iml += inv.tax_line_move_line_get() + + diff_currency = inv.currency_id != company_currency + # create one move line for the total and possibly adjust + # the other lines amount + total, total_currency, iml = inv.compute_invoice_totals( + company_currency, iml) + + name = inv.name or '' + if inv.payment_term_id: + totlines = inv.payment_term_id.with_context(currency_id=company_currency.id).compute(total, inv.date_invoice, inv)[0] + res_amount_currency = total_currency + for i, t in enumerate(totlines): + if inv.currency_id != company_currency: + amount_currency = company_currency._convert( + t[1], + inv.currency_id, + inv.company_id, + inv._get_currency_rate_date() or fields.Date.today()) + else: + amount_currency = False + + # last line: add the diff + res_amount_currency -= amount_currency or 0 + if i + 1 == len(totlines): + amount_currency += res_amount_currency + + iml.append({ + 'type': 'dest', + 'name': name, + 'price': t[1], + 'account_id': inv.account_id.id, + 'date_maturity': t[0], + 'amount_currency': diff_currency and amount_currency, + 'currency_id': diff_currency and inv.currency_id.id, + 'invoice_id': inv.id + }) + else: + iml.append({ + 'type': 'dest', + 'name': name, + 'price': total, + 'account_id': inv.account_id.id, + 'date_maturity': inv.date_due, + 'amount_currency': diff_currency and total_currency, + 'currency_id': diff_currency and inv.currency_id.id, + 'invoice_id': inv.id + }) + part = self.env['res.partner']._find_accounting_partner( + inv.partner_id) + line = [(0, 0, self.line_get_convert(l, part.id)) for l in iml] + line = inv.group_lines(iml, line) + + line = inv.finalize_invoice_move_lines(line) + + date = inv.date or inv.date_invoice + move_vals = { + 'ref': inv.reference, + 'line_ids': line, + 'journal_id': inv.journal_id.id, + 'date': date, + 'narration': inv.comment, + } + move = account_move.create(move_vals) + # Pass invoice in method post: used if you want to get the same + # account move reference when creating the same invoice + # after a cancelled one: + move.post(invoice=inv) + # make the invoice point to that move + vals = { + 'move_id': move.id, + 'date': date, + 'move_name': move.name, } - while i != 0: - key = 'quarter_' + str(i) - schedule.pop(key, None) - i -= 1 - contrib_id.update(schedule) - return contrib_id + inv.write(vals) + return True # ------------------------------------------------------ # Global functions diff --git a/models/account_payment_term.py b/models/account_payment_term.py new file mode 100644 index 0000000000000000000000000000000000000000..d2d1c7f191d312148fa7c67de81b3f70d6cacc35 --- /dev/null +++ b/models/account_payment_term.py @@ -0,0 +1,50 @@ +# Copyright 2020 Le Filament +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AccountPaymentTerm(models.Model): + _inherit = 'account.payment.term' + + is_contribution = fields.Boolean('Conditions de paiement des cotisations') + + def compute(self, value, date_ref=False, invoice=None): + """ + Override la fonction compute du modèle account.payment.term + La fonction initiale checke les conditions de paiement et crée + les lignes de paiement associées + + L'héritage permet de créer un échéancier en fonction de celui + défini sur la base de cotisations + """ + date_ref = date_ref or fields.Date.today() + amount = value + sign = value < 0 and -1 or 1 + if self.env.context.get('currency_id'): + currency = self.env['res.currency'].browse( + self.env.context['currency_id']) + else: + currency = self.env.user.company_id.currency_id + + # si base de cotisation + if self.is_contribution and invoice and invoice.cotisation_aura_id: + result = [] + base_contrib_field = 'cotisation_aura_id' + trimesters = { + 4: invoice[base_contrib_field].trimester_1, + 3: invoice[base_contrib_field].trimester_2, + 2: invoice[base_contrib_field].trimester_3, + 1: invoice[base_contrib_field].trimester_4, + } + for i in range(invoice.nb_quarter_aura, 0, -1): + # Gestion de l'arrondi de la division + if i == 1: + amt = currency.round(amount) + else: + amt = currency.round(value / invoice.nb_quarter_aura) + result.append((fields.Date.to_string(trimesters.get(i)), amt)) + amount -= amt + return [result] + else: + return super(AccountPaymentTerm, self).compute(value, date_ref) diff --git a/models/res_company.py b/models/res_company.py index 87ad537a2707d163757b108900b9b7386306a8c1..007c9f4a0cdee39e0c4b564d11d6cda170bbdb7f 100644 --- a/models/res_company.py +++ b/models/res_company.py @@ -15,3 +15,9 @@ class ScopCotisationAuraCompany(models.Model): string='Article de cotisation AURA', domain="[('sale_ok', '=', True), ('company_id', '=', company_id)]" ) + + contribution_default_payment_term_id = fields.Many2one( + comodel_name='account.payment.term', + string="Conditions de paiement par défaut pour les cotisations", + domain=[('is_contribution', '=', True)], + ) diff --git a/models/res_config_settings.py b/models/res_config_settings.py index fccd99da79bc5aeec0de0ca5963e3b69c67a9c4b..48150bebec86df0e1c6b0a4bae1ea222161e4d02 100644 --- a/models/res_config_settings.py +++ b/models/res_config_settings.py @@ -19,6 +19,14 @@ class CotisationsAuraConfigSettings(models.TransientModel): domain="[('sale_ok', '=', True), ('company_id', '=', company_id)]" ) + contribution_default_payment_term_id = fields.Many2one( + comodel_name='account.payment.term', + related="company_id.contribution_default_payment_term_id", + readonly=False, + string="Conditions de paiement par défaut pour les cotisations", + domain=[('is_contribution', '=', True)], + ) + @api.onchange('is_contribution') def _onchange_is_contribution_aura(self): if not self.is_contribution: diff --git a/models/scop_cotisation_aura.py b/models/scop_cotisation_aura.py index 49fa052df8abeac909f35297bb5ebed50de4d38d..e8087eae75433900f21df58ef95bae5a8fc8b0c9 100755 --- a/models/scop_cotisation_aura.py +++ b/models/scop_cotisation_aura.py @@ -39,6 +39,13 @@ class ScopCotisationAura(models.Model): compute='_compute_state', store=True) + payment_term_id = fields.Many2one( + comodel_name='account.payment.term', + string="Conditions de paiement", + domain=[('is_contribution', '=', True)], + required=True, + ) + amount_total = fields.Monetary( string="Montant total", compute='_compute_amount_total', store=True, @@ -308,6 +315,76 @@ class ScopCotisationAura(models.Model): # ------------------------------------------------------ # Global functions # ------------------------------------------------------ + def create_contribution_aura( + self, partner, amount, nb_quarter_aura, liasse=None, date=False): + """ + Create invoice from Contribution Base + :param partner: partner_id + :param liasse: liasse_fiscale_id (reference) + :param amount: contribution amount (float) + :param nb_quarter_aura: nb_quarter_aura (Selection) + :param date: date invoice + :return: invoice + """ + product_aura = self.company_id.contribution_aura_id + type_contrib_ur = self.env.ref('cgscop_partner.riga_14399').id + Invoice = self.env['account.invoice'] + InvoiceLine = self.env['account.invoice.line'] + + domain = [ + ('partner_id', '=', partner.id), + ('year', '=', self.year), + ('type_contribution_id', '=', type_contrib_ur), + ] + + exisiting_invoice = Invoice.search(domain) + + if not exisiting_invoice: + date_invoice = date if date else self.date_cotisation + journal_id = self.company_id.contribution_journal_id + account_id = partner.property_account_receivable_id + member_invoice = Invoice.create({ + 'partner_id': partner.id, + 'liasse_fiscale_id': liasse.id, + 'type': 'out_invoice', + 'year': self.year, + 'is_contribution': True, + 'type_contribution_id': type_contrib_ur, + 'journal_id': journal_id.id, + 'state': 'draft', + 'account_id': account_id.id, + 'payment_term_id': self.payment_term_id.id, + 'payment_mode_id': partner.customer_payment_mode_id.id, + 'date_invoice': date_invoice, + 'cotisation_aura_id': self.id, + 'wage_cg_connu': liasse.wage_cg, + 'wage_cg_retenu': liasse.wage_cg, + 'nb_quarter_aura': nb_quarter_aura + }) + else: + member_invoice = exisiting_invoice + + # Création de la ligne CG Scop + exisiting_invoice_line_ids = InvoiceLine.search([ + ('invoice_id', '=', member_invoice.id), + ('product_id', '=', product_aura.id) + ]) + if not exisiting_invoice_line_ids: + InvoiceLine.create({ + 'invoice_id': member_invoice.id, + 'product_id': product_aura.id, + 'account_id': product_aura.property_account_income_id.id, + 'invoice_line_tax_ids': [(6, 0, product_aura.taxes_id.ids)], + 'name': product_aura.name, + 'price_unit': amount + }) + else: + exisiting_invoice_line_ids[0].write({ + 'price_unit': amount + }) + + return member_invoice + @api.multi def get_members(self): self.ensure_one() @@ -382,14 +459,16 @@ class ScopCotisationAura(models.Model): return plancher else: return cotiz + else: + return plancher else: return cotiz # ------------------------------------------------------ # Threading task # ------------------------------------------------------ - def process_cotiz_generate(self, partner_ids, - cotiz_aura_task, nb_quarter=4, date=False): + def process_cotiz_generate(self, partner_ids, cotiz_aura_task, + nb_quarter_aura=4, date=False): """ Process de génération des cotiz Aura en background """ @@ -407,10 +486,7 @@ class ScopCotisationAura(models.Model): task = cotiz_aura_task.with_env(new_env) new_self = self.with_env(new_env) - # Definition of specific params - product_aura_id = new_self.company_id.contribution_aura_id - type_cotisation_ur = new_self.env.ref( - 'cgscop_partner.riga_14399').id + # Definition of specific param # Creation of cotiz invoice for each member for member_id in partner_ids: @@ -419,16 +495,10 @@ class ScopCotisationAura(models.Model): liasse = new_self.get_liasse(member) amount_aura = new_self.round_to_closest_multiple( new_self.get_cotiz_aura(member, liasse), 4) - invoice_cotiz_aura = new_self.create_contribution( - product_aura_id, member, type_cotisation_ur, - liasse, amount_aura, date) - invoice_cotiz_aura.write({ - 'cotisation_aura_id': new_self.id, - 'wage_cg_connu': liasse.wage_cg, - 'wage_cg_retenu': liasse.wage_cg, - 'nb_quarter': nb_quarter - }) - if nb_quarter != 4: + invoice_cotiz_aura = \ + new_self.create_contribution_aura( + member, amount_aura, nb_quarter_aura, liasse, date) + if nb_quarter_aura != 4: invoice_cotiz_aura.recalcul_cotiz_aura() cotiz_created += 1 diff --git a/views/account_invoice.xml b/views/account_invoice.xml index 34a00fd026be73a2b20a67f969c396440be6b01e..e9d897711b36fffc8ed035dd8b157d6e4c58df1d 100644 --- a/views/account_invoice.xml +++ b/views/account_invoice.xml @@ -29,7 +29,7 @@ <group> <field name="wage_cg_retenu" attrs="{'readonly': [('state', '!=', 'draft')], 'invisible': [('cotisation_aura_id', '=', False)]}"/> <field name="amount_aura_calculated" attrs="{'invisible': [('cotisation_aura_id', '=', False)]}"/> - <field name="nb_quarter" widget="radio" options="{'horizontal': true}" attrs="{'readonly': [('state', '!=', 'draft')], 'invisible': [('cotisation_aura_id', '=', False)]}"/> + <field name="nb_quarter_aura" widget="radio" options="{'horizontal': true}" attrs="{'readonly': [('state', '!=', 'draft')], 'invisible': [('cotisation_aura_id', '=', False)]}"/> <field name="amount_aura_prorata" attrs="{'invisible': [('cotisation_aura_id', '=', False)]}"/> <button name="recalcul_cotiz_aura" type="object" string="Affecter le nouveau montant" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('cotisation_aura_id', '=', False)]}"/> </group> diff --git a/views/account_payment_term.xml b/views/account_payment_term.xml new file mode 100644 index 0000000000000000000000000000000000000000..926975d35503140dc179961186dbd57f571e5261 --- /dev/null +++ b/views/account_payment_term.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!-- Copyright 2020 Le Filament + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> + <data> + + <record id="view_payment_term_form_inherited" model="ir.ui.view"> + <field name="name">account.payment.term.form</field> + <field name="model">account.payment.term</field> + <field name="inherit_id" ref="account.view_payment_term_form"/> + <field name="arch" type="xml"> + <xpath expr="//group" position="after"> + <group name="schedule"> + <field name="is_contribution" widget="boolean_toggle"/> + </group> + </xpath> + </field> + </record> + + </data> +</odoo> \ No newline at end of file diff --git a/views/report_cotisation_aura.xml b/views/report_cotisation_aura.xml index cd1994776b3b749f89c8c6b74fcd57b3612d8a78..7e6476e24c40bbf2386c74f15ed0568c73277388 100644 --- a/views/report_cotisation_aura.xml +++ b/views/report_cotisation_aura.xml @@ -41,14 +41,14 @@ Ce montant est calculé de la façon suivante : </p> <p> - Masse salariale <span t-esc="str(o.year - 2)"/> : <span t-field="o.wage_cg_retenu" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/> x 0,50% = <span t-field="o.amount_total"/> * + Masse salariale <span t-esc="str(o.year - 2)"/> : <span t-field="o.wage_cg_retenu" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/> x 0,50% x prorata du nombre de trimestres de cotisations = <span t-field="o.amount_total"/> * </p> <p style="font-style: italic; font-size: 12px;"> * Pour mémoire, à compter de 2016, les masses salariales inférieures à 60 000,00 € génèrent une cotisation forfaitaire annuelle de 300,00 €, soit 75,00 € par trimestre. </p> <p> <t t-if="o.is_sdd"> - Ce montant vous sera prélevé en <span t-field="o.nb_quarter"/> échéances trimestrielles comme décrit dans le tableau suivant : + Ce montant vous sera prélevé en <span t-field="o.nb_quarter_aura"/> échéances trimestrielles comme décrit dans le tableau suivant : </t> <t t-else=""> En vous remerciant de vos règlements aux échéances suivantes : diff --git a/views/res_config_settings.xml b/views/res_config_settings.xml index 0170c88d6c0dbfa04c7d5ced454f6bc3a17145ef..a9df8ef8475d0bff2b78652e821a96a421ec8102 100644 --- a/views/res_config_settings.xml +++ b/views/res_config_settings.xml @@ -24,6 +24,13 @@ <div><label for="contribution_aura_id"/></div> <field name="contribution_aura_id" options="{'no_open': True, 'no_create': True}" attrs="{'required': [('is_contribution_aura', '=', True)]}"/> </div> + <div class="o_setting_right_pane"> + <label for="contribution_default_payment_term_id"/> + <div class="text-muted"> + Conditions de paiement par défault + </div> + <field name="contribution_default_payment_term_id" options="{'no_open': True, 'no_create': True}"/> + </div> </div> </xpath> </field> diff --git a/wizard/scop_cotisation_aura_wizard.py b/wizard/scop_cotisation_aura_wizard.py index c93c7be6294b084bb7ee99f8327b78e31854041b..e9026ef220c90a6568c3ba538e8182d017460898 100644 --- a/wizard/scop_cotisation_aura_wizard.py +++ b/wizard/scop_cotisation_aura_wizard.py @@ -19,13 +19,10 @@ class ScopCotisationWizard(models.TransientModel): cotisation_aura_id = fields.Many2one( comodel_name='scop.cotisation.aura', string='Base de cotisation') - nb_quarter = fields.Selection( + nb_quarter_aura = fields.Selection( string='Nombre de trimestres de cotisation', - selection=[('1', '1'), - ('2', '2'), - ('3', '3'), - ('4', '4')], - default='4', + selection=[(1, 1), (2, 2), (3, 3), (4, 4)], + default=4, required=True) type = fields.Selection([ ('all', 'Toutes les coop nouvelles adhérentes'), @@ -100,7 +97,7 @@ class ScopCotisationWizard(models.TransientModel): threaded_cotiz = threading.Thread( target=self.cotisation_aura_id.process_cotiz_generate, args=(members_to_invoice, cotiz_aura_task, - self.nb_quarter, self.date)) + self.nb_quarter_aura, self.date)) threaded_cotiz.start() else: message = ("<p class='text-center'>Tous les appels de cotisations " diff --git a/wizard/scop_cotisation_aura_wizard.xml b/wizard/scop_cotisation_aura_wizard.xml index e8a7f509f1729d84250648bf37ca1a116200ad2d..01b3bdccec865d12bedda75861eba8c1a1b517b0 100644 --- a/wizard/scop_cotisation_aura_wizard.xml +++ b/wizard/scop_cotisation_aura_wizard.xml @@ -14,7 +14,7 @@ <group> <field name="year" required="1" invisible="1"/> <field name="date" required="1"/> - <field name="nb_quarter" required="1"/> + <field name="nb_quarter_aura" required="1"/> <field name="type" widget="radio"/> <field name="partner_ids" attrs="{'invisible': [('type', '=', 'all')]}" widget="many2many_tags" options="{'no_create': True, 'no_open': True}"/> </group>