From ebe92a17d32b9f2075f0ec94763269a63f3888cc Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 15 Jun 2020 18:38:14 +0200 Subject: [PATCH 01/61] Add module account_invoice_overdue_reminder --- account_invoice_overdue_reminder/__init__.py | 2 + .../__manifest__.py | 32 + .../data/mail_template.xml | 82 +++ .../data/overdue_reminder_result.xml | 47 ++ .../migrations/12.0.2.0.0/post-migration.py | 52 ++ .../migrations/12.0.2.0.0/pre-migration.py | 11 + .../models/__init__.py | 7 + .../models/account_invoice.py | 60 ++ .../account_invoice_overdue_reminder.py | 61 ++ .../models/company.py | 38 ++ .../models/config_settings.py | 20 + .../models/overdue_reminder_action.py | 66 +++ .../models/overdue_reminder_result.py | 20 + .../models/partner.py | 13 + .../readme/CONFIGURATION.rst | 9 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 31 + .../readme/USAGE.rst | 12 + .../security/ir.model.access.csv | 7 + .../security/rule.xml | 18 + .../views/account_invoice.xml | 50 ++ .../account_invoice_overdue_reminder.xml | 109 ++++ .../views/config_settings.xml | 46 ++ .../views/overdue_reminder_action.xml | 101 ++++ .../views/overdue_reminder_result.xml | 62 ++ .../views/partner.xml | 25 + .../views/report.xml | 19 + .../views/report_overdue_reminder.xml | 94 +++ .../wizard/__init__.py | 1 + .../wizard/overdue_reminder_wizard.py | 558 ++++++++++++++++++ .../wizard/overdue_reminder_wizard_view.xml | 241 ++++++++ 31 files changed, 1895 insertions(+) create mode 100644 account_invoice_overdue_reminder/__init__.py create mode 100644 account_invoice_overdue_reminder/__manifest__.py create mode 100644 account_invoice_overdue_reminder/data/mail_template.xml create mode 100644 account_invoice_overdue_reminder/data/overdue_reminder_result.xml create mode 100644 account_invoice_overdue_reminder/migrations/12.0.2.0.0/post-migration.py create mode 100644 account_invoice_overdue_reminder/migrations/12.0.2.0.0/pre-migration.py create mode 100644 account_invoice_overdue_reminder/models/__init__.py create mode 100644 account_invoice_overdue_reminder/models/account_invoice.py create mode 100644 account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py create mode 100644 account_invoice_overdue_reminder/models/company.py create mode 100644 account_invoice_overdue_reminder/models/config_settings.py create mode 100644 account_invoice_overdue_reminder/models/overdue_reminder_action.py create mode 100644 account_invoice_overdue_reminder/models/overdue_reminder_result.py create mode 100644 account_invoice_overdue_reminder/models/partner.py create mode 100644 account_invoice_overdue_reminder/readme/CONFIGURATION.rst create mode 100644 account_invoice_overdue_reminder/readme/CONTRIBUTORS.rst create mode 100644 account_invoice_overdue_reminder/readme/DESCRIPTION.rst create mode 100644 account_invoice_overdue_reminder/readme/USAGE.rst create mode 100644 account_invoice_overdue_reminder/security/ir.model.access.csv create mode 100644 account_invoice_overdue_reminder/security/rule.xml create mode 100644 account_invoice_overdue_reminder/views/account_invoice.xml create mode 100644 account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml create mode 100644 account_invoice_overdue_reminder/views/config_settings.xml create mode 100644 account_invoice_overdue_reminder/views/overdue_reminder_action.xml create mode 100644 account_invoice_overdue_reminder/views/overdue_reminder_result.xml create mode 100644 account_invoice_overdue_reminder/views/partner.xml create mode 100644 account_invoice_overdue_reminder/views/report.xml create mode 100644 account_invoice_overdue_reminder/views/report_overdue_reminder.xml create mode 100644 account_invoice_overdue_reminder/wizard/__init__.py create mode 100644 account_invoice_overdue_reminder/wizard/overdue_reminder_wizard.py create mode 100644 account_invoice_overdue_reminder/wizard/overdue_reminder_wizard_view.xml diff --git a/account_invoice_overdue_reminder/__init__.py b/account_invoice_overdue_reminder/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/account_invoice_overdue_reminder/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/account_invoice_overdue_reminder/__manifest__.py b/account_invoice_overdue_reminder/__manifest__.py new file mode 100644 index 000000000..09cda1fd9 --- /dev/null +++ b/account_invoice_overdue_reminder/__manifest__.py @@ -0,0 +1,32 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Overdue Invoice Reminder', + 'version': '12.0.2.0.0', + 'category': 'Accounting', + 'license': 'AGPL-3', + 'summary': 'Simple mail/letter/phone overdue customer invoice reminder ', + 'author': 'Akretion,Odoo Community Association (OCA)', + 'maintainers': ['alexis-via'], + 'website': 'https://github.com/OCA/credit-control', + 'depends': ['account'], + 'data': [ + 'security/ir.model.access.csv', + 'security/rule.xml', + 'wizard/overdue_reminder_wizard_view.xml', + 'views/partner.xml', + 'views/report.xml', + 'views/report_overdue_reminder.xml', + 'views/account_invoice.xml', + 'views/account_invoice_overdue_reminder.xml', + 'views/overdue_reminder_result.xml', + 'views/overdue_reminder_action.xml', + 'views/config_settings.xml', + 'data/overdue_reminder_result.xml', + 'data/mail_template.xml', + ], + 'installable': True, + 'application': True, +} diff --git a/account_invoice_overdue_reminder/data/mail_template.xml b/account_invoice_overdue_reminder/data/mail_template.xml new file mode 100644 index 000000000..7f79eadd5 --- /dev/null +++ b/account_invoice_overdue_reminder/data/mail_template.xml @@ -0,0 +1,82 @@ + + + + + + + + Overdue Invoice Reminder + + + ${object.partner_id.lang} + + ${object.user_id.email or object.company_id.email} + ${object.partner_id.email} + ${object.company_id.name} - Overdue invoice reminder n°${object.counter} + +

Dear customer,

+ +

According to our books, the following invoices are overdue:

+ + + + + + + + + + + + + +% for inv in object.invoice_ids: + + + + + + + + + + + +% endfor +% for (currency, total_residual) in object.total_residual(): + + + + + + + + + + +% endfor +
Invoice NumberInvoice DatePayment TermsDue DateOrder Ref.Total UntaxedTotalResidualPast Reminders
${inv.number}${format_date(inv.date_invoice)}${inv.payment_term_id.name or ''}${format_date(inv.date_due)}${inv.name or ''}${format_amount(inv.amount_untaxed_invoice_signed, inv.currency_id)}${format_amount(inv.amount_total_signed, inv.currency_id)}${format_amount(inv.residual_signed, inv.currency_id)}${inv.overdue_reminder_counter}
Total Residual in ${currency.name}:${format_amount(total_residual, currency)}
+ +

If you made a payment for these invoices a few days ago, please ignore this email.

+ +% if object.company_id.overdue_reminder_attach_invoice: +

You will find enclosed the overdue invoices.

+% endif + +% if object.counter > 2: +

Despite several reminders, we are disappointed to see that these overdue invoices are still unpaid. In order to avoid legal proceedings, we urge you to paid these overdue invoices in the next days.

+% endif + +

Regards,

+ + +]]>
+
+ + +
diff --git a/account_invoice_overdue_reminder/data/overdue_reminder_result.xml b/account_invoice_overdue_reminder/data/overdue_reminder_result.xml new file mode 100644 index 000000000..dbd1c8f20 --- /dev/null +++ b/account_invoice_overdue_reminder/data/overdue_reminder_result.xml @@ -0,0 +1,47 @@ + + + + + + + + Message left on voicemail + 10 + + + + Unreachable + 20 + + + + Invoice not received + 30 + + + + Invoice waiting approval + 40 + + + + Invoice dispute + 50 + + + + Invoice in payment pipe + 60 + + + + Payment sent + 70 + + + + diff --git a/account_invoice_overdue_reminder/migrations/12.0.2.0.0/post-migration.py b/account_invoice_overdue_reminder/migrations/12.0.2.0.0/post-migration.py new file mode 100644 index 000000000..8b491d96a --- /dev/null +++ b/account_invoice_overdue_reminder/migrations/12.0.2.0.0/post-migration.py @@ -0,0 +1,52 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, SUPERUSER_ID + + +def migrate(cr, version): + if not version: + return + + with api.Environment.manage(): + env = api.Environment(cr, SUPERUSER_ID, {}) + orao = env['overdue.reminder.action'] + aioro = env['account.invoice.overdue.reminder'] + + # The system is designed so that you can't + # send 2 reminders for the same customer the + # same day + # So, in order to create overdue.reminder.action, we + # read account.invoice.overdue.reminder and we group by + # date/company/partner + cr.execute( + """ + SELECT id, partner_id as commercial_partner_id, date, user_id, + reminder_type, result_id, result_notes, mail_id, company_id + FROM account_invoice_overdue_reminder + """) + tmp = {} # (key = date, company, commercial_partner_id) + # value = vals with list of ids + for old in cr.dictfetchall(): + key = (old['date'], old['company_id'], old['commercial_partner_id']) + if key in tmp: + tmp[key]['reminder_ids'].append(old['id']) + else: + tmp[key] = { + 'reminder_ids': [old['id']], + 'date': old['date'], + 'commercial_partner_id': old['commercial_partner_id'], + 'partner_id': old['commercial_partner_id'], + 'user_id': old['user_id'], + 'reminder_type': old['reminder_type'], + 'result_id': old['result_id'], + 'result_notes': old['result_notes'], + 'mail_id': old['mail_id'], + 'company_id': old['company_id'], + } + for vals in tmp.values(): + reminder_ids = vals.pop('reminder_ids') + action = orao.create(vals) + reminders = aioro.browse(reminder_ids) + reminders.write({'action_id': action.id}) diff --git a/account_invoice_overdue_reminder/migrations/12.0.2.0.0/pre-migration.py b/account_invoice_overdue_reminder/migrations/12.0.2.0.0/pre-migration.py new file mode 100644 index 000000000..6b278bc9f --- /dev/null +++ b/account_invoice_overdue_reminder/migrations/12.0.2.0.0/pre-migration.py @@ -0,0 +1,11 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +def migrate(cr, version): + if not version: + return + + cr.execute( + 'DELETE from overdue_reminder_action') diff --git a/account_invoice_overdue_reminder/models/__init__.py b/account_invoice_overdue_reminder/models/__init__.py new file mode 100644 index 000000000..8799acc16 --- /dev/null +++ b/account_invoice_overdue_reminder/models/__init__.py @@ -0,0 +1,7 @@ +from . import company +from . import config_settings +from . import partner +from . import account_invoice +from . import overdue_reminder_result +from . import overdue_reminder_action +from . import account_invoice_overdue_reminder diff --git a/account_invoice_overdue_reminder/models/account_invoice.py b/account_invoice_overdue_reminder/models/account_invoice.py new file mode 100644 index 000000000..4237791fe --- /dev/null +++ b/account_invoice_overdue_reminder/models/account_invoice.py @@ -0,0 +1,60 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + no_overdue_reminder = fields.Boolean( + string='Disable Overdue Reminder', + track_visibility='onchange') + overdue_reminder_ids = fields.One2many( + 'account.invoice.overdue.reminder', + 'invoice_id', + string='Overdue Reminder Action History') + overdue_reminder_last_date = fields.Date( + compute='_compute_overdue_reminder', + string='Last Overdue Reminder Date', store=True) + overdue_reminder_counter = fields.Integer( + string='Overdue Reminder Count', store=True, + compute='_compute_overdue_reminder', + help="This counter is not increased in case of phone reminder.") + overdue = fields.Boolean(compute='_compute_overdue') + + _sql_constraints = [( + 'counter_positive', + 'CHECK(overdue_reminder_counter >= 0)', + 'Overdue Invoice Counter must always be positive')] + + @api.depends('type', 'state', 'date_due') + def _compute_overdue(self): + today = fields.Date.context_today(self) + for inv in self: + overdue = False + if ( + inv.type == 'out_invoice' and + inv.state == 'open' and + inv.date_due < today): + overdue = True + inv.overdue = overdue + + @api.depends( + 'overdue_reminder_ids.action_id.date', + 'overdue_reminder_ids.counter', + 'overdue_reminder_ids.action_id.reminder_type') + def _compute_overdue_reminder(self): + aioro = self.env['account.invoice.overdue.reminder'] + for inv in self: + reminder = aioro.search( + [('invoice_id', '=', inv.id)], order='action_date desc', limit=1) + date = reminder and reminder.action_date or False + counter_reminder = aioro.search([ + ('invoice_id', '=', inv.id), + ('action_reminder_type', 'in', ('mail', 'post'))], + order='action_date desc, id desc', limit=1) + counter = counter_reminder and counter_reminder.counter or False + inv.overdue_reminder_last_date = date + inv.overdue_reminder_counter = counter diff --git a/account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py b/account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py new file mode 100644 index 000000000..45178b8c0 --- /dev/null +++ b/account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py @@ -0,0 +1,61 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class AccountInvoiceOverdueReminder(models.Model): + _name = 'account.invoice.overdue.reminder' + _description = 'Overdue Invoice Reminder Action History' + _order = 'id desc' + + # For the link to invoice: why a M2O and not a M2M ? + # Because of the "counter" field: a single reminder action for a customer, + # the "counter" may not be the same for each invoice + invoice_id = fields.Many2one( + 'account.invoice', string='Invoice', ondelete='cascade', readonly=True) + action_id = fields.Many2one( + 'overdue.reminder.action', string='Overdue Reminder Action', + ondelete='cascade') + action_commercial_partner_id = fields.Many2one( + related='action_id.commercial_partner_id', store=True) + action_partner_id = fields.Many2one( + related='action_id.partner_id', store=True) + action_date = fields.Date(related='action_id.date', store=True) + action_user_id = fields.Many2one(related='action_id.user_id') + action_reminder_type = fields.Selection( + related='action_id.reminder_type', store=True) + action_result_id = fields.Many2one( + related='action_id.result_id', readonly=False) + action_result_notes = fields.Text( + related='action_id.result_notes', readonly=False) + action_mail_id = fields.Many2one( + related='action_id.mail_id') + action_mail_state = fields.Selection( + related='action_id.mail_id.state', string='E-mail Status') + counter = fields.Integer(readonly=True) + company_id = fields.Many2one( + related='invoice_id.company_id', store=True) + + _sql_constraints = [( + 'counter_positive', + 'CHECK(counter >= 0)', + 'Counter must always be positive')] + + @api.constrains('invoice_id') + def invoice_id_check(self): + for action in self: + if action.invoice_id and action.invoice_id.type != 'out_invoice': + raise ValidationError(_( + "An overdue reminder can only be attached " + "to a customer invoice")) + + @api.depends('invoice_id', 'counter') + def name_get(self): + res = [] + for rec in self: + name = _('%s Reminder %d') % (rec.invoice_id.number, rec.counter) + res.append((rec.id, name)) + return res diff --git a/account_invoice_overdue_reminder/models/company.py b/account_invoice_overdue_reminder/models/company.py new file mode 100644 index 000000000..65904039c --- /dev/null +++ b/account_invoice_overdue_reminder/models/company.py @@ -0,0 +1,38 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, _ + + +class ResCompany(models.Model): + _inherit = 'res.company' + + overdue_reminder_attach_invoice = fields.Boolean( + string='Attach Invoices to Overdue Reminder E-mails', default=True) + overdue_reminder_start_days = fields.Integer( + string='Default Overdue Reminder Trigger Delay (days)') + overdue_reminder_min_interval_days = fields.Integer( + string='Default Overdue Reminder Minimum Interval (days)', default=5) + overdue_reminder_interface = fields.Selection( + '_overdue_reminder_interface_selection', + string='Default Overdue Reminder Wizard Interface', + default='onebyone') + + @api.model + def _overdue_reminder_interface_selection(self): + return [ + ('onebyone', _('One by One')), + ('mass', _('Mass')), + ] + + _sql_constraints = [ + ( + 'overdue_reminder_start_days_positive', + 'CHECK(overdue_reminder_start_days >= 0)', + 'Overdue Reminder Trigger Delay must always be positive'), + ( + 'overdue_reminder_min_interval_days_positive', + 'CHECK(overdue_reminder_min_interval_days > 0)', + 'Overdue Reminder Trigger Delay must always be strictly positive'), + ] diff --git a/account_invoice_overdue_reminder/models/config_settings.py b/account_invoice_overdue_reminder/models/config_settings.py new file mode 100644 index 000000000..2e2e34541 --- /dev/null +++ b/account_invoice_overdue_reminder/models/config_settings.py @@ -0,0 +1,20 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + overdue_reminder_attach_invoice = fields.Boolean( + related='company_id.overdue_reminder_attach_invoice', readonly=False) + overdue_reminder_start_days = fields.Integer( + related='company_id.overdue_reminder_start_days', readonly=False) + overdue_reminder_min_interval_days = fields.Integer( + related='company_id.overdue_reminder_min_interval_days', + readonly=False) + overdue_reminder_interface = fields.Selection( + related='company_id.overdue_reminder_interface', + readonly=False) diff --git a/account_invoice_overdue_reminder/models/overdue_reminder_action.py b/account_invoice_overdue_reminder/models/overdue_reminder_action.py new file mode 100644 index 000000000..bcb1eb6cf --- /dev/null +++ b/account_invoice_overdue_reminder/models/overdue_reminder_action.py @@ -0,0 +1,66 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, _ + + +class OverdueReminderAction(models.Model): + _name = 'overdue.reminder.action' + _description = 'Overdue Reminder Action History' + _order = 'date desc, id desc' + + commercial_partner_id = fields.Many2one( + 'res.partner', readonly=True, string='Customer', index=True, + domain=[('parent_id', '=', False)]) + partner_id = fields.Many2one( + 'res.partner', readonly=True, string='Contact') + date = fields.Date( + default=fields.Date.context_today, required=True, index=True, + readonly=True) + user_id = fields.Many2one( + 'res.users', string='Performed by', required=True, readonly=True, + ondelete='restrict', default=lambda self: self.env.user) + reminder_type = fields.Selection( + '_reminder_type_selection', default='mail', string='Type', + required=True, readonly=True) + result_id = fields.Many2one( + 'overdue.reminder.result', ondelete='restrict', + string='Info/Result') + result_notes = fields.Text(string='Info/Result Notes') + mail_id = fields.Many2one( + 'mail.mail', string='Reminder E-mail', readonly=True) + mail_state = fields.Selection( + related='mail_id.state', string='E-mail Status') + company_id = fields.Many2one( + 'res.company', string='Company', readonly=True) + reminder_count = fields.Integer( + compute='_compute_invoice_count', store=True, string='Number of invoices') + reminder_ids = fields.One2many( + 'account.invoice.overdue.reminder', 'action_id', readonly=True) + + @api.model + def _reminder_type_selection(self): + return [ + ('mail', _('E-mail')), + ('phone', _('Phone')), + ('post', _('Letter')), + ] + + @api.depends('reminder_ids') + def _compute_invoice_count(self): + rg_res = self.env['account.invoice.overdue.reminder'].read_group( + [('action_id', 'in', self.ids), ('invoice_id', '!=', False)], + ['action_id'], ['action_id']) + mapped_data = dict([(x['action_id'][0], x['action_id_count']) for x in rg_res]) + for rec in self: + rec.reminder_count = mapped_data.get(rec.id, 0) + + @api.depends('commercial_partner_id', 'date') + def name_get(self): + res = [] + for action in self: + name = _('%s, Reminder %s') % ( + action.commercial_partner_id.display_name, action.date) + res.append((action.id, name)) + return res diff --git a/account_invoice_overdue_reminder/models/overdue_reminder_result.py b/account_invoice_overdue_reminder/models/overdue_reminder_result.py new file mode 100644 index 000000000..4bbf342ce --- /dev/null +++ b/account_invoice_overdue_reminder/models/overdue_reminder_result.py @@ -0,0 +1,20 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class OverdueReminderResult(models.Model): + _name = 'overdue.reminder.result' + _description = 'Overdue Invoice Reminder Result/Info' + _order = 'sequence, id desc' + + name = fields.Char(required=True, translate=True) + active = fields.Boolean(default=True) + sequence = fields.Integer() + + _sql_constraints = [( + 'name_unique', + 'unique(name)', + 'This overdue reminder result already exists')] diff --git a/account_invoice_overdue_reminder/models/partner.py b/account_invoice_overdue_reminder/models/partner.py new file mode 100644 index 000000000..3f8afa07f --- /dev/null +++ b/account_invoice_overdue_reminder/models/partner.py @@ -0,0 +1,13 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + # Property of commercial partner, applies for the whole entity + no_overdue_reminder = fields.Boolean( + string='Disable Overdue Invoice Reminder', company_dependent=True) diff --git a/account_invoice_overdue_reminder/readme/CONFIGURATION.rst b/account_invoice_overdue_reminder/readme/CONFIGURATION.rst new file mode 100644 index 000000000..8ddc48931 --- /dev/null +++ b/account_invoice_overdue_reminder/readme/CONFIGURATION.rst @@ -0,0 +1,9 @@ +You should increase the **osv_memory_age_limit** (default value = 1, which means 1 hour) in the Odoo server config file: for example, you can set it to 12 (12 hours). The value must be superior to the duration of the invoicing reminder wizard from the start screen to the end. + +Go to the menu *Invoicing > Configuration > Settings* then go to the section *Overdue Invoice Reminder*: you will be able to configure if you want to attach the overdue invoice to the reminder emails and set default values for some parameters. + +Then, go to the menu *Settings > Technical > E-mail > Templates* and search for the mail template *Overdue Invoice Reminder*. You can edit the subject and the body of this email template. If you are in a multi-lang setup, don't forget to also update the translations. + +Go to the menu *Invoicing > Configuration > Management > Invoice Reminder Results* and customize the list of entries. + +If `py3o `_ is your favorite reporting engine for Odoo (with the module *report_py3o* of the project `OCA/reporting-engine `_), you can use the sample py3o report for the overdue reminder letter available in the module *account_invoice_overdue_reminder_py3o* of Akretion's `py3o report templates `_ project. diff --git a/account_invoice_overdue_reminder/readme/CONTRIBUTORS.rst b/account_invoice_overdue_reminder/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..ff65d68ce --- /dev/null +++ b/account_invoice_overdue_reminder/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Alexis de Lattre diff --git a/account_invoice_overdue_reminder/readme/DESCRIPTION.rst b/account_invoice_overdue_reminder/readme/DESCRIPTION.rst new file mode 100644 index 000000000..5aa9e9652 --- /dev/null +++ b/account_invoice_overdue_reminder/readme/DESCRIPTION.rst @@ -0,0 +1,31 @@ +This Odoo module is designed to send overdue invoice reminders to customers. It handles reminders by e-mail, letter and phone. + +This module is an alternative to the OCA module *account_credit_control*. Why another module for invoice reminders ? Because the module *account_credit_control* is quite complex (we experienced that some users find it too complex and eventually stop using it) and its interface is designed to send massive volume of reminders. + +This module has been designed from the start with the following priorities: + +* **keep control**: you must keep tight control on the overdue invoice reminders that you send. Overdue invoice reminders are part of the communication with your customers, and this is very important to keep a good relation with your customers. +* **usability**: the module is easy to configure and easy to use. +* **no accounting skills needed**: the module can be used by users without accounting skills. It can even be used by salesman! +* **multi-currency**: if you invoice your customer in another currency that your company currency, the invoice reminders only mention the currency of the invoices. And if you invoice a customer with different currencies, the reminder is clear and easy-to-understand by your customer, with a total residual per currency. +* **multi-channel**: supports overdue invoice reminders by e-mail (default), phone and letter. +* **simplicity**: for the developers, the code is small and easy to understand. + +The specifications written before starting the development of this module are written in this `document `_ (in French). + +The module has one important limitation: it sends a reminder for an invoice when it has past it's *Due Date* (which is in fact the *Final Due Date*): if the invoice has a payment term with several lines, it won't send a reminder before the last term is overdue. + +An overdue reminder for a customer always include all the overdue invoices of that customer. + +The module supports a clever per-invoice reminder counter mechanism: + +* the reminder counter is a property of an invoice, +* the reminder counter of each overdue invoice is incremented when sending a reminder by email or by post. It is not incremented for reminders by phone. +* in an email or a letter, the subject will be *Overdue invoice reminder n°N* where N is the maximum value of the counter of the overdue invoices plus one. + +There are two user interfaces to send reminders: + +* the **one-by-one** interface, which displays one screen for each customer that has overdue invoices, one after the other. You should use this interface when you have a reasonable volume of reminders to send (less than 100 overdue reminders for example). It gives you a tight control on the reminders and the possibility to easily and rapidly customize the reminder e-mails. +* the **mass** interface, which displays a list view of all customers that have overdue invoices, and you can process several reminders at the same time (via the *Actions* menu). + +This video tutorial in English will show you how to configure and use the module: `Youtube link `_. diff --git a/account_invoice_overdue_reminder/readme/USAGE.rst b/account_invoice_overdue_reminder/readme/USAGE.rst new file mode 100644 index 000000000..6709a03ca --- /dev/null +++ b/account_invoice_overdue_reminder/readme/USAGE.rst @@ -0,0 +1,12 @@ +Of course, before sending invoice reminders, you must import your bank statements and process them, so that you are up-to-date on customer payments. + +Then, go to the menu *Invoicing > Accounting > Actions > Overdue Invoice Remind*: you will get the start screen where you can: + +* filter the customers that you want to remind (filter by customer or by salesman), +* check that your bank journals are up-to-date, +* choose between the *one-by-one* and *mass* interfaces, +* customize some parameters. + +Then follow the process until the end. + +You can also start the invoice reminder wizard via the button *Overdue Reminder* on an overdue invoice. diff --git a/account_invoice_overdue_reminder/security/ir.model.access.csv b/account_invoice_overdue_reminder/security/ir.model.access.csv new file mode 100644 index 000000000..06b103e78 --- /dev/null +++ b/account_invoice_overdue_reminder/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_overdue_reminder_result_read,Read access on overdue.reminder.result,model_overdue_reminder_result,account.group_account_invoice,1,0,0,0 +access_overdue_reminder_result_full,Full access on overdue.reminder.result,model_overdue_reminder_result,account.group_account_manager,1,1,1,1 +access_overdue_reminder_action_user,Read/create/write on reminder actions,model_overdue_reminder_action,account.group_account_invoice,1,1,1,0 +access_overdue_reminder_action_manager,Full access on reminder actions,model_overdue_reminder_action,account.group_account_manager,1,1,1,1 +access_account_invoice_overdue_reminder_user,Read/create/write on reminder counters,model_account_invoice_overdue_reminder,account.group_account_invoice,1,1,1,0 +access_account_invoice_overdue_reminder_manager,Full access on reminder counters,model_account_invoice_overdue_reminder,account.group_account_manager,1,1,1,1 diff --git a/account_invoice_overdue_reminder/security/rule.xml b/account_invoice_overdue_reminder/security/rule.xml new file mode 100644 index 000000000..ddb45be5d --- /dev/null +++ b/account_invoice_overdue_reminder/security/rule.xml @@ -0,0 +1,18 @@ + + + + + + + + Overdue Invoice Reminder multi-company + + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] + + + + diff --git a/account_invoice_overdue_reminder/views/account_invoice.xml b/account_invoice_overdue_reminder/views/account_invoice.xml new file mode 100644 index 000000000..9b0ad26bb --- /dev/null +++ b/account_invoice_overdue_reminder/views/account_invoice.xml @@ -0,0 +1,50 @@ + + + + + + + + overdue.reminder.customer.invoice.form + account.invoice + + + + + + + + + + + + + + + + + + + + + overdue.reminder.customer.invoice.search + account.invoice + + + + + + + + + + diff --git a/account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml b/account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml new file mode 100644 index 000000000..f4d10d48b --- /dev/null +++ b/account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml @@ -0,0 +1,109 @@ + + + + + + + + account.invoice.overdue.reminder.form + account.invoice.overdue.reminder + +
+ + + + + + + + + + + + + + + +
+
+
+ + + account.invoice.overdue.reminder.norelated.form + account.invoice.overdue.reminder + 100 + +
+ + + + + +
+
+
+ + + account.invoice.overdue.reminder.tree + account.invoice.overdue.reminder + + + + + + + + + + + + + + + + account.invoice.overdue.reminder.norelated.tree + account.invoice.overdue.reminder + 100 + + + + + + + + + + account.invoice.overdue.reminder.search + account.invoice.overdue.reminder + + + + + + + + + + + + + + + + + + + + + Invoice Reminder Counters + account.invoice.overdue.reminder + tree,form + {'overdue_reminder_main_view': True} + + + +
diff --git a/account_invoice_overdue_reminder/views/config_settings.xml b/account_invoice_overdue_reminder/views/config_settings.xml new file mode 100644 index 000000000..2d1cfc876 --- /dev/null +++ b/account_invoice_overdue_reminder/views/config_settings.xml @@ -0,0 +1,46 @@ + + + + + + + + overdue.reminder.res.config.settings.form + res.config.settings + + + +

Overdue Invoice Reminder

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + diff --git a/account_invoice_overdue_reminder/views/overdue_reminder_action.xml b/account_invoice_overdue_reminder/views/overdue_reminder_action.xml new file mode 100644 index 000000000..4b7c96084 --- /dev/null +++ b/account_invoice_overdue_reminder/views/overdue_reminder_action.xml @@ -0,0 +1,101 @@ + + + + + + + + overdue.reminder.action.form + overdue.reminder.action + +
+ + + + + + + + + + + + + + + + +
+
+
+ + + overdue.reminder.action.tree + overdue.reminder.action + + + + + + + + + + + + + overdue.reminder.action.search + overdue.reminder.action + + + + + + + + + + + + + + + + + + + overdue.reminder.action.pivot + overdue.reminder.action + + + + + + + + + + overdue.reminder.action.graph + overdue.reminder.action + + + + + + + + + Invoice Reminder Actions + overdue.reminder.action + pivot,graph,tree,form + {'pivot_measures': ['__count', 'reminder_count']} + + + + +
diff --git a/account_invoice_overdue_reminder/views/overdue_reminder_result.xml b/account_invoice_overdue_reminder/views/overdue_reminder_result.xml new file mode 100644 index 000000000..ea431537e --- /dev/null +++ b/account_invoice_overdue_reminder/views/overdue_reminder_result.xml @@ -0,0 +1,62 @@ + + + + + + + + overdue.reminder.result.form + overdue.reminder.result + +
+ +
+ +
+ + + +
+
+
+
+ + + overdue.reminder.result.tree + overdue.reminder.result + + + + + + + + + + overdue.reminder.result.search + overdue.reminder.result + + + + + + + + + + Invoice Reminder Results + overdue.reminder.result + tree,form + + + + +
diff --git a/account_invoice_overdue_reminder/views/partner.xml b/account_invoice_overdue_reminder/views/partner.xml new file mode 100644 index 000000000..b7643bb4a --- /dev/null +++ b/account_invoice_overdue_reminder/views/partner.xml @@ -0,0 +1,25 @@ + + + + + + + + overdue.reminder.res.partner.form + res.partner + + + + + + + + + + + + diff --git a/account_invoice_overdue_reminder/views/report.xml b/account_invoice_overdue_reminder/views/report.xml new file mode 100644 index 000000000..aac57854a --- /dev/null +++ b/account_invoice_overdue_reminder/views/report.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/account_invoice_overdue_reminder/views/report_overdue_reminder.xml b/account_invoice_overdue_reminder/views/report_overdue_reminder.xml new file mode 100644 index 000000000..acac84946 --- /dev/null +++ b/account_invoice_overdue_reminder/views/report_overdue_reminder.xml @@ -0,0 +1,94 @@ + + + + + + diff --git a/account_invoice_overdue_reminder/wizard/__init__.py b/account_invoice_overdue_reminder/wizard/__init__.py new file mode 100644 index 000000000..62b5c3a6f --- /dev/null +++ b/account_invoice_overdue_reminder/wizard/__init__.py @@ -0,0 +1 @@ +from . import overdue_reminder_wizard diff --git a/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard.py b/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard.py new file mode 100644 index 000000000..42b76047d --- /dev/null +++ b/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard.py @@ -0,0 +1,558 @@ +# Copyright 2020 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, tools, _ +from odoo.exceptions import UserError +from dateutil.relativedelta import relativedelta +import base64 +import logging +logger = logging.getLogger(__name__) + +MOD = 'account_invoice_overdue_reminder' + + +class OverdueReminderStart(models.TransientModel): + _name = 'overdue.reminder.start' + _description = 'Wizard to reminder overdue customer invoice' + + partner_ids = fields.Many2many( + 'res.partner', string='Customers', + domain=[('customer', '=', True), ('parent_id', '=', False)]) + user_ids = fields.Many2many( + 'res.users', string='Salesman') + payment_ids = fields.Many2many( + 'overdue.reminder.start.payment', 'wizard_id', readonly=True) + start_days = fields.Integer( + string='Trigger Delay', + help="Odoo will propose to send an overdue reminder to a customer " + "if it has at least one invoice which is overdue for more than " + "N days (N = trigger delay).") + min_interval_days = fields.Integer( + string='Minimum Delay Since Last Reminder', + help="Odoo will not propose to send a reminder to a customer " + "that already got a reminder for some of the same overdue invoices " + "less than N days ago (N = Minimum Delay Since Last Reminder).") + up_to_date = fields.Boolean( + string='I consider that payments are up-to-date') + company_id = fields.Many2one( + 'res.company', readonly=True, required=True, + default=lambda self: self.env['res.company']._company_default_get()) + interface = fields.Selection( + '_interface_selection', + string='Wizard Interface', + default='onebyone', required=True) + + @api.model + def _interface_selection(self): + return self.env['res.company']._overdue_reminder_interface_selection() + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + amo = self.env['account.move'] + company = self.env.user.company_id + journals = self.env['account.journal'].search([ + ('company_id', '=', company.id), + ('type', 'in', ('bank', 'cash'))]) + payments = [] + for journal in journals: + last = amo.search( + [('journal_id', '=', journal.id)], + order='date desc, id desc', limit=1) + vals = { + 'journal_id': journal.id, + 'last_entry_date': last and last.date or False, + 'last_entry_create_date': last and last.create_date or False, + 'last_entry_create_uid': last and last.create_uid.id or False, + } + payments.append((0, 0, vals)) + res.update({ + 'payment_ids': payments, + 'start_days': company.overdue_reminder_start_days, + 'min_interval_days': company.overdue_reminder_min_interval_days, + }) + return res + + def _prepare_base_domain(self): + base_domain = [ + ('company_id', '=', self.company_id.id), + ('type', '=', 'out_invoice'), + ('state', '=', 'open'), + ('no_overdue_reminder', '=', False), + ] + return base_domain + + def _prepare_remind_trigger_domain(self, base_domain): + today = fields.Date.context_today(self) + limit_date = today + if self.start_days: + limit_date -= relativedelta(days=self.start_days) + domain = base_domain + [('date_due', '<', limit_date)] + if self.partner_ids: + domain.append(('commercial_partner_id', 'in', self.partner_ids.ids)) + if self.user_ids: + domain.append(('user_id', 'in', self.user_ids.ids)) + return domain + + def run(self): + self.ensure_one() + if not self.up_to_date: + raise UserError(_( + "In order to start overdue reminders, you must make sure that " + "customer payments are up-to-date.")) + if self.start_days < 0: + raise UserError(_( + "The trigger delay cannot be negative.")) + if self.min_interval_days < 1: + raise UserError(_( + "The minimum delay since last reminder must be strictly positive.")) + aio = self.env['account.invoice'] + ajo = self.env['account.journal'] + rpo = self.env['res.partner'] + orso = self.env['overdue.reminder.step'] + user_id = self.env.user.id + existing_actions = orso.search([('user_id', '=', user_id)]) + existing_actions.unlink() + payment_journals = ajo.search([ + ('company_id', '=', self.company_id.id), + ('type', 'in', ('bank', 'cash')), + ]) + sale_journals = ajo.search([ + ('company_id', '=', self.company_id.id), + ('type', '=', 'sale'), + ]) + today = fields.Date.context_today(self) + min_interval_date = today - relativedelta(days=self.min_interval_days) + # It is important to understand this: there are 2 search on invoice : + # 1. a first search to know if a partner must be reminded or not + # 2. a second search to get the invoices to remind for that partner + # There are some slight differences between these 2 searches; + # for example: search 1 compares due_date to (today + start_days) + # whereas search 2 compares due_date to today + base_domain = self._prepare_base_domain() + domain = self._prepare_remind_trigger_domain(base_domain) + rg_res = aio.read_group( + domain, + ['commercial_partner_id', 'residual_company_signed'], + ['commercial_partner_id']) + # Sort by residual amount desc + rg_res_sorted = sorted( + rg_res, + key=lambda to_sort: to_sort['residual_company_signed'], + reverse=True) + action_ids = [] + for rg_re in rg_res_sorted: + commercial_partner_id = rg_re['commercial_partner_id'][0] + commercial_partner = rpo.browse(commercial_partner_id) + vals = self._prepare_reminder_step( + commercial_partner, base_domain, min_interval_date, + payment_journals, sale_journals) + if vals: + action = orso.create(vals) + action_ids.append(action.id) + if not action_ids: + raise UserError(_( + "There are no overdue reminders.")) + if self.interface == 'onebyone': + xid = MOD + '.overdue_reminder_step_onebyone_action' + action = self.env.ref(xid).read()[0] + action['res_id'] = action_ids[0] + elif self.interface == 'mass': + action = orso.goto_list_view() + return action + + def _prepare_reminder_step( + self, commercial_partner, base_domain, min_interval_date, + payment_journals, sale_journals): + amlo = self.env['account.move.line'] + if commercial_partner.no_overdue_reminder: + logger.info( + 'Skipping customer %s that has no_overdue_reminder=True', + commercial_partner.display_name) + return False + invs = self.env['account.invoice'].search( + base_domain + [ + ('commercial_partner_id', '=', commercial_partner.id), + ('date_due', '<', fields.Date.context_today(self))]) + assert invs + # Check min interval + if any([ + inv.overdue_reminder_last_date > min_interval_date + for inv in invs + if inv.overdue_reminder_last_date]): + logger.info( + 'Skipping customer %s that has at least one invoice ' + 'with last reminder after %s', + commercial_partner.display_name, + fields.Date.to_string(min_interval_date)) + return False + max_counter = max([inv.overdue_reminder_counter for inv in invs]) + unrec_domain = [ + ('account_id', '=', commercial_partner.property_account_receivable_id.id), + ('partner_id', '=', commercial_partner.id), + ('full_reconcile_id', '=', False), + ('matched_debit_ids', '=', False), + ('matched_credit_ids', '=', False), + ] + unrec_payments = amlo.search( + unrec_domain + [ + ('journal_id', 'in', payment_journals.ids), + ]) + unrec_refunds = amlo.search( + unrec_domain + [ + ('journal_id', 'in', sale_journals.ids), + ('credit', '>', 0), + ]) + warn_unrec = unrec_payments + unrec_refunds + vals = { + 'partner_id': invs[0].partner_id.id, + 'commercial_partner_id': commercial_partner.id, + 'user_id': self.env.user.id, + 'invoice_ids': [(6, 0, invs.ids)], + 'company_id': self.company_id.id, + 'warn_unreconciled_move_line_ids': [(6, 0, warn_unrec.ids)], + 'counter': max_counter + 1, + 'interface': self.interface, + } + return vals + + +class OverdueReminderStartPayment(models.TransientModel): + _name = 'overdue.reminder.start.payment' + _description = 'Status of payments' + + wizard_id = fields.Many2one( + 'overdue.reminder.start', ondelete='cascade') + journal_id = fields.Many2one( + 'account.journal', string='Journal', readonly=True) + last_entry_date = fields.Date( + string='Last Entry', readonly=True) + last_entry_create_date = fields.Datetime( + string='Last Entry Created on', readonly=True) + last_entry_create_uid = fields.Many2one( + 'res.users', string='Last Entry Created by', readonly=True) + + +class OverdueReminderStep(models.TransientModel): + _name = 'overdue.reminder.step' + _description = 'Overdue reminder wizard step' + + partner_id = fields.Many2one( + 'res.partner', required=True, string='Invoicing Contact') + partner_email = fields.Char(related='partner_id.email', readonly=True) + partner_phone = fields.Char(related='partner_id.phone', readonly=True) + partner_mobile = fields.Char(related='partner_id.mobile', readonly=True) + commercial_partner_id = fields.Many2one( + 'res.partner', string='Customer', readonly=True, required=True) + user_id = fields.Many2one('res.users', required=True, readonly=True) + counter = fields.Integer(string="New Remind Counter", readonly=True) + date = fields.Date(default=fields.Date.context_today, readonly=True) + reminder_type = fields.Selection( + '_reminder_type_selection', default='mail', + string='Reminder Type', required=True) + mail_subject = fields.Char(string='Subject') + mail_body = fields.Html() + result_id = fields.Many2one( + 'overdue.reminder.result', string='Call Result/Info') + result_notes = fields.Text(string='Call Notes') + create_activity = fields.Boolean() + activity_type_id = fields.Many2one( + 'mail.activity.type', string='Activity') + activity_summary = fields.Char(string='Summary') + activity_deadline = fields.Date('Deadline') + activity_note = fields.Html(string='Note') + activity_user_id = fields.Many2one( + 'res.users', string='Assigned to', default=lambda self: self.env.user) + letter_printed = fields.Boolean(readonly=True) + invoice_ids = fields.Many2many( + 'account.invoice', string='Overdue Invoices', readonly=True) + company_id = fields.Many2one( + 'res.company', readonly=True, required=True, + default=lambda self: self.env['res.company']._company_default_get()) + warn_unreconciled_move_line_ids = fields.Many2many( + 'account.move.line', string='Unreconciled Payments/Refunds', + readonly=True) + unreconciled_move_line_normal = fields.Boolean( + string='Check if unreconciled payments/refunds above have a good ' + 'reason not to be reconciled with an open invoice') + interface = fields.Char(readonly=True) + state = fields.Selection([ + ('draft', 'Draft'), + ('skipped', 'Skipped'), + ('done', 'Done'), + ], default='draft', readonly=True) + + @api.model + def _reminder_type_selection(self): + return self.env['overdue.reminder.action']._reminder_type_selection() + + @api.model + def create(self, vals): + action = super().create(vals) + commercial_partner = self.env['res.partner'].browse( + vals['commercial_partner_id']) + xmlid = MOD + '.overdue_invoice_reminder_mail_template' + mail_tpl = self.env.ref(xmlid) + mail_tpl_lang = mail_tpl.with_context(lang=commercial_partner.lang or 'en_US') + mail_subject = mail_tpl_lang._render_template( + mail_tpl_lang.subject, self._name, action.id) + mail_body = mail_tpl_lang._render_template( + mail_tpl_lang.body_html, self._name, action.id) + if mail_tpl.user_signature: + signature = self.env.user.signature + if signature: + mail_body = tools.append_content_to_html( + mail_body, signature, plaintext=False) + mail_body = tools.html_sanitize(mail_body) + action.write({ + 'mail_subject': mail_subject, + 'mail_body': mail_body, + }) + return action + + @api.onchange('reminder_type') + def reminder_type_change(self): + if self.reminder_type and self.reminder_type != 'phone': + self.result_id = False + self.result_notes = False + self.create_activity = False + + def next(self): + self.ensure_one() + left = self.search([ + ('state', '=', 'draft'), + ('user_id', '=', self.user_id.id), + ('company_id', '=', self.company_id.id)], limit=1) + if left: + action = self.env.ref( + MOD + '.overdue_reminder_step_onebyone_action').read()[0] + action['res_id'] = left.id + else: + action = self.env.ref( + MOD + '.overdue_reminder_end_action').read()[0] + return action + + def goto_list_view(self): + action = self.env.ref( + MOD + '.overdue_reminder_step_mass_action').read()[0] + return action + + def skip(self): + self.write({'state': 'skipped'}) + if len(self) == 1: + if self.interface == 'onebyone': + action = self.next() + else: + action = self.goto_list_view() + return action + + def _prepare_mail_activity(self): + self.ensure_one() + partner_model_id = self.env.ref('base.model_res_partner').id + if not self.activity_user_id: + raise UserError(_( + "For the reminder of customer '%s', you must assign someone " + "for the activity.") % self.commercial_partner_id.display_name) + if not self.activity_deadline: + raise UserError(_( + "For the reminder of customer '%s', the deadline is missing " + "for the activity.") % self.commercial_partner_id.display_name) + vals = { + 'activity_type_id': self.activity_type_id.id or False, + 'summary': self.activity_summary, + 'date_deadline': self.activity_deadline, + 'user_id': self.activity_user_id.id, + 'note': self.activity_note, + 'res_id': self.commercial_partner_id.id, + 'res_model_id': partner_model_id, + } + return vals + + def check_warnings(self): + self.ensure_one() + for rec in self: + if rec.company_id != self.env.user.company_id: + raise UserError(_( + "User company is different from action company. " + "This should never happen.")) + if ( + rec.warn_unreconciled_move_line_ids and + not rec.unreconciled_move_line_normal): + raise UserError(_( + "Customer '%s' has unreconciled payments/refunds. " + "You should reconcile these payments/refunds and start the " + "overdue remind process again " + "(or check the option to confirm that these unreconciled " + "payments/refunds have a good reason not to be " + "reconciled with an open invoice).") + % rec.commercial_partner_id.display_name) + + def validate(self): + orao = self.env['overdue.reminder.action'] + mao = self.env['mail.activity'] + self.check_warnings() + for rec in self: + vals = {} + if rec.reminder_type == 'mail': + vals = rec.validate_mail() + elif rec.reminder_type == 'phone': + vals = rec.validate_phone() + elif rec.reminder_type == 'post': + vals = rec.validate_post() + rec._prepare_overdue_reminder_action(vals) + orao.create(vals) + if rec.create_activity: + mao.create(self._prepare_mail_activity()) + self.write({'state': 'done'}) + if len(self) == 1: + if self.interface == 'onebyone': + action = self.next() + else: + action = self.goto_list_view() + return action + + def validate_mail(self): + self.ensure_one() + iao = self.env['ir.attachment'] + if not self.mail_subject: + raise UserError(_('Mail subject is empty.')) + if not self.mail_body: + raise UserError(_('Mail body is empty.')) + xmlid = MOD + '.overdue_invoice_reminder_mail_template' + mvals = self.env.ref(xmlid).generate_email(self.id) + mvals.update({ + 'subject': self.mail_subject, + 'body_html': self.mail_body, + }) + mvals.pop('attachment_ids', None) + mvals.pop('attachments', None) + mail = self.env['mail.mail'].create(mvals) + inv_report = self.env['ir.actions.report']._get_report_from_name( + 'account.report_invoice_with_payments') + if self.company_id.overdue_reminder_attach_invoice: + attachment_ids = [] + for inv in self.invoice_ids: + if inv_report.report_type in ('qweb-html', 'qweb-pdf'): + report_bin, report_format = inv_report.render_qweb_pdf([inv.id]) + else: + res = inv_report.render([inv.id]) + if not res: + raise UserError(_( + "Report format '%s' is not supported.") + % inv_report.report_type) + report_bin, report_format = res + # WARN : update when backporting + filename = '%s.%s' % (inv._get_report_base_filename(), report_format) + attach = iao.create({ + 'name': filename, + 'datas_fname': filename, + 'datas': base64.b64encode(report_bin), + 'res_model': 'mail.message', + 'res_id': mail.mail_message_id.id, + }) + attachment_ids.append(attach.id) + mail.write({'attachment_ids': [(6, 0, attachment_ids)]}) + vals = {'mail_id': mail.id} + return vals + + def validate_phone(self): + self.ensure_one() + assert self.reminder_type == 'phone' + vals = { + 'result_id': self.result_id.id or False, + 'result_notes': self.result_notes, + } + return vals + + def validate_post(self): + self.ensure_one() + assert self.reminder_type == 'post' + if not self.letter_printed: + raise UserError(_( + "Remind letter hasn't been printed!")) + return {} + + def _prepare_overdue_reminder_action(self, vals): + vals.update({ + 'user_id': self.user_id.id, + 'reminder_type': self.reminder_type, + 'reminder_ids': [], + 'company_id': self.company_id.id, + 'commercial_partner_id': self.commercial_partner_id.id, + 'partner_id': self.partner_id.id, + }) + for inv in self.invoice_ids: + rvals = {'invoice_id': inv.id} + if self.reminder_type != 'phone': + rvals['counter'] = inv.overdue_reminder_counter + 1 + vals['reminder_ids'].append((0, 0, rvals)) + + def print_letter(self): + self.check_warnings() + self.write({'letter_printed': True}) + action = action = self.env.ref( + MOD + '.overdue_reminder_step_report').with_context( + {'discard_logo_check': True}).report_action(self) + return action + + def print_invoices(self): + # in v12, it seems printing several invoices at the same time + # doesn't work + action = self.env.ref('account.account_invoices')\ + .with_context( + {'discard_logo_check': True}).report_action(self.invoice_ids.ids) + return action + + def total_residual(self): + self.ensure_one() + res = {} + for inv in self.invoice_ids: + if inv.currency_id in res: + res[inv.currency_id] += inv.residual_signed + else: + res[inv.currency_id] = inv.residual_signed + return res.items() + + def _get_report_base_filename(self): + self.ensure_one() + fname = 'overdue_letter-%s' % self.commercial_partner_id.name.replace(' ', '_') + return fname + + +class OverdueReminderEnd(models.TransientModel): + _name = 'overdue.reminder.end' + _description = 'Congratulation end screen for overdue reminder wizard' + + +class OverdueRemindMassUpdate(models.TransientModel): + _name = 'overdue.reminder.mass.update' + _description = 'Update several actions at the same time' + + update_action = fields.Selection([ + ('validate', 'Validate'), + ('reminder_type', 'Change Reminder Type'), + ('skip', 'Skip')], + required=True, readonly=True) + reminder_type = fields.Selection( + '_reminder_type_selection', + string='New Reminder Type') + + @api.model + def _reminder_type_selection(self): + return self.env['overdue.reminder.action']._reminder_type_selection() + + def run(self): + self.ensure_one() + assert self._context.get('active_model') == 'overdue.reminder.step' + actions = self.env['overdue.reminder.step'].browse( + self._context.get('active_ids')) + if self.update_action == 'validate': + actions.validate() + elif self.update_action == 'skip': + actions.skip() + elif self.update_action == 'reminder_type': + if not self.reminder_type: + raise UserError(_("You must select the new reminder type.")) + actions.write({'reminder_type': self.reminder_type}) + return diff --git a/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard_view.xml b/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard_view.xml new file mode 100644 index 000000000..a3c2aefc9 --- /dev/null +++ b/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard_view.xml @@ -0,0 +1,241 @@ + + + + + + + + overdue.reminder.start.form + overdue.reminder.start + +
+ + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+
+ + + Overdue Invoice Remind + overdue.reminder.start + form + new + + + + + + overdue.reminder.step.form + overdue.reminder.step + +
+ + + + + + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- + From 4c15883f106f2793c0f3b75143217ed2ac1f36ef Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 12 Nov 2020 23:55:58 +0100 Subject: [PATCH 06/61] overdue_reminder: Order overdue invoices starting from oldest (NOTE: update your mail templates) Add ability to add contacts as Cc of the reminder email (added to the Cc of the mail template) Add partner_policy with 3 options to give some choice about which contact should be selected to send reminders Access reminders from partner via Action menu --- .../data/mail_template.xml | 2 +- .../account_invoice_overdue_reminder.py | 2 + .../models/company.py | 11 ++++++ .../models/config_settings.py | 5 ++- .../models/overdue_reminder_action.py | 3 +- .../account_invoice_overdue_reminder.xml | 3 +- .../views/config_settings.xml | 4 ++ .../views/overdue_reminder_action.xml | 3 +- .../views/partner.xml | 7 ++++ .../views/report_overdue_reminder.xml | 2 +- .../wizard/overdue_reminder_wizard.py | 38 ++++++++++++++++++- .../wizard/overdue_reminder_wizard_view.xml | 4 +- 12 files changed, 75 insertions(+), 9 deletions(-) diff --git a/account_invoice_overdue_reminder/data/mail_template.xml b/account_invoice_overdue_reminder/data/mail_template.xml index 7f79eadd5..70a2bad17 100644 --- a/account_invoice_overdue_reminder/data/mail_template.xml +++ b/account_invoice_overdue_reminder/data/mail_template.xml @@ -35,7 +35,7 @@ Residual Past Reminders -% for inv in object.invoice_ids: +% for inv in object.invoice_ids.sorted(key='date_invoice'): ${inv.number} ${format_date(inv.date_invoice)} diff --git a/account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py b/account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py index 45178b8c0..d94e92a91 100644 --- a/account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py +++ b/account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py @@ -33,6 +33,8 @@ class AccountInvoiceOverdueReminder(models.Model): related='action_id.result_notes', readonly=False) action_mail_id = fields.Many2one( related='action_id.mail_id') + action_mail_cc = fields.Char( + related='action_id.mail_id.email_cc', readonly=True, string='Cc') action_mail_state = fields.Selection( related='action_id.mail_id.state', string='E-mail Status') counter = fields.Integer(readonly=True) diff --git a/account_invoice_overdue_reminder/models/company.py b/account_invoice_overdue_reminder/models/company.py index 65904039c..80b96bd2b 100644 --- a/account_invoice_overdue_reminder/models/company.py +++ b/account_invoice_overdue_reminder/models/company.py @@ -18,6 +18,9 @@ class ResCompany(models.Model): '_overdue_reminder_interface_selection', string='Default Overdue Reminder Wizard Interface', default='onebyone') + overdue_reminder_partner_policy = fields.Selection( + '_overdue_reminder_partner_policy_selection', + default='last_reminder', string='Contact to Remind') @api.model def _overdue_reminder_interface_selection(self): @@ -26,6 +29,14 @@ def _overdue_reminder_interface_selection(self): ('mass', _('Mass')), ] + @api.model + def _overdue_reminder_partner_policy_selection(self): + return [ + ('last_reminder', 'Last Reminder'), + ('last_invoice', 'Last Invoice'), + ('invoice_contact', 'Invoice Contact'), + ] + _sql_constraints = [ ( 'overdue_reminder_start_days_positive', diff --git a/account_invoice_overdue_reminder/models/config_settings.py b/account_invoice_overdue_reminder/models/config_settings.py index 2e2e34541..5c1946286 100644 --- a/account_invoice_overdue_reminder/models/config_settings.py +++ b/account_invoice_overdue_reminder/models/config_settings.py @@ -16,5 +16,6 @@ class ResConfigSettings(models.TransientModel): related='company_id.overdue_reminder_min_interval_days', readonly=False) overdue_reminder_interface = fields.Selection( - related='company_id.overdue_reminder_interface', - readonly=False) + related='company_id.overdue_reminder_interface', readonly=False) + overdue_reminder_partner_policy = fields.Selection( + related='company_id.overdue_reminder_partner_policy', readonly=False) diff --git a/account_invoice_overdue_reminder/models/overdue_reminder_action.py b/account_invoice_overdue_reminder/models/overdue_reminder_action.py index bcb1eb6cf..2ab659902 100644 --- a/account_invoice_overdue_reminder/models/overdue_reminder_action.py +++ b/account_invoice_overdue_reminder/models/overdue_reminder_action.py @@ -17,7 +17,7 @@ class OverdueReminderAction(models.Model): 'res.partner', readonly=True, string='Contact') date = fields.Date( default=fields.Date.context_today, required=True, index=True, - readonly=True) + readonly=False) user_id = fields.Many2one( 'res.users', string='Performed by', required=True, readonly=True, ondelete='restrict', default=lambda self: self.env.user) @@ -32,6 +32,7 @@ class OverdueReminderAction(models.Model): 'mail.mail', string='Reminder E-mail', readonly=True) mail_state = fields.Selection( related='mail_id.state', string='E-mail Status') + mail_cc = fields.Char(related='mail_id.email_cc', readonly=True) company_id = fields.Many2one( 'res.company', string='Company', readonly=True) reminder_count = fields.Integer( diff --git a/account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml b/account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml index f4d10d48b..40dea6662 100644 --- a/account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml +++ b/account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml @@ -16,12 +16,13 @@ - + + diff --git a/account_invoice_overdue_reminder/views/config_settings.xml b/account_invoice_overdue_reminder/views/config_settings.xml index 2d1cfc876..5f986859f 100644 --- a/account_invoice_overdue_reminder/views/config_settings.xml +++ b/account_invoice_overdue_reminder/views/config_settings.xml @@ -23,6 +23,10 @@