From ef1ef68222b3e37c25248e0b28bbc81986e37018 Mon Sep 17 00:00:00 2001 From: git_admin Date: Sat, 2 May 2026 07:02:22 +0000 Subject: [PATCH] Tower: upload om_account_followup 19.0.1.0.2 (was 1.0.2, via marketplace) --- addons/om_account_followup/models/partner.py | 408 +++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 addons/om_account_followup/models/partner.py 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 += ''' + + + + + + + + + + ''' + total = 0 + for aml in currency_dict['line']: + total += aml['balance'] + strbegin = "" + date = aml['date_maturity'] or aml['date'] + date = datetime.strptime(date, "%m/%d/%Y").date() + if date <= current_date and aml['balance'] > 0: + strbegin = "" + 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 += ''' +
''' + _("Invoice Date") + '''''' + _("Description") + '''''' + _("Reference") + '''''' + _("Due Date") + '''''' + _("Amount") + " (%s)" % ( + currency.symbol) + '''''' + _("Lit.") + '''
" + strend = "" + strend = "
+
''' + _( + "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' + )