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 ##############################################################################
25 from openerp import tools
26 from openerp.osv import fields, osv
27 from openerp.tools.translate import _
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'
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),
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.
53 create or replace view account_followup_stat_by_partner as (
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
65 LEFT JOIN account_account a ON (l.account_id = a.id)
68 a.type = 'receivable' AND
69 l.reconcile_id is NULL AND
70 l.partner_id IS NOT NULL AND
73 l.partner_id, l.company_id
74 )""") #Blocked is to take into account litigation
75 account_followup_stat_by_partner()
78 class account_followup_sending_results(osv.osv_memory):
80 def do_report(self, cr, uid, ids, context=None):
83 return context.get('report_data')
85 def do_done(self, cr, uid, ids, context=None):
88 def _get_description(self, cr, uid, context=None):
91 return context.get('description')
93 def _get_need_printing(self, cr, uid, context=None):
96 return context.get('needprinting')
98 _name = 'account_followup.sending.results'
99 _description = 'Results from the sending of the different letters and emails'
101 'description': fields.text("Description", readonly=True),
102 'needprinting': fields.boolean("Needs Printing")
105 'needprinting':_get_need_printing,
106 'description':_get_description,
109 account_followup_sending_results()
112 class account_followup_print(osv.osv_memory):
113 _name = 'account_followup.print'
114 _description = 'Print Follow-up & Send Mail to Customers'
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.'),
133 def _get_followup(self, cr, uid, context=None):
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
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 = []
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():
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)
163 if partner.max_followup_id.send_letter:
164 partner_ids_to_print.append(partner.id)
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")
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:")
176 resulttext += "<p align=\"center\">"
178 resulttext = resulttext + "<li>" + item + ":" + str(manuals[item]) + "\n </li>"
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 {}
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})
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)
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)
209 def do_process(self, cr, uid, ids, context=None):
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]
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']
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})
237 'name': _('Send Letters and Emails: Actions Summary'),
240 'view_mode': 'tree,form',
241 'res_model': 'account_followup.sending.results',
242 'views': [(resource_id,'form')],
243 'type': 'ir.actions.act_window',
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
251 'date': lambda *a: time.strftime('%Y-%m-%d'),
252 'followup_id': _get_followup,
254 'email_subject': _('Invoices Reminder'),
255 'partner_lang': True,
258 def _get_partners_followp(self, cr, uid, ids, context=None):
260 data = self.browse(cr, uid, ids, context=context)[0]
261 company_id = data.company_id.id
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) "\
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()
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
283 current_date = datetime.date(*time.strptime(date,
287 "FROM account_followup_followup_line "\
288 "WHERE followup_id=%s "\
289 "ORDER BY delay", (fup_id,))
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'])
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:
304 if followup_line_id not in fups:
306 stat_line_id = partner_id * 10000 + company_id
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}
318 account_followup_print()
320 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: