703 lines
33 KiB
Python
703 lines
33 KiB
Python
import ast
|
|
import logging
|
|
|
|
from collections import defaultdict
|
|
from collections.abc import Iterable
|
|
from datetime import datetime
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError
|
|
from odoo.fields import Domain
|
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, SQL
|
|
from odoo.tools.misc import format_date, get_lang
|
|
|
|
_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='''No follow-up action will be taken before this date.
|
|
Sending a reminder will set this date depending on the levels configuration, and you can change it manually.''',
|
|
)
|
|
|
|
# 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_total_due', readonly=False)
|
|
|
|
unpaid_invoice_ids = fields.One2many('account.move', compute='_compute_unpaid_invoices')
|
|
unpaid_invoices_count = fields.Integer(compute='_compute_unpaid_invoices')
|
|
# These two fields are meant to receive the due and overdue amounts, including asset_receivable AND liability_payable accounts
|
|
# In opposition to the total_due and total_overdue fields which only take into account asset_receivable accounts
|
|
# To be renamed in master
|
|
total_all_due = fields.Monetary(
|
|
compute='_compute_total_due',
|
|
groups='account.group_account_readonly,account.group_account_invoice')
|
|
total_all_overdue = fields.Monetary(
|
|
compute='_compute_total_due',
|
|
groups='account.group_account_readonly,account.group_account_invoice')
|
|
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')
|
|
total_overdue_followup = 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')
|
|
followup_responsible_id = fields.Many2one(
|
|
comodel_name='res.users',
|
|
string='Responsible',
|
|
help="The responsible assigned to manual followup activities, if defined in the level.",
|
|
tracking=True,
|
|
copy=False,
|
|
company_dependent=True,
|
|
groups='account.group_account_readonly,account.group_account_invoice',
|
|
)
|
|
has_moves = fields.Boolean(compute='_compute_has_moves')
|
|
|
|
def _search_status(self, operator, value):
|
|
"""
|
|
Compute the search on the field 'followup_status'
|
|
"""
|
|
if operator != 'in':
|
|
return NotImplemented
|
|
value = set(value) & {'in_need_of_action', 'with_overdue_invoices', 'no_action_needed'}
|
|
if not value:
|
|
return Domain.FALSE
|
|
|
|
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):
|
|
if isinstance(value, str) or (isinstance(value, Iterable) and all(isinstance(v, str) for v in value)):
|
|
domain = Domain('name', operator, value)
|
|
else:
|
|
domain = Domain('id', operator, value)
|
|
domain &= Domain('company_id', 'parent_of', self.env.company.id)
|
|
|
|
line_ids = set(self.env['account_followup.followup.line']._search(domain).get_result_ids())
|
|
if not line_ids:
|
|
return Domain.FALSE
|
|
|
|
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_followup_status(self):
|
|
all_data = self._query_followup_data()
|
|
for partner in self:
|
|
partner_data = all_data.get(partner._origin.id, {'followup_status': 'no_action_needed', 'followup_line_id': False})
|
|
partner.followup_status = partner_data['followup_status']
|
|
partner.followup_line_id = partner_data['followup_line_id']
|
|
|
|
def _compute_unpaid_invoices(self):
|
|
partners_unpaid_receivable_lines = self.env['account.move.line'].search([
|
|
('company_id', 'child_of', self.env.company.id),
|
|
('move_id.commercial_partner_id', 'in', self.ids),
|
|
('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'),
|
|
]).grouped(lambda line: line.move_id.commercial_partner_id.id)
|
|
for partner in self:
|
|
unpaid_receivable_lines = partners_unpaid_receivable_lines.get(partner.id, self.env['account.move.line'])
|
|
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
|
|
|
|
def action_open_overdue_entries(self):
|
|
self.ensure_one()
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _("Overdue Invoices"),
|
|
'res_model': 'account.move',
|
|
'domain': [('commercial_partner_id', '=', self.id), ('move_type', 'in', ('out_invoice', 'out_refund'))],
|
|
'views': [(self.env.ref('account_followup.view_followup_invoice_list').id, 'list'), (None, 'form')],
|
|
'context': {'search_default_late': True},
|
|
}
|
|
|
|
def action_open_partner_followup_journal_items(self):
|
|
report = self.env.ref('account_reports.followup_report')
|
|
options = report.get_options({})
|
|
params = {
|
|
'line_id': report._get_generic_line_id('res.partner', self.id),
|
|
}
|
|
custom_handler = self.env[report.custom_handler_model_name]
|
|
return {
|
|
**custom_handler.open_journal_items(options, params),
|
|
'name': _("Journal Items"),
|
|
}
|
|
|
|
def action_open_unreconciled_partner(self):
|
|
action_values = self.env["ir.actions.actions"]._for_xml_id("account_accountant.action_move_line_posted_unreconciled")
|
|
domain = ast.literal_eval(action_values['domain'])
|
|
domain.append(('partner_id', 'in', self.ids))
|
|
action_values['domain'] = domain
|
|
return action_values
|
|
|
|
@api.depends('invoice_ids.line_ids.no_followup')
|
|
@api.depends_context('company', 'allowed_company_ids')
|
|
def _compute_total_due(self):
|
|
due_data = defaultdict(float)
|
|
overdue_data = defaultdict(float)
|
|
receivable_due_data = defaultdict(float)
|
|
receivable_overdue_data = defaultdict(float)
|
|
receivable_overdue_followup_data = defaultdict(float)
|
|
unreconciled_aml_ids = defaultdict(list)
|
|
for account_type, overdue, partner, no_followup, amount_residual_sum, aml_ids in self.env['account.move.line']._read_group(
|
|
domain=self._get_unreconciled_aml_domain(),
|
|
groupby=['account_type', 'followup_overdue', 'partner_id', 'no_followup'],
|
|
aggregates=['amount_residual:sum', 'id:array_agg'],
|
|
):
|
|
if account_type == 'asset_receivable':
|
|
unreconciled_aml_ids[partner] += aml_ids
|
|
receivable_due_data[partner] += amount_residual_sum
|
|
if overdue:
|
|
receivable_overdue_data[partner] += amount_residual_sum
|
|
if not no_followup:
|
|
receivable_overdue_followup_data[partner] += amount_residual_sum
|
|
|
|
due_data[partner] += amount_residual_sum
|
|
if overdue:
|
|
overdue_data[partner] += amount_residual_sum
|
|
|
|
for partner in self:
|
|
partner.total_all_due = due_data.get(partner, 0.0)
|
|
partner.total_all_overdue = overdue_data.get(partner, 0.0)
|
|
partner.total_due = receivable_due_data.get(partner, 0.0)
|
|
partner.total_overdue = receivable_overdue_data.get(partner, 0.0)
|
|
partner.total_overdue_followup = receivable_overdue_followup_data.get(partner, 0.0)
|
|
partner.unreconciled_aml_ids = self.env['account.move.line'].browse(unreconciled_aml_ids.get(partner, []))
|
|
|
|
def _set_followup_line_on_unreconciled_amls(self):
|
|
today = fields.Date.context_today(self)
|
|
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', 'parent_of', self.env.company.id)], order='delay desc', limit=1)
|
|
for unreconciled_aml in partner.unreconciled_aml_ids:
|
|
unreconciled_aml.followup_line_id = previous_followup_line
|
|
|
|
def _get_unreconciled_aml_domain(self):
|
|
return Domain.AND([
|
|
Domain('reconciled', '=', False),
|
|
Domain('account_id.account_type', 'in', ('asset_receivable', 'liability_payable')),
|
|
Domain('parent_state', '=', 'posted'),
|
|
Domain('partner_id', 'in', self.ids),
|
|
Domain('company_id', 'child_of', self.env.company.id),
|
|
])
|
|
|
|
def _get_followup_responsible(self, multiple_responsible=False):
|
|
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
|
|
|
|
all_aml_responsible = self.unreconciled_aml_ids.move_id.invoice_user_id
|
|
max_amount_aml = max(
|
|
self.unreconciled_aml_ids.filtered('move_id.invoice_user_id'),
|
|
default=self.env['account.move.line'],
|
|
key=lambda l: l.amount_residual,
|
|
)
|
|
candidates = [
|
|
(responsible_type == 'salesperson' and (
|
|
all_aml_responsible if multiple_responsible else max_amount_aml.move_id.invoice_user_id
|
|
)),
|
|
self.followup_responsible_id,
|
|
self.user_id,
|
|
max_amount_aml.move_id.invoice_user_id,
|
|
]
|
|
return next((u for u in candidates if u and u.filtered('active')), super()._get_followup_responsible())
|
|
|
|
def _get_all_followup_contacts(self):
|
|
""" Followup contacts are defined as billing address and defaults to
|
|
contact as defined in 'address_get' """
|
|
self.ensure_one()
|
|
return self.env['res.partner'].browse(self.address_get(['invoice'])['invoice'])
|
|
|
|
def _get_invoices_to_print(self, options):
|
|
self.ensure_one()
|
|
if not options:
|
|
options = {}
|
|
if not options.get('join_invoices', options.get('followup_line', self.followup_line_id).join_invoices):
|
|
return self.env['account.move']
|
|
invoices_to_print = self.unreconciled_aml_ids.filtered(
|
|
lambda l: not l.no_followup and l.move_id.is_invoice(include_receipts=True)
|
|
).move_id
|
|
if options.get('manual_followup'):
|
|
# For manual reminders, only print invoices with the selected attachments
|
|
return invoices_to_print.filtered(lambda inv: inv.invoice_pdf_report_id.id in options.get('attachment_ids', []))
|
|
return invoices_to_print.filtered(lambda inv: inv.invoice_pdf_report_id)
|
|
|
|
@api.model
|
|
def _get_first_followup_level(self):
|
|
return self.env['account_followup.followup.line'].search([('company_id', 'parent_of', 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()
|
|
if followup_line:
|
|
next_date = followup_line._get_next_date()
|
|
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.context_today(self)
|
|
previous_levels = self.env['account_followup.followup.line'].search([('delay', '<=', followup_line.delay), ('company_id', '=', self.env.company.id)])
|
|
for aml in self.unreconciled_aml_ids.filtered('date_maturity'):
|
|
eligible_levels = previous_levels.filtered(lambda level: (today - aml.date_maturity).days >= level.delay)
|
|
if eligible_levels:
|
|
aml.followup_line_id = max(eligible_levels, key=lambda level: level.delay)
|
|
|
|
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,
|
|
})
|
|
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):
|
|
""" Return 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 2 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', 'parent_of', 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 _get_all_followup_data(self):
|
|
if 'res_partner_all_followup' in self.env.cr.cache:
|
|
return self.env.cr.cache['res_partner_all_followup']
|
|
|
|
# Put the data in a cache in the cursor to avoid running costly query multiple times in same transaction.
|
|
# Only do it if the table doesn't exist yet.
|
|
query, params = self._get_followup_data_query()
|
|
self.env.cr.execute(query, params)
|
|
self.env.cr.cache['res_partner_all_followup'] = {
|
|
r['partner_id']: r for r in self.env.cr.dictfetchall()
|
|
}
|
|
return self.env.cr.cache['res_partner_all_followup']
|
|
|
|
def _query_followup_data(self, all_partners=False):
|
|
if all_partners:
|
|
return self._get_all_followup_data()
|
|
if not self.ids:
|
|
return {}
|
|
if 'res_partner_all_followup' in self.env.cr.cache:
|
|
cache_dict = self.env.cr.cache['res_partner_all_followup']
|
|
return {id_: cache_dict[id_] for id_ in self.ids if id_ in cache_dict}
|
|
query, params = self._get_followup_data_query(self.ids)
|
|
self.env.cr.execute(query, params)
|
|
return {r['partner_id']: r for r in self.env.cr.dictfetchall()}
|
|
|
|
def _get_followup_data_query(self, partner_ids=None):
|
|
self.env['account.move.line'].check_access('read')
|
|
self.env['account.move.line'].flush_model()
|
|
self.env['res.partner'].flush_model()
|
|
self.env['account_followup.followup.line'].flush_model()
|
|
ResPartner = self.env['res.partner']
|
|
return f"""
|
|
SELECT partner.id as partner_id,
|
|
ful.id as followup_line_id,
|
|
CASE WHEN partner.amount_residual <= 0 THEN 'no_action_needed'
|
|
WHEN in_need_of_action_aml.id IS NOT NULL AND (followup_next_action_date IS NULL OR followup_next_action_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,
|
|
{self.env.cr.mogrify(ResPartner._field_to_sql('partner', 'followup_next_action_date')).decode(self.env.cr.connection.encoding)} AS followup_next_action_date,
|
|
MAX(COALESCE(next_ful.delay, ful.delay)) as followup_delay,
|
|
SUM(aml.amount_residual) as amount_residual
|
|
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
|
|
LEFT JOIN account_followup_followup_line ful ON ful.id = aml.followup_line_id
|
|
LEFT 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, %(min_delay)s - 1)
|
|
AND next_ful.company_id = %(company_id)s
|
|
ORDER BY next_ful.delay ASC
|
|
LIMIT 1
|
|
)
|
|
WHERE account.active
|
|
AND account.account_type = 'asset_receivable'
|
|
AND aml.parent_state = 'posted'
|
|
AND aml.reconciled IS NOT TRUE
|
|
AND aml.company_id = ANY(%(company_ids)s)
|
|
{"" if partner_ids is None else "AND aml.partner_id IN %(partner_ids)s"}
|
|
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
|
|
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.active
|
|
AND line.no_followup IS NOT TRUE
|
|
AND line.parent_state = 'posted'
|
|
AND line.reconciled IS NOT TRUE
|
|
AND line.balance > 0
|
|
AND line.company_id = ANY(%(company_ids)s)
|
|
AND COALESCE(ful.delay, %(min_delay)s - 1) < partner.followup_delay
|
|
AND line.date_maturity IS NOT NULL
|
|
AND line.date_maturity + COALESCE(ful.delay, %(min_delay)s - 1) < %(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
|
|
WHERE line.partner_id = partner.id
|
|
AND account.account_type = 'asset_receivable'
|
|
AND account.active
|
|
AND line.no_followup IS NOT TRUE
|
|
AND line.parent_state = 'posted'
|
|
AND line.reconciled IS NOT TRUE
|
|
AND line.balance > 0
|
|
AND line.company_id = ANY(%(company_ids)s)
|
|
AND line.date_maturity IS NOT NULL
|
|
AND line.date_maturity < %(current_date)s
|
|
LIMIT 1
|
|
) exceeded_unreconciled_aml ON true
|
|
""", {
|
|
'company_ids': self.env.company.search([('id', 'child_of', self.env.company.id)]).ids,
|
|
'company_id': self.env.company.id,
|
|
'partner_ids': tuple(partner_ids or []),
|
|
'current_date': fields.Date.context_today(self), # Allow mocking the current day for testing purpose.
|
|
'min_delay': self._get_first_followup_level().delay or 0,
|
|
}
|
|
|
|
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 _get_followup_report(self, options):
|
|
followup_report = self.env.ref('account_reports.followup_report')
|
|
options = followup_report.get_options({
|
|
'forced_companies': self.env.company.search([('id', 'child_of', self.env.context.get('allowed_company_ids', self.env.company.id))]).ids,
|
|
'partner_ids': self.ids,
|
|
'unfold_all': True,
|
|
'unreconciled': True,
|
|
'all_entries': False,
|
|
'export_mode': 'print',
|
|
})
|
|
return self._get_partner_account_report_attachment(followup_report, options=options).id
|
|
|
|
def _get_followup_attachments(self, options):
|
|
res_attachment_ids = []
|
|
followup_line = options.get('followup_line')
|
|
|
|
# Add the Follow-up report
|
|
options['report_attachment_id'] = self._get_followup_report(options)
|
|
res_attachment_ids.append(options['report_attachment_id'])
|
|
|
|
# Add the attachments from the template
|
|
if template_id := options.get('template_id', followup_line.mail_template_id):
|
|
template_attachments = template_id._generate_template_attachments(self.ids, {'attachment_ids', 'report_template_ids'})[self.id]
|
|
res_attachment_ids += template_attachments['attachment_ids']
|
|
|
|
attachments_to_create = []
|
|
for dynamic_report in template_attachments['attachments']:
|
|
attachments_to_create.append({
|
|
'name': dynamic_report[0],
|
|
'datas': dynamic_report[1],
|
|
'res_model': self._name,
|
|
'res_id': self.id,
|
|
})
|
|
res_attachment_ids += self.env['ir.attachment'].create(attachments_to_create).ids
|
|
|
|
if not options.get('join_invoices', followup_line.join_invoices):
|
|
return res_attachment_ids
|
|
|
|
if options.get('manual_followup'):
|
|
# When coming from the wizard, the invoices are already in the attachments
|
|
res_attachment_ids += options.get('attachment_ids', [])
|
|
return res_attachment_ids
|
|
|
|
# Add the PDFs from overdue invoices
|
|
res_attachment_ids += self._get_invoices_to_print(options).invoice_pdf_report_id.ids
|
|
return res_attachment_ids
|
|
|
|
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
|
|
for user in self._get_followup_responsible(multiple_responsible=True):
|
|
self.activity_schedule(
|
|
activity_type_id=followup_line.activity_type_id.id or self._default_activity_type().id,
|
|
note=followup_line.activity_note,
|
|
summary=followup_line.activity_summary,
|
|
user_id=user.id,
|
|
)
|
|
|
|
options['followup_line'] = followup_line
|
|
self._update_next_followup_action_date(followup_line)
|
|
|
|
options['attachment_ids'] = self._get_followup_attachments(options)
|
|
|
|
self._send_followup(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 _create_followup_missing_information_wizard(self):
|
|
""" Returns a wizard containing all the partners with missing information.
|
|
"""
|
|
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _("Missing information"),
|
|
'view_mode': 'form',
|
|
'res_model': 'account_followup.missing.information.wizard',
|
|
'target': 'new',
|
|
'context': {'default_partner_ids': self.ids},
|
|
}
|
|
|
|
def _has_missing_followup_info(self):
|
|
self.ensure_one()
|
|
|
|
followup_contacts = self._get_all_followup_contacts() or self
|
|
|
|
return (
|
|
(self.followup_line_id.send_email and not any(followup_contacts.mapped('email'))) or
|
|
(self.followup_line_id.send_sms and not any(followup_contacts.mapped('phone')))
|
|
)
|
|
|
|
def action_manually_process_automatic_followups(self):
|
|
partners_with_missing_info = self.env['res.partner']
|
|
|
|
for partner in self:
|
|
if partner.followup_status != 'in_need_of_action':
|
|
continue
|
|
|
|
# Skip partner with missing info.
|
|
if partner._has_missing_followup_info():
|
|
partners_with_missing_info |= partner
|
|
continue
|
|
|
|
partner._execute_followup_partner()
|
|
|
|
# If one or more partners are missing information, open a wizard listing them.
|
|
if partners_with_missing_info:
|
|
return partners_with_missing_info._create_followup_missing_information_wizard()
|
|
|
|
def _cron_execute_followup_company(self, batch_size=1000):
|
|
"""Execute pending followups for the current company.
|
|
:param batch_size: maximum number of followup to process
|
|
:return: Return True if all followup were processed, False otherwise
|
|
:rtype: boolean
|
|
"""
|
|
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[:batch_size]:
|
|
try:
|
|
partner._execute_followup_partner()
|
|
except UserError as e:
|
|
# followup may raise exception due to configuration issues
|
|
# i.e. partner missing email
|
|
partner._message_log(body=e)
|
|
_logger.warning(e, exc_info=True)
|
|
return bool(not in_need_of_action_auto[batch_size:])
|
|
|
|
def _cron_execute_followup(self, batch_size=1000):
|
|
"""Execute pending followups for all companies.
|
|
:param batch_size: maximum number of followup to process for each company
|
|
"""
|
|
all_companies = self.env["res.company"].search([])
|
|
self.env["ir.cron"]._commit_progress(remaining=len(all_companies))
|
|
unfinished = 0
|
|
for company in all_companies:
|
|
# Since the cache is done by database and not by company, we need to invalidate in this special case
|
|
# where the context is changing in the same transaction
|
|
self.env.cr.cache.pop('res_partner_all_followup', None)
|
|
all_done = self.with_context(allowed_company_ids=company.ids)._cron_execute_followup_company(batch_size=batch_size)
|
|
if not all_done:
|
|
unfinished += 1
|
|
self.env["ir.cron"]._commit_progress(1)
|
|
if unfinished:
|
|
# if not all followups could be processed, let the cron be retriggered
|
|
self.env["ir.cron"]._commit_progress(remaining=unfinished)
|
|
|
|
def _show_pay_now_button(self):
|
|
invoice_online_payment = bool(self.env['ir.config_parameter'].sudo().get_param('account_payment.enable_portal_payment'))
|
|
payment_method_available = bool('payment.method' in self.env and self.env['payment.method'].sudo().search_count([('active', '=', 'True')]))
|
|
partner_has_user = bool(self.user_ids)
|
|
return invoice_online_payment and payment_method_available and partner_has_user
|
|
|
|
def _compute_has_moves(self):
|
|
field_names = ['partner_id', 'partner_shipping_id', 'commercial_partner_id']
|
|
partner_ids = {row[0] for row in self.env.execute_query(SQL("\nUNION ").join(
|
|
[self.env['account.move']._search(
|
|
[('company_id', 'in', self.env.companies.ids), (name, 'in', self.ids)]
|
|
).subselect('account_move.' + name) for name in field_names] +
|
|
[self.env['account.move.line']._search(
|
|
[('company_id', 'in', self.env.companies.ids), ('partner_id', 'in', self.ids)]
|
|
).subselect('account_move_line.partner_id')]
|
|
))}
|
|
|
|
for partner in self:
|
|
partner.has_moves = partner.id in partner_ids
|
|
|
|
def _get_followup_report_pdf(self, options):
|
|
"""
|
|
Generate the follow-up report and return a tuple (filename, pdf_bin).
|
|
"""
|
|
tz_date_str = format_date(self.env, fields.Date.today(), lang_code=self.env.user.lang or get_lang(self.env).code)
|
|
# To avoid having dots in the name of the file.
|
|
tz_date_str = tz_date_str.replace('.', '-')
|
|
followup_letter_name = _("Follow-up %(partner)s - %(date)s.pdf", partner=self.display_name, date=tz_date_str)
|
|
|
|
action = self.env.ref('account_followup.action_report_followup')
|
|
followup_letter = action.with_context(lang=self.lang or self.env.user.lang)._render_qweb_pdf('account_followup.report_followup_print_all', self.id, data={'options': options or {}})[0]
|
|
|
|
return followup_letter_name, followup_letter
|
|
|
|
def _get_followup_report_attachment(self, options):
|
|
"""
|
|
Generate the follow-up report and returns it as an attachment.
|
|
"""
|
|
|
|
followup_letter_name, followup_letter = self._get_followup_report_pdf(options)
|
|
return self.env['ir.attachment'].create({
|
|
'name': followup_letter_name,
|
|
'raw': followup_letter,
|
|
'res_id': self.id,
|
|
'res_model': 'res.partner',
|
|
'type': 'binary',
|
|
'mimetype': 'application/pdf',
|
|
})
|