diff --git a/addons/om_account_followup/models/partner.py b/addons/om_account_followup/models/partner.py
new file mode 100644
index 0000000..a7590ea
--- /dev/null
+++ b/addons/om_account_followup/models/partner.py
@@ -0,0 +1,408 @@
+from functools import reduce
+from lxml import etree
+from odoo import api, fields, models, _
+from datetime import datetime
+from odoo.exceptions import ValidationError
+from odoo.tools.misc import formatLang
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ def fields_view_get(self, view_id=None, view_type='form', toolbar=False,
+ submenu=False):
+ res = super(ResPartner, self).fields_view_get(
+ view_id=view_id, view_type=view_type, toolbar=toolbar,
+ submenu=submenu)
+ if view_type == 'form' and self.env.context.get('Followupfirst'):
+ doc = etree.XML(res['arch'], parser=None, base_url=None)
+ first_node = doc.xpath("//page[@name='followup_tab']")
+ root = first_node[0].getparent()
+ root.insert(0, first_node[0])
+ res['arch'] = etree.tostring(doc, encoding="utf-8")
+ return res
+
+ def _get_latest(self):
+ company = self.env.user.company_id
+ for partner in self:
+ amls = partner.unreconciled_aml_ids
+ latest_date = False
+ latest_level = False
+ latest_days = False
+ latest_level_without_lit = False
+ latest_days_without_lit = False
+ for aml in amls:
+ aml_followup = aml.followup_line_id
+ if (aml.company_id == company) and aml_followup and \
+ (not latest_days or latest_days < aml_followup.delay):
+ latest_days = aml_followup.delay
+ latest_level = aml_followup.id
+ if (aml.company_id == company) and aml.followup_date and (
+ not latest_date or latest_date < aml.followup_date):
+ latest_date = aml.followup_date
+ if (aml.company_id == company) and \
+ (aml_followup and (not latest_days_without_lit or
+ latest_days_without_lit < aml_followup.delay)):
+ latest_days_without_lit = aml_followup.delay
+ latest_level_without_lit = aml_followup.id
+ partner.latest_followup_date = latest_date
+ partner.latest_followup_level_id = latest_level
+ partner.latest_followup_level_id_without_lit = latest_level_without_lit
+
+ def do_partner_manual_action_dermanord(self, followup_line):
+ action_text = followup_line.manual_action_note or ''
+
+ action_date = self.payment_next_action_date or \
+ fields.Date.today()
+ if self.payment_responsible_id:
+ responsible_id = self.payment_responsible_id.id
+ else:
+ p = followup_line.manual_action_responsible_id
+ responsible_id = p and p.id or False
+ self.write({'payment_next_action_date': action_date,
+ 'payment_next_action': action_text,
+ 'payment_responsible_id': responsible_id})
+
+ def do_partner_manual_action(self, partner_ids):
+ for partner in self.browse(partner_ids):
+ followup_without_lit = partner.latest_followup_level_id_without_lit
+ if partner.payment_next_action:
+ action_text = \
+ (partner.payment_next_action or '') + "\n" + \
+ (followup_without_lit.manual_action_note or '')
+ else:
+ action_text = followup_without_lit.manual_action_note or ''
+
+ action_date = partner.payment_next_action_date or \
+ fields.Date.today()
+
+ if partner.payment_responsible_id:
+ responsible_id = partner.payment_responsible_id.id
+ else:
+ p = followup_without_lit.manual_action_responsible_id
+ responsible_id = p and p.id or False
+ partner.write({'payment_next_action_date': action_date,
+ 'payment_next_action': action_text,
+ 'payment_responsible_id': responsible_id})
+
+ def do_partner_print(self, wizard_partner_ids, data):
+ if not wizard_partner_ids:
+ return {}
+ data['partner_ids'] = wizard_partner_ids
+ datas = {
+ 'ids': wizard_partner_ids,
+ 'model': 'followup.followup',
+ 'form': data
+ }
+ return self.env.ref(
+ 'om_account_followup.action_report_followup').report_action(
+ self, data=datas)
+
+ def do_partner_mail(self):
+ ctx = self.env.context.copy()
+ ctx['followup'] = True
+ template = 'om_account_followup.email_template_om_account_followup_default'
+ unknown_mails = 0
+ for partner in self:
+ partners_to_email = [child for child in partner.child_ids if
+ child.type == 'invoice' and child.email]
+ if not partners_to_email and partner.email:
+ partners_to_email = [partner]
+ if partners_to_email:
+ level = partner.latest_followup_level_id_without_lit
+ for partner_to_email in partners_to_email:
+ if level and level.send_email and \
+ level.email_template_id and \
+ level.email_template_id.id:
+ level.email_template_id.with_context(ctx).send_mail(
+ partner_to_email.id)
+ else:
+ mail_template_id = self.env.ref(template)
+ mail_template_id.with_context(ctx).send_mail(
+ partner_to_email.id)
+ if partner not in partners_to_email:
+ partner.message_post(body=_(
+ 'Overdue email sent to %s' % ', '.join(
+ ['%s <%s>' % (partner.name, partner.email) for
+ partner in partners_to_email])))
+ else:
+ unknown_mails = unknown_mails + 1
+ action_text = _("Email not sent because of email address "
+ "of partner not filled in")
+ if partner.payment_next_action_date:
+ payment_action_date = min(
+ fields.Date.today(),
+ partner.payment_next_action_date)
+ else:
+ payment_action_date = fields.Date.today()
+ if partner.payment_next_action:
+ payment_next_action = \
+ partner.payment_next_action + " \n " + action_text
+ else:
+ payment_next_action = action_text
+ partner.with_context(ctx).write(
+ {'payment_next_action_date': payment_action_date,
+ 'payment_next_action': payment_next_action})
+ return unknown_mails
+
+ def get_followup_table_html(self):
+ self.ensure_one()
+ partner = self.commercial_partner_id
+ followup_table = ''
+ if partner.unreconciled_aml_ids:
+ company = self.env.user.company_id
+ current_date = fields.Date.today()
+ report = self.env['report.om_account_followup.report_followup']
+ final_res = report._lines_get_with_partner(partner, company.id)
+
+ for currency_dict in final_res:
+ currency = currency_dict.get('line', [
+ {'currency_id': company.currency_id}])[0]['currency_id']
+ followup_table += '''
+
+
+ | ''' + _("Invoice Date") + ''' |
+ ''' + _("Description") + ''' |
+ ''' + _("Reference") + ''' |
+ ''' + _("Due Date") + ''' |
+ ''' + _("Amount") + " (%s)" % (
+ currency.symbol) + ''' |
+ ''' + _("Lit.") + ''' |
+
+ '''
+ total = 0
+ for aml in currency_dict['line']:
+ total += aml['balance']
+ strbegin = ""
+ strend = " | "
+ date = aml['date_maturity'] or aml['date']
+ date = datetime.strptime(date, "%m/%d/%Y").date()
+ if date <= current_date and aml['balance'] > 0:
+ strbegin = ""
+ strend = " | "
+ followup_table += "" + strbegin + str(aml['date']) + \
+ strend + strbegin + aml['name'] + \
+ strend + strbegin + \
+ (aml['ref'] or '') + strend + \
+ strbegin + str(date) + strend + \
+ strbegin + str(aml['balance']) + \
+ strend + "
"
+
+ total = reduce(lambda x, y: x + y['balance'],
+ currency_dict['line'], 0.00)
+ total = formatLang(self.env, total, currency_obj=currency)
+ followup_table += '''
+
+ ''' + _(
+ "Amount due") + ''' : %s ''' % (total)
+ return followup_table
+
+ def write(self, vals):
+ if vals.get("payment_responsible_id", False):
+ for part in self:
+ if part.payment_responsible_id != \
+ self.env['res.users'].browse(vals["payment_responsible_id"]):
+ # Find partner_id of user put as responsible
+ responsible_partner_id = self.env["res.users"].browse(
+ vals['payment_responsible_id']).partner_id.id
+ part.message_post(
+ body=_("You became responsible to do the next action "
+ "for the payment follow-up of") +
+ " " + part.name +
+ " ",
+ type='comment',
+ context=self.env.context,
+ partner_ids=[responsible_partner_id])
+ return super(ResPartner, self).write(vals)
+
+ def action_done(self):
+ return self.write({'payment_next_action_date': False,
+ 'payment_next_action': '',
+ 'payment_responsible_id': False})
+
+ def do_button_print(self):
+ self.ensure_one()
+ company_id = self.env.user.company_id.id
+ if not self.env['account.move.line'].search(
+ [('partner_id', '=', self.id),
+ ('account_id.account_type', '=', 'asset_receivable'),
+ ('full_reconcile_id', '=', False),
+ ('company_id', '=', company_id),
+ '|', ('date_maturity', '=', False),
+ ('date_maturity', '<=', fields.Date.today())]):
+ raise ValidationError(
+ _("The partner does not have any accounting entries to "
+ "print in the overdue report for the current company."))
+ self.message_post(body=_('Printed overdue payments report'))
+ self.message_post(body=_('Printed overdue payments report'))
+
+ wizard_partner_ids = [self.id * 10000 + company_id]
+ followup_ids = self.env['followup.followup'].search(
+ [('company_id', '=', company_id)])
+ if not followup_ids:
+ raise ValidationError(_(
+ "There is no followup plan defined for the current company."))
+ data = {
+ 'date': fields.date.today(),
+ 'followup_id': followup_ids[0].id,
+ }
+ return self.do_partner_print(wizard_partner_ids, data)
+
+ def _get_amounts_and_date(self):
+ company = self.env.user.company_id
+ current_date = fields.Date.today()
+ for partner in self:
+ worst_due_date = False
+ amount_due = amount_overdue = 0.0
+ for aml in partner.unreconciled_aml_ids:
+ if (aml.company_id == company):
+ date_maturity = aml.date_maturity or aml.date
+ if not worst_due_date or date_maturity < worst_due_date:
+ worst_due_date = date_maturity
+ amount_due += aml.result
+ if (date_maturity <= current_date):
+ amount_overdue += aml.result
+ partner.payment_amount_due = amount_due
+ partner.payment_amount_overdue = amount_overdue
+ partner.payment_earliest_due_date = worst_due_date
+
+ def _get_followup_overdue_query(self, args, overdue_only=False):
+ company_id = self.env.user.company_id.id
+ having_clauses = []
+ having_values = []
+
+ for field, operator, value in args:
+ if operator in ['=', '!=', '>', '>=', '<', '<=']:
+ having_clauses.append(f'SUM(bal2) {operator} %s')
+ having_values.append(value)
+ else:
+ raise ValueError(f"Unsupported operator: {operator}")
+
+ having_where_clause = ' AND '.join(having_clauses)
+ overdue_only_str = 'AND date_maturity <= NOW()' if overdue_only else ''
+
+ query = ('''
+ SELECT pid AS partner_id, SUM(bal2) FROM (
+ SELECT
+ CASE WHEN bal IS NOT NULL THEN bal ELSE 0.0 END AS bal2,
+ p.id as pid
+ FROM (
+ SELECT
+ (debit - credit) AS bal,
+ partner_id
+ FROM account_move_line l
+ LEFT JOIN account_account a ON a.id = l.account_id
+ WHERE a.account_type = 'asset_receivable'
+ %s AND full_reconcile_id IS NULL
+ AND l.company_id = %%s
+ ) AS l
+ RIGHT JOIN res_partner p ON p.id = partner_id
+ ) AS pl
+ GROUP BY pid HAVING %s
+ ''') % (overdue_only_str, having_where_clause)
+
+ params = [company_id] + having_values
+ return query, params
+
+ def _payment_overdue_search(self, operator, operand):
+ args = [('payment_amount_overdue', operator, operand)]
+ query, params = self._get_followup_overdue_query(args, overdue_only=True)
+ self.env.cr.execute(query, params)
+ res = self.env.cr.fetchall()
+ if not res:
+ return [('id', '=', '0')]
+ return [('id', 'in', [x[0] for x in res])]
+
+ def _payment_earliest_date_search(self, operator, operand):
+ args = [('payment_earliest_due_date', operator, operand)]
+ company_id = self.env.user.company_id.id
+ having_where_clause = ' AND '.join(
+ map(lambda x: "(MIN(l.date_maturity) %s '%%s')" % (x[1]), args))
+ having_values = [x[2] for x in args]
+ having_where_clause = having_where_clause % (having_values[0])
+ query = """SELECT partner_id FROM account_move_line l
+ LEFT JOIN account_account a ON a.id = l.account_id
+ WHERE a.account_type = 'asset_receivable'
+ AND l.company_id = %s
+ AND l.full_reconcile_id IS NULL
+ AND partner_id IS NOT NULL GROUP BY partner_id"""
+ query = query % company_id
+ if having_where_clause:
+ query += ' HAVING %s ' % (having_where_clause)
+ self.env.cr.execute(query)
+ res = self.env.cr.fetchall()
+ if not res:
+ return [('id', '=', '0')]
+ return [('id', 'in', [x[0] for x in res])]
+
+ def _payment_due_search(self, operator, operand):
+ args = [('payment_amount_due', operator, operand)]
+ query, params = self._get_followup_overdue_query(args, overdue_only=False)
+ self.env.cr.execute(query, params)
+ res = self.env.cr.fetchall()
+ if not res:
+ return [('id', '=', '0')]
+ return [('id', 'in', [x[0] for x in res])]
+
+ def _get_partners(self):
+ partners = set()
+ for aml in self:
+ if aml.partner_id:
+ partners.add(aml.partner_id.id)
+ return list(partners)
+
+ payment_responsible_id = fields.Many2one(
+ 'res.users', ondelete='set null',
+ string='Follow-up Responsible', tracking=True, copy=False,
+ help="Optionally you can assign a user to this field, which will make "
+ "him responsible for the action.")
+ payment_note = fields.Text(
+ 'Customer Payment Promise', help="Payment Note", copy=False
+ )
+ payment_next_action = fields.Text(
+ 'Next Action', copy=False, tracking=True,
+ help="This is the next action to be taken. It will automatically be "
+ "set when the partner gets a follow-up level that requires a manual action. "
+ )
+ payment_next_action_date = fields.Date(
+ 'Next Action Date', copy=False,
+ help="This is when the manual follow-up is needed. The date will be "
+ "set to the current date when the partner gets a follow-up level "
+ "that requires a manual action. Can be practical to set manually "
+ "e.g. to see if he keeps his promises."
+ )
+ unreconciled_aml_ids = fields.One2many(
+ 'account.move.line', 'partner_id',
+ domain=[('full_reconcile_id', '=', False), ('account_id.account_type', '=', 'asset_receivable')]
+ )
+ latest_followup_date = fields.Date(
+ compute='_get_latest', string="Latest Follow-up Date", compute_sudo=True,
+ help="Latest date that the follow-up level of the partner was changed"
+ )
+ latest_followup_level_id = fields.Many2one(
+ 'followup.line', compute='_get_latest', compute_sudo=True,
+ string="Latest Follow-up Level", help="The maximum follow-up level"
+ )
+
+ latest_followup_sequence = fields.Integer(
+ 'Sequence',
+ help="Gives the sequence order when displaying a list of follow-up lines.", default=0
+ )
+ latest_followup_level_id_without_lit = fields.Many2one(
+ 'followup.line', compute='_get_latest', compute_sudo=True,
+ string="Latest Follow-up Level without litigation",
+ help="The maximum follow-up level without taking into "
+ "account the account move lines with litigation")
+ payment_amount_due = fields.Float(
+ compute='_get_amounts_and_date',
+ string="Amount Due", search='_payment_due_search'
+ )
+ payment_amount_overdue = fields.Float(
+ compute='_get_amounts_and_date',
+ string="Amount Overdue", search='_payment_overdue_search'
+ )
+ payment_earliest_due_date = fields.Date(
+ compute='_get_amounts_and_date', string="Worst Due Date",
+ search='_payment_earliest_date_search'
+ )