You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
554 lines
27 KiB
Python
554 lines
27 KiB
Python
8 months ago
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
import logging
|
||
|
from odoo import api, fields, models, _
|
||
|
from odoo.tools.misc import format_date
|
||
|
from datetime import datetime, timedelta
|
||
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||
|
from odoo.exceptions import UserError
|
||
|
|
||
|
_logger = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
class ResPartner(models.Model):
|
||
|
_inherit = 'res.partner'
|
||
|
|
||
|
followup_next_action_date = fields.Date(string='Next reminder', copy=False, company_dependent=True,
|
||
|
help="The date before which no follow-up action should be taken.")
|
||
|
|
||
|
# readonly=False in order to be able to edit it directly in the view form, without having to click on 'Edit'
|
||
|
# It's mainly used for usability purposes to easily include/exclude unreconciled move lines
|
||
|
unreconciled_aml_ids = fields.One2many('account.move.line', compute='_compute_unreconciled_aml_ids', readonly=False)
|
||
|
|
||
|
unpaid_invoice_ids = fields.One2many('account.move', compute='_compute_unpaid_invoices')
|
||
|
unpaid_invoices_count = fields.Integer(compute='_compute_unpaid_invoices')
|
||
|
total_due = fields.Monetary(
|
||
|
compute='_compute_total_due',
|
||
|
groups='account.group_account_readonly,account.group_account_invoice')
|
||
|
total_overdue = fields.Monetary(
|
||
|
compute='_compute_total_due',
|
||
|
groups='account.group_account_readonly,account.group_account_invoice')
|
||
|
followup_status = fields.Selection(
|
||
|
[('in_need_of_action', 'In need of action'), ('with_overdue_invoices', 'With overdue invoices'), ('no_action_needed', 'No action needed')],
|
||
|
compute='_compute_followup_status',
|
||
|
string='Follow-up Status',
|
||
|
search='_search_status',
|
||
|
groups='account.group_account_readonly,account.group_account_invoice',
|
||
|
)
|
||
|
followup_line_id = fields.Many2one(
|
||
|
comodel_name='account_followup.followup.line',
|
||
|
string="Follow-up Level",
|
||
|
compute='_compute_followup_status',
|
||
|
inverse='_set_followup_line_on_unreconciled_amls',
|
||
|
search='_search_followup_line',
|
||
|
groups='account.group_account_readonly,account.group_account_invoice',
|
||
|
)
|
||
|
followup_reminder_type = fields.Selection([('automatic', 'Automatic'), ('manual', 'Manual')], string="Reminders", default='automatic')
|
||
|
type = fields.Selection(selection_add=[('followup', 'Follow-up Address')])
|
||
|
followup_responsible_id = fields.Many2one(
|
||
|
comodel_name='res.users',
|
||
|
string='Responsible',
|
||
|
help="Optionally you can assign a user to this field, which will make him responsible for the activities. If empty, we will find someone responsible.",
|
||
|
tracking=True,
|
||
|
copy=False,
|
||
|
company_dependent=True,
|
||
|
groups='account.group_account_readonly,account.group_account_invoice',
|
||
|
)
|
||
|
|
||
|
def _get_name(self):
|
||
|
# OVERRIDE base/models/res_partner.py
|
||
|
name = super()._get_name()
|
||
|
# Add a placeholder name for the followup address
|
||
|
if self.type == 'followup' and not self.name:
|
||
|
name += dict(self.fields_get(['type'])['type']['selection'])[self.type]
|
||
|
return name
|
||
|
|
||
|
def _search_status(self, operator, value):
|
||
|
"""
|
||
|
Compute the search on the field 'followup_status'
|
||
|
"""
|
||
|
if isinstance(value, str):
|
||
|
value = [value]
|
||
|
value = [v for v in value if v in ['in_need_of_action', 'with_overdue_invoices', 'no_action_needed']]
|
||
|
if operator not in ('in', '=') or not value:
|
||
|
return []
|
||
|
|
||
|
followup_data = self._query_followup_data(all_partners=True)
|
||
|
|
||
|
return [('id', 'in', [
|
||
|
d['partner_id']
|
||
|
for d in followup_data.values()
|
||
|
if d['followup_status'] in value
|
||
|
])]
|
||
|
|
||
|
def _search_followup_line(self, operator, value):
|
||
|
company_domain = [('company_id', '=', self.env.company.id)]
|
||
|
if isinstance(value, str):
|
||
|
domain = [('name', operator, value)]
|
||
|
elif isinstance(value, (int, list, tuple)):
|
||
|
domain = [('id', operator, value)]
|
||
|
|
||
|
first_followup_line = self.env['account_followup.followup.line'].search(company_domain, order="delay asc", limit=1)
|
||
|
line_ids = set(self.env['account_followup.followup.line'].search(domain+company_domain).ids)
|
||
|
if first_followup_line.id in line_ids:
|
||
|
# If we are searching for the 1st followup line, we also have to include None (aka no followup level at all)
|
||
|
# The result from the query is None when a partner is not yet at a followup level
|
||
|
line_ids.add(None)
|
||
|
|
||
|
followup_data = self._query_followup_data(all_partners=True)
|
||
|
|
||
|
return [('id', 'in', [
|
||
|
d['partner_id']
|
||
|
for d in followup_data.values()
|
||
|
if d['followup_line_id'] in line_ids
|
||
|
])]
|
||
|
|
||
|
@api.depends('unreconciled_aml_ids', 'followup_next_action_date')
|
||
|
@api.depends_context('company', 'allowed_company_ids')
|
||
|
def _compute_total_due(self):
|
||
|
today = fields.Date.context_today(self)
|
||
|
for partner in self:
|
||
|
total_overdue = 0
|
||
|
total_due = 0
|
||
|
for aml in partner.unreconciled_aml_ids:
|
||
|
is_overdue = today >= aml.date_maturity if aml.date_maturity else today >= aml.date
|
||
|
if aml.company_id == self.env.company and not aml.blocked:
|
||
|
total_due += aml.amount_residual
|
||
|
if is_overdue:
|
||
|
total_overdue += aml.amount_residual
|
||
|
partner.total_due = total_due
|
||
|
partner.total_overdue = total_overdue
|
||
|
|
||
|
@api.depends('unreconciled_aml_ids', 'followup_next_action_date')
|
||
|
@api.depends_context('company', 'allowed_company_ids')
|
||
|
def _compute_followup_status(self):
|
||
|
followup_lines_info = self._get_followup_lines_info()
|
||
|
today = fields.Date.context_today(self)
|
||
|
for partner in self:
|
||
|
max_followup = partner._included_unreconciled_aml_max_followup()
|
||
|
max_aml_delay = max_followup.get('max_delay') or 0
|
||
|
next_followup_delay = max_followup.get('next_followup_delay') or 0
|
||
|
has_overdue_invoices = max_followup.get('has_overdue_invoices')
|
||
|
most_delayed_aml = max_followup.get('most_delayed_aml')
|
||
|
highest_followup_line = max_followup.get('highest_followup_line')
|
||
|
|
||
|
# computation of followup_status
|
||
|
new_status = 'no_action_needed'
|
||
|
if has_overdue_invoices and most_delayed_aml:
|
||
|
new_status = 'with_overdue_invoices'
|
||
|
next_followup_date_exceeded = today >= partner.followup_next_action_date if partner.followup_next_action_date else True
|
||
|
if max_aml_delay >= next_followup_delay and next_followup_date_exceeded and followup_lines_info:
|
||
|
new_status = 'in_need_of_action'
|
||
|
partner.followup_status = new_status
|
||
|
|
||
|
# computation of followup_line_id
|
||
|
new_line = highest_followup_line
|
||
|
if most_delayed_aml and (most_delayed_aml.last_followup_date or max_aml_delay >= next_followup_delay) and followup_lines_info:
|
||
|
index = highest_followup_line.id if highest_followup_line else None
|
||
|
next_line_id = followup_lines_info[index].get('next_followup_line_id')
|
||
|
new_line = self.env['account_followup.followup.line'].browse(next_line_id)
|
||
|
partner.followup_line_id = new_line
|
||
|
|
||
|
def _compute_unpaid_invoices(self):
|
||
|
for partner in self:
|
||
|
unpaid_receivable_lines = self.env['account.move.line'].search([
|
||
|
('company_id', '=', self.env.company.id),
|
||
|
('move_id.commercial_partner_id', '=', partner.id),
|
||
|
('parent_state', '=', 'posted'),
|
||
|
('move_id.payment_state', 'in', ('not_paid', 'partial')),
|
||
|
('move_id.move_type', 'in', self.env['account.move'].get_sale_types()),
|
||
|
('account_id.account_type', '=', 'asset_receivable'),
|
||
|
])
|
||
|
unpaid_invoices = unpaid_receivable_lines.move_id
|
||
|
partner.unpaid_invoice_ids = unpaid_invoices
|
||
|
partner.unpaid_invoices_count = len(unpaid_invoices)
|
||
|
|
||
|
def action_view_unpaid_invoices(self):
|
||
|
self.ensure_one()
|
||
|
action = self.env["ir.actions.actions"]._for_xml_id("account.action_move_out_invoice_type")
|
||
|
action['domain'] = [('id', 'in', self.unpaid_invoice_ids.ids)]
|
||
|
action['context'] = {
|
||
|
'default_move_type': 'out_invoice',
|
||
|
'move_type': 'out_invoice',
|
||
|
'journal_type': 'sale',
|
||
|
'partner_id': self.id
|
||
|
}
|
||
|
return action
|
||
|
|
||
|
@api.depends('invoice_ids')
|
||
|
@api.depends_context('company', 'allowed_company_ids')
|
||
|
def _compute_unreconciled_aml_ids(self):
|
||
|
values = {
|
||
|
read['partner_id'][0]: read['line_ids']
|
||
|
for read in self.env['account.move.line'].read_group(
|
||
|
domain=self._get_unreconciled_aml_domain(),
|
||
|
fields=['line_ids:array_agg(id)'],
|
||
|
groupby=['partner_id']
|
||
|
)
|
||
|
}
|
||
|
for partner in self:
|
||
|
partner.unreconciled_aml_ids = values.get(partner.id, False)
|
||
|
|
||
|
def _set_followup_line_on_unreconciled_amls(self):
|
||
|
today = fields.Date.today()
|
||
|
for partner in self:
|
||
|
current_followup_line = partner.followup_line_id
|
||
|
previous_followup_line = self.env['account_followup.followup.line'].search([('delay', '<', current_followup_line.delay), ('company_id', '=', self.env.company.id)], order='delay desc', limit=1)
|
||
|
for unreconciled_aml in partner.unreconciled_aml_ids:
|
||
|
if not unreconciled_aml.blocked:
|
||
|
unreconciled_aml.followup_line_id = previous_followup_line
|
||
|
# When a specific followup line is manually selected, we consider the followup as processed
|
||
|
unreconciled_aml.last_followup_date = today
|
||
|
|
||
|
def _get_unreconciled_aml_domain(self):
|
||
|
return [
|
||
|
('reconciled', '=', False),
|
||
|
('account_id.deprecated', '=', False),
|
||
|
('account_id.account_type', '=', 'asset_receivable'),
|
||
|
('move_id.state', '=', 'posted'),
|
||
|
('partner_id', 'in', self.ids),
|
||
|
('company_id', '=', self.env.company.id),
|
||
|
]
|
||
|
|
||
|
def _get_followup_responsible(self):
|
||
|
self.ensure_one()
|
||
|
|
||
|
responsible_type = self.followup_line_id.activity_default_responsible_type
|
||
|
if responsible_type == 'account_manager' and self.user_id:
|
||
|
return self.user_id
|
||
|
|
||
|
most_delayed_aml = self._included_unreconciled_aml_max_followup().get('most_delayed_aml')
|
||
|
if responsible_type == 'salesperson' and most_delayed_aml and most_delayed_aml.move_id.invoice_user_id:
|
||
|
return most_delayed_aml.move_id.invoice_user_id
|
||
|
|
||
|
if self.followup_responsible_id:
|
||
|
return self.followup_responsible_id
|
||
|
|
||
|
if self.user_id:
|
||
|
return self.user_id
|
||
|
|
||
|
if most_delayed_aml and most_delayed_aml.move_id.invoice_user_id:
|
||
|
return most_delayed_aml.move_id.invoice_user_id
|
||
|
|
||
|
return self.env.user
|
||
|
|
||
|
def _get_all_followup_contacts(self):
|
||
|
""" Returns every contact of type 'followup' in the children of self.
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
return self.child_ids.filtered(lambda partner: partner.type == 'followup')
|
||
|
|
||
|
def _included_unreconciled_aml_max_followup(self):
|
||
|
""" Computes the maximum delay in days and the highest level of followup (followup line with highest delay) of all the unreconciled amls included.
|
||
|
Also returns the delay for the next level (after the highest_followup_line), the most delayed aml and a boolean specifying if any invoice is overdue.
|
||
|
:return dict with key/values: most_delayed_aml, max_delay, highest_followup_line, next_followup_delay, has_overdue_invoices
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
today = fields.Date.context_today(self)
|
||
|
highest_followup_line = None
|
||
|
most_delayed_aml = self.env['account.move.line']
|
||
|
first_followup_line = self._get_first_followup_level()
|
||
|
# Minimum value for delay, will always be smaller than any other delay
|
||
|
max_delay = first_followup_line.delay - 1
|
||
|
has_overdue_invoices = False
|
||
|
for aml in self.unreconciled_aml_ids:
|
||
|
aml_delay = (today - (aml.date_maturity or aml.date)).days
|
||
|
is_overdue = aml_delay >= 0
|
||
|
if is_overdue:
|
||
|
has_overdue_invoices = True
|
||
|
if aml.company_id == self.env.company and not aml.blocked:
|
||
|
if aml.followup_line_id and aml.followup_line_id.delay >= (highest_followup_line or first_followup_line).delay:
|
||
|
highest_followup_line = aml.followup_line_id
|
||
|
max_delay = max(max_delay, aml_delay)
|
||
|
if most_delayed_aml.amount_residual < aml.amount_residual:
|
||
|
most_delayed_aml = aml
|
||
|
followup_lines_info = self._get_followup_lines_info()
|
||
|
next_followup_delay = None
|
||
|
if followup_lines_info:
|
||
|
key = highest_followup_line.id if highest_followup_line else None
|
||
|
current_followup_line_info = followup_lines_info.get(key)
|
||
|
next_followup_delay = current_followup_line_info.get('next_delay')
|
||
|
return {
|
||
|
'most_delayed_aml': most_delayed_aml,
|
||
|
'max_delay': max_delay,
|
||
|
'highest_followup_line': highest_followup_line,
|
||
|
'next_followup_delay': next_followup_delay,
|
||
|
'has_overdue_invoices': has_overdue_invoices,
|
||
|
}
|
||
|
|
||
|
def _get_included_unreconciled_aml_ids(self):
|
||
|
self.ensure_one()
|
||
|
return self.unreconciled_aml_ids.filtered(lambda aml: not aml.blocked)
|
||
|
|
||
|
def _get_first_followup_level(self):
|
||
|
self.ensure_one()
|
||
|
return self.env['account_followup.followup.line'].search([('company_id', '=', self.env.company.id)], order='delay asc', limit=1)
|
||
|
|
||
|
def _update_next_followup_action_date(self, followup_line):
|
||
|
"""Updates the followup_next_action_date of the right account move lines
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
|
||
|
# Arbitrary 14 days delay (like the _get_next_date() method) if there is no followup_line
|
||
|
# This will be changed/removed in an upcoming improvement
|
||
|
next_date = followup_line._get_next_date() if followup_line else fields.Date.today() + timedelta(days=14)
|
||
|
self.followup_next_action_date = datetime.strftime(next_date, DEFAULT_SERVER_DATE_FORMAT)
|
||
|
msg = _('Next Reminder Date set to %s', format_date(self.env, self.followup_next_action_date))
|
||
|
self.message_post(body=msg)
|
||
|
|
||
|
today = fields.Date.today()
|
||
|
for aml in self._get_included_unreconciled_aml_ids():
|
||
|
aml.followup_line_id = followup_line
|
||
|
aml.last_followup_date = today
|
||
|
|
||
|
def open_action_followup(self):
|
||
|
self.ensure_one()
|
||
|
return {
|
||
|
'name': _("Overdue Payments for %s", self.display_name),
|
||
|
'type': 'ir.actions.act_window',
|
||
|
'view_mode': 'form',
|
||
|
'views': [[self.env.ref('account_followup.customer_statements_form_view').id, 'form']],
|
||
|
'res_model': 'res.partner',
|
||
|
'res_id': self.id,
|
||
|
}
|
||
|
|
||
|
def send_followup_email(self, options):
|
||
|
"""
|
||
|
Send a follow-up report by email to customers in self
|
||
|
"""
|
||
|
for record in self:
|
||
|
options['partner_id'] = record.id
|
||
|
self.env['account.followup.report']._send_email(options)
|
||
|
|
||
|
def send_followup_sms(self, options):
|
||
|
"""
|
||
|
Send a follow-up report by sms to customers in self
|
||
|
"""
|
||
|
for partner in self:
|
||
|
options['partner_id'] = partner.id
|
||
|
self.env['account.followup.report']._send_sms(options)
|
||
|
|
||
|
def get_followup_html(self, options=None):
|
||
|
"""
|
||
|
Return the content of the follow-up report in HTML
|
||
|
"""
|
||
|
if options is None:
|
||
|
options = {}
|
||
|
options.update({
|
||
|
'partner_id': self.id,
|
||
|
'followup_line_id': self.followup_line_id,
|
||
|
'keep_summary': True
|
||
|
})
|
||
|
return self.env['account.followup.report'].with_context(print_mode=True, lang=self.lang or self.env.user.lang).get_followup_report_html(options)
|
||
|
|
||
|
def _get_followup_lines_info(self):
|
||
|
""" returns the followup plan of the current user's company
|
||
|
in the form of a dictionary with
|
||
|
* keys being the different possible lines of followup for account.move.line's (None or IDs of account_followup.followup.line)
|
||
|
* values being a dict of 3 elements:
|
||
|
- 'next_followup_line_id': the followup ID of the next followup line
|
||
|
- 'next_delay': the delay in days of the next followup line
|
||
|
"""
|
||
|
followup_lines = self.env['account_followup.followup.line'].search([('company_id', '=', self.env.company.id)], order="delay asc")
|
||
|
|
||
|
previous_line_id = None
|
||
|
followup_lines_info = {}
|
||
|
for line in followup_lines:
|
||
|
delay_in_days = line.delay
|
||
|
followup_lines_info[previous_line_id] = {
|
||
|
'next_followup_line_id': line.id,
|
||
|
'next_delay': delay_in_days,
|
||
|
}
|
||
|
previous_line_id = line.id
|
||
|
if previous_line_id:
|
||
|
followup_lines_info[previous_line_id] = {
|
||
|
'next_followup_line_id': previous_line_id,
|
||
|
'next_delay': delay_in_days,
|
||
|
}
|
||
|
return followup_lines_info
|
||
|
|
||
|
def _query_followup_data(self, all_partners=False):
|
||
|
# Allow mocking the current day for testing purpose.
|
||
|
today = fields.Date.context_today(self)
|
||
|
if not self.ids and not all_partners:
|
||
|
return {}
|
||
|
self.env['account.move.line'].check_access_rights('read')
|
||
|
|
||
|
sql = """
|
||
|
SELECT partner.id as partner_id,
|
||
|
ful.id as followup_line_id,
|
||
|
CASE WHEN partner.balance <= 0 THEN 'no_action_needed'
|
||
|
WHEN in_need_of_action_aml.id IS NOT NULL AND (prop_date.value_datetime IS NULL OR prop_date.value_datetime::date <= %(current_date)s) THEN 'in_need_of_action'
|
||
|
WHEN exceeded_unreconciled_aml.id IS NOT NULL THEN 'with_overdue_invoices'
|
||
|
ELSE 'no_action_needed' END as followup_status
|
||
|
FROM (
|
||
|
SELECT partner.id,
|
||
|
max(current_followup_line.delay) as followup_delay,
|
||
|
SUM(aml.balance) as balance
|
||
|
FROM res_partner partner
|
||
|
JOIN account_move_line aml ON aml.partner_id = partner.id
|
||
|
JOIN account_account account ON account.id = aml.account_id
|
||
|
JOIN account_move move ON move.id = aml.move_id
|
||
|
-- Get the followup line
|
||
|
LEFT JOIN LATERAL (
|
||
|
SELECT COALESCE(next_ful.id, ful.id) as id, COALESCE(next_ful.delay, ful.delay) as delay
|
||
|
FROM account_move_line line
|
||
|
LEFT OUTER JOIN account_followup_followup_line ful ON ful.id = aml.followup_line_id
|
||
|
LEFT OUTER JOIN account_followup_followup_line next_ful ON next_ful.id = (
|
||
|
SELECT next_ful.id FROM account_followup_followup_line next_ful
|
||
|
WHERE next_ful.delay > COALESCE(ful.delay, -999)
|
||
|
AND COALESCE(aml.date_maturity, aml.date) + next_ful.delay <= %(current_date)s
|
||
|
AND next_ful.company_id = %(company_id)s
|
||
|
ORDER BY next_ful.delay ASC
|
||
|
LIMIT 1
|
||
|
)
|
||
|
WHERE line.id = aml.id
|
||
|
AND aml.partner_id = partner.id
|
||
|
AND aml.balance > 0
|
||
|
) current_followup_line ON true
|
||
|
WHERE account.deprecated IS NOT TRUE
|
||
|
AND account.account_type = 'asset_receivable'
|
||
|
AND move.state = 'posted'
|
||
|
AND aml.reconciled IS NOT TRUE
|
||
|
AND aml.blocked IS FALSE
|
||
|
AND aml.company_id = %(company_id)s
|
||
|
{where}
|
||
|
GROUP BY partner.id
|
||
|
) partner
|
||
|
LEFT JOIN account_followup_followup_line ful ON ful.delay = partner.followup_delay AND ful.company_id = %(company_id)s
|
||
|
-- Get the followup status data
|
||
|
LEFT OUTER JOIN LATERAL (
|
||
|
SELECT line.id
|
||
|
FROM account_move_line line
|
||
|
JOIN account_account account ON line.account_id = account.id
|
||
|
JOIN account_move move ON line.move_id = move.id
|
||
|
LEFT JOIN account_followup_followup_line ful ON ful.id = line.followup_line_id
|
||
|
WHERE line.partner_id = partner.id
|
||
|
AND account.account_type = 'asset_receivable'
|
||
|
AND account.deprecated IS NOT TRUE
|
||
|
AND move.state = 'posted'
|
||
|
AND line.reconciled IS NOT TRUE
|
||
|
AND line.balance > 0
|
||
|
AND line.blocked IS FALSE
|
||
|
AND line.company_id = %(company_id)s
|
||
|
AND COALESCE(ful.delay, -999) <= partner.followup_delay
|
||
|
AND COALESCE(line.date_maturity, line.date) + COALESCE(ful.delay, -999) <= %(current_date)s
|
||
|
LIMIT 1
|
||
|
) in_need_of_action_aml ON true
|
||
|
LEFT OUTER JOIN LATERAL (
|
||
|
SELECT line.id
|
||
|
FROM account_move_line line
|
||
|
JOIN account_account account ON line.account_id = account.id
|
||
|
JOIN account_move move ON line.move_id = move.id
|
||
|
WHERE line.partner_id = partner.id
|
||
|
AND account.account_type = 'asset_receivable'
|
||
|
AND account.deprecated IS NOT TRUE
|
||
|
AND move.state = 'posted'
|
||
|
AND line.reconciled IS NOT TRUE
|
||
|
AND line.balance > 0
|
||
|
AND line.company_id = %(company_id)s
|
||
|
AND COALESCE(line.date_maturity, line.date) <= %(current_date)s
|
||
|
LIMIT 1
|
||
|
) exceeded_unreconciled_aml ON true
|
||
|
LEFT OUTER JOIN ir_property prop_date ON prop_date.res_id = CONCAT('res.partner,', partner.id)
|
||
|
AND prop_date.name = 'followup_next_action_date'
|
||
|
AND prop_date.company_id = %(company_id)s
|
||
|
""".format(
|
||
|
where="" if all_partners else "AND aml.partner_id in %(partner_ids)s",
|
||
|
)
|
||
|
params = {
|
||
|
'company_id': self.env.company.id,
|
||
|
'partner_ids': tuple(self.ids),
|
||
|
'current_date': today,
|
||
|
}
|
||
|
self.env['account.move.line'].flush_model()
|
||
|
self.env['res.partner'].flush_model()
|
||
|
self.env['account_followup.followup.line'].flush_model()
|
||
|
self.env.cr.execute(sql, params)
|
||
|
result = self.env.cr.dictfetchall()
|
||
|
result = {r['partner_id']: r for r in result}
|
||
|
return result
|
||
|
|
||
|
def _send_followup(self, options):
|
||
|
""" Send the follow-up to the partner, depending on selected options.
|
||
|
Can be overridden to include more ways of sending the follow-up.
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
followup_line = options.get('followup_line')
|
||
|
if options.get('email', followup_line.send_email):
|
||
|
self.send_followup_email(options)
|
||
|
if options.get('sms', followup_line.send_sms):
|
||
|
self.send_followup_sms(options)
|
||
|
|
||
|
def _execute_followup_partner(self, options=None):
|
||
|
""" Execute the actions to do with follow-ups for this partner (apart from printing).
|
||
|
This is either called when processing the follow-ups manually (wizard), or automatically (cron).
|
||
|
Automatic follow-ups can also be triggered manually with *action_manually_process_automatic_followups*.
|
||
|
When processing automatically, options is None.
|
||
|
|
||
|
Returns True if any action was processed, False otherwise
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
if options is None:
|
||
|
options = {}
|
||
|
if options.get('manual_followup', self.followup_status == 'in_need_of_action'):
|
||
|
followup_line = self.followup_line_id or self._get_first_followup_level()
|
||
|
|
||
|
if followup_line.create_activity:
|
||
|
# log a next activity for today
|
||
|
self.activity_schedule(
|
||
|
activity_type_id=followup_line.activity_type_id and followup_line.activity_type_id.id or self._default_activity_type().id,
|
||
|
note=followup_line.activity_note,
|
||
|
summary=followup_line.activity_summary,
|
||
|
user_id=(self._get_followup_responsible()).id
|
||
|
)
|
||
|
|
||
|
self._update_next_followup_action_date(followup_line)
|
||
|
|
||
|
self._send_followup(options={'followup_line': followup_line, **options})
|
||
|
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def execute_followup(self, options):
|
||
|
""" Execute the actions to do with follow-ups for this partner.
|
||
|
This is called when processing the follow-ups manually, via the wizard.
|
||
|
|
||
|
options is a dictionary containing at least the following information:
|
||
|
- 'partner_id': id of partner (self)
|
||
|
- 'email': boolean to trigger the sending of email or not
|
||
|
- 'email_subject': subject of email
|
||
|
- 'followup_contacts': partners (contacts) to send the followup to
|
||
|
- 'body': email body
|
||
|
- 'attachment_ids': invoice attachments to join to email/letter
|
||
|
- 'sms': boolean to trigger the sending of sms or not
|
||
|
- 'sms_body': sms body
|
||
|
- 'print': boolean to trigger the printing of pdf letter or not
|
||
|
- 'manual_followup': boolean to indicate whether this followup is triggered via the manual reminder wizard
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
to_print = self._execute_followup_partner(options=options)
|
||
|
if options.get('print') and to_print:
|
||
|
return self.env['account.followup.report']._print_followup_letter(self, options)
|
||
|
|
||
|
def action_manually_process_automatic_followups(self):
|
||
|
for partner in self:
|
||
|
partner._execute_followup_partner()
|
||
|
|
||
|
def _cron_execute_followup_company(self):
|
||
|
followup_data = self._query_followup_data(all_partners=True)
|
||
|
in_need_of_action = self.env['res.partner'].browse([d['partner_id'] for d in followup_data.values() if d['followup_status'] == 'in_need_of_action'])
|
||
|
in_need_of_action_auto = in_need_of_action.filtered(lambda p: p.followup_line_id.auto_execute and p.followup_reminder_type == 'automatic')
|
||
|
for partner in in_need_of_action_auto:
|
||
|
try:
|
||
|
partner._execute_followup_partner()
|
||
|
except UserError as e:
|
||
|
# followup may raise exception due to configuration issues
|
||
|
# i.e. partner missing email
|
||
|
_logger.exception(e)
|
||
|
|
||
|
def _cron_execute_followup(self):
|
||
|
for company in self.env["res.company"].search([]):
|
||
|
self.with_context(allowed_company_ids=company.ids)._cron_execute_followup_company()
|