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 view account_followup_stat_by_partner as (
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
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
72 l.partner_id, l.company_id
76 class account_followup_sending_results(osv.osv_memory):
78 def do_report(self, cr, uid, ids, context=None):
81 return context.get('report_data')
83 def do_done(self, cr, uid, ids, context=None):
86 def _get_description(self, cr, uid, context=None):
89 return context.get('description')
91 def _get_need_printing(self, cr, uid, context=None):
94 return context.get('needprinting')
96 _name = 'account_followup.sending.results'
97 _description = 'Results from the sending of the different letters and emails'
99 'description': fields.text("Description", readonly=True),
100 'needprinting': fields.boolean("Needs Printing")
103 'needprinting':_get_need_printing,
104 'description':_get_description,
109 class account_followup_print(osv.osv_memory):
110 _name = 'account_followup.print'
111 _description = 'Print Follow-up & Send Mail to Customers'
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.'),
130 def _get_followup(self, cr, uid, context=None):
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
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 = []
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():
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)
160 if partner.max_followup_id.send_letter:
161 partner_ids_to_print.append(partner.id)
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")
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:")
173 resulttext += "<p align=\"center\">"
175 resulttext = resulttext + "<li>" + item + ":" + str(manuals[item]) + "\n </li>"
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 {}
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})
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)
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)
206 def do_process(self, cr, uid, ids, context=None):
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]
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']
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})
234 'name': _('Send Letters and Emails: Actions Summary'),
237 'view_mode': 'tree,form',
238 'res_model': 'account_followup.sending.results',
239 'views': [(resource_id,'form')],
240 'type': 'ir.actions.act_window',
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
248 'date': lambda *a: time.strftime('%Y-%m-%d'),
249 'followup_id': _get_followup,
251 'email_subject': _('Invoices Reminder'),
252 'partner_lang': True,
255 def _get_partners_followp(self, cr, uid, ids, context=None):
257 data = self.browse(cr, uid, ids, context=context)[0]
258 company_id = data.company_id.id
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) "\
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()
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
280 current_date = datetime.date(*time.strptime(date,
284 "FROM account_followup_followup_line "\
285 "WHERE followup_id=%s "\
286 "ORDER BY delay", (fup_id,))
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'])
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:
301 if followup_line_id not in fups:
303 stat_line_id = partner_id * 10000 + company_id
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}
316 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: