Launchpad automatic translations update.
[odoo/odoo.git] / addons / account_followup / account_followup.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from openerp.osv import fields, osv
23 from lxml import etree
24
25 from openerp.tools.translate import _
26
27 class followup(osv.osv):
28     _name = 'account_followup.followup'
29     _description = 'Account Follow-up'
30     _rec_name = 'name'
31     _columns = {
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"),
35     }
36     _defaults = {
37         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'account_followup.followup', context=c),
38     }
39     _sql_constraints = [('company_uniq', 'unique(company_id)', 'Only one follow-up per company is allowed')] 
40
41
42 class followup_line(osv.osv):
43
44     def _get_default_template(self, cr, uid, ids, context=None):
45         try:
46             return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_followup', 'email_template_account_followup_default')[1]
47         except ValueError:
48             return False
49
50     _name = 'account_followup.followup.line'
51     _description = 'Follow-up Criteria'
52     _columns = {
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'),
64     }
65     _order = 'delay'
66     _sql_constraints = [('days_uniq', 'unique(followup_id, delay)', 'Days of the follow-up levels must be different')]
67     _defaults = {
68         'send_email': True,
69         'send_letter': True,
70         'manual_action':False,
71         'description': """
72         Dear %(partner_name)s,
73
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.
75
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.
77
78 Best Regards,
79 """,
80     'email_template_id': _get_default_template,
81     }
82
83
84     def _check_description(self, cr, uid, ids, context=None):
85         for line in self.browse(cr, uid, ids, context=context):
86             if line.description:
87                 try:
88                     line.description % {'partner_name': '', 'date':'', 'user_signature': '', 'company_name': ''}
89                 except:
90                     return False
91         return True
92
93     _constraints = [
94         (_check_description, 'Your description is invalid, use the right legend or %% if you want to use the percent character.', ['description']),
95     ]
96
97
98 class account_move_line(osv.osv):
99
100     def _get_result(self, cr, uid, ids, name, arg, context=None):
101         res = {}
102         for aml in self.browse(cr, uid, ids, context=context):
103             res[aml.id] = aml.debit - aml.credit
104         return res
105
106     _inherit = 'account.move.line'
107     _columns = {
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
113     }
114
115
116 class res_partner(osv.osv):
117
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")
128         return res
129
130     def _get_latest(self, cr, uid, ids, names, arg, context=None, company_id=None):
131         res={}
132         if company_id == None:
133             company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
134         else:
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
138             latest_date = False
139             latest_level = False
140             latest_days = False
141             latest_level_without_lit = False
142             latest_days_without_lit = False
143             for aml in amls:
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}
156         return res
157
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
162             action_text= ""
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 '')
165             else:
166                 action_text = partner.latest_followup_level_id_without_lit.manual_action_note or ''
167
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)
171
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
176             else:
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})
182
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:
186             return {}
187         data['partner_ids'] = wizard_partner_ids
188         datas = {
189              'ids': [],
190              'model': 'account_followup.followup',
191              'form': data
192         }
193         return {
194             'type': 'ir.actions.report.xml',
195             'report_name': 'account_followup.followup.print',
196             'datas': datas,
197             }
198
199     def do_partner_mail(self, cr, uid, partner_ids, context=None):
200         if context is None:
201             context = {}
202         ctx = context.copy()
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')
207         unknown_mails = 0
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)
213                 else:
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)
217             else:
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)
222                 else:
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
226                 else:
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)
230         return unknown_mails
231
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
236             :rtype: string
237         """
238         from report import account_followup_print
239
240         assert len(ids) == 1
241         partner = self.browse(cr, uid, ids[0], context=context)
242         followup_table = ''
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)
248
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%%>
253                 <tr>
254                     <td>Invoice date</td>
255                     <td>Reference</td>
256                     <td>Due date</td>
257                     <td>Amount (%s)</td>
258                     <td>Lit.</td>
259                 </tr>
260                 ''' % (currency.symbol)
261                 total = 0
262                 for aml in currency_dict['line']:
263                     block = aml['blocked'] and 'X' or ' '
264                     total += aml['balance']
265                     strbegin = "<TD>"
266                     strend = "</TD>"
267                     date = aml['date_maturity'] or aml['date']
268                     if date <= current_date and aml['balance'] > 0:
269                         strbegin = "<TD><B>"
270                         strend = "</B></TD>"
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>
274                                 </table>
275                                 <center>Amount due: %s </center>''' % (total)
276         return followup_table
277
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)
280
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),
292                                                                   ], context=context):
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)
298         if not followup_ids:
299             raise osv.except_osv(_('Error!'),_("There is no followup plan defined for the current company."))
300         data = {
301             'date': fields.date.today(),
302             'followup_id': followup_ids[0],
303         }
304         #call the print overdue report on this partner
305         return self.do_partner_print(cr, uid, wizard_partner_ids, data, context=context)
306
307     def _get_amounts_and_date(self, cr, uid, ids, name, arg, context=None):
308         '''
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.
311         '''
312         res = {}
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}
329         return res
330
331     def _get_followup_overdue_query(self, cr, uid, args, overdue_only=False, context=None):
332         '''
333         This function is used to build the query and arguments to use when making a search on functional fields
334             * payment_amount_due
335             * payment_amount_overdue
336         Basically, the query is exactly the same except that for overdue there is an extra clause in the WHERE.
337
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
343         :rtype: (string, [])
344         '''
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
355                     WHERE account_id IN
356                             (SELECT id FROM account_account
357                             WHERE type=\'receivable\' AND active)
358                     ''' + overdue_only_str + '''
359                     AND reconcile_id IS NULL
360                     AND company_id = %s
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)
365
366     def _payment_overdue_search(self, cr, uid, obj, name, args, context=None):
367         if not args:
368             return []
369         query, query_args = self._get_followup_overdue_query(cr, uid, args, overdue_only=True, context=context)
370         cr.execute(query, query_args)
371         res = cr.fetchall()
372         if not res:
373             return [('id','=','0')]
374         return [('id','in', [x[0] for x in res])]
375
376     def _payment_earliest_date_search(self, cr, uid, obj, name, args, context=None):
377         if not args:
378             return []
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 '\
389                     'AND '+query+' '\
390                     'AND partner_id IS NOT NULL '\
391                     'GROUP BY partner_id HAVING '+ having_where_clause,
392                      [company_id] + having_values)
393         res = cr.fetchall()
394         if not res:
395             return [('id','=','0')]
396         return [('id','in', [x[0] for x in res])]
397
398     def _payment_due_search(self, cr, uid, obj, name, args, context=None):
399         if not args:
400             return []
401         query, query_args = self._get_followup_overdue_query(cr, uid, args, overdue_only=False, context=context)
402         cr.execute(query, query_args)
403         res = cr.fetchall()
404         if not res:
405             return [('id','=','0')]
406         return [('id','in', [x[0] for x in res])]
407
408     _inherit = "res.partner"
409     _columns = {
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", 
426             store=False, 
427             multi="latest"), 
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", 
431             store=False, 
432             multi="latest"),
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,
442                                                     type='date',
443                                                     string = "Worst Due Date",
444                                                     multi="followup",
445                                                     fnct_search=_payment_earliest_date_search),
446         }
447
448 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: