[MERGE] merged trunk.
[odoo/odoo.git] / addons / account_followup / wizard / account_followup_print.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 import datetime
23 import time
24
25 from openerp import tools
26 from openerp.osv import fields, osv
27 from openerp.tools.translate import _
28
29 class account_followup_stat_by_partner(osv.osv):
30     _name = "account_followup.stat.by.partner"
31     _description = "Follow-up Statistics by Partner"
32     _rec_name = 'partner_id'
33     _auto = False
34     _columns = {
35         'partner_id': fields.many2one('res.partner', 'Partner', readonly=True),
36         'date_move':fields.date('First move', readonly=True),
37         'date_move_last':fields.date('Last move', readonly=True),
38         'date_followup':fields.date('Latest follow-up', readonly=True),
39         'max_followup_id': fields.many2one('account_followup.followup.line',
40                                     'Max Follow Up Level', readonly=True, ondelete="cascade"),
41         'balance':fields.float('Balance', readonly=True),
42         'company_id': fields.many2one('res.company', 'Company', readonly=True),
43     }
44
45     def init(self, cr):
46         tools.drop_view_if_exists(cr, 'account_followup_stat_by_partner')
47         # Here we don't have other choice but to create a virtual ID based on the concatenation
48         # of the partner_id and the company_id, because if a partner is shared between 2 companies,
49         # we want to see 2 lines for him in this table. It means that both company should be able
50         # to send him follow-ups separately . An assumption that the number of companies will not
51         # reach 10 000 records is made, what should be enough for a time.
52         cr.execute("""
53             create or replace view account_followup_stat_by_partner as (
54                 SELECT
55                     l.partner_id * 10000 + l.company_id as id,
56                     l.partner_id AS partner_id,
57                     min(l.date) AS date_move,
58                     max(l.date) AS date_move_last,
59                     max(l.followup_date) AS date_followup,
60                     max(l.followup_line_id) AS max_followup_id,
61                     sum(l.debit - l.credit) AS balance,
62                     l.company_id as company_id
63                 FROM
64                     account_move_line l
65                     LEFT JOIN account_account a ON (l.account_id = a.id)
66                 WHERE
67                     a.active AND
68                     a.type = 'receivable' AND
69                     l.reconcile_id is NULL AND
70                     l.partner_id IS NOT NULL AND
71                     (l.blocked = False)
72                     GROUP BY
73                     l.partner_id, l.company_id
74             )""") #Blocked is to take into account litigation
75 account_followup_stat_by_partner()
76
77
78 class account_followup_sending_results(osv.osv_memory):
79     
80     def do_report(self, cr, uid, ids, context=None):
81         if context is None:
82             context = {}
83         return context.get('report_data')
84
85     def do_done(self, cr, uid, ids, context=None):
86         return {}
87
88     def _get_description(self, cr, uid, context=None):
89         if context is None:
90             context = {}
91         return context.get('description')
92
93     def _get_need_printing(self, cr, uid, context=None):
94         if context is None:
95             context = {}
96         return context.get('needprinting')
97
98     _name = 'account_followup.sending.results'
99     _description = 'Results from the sending of the different letters and emails'
100     _columns  = {
101         'description': fields.text("Description", readonly=True),
102         'needprinting': fields.boolean("Needs Printing")
103     }
104     _defaults = {
105         'needprinting':_get_need_printing,
106         'description':_get_description,
107     }
108  
109 account_followup_sending_results()
110
111
112 class account_followup_print(osv.osv_memory):
113     _name = 'account_followup.print'
114     _description = 'Print Follow-up & Send Mail to Customers'
115     _columns = {
116         'date': fields.date('Follow-up Sending Date', required=True, 
117                             help="This field allow you to select a forecast date to plan your follow-ups"),
118         'followup_id': fields.many2one('account_followup.followup', 'Follow-Up', required=True, readonly = True),
119         'partner_ids': fields.many2many('account_followup.stat.by.partner', 'partner_stat_rel', 
120                                         'osv_memory_id', 'partner_id', 'Partners', required=True),
121         'company_id':fields.related('followup_id', 'company_id', type='many2one',
122                                     relation='res.company', store=True, readonly=True),
123         'email_conf': fields.boolean('Send Email Confirmation'),
124         'email_subject': fields.char('Email Subject', size=64),
125         'partner_lang': fields.boolean('Send Email in Partner Language',
126                                     help='Do not change message text, if you want to send email in partner language, or configure from company'),
127         'email_body': fields.text('Email Body'),
128         'summary': fields.text('Summary', readonly=True),
129         'test_print': fields.boolean('Test Print', 
130                                      help='Check if you want to print follow-ups without changing follow-ups level.'),
131     }
132
133     def _get_followup(self, cr, uid, context=None):
134         if context is None:
135             context = {}
136         if context.get('active_model', 'ir.ui.menu') == 'account_followup.followup':
137             return context.get('active_id', False)
138         company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
139         followp_id = self.pool.get('account_followup.followup').search(cr, uid, [('company_id', '=', company_id)], context=context)
140         return followp_id and followp_id[0] or False
141
142     def process_partners(self, cr, uid, partner_ids, data, context=None):
143         partner_obj = self.pool.get('res.partner')
144         partner_ids_to_print = []
145         nbmanuals = 0
146         manuals = {}
147         nbmails = 0
148         nbunknownmails = 0
149         nbprints = 0
150         resulttext = " "
151         for partner in self.pool.get('account_followup.stat.by.partner').browse(cr, uid, partner_ids, context=context):
152             if partner.max_followup_id.manual_action:
153                 partner_obj.do_partner_manual_action(cr, uid, [partner.partner_id.id], context=context)
154                 nbmanuals = nbmanuals + 1
155                 key = partner.partner_id.payment_responsible_id.name or _("Anybody")
156                 if not key in manuals.keys():
157                     manuals[key]= 1
158                 else:
159                     manuals[key] = manuals[key] + 1
160             if partner.max_followup_id.send_email:
161                 nbunknownmails += partner_obj.do_partner_mail(cr, uid, [partner.partner_id.id], context=context)
162                 nbmails += 1
163             if partner.max_followup_id.send_letter:
164                 partner_ids_to_print.append(partner.id)
165                 nbprints += 1
166                 message = _("Follow-up letter of ") + "<I> " + partner.partner_id.latest_followup_level_id_without_lit.name + "</I>" + _(" will be sent")
167                 partner_obj.message_post(cr, uid, [partner.partner_id.id], body=message, context=context)
168         if nbunknownmails == 0:
169             resulttext += str(nbmails) + _(" email(s) sent")
170         else:
171             resulttext += str(nbmails) + _(" email(s) should have been sent, but ") + str(nbunknownmails) + _(" had unknown email address(es)") + "\n <BR/> "
172         resulttext += "<BR/>" + str(nbprints) + _(" letter(s) in report") + " \n <BR/>" + str(nbmanuals) + _(" manual action(s) assigned:")
173         needprinting = False
174         if nbprints > 0:
175             needprinting = True
176         resulttext += "<p align=\"center\">"
177         for item in manuals:
178             resulttext = resulttext + "<li>" + item + ":" + str(manuals[item]) +  "\n </li>"
179         resulttext += "</p>"
180         result = {}
181         action = partner_obj.do_partner_print(cr, uid, partner_ids_to_print, data, context=context)
182         result['needprinting'] = needprinting
183         result['resulttext'] = resulttext
184         result['action'] = action or {}
185         return result
186
187     def do_update_followup_level(self, cr, uid, to_update, partner_list, date, context=None):
188         #update the follow-up level on account.move.line
189         for id in to_update.keys():
190             if to_update[id]['partner_id'] in partner_list:
191                 self.pool.get('account.move.line').write(cr, uid, [int(id)], {'followup_line_id': to_update[id]['level'], 
192                                                                               'followup_date': date})
193
194     def clear_manual_actions(self, cr, uid, partner_list, context=None):
195         # Partnerlist is list to exclude
196         # Will clear the actions of partners that have no due payments anymore
197         partner_list_ids = [partner.partner_id.id for partner in self.pool.get('account_followup.stat.by.partner').browse(cr, uid, partner_list, context=context)]
198         ids = self.pool.get('res.partner').search(cr, uid, ['&', ('id', 'not in', partner_list_ids), '|', 
199                                                              ('payment_responsible_id', '!=', False), 
200                                                              ('payment_next_action_date', '!=', False)], context=context)
201
202         partners_to_clear = []
203         for part in self.pool.get('res.partner').browse(cr, uid, ids, context=context): 
204             if not part.unreconciled_aml_ids: 
205                 partners_to_clear.append(part.id)
206         self.pool.get('res.partner').action_done(cr, uid, partners_to_clear, context=context)
207         return len(ids)
208
209     def do_process(self, cr, uid, ids, context=None):
210         if context is None:
211             context = {}
212
213         #Get partners
214         tmp = self._get_partners_followp(cr, uid, ids, context=context)
215         partner_list = tmp['partner_ids']
216         to_update = tmp['to_update']
217         date = self.browse(cr, uid, ids, context=context)[0].date
218         data = self.read(cr, uid, ids, [], context=context)[0]
219         data['followup_id'] = data['followup_id'][0]
220
221         #Update partners
222         self.do_update_followup_level(cr, uid, to_update, partner_list, date, context=context)
223         #process the partners (send mails...)
224         restot = self.process_partners(cr, uid, partner_list, data, context=context)
225         #clear the manual actions if nothing is due anymore
226         nbactionscleared = self.clear_manual_actions(cr, uid, partner_list, context=context)
227         if nbactionscleared > 0:
228             restot['resulttext'] = restot['resulttext'] + "<li>" +  _("%s partners have no credits and as such the action is cleared") %(str(nbactionscleared)) + "</li>" 
229         res = restot['action']
230
231         #return the next action
232         mod_obj = self.pool.get('ir.model.data')
233         model_data_ids = mod_obj.search(cr, uid, [('model','=','ir.ui.view'),('name','=','view_account_followup_sending_results')], context=context)
234         resource_id = mod_obj.read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id']
235         context.update({'description': restot['resulttext'], 'needprinting': restot['needprinting'], 'report_data': res})
236         return {
237             'name': _('Send Letters and Emails: Actions Summary'),
238             'view_type': 'form',
239             'context': context,
240             'view_mode': 'tree,form',
241             'res_model': 'account_followup.sending.results',
242             'views': [(resource_id,'form')],
243             'type': 'ir.actions.act_window',
244             'target': 'new',
245             }
246
247     def _get_msg(self, cr, uid, context=None):
248         return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.follow_up_msg
249
250     _defaults = {
251         'date': lambda *a: time.strftime('%Y-%m-%d'),
252         'followup_id': _get_followup,
253         'email_body': "",
254         'email_subject': _('Invoices Reminder'),
255         'partner_lang': True,
256     }
257
258     def _get_partners_followp(self, cr, uid, ids, context=None):
259         data = {}
260         data = self.browse(cr, uid, ids, context=context)[0]
261         company_id = data.company_id.id
262
263         cr.execute(
264             "SELECT l.partner_id, l.followup_line_id,l.date_maturity, l.date, l.id "\
265             "FROM account_move_line AS l "\
266                 "LEFT JOIN account_account AS a "\
267                 "ON (l.account_id=a.id) "\
268             "WHERE (l.reconcile_id IS NULL) "\
269                 "AND (a.type='receivable') "\
270                 "AND (l.state<>'draft') "\
271                 "AND (l.partner_id is NOT NULL) "\
272                 "AND (a.active) "\
273                 "AND (l.debit > 0) "\
274                 "AND (l.company_id = %s) " \
275                 "AND (l.blocked = False)" \
276             "ORDER BY l.date", (company_id,))  #l.blocked added to take litigation into account and it is not necessary to change follow-up level of account move lines without debit
277         move_lines = cr.fetchall()
278         old = None
279         fups = {}
280         fup_id = 'followup_id' in context and context['followup_id'] or data.followup_id.id
281         date = 'date' in context and context['date'] or data.date
282
283         current_date = datetime.date(*time.strptime(date,
284             '%Y-%m-%d')[:3])
285         cr.execute(
286             "SELECT * "\
287             "FROM account_followup_followup_line "\
288             "WHERE followup_id=%s "\
289             "ORDER BY delay", (fup_id,))
290         
291         #Create dictionary of tuples where first element is the date to compare with the due date and second element is the id of the next level
292         for result in cr.dictfetchall():
293             delay = datetime.timedelta(days=result['delay'])
294             fups[old] = (current_date - delay, result['id'])
295             old = result['id']
296
297         partner_list = []
298         to_update = {}
299         
300         #Fill dictionary of accountmovelines to_update with the partners that need to be updated
301         for partner_id, followup_line_id, date_maturity,date, id in move_lines:
302             if not partner_id:
303                 continue
304             if followup_line_id not in fups:
305                 continue
306             stat_line_id = partner_id * 10000 + company_id
307             if date_maturity:
308                 if date_maturity <= fups[followup_line_id][0].strftime('%Y-%m-%d'):
309                     if stat_line_id not in partner_list:
310                         partner_list.append(stat_line_id)
311                     to_update[str(id)]= {'level': fups[followup_line_id][1], 'partner_id': stat_line_id}
312             elif date and date <= fups[followup_line_id][0].strftime('%Y-%m-%d'):
313                 if stat_line_id not in partner_list:
314                     partner_list.append(stat_line_id)
315                 to_update[str(id)]= {'level': fups[followup_line_id][1], 'partner_id': stat_line_id}
316         return {'partner_ids': partner_list, 'to_update': to_update}
317
318 account_followup_print()
319
320 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: