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