diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 8d13ec9dc8f..f89f7828269 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -524,11 +524,11 @@ class account_invoice(osv.osv): return result def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice): - res = {} - if not payment_term_id: - return res + res = {} if not date_invoice: date_invoice = time.strftime('%Y-%m-%d') + if not payment_term_id: + return {'value':{'date_due': date_invoice}} #To make sure the invoice has a due date when no payment term pterm_list = self.pool.get('account.payment.term').compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice) if pterm_list: pterm_list = [line[0] for line in pterm_list] diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py index 7cc9768f00e..698b1bcf00c 100644 --- a/addons/account/account_move_line.py +++ b/addons/account/account_move_line.py @@ -475,7 +475,8 @@ class account_move_line(osv.osv): type='many2one', relation='account.invoice', fnct_search=_invoice_search), 'account_tax_id':fields.many2one('account.tax', 'Tax'), 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'), - 'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True) + 'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', + string='Company', store=True, readonly=True) } def _get_date(self, cr, uid, context=None): diff --git a/addons/account/report/account_print_overdue.rml b/addons/account/report/account_print_overdue.rml index dd573580c6c..9d1898572ba 100644 --- a/addons/account/report/account_print_overdue.rml +++ b/addons/account/report/account_print_overdue.rml @@ -211,7 +211,7 @@ [[ (line['account_id']['type'] == 'receivable' and formatLang(line['credit']) or 0) or (line['account_id']['type'] == 'payable' and formatLang(line['debit'] * -1) or 0) ]] - [[ time.strftime('%Y-%m-%d') > formatLang((line['date_maturity'])) and formatLang(line['debit'] - line['credit'], currency_obj = company.currency_id) ]] + [[ (time.strftime('%Y-%m-%d') > line['date_maturity']) and formatLang(line['debit'] - line['credit'], currency_obj = company.currency_id) ]] [[ line['blocked'] and 'X' or '' ]] diff --git a/addons/account_followup/__openerp__.py b/addons/account_followup/__openerp__.py index 87b04c18219..fc2f17e6a97 100644 --- a/addons/account_followup/__openerp__.py +++ b/addons/account_followup/__openerp__.py @@ -20,7 +20,7 @@ ############################################################################## { - 'name': 'Follow-up Management', + 'name': 'Payment Follow-up Management', 'version': '1.0', 'category': 'Accounting & Finance', 'description': """ @@ -29,19 +29,18 @@ Module to automate letters for unpaid invoices, with multi-level recalls. You can define your multiple levels of recall through the menu: --------------------------------------------------------------- - **Invoicing** / **Configuration** / **Miscellaneous** / **Follow-ups** - + Configuration / Follow-Up Levels + Once it is defined, you can automatically print recalls every day through simply clicking on the menu: ------------------------------------------------------------------------------------------------------ - **Invoicing** / **Periodical Processing** / **Billing** / **Send follow-ups** + Payment Follow-Up / Send Email and letters -It will generate a PDF with all the letters according to the the different levels -of recall defined. You can define different policies for different companies. You -can also send mail to the customer. +It will generate a PDF / send emails / set manual actions according to the the different levels +of recall defined. You can define different policies for different companies. Note that if you want to check the follow-up level for a given partner/account entry, you can do from in the menu: ------------------------------------------------------------------------------------------------------------------ - **Invoicing** / **Reporting** / **Generic Reporting** / **Partners** / **Follow-ups Sent** + Reporting / Accounting / **Follow-ups Analysis """, 'author': 'OpenERP SA', @@ -51,16 +50,16 @@ Note that if you want to check the follow-up level for a given partner/account e 'data': [ 'security/account_followup_security.xml', 'security/ir.model.access.csv', - 'wizard/account_followup_print_view.xml', 'report/account_followup_report.xml', - 'account_followup_demo.xml', # Defined by default - 'account_followup_view.xml', 'account_followup_data.xml', + 'account_followup_view.xml', + 'account_followup_customers.xml', + 'wizard/account_followup_print_view.xml', ], - 'demo': [], + 'demo': ['account_followup_demo.xml'], 'test': [ 'test/account_followup.yml', - 'test/account_followup_report.yml', + #TODO 'test/account_followup_report.yml', --> Need to wait for second step in order to check report (expects after first) ], 'installable': True, 'auto_install': False, diff --git a/addons/account_followup/account_followup.py b/addons/account_followup/account_followup.py index 60499603295..55903c85f92 100644 --- a/addons/account_followup/account_followup.py +++ b/addons/account_followup/account_followup.py @@ -20,37 +20,66 @@ ############################################################################## from osv import fields, osv +from lxml import etree + +from tools.translate import _ + class followup(osv.osv): _name = 'account_followup.followup' _description = 'Account Follow-up' + _rec_name = 'name' _columns = { - 'name': fields.char('Name', size=64, required=True), - 'description': fields.text('Description'), 'followup_line': fields.one2many('account_followup.followup.line', 'followup_id', 'Follow-up'), 'company_id': fields.many2one('res.company', 'Company', required=True), + 'name': fields.related('company_id', 'name', string = "Name"), } _defaults = { 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'account_followup.followup', context=c), } - -followup() + _sql_constraints = [('company_uniq', 'unique(company_id)', 'Only one follow-up per company is allowed')] + class followup_line(osv.osv): + + def _get_default_template(self, cr, uid, ids, context=None): + dummy, templ = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_followup', 'email_template_account_followup_default') + return templ + _name = 'account_followup.followup.line' _description = 'Follow-up Criteria' _columns = { - 'name': fields.char('Name', size=64, required=True), + 'name': fields.char('Follow-Up Action', size=64, required=True), 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of follow-up lines."), - 'delay': fields.integer('Days of delay'), - 'start': fields.selection([('days','Net Days'),('end_of_month','End of Month')], 'Type of Term', size=64, required=True), + 'delay': fields.integer('Due Days', help="The number of days after the due date of the invoice to wait before sending the reminder. Could be negative if you want to send a polite alert beforehand.", required=True), 'followup_id': fields.many2one('account_followup.followup', 'Follow Ups', required=True, ondelete="cascade"), 'description': fields.text('Printed Message', translate=True), + 'send_email':fields.boolean('Send an Email', help="When processing, it will send an email"), + 'send_letter':fields.boolean('Send a Letter', help="When processing, it will print a letter"), + 'manual_action':fields.boolean('Manual Action', help="When processing, it will set the manual action to be taken for that customer. "), + 'manual_action_note':fields.text('Action To Do', placeholder="e.g. Give a phone call, check with others , ..."), + 'manual_action_responsible_id':fields.many2one('res.users', 'Assign a Responsible', ondelete='set null'), + 'email_template_id':fields.many2one('email.template', 'Email Template', ondelete='set null'), } + _order = 'delay' + _sql_constraints = [('days_uniq', 'unique(followup_id, delay)', 'Days of the follow-up levels must be different')] _defaults = { - 'start': 'days', + 'send_email': True, + 'send_letter': True, + 'manual_action':False, + 'description': """ + Dear %(partner_name)s, +Exception made if there was a mistake of ours, it seems that the following amount stays unpaid. Please, take appropriate measures in order to carry out this payment in the next 8 days. + +Would your payment have been carried out after this mail was sent, please ignore this message. Do not hesitate to contact our accounting department at (+32).10.68.94.39. + +Best Regards, +""", + 'email_template_id': _get_default_template, } + + def _check_description(self, cr, uid, ids, context=None): for line in self.browse(cr, uid, ids, context=context): if line.description: @@ -64,40 +93,192 @@ class followup_line(osv.osv): (_check_description, 'Your description is invalid, use the right legend or %% if you want to use the percent character.', ['description']), ] -followup_line() class account_move_line(osv.osv): + + def _get_result(self, cr, uid, ids, name, arg, context=None): + res = {} + for aml in self.browse(cr, uid, ids, context=context): + res[aml.id] = aml.debit - aml.credit + return res + _inherit = 'account.move.line' _columns = { - 'followup_line_id': fields.many2one('account_followup.followup.line', 'Follow-up Level'), + 'followup_line_id': fields.many2one('account_followup.followup.line', 'Follow-up Level', + ondelete='restrict'), #restrict deletion of the followup line 'followup_date': fields.date('Latest Follow-up', select=True), + 'result':fields.function(_get_result, type='float', method=True, + string="Balance") #'balance' field is not the same } -account_move_line() -class res_company(osv.osv): - _inherit = "res.company" + +class email_template(osv.osv): + _inherit = 'email.template' + + # Adds current_date to the context. That way it can be used to put + # the account move lines in bold that are overdue in the email + def render_template(self, cr, uid, template, model, res_id, context=None): + context['current_date'] = fields.date.context_today(cr, uid, context) + return super(email_template, self).render_template(cr, uid, template, model, res_id, context=context) + + +class res_partner(osv.osv): + + def fields_view_get(self, cr, uid, view_id=None, view_type=None, context=None, toolbar=False, submenu=False): + res = super(res_partner, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, + toolbar=toolbar, submenu=submenu) + if view_type == 'form' and context and 'Followupfirst' in context.keys() and context['Followupfirst'] == True: + doc = etree.XML(res['arch'], parser=None, base_url=None) + first_node = doc.xpath("//page[@string='Payment Follow-up']") + root = first_node[0].getparent() + root.insert(0, first_node[0]) + res['arch'] = etree.tostring(doc) + return res + + def _get_latest(self, cr, uid, ids, names, arg, context=None, company_id=None): + res={} + if company_id == None: + company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id + else: + company = self.pool.get('res.company').browse(cr, uid, company_id, context=context) + for partner in self.browse(cr, uid, ids, context=context): + amls = partner.unreconciled_aml_ids + latest_date = False + latest_level = False + latest_days = False + latest_level_without_lit = False + latest_days_without_lit = False + for aml in amls: + if (aml.company_id == company) and (aml.followup_line_id != False) and (not latest_days or latest_days < aml.followup_line_id.delay): + latest_days = aml.followup_line_id.delay + latest_level = aml.followup_line_id.id + if (aml.company_id == company) and (not latest_date or latest_date < aml.followup_date): + latest_date = aml.followup_date + if (aml.company_id == company) and (aml.blocked == False) and (aml.followup_line_id != False and + (not latest_days_without_lit or latest_days_without_lit < aml.followup_line_id.delay)): + latest_days_without_lit = aml.followup_line_id.delay + latest_level_without_lit = aml.followup_line_id.id + res[partner.id] = {'latest_followup_date': latest_date, + 'latest_followup_level_id': latest_level, + 'latest_followup_level_id_without_lit': latest_level_without_lit} + return res + + def do_partner_manual_action(self, cr, uid, partner_ids, context=None): + #partner_ids -> res.partner + for partner in self.browse(cr, uid, partner_ids, context=context): + #Check action: check if the action was not empty, if not add + action_text= "" + if partner.payment_next_action: + action_text = (partner.payment_next_action or '') + "\n" + (partner.latest_followup_level_id_without_lit.manual_action_note or '') + else: + action_text = partner.latest_followup_level_id_without_lit.manual_action_note or '' + + #Check date: put the minimum date if it existed already + action_date = (partner.payment_next_action_date and min(partner.payment_next_action_date, fields.date.context_today(cr, uid, context)) + ) or fields.date.context_today(cr, uid, context) + + # Check responsible: if partner has not got a responsible already, take from follow-up + responsible_id = False + if partner.payment_responsible_id: + responsible_id = partner.payment_responsible_id.id + else: + p = partner.latest_followup_level_id_without_lit.manual_action_responsible_id + responsible_id = p and p.id or False + self.write(cr, uid, [partner.id], {'payment_next_action_date': action_date, + 'payment_next_action': action_text, + 'payment_responsible_id': responsible_id}) + + def do_partner_print(self, cr, uid, wizard_partner_ids, data, context=None): + #wizard_partner_ids are ids from special view, not from res.partner + if not wizard_partner_ids: + return {} + data['partner_ids'] = wizard_partner_ids + datas = { + 'ids': [], + 'model': 'account_followup.followup', + 'form': data + } + return { + 'type': 'ir.actions.report.xml', + 'report_name': 'account_followup.followup.print', + 'datas': datas, + } + + def do_partner_mail(self, cr, uid, partner_ids, context=None): + #partner_ids are res.partner ids + # If not defined by latest follow-up level, it will be the default template if it can find it + mtp = self.pool.get('email.template') + unknown_mails = 0 + for partner in self.browse(cr, uid, partner_ids, context=context): + if partner.email and partner.email.strip(): + level = partner.latest_followup_level_id_without_lit + if level and level.send_email and level.email_template_id and level.email_template_id.id: + mtp.send_mail(cr, uid, level.email_template_id.id, partner.id, context=context) + else: + mail_template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, + 'account_followup', 'email_template_account_followup_default') + mtp.send_mail(cr, uid, mail_template_id[1], partner.id, context=context) + else: + unknown_mails = unknown_mails + 1 + action_text = _("Email not sent because of email address of partner not filled in") + if partner.payment_next_action_date: + payment_action_date = min(fields.date.context_today(cr, uid, context), partner.payment_next_action_date) + else: + payment_action_date = fields.date.context_today(cr, uid, context) + if partner.payment_next_action: + payment_next_action = partner.payment_next_action + " + " + action_text + else: + payment_next_action = action_text + self.write(cr, uid, [partner.id], {'payment_next_action_date': payment_action_date, + 'payment_next_action': payment_next_action}, context=context) + return unknown_mails + + def action_done(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'payment_next_action_date': False, 'payment_next_action':'', 'payment_responsible_id': False}, context=context) + + def do_button_print(self, cr, uid, ids, context=None): + assert(len(ids) == 1) + self.message_post(cr, uid, [ids[0]], body=_('Printed overdue payments report'), context=context) + datas = { + 'ids': ids, + 'model': 'res.partner', + 'form': self.read(cr, uid, ids[0], context=context) + } + return { + 'type': 'ir.actions.report.xml', + 'report_name': 'account.overdue', + 'datas': datas, + 'nodestroy' : True + } + + + _inherit = "res.partner" _columns = { - 'follow_up_msg': fields.text('Follow-up Message', translate=True), - } - - _defaults = { - 'follow_up_msg': ''' -Date: %(date)s - -Dear %(partner_name)s, - -Please find in attachment a reminder of all your unpaid invoices, for a total amount due of: - -%(followup_amount).2f %(company_currency)s - -Thanks, --- -%(user_signature)s -%(company_name)s - ''' - } - -res_company() + 'payment_responsible_id':fields.many2one('res.users', ondelete='set null', string='Follow-up Responsible', + help="Responsible for making sure the action happens."), + 'payment_note':fields.text('Customer Payment Promise', help="Payment Note"), + 'payment_next_action':fields.text('Next Action', + help="This is the next action to be taken by the user. It will automatically be set when the action fields are empty and the partner gets a follow-up level that requires a manual action. "), + 'payment_next_action_date':fields.date('Next Action Date', + help="This is when further follow-up is needed. The date will have been set to the current date if the action fields are empty and the partner gets a follow-up level that requires a manual action. "), + 'unreconciled_aml_ids':fields.one2many('account.move.line', 'partner_id', domain=['&', ('reconcile_id', '=', False), '&', + ('account_id.active','=', True), '&', ('account_id.type', '=', 'receivable'), ('state', '!=', 'draft')]), + 'latest_followup_date':fields.function(_get_latest, method=True, type='date', string="Latest Follow-up Date", + help="Latest date that the follow-up level of the partner was changed", + store=False, + multi="latest"), + 'latest_followup_level_id':fields.function(_get_latest, method=True, + type='many2one', relation='account_followup.followup.line', string="Latest Follow-up Level", + help="The maximum follow-up level", + store=False, + multi="latest"), + 'latest_followup_level_id_without_lit':fields.function(_get_latest, method=True, + type='many2one', relation='account_followup.followup.line', string="Latest Follow-up Level without litigation", + help="The maximum follow-up level without taking into account the account move lines with litigation", + store=False, + multi="latest"), + 'payment_amount_due':fields.related('credit', type='float', string="Total amount due", readonly=True), + } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/account_followup/account_followup_customers.xml b/addons/account_followup/account_followup_customers.xml new file mode 100644 index 00000000000..223e7ff4b90 --- /dev/null +++ b/addons/account_followup/account_followup_customers.xml @@ -0,0 +1,159 @@ + + + + + + + + res.partner.followup.inherit.tree + res.partner + + + + + + + + + + + + + + + + Search + res.partner + + + + + + + + + + + + + + + + + + + + + Search + res.partner + + + + + + + + + + + + + + + + + + + + + + Manual Follow-Ups + + res.partner + form + tree,form + {} + {'Followupfirst':True, 'search_default_todo': True} + + + + + + res.partner.followup.form.inherit + + res.partner + + + + +
+
+

+ The , the latest payment follow-up + was: +

+ + + + +
+ + + + + + Follow-up of overdue invoices level 1 + ${user.email or ''} + ${user.company_id.name} Payment Follow-up + ${object.email} + ${object.lang} + + + + +

Dear ${object.name},

+

+ We are disappointed to see that despite sending a reminder, that your account is now seriously overdue. +It is essential that immediate payment is made, otherwise we will have to consider placing a stop on your account +which means that we will no longer be able to supply your company with (goods/services). +Please, take appropriate measures in order to carry out this payment in the next 8 days. +If there is a problem with paying invoice that we are not aware of, do not hesitate to contact our accounting +department at (+32).10.68.94.39. so that we can resolve the matter quickly. +Details of due payments is printed below. +

+
+Best Regards, + +
+${user.name} + +
+
+ + +<% + from openerp.addons.account_followup.report import account_followup_print + rml_parse = account_followup_print.report_rappel(object._cr, user.id, "followup_rml_parser") + final_res = rml_parse._lines_get_with_partner(object, user.company_id.id) + followup_table = '' + for currency_dict in final_res: + currency_symbol = currency_dict.get('line', [{'currency_id': user.company_id.currency_id}])[0]['currency_id'].symbol + followup_table += ''' + + + + + + + + + ''' % (currency_symbol) + total = 0 + for aml in currency_dict['line']: + block = aml['blocked'] and 'X' or ' ' + total += aml['balance'] + strbegin = " " + date = aml['date_maturity'] or aml['date'] + if date <= ctx['current_date'] and aml['balance'] > 0: + strbegin = "" + followup_table +="" + strbegin + str(aml['date']) + strend + strbegin + aml['ref'] + strend + strbegin + str(date) + strend + strbegin + str(aml['balance']) + strend + strbegin + block + strend + "" + total = rml_parse.formatLang(total, dp='Account', currency_obj=object.company_id.currency_id) + followup_table += ''' +
Invoice dateReferenceDue dateAmount (%s)Lit.
" + strend = "" + strend = "
+
Amount due: %s
''' % (total) + +%> + +${followup_table} + +
+ + + ]]>
+
+ + + Follow-up of overdue invoices level 2 + ${user.email or ''} + ${user.company_id.name} Payment Follow-up + ${object.email} + ${object.lang} + + + + +

Dear ${object.name},

+

+ Despite several reminders, your account is still not settled. +Unless full payment is made in next 8 days, legal action for the recovery of the debt will be taken without +further notice. +I trust that this action will prove unnecessary and details of due payments is printed below. +In case of any queries concerning this matter, do not hesitate to contact our accounting department at (+32).10.68.94.39. +

+
+Best Regards, + +
+${user.name} + +
+
+ + + +<% + from openerp.addons.account_followup.report import account_followup_print + rml_parse = account_followup_print.report_rappel(object._cr, user.id, "followup_rml_parser") + final_res = rml_parse._lines_get_with_partner(object, user.company_id.id) + followup_table = '' + for currency_dict in final_res: + currency_symbol = currency_dict.get('line', [{'currency_id': user.company_id.currency_id}])[0]['currency_id'].symbol + followup_table += ''' + + + + + + + + + ''' % (currency_symbol) + total = 0 + for aml in currency_dict['line']: + block = aml['blocked'] and 'X' or ' ' + total += aml['balance'] + strbegin = " " + date = aml['date_maturity'] or aml['date'] + if date <= ctx['current_date'] and aml['balance'] > 0: + strbegin = "" + followup_table +="" + strbegin + str(aml['date']) + strend + strbegin + aml['ref'] + strend + strbegin + str(date) + strend + strbegin + str(aml['balance']) + strend + strbegin + block + strend + "" + total = rml_parse.formatLang(total, dp='Account', currency_obj=object.company_id.currency_id) + followup_table += ''' +
Invoice dateReferenceDue dateAmount (%s)Lit.
" + strend = "" + strend = "
+
Amount due: %s
''' % (total) + +%> + +${followup_table} + +
+ + + ]]>
+
+ + + + + Default follow-up of overdue invoices + ${user.email or ''} + ${user.company_id.name} Payment Follow-up + ${object.email} + ${object.lang} + + + + +

Dear ${object.name},

+

+ Exception made if there was a mistake of ours, it seems that the following amount stays unpaid. Please, take +appropriate measures in order to carry out this payment in the next 8 days. +Would your payment have been carried out after this mail was sent, please ignore this message. Do not hesitate to +contact our accounting department at (+32).10.68.94.39. +

+
+Best Regards, +
+
+
+${user.name} + +
+
+ + + +<% + from openerp.addons.account_followup.report import account_followup_print + rml_parse = account_followup_print.report_rappel(object._cr, user.id, "followup_rml_parser") + final_res = rml_parse._lines_get_with_partner(object, user.company_id.id) + followup_table = '' + for currency_dict in final_res: + currency_symbol = currency_dict.get('line', [{'currency_id': user.company_id.currency_id}])[0]['currency_id'].symbol + followup_table += ''' + + + + + + + + + ''' % (currency_symbol) + total = 0 + for aml in currency_dict['line']: + block = aml['blocked'] and 'X' or ' ' + total += aml['balance'] + strbegin = " " + date = aml['date_maturity'] or aml['date'] + if date <= ctx['current_date'] and aml['balance'] > 0: + strbegin = "" + followup_table +="" + strbegin + str(aml['date']) + strend + strbegin + aml['ref'] + strend + strbegin + str(date) + strend + strbegin + str(aml['balance']) + strend + strbegin + block + strend + "" + total = rml_parse.formatLang(total, dp='Account', currency_obj=object.company_id.currency_id) + followup_table += ''' +
Invoice dateReferenceDue dateAmount (%s)Lit.
" + strend = "" + strend = "
+
Amount due: %s
''' % (total) + +%> + +${followup_table} + +
+ + + ]]>
+
+ + + + + + + + + + + Send first reminder email + 0 + 15 + + True + Dear %(partner_name)s, -Please find in attachment a reminder of all your unpaid invoices, for a total amount due of: +Exception made if there was a mistake of ours, it seems that the following amount stays unpaid. Please, take appropriate measures in order to carry out this payment in the next 8 days. -%(followup_amount).2f %(company_currency)s +Would your payment have been carried out after this mail was sent, please ignore this message. Do not hesitate to contact our accounting department at (+32).10.68.94.39. -Thanks, --- -%(user_signature)s -%(company_name)s +Best Regards, + + + + + + Send reminder letter and email + 1 + 30 + + + True + True + +Dear %(partner_name)s, + +We are disappointed to see that despite sending a reminder, that your account is now seriously overdue. + +It is essential that immediate payment is made, otherwise we will have to consider placing a stop on your account which means that we will no longer be able to supply your company with (goods/services). +Please, take appropriate measures in order to carry out this payment in the next 8 days. + +If there is a problem with paying invoice that we are not aware of, do not hesitate to contact our accounting department at (+32).10.68.94.39. so that we can resolve the matter quickly. + +Details of due payments is printed below. + +Best Regards, + + + + + + Call the customer on the phone + 3 + 40 + + + + True + Call the customer on the phone! + +Dear %(partner_name)s, + +Despite several reminders, your account is still not settled. + +Unless full payment is made in next 8 days, then legal action for the recovery of the debt will be taken without further notice. + +I trust that this action will prove unnecessary and details of due payments is printed below. + +In case of any queries concerning this matter, do not hesitate to contact our accounting department at (+32).10.68.94.39. + +Best Regards, +
diff --git a/addons/account_followup/account_followup_demo.xml b/addons/account_followup/account_followup_demo.xml index 7103ffa0386..4f22c18857d 100644 --- a/addons/account_followup/account_followup_demo.xml +++ b/addons/account_followup/account_followup_demo.xml @@ -1,58 +1,13 @@ - - - - Default Follow-up - - First letter after 15 net days, 30 net days and 45 days end of month levels. - - - - Level 0 : 15 net days - 0 - days - 15 - - -Dear %(partner_name)s, - -Exception made if there was a mistake of ours, it seems that the following amount stays unpaid. Please, take appropriate measures in order to carry out this payment in the next 8 days. - -Would your payment have been carried out after this mail was sent, please ignore this message. Do not hesitate to contact our accounting department at (+32).10.68.94.39. - -Best Regards, - - - - - Level 1 : 30 net days - 1 - days - 30 - - -Dear %(partner_name)s, - -We are disappointed to see that despite sending a reminder, that your account is now seriously overdue. - -It is essential that immediate payment is made, otherwise we will have to consider placing a stop on your account which means that we will no longer be able to supply your company with (goods/services). -Please, take appropriate measures in order to carry out this payment in the next 8 days. - -If there is a problem with paying invoice that we are not aware of, do not hesitate to contact our accounting department at (+32).10.68.94.39. so that we can resolve the matter quickly. - -Details of due payments is printed below. - -Best Regards, - - - - - Level 2 : 45 days end of month - 2 - end_of_month - 45 + + + Urging reminder email + 4 + 50 + True + Dear %(partner_name)s, @@ -65,8 +20,29 @@ I trust that this action will prove unnecessary and details of due payments is p In case of any queries concerning this matter, do not hesitate to contact our accounting department at (+32).10.68.94.39. Best Regards, - + + + Urging reminder letter + 5 + 60 + + + True + + +Dear %(partner_name)s, +Despite several reminders, your account is still not settled. + +Unless full payment is made in next 8 days, then legal action for the recovery of the debt will be taken without further notice. + +I trust that this action will prove unnecessary and details of due payments is printed below. + +In case of any queries concerning this matter, do not hesitate to contact our accounting department at (+32).10.68.94.39. + +Best Regards, + + diff --git a/addons/account_followup/account_followup_view.xml b/addons/account_followup/account_followup_view.xml index 0617a95c2f0..a5464b241ce 100644 --- a/addons/account_followup/account_followup_view.xml +++ b/addons/account_followup/account_followup_view.xml @@ -1,14 +1,16 @@ - + account_followup.followup.line.tree account_followup.followup.line - + - + + + @@ -18,19 +20,47 @@ account_followup.followup.line
- - - - + --> - - - - - - - - - - res.company.followup.form.inherit - - res.company - - - - - - - -
diff --git a/addons/account_followup/report/account_followup_print.py b/addons/account_followup/report/account_followup_print.py index 593430691aa..90dd5969d53 100644 --- a/addons/account_followup/report/account_followup_print.py +++ b/addons/account_followup/report/account_followup_print.py @@ -25,7 +25,7 @@ import pooler from report import report_sxw class report_rappel(report_sxw.rml_parse): - def __init__(self, cr, uid, name, context): + def __init__(self, cr, uid, name, context=None): super(report_rappel, self).__init__(cr, uid, name, context=context) self.localcontext.update({ 'time': time, @@ -43,14 +43,17 @@ class report_rappel(report_sxw.rml_parse): return all_lines def _lines_get(self, stat_by_partner_line): + return self._lines_get_with_partner(stat_by_partner_line.partner_id, stat_by_partner_line.company_id.id) + + def _lines_get_with_partner(self, partner, company_id): pool = pooler.get_pool(self.cr.dbname) moveline_obj = pool.get('account.move.line') company_obj = pool.get('res.company') obj_currency = pool.get('res.currency') movelines = moveline_obj.search(self.cr, self.uid, - [('partner_id', '=', stat_by_partner_line.partner_id.id), + [('partner_id', '=', partner.id), ('account_id.type', '=', 'receivable'), - ('reconcile_id', '=', False), ('state', '<>', 'draft'),('company_id','=', stat_by_partner_line.company_id.id)]) + ('reconcile_id', '=', False), ('state', '<>', 'draft'),('company_id','=', company_id)]) movelines = moveline_obj.browse(self.cr, self.uid, movelines) base_currency = movelines[0].company_id.currency_id final_res = [] @@ -67,7 +70,7 @@ class report_rappel(report_sxw.rml_parse): 'date_maturity': line.date_maturity, 'balance': currency.id <> line.company_id.currency_id.id and line.amount_currency or (line.debit - line.credit), 'blocked': line.blocked, - 'currency_id': currency.symbol or currency.name, + 'currency_id': currency, } line_cur[currency.id]['line'].append(line_data) @@ -88,7 +91,7 @@ class report_rappel(report_sxw.rml_parse): li_delay.sort(reverse=True) text = "" a = {} - partner_line_ids = pooler.get_pool(self.cr.dbname).get('account.move.line').search(self.cr, self.uid, [('partner_id','=',stat_line.partner_id.id),('reconcile_id','=',False),('company_id','=',stat_line.company_id.id)]) + partner_line_ids = pooler.get_pool(self.cr.dbname).get('account.move.line').search(self.cr, self.uid, [('partner_id','=',stat_line.partner_id.id),('reconcile_id','=',False),('company_id','=',stat_line.company_id.id),('blocked','=',False)]) partner_delay = [] context.update({'lang': stat_line.partner_id.lang}) for i in pooler.get_pool(self.cr.dbname).get('account.move.line').browse(self.cr, self.uid, partner_line_ids, context): diff --git a/addons/account_followup/report/account_followup_print.rml b/addons/account_followup/report/account_followup_print.rml index 7f2242991de..fd3a35ab19c 100644 --- a/addons/account_followup/report/account_followup_print.rml +++ b/addons/account_followup/report/account_followup_print.rml @@ -194,7 +194,7 @@ [[ line['date_maturity'] and formatLang(line['date_maturity'], date=True) ]] - [[ formatLang(line['balance']) ]] [[ line['currency_id'] ]] + [[ formatLang(line['balance'], currency_obj=line['currency_id']) ]] [[ line['blocked'] and 'X' or '' ]] @@ -212,7 +212,7 @@ Total: - [[formatLang(reduce(lambda x,y: x+y['balance'], cur_lines['line'], 0.00)) ]] [[ line['currency_id'] ]] + [[formatLang(reduce(lambda x,y: x+y['balance'], cur_lines['line'], 0.00), currency_obj=line['currency_id']) ]] diff --git a/addons/account_followup/report/account_followup_report.xml b/addons/account_followup/report/account_followup_report.xml index 966931d78a9..6fee6a77ac1 100644 --- a/addons/account_followup/report/account_followup_report.xml +++ b/addons/account_followup/report/account_followup_report.xml @@ -64,8 +64,8 @@ {'search_default_followup_level':1} - - + + diff --git a/addons/account_followup/security/ir.model.access.csv b/addons/account_followup/security/ir.model.access.csv index 57292899825..8774015efc6 100644 --- a/addons/account_followup/security/ir.model.access.csv +++ b/addons/account_followup/security/ir.model.access.csv @@ -1,7 +1,7 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_account_followup_followup_line,account_followup.followup.line,model_account_followup_followup_line,account.group_account_user,1,0,0,0 -access_account_followup_followup_line_manager,account_followup.followup.line.manager,model_account_followup_followup_line,account.group_account_manager,1,1,1,1 -access_account_followup_followup_accountant,account_followup.followup user,model_account_followup_followup,account.group_account_user,1,0,0,0 -access_account_followup_followup_manager,account_followup.followup.manager,model_account_followup_followup,account.group_account_manager,1,1,1,1 -access_account_followup_stat_invoice,account_followup.stat.invoice,model_account_followup_stat,account.group_account_invoice,1,1,1,1 -access_account_followup_stat_by_partner_manager,account_followup.stat.by.partner,model_account_followup_stat_by_partner,account.group_account_manager,1,1,1,1 +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_followup_followup_line,account_followup.followup.line,model_account_followup_followup_line,account.group_account_invoice,1,0,0,0 +access_account_followup_followup_line_manager,account_followup.followup.line.manager,model_account_followup_followup_line,account.group_account_manager,1,1,1,1 +access_account_followup_followup_accountant,account_followup.followup user,model_account_followup_followup,account.group_account_invoice,1,0,0,0 +access_account_followup_followup_manager,account_followup.followup.manager,model_account_followup_followup,account.group_account_manager,1,1,1,1 +access_account_followup_stat_invoice,account_followup.stat.invoice,model_account_followup_stat,account.group_account_invoice,1,1,1,1 +access_account_followup_stat_by_partner_manager,account_followup.stat.by.partner,model_account_followup_stat_by_partner,account.group_account_user,1,1,1,1 diff --git a/addons/account_followup/test/account_followup.yml b/addons/account_followup/test/account_followup.yml index 06617b7af97..54dd3280984 100644 --- a/addons/account_followup/test/account_followup.yml +++ b/addons/account_followup/test/account_followup.yml @@ -3,34 +3,23 @@ - !record {model: account.invoice, id: account.demo_invoice_0}: check_total: 14.0 -- + date_invoice: 2012-06-2 + invoice_line: + - account_id : account.a_sale + name: 'Test PC' + quantity: 1.0 + journal_id: account.bank_journal + partner_id: base.res_partner_12 + reference_type: none +- !workflow {model: account.invoice, action: invoice_open, ref: account.demo_invoice_0} - I create a follow-up. - - !record {model: account.followup.print, id: account_followup_print_0}: + !record {model: account_followup.print, id: account_followup_print_0}: {} -- - I select the follow-up to send it to the partner. -- - !python {model: account.followup.print}: | - self.do_continue(cr, uid, [ref("account_followup_print_0")], {"active_ids": [ref("account_followup.account_followup_print_menu")], "active_id": ref("account_followup.account_followup_print_menu"), - }) -- - I select partners whom I want to send follow-ups. -- - !record {model: account.followup.print.all, id: account_followup_print_all_0}: - email_body: 'Date : %(date)s\n\nDear %(partner_name)s,\n\nPlease find in attachment - a reminder of all your unpaid invoices, for a total amount due of:\n\n%(followup_amount).2f - %(company_currency)s\n\nThanks,\n--\n%(user_signature)s\n%(company_name)s' - email_subject: Invoices Reminder - partner_ids: - - base.res_partner_12 - partner_lang: 1 -- - I send a follow-up mail to partner. -- - !python {model: account.followup.print.all}: | - import time - self.do_mail(cr, uid, [ref("account_followup_print_all_0")], {"active_ids": [ref("account_followup.account_followup_print_menu")], "date": time.strftime('%Y-%m-%d'), "followup_id": ref("account_followup.demo_followup1"), "active_id": ref("account_followup.account_followup_print_menu"), - }) +- + I will process follow-ups +- + !python {model: account_followup.print}: | + self.do_process(cr, uid, [ref("account_followup_print_0")], {"active_ids": [ref("account_followup.account_followup_print_menu")], "active_id": ref("account_followup.account_followup_print_menu"),}) \ No newline at end of file diff --git a/addons/account_followup/test/account_followup_report.yml b/addons/account_followup/test/account_followup_report.yml deleted file mode 100644 index f2ad8a5bb9f..00000000000 --- a/addons/account_followup/test/account_followup_report.yml +++ /dev/null @@ -1,10 +0,0 @@ -- - In order to test the report I print follow-up report. -- - !python {model: account.followup.print.all}: | - import time - ctx = {'form_view_ref':'account_followup.view_account_followup_print_all', 'followup_id': ref('account_followup.demo_followup1'),'date': time.strftime('%Y-%m-%d'),'model': 'account_followup.followup','active_ids':[ref('account_followup_print_all_0')], 'company_id':ref('base.main_company')} - data_dict = {'email_conf': 1} - from tools import test_reports - test_reports.try_report_action(cr, uid, 'action_account_followup_print_all', context=ctx, wiz_data=data_dict,wiz_buttons=["Print Follow-ups"], our_module='account_followup') - diff --git a/addons/account_followup/tests/__init__.py b/addons/account_followup/tests/__init__.py new file mode 100644 index 00000000000..cf0ea3acbe6 --- /dev/null +++ b/addons/account_followup/tests/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2012-TODAY OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import test_account_followup + +checks = [ + test_account_followup, +] + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/account_followup/tests/test_account_followup.py b/addons/account_followup/tests/test_account_followup.py new file mode 100644 index 00000000000..c46977bd6b5 --- /dev/null +++ b/addons/account_followup/tests/test_account_followup.py @@ -0,0 +1,159 @@ + + +import datetime + +import tools +from openerp.tests.common import TransactionCase + +import netsvc + +class TestAccountFollowup(TransactionCase): + def setUp(self): + """ setUp ***""" + super(TestAccountFollowup, self).setUp() + cr, uid = self.cr, self.uid + + self.user = self.registry('res.users') + self.user_id = self.user.browse(cr, uid, uid) + self.partner = self.registry('res.partner') + self.invoice = self.registry('account.invoice') + self.invoice_line = self.registry('account.invoice.line') + self.wizard = self.registry('account_followup.print') + self.followup_id = self.registry('account_followup.followup') + + self.partner_id = self.partner.create(cr, uid, {'name':'Test Company', + 'email':'test@localhost', + 'is_company': True, + }, + context=None) + self.followup_id = self.registry("ir.model.data").get_object_reference(cr, uid, "account_followup", "demo_followup1")[1] + self.account_id = self.registry("ir.model.data").get_object_reference(cr, uid, "account", "a_recv")[1] + self.journal_id = self.registry("ir.model.data").get_object_reference(cr, uid, "account", "bank_journal")[1] + self.pay_account_id = self.registry("ir.model.data").get_object_reference(cr, uid, "account", "cash")[1] + self.period_id = self.registry("ir.model.data").get_object_reference(cr, uid, "account", "period_10")[1] + + self.first_followup_line_id = self.registry("ir.model.data").get_object_reference(cr, uid, "account_followup", "demo_followup_line1")[1] + self.last_followup_line_id = self.registry("ir.model.data").get_object_reference(cr, uid, "account_followup", "demo_followup_line3")[1] + self.product_id = self.registry("ir.model.data").get_object_reference(cr, uid, "product", "product_product_6")[1] + self.invoice_id = self.invoice.create(cr, uid, {'partner_id': self.partner_id, + 'account_id': self.account_id, + 'journal_id': self.journal_id, + 'invoice_line': [(0, 0, { + 'name': "LCD Screen", + 'product_id': self.product_id, + 'quantity': 5, + 'price_unit':200 + })]}) + wf_service = netsvc.LocalService("workflow") + wf_service.trg_validate(uid, 'account.invoice', self.invoice_id, 'invoice_open', cr) + + self.voucher = self.registry("account.voucher") + + + def test_00_send_followup_after_3_days(self): + """ Send follow up after 3 days and check nothing is done (as first follow-up level is only after 15 days)""" + cr, uid = self.cr, self.uid + current_date = datetime.datetime.now() + delta = datetime.timedelta(days=3) + result = current_date + delta + self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), + 'followup_id': self.followup_id + }, context={"followup_id": self.followup_id}) + self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) + self.assertFalse(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id) + + + def run_wizard_three_times(self): + cr, uid = self.cr, self.uid + current_date = datetime.datetime.now() + delta = datetime.timedelta(days=40) + result = current_date + delta + self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), + 'followup_id': self.followup_id + }, context={"followup_id": self.followup_id}) + self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) + self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), + 'followup_id': self.followup_id + }, context={"followup_id": self.followup_id}) + self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) + self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), + 'followup_id': self.followup_id + }, context={"followup_id": self.followup_id}) + self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) + + + def test_01_send_followup_later_for_upgrade(self): + """ Send one follow-up after 15 days to check it upgrades to level 1""" + cr, uid = self.cr, self.uid + current_date = datetime.datetime.now() + delta = datetime.timedelta(days=15) + result = current_date + delta + self.wizard_id = self.wizard.create(cr, uid, { + 'date':result.strftime("%Y-%m-%d"), + 'followup_id': self.followup_id + }, context={"followup_id": self.followup_id}) + self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) + self.assertEqual(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id.id, self.first_followup_line_id, + "Not updated to the correct follow-up level") + + def test_02_check_manual_action(self): + """ Check that when running the wizard three times that the manual action is set""" + cr, uid = self.cr, self.uid + self.run_wizard_three_times() + self.assertEqual(self.partner.browse(cr, uid, self.partner_id).payment_next_action, + "Call the customer on the phone! ", "Manual action not set") + self.assertEqual(self.partner.browse(cr, uid, self.partner_id).payment_next_action_date, + datetime.datetime.now().strftime("%Y-%m-%d")) + + def test_03_filter_on_credit(self): + """ Check the partners can be filtered on having credits """ + cr, uid = self.cr, self.uid + ids = self.partner.search(cr, uid, [('credit', '>=', 0.0)]) + self.assertIn(self.partner_id, ids) + + def test_04_action_done(self): + """ Run the wizard 3 times, mark it as done, check the action fields are empty""" + cr, uid = self.cr, self.uid + partner_rec = self.partner.browse(cr, uid, self.partner_id) + self.run_wizard_three_times() + self.partner.action_done(cr, uid, self.partner_id) + self.assertEqual(partner_rec.payment_next_action, + "", "Manual action not emptied") + self.assertFalse(partner_rec.payment_responsible_id) + self.assertFalse(partner_rec.payment_next_action_date) + + def test_05_litigation(self): + """ Set the account move line as litigation, run the wizard 3 times and check nothing happened. + Turn litigation off. Run the wizard 3 times and check it is in the right follow-up level. + """ + cr, uid = self.cr, self.uid + aml_id = self.partner.browse(cr, uid, self.partner_id).unreconciled_aml_ids[0].id + self.registry('account.move.line').write(cr, uid, aml_id, {'blocked': True}) + self.run_wizard_three_times() + self.assertFalse(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id, "Litigation does not work") + self.registry('account.move.line').write(cr, uid, aml_id, {'blocked': False}) + self.run_wizard_three_times() + self.assertEqual(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id.id, + self.last_followup_line_id, "Lines are not equal") + + def test_06_pay_the_invoice(self): + """Run wizard until manual action, pay the invoice and check that partner has no follow-up level anymore and after running the wizard the action is empty""" + cr, uid = self.cr, self.uid + self.test_02_check_manual_action() + current_date = datetime.datetime.now() + delta = datetime.timedelta(days=1) + result = current_date + delta + self.invoice.pay_and_reconcile(cr, uid, [self.invoice_id], 1000.0, self.pay_account_id, + self.period_id, self.journal_id, self.pay_account_id, + self.period_id, self.journal_id, + name = "Payment for test customer invoice follow-up") + self.assertFalse(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id, "Level not empty") + self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), + 'followup_id': self.followup_id + }, context={"followup_id": self.followup_id}) + self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) + partner_ref = self.partner.browse(cr, uid, self.partner_id) + print partner_ref.credit, partner_ref.payment_next_action_date, partner_ref.payment_responsible_id + self.assertEqual(0, self.partner.browse(cr, uid, self.partner_id).credit, "Credit != 0") + self.assertFalse(self.partner.browse(cr, uid, self.partner_id).payment_next_action_date, "Next action date not cleared") + \ No newline at end of file diff --git a/addons/account_followup/wizard/account_followup_print.py b/addons/account_followup/wizard/account_followup_print.py index 5791bacbed0..db93b3c63a8 100644 --- a/addons/account_followup/wizard/account_followup_print.py +++ b/addons/account_followup/wizard/account_followup_print.py @@ -26,49 +26,6 @@ import tools from osv import fields, osv from tools.translate import _ -class account_followup_print(osv.osv_memory): - _name = 'account.followup.print' - _description = 'Print Follow-up & Send Mail to Customers' - _columns = { - 'date': fields.date('Follow-up Sending Date', required=True, help="This field allow you to select a forecast date to plan your follow-ups"), - 'followup_id': fields.many2one('account_followup.followup', 'Follow-Up', required=True), - } - - def _get_followup(self, cr, uid, context=None): - if context is None: - context = {} - if context.get('active_model', 'ir.ui.menu') == 'account_followup.followup': - return context.get('active_id', False) - company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - followp_id = self.pool.get('account_followup.followup').search(cr, uid, [('company_id', '=', company_id)], context=context) - return followp_id and followp_id[0] or False - - def do_continue(self, cr, uid, ids, context=None): - mod_obj = self.pool.get('ir.model.data') - - if context is None: - context = {} - data = self.browse(cr, uid, ids, context=context)[0] - model_data_ids = mod_obj.search(cr, uid, [('model','=','ir.ui.view'),('name','=','view_account_followup_print_all')], context=context) - resource_id = mod_obj.read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id'] - context.update({'followup_id': data.followup_id.id, 'date': data.date, 'company_id': data.followup_id.company_id.id}) - return { - 'name': _('Select Partners'), - 'view_type': 'form', - 'context': context, - 'view_mode': 'tree,form', - 'res_model': 'account.followup.print.all', - 'views': [(resource_id,'form')], - 'type': 'ir.actions.act_window', - 'target': 'new', - } - - _defaults = { - 'date': lambda *a: time.strftime('%Y-%m-%d'), - 'followup_id': _get_followup, - } -account_followup_print() - class account_followup_stat_by_partner(osv.osv): _name = "account_followup.stat.by.partner" _description = "Follow-up Statistics by Partner" @@ -110,50 +67,201 @@ class account_followup_stat_by_partner(osv.osv): a.active AND a.type = 'receivable' AND l.reconcile_id is NULL AND - l.partner_id IS NOT NULL + l.partner_id IS NOT NULL AND + (l.blocked = False) GROUP BY l.partner_id, l.company_id - )""") + )""") #Blocked is to take into account litigation account_followup_stat_by_partner() -class account_followup_print_all(osv.osv_memory): - _name = 'account.followup.print.all' - _description = 'Print Follow-up & Send Mail to Customers' - _columns = { - 'partner_ids': fields.many2many('account_followup.stat.by.partner', 'partner_stat_rel', 'osv_memory_id', 'partner_id', 'Partners', required=True), - 'email_conf': fields.boolean('Send Email Confirmation'), - 'email_subject': fields.char('Email Subject', size=64), - 'partner_lang': fields.boolean('Send Email in Partner Language', help='Do not change message text, if you want to send email in partner language, or configure from company'), - 'email_body': fields.text('Email Body'), - 'summary': fields.text('Summary', required=True, readonly=True), - 'test_print': fields.boolean('Test Print', help='Check if you want to print follow-ups without changing follow-ups level.') - } - def _get_summary(self, cr, uid, context=None): + +class account_followup_sending_results(osv.osv_memory): + + def do_report(self, cr, uid, ids, context=None): if context is None: context = {} - return context.get('summary', '') + return context.get('report_data') - def _get_partners(self, cr, uid, context=None): - return self._get_partners_followp(cr, uid, [], context=context)['partner_ids'] + def do_done(self, cr, uid, ids, context=None): + return {} + + def _get_description(self, cr, uid, context=None): + if context is None: + context = {} + return context.get('description') + + def _get_need_printing(self, cr, uid, context=None): + if context is None: + context = {} + return context.get('needprinting') + + _name = 'account_followup.sending.results' + _description = 'Results from the sending of the different letters and emails' + _columns = { + 'description': fields.text("Description", readonly=True), + 'needprinting': fields.boolean("Needs Printing") + } + _defaults = { + 'needprinting':_get_need_printing, + 'description':_get_description, + } + +account_followup_sending_results() + + +class account_followup_print(osv.osv_memory): + _name = 'account_followup.print' + _description = 'Print Follow-up & Send Mail to Customers' + _columns = { + 'date': fields.date('Follow-up Sending Date', required=True, + help="This field allow you to select a forecast date to plan your follow-ups"), + 'followup_id': fields.many2one('account_followup.followup', 'Follow-Up', required=True, readonly = True), + 'partner_ids': fields.many2many('account_followup.stat.by.partner', 'partner_stat_rel', + 'osv_memory_id', 'partner_id', 'Partners', required=True), + 'company_id':fields.related('followup_id', 'company_id', type='many2one', + relation='res.company', store=True, readonly=True), + 'email_conf': fields.boolean('Send Email Confirmation'), + 'email_subject': fields.char('Email Subject', size=64), + 'partner_lang': fields.boolean('Send Email in Partner Language', + help='Do not change message text, if you want to send email in partner language, or configure from company'), + 'email_body': fields.text('Email Body'), + 'summary': fields.text('Summary', readonly=True), + 'test_print': fields.boolean('Test Print', + help='Check if you want to print follow-ups without changing follow-ups level.'), + } + + def _get_followup(self, cr, uid, context=None): + if context is None: + context = {} + if context.get('active_model', 'ir.ui.menu') == 'account_followup.followup': + return context.get('active_id', False) + company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id + followp_id = self.pool.get('account_followup.followup').search(cr, uid, [('company_id', '=', company_id)], context=context) + return followp_id and followp_id[0] or False + + def process_partners(self, cr, uid, partner_ids, data, context=None): + partner_obj = self.pool.get('res.partner') + partner_ids_to_print = [] + nbmanuals = 0 + manuals = {} + nbmails = 0 + nbunknownmails = 0 + nbprints = 0 + resulttext = " " + for partner in self.pool.get('account_followup.stat.by.partner').browse(cr, uid, partner_ids, context=context): + if partner.max_followup_id.manual_action: + partner_obj.do_partner_manual_action(cr, uid, [partner.partner_id.id], context=context) + nbmanuals = nbmanuals + 1 + key = partner.partner_id.payment_responsible_id.name or _("Nobody") + if not key in manuals.keys(): + manuals[key]= 1 + else: + manuals[key] = manuals[key] + 1 + if partner.max_followup_id.send_email: + nbunknownmails += partner_obj.do_partner_mail(cr, uid, [partner.partner_id.id], context=context) + nbmails += 1 + if partner.max_followup_id.send_letter: + partner_ids_to_print.append(partner.id) + nbprints += 1 + message = _("Follow-up letter of ") + " " + partner.partner_id.latest_followup_level_id_without_lit.name + "" + _(" will be sent") + partner_obj.message_post(cr, uid, [partner.partner_id.id], body=message, context=context) + if nbunknownmails == 0: + resulttext += str(nbmails) + _(" email(s) sent") + else: + resulttext += str(nbmails) + _(" email(s) should have been sent, but ") + str(nbunknownmails) + _(" had unknown email address(es)") + "\n
" + resulttext += "
" + str(nbprints) + _(" letter(s) in report") + " \n
" + str(nbmanuals) + _(" manual action(s) assigned:") + needprinting = False + if nbprints > 0: + needprinting = True + resulttext += "

" + for item in manuals: + resulttext = resulttext + "

  • " + item + ":" + str(manuals[item]) + "\n
  • " + resulttext += "

    " + result = {} + action = partner_obj.do_partner_print(cr, uid, partner_ids_to_print, data, context=context) + result['needprinting'] = needprinting + result['resulttext'] = resulttext + result['action'] = action or {} + return result + + def do_update_followup_level(self, cr, uid, to_update, partner_list, date, context=None): + #update the follow-up level on account.move.line + for id in to_update.keys(): + if to_update[id]['partner_id'] in partner_list: + self.pool.get('account.move.line').write(cr, uid, [int(id)], {'followup_line_id': to_update[id]['level'], + 'followup_date': date}) + + def clear_manual_actions(self, cr, uid, partner_list, context=None): + # Partnerlist is list to exclude + # Will clear the actions of partners that have no due payments anymore + 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)] + ids = self.pool.get('res.partner').search(cr, uid, ['&', ('id', 'not in', partner_list_ids), '|', + ('payment_responsible_id', '!=', False), + ('payment_next_action_date', '!=', False)], context=context) + partners = self.pool.get('res.partner').browse(cr, uid, ids, context=context) + newids = [] + for part in partners: + credit = 0 + for aml in part.unreconciled_aml_ids: + credit +=aml.result + if credit <= 0: + newids.append(part.id) + self.pool.get('res.partner').action_done(cr, uid, newids, context=context) + return len(ids) + + def do_process(self, cr, uid, ids, context=None): + if context is None: + context = {} + + #Get partners + tmp = self._get_partners_followp(cr, uid, ids, context=context) + partner_list = tmp['partner_ids'] + to_update = tmp['to_update'] + date = self.browse(cr, uid, ids, context=context)[0].date + data = self.read(cr, uid, ids, [], context=context)[0] + data['followup_id'] = data['followup_id'][0] + + #Update partners + self.do_update_followup_level(cr, uid, to_update, partner_list, date, context=context) + #process the partners (send mails...) + restot = self.process_partners(cr, uid, partner_list, data, context=context) + #clear the manual actions if nothing is due anymore + nbactionscleared = self.clear_manual_actions(cr, uid, partner_list, context=context) + if nbactionscleared > 0: + restot['resulttext'] = restot['resulttext'] + "
  • " + _("%s partners have no credits and as such the action is cleared") %(str(nbactionscleared)) + "
  • " + res = restot['action'] + + #return the next action + mod_obj = self.pool.get('ir.model.data') + model_data_ids = mod_obj.search(cr, uid, [('model','=','ir.ui.view'),('name','=','view_account_followup_sending_results')], context=context) + resource_id = mod_obj.read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id'] + context.update({'description': restot['resulttext'], 'needprinting': restot['needprinting'], 'report_data': res}) + return { + 'name': _('Send Letters and Emails: Actions Summary'), + 'view_type': 'form', + 'context': context, + 'view_mode': 'tree,form', + 'res_model': 'account_followup.sending.results', + 'views': [(resource_id,'form')], + 'type': 'ir.actions.act_window', + 'target': 'new', + } def _get_msg(self, cr, uid, context=None): return self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.follow_up_msg _defaults = { - 'email_body': _get_msg, - 'email_subject': _('Invoices Reminder'), - 'partner_lang': True, - 'partner_ids': _get_partners, - 'summary': _get_summary, + 'date': lambda *a: time.strftime('%Y-%m-%d'), + 'followup_id': _get_followup, + 'email_body': "", + 'email_subject': _('Invoices Reminder'), + 'partner_lang': True, } def _get_partners_followp(self, cr, uid, ids, context=None): data = {} - if context is None: - context = {} - if ids: - data = self.browse(cr, uid, ids, context=context)[0] - company_id = 'company_id' in context and context['company_id'] or data.company_id.id + data = self.browse(cr, uid, ids, context=context)[0] + company_id = data.company_id.id cr.execute( "SELECT l.partner_id, l.followup_line_id,l.date_maturity, l.date, l.id "\ @@ -166,8 +274,9 @@ class account_followup_print_all(osv.osv_memory): "AND (l.partner_id is NOT NULL) "\ "AND (a.active) "\ "AND (l.debit > 0) "\ - "AND (l.company_id = %s) "\ - "ORDER BY l.date", (company_id,)) + "AND (l.company_id = %s) " \ + "AND (l.blocked = False)" \ + "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 move_lines = cr.fetchall() old = None fups = {} @@ -181,17 +290,17 @@ class account_followup_print_all(osv.osv_memory): "FROM account_followup_followup_line "\ "WHERE followup_id=%s "\ "ORDER BY delay", (fup_id,)) + + #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 for result in cr.dictfetchall(): delay = datetime.timedelta(days=result['delay']) fups[old] = (current_date - delay, result['id']) - if result['start'] == 'end_of_month': - fups[old][0].replace(day=1) old = result['id'] - fups[old] = (datetime.date(datetime.MAXYEAR, 12, 31), old) - partner_list = [] to_update = {} + + #Fill dictionary of accountmovelines to_update with the partners that need to be updated for partner_id, followup_line_id, date_maturity,date, id in move_lines: if not partner_id: continue @@ -209,134 +318,6 @@ class account_followup_print_all(osv.osv_memory): to_update[str(id)]= {'level': fups[followup_line_id][1], 'partner_id': stat_line_id} return {'partner_ids': partner_list, 'to_update': to_update} - def do_mail(self, cr, uid, ids, context=None): - mod_obj = self.pool.get('ir.model.data') - move_obj = self.pool.get('account.move.line') - user_obj = self.pool.get('res.users') - - if context is None: - context = {} - data = self.browse(cr, uid, ids, context=context)[0] - stat_by_partner_line_ids = [partner_id.id for partner_id in data.partner_ids] - partners = [stat_by_partner_line / 10000 for stat_by_partner_line in stat_by_partner_line_ids] - model_data_ids = mod_obj.search(cr, uid, [('model','=','ir.ui.view'),('name','=','view_account_followup_print_all_msg')], context=context) - resource_id = mod_obj.read(cr, uid, model_data_ids, fields=['res_id'], context=context)[0]['res_id'] - if data.email_conf: - msg_sent = '' - msg_unsent = '' - data_user = user_obj.browse(cr, uid, uid, context=context) - for partner in self.pool.get('res.partner').browse(cr, uid, partners, context=context): - 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))]) - data_lines = move_obj.browse(cr, uid, ids_lines, context=context) - total_amt = 0.0 - for line in data_lines: - total_amt += line.debit - line.credit - dest = False - if partner: - dest = [partner.email] - if not data.partner_lang: - body = data.email_body - else: - cxt = context.copy() - cxt['lang'] = partner.lang - body = user_obj.browse(cr, uid, uid, context=cxt).company_id.follow_up_msg - move_line = '' - subtotal_due = 0.0 - subtotal_paid = 0.0 - subtotal_maturity = 0.0 - balance = 0.0 - l = '--------------------------------------------------------------------------------------------------------------------------' - 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 - for i in data_lines: - maturity = 0.00 - if i.date_maturity < time.strftime('%Y-%m-%d') and (i.debit - i.credit): - maturity = i.debit - i.credit - subtotal_due = subtotal_due + i.debit - subtotal_paid = subtotal_paid + i.credit - subtotal_maturity = subtotal_maturity + int(maturity) - balance = balance + (i.debit - i.credit) - 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' - 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' - move_line = move_line + '\t\t\t' + 'Balance'.rjust(33) + '\t' + str(balance).rjust(10) + '\n' + l - val = { - 'partner_name':partner.name, - 'followup_amount':total_amt, - 'user_signature':data_user.name, - 'company_name':data_user.company_id.name, - 'company_currency':data_user.company_id.currency_id.name, - 'line':move_line, - 'heading': head, - 'date':time.strftime('%Y-%m-%d'), - } - body = body%val - sub = tools.ustr(data.email_subject) - msg = '' - if dest: - try: - vals = {'state': 'outgoing', - 'subject': sub, - 'body_html': '
    %s
    ' % body, - 'email_to': dest, - 'email_from': data_user.email or tools.config.options['email_from']} - self.pool.get('mail.mail').create(cr, uid, vals, context=context) - msg_sent += partner.name + '\n' - except Exception, e: - raise osv.except_osv('Error !', e ) - else: - msg += partner.name + '\n' - msg_unsent += msg - if not msg_unsent: - summary = _("All Emails have been successfully sent to Partners:.\n\n%s") % msg_sent - else: - msg_unsent = _("Email not sent to following Partners, Email not available !\n\n%s") % msg_unsent - msg_sent = msg_sent and _("\n\nEmail sent to following Partners successfully. !\n\n%s") % msg_sent - line = '==========================================================================' - summary = msg_unsent + line + msg_sent - context.update({'summary': summary}) - else: - 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.'}) - - return { - 'name': _('Followup Summary'), - 'view_type': 'form', - 'context': context, - 'view_mode': 'tree,form', - 'res_model': 'account.followup.print.all', - 'views': [(resource_id,'form')], - 'type': 'ir.actions.act_window', - 'target': 'new', - 'nodestroy': True - } - - def do_print(self, cr, uid, ids, context=None): - if context is None: - context = {} - data = self.read(cr, uid, ids, [], context=context)[0] - res = self._get_partners_followp(cr, uid, ids, context)['to_update'] - to_update = res - data['followup_id'] = 'followup_id' in context and context['followup_id'] or False - date = 'date' in context and context['date'] or data['date'] - if not data['test_print']: - for id in to_update.keys(): - if to_update[id]['partner_id'] in data['partner_ids']: - cr.execute( - "UPDATE account_move_line "\ - "SET followup_line_id=%s, followup_date=%s "\ - "WHERE id=%s", - (to_update[id]['level'], - date, int(id),)) - data.update({'date': context['date']}) - datas = { - 'ids': [], - 'model': 'account_followup.followup', - 'form': data - } - return { - 'type': 'ir.actions.report.xml', - 'report_name': 'account_followup.followup.print', - 'datas': datas, - } - -account_followup_print_all() +account_followup_print() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/account_followup/wizard/account_followup_print_view.xml b/addons/account_followup/wizard/account_followup_print_view.xml index fe77dbc053e..2def3550533 100644 --- a/addons/account_followup/wizard/account_followup_print_view.xml +++ b/addons/account_followup/wizard/account_followup_print_view.xml @@ -4,25 +4,30 @@ account.followup.print.form - account.followup.print + account_followup.print
    - - + + -
    -
    +

    + When you hit the button below, for every partner the most overdue invoice (without litigation) is checked. + If the amount of days overdue is greater or equal than the amount of days specified by a next follow-up level, + it will be updated to that next level and the actions of that level will be executed. (See Configuration > Follow-up Levels) +

    +
    +
    -
    +
    Send Follow-Ups - account.followup.print + account_followup.print form form new @@ -30,8 +35,10 @@ + parent="menu_finance_followup" + name = "Send Letters and Emails" + groups = "account.group_account_user" + sequence="2"/> account_followup.stat.by.partner.search @@ -47,7 +54,6 @@ - account_followup.stat.by.partner.tree account_followup.stat.by.partner @@ -63,63 +69,25 @@ - - account.followup.print.all.form - account.followup.print.all + + account_followup.sending.results.form + account_followup.sending.results -
    +
    -
    - - - - - - - - - - - - - - - - +
    + +
    +
    +
    +
    +
    - - - account.followup.print.all.msg.form - account.followup.print.all - -
    - - - - -
    - - - Send Follow-Ups - ir.actions.act_window - account.followup.print.all - form - form - new - diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 3273870f5e0..aec106f766b 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -295,7 +295,7 @@ class mail_message(osv.Model): return {'id': message.id, 'type': message.type, - 'body': html_email_clean(message.body), + 'body': html_email_clean(message.body or ''), 'model': message.model, 'res_id': message.res_id, 'record_name': message.record_name,