[WIP] Adding search filters and changing amount to overdue amount
[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 osv import fields, osv
23 from datetime import date
24 import time
25
26 class followup(osv.osv):
27     _name = 'account_followup.followup'
28     _description = 'Account Follow-up'
29     _columns = {
30         'name': fields.char('Name', size=64, required=True),
31         'description': fields.text('Description'),
32         'followup_line': fields.one2many('account_followup.followup.line', 'followup_id', 'Follow-up'),
33         'company_id': fields.many2one('res.company', 'Company', required=True),        
34     }
35     _defaults = {
36         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'account_followup.followup', context=c),
37     }
38     
39 followup()
40
41 class followup_line(osv.osv):
42     _name = 'account_followup.followup.line'
43     _description = 'Follow-up Criteria'
44     _columns = {
45         'name': fields.char('Name', size=64, required=True),
46         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of follow-up lines."),
47         'delay': fields.integer('Days of delay'),
48         'start': fields.selection([('days','Net Days'),('end_of_month','End of Month')], 'Type of Term', size=64, required=True),
49         'followup_id': fields.many2one('account_followup.followup', 'Follow Ups', required=True, ondelete="cascade"),
50         'description': fields.text('Printed Message', translate=True),
51         'send_email':fields.boolean('Send email', help="When processing, it will send an email"),
52         'send_letter':fields.boolean('Print email'),
53         'phonecall':fields.boolean('Phone call'), 
54         'email_template_id':fields.many2one('email.template', 'Email template', required = False, ondelete='set null')
55     }
56     _defaults = {
57         'start': 'days',
58         'send_email': True,
59         'send_letter': False,
60     }
61     def _check_description(self, cr, uid, ids, context=None):
62         for line in self.browse(cr, uid, ids, context=context):
63             if line.description:
64                 try:
65                     line.description % {'partner_name': '', 'date':'', 'user_signature': '', 'company_name': ''}
66                 except:
67                     return False
68         return True
69
70     _constraints = [
71         (_check_description, 'Your description is invalid, use the right legend or %% if you want to use the percent character.', ['description']),
72     ]
73
74 followup_line()
75
76 class account_move_line(osv.osv):
77     _inherit = 'account.move.line'
78     _columns = {
79         'followup_line_id': fields.many2one('account_followup.followup.line', 'Follow-up Level'),
80         'followup_date': fields.date('Latest Follow-up', select=True),
81         'payment_commitment':fields.text('Commitment'),
82         'payment_date':fields.date('Date'),
83         #'payment_note':fields.text('Payment note'),
84         'payment_next_action':fields.text('New action'),
85     }
86
87 account_move_line()
88
89 class res_company(osv.osv):
90     _inherit = "res.company"
91     _columns = {
92         'follow_up_msg': fields.text('Follow-up Message', translate=True),
93         
94     }
95
96     _defaults = {
97         'follow_up_msg': '''
98 Date: %(date)s
99
100 Dear %(partner_name)s,
101
102 Please find in attachment a reminder of all your unpaid invoices, for a total amount due of:
103
104 %(followup_amount).2f %(company_currency)s
105
106 Thanks,
107 --
108 %(user_signature)s
109 %(company_name)s
110         '''
111     }
112
113 res_company()
114
115
116
117 class res_partner(osv.osv):
118
119
120     def _get_latest_followup_date(self, cr, uid, ids, name, arg, context=None):
121         res = {}
122         for partner in self.browse(cr, uid, ids): 
123
124
125             accountmovelines = partner.accountmoveline_ids
126             #max(x.followup_date for x in accountmovelines)
127             #latest_date = lambda a: date(2011, 1, 1)
128             #for accountmoveline in accountmovelines:
129             #    if (accountmoveline.followup_date != False) and (latest_date < accountmoveline.followup_date):
130             #        latest_date = accountmoveline.followup_date
131             #if accountmovelines:
132             amls2 = filter(lambda a: (a.state != 'draft') and (a.debit > 0), accountmovelines)
133             res[partner.id] = max(x.followup_date for x in amls2) if len(amls2) else False
134             #else:
135             #    res[partner.id] = False
136
137             #res[partner.id] = max(x.followup_date for x in accountmovelines) if len(accountmovelines) else False
138         return res
139
140    
141
142     
143     def _get_latest_followup_level_id(self, cr, uid, ids, name, arg, context=None):
144         res = {}
145         for partner in self.browse(cr, uid, ids):
146             amls = partner.accountmoveline_ids
147             level_id = 0
148             level_days = False  #TO BE IMPROVED with boolean checking first time or by using MAX
149             latest_level = False            
150             res[partner.id] = False
151             for accountmoveline in amls:
152                 if (accountmoveline.followup_line_id != False) and (not level_days or level_days < accountmoveline.followup_line_id.delay): 
153                 # and (accountmoveline.debit > 0):   (accountmoveline.state != "draft") and
154                 #and  (accountmoveline.reconcile_id == None)
155                     level_days = accountmoveline.followup_line_id.delay
156                     latest_level = accountmoveline.followup_line_id.id
157                     res[partner.id] = latest_level
158             #res[partner.id] = max(x.followup_line_id.delay for x in amls) if len(amls) else False
159         return res
160
161     def get_latest_followup_level(self):
162         amls = self.accountmoveline_ids
163
164     def _get_next_followup_level_id_optimized(self, cr, uid, ids, name, arg, context=None):
165         res = {}
166         for partner in self.browse(cr, uid, ids):            
167             latest_id = partner.latest_followup_level_id
168             if latest_id:
169                 latest = latest_id
170             else:
171                 latest = False
172
173             delay = False
174             newlevel = False
175             if latest: #if latest exists                
176                 newlevel = latest.id
177                 old_delay = latest.delay
178             else:
179                 old_delay = False
180             fl_ar = self.pool.get('account_followup.followup.line').search(cr, uid, [('followup_id.company_id.id','=', partner.company_id.id)])
181             
182             for fl_obj in self.pool.get('account_followup.followup.line').browse(cr, uid, fl_ar):
183                 if not old_delay: 
184                     if not delay or fl_obj.delay < delay: 
185                         delay = fl_obj.delay
186                         newlevel = fl_obj.id
187                 else:
188                     if (not delay and (fl_obj.delay > old_delay)) or ((fl_obj.delay < delay) and (fl_obj.delay > old_delay)):
189                         delay = fl_obj.delay
190                         newlevel = fl_obj.id
191             res[partner.id] = newlevel
192             #Now search one level higher
193         return res
194
195
196     def _get_next_followup_level_id(self, cr, uid, ids, name, arg, context=None):
197         res = {}
198         for partner in self.browse(cr, uid, ids):            
199             latest_id = self._get_latest_followup_level_id(cr, uid, [partner.id], name, arg, context)[partner.id]
200             if latest_id:
201                 latest = self.pool.get('account_followup.followup.line').browse(cr, uid, [latest_id], context)[0]
202             else:
203                 latest = False
204
205             delay = False
206             newlevel = False
207             if latest: #if latest exists                
208                 newlevel = latest.id
209                 old_delay = latest.delay
210             else:
211                 old_delay = False
212             fl_ar = self.pool.get('account_followup.followup.line').search(cr, uid, [('followup_id.company_id.id','=', partner.company_id.id)])
213             
214             for fl_obj in self.pool.get('account_followup.followup.line').browse(cr, uid, fl_ar):
215                 if not old_delay: 
216                     if not delay or fl_obj.delay < delay: 
217                         delay = fl_obj.delay
218                         newlevel = fl_obj.id
219                 else:
220                     if (not delay and (fl_obj.delay > old_delay)) or ((fl_obj.delay < delay) and (fl_obj.delay > old_delay)):
221                         delay = fl_obj.delay
222                         newlevel = fl_obj.id
223             res[partner.id] = newlevel
224             #Now search one level higher
225         return res
226
227
228
229     def _get_amount(self, cr, uid, ids, name, arg, context=None):
230         ''' 
231          Get the total outstanding amount in the account move lines
232         '''
233         res={}
234         for partner in self.browse(cr, uid, ids, context):
235             res[partner.id] = 0.0
236             for aml in partner.accountmoveline_ids:
237                 if ((not aml.date_maturity) and (aml.date > fields.date.context_today(cr, uid, context))) or (aml.date_maturity > fields.date.context_today(cr, uid, context)):
238                      res[partner.id] = res[partner.id] + aml.debit
239         return res
240
241
242     def do_partner_phonecall(self, cr, uid, partner_ids, context=None): 
243         #partners = self.browse(cr, uid, partner_ids, context)
244         #print partner_ids
245         #print "Testing: " ,  fields.date.context_today(cr, uid, context)
246         self.write(cr, uid, partner_ids, {'payment_next_action_date': fields.date.context_today(cr, uid, context),}, context)
247         
248
249
250
251     def do_partner_print(self, cr, uid, partner_ids, data, context=None):        
252         #data.update({'date': context['date']})
253         if not partner_ids: 
254             return {}
255         data['partner_ids'] = partner_ids
256         datas = {
257              'ids': [],
258              'model': 'account_followup.followup',
259              'form': data
260         }
261         
262         return {
263             'type': 'ir.actions.report.xml', 
264             'report_name': 'account_followup.followup.print', 
265             'datas': datas, 
266             }
267
268     def do_partner_mail(self, cr, uid, partner_ids, context=None):
269         #get the mail template to use
270         mail_template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_followup', 'email_template_account_followup')
271         mtp = self.pool.get('email.template')
272         #mtp.subject = "Invoices overdue"
273         #user_obj = self.pool.get('res.users')
274         #mtp.email_from = user_obj.browse(cr, uid, uid, context=context)
275         for partner in self.browse(cr, uid, partner_ids, context):
276             
277             #Get max level of ids
278             if partner.latest_followup_level_id.email_template_id != False:                
279                 #print "From latest followup level", partner.latest_followup_level_id.email_template_id.id
280                 mtp.send_mail(cr, uid, partner.latest_followup_level_id.email_template_id.id, partner.id, context=context)
281             else:
282                 #print "From mail template", mail_template_id.id
283                 mtp.send_mail(cr, uid, mail_template_id.id, partner.id, context=context)
284
285         #complete the mail body with partner information
286         #(to be discussed with fp) attach the report to the mail or include the move lines in the mail body
287         #send the mail (need to check the function name)
288
289         
290 #        mod_obj = self.pool.get('ir.model.data')
291 #        move_obj = self.pool.get('account.move.line')
292 #        user_obj = self.pool.get('res.users')
293 #
294 #        if context is None:
295 #            context = {}
296 #        data = self.browse(cr, uid, ids, context=context)[0]
297 #        stat_by_partner_line_ids = [partner_id.id for partner_id in data.partner_ids]
298 #        partners = [stat_by_partner_line / 10000 for stat_by_partner_line in stat_by_partner_line_ids]
299 #        model_data_ids = mod_obj.search(cr, uid, [('model','=','ir.ui.view'),('name','=','view_account_followup_print_all_msg')], context=context)
300 #        resource_id = mod_obj.read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id']
301 #        if True: #should not depend on this
302 #            msg_sent = ''
303 #            msg_unsent = ''
304 #            data_user = user_obj.browse(cr, uid, uid, context=context)
305 #            for partner in self.pool.get('res.partner').browse(cr, uid, partners, context=context):
306 #                if partner.next_followup_level_id.send_email:
307 #                    ids_lines = move_obj.search(cr,uid,[('partner_id','=',partner.id),('reconcile_id','=',False),('account_id.type','in',['receivable']),('company_id','=',context.get('company_id', False))])
308 #                    data_lines = move_obj.browse(cr, uid, ids_lines, context=context)
309 #                    total_amt = 0.0
310 #                    for line in data_lines:
311 #                        total_amt += line.debit - line.credit
312 #                    dest = False
313 #                    if partner:
314 #                        dest = [partner.email]
315 #                    if not data.partner_lang:
316 #                        body = data.email_body 
317 #                    else:
318 #                        cxt = context.copy()
319 #                        cxt['lang'] = partner.lang
320 #                        body = user_obj.browse(cr, uid, uid, context=cxt).company_id.follow_up_msg
321 #                    move_line = ''
322 #                    subtotal_due = 0.0
323 #                    subtotal_paid = 0.0
324 #                    subtotal_maturity = 0.0
325 #                    balance = 0.0
326 #                    l = '--------------------------------------------------------------------------------------------------------------------------'
327 #                    head = l+ '\n' + 'Date'.rjust(10) + '\t' + 'Description'.rjust(10) + '\t' + 'Ref'.rjust(10) + '\t' + 'Due date'.rjust(10) + '\t' + 'Due'.rjust(10) + '\t' + 'Paid'.rjust(10) + '\t' + 'Maturity'.rjust(10) + '\t' + 'Litigation'.rjust(10) + '\n' + l
328 #                    for i in data_lines:
329 #                        maturity = 0.00
330 #                        if i.date_maturity < time.strftime('%Y-%m-%d') and (i.debit - i.credit):
331 #                            maturity = i.debit - i.credit
332 #                        subtotal_due = subtotal_due + i.debit
333 #                        subtotal_paid = subtotal_paid + i.credit
334 #                        subtotal_maturity = subtotal_maturity + int(maturity)
335 #                        balance = balance + (i.debit - i.credit)
336 #                        move_line = move_line + (i.date).rjust(10) + '\t'+ (i.name).rjust(10) + '\t'+ (i.ref or '').rjust(10) + '\t' + (i.date_maturity or '').rjust(10) + '\t' + str(i.debit).rjust(10)  + '\t' + str(i.credit).rjust(10)  + '\t' + str(maturity).rjust(10) + '\t' + str(i.blocked).rjust(10) + '\n'
337 #                    move_line = move_line + l + '\n'+ '\t\t\t' + 'Sub total'.rjust(35) + '\t' + (str(subtotal_due) or '').rjust(10) + '\t' + (str(subtotal_paid) or '').rjust(10) + '\t' + (str(subtotal_maturity) or '').rjust(10)+ '\n'
338 #                    move_line = move_line + '\t\t\t' + 'Balance'.rjust(33) + '\t' + str(balance).rjust(10) + '\n' + l
339 #                    val = {
340 #                        'partner_name':partner.name,
341 #                        'followup_amount':total_amt,
342 #                        'user_signature':data_user.name,
343 #                        'company_name':data_user.company_id.name,
344 #                        'company_currency':data_user.company_id.currency_id.name,
345 #                        'line':move_line,
346 #                        'heading': head,
347 #                        'date':time.strftime('%Y-%m-%d'),
348 #                    }
349 #                    body = body%val
350 #                    sub = tools.ustr(data.email_subject)
351 #                    msg = ''
352 #                    if dest:
353 #                        try:
354 #                            vals = {'state': 'outgoing',
355 #                                    'subject': sub,
356 #                                    'body_html': '<pre>%s</pre>' % body,
357 #                                    'email_to': dest,
358 #                                    'email_from': data_user.email or tools.config.options['email_from']}
359 #                            self.pool.get('mail.mail').create(cr, uid, vals, context=context)
360 #                            msg_sent += partner.name + '\n'
361 #                        except Exception, e:
362 #                            raise osv.except_osv('Error !', e )
363 #                    else:
364 #                        msg += partner.name + '\n'
365 #                        msg_unsent += msg
366 #            if not msg_unsent:
367 #                summary = _("All Emails have been successfully sent to Partners:.\n\n%s") % msg_sent
368 #            else:
369 #                msg_unsent = _("Email not sent to following Partners, Email not available !\n\n%s") % msg_unsent
370 #                msg_sent = msg_sent and _("\n\nEmail sent to following Partners successfully. !\n\n%s") % msg_sent
371 #                line = '=========================================================================='
372 #                summary = msg_unsent + line + msg_sent
373 #            context.update({'summary': summary})
374 #        else:
375 #            context.update({'summary': '\n\n\nEmail has not been sent to any partner. If you want to send it, please tick send email confirmation on wizard.'})
376 #
377 #        return {
378 #            'name': _('Followup Summary'),
379 #            'view_type': 'form',
380 #            'context': context,
381 #            'view_mode': 'tree,form',
382 #            'res_model': 'account.followup.print.all',
383 #            'views': [(resource_id,'form')],
384 #            'type': 'ir.actions.act_window',
385 #            'target': 'new',
386 #            'nodestroy': True
387 #            }
388
389
390     def action_done(self, cr, uid, ids, context=None):
391         
392         self.write(cr, uid, ids,  {'payment_next_action_date': False, 'payment_next_action':''}, context)
393         
394             
395     
396
397     _inherit = "res.partner"
398     _columns = {
399         'payment_responsible_id':fields.many2one('res.users', ondelete='set null', string='Responsible', help="Responsible"), 
400         #'payment_followup_level_id':fields.many2one('account_followup.followup.line', 'Followup line'),
401         'payment_note':fields.text('Payment note', help="Payment note"), 
402         'payment_next_action':fields.text('Next action', help="Write here the comments of your customer"), #Just a note
403         'payment_next_action_date':fields.date('Next action date', help="Next date to take action"), # next action date
404         'accountmoveline_ids':fields.one2many('account.move.line', 'partner_id', domain=['&', ('debit', '>', 0.0), '&', ('reconcile_id', '=', False), '&', 
405             ('account_id.active','=', True), '&', ('account_id.type', '=', 'receivable'), ('state', '!=', 'draft')]), 
406         'latest_followup_date':fields.function(_get_latest_followup_date, method=True, type='date', string="latest followup date", store=True), 
407         'latest_followup_level_id':fields.function(_get_latest_followup_level_id, method=True, 
408             type='many2one', relation='account_followup.followup.line', string="Latest Followup Level", store=True), 
409         'next_followup_level_id':fields.function(_get_next_followup_level_id_optimized, method=True, type='many2one', relation='account_followup.followup.line', string="Next Level", help="Next level that will be printed"),
410         'payment_amount_outstanding':fields.function(_get_amount, method=True, type='float', string="Amount Overdue", store=True),
411     }
412
413 res_partner()
414
415 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: