1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from openerp.osv import fields, osv
23 from lxml import etree
25 from openerp.tools.translate import _
27 class followup(osv.osv):
28 _name = 'account_followup.followup'
29 _description = 'Account Follow-up'
32 'followup_line': fields.one2many('account_followup.followup.line', 'followup_id', 'Follow-up'),
33 'company_id': fields.many2one('res.company', 'Company', required=True),
34 'name': fields.related('company_id', 'name', string = "Name"),
37 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'account_followup.followup', context=c),
39 _sql_constraints = [('company_uniq', 'unique(company_id)', 'Only one follow-up per company is allowed')]
42 class followup_line(osv.osv):
44 def _get_default_template(self, cr, uid, ids, context=None):
46 return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_followup', 'email_template_account_followup_default')[1]
50 _name = 'account_followup.followup.line'
51 _description = 'Follow-up Criteria'
53 'name': fields.char('Follow-Up Action', size=64, required=True),
54 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of follow-up lines."),
55 'delay': fields.integer('Due Days', help="The number of days after the due date of the invoice to wait before sending the reminder. Could be negative if you want to send a polite alert beforehand.", required=True),
56 'followup_id': fields.many2one('account_followup.followup', 'Follow Ups', required=True, ondelete="cascade"),
57 'description': fields.text('Printed Message', translate=True),
58 'send_email':fields.boolean('Send an Email', help="When processing, it will send an email"),
59 'send_letter':fields.boolean('Send a Letter', help="When processing, it will print a letter"),
60 'manual_action':fields.boolean('Manual Action', help="When processing, it will set the manual action to be taken for that customer. "),
61 'manual_action_note':fields.text('Action To Do', placeholder="e.g. Give a phone call, check with others , ..."),
62 'manual_action_responsible_id':fields.many2one('res.users', 'Assign a Responsible', ondelete='set null'),
63 'email_template_id':fields.many2one('email.template', 'Email Template', ondelete='set null'),
66 _sql_constraints = [('days_uniq', 'unique(followup_id, delay)', 'Days of the follow-up levels must be different')]
70 'manual_action':False,
72 Dear %(partner_name)s,
74 Exception made if there was a mistake of ours, it seems that the following amount stays unpaid. Please, take appropriate measures in order to carry out this payment in the next 8 days.
76 Would your payment have been carried out after this mail was sent, please ignore this message. Do not hesitate to contact our accounting department.
80 'email_template_id': _get_default_template,
84 def _check_description(self, cr, uid, ids, context=None):
85 for line in self.browse(cr, uid, ids, context=context):
88 line.description % {'partner_name': '', 'date':'', 'user_signature': '', 'company_name': ''}
94 (_check_description, 'Your description is invalid, use the right legend or %% if you want to use the percent character.', ['description']),
98 class account_move_line(osv.osv):
100 def _get_result(self, cr, uid, ids, name, arg, context=None):
102 for aml in self.browse(cr, uid, ids, context=context):
103 res[aml.id] = aml.debit - aml.credit
106 _inherit = 'account.move.line'
108 'followup_line_id': fields.many2one('account_followup.followup.line', 'Follow-up Level',
109 ondelete='restrict'), #restrict deletion of the followup line
110 'followup_date': fields.date('Latest Follow-up', select=True),
111 'result':fields.function(_get_result, type='float', method=True,
112 string="Balance") #'balance' field is not the same
116 class res_partner(osv.osv):
118 def fields_view_get(self, cr, uid, view_id=None, view_type=None, context=None, toolbar=False, submenu=False):
119 res = super(res_partner, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context,
120 toolbar=toolbar, submenu=submenu)
121 context = context or {}
122 if view_type == 'form' and context.get('Followupfirst'):
123 doc = etree.XML(res['arch'], parser=None, base_url=None)
124 first_node = doc.xpath("//page[@name='followup_tab']")
125 root = first_node[0].getparent()
126 root.insert(0, first_node[0])
127 res['arch'] = etree.tostring(doc, encoding="utf-8")
130 def _get_latest(self, cr, uid, ids, names, arg, context=None, company_id=None):
132 if company_id == None:
133 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
135 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
136 for partner in self.browse(cr, uid, ids, context=context):
137 amls = partner.unreconciled_aml_ids
141 latest_level_without_lit = False
142 latest_days_without_lit = False
144 if (aml.company_id == company) and (aml.followup_line_id != False) and (not latest_days or latest_days < aml.followup_line_id.delay):
145 latest_days = aml.followup_line_id.delay
146 latest_level = aml.followup_line_id.id
147 if (aml.company_id == company) and (not latest_date or latest_date < aml.followup_date):
148 latest_date = aml.followup_date
149 if (aml.company_id == company) and (aml.blocked == False) and (aml.followup_line_id != False and
150 (not latest_days_without_lit or latest_days_without_lit < aml.followup_line_id.delay)):
151 latest_days_without_lit = aml.followup_line_id.delay
152 latest_level_without_lit = aml.followup_line_id.id
153 res[partner.id] = {'latest_followup_date': latest_date,
154 'latest_followup_level_id': latest_level,
155 'latest_followup_level_id_without_lit': latest_level_without_lit}
158 def do_partner_manual_action(self, cr, uid, partner_ids, context=None):
159 #partner_ids -> res.partner
160 for partner in self.browse(cr, uid, partner_ids, context=context):
161 #Check action: check if the action was not empty, if not add
163 if partner.payment_next_action:
164 action_text = (partner.payment_next_action or '') + "\n" + (partner.latest_followup_level_id_without_lit.manual_action_note or '')
166 action_text = partner.latest_followup_level_id_without_lit.manual_action_note or ''
168 #Check date: put the minimum date if it existed already
169 action_date = (partner.payment_next_action_date and min(partner.payment_next_action_date, fields.date.context_today(self, cr, uid, context=context))
170 ) or fields.date.context_today(self, cr, uid, context=context)
172 # Check responsible: if partner has not got a responsible already, take from follow-up
173 responsible_id = False
174 if partner.payment_responsible_id:
175 responsible_id = partner.payment_responsible_id.id
177 p = partner.latest_followup_level_id_without_lit.manual_action_responsible_id
178 responsible_id = p and p.id or False
179 self.write(cr, uid, [partner.id], {'payment_next_action_date': action_date,
180 'payment_next_action': action_text,
181 'payment_responsible_id': responsible_id})
183 def do_partner_print(self, cr, uid, wizard_partner_ids, data, context=None):
184 #wizard_partner_ids are ids from special view, not from res.partner
185 if not wizard_partner_ids:
187 data['partner_ids'] = wizard_partner_ids
190 'model': 'account_followup.followup',
194 'type': 'ir.actions.report.xml',
195 'report_name': 'account_followup.followup.print',
199 def do_partner_mail(self, cr, uid, partner_ids, context=None):
203 ctx['followup'] = True
204 #partner_ids are res.partner ids
205 # If not defined by latest follow-up level, it will be the default template if it can find it
206 mtp = self.pool.get('email.template')
208 for partner in self.browse(cr, uid, partner_ids, context=ctx):
209 if partner.email and partner.email.strip():
210 level = partner.latest_followup_level_id_without_lit
211 if level and level.send_email and level.email_template_id and level.email_template_id.id:
212 mtp.send_mail(cr, uid, level.email_template_id.id, partner.id, context=ctx)
214 mail_template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid,
215 'account_followup', 'email_template_account_followup_default')
216 mtp.send_mail(cr, uid, mail_template_id[1], partner.id, context=ctx)
218 unknown_mails = unknown_mails + 1
219 action_text = _("Email not sent because of email address of partner not filled in")
220 if partner.payment_next_action_date:
221 payment_action_date = min(fields.date.context_today(self, cr, uid, context=ctx), partner.payment_next_action_date)
223 payment_action_date = fields.date.context_today(self, cr, uid, context=ctx)
224 if partner.payment_next_action:
225 payment_next_action = partner.payment_next_action + " \n " + action_text
227 payment_next_action = action_text
228 self.write(cr, uid, [partner.id], {'payment_next_action_date': payment_action_date,
229 'payment_next_action': payment_next_action}, context=ctx)
232 def get_followup_table_html(self, cr, uid, ids, context=None):
233 """ Build the html tables to be included in emails send to partners,
234 when reminding them their overdue invoices.
235 :param ids: [id] of the partner for whom we are building the tables
238 from report import account_followup_print
241 partner = self.browse(cr, uid, ids[0], context=context)
243 if partner.unreconciled_aml_ids:
244 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
245 current_date = fields.date.context_today(self, cr, uid, context=context)
246 rml_parse = account_followup_print.report_rappel(cr, uid, "followup_rml_parser")
247 final_res = rml_parse._lines_get_with_partner(partner, company.id)
249 for currency_dict in final_res:
250 currency = currency_dict.get('line', [{'currency_id': company.currency_id}])[0]['currency_id']
251 followup_table += '''
252 <table border="2" width=100%%>
254 <td>Invoice date</td>
260 ''' % (currency.symbol)
262 for aml in currency_dict['line']:
263 block = aml['blocked'] and 'X' or ' '
264 total += aml['balance']
267 date = aml['date_maturity'] or aml['date']
268 if date <= current_date and aml['balance'] > 0:
271 followup_table +="<TR>" + strbegin + str(aml['date']) + strend + strbegin + aml['ref'] + strend + strbegin + str(date) + strend + strbegin + str(aml['balance']) + strend + strbegin + block + strend + "</TR>"
272 total = rml_parse.formatLang(total, dp='Account', currency_obj=currency)
273 followup_table += '''<tr> </tr>
275 <center>Amount due: %s </center>''' % (total)
276 return followup_table
278 def action_done(self, cr, uid, ids, context=None):
279 return self.write(cr, uid, ids, {'payment_next_action_date': False, 'payment_next_action':'', 'payment_responsible_id': False}, context=context)
281 def do_button_print(self, cr, uid, ids, context=None):
282 assert(len(ids) == 1)
283 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
284 #search if the partner has accounting entries to print. If not, it may not be present in the
285 #psql view the report is based on, so we need to stop the user here.
286 if not self.pool.get('account.move.line').search(cr, uid, [
287 ('partner_id', '=', ids[0]),
288 ('account_id.type', '=', 'receivable'),
289 ('reconcile_id', '=', False),
290 ('state', '!=', 'draft'),
291 ('company_id', '=', company_id),
293 raise osv.except_osv(_('Error!'),_("The partner does not have any accounting entries to print in the overdue report for the current company."))
294 self.message_post(cr, uid, [ids[0]], body=_('Printed overdue payments report'), context=context)
295 #build the id of this partner in the psql view. Could be replaced by a search with [('company_id', '=', company_id),('partner_id', '=', ids[0])]
296 wizard_partner_ids = [ids[0] * 10000 + company_id]
297 followup_ids = self.pool.get('account_followup.followup').search(cr, uid, [('company_id', '=', company_id)], context=context)
299 raise osv.except_osv(_('Error!'),_("There is no followup plan defined for the current company."))
301 'date': fields.date.today(),
302 'followup_id': followup_ids[0],
304 #call the print overdue report on this partner
305 return self.do_partner_print(cr, uid, wizard_partner_ids, data, context=context)
307 def _get_amounts_and_date(self, cr, uid, ids, name, arg, context=None):
309 Function that computes values for the followup functional fields. Note that 'payment_amount_due'
310 is similar to 'credit' field on res.partner except it filters on user's company.
313 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
314 current_date = fields.date.context_today(self, cr, uid, context=context)
315 for partner in self.browse(cr, uid, ids, context=context):
316 worst_due_date = False
317 amount_due = amount_overdue = 0.0
318 for aml in partner.unreconciled_aml_ids:
319 if (aml.company_id == company):
320 date_maturity = aml.date_maturity or aml.date
321 if not worst_due_date or date_maturity < worst_due_date:
322 worst_due_date = date_maturity
323 amount_due += aml.result
324 if (date_maturity <= current_date):
325 amount_overdue += aml.result
326 res[partner.id] = {'payment_amount_due': amount_due,
327 'payment_amount_overdue': amount_overdue,
328 'payment_earliest_due_date': worst_due_date}
331 def _get_followup_overdue_query(self, cr, uid, args, overdue_only=False, context=None):
333 This function is used to build the query and arguments to use when making a search on functional fields
335 * payment_amount_overdue
336 Basically, the query is exactly the same except that for overdue there is an extra clause in the WHERE.
338 :param args: arguments given to the search in the usual domain notation (list of tuples)
339 :param overdue_only: option to add the extra argument to filter on overdue accounting entries or not
340 :returns: a tuple with
341 * the query to execute as first element
342 * the arguments for the execution of this query
345 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
346 having_where_clause = ' AND '.join(map(lambda x: '(SUM(bal2) %s %%s)' % (x[1]), args))
347 having_values = [x[2] for x in args]
348 query = self.pool.get('account.move.line')._query_get(cr, uid, context=context)
349 overdue_only_str = overdue_only and 'AND date_maturity <= NOW()' or ''
350 return ('''SELECT pid AS partner_id, SUM(bal2) FROM
351 (SELECT CASE WHEN bal IS NOT NULL THEN bal
352 ELSE 0.0 END AS bal2, p.id as pid FROM
353 (SELECT (debit-credit) AS bal, partner_id
354 FROM account_move_line l
356 (SELECT id FROM account_account
357 WHERE type=\'receivable\' AND active)
358 ''' + overdue_only_str + '''
359 AND reconcile_id IS NULL
361 AND ''' + query + ''') AS l
362 RIGHT JOIN res_partner p
363 ON p.id = partner_id ) AS pl
364 GROUP BY pid HAVING ''' + having_where_clause, [company_id] + having_values)
366 def _payment_overdue_search(self, cr, uid, obj, name, args, context=None):
369 query, query_args = self._get_followup_overdue_query(cr, uid, args, overdue_only=True, context=context)
370 cr.execute(query, query_args)
373 return [('id','=','0')]
374 return [('id','in', [x[0] for x in res])]
376 def _payment_earliest_date_search(self, cr, uid, obj, name, args, context=None):
379 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
380 having_where_clause = ' AND '.join(map(lambda x: '(MIN(l.date_maturity) %s %%s)' % (x[1]), args))
381 having_values = [x[2] for x in args]
382 query = self.pool.get('account.move.line')._query_get(cr, uid, context=context)
383 cr.execute('SELECT partner_id FROM account_move_line l '\
384 'WHERE account_id IN '\
385 '(SELECT id FROM account_account '\
386 'WHERE type=\'receivable\' AND active) '\
387 'AND l.company_id = %s '
388 'AND reconcile_id IS NULL '\
390 'AND partner_id IS NOT NULL '\
391 'GROUP BY partner_id HAVING '+ having_where_clause,
392 [company_id] + having_values)
395 return [('id','=','0')]
396 return [('id','in', [x[0] for x in res])]
398 def _payment_due_search(self, cr, uid, obj, name, args, context=None):
401 query, query_args = self._get_followup_overdue_query(cr, uid, args, overdue_only=False, context=context)
402 cr.execute(query, query_args)
405 return [('id','=','0')]
406 return [('id','in', [x[0] for x in res])]
408 _inherit = "res.partner"
410 'payment_responsible_id':fields.many2one('res.users', ondelete='set null', string='Follow-up Responsible',
411 help="Optionally you can assign a user to this field, which will make him responsible for the action."),
412 'payment_note':fields.text('Customer Payment Promise', help="Payment Note"),
413 'payment_next_action':fields.text('Next Action',
414 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. "),
415 'payment_next_action_date':fields.date('Next Action Date',
416 help="This is when the manual follow-up is needed. " \
417 "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."),
418 'unreconciled_aml_ids':fields.one2many('account.move.line', 'partner_id', domain=['&', ('reconcile_id', '=', False), '&',
419 ('account_id.active','=', True), '&', ('account_id.type', '=', 'receivable'), ('state', '!=', 'draft')]),
420 'latest_followup_date':fields.function(_get_latest, method=True, type='date', string="Latest Follow-up Date",
421 help="Latest date that the follow-up level of the partner was changed",
422 store=False, multi="latest"),
423 'latest_followup_level_id':fields.function(_get_latest, method=True,
424 type='many2one', relation='account_followup.followup.line', string="Latest Follow-up Level",
425 help="The maximum follow-up level",
428 'latest_followup_level_id_without_lit':fields.function(_get_latest, method=True,
429 type='many2one', relation='account_followup.followup.line', string="Latest Follow-up Level without litigation",
430 help="The maximum follow-up level without taking into account the account move lines with litigation",
433 'payment_amount_due':fields.function(_get_amounts_and_date,
434 type='float', string="Amount Due",
435 store = False, multi="followup",
436 fnct_search=_payment_due_search),
437 'payment_amount_overdue':fields.function(_get_amounts_and_date,
438 type='float', string="Amount Overdue",
439 store = False, multi="followup",
440 fnct_search = _payment_overdue_search),
441 'payment_earliest_due_date':fields.function(_get_amounts_and_date,
443 string = "Worst Due Date",
445 fnct_search=_payment_earliest_date_search),
448 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: