Files
enterprise-addons/account_followup/models/res_partner.py
T

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',
})