From f792437205f093d1eec2fec2260d9036d5756b61 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 15 Jun 2020 18:38:14 +0200 Subject: [PATCH 01/57] 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 d08e9f16ded94b62348c0c404bfd78f952a32433 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 12 Nov 2020 23:55:58 +0100 Subject: [PATCH 06/57] 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 @@
- + + Overdue Reminder Actions + {'search_default_commercial_partner_id': [active_id]} + overdue.reminder.action + + form +
diff --git a/account_invoice_overdue_reminder/wizard/__init__.py b/account_invoice_overdue_reminder/wizard/__init__.py index 62b5c3a6f..c4ac6ccc8 100644 --- a/account_invoice_overdue_reminder/wizard/__init__.py +++ b/account_invoice_overdue_reminder/wizard/__init__.py @@ -1 +1,2 @@ +from . import res_config_settings 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 index 0872c1b89..f59161854 100644 --- a/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard.py +++ b/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard.py @@ -1,10 +1,11 @@ -# Copyright 2020 Akretion France (http://www.akretion.com/) +# Copyright 2020-2021 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 +from collections import defaultdict import base64 import logging logger = logging.getLogger(__name__) @@ -18,7 +19,7 @@ class OverdueReminderStart(models.TransientModel): partner_ids = fields.Many2many( 'res.partner', string='Customers', - domain=[('customer', '=', True), ('parent_id', '=', False)]) + domain=[('customer_rank', '>', 0), ('parent_id', '=', False)]) user_ids = fields.Many2many( 'res.users', string='Salesman') payment_ids = fields.Many2many( @@ -37,7 +38,7 @@ class OverdueReminderStart(models.TransientModel): 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()) + default=lambda self: self.env.company) interface = fields.Selection( '_interface_selection', string='Wizard Interface', @@ -59,7 +60,7 @@ def _partner_policy_selection(self): def default_get(self, fields_list): res = super().default_get(fields_list) amo = self.env['account.move'] - company = self.env.user.company_id + company = self.env.company journals = self.env['account.journal'].search([ ('company_id', '=', company.id), ('type', 'in', ('bank', 'cash'))]) @@ -86,8 +87,9 @@ def default_get(self, fields_list): def _prepare_base_domain(self): base_domain = [ ('company_id', '=', self.company_id.id), - ('type', '=', 'out_invoice'), - ('state', '=', 'open'), + ('move_type', '=', 'out_invoice'), + ('state', '=', 'posted'), + ('payment_state', 'not in', ('paid', 'reversed', 'in_payment')), ('no_overdue_reminder', '=', False), ] return base_domain @@ -97,7 +99,7 @@ def _prepare_remind_trigger_domain(self, base_domain): limit_date = today if self.start_days: limit_date -= relativedelta(days=self.start_days) - domain = base_domain + [('date_due', '<', limit_date)] + domain = base_domain + [('invoice_date_due', '<', limit_date)] if self.partner_ids: domain.append(('commercial_partner_id', 'in', self.partner_ids.ids)) if self.user_ids: @@ -116,7 +118,7 @@ def run(self): if self.min_interval_days < 1: raise UserError(_( "The minimum delay since last reminder must be strictly positive.")) - aio = self.env['account.invoice'] + amo = self.env['account.move'] ajo = self.env['account.journal'] rpo = self.env['res.partner'] orso = self.env['overdue.reminder.step'] @@ -141,14 +143,14 @@ def run(self): # 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( + rg_res = amo.read_group( domain, - ['commercial_partner_id', 'residual_company_signed'], + ['commercial_partner_id', 'amount_residual_signed'], ['commercial_partner_id']) # Sort by residual amount desc rg_res_sorted = sorted( rg_res, - key=lambda to_sort: to_sort['residual_company_signed'], + key=lambda to_sort: to_sort['amount_residual_signed'], reverse=True) action_ids = [] for rg_re in rg_res_sorted: @@ -165,7 +167,7 @@ def run(self): "There are no overdue reminders.")) if self.interface == 'onebyone': xid = MOD + '.overdue_reminder_step_onebyone_action' - action = self.env.ref(xid).read()[0] + action = self.env.ref(xid).sudo().read()[0] action['res_id'] = action_ids[0] elif self.interface == 'mass': action = orso.goto_list_view() @@ -180,10 +182,10 @@ def _prepare_reminder_step( 'Skipping customer %s that has no_overdue_reminder=True', commercial_partner.display_name) return False - invs = self.env['account.invoice'].search( + invs = self.env['account.move'].search( base_domain + [ ('commercial_partner_id', '=', commercial_partner.id), - ('date_due', '<', fields.Date.context_today(self))]) + ('invoice_date_due', '<', fields.Date.context_today(self))]) assert invs # Check min interval if any([ @@ -225,12 +227,12 @@ def _prepare_reminder_step( partner_id = commercial_partner.address_get( ['invoice'])['invoice'] elif self.partner_policy == 'last_invoice': - last_inv = self.env['account.invoice'].search([ + last_inv = self.env['account.move'].search([ ('company_id', '=', self.company_id.id), - ('type', 'in', ('out_invoice', 'out_refund')), + ('move_type', 'in', ('out_invoice', 'out_refund')), ('commercial_partner_id', '=', commercial_partner.id), - ('state', 'in', ('open', 'in_payment', 'paid')), - ], order='date_invoice desc', limit=1) + ('state', '=', 'posted'), + ], order='invoice_date desc', limit=1) partner_id = last_inv.partner_id.id elif self.partner_policy == 'invoice_contact': partner_id = commercial_partner.address_get( @@ -298,10 +300,10 @@ class OverdueReminderStep(models.TransientModel): '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) + 'account.move', 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()) + default=lambda self: self.env.company) warn_unreconciled_move_line_ids = fields.Many2many( 'account.move.line', string='Unreconciled Payments/Refunds', readonly=True) @@ -321,27 +323,22 @@ def _reminder_type_selection(self): @api.model def create(self, vals): - action = super().create(vals) + step = 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_tpl_lang.subject, self._name, [step.id])[step.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_tpl_lang.body_html, self._name, [step.id])[step.id] mail_body = tools.html_sanitize(mail_body) - action.write({ + step.write({ 'mail_subject': mail_subject, 'mail_body': mail_body, }) - return action + return step @api.onchange('reminder_type') def reminder_type_change(self): @@ -358,16 +355,16 @@ def next(self): ('company_id', '=', self.company_id.id)], limit=1) if left: action = self.env.ref( - MOD + '.overdue_reminder_step_onebyone_action').read()[0] + MOD + '.overdue_reminder_step_onebyone_action').sudo().read()[0] action['res_id'] = left.id else: action = self.env.ref( - MOD + '.overdue_reminder_end_action').read()[0] + MOD + '.overdue_reminder_end_action').sudo().read()[0] return action def goto_list_view(self): action = self.env.ref( - MOD + '.overdue_reminder_step_mass_action').read()[0] + MOD + '.overdue_reminder_step_mass_action').sudo().read()[0] return action def skip(self): @@ -404,7 +401,7 @@ def _prepare_mail_activity(self): def check_warnings(self): self.ensure_one() for rec in self: - if rec.company_id != self.env.user.company_id: + if rec.company_id != self.env.company: raise UserError(_( "User company is different from action company. " "This should never happen.")) @@ -455,7 +452,8 @@ def validate_mail(self): 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 = self.env.ref(xmlid).generate_email( + self.id, ['email_from', 'email_to', 'partner_to', 'reply_to']) cc_list = [p.email for p in self.mail_cc_partner_ids if p.email] if mvals.get('email_cc'): cc_list.append(mvals['email_cc']) @@ -475,7 +473,7 @@ def validate_mail(self): 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]) + report_bin, report_format = inv_report._render_qweb_pdf([inv.id]) else: res = inv_report.render([inv.id]) if not res: @@ -483,11 +481,9 @@ def validate_mail(self): "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, @@ -547,12 +543,9 @@ def print_invoices(self): def total_residual(self): self.ensure_one() - res = {} + res = defaultdict(float) 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 + res[inv.currency_id] += inv.amount_residual * (inv.move_type == 'out_refund' and -1 or 1) return res.items() def _get_report_base_filename(self): diff --git a/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard_view.xml b/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard_view.xml index 292e5f41f..ca077259c 100644 --- a/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard_view.xml +++ b/account_invoice_overdue_reminder/wizard/overdue_reminder_wizard_view.xml @@ -1,13 +1,12 @@ - overdue.reminder.start.form overdue.reminder.start @@ -58,7 +57,7 @@ new - + overdue.reminder.step.form @@ -83,23 +82,25 @@ - - + + - - - - - + + + + + - + + - + + @@ -166,7 +167,6 @@ [('state', '=', 'draft'), ('user_id', '=', uid)] - overdue.reminder.end.form overdue.reminder.end @@ -208,36 +208,34 @@ - - - - - + + Change Reminder Type + overdue.reminder.mass.update + form + {'default_update_action': 'reminder_type'} + + list + new + + + Validate + overdue.reminder.mass.update + form + {'default_update_action': 'validate'} + + list + new + + + Skip + overdue.reminder.mass.update + form + {'default_update_action': 'skip'} + + list + new + diff --git a/account_invoice_overdue_reminder/models/config_settings.py b/account_invoice_overdue_reminder/wizard/res_config_settings.py similarity index 93% rename from account_invoice_overdue_reminder/models/config_settings.py rename to account_invoice_overdue_reminder/wizard/res_config_settings.py index 5c1946286..4f71cb136 100644 --- a/account_invoice_overdue_reminder/models/config_settings.py +++ b/account_invoice_overdue_reminder/wizard/res_config_settings.py @@ -1,4 +1,4 @@ -# Copyright 2020 Akretion France (http://www.akretion.com/) +# Copyright 2020-2021 Akretion France (http://www.akretion.com/) # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). diff --git a/account_invoice_overdue_reminder/views/config_settings.xml b/account_invoice_overdue_reminder/wizard/res_config_settings_view.xml similarity index 65% rename from account_invoice_overdue_reminder/views/config_settings.xml rename to account_invoice_overdue_reminder/wizard/res_config_settings_view.xml index 5f986859f..a9ea73deb 100644 --- a/account_invoice_overdue_reminder/views/config_settings.xml +++ b/account_invoice_overdue_reminder/wizard/res_config_settings_view.xml @@ -1,6 +1,6 @@ @@ -13,31 +13,37 @@ res.config.settings - +

Overdue Invoice Reminder

-
-
+
+
+ +
-
+
+
+
+
+
+
-
-
-
-
From f37a98bed1c84a50b6abcf320a1c304ba9e34a2d Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 11 Feb 2021 19:04:15 +0100 Subject: [PATCH 12/57] account_invoice_overdue_reminder: black, isort and other reformatting --- .../__manifest__.py | 50 +- .../data/mail_template.xml | 18 +- .../data/overdue_reminder_result.xml | 3 +- .../account_invoice_overdue_reminder.py | 70 +- .../models/account_move.py | 75 +- .../models/overdue_reminder_action.py | 91 ++- .../models/overdue_reminder_result.py | 13 +- .../models/res_company.py | 56 +- .../models/res_partner.py | 5 +- .../readme/CONFIGURATION.rst | 2 +- .../security/ir.model.access.csv | 1 - .../security/ir_rule.xml | 9 +- .../account_invoice_overdue_reminder.xml | 136 +++- .../views/account_move.xml | 43 +- .../views/overdue_reminder_action.xml | 110 ++- .../views/overdue_reminder_result.xml | 33 +- .../views/report.xml | 11 +- .../views/report_overdue_reminder.xml | 70 +- .../views/res_partner.xml | 15 +- .../wizard/overdue_reminder_wizard.py | 766 ++++++++++-------- .../wizard/overdue_reminder_wizard_view.xml | 238 ++++-- .../wizard/res_config_settings.py | 18 +- .../wizard/res_config_settings_view.xml | 66 +- 23 files changed, 1159 insertions(+), 740 deletions(-) diff --git a/account_invoice_overdue_reminder/__manifest__.py b/account_invoice_overdue_reminder/__manifest__.py index ab1c08a22..0abe4aa44 100644 --- a/account_invoice_overdue_reminder/__manifest__.py +++ b/account_invoice_overdue_reminder/__manifest__.py @@ -3,30 +3,30 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - 'name': 'Overdue Invoice Reminder', - 'version': '14.0.1.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/ir_rule.xml', - 'wizard/overdue_reminder_wizard_view.xml', - 'views/res_partner.xml', - 'views/report.xml', - 'views/report_overdue_reminder.xml', - 'views/account_move.xml', - 'views/account_invoice_overdue_reminder.xml', - 'views/overdue_reminder_result.xml', - 'views/overdue_reminder_action.xml', - 'wizard/res_config_settings_view.xml', - 'data/overdue_reminder_result.xml', - 'data/mail_template.xml', + "name": "Overdue Invoice Reminder", + "version": "14.0.1.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/ir_rule.xml", + "wizard/overdue_reminder_wizard_view.xml", + "views/res_partner.xml", + "views/report.xml", + "views/report_overdue_reminder.xml", + "views/account_move.xml", + "views/account_invoice_overdue_reminder.xml", + "views/overdue_reminder_result.xml", + "views/overdue_reminder_action.xml", + "wizard/res_config_settings_view.xml", + "data/overdue_reminder_result.xml", + "data/mail_template.xml", ], - 'installable': True, - 'application': True, + "installable": True, + "application": True, } diff --git a/account_invoice_overdue_reminder/data/mail_template.xml b/account_invoice_overdue_reminder/data/mail_template.xml index fb6c5906b..d0b893a2b 100644 --- a/account_invoice_overdue_reminder/data/mail_template.xml +++ b/account_invoice_overdue_reminder/data/mail_template.xml @@ -1,22 +1,28 @@ - + - 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} - ${object.company_id.name} - Overdue invoice reminder n°${object.counter} +

Dear customer,

diff --git a/account_invoice_overdue_reminder/data/overdue_reminder_result.xml b/account_invoice_overdue_reminder/data/overdue_reminder_result.xml index 548b6388f..5e4d5c5f1 100644 --- a/account_invoice_overdue_reminder/data/overdue_reminder_result.xml +++ b/account_invoice_overdue_reminder/data/overdue_reminder_result.xml @@ -1,10 +1,9 @@ - + - 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 ef0b14a27..98bdf925e 100644 --- a/account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py +++ b/account_invoice_overdue_reminder/models/account_invoice_overdue_reminder.py @@ -2,62 +2,64 @@ # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models, _ +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' + _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.move', string='Invoice', ondelete='cascade', readonly=True) + "account.move", string="Invoice", ondelete="cascade", readonly=True + ) action_id = fields.Many2one( - 'overdue.reminder.action', string='Overdue Reminder Action', - ondelete='cascade') + "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') + 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') + 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_cc = fields.Char( - related='action_id.mail_id.email_cc', readonly=True, string='Cc') + 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') + 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) + company_id = fields.Many2one(related="invoice_id.company_id", store=True) - _sql_constraints = [( - 'counter_positive', - 'CHECK(counter >= 0)', - 'Counter must always be positive')] + _sql_constraints = [ + ("counter_positive", "CHECK(counter >= 0)", "Counter must always be positive") + ] - @api.constrains('invoice_id') + @api.constrains("invoice_id") def invoice_id_check(self): for action in self: - if action.invoice_id and action.invoice_id.move_type != 'out_invoice': - raise ValidationError(_( - "An overdue reminder can only be attached " - "to a customer invoice")) + if action.invoice_id and action.invoice_id.move_type != "out_invoice": + raise ValidationError( + _( + "An overdue reminder can only be attached " + "to a customer invoice" + ) + ) - @api.depends('invoice_id', 'counter') + @api.depends("invoice_id", "counter") def name_get(self): res = [] for rec in self: - name = _('%s Reminder %d') % (rec.invoice_id.name, rec.counter) + name = _("%s Reminder %d") % (rec.invoice_id.name, rec.counter) res.append((rec.id, name)) return res diff --git a/account_invoice_overdue_reminder/models/account_move.py b/account_invoice_overdue_reminder/models/account_move.py index 9776ba5d1..3c1716786 100644 --- a/account_invoice_overdue_reminder/models/account_move.py +++ b/account_invoice_overdue_reminder/models/account_move.py @@ -6,56 +6,71 @@ class AccountMove(models.Model): - _inherit = 'account.move' + _inherit = "account.move" no_overdue_reminder = fields.Boolean( - string='Disable Overdue Reminder', - tracking=True) + string="Disable Overdue Reminder", tracking=True + ) overdue_reminder_ids = fields.One2many( - 'account.invoice.overdue.reminder', - 'invoice_id', - string='Overdue Reminder Action History') + "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) + 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') + 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')] + _sql_constraints = [ + ( + "counter_positive", + "CHECK(overdue_reminder_counter >= 0)", + "Overdue Invoice Counter must always be positive", + ) + ] - @api.depends('move_type', 'state', 'payment_state', 'invoice_date_due') + @api.depends("move_type", "state", "payment_state", "invoice_date_due") def _compute_overdue(self): today = fields.Date.context_today(self) for move in self: overdue = False if ( - move.move_type == 'out_invoice' and - move.state == 'posted' and - move.payment_state not in ('paid', 'reversed', 'in_payment') and - move.invoice_date_due < today): + move.move_type == "out_invoice" + and move.state == "posted" + and move.payment_state not in ("paid", "reversed", "in_payment") + and move.invoice_date_due < today + ): overdue = True move.overdue = overdue @api.depends( - 'overdue_reminder_ids.action_id.date', - 'overdue_reminder_ids.counter', - 'overdue_reminder_ids.action_id.reminder_type') + "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'] + aioro = self.env["account.invoice.overdue.reminder"] for move in self: reminder = aioro.search( - [('invoice_id', '=', move.id)], order='action_date desc', limit=1) + [("invoice_id", "=", move.id)], order="action_date desc", limit=1 + ) date = reminder and reminder.action_date or False - counter_reminder = aioro.search([ - ('invoice_id', '=', move.id), - ('action_reminder_type', 'in', ('mail', 'post'))], - order='action_date desc, id desc', limit=1) + counter_reminder = aioro.search( + [ + ("invoice_id", "=", move.id), + ("action_reminder_type", "in", ("mail", "post")), + ], + order="action_date desc, id desc", + limit=1, + ) counter = counter_reminder and counter_reminder.counter or False move.overdue_reminder_last_date = date move.overdue_reminder_counter = counter diff --git a/account_invoice_overdue_reminder/models/overdue_reminder_action.py b/account_invoice_overdue_reminder/models/overdue_reminder_action.py index 401146d7e..a5c15e5e4 100644 --- a/account_invoice_overdue_reminder/models/overdue_reminder_action.py +++ b/account_invoice_overdue_reminder/models/overdue_reminder_action.py @@ -2,66 +2,81 @@ # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models, _ +from odoo import _, api, fields, models class OverdueReminderAction(models.Model): - _name = 'overdue.reminder.action' - _description = 'Overdue Reminder Action History' - _order = 'date desc, id desc' + _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') + "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=False) + default=fields.Date.context_today, required=True, index=True, readonly=False + ) user_id = fields.Many2one( - 'res.users', string='Performed by', required=True, readonly=True, - ondelete='restrict', default=lambda self: self.env.user) + "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) + "_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') - mail_cc = fields.Char(related='mail_id.email_cc', readonly=True) - company_id = fields.Many2one( - 'res.company', string='Company', readonly=True) + "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") + 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( - compute='_compute_invoice_count', store=True, string='Number of invoices') + compute="_compute_invoice_count", store=True, string="Number of invoices" + ) reminder_ids = fields.One2many( - 'account.invoice.overdue.reminder', 'action_id', readonly=True) + "account.invoice.overdue.reminder", "action_id", readonly=True + ) @api.model def _reminder_type_selection(self): return [ - ('mail', _('E-mail')), - ('phone', _('Phone')), - ('post', _('Letter')), - ] + ("mail", _("E-mail")), + ("phone", _("Phone")), + ("post", _("Letter")), + ] - @api.depends('reminder_ids') + @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]) + rg_res = self.env["account.invoice.overdue.reminder"].read_group( + [("action_id", "in", self.ids), ("invoice_id", "!=", False)], + ["action_id"], + ["action_id"], + ) + mapped_data = {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') + @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) + 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 index d6c705353..d246250e3 100644 --- a/account_invoice_overdue_reminder/models/overdue_reminder_result.py +++ b/account_invoice_overdue_reminder/models/overdue_reminder_result.py @@ -6,15 +6,14 @@ class OverdueReminderResult(models.Model): - _name = 'overdue.reminder.result' - _description = 'Overdue Invoice Reminder Result/Info' - _order = 'sequence, id desc' + _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')] + _sql_constraints = [ + ("name_unique", "unique(name)", "This overdue reminder result already exists") + ] diff --git a/account_invoice_overdue_reminder/models/res_company.py b/account_invoice_overdue_reminder/models/res_company.py index fcd0a684a..73c715810 100644 --- a/account_invoice_overdue_reminder/models/res_company.py +++ b/account_invoice_overdue_reminder/models/res_company.py @@ -2,48 +2,56 @@ # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models, _ +from odoo import _, api, fields, models class ResCompany(models.Model): - _inherit = 'res.company' + _inherit = "res.company" overdue_reminder_attach_invoice = fields.Boolean( - string='Attach Invoices to Overdue Reminder E-mails', default=True) + string="Attach Invoices to Overdue Reminder E-mails", default=True + ) overdue_reminder_start_days = fields.Integer( - string='Default Overdue Reminder Trigger Delay (days)') + string="Default Overdue Reminder Trigger Delay (days)" + ) overdue_reminder_min_interval_days = fields.Integer( - string='Default Overdue Reminder Minimum Interval (days)', default=5) + 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') + "_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') + "_overdue_reminder_partner_policy_selection", + default="last_reminder", + string="Contact to Remind", + ) @api.model def _overdue_reminder_interface_selection(self): return [ - ('onebyone', _('One by One')), - ('mass', _('Mass')), - ] + ("onebyone", _("One by One")), + ("mass", _("Mass")), + ] @api.model def _overdue_reminder_partner_policy_selection(self): return [ - ('last_reminder', _('Last Reminder')), - ('last_invoice', _('Last Invoice')), - ('invoice_contact', _('Invoice Contact')), - ] + ("last_reminder", _("Last Reminder")), + ("last_invoice", _("Last Invoice")), + ("invoice_contact", _("Invoice Contact")), + ] _sql_constraints = [ ( - 'overdue_reminder_start_days_positive', - 'CHECK(overdue_reminder_start_days >= 0)', - 'Overdue Reminder Trigger Delay must always be positive'), + "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'), - ] + "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/res_partner.py b/account_invoice_overdue_reminder/models/res_partner.py index 90cba9cf7..1b6a6d717 100644 --- a/account_invoice_overdue_reminder/models/res_partner.py +++ b/account_invoice_overdue_reminder/models/res_partner.py @@ -6,8 +6,9 @@ class ResPartner(models.Model): - _inherit = 'res.partner' + _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) + 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 index 8ddc48931..45b71ca32 100644 --- a/account_invoice_overdue_reminder/readme/CONFIGURATION.rst +++ b/account_invoice_overdue_reminder/readme/CONFIGURATION.rst @@ -1,4 +1,4 @@ -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. +You should increase the **transient_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. diff --git a/account_invoice_overdue_reminder/security/ir.model.access.csv b/account_invoice_overdue_reminder/security/ir.model.access.csv index 9c2540cc8..91a3322f3 100644 --- a/account_invoice_overdue_reminder/security/ir.model.access.csv +++ b/account_invoice_overdue_reminder/security/ir.model.access.csv @@ -10,4 +10,3 @@ access_overdue_reminder_start_payment,Full access on overdue.reminder.start.paym access_overdue_reminder_step,Full access on overdue.reminder.step (wizard),model_overdue_reminder_step,account.group_account_invoice,1,1,1,1 access_overdue_reminder_end,Full access on overdue.reminder.end (wizard),model_overdue_reminder_end,account.group_account_invoice,1,1,1,1 access_overdue_reminder_mass_update,Full access on overdue.reminder.mass.update (wizard),model_overdue_reminder_mass_update,account.group_account_invoice,1,1,1,1 - diff --git a/account_invoice_overdue_reminder/security/ir_rule.xml b/account_invoice_overdue_reminder/security/ir_rule.xml index 607756400..45c3a62d7 100644 --- a/account_invoice_overdue_reminder/security/ir_rule.xml +++ b/account_invoice_overdue_reminder/security/ir_rule.xml @@ -1,17 +1,18 @@ - + - Overdue Invoice Reminder multi-company - - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] 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 8987b1d99..b434e6d11 100644 --- a/account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml +++ b/account_invoice_overdue_reminder/views/account_invoice_overdue_reminder.xml @@ -1,10 +1,9 @@ - + - @@ -14,20 +13,38 @@
- - - - - - - - - - + + + + + + + + + + - - + +
@@ -40,9 +57,12 @@
- - - + + +
@@ -53,14 +73,26 @@ account.invoice.overdue.reminder - - - - - - - - + + + + + + + +
@@ -71,8 +103,8 @@ 100 - - + + @@ -82,18 +114,46 @@ account.invoice.overdue.reminder - - - - - - - + + + + + + + - - - - + + + + diff --git a/account_invoice_overdue_reminder/views/account_move.xml b/account_invoice_overdue_reminder/views/account_move.xml index a0202b1e2..bebac21d0 100644 --- a/account_invoice_overdue_reminder/views/account_move.xml +++ b/account_invoice_overdue_reminder/views/account_move.xml @@ -1,34 +1,41 @@ - + - overdue.reminder.customer.invoice.form account.move - + - + - - - - + + + + - + @@ -37,10 +44,10 @@ account.move - + - + @@ -48,11 +55,15 @@ overdue.reminder.customer.invoice.search account.move - + - - + + diff --git a/account_invoice_overdue_reminder/views/overdue_reminder_action.xml b/account_invoice_overdue_reminder/views/overdue_reminder_action.xml index cb8bdfaa5..a1d80ab33 100644 --- a/account_invoice_overdue_reminder/views/overdue_reminder_action.xml +++ b/account_invoice_overdue_reminder/views/overdue_reminder_action.xml @@ -1,10 +1,9 @@ - + - @@ -14,22 +13,34 @@
- - - - - - - - + + + + + + + + - - + + - +
@@ -40,11 +51,17 @@ overdue.reminder.action - - - - - + + + + +
@@ -54,16 +71,40 @@ overdue.reminder.action - - - - - - + + + + + + - - - + + + @@ -74,8 +115,8 @@ overdue.reminder.action - - + + @@ -85,7 +126,7 @@ overdue.reminder.action - + @@ -97,6 +138,11 @@ {'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 index 90ebde524..e6a2f0c50 100644 --- a/account_invoice_overdue_reminder/views/overdue_reminder_result.xml +++ b/account_invoice_overdue_reminder/views/overdue_reminder_result.xml @@ -1,10 +1,9 @@ - + - @@ -14,10 +13,15 @@
- + - - + +
@@ -29,8 +33,8 @@ overdue.reminder.result - - + + @@ -40,8 +44,12 @@ overdue.reminder.result - - + + @@ -52,6 +60,11 @@ tree,form - +
diff --git a/account_invoice_overdue_reminder/views/report.xml b/account_invoice_overdue_reminder/views/report.xml index af5ed4bd4..df63c5ea1 100644 --- a/account_invoice_overdue_reminder/views/report.xml +++ b/account_invoice_overdue_reminder/views/report.xml @@ -1,18 +1,21 @@ - + - Overdue Letter overdue.reminder.step qweb-pdf - account_invoice_overdue_reminder.report_overdue_reminder - account_invoice_overdue_reminder.report_overdue_reminder + account_invoice_overdue_reminder.report_overdue_reminder + account_invoice_overdue_reminder.report_overdue_reminder (object._get_report_base_filename()) diff --git a/account_invoice_overdue_reminder/views/report_overdue_reminder.xml b/account_invoice_overdue_reminder/views/report_overdue_reminder.xml index 77154e072..2bd040f2a 100644 --- a/account_invoice_overdue_reminder/views/report_overdue_reminder.xml +++ b/account_invoice_overdue_reminder/views/report_overdue_reminder.xml @@ -1,15 +1,20 @@ - +