← Back to team overview

openerp-community-reviewer team mailing list archive

[Merge] lp:~camptocamp/account-financial-tools/add-manual-line-and-fees-nbi into lp:account-financial-tools

 

Nicolas Bessi - Camptocamp has proposed merging lp:~camptocamp/account-financial-tools/add-manual-line-and-fees-nbi into lp:account-financial-tools.

Requested reviews:
  Account Core Editors (account-core-editors)

For more details, see:
https://code.launchpad.net/~camptocamp/account-financial-tools/add-manual-line-and-fees-nbi/+merge/218584

Extend credit control to add new functionnalities:

There is now a wizard that allows to manually force policy level of an invoice.
The wizard can be reach from invoice.

Adds first version of dunning fees addons
-- 
https://code.launchpad.net/~camptocamp/account-financial-tools/add-manual-line-and-fees-nbi/+merge/218584
Your team Account Core Editors is requested to review the proposed merge of lp:~camptocamp/account-financial-tools/add-manual-line-and-fees-nbi into lp:account-financial-tools.
=== modified file 'account_credit_control/__openerp__.py'
--- account_credit_control/__openerp__.py	2014-03-24 08:37:48 +0000
+++ account_credit_control/__openerp__.py	2014-05-07 11:50:00 +0000
@@ -19,7 +19,7 @@
 #
 ##############################################################################
 {'name': 'Account Credit Control',
- 'version': '0.1.1',
+ 'version': '0.2.0',
  'author': 'Camptocamp',
  'maintainer': 'Camptocamp',
  'category': 'Finance',
@@ -69,6 +69,7 @@
           "wizard/credit_control_emailer_view.xml",
           "wizard/credit_control_marker_view.xml",
           "wizard/credit_control_printer_view.xml",
+          "wizard/credit_control_policy_changer_view.xml",
           "security/ir.model.access.csv"],
  'demo_xml': ["credit_control_demo.xml"],
  'tests': [],

=== modified file 'account_credit_control/account.py'
--- account_credit_control/account.py	2013-09-04 12:37:34 +0000
+++ account_credit_control/account.py	2014-05-07 11:50:00 +0000
@@ -27,11 +27,11 @@
     _inherit = "account.account"
 
     _columns = {
-        'credit_control_line_ids':
-            fields.one2many('credit.control.line',
-                            'account_id',
-                            string='Credit Lines',
-                            readonly=True),
+        'credit_control_line_ids': fields.one2many(
+            'credit.control.line',
+            'account_id',
+            string='Credit Lines',
+            readonly=True),
         }
 
     def copy_data(self, cr, uid, id, default=None, context=None):
@@ -42,34 +42,3 @@
         default['credit_control_line_ids'] = False
         return super(AccountAccount, self).copy_data(
             cr, uid, id, default=default, context=context)
-
-
-class AccountInvoice(orm.Model):
-    """Add a link to a credit control policy on account.account"""
-
-    _inherit = "account.invoice"
-    _columns = {
-        'credit_policy_id':
-            fields.many2one('credit.control.policy',
-                            'Credit Control Policy',
-                            help=("The Credit Control Policy used for this "
-                                  "invoice. If nothing is defined, it will "
-                                  "use the account setting or the partner "
-                                  "setting.")
-                            ),
-        'credit_control_line_ids':
-            fields.one2many('credit.control.line',
-                            'invoice_id',
-                            string='Credit Lines',
-                            readonly=True),
-        }
-
-    def copy_data(self, cr, uid, id, default=None, context=None):
-        if default is None:
-            default = {}
-        else:
-            default = default.copy()
-        default = default.copy()
-        default['credit_control_line_ids'] = False
-        return super(AccountInvoice, self).copy_data(
-            cr, uid, id, default=default, context=context)

=== modified file 'account_credit_control/invoice.py'
--- account_credit_control/invoice.py	2013-09-18 07:59:03 +0000
+++ account_credit_control/invoice.py	2014-05-07 11:50:00 +0000
@@ -18,7 +18,7 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-from openerp.osv import orm
+from openerp.osv import orm, fields
 from openerp.tools.translate import _
 
 
@@ -26,7 +26,37 @@
     """Check on cancelling of an invoice"""
     _inherit = 'account.invoice'
 
+    _columns = {
+        'credit_policy_id':
+            fields.many2one('credit.control.policy',
+                            'Credit Control Policy',
+                            help=("The Credit Control Policy used for this "
+                                  "invoice. If nothing is defined, it will "
+                                  "use the account setting or the partner "
+                                  "setting."),
+                            readonly=True,
+                            ),
+        'credit_control_line_ids':
+            fields.one2many('credit.control.line',
+                            'invoice_id',
+                            string='Credit Lines',
+                            readonly=True),
+        }
+
+    def copy_data(self, cr, uid, id, default=None, context=None):
+        """Ensure that credit lines and policcy are not copied"""
+        if default is None:
+            default = {}
+        else:
+            default = default.copy()
+        default = default.copy()
+        default['credit_control_line_ids'] = False
+        default['credit_policy_id'] = False
+        return super(AccountInvoice, self).copy_data(
+            cr, uid, id, default=default, context=context)
+
     def action_cancel(self, cr, uid, ids, context=None):
+        """Prevent to cancel invoice related to credit line"""
         # We will search if this invoice is linked with credit
         cc_line_obj = self.pool.get('credit.control.line')
         for invoice_id in ids:

=== modified file 'account_credit_control/line.py'
--- account_credit_control/line.py	2013-09-04 12:37:34 +0000
+++ account_credit_control/line.py	2014-05-07 11:50:00 +0000
@@ -38,9 +38,11 @@
     _name = "credit.control.line"
     _description = "A credit control line"
     _rec_name = "id"
-
+    _order = "date DESC"
     _columns = {
-        'date': fields.date('Controlling date', required=True),
+        'date': fields.date('Controlling date',
+                            required=True,
+                            select=True),
         # maturity date of related move line we do not use a related field in order to
         # allow manual changes
         'date_due': fields.date('Due date',
@@ -116,6 +118,7 @@
                                 string='Level',
                                 store=True,
                                 readonly=True),
+        'manually_overriden': fields.boolean('Manually overriden')
     }
 
 
@@ -141,8 +144,27 @@
         return data
 
     def create_or_update_from_mv_lines(self, cr, uid, ids, lines,
-                                       level_id, controlling_date, context=None):
-        """Create or update line based on levels"""
+                                       level_id, controlling_date,
+                                       check_tolerance=True, context=None):
+        """Create or update line based on levels
+
+        if check_tolerance is true credit line will not be
+        created if open amount is too small.
+        eg. we do not want to send a letter for 10 cents
+        of open amount.
+
+        :param lines: move.line id list
+        :param level_id: credit.control.policy.level id
+        :param controlling_date: date string of the credit controlling date.
+                                 Generally it should be the same
+                                 as create date
+        :param check_tolerance: boolean if True credit line
+                                will not be generated if open amount
+                                is smaller than company defined
+                                tolerance
+
+        :returns: list of created credit line ids
+        """
         currency_obj = self.pool.get('res.currency')
         level_obj = self.pool.get('credit.control.policy.level')
         ml_obj = self.pool.get('account.move.line')
@@ -164,26 +186,31 @@
         for line in ml_obj.browse(cr, uid, lines, context):
 
             open_amount = line.amount_residual_currency
-
-            if open_amount > tolerance.get(line.currency_id.id, tolerance_base):
-                vals = self._prepare_from_move_line(
-                    cr, uid, line, level, controlling_date, open_amount, context=context)
-                line_id = self.create(cr, uid, vals, context=context)
-                line_ids.append(line_id)
-
-                # when we have lines generated earlier in draft,
-                # on the same level, it means that we have left
-                # them, so they are to be considered as ignored
-                previous_draft_ids = self.search(
-                    cr, uid,
-                    [('move_line_id', '=', line.id),
-                     ('level', '=', level.id),
-                     ('state', '=', 'draft'),
-                     ('id', '!=', line_id)],
-                    context=context)
-                if previous_draft_ids:
-                    self.write(cr, uid, previous_draft_ids,
-                               {'state': 'ignored'}, context=context)
+            cur_tolerance = tolerance.get(line.currency_id.id, tolerance_base)
+            if check_tolerance and open_amount < cur_tolerance:
+                continue
+            vals = self._prepare_from_move_line(cr, uid,
+                                                line,
+                                                level,
+                                                controlling_date,
+                                                open_amount,
+                                                context=context)
+            line_id = self.create(cr, uid, vals, context=context)
+            line_ids.append(line_id)
+
+            # when we have lines generated earlier in draft,
+            # on the same level, it means that we have left
+            # them, so they are to be considered as ignored
+            previous_draft_ids = self.search(
+                cr, uid,
+                [('move_line_id', '=', line.id),
+                 ('policy_level_id', '=', level.id),
+                 ('state', '=', 'draft'),
+                 ('id', '!=', line_id)],
+                context=context)
+            if previous_draft_ids:
+                self.write(cr, uid, previous_draft_ids,
+                           {'state': 'ignored'}, context=context)
 
         return line_ids
 

=== modified file 'account_credit_control/line_view.xml'
--- account_credit_control/line_view.xml	2013-03-18 09:21:33 +0000
+++ account_credit_control/line_view.xml	2014-05-07 11:50:00 +0000
@@ -10,6 +10,7 @@
           <field name="date_due"/>
           <field name="date_sent"/>
           <field name="level"/>
+          <field name="manually_overriden"/>
           <field name="state"/>
           <field name="channel"/>
           <field name="invoice_id"/>
@@ -32,7 +33,7 @@
       <field name="type">search</field>
       <field name="arch" type="xml">
         <search string="Control Credit Lines">
-          <group>
+          <group string="Filters">
             <filter name="filter_draft" icon="terp-mail-message-new"
                     string="Draft" domain="[('state', '=', 'draft')]"
                     help="Draft lines have to be triaged."/>
@@ -48,6 +49,9 @@
             <filter name="filter_error" icon="terp-gtk-stop" string="Error"
                     domain="[('state', 'in', ('error', 'email_error'))]"
                     help="An error has occured during the sending of the email."/>
+            <filter name="filter_manual" icon="terp-gtk-stop" string="Manual change"
+                    domain="[('manually_overriden', '=', True)]"
+                    help="The line was deprecated by a manual change of policy on invoice."/>
             <separator orientation="vertical"/>
 
             <field name="date"/>
@@ -89,6 +93,8 @@
             <separator orientation="vertical"/>
             <filter domain='[]' context="{'group_by': 'channel'}"
                     icon="terp-document-new" string="Channel"/>
+            <filter domain='[]' context="{'group_by': 'manually_overriden'}"
+                    icon="terp-document-new" string="Manual change"/>
           </group>
         </search>
       </field>
@@ -103,6 +109,7 @@
           <field name="date"/>
           <field name="date_due"/>
           <field name="level"/>
+          <field name="manually_overriden"/>
           <field name="state"/>
           <field name="channel"/>
           <field name="invoice_id"/>

=== modified file 'account_credit_control/partner.py'
--- account_credit_control/partner.py	2013-09-04 12:29:15 +0000
+++ account_credit_control/partner.py	2014-05-07 11:50:00 +0000
@@ -19,6 +19,7 @@
 #
 ##############################################################################
 from openerp.osv import orm, fields
+from openerp.tools.translate import _
 
 
 class ResPartner(orm.Model):
@@ -28,21 +29,47 @@
     _inherit = "res.partner"
 
     _columns = {
-        'credit_policy_id':
-            fields.many2one('credit.control.policy',
-                            'Credit Control Policy',
-                            help=("The Credit Control Policy used for this "
-                                  "partner. This setting can be forced on the "
-                                  "invoice. If nothing is defined, it will use "
-                                  "the company setting.")),
-        'credit_control_line_ids':
-            fields.one2many('credit.control.line',
-                            'invoice_id',
-                            string='Credit Control Lines',
-                            readonly=True)
+        'credit_policy_id': fields.many2one(
+            'credit.control.policy',
+            'Credit Control Policy',
+            domain="[('account_ids', 'in', property_account_receivable)]",
+            help=("The Credit Control Policy used for this "
+                  "partner. This setting can be forced on the "
+                  "invoice. If nothing is defined, it will use "
+                  "the company setting.")
+        ),
+        'credit_control_line_ids': fields.one2many(
+            'credit.control.line',
+            'invoice_id',
+            string='Credit Control Lines',
+            readonly=True
+        )
     }
 
+    def _check_credit_policy(self, cr, uid, part_ids, context=None):
+        """Ensure that policy on partner are limited to the account policy"""
+        if isinstance(part_ids, (int, long)):
+            part_ids = [part_ids]
+        policy_obj = self.pool['credit.control.policy']
+        for partner in self.browse(cr, uid, part_ids, context):
+            if not partner.property_account_receivable or \
+               not partner.credit_policy_id:
+                return True
+            account = partner.property_account_receivable
+            policy_obj.check_policy_against_account(
+                cr, uid,
+                account.id,
+                partner.credit_policy_id.id,
+                context=context
+            )
+        return True
+
+    _constraints = [(_check_credit_policy,
+                     'The policy must be related to the receivable account',
+                     ['credit_policy_id'])]
+
     def copy_data(self, cr, uid, id, default=None, context=None):
+        """Remove credit lines when copying partner"""
         if default is None:
             default = {}
         else:

=== modified file 'account_credit_control/policy.py'
--- account_credit_control/policy.py	2014-03-04 07:52:47 +0000
+++ account_credit_control/policy.py	2014-05-07 11:50:00 +0000
@@ -18,11 +18,11 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-from openerp.osv.orm import Model, fields
+from openerp.osv import orm, fields
 from openerp.tools.translate import _
 
 
-class CreditControlPolicy(Model):
+class CreditControlPolicy(orm.Model):
     """Define a policy of reminder"""
 
     _name = "credit.control.policy"
@@ -42,7 +42,7 @@
                 'account_ids': fields.many2many('account.account',
                                                 string='Accounts',
                                                 required=True,
-                                                domain="[('reconcile', '=', True)]",
+                                                domain="[('type', '=', 'receivable')]",
                                                 help="This policy will be active only"
                                                      " for the selected accounts"),
                 'active': fields.boolean('Active'),
@@ -103,7 +103,10 @@
         my_obj = self.pool.get(model)
         move_l_obj = self.pool.get('account.move.line')
 
-        default_domain = self._move_lines_domain(cr, uid, policy, controlling_date, context=context)
+        default_domain = self._move_lines_domain(cr, uid,
+                                                 policy,
+                                                 controlling_date,
+                                                 context=context)
         to_add_ids = set()
         to_remove_ids = set()
 
@@ -198,15 +201,34 @@
         if isinstance(policy_id, list):
             policy_id = policy_id[0]
         cr.execute("SELECT move_line_id FROM credit_control_line"
-                   "    WHERE policy_id != %s and move_line_id in %s",
+                   "    WHERE policy_id != %s and move_line_id in %s"
+                   "    AND manually_overriden IS false",
                    (policy_id, tuple(lines)))
         res = cr.fetchall()
         if res:
             different_lines.update([x[0] for x in res])
         return different_lines
 
-
-class CreditControlPolicyLevel(Model):
+    def check_policy_against_account(self, cr, uid, account_id, policy_id,
+                                     context=None):
+        """Ensure that policy correspond to account relation"""
+        policy = self.browse(cr, uid, policy_id, context=context)
+        account = self.pool['account.account'].browse(cr, uid, account_id,
+                                                      context=context)
+        policies_id = self.search(cr, uid, [],
+                                  context=context)
+        policies = self.browse(cr, uid, policies_id, context=context)
+        allowed = [x for x in policies if
+                   account in x.account_ids or x.do_nothing]
+        if policy not in allowed:
+            allowed_names = u"\n".join(x.name for x in allowed)
+            raise orm.except_orm(
+                _('You can only use a policy set on  account %s') % account.name,
+                _("Please choose one of the following policies:\n %s") % allowed_names)
+        return True
+
+
+class CreditControlPolicyLevel(orm.Model):
     """Define a policy level. A level allows to determine if
     a move line is due and the level of overdue of the line"""
 
@@ -319,8 +341,11 @@
                "                 FROM credit_control_line\n"
                "                 WHERE move_line_id = mv_line.id\n"
                # lines from a previous level with a draft or ignored state
+               # or manually overriden
                # have to be generated again for the previous level
-               "                 AND state not in ('draft', 'ignored'))")
+               "                 AND NOT manually_overriden\n"
+               "                 AND state NOT IN ('draft', 'ignored'))"
+               " AND (mv_line.debit IS NOT NULL AND mv_line.debit != 0.0)\n")
         sql += " AND"
         sql += self._get_sql_date_boundary_for_computation_mode(cr, uid, level,
                                                                 controlling_date, context)
@@ -346,11 +371,15 @@
                " WHERE cr_line.id = (SELECT credit_control_line.id FROM credit_control_line\n"
                "                            WHERE credit_control_line.move_line_id = mv_line.id\n"
                "                            AND state != 'ignored'"
+               "                            AND NOT manually_overriden"
                "                              ORDER BY credit_control_line.level desc limit 1)\n"
                " AND cr_line.level = %(previous_level)s\n"
+               " AND (mv_line.debit IS NOT NULL AND mv_line.debit != 0.0)\n"
                # lines from a previous level with a draft or ignored state
+               # or manually overriden
                # have to be generated again for the previous level
-               " AND cr_line.state not in ('draft', 'ignored')\n"
+               " AND NOT manually_overriden\n"
+               " AND cr_line.state NOT IN ('draft', 'ignored')\n"
                " AND mv_line.id in %(line_ids)s\n")
         sql += " AND "
         sql += self._get_sql_date_boundary_for_computation_mode(cr, uid, level,

=== modified file 'account_credit_control/run.py'
--- account_credit_control/run.py	2013-09-04 12:29:15 +0000
+++ account_credit_control/run.py	2014-05-07 11:50:00 +0000
@@ -81,14 +81,24 @@
 
     def _check_run_date(self, cr, uid, ids, controlling_date, context=None):
         """Ensure that there is no credit line in the future using controlling_date"""
-        line_obj =  self.pool.get('credit.control.line')
+        run_obj = self.pool['credit.control.run']
+        runs = run_obj.search(cr, uid, [('date', '>', controlling_date)],
+                              order='date DESC', limit=1, context=context)
+        if runs:
+            run = run_obj.browse(cr, uid, runs[0], context=context)
+            raise orm.except_orm(_('Error'),
+                                 _('A run has already been executed more '
+                                   'recently than %s') % (run.date))
+
+        line_obj = self.pool['credit.control.line']
         lines = line_obj.search(cr, uid, [('date', '>', controlling_date)],
                                 order='date DESC', limit=1, context=context)
         if lines:
             line = line_obj.browse(cr, uid, lines[0], context=context)
             raise orm.except_orm(_('Error'),
-                                 _('A run has already been executed more '
-                                   'recently than %s') % (line.date))
+                                 _('A credit control line more '
+                                   'recent than %s exists at %s') %
+                                 (controlling_date, line.date))
         return True
 
     def _generate_credit_lines(self, cr, uid, run_id, context=None):
@@ -110,10 +120,10 @@
                                  _('Please select a policy'))
 
         report = ''
+        generated_ids = []
         for policy in policies:
             if policy.do_nothing:
                 continue
-
             lines = policy._get_move_lines_to_process(run.date, context=context)
             manual_lines = policy._lines_different_policy(lines, context=context)
             lines.difference_update(manual_lines)
@@ -125,7 +135,7 @@
                     level_lines = level.get_level_lines(run.date, lines, context=context)
                     policy_generated_ids += cr_line_obj.create_or_update_from_mv_lines(
                         cr, uid, [], list(level_lines), level.id, run.date, context=context)
-
+            generated_ids.extend(policy_generated_ids)
             if policy_generated_ids:
                 report += _("Policy \"%s\" has generated %d Credit Control Lines.\n") % \
                         (policy.name, len(policy_generated_ids))
@@ -138,6 +148,7 @@
                 'report': report,
                 'manual_ids': [(6, 0, manually_managed_lines)]}
         run.write(vals, context=context)
+        return generated_ids
 
     def generate_credit_lines(self, cr, uid, run_id, context=None):
         """Generate credit control lines
@@ -147,7 +158,7 @@
         """
         try:
             cr.execute('SELECT id FROM credit_control_run'
-                           ' LIMIT 1 FOR UPDATE NOWAIT')
+                       ' LIMIT 1 FOR UPDATE NOWAIT')
         except Exception as exc:
             # in case of exception openerp will do a rollback for us and free the lock
             raise orm.except_orm(_('Error'),

=== modified file 'account_credit_control/run_view.xml'
--- account_credit_control/run_view.xml	2013-03-18 09:21:33 +0000
+++ account_credit_control/run_view.xml	2014-05-07 11:50:00 +0000
@@ -19,7 +19,8 @@
       <field name="type">form</field>
       <field name="arch" type="xml">
         <form string="Credit control run">
-          <field name="date"/>
+          <field name="date"
+                 attrs="{'readonly': [('state', '!=', 'draft')]}"/>
           <newline/>
           <notebook colspan="4">
             <page string="Policies">

=== modified file 'account_credit_control/scenarios/features/09_credit_control_run_jul.feature'
--- account_credit_control/scenarios/features/09_credit_control_run_jul.feature	2014-03-03 11:23:20 +0000
+++ account_credit_control/scenarios/features/09_credit_control_run_jul.feature	2014-05-07 11:50:00 +0000
@@ -13,10 +13,10 @@
 Feature: Ensure that email credit line generation first pass is correct
 
     @account_credit_control_mark
-  Scenario: mark lines
-    Given there is "draft" credit lines
-    And I mark all draft email to state "to_be_sent"
-    Then the draft line should be in state "to_be_sent"
+    Scenario: mark lines
+      Given there is "draft" credit lines
+      And I mark all draft email to state "to_be_sent"
+      Then the draft line should be in state "to_be_sent"
 
   @account_credit_control_run_month
   Scenario: Create run

=== added file 'account_credit_control/scenarios/features/11_credit_control_manual_setting.feature'
--- account_credit_control/scenarios/features/11_credit_control_manual_setting.feature	1970-01-01 00:00:00 +0000
+++ account_credit_control/scenarios/features/11_credit_control_manual_setting.feature	2014-05-07 11:50:00 +0000
@@ -0,0 +1,42 @@
+###############################################################################
+#
+#    OERPScenario, OpenERP Functional Tests
+#    Copyright 2012 Camptocamp SA
+#    Author Nicolas Bessi
+##############################################################################
+
+# Features Generic tags (none for all)
+##############################################################################
+
+@account_credit_control  @account_credit_control_run  @account_credit_control_run_change_level
+
+Feature: Ensure that manually changing  an invoice level feature works as expected
+
+  @account_credit_control_change_level
+  Scenario: Change level
+    Given I change level for invoice "SAJ/2014/0004" to "10 days net" of policy "3 time policy"
+    Then wizard selected move lines should be:
+      | name  |
+      | SI_4  |
+    When I confirm the level change
+    And I should have "3" credit control lines overriden
+    And one new credit control line of level "10 days net" related to invoice "SAJ/2014/0004"
+    Then I force date of generated credit line to "2013-09-15"
+
+  @account_credit_control_run_month_sept
+  Scenario: Create run
+    Given there is "draft" credit lines
+    And I mark all draft email to state "to_be_sent"
+    Then the draft line should be in state "to_be_sent"
+    Given I need a "credit.control.run" with oid: credit_control.manual_change
+    And having:
+      | name |      value |
+      | date | 2013-09-30 |
+    When I launch the credit run
+    Then my credit run should be in state "done"
+
+  @account_credit_control_manual_next_step
+  Scenario: Check manually  managed line on run
+    Given the invoice "SAJ/2014/0004" with manual changes
+    And the invoice has "1" line of level "1" for policy "3 time policy"
+    And the invoice has "1" line of level "2" for policy "3 time policy"

=== modified file 'account_credit_control/scenarios/features/steps/account_credit_control.py'
--- account_credit_control/scenarios/features/steps/account_credit_control.py	2014-03-04 07:52:47 +0000
+++ account_credit_control/scenarios/features/steps/account_credit_control.py	2014-05-07 11:50:00 +0000
@@ -142,4 +142,4 @@
                                                     ('state', 'in', ('draft', 'ignored'))])
     assert_equal(len(to_check), int(number), msg="More than %s found" % number)
     lines = model('credit.control.line').browse(to_check)
-    assert ['ignored', 'draft'] == lines.state
+    assert set(['ignored', 'draft']) == set(lines.state)

=== added file 'account_credit_control/scenarios/features/steps/account_credit_control_changer.py'
--- account_credit_control/scenarios/features/steps/account_credit_control_changer.py	1970-01-01 00:00:00 +0000
+++ account_credit_control/scenarios/features/steps/account_credit_control_changer.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+@given(u'I change level for invoice "{invoice_name}" to "{level_name}" of policy "{policy_name}"')
+def impl(ctx, invoice_name, level_name, policy_name):
+   invoice = model('account.invoice').get([('number', '=', invoice_name)])
+   assert_true(invoice, msg='No invoices found')
+   level = model('credit.control.policy.level').get([('name', '=', level_name)])
+   assert_true(level, 'level not found')
+   policy = model('credit.control.policy').get([('name', '=', policy_name)])
+   assert_true(policy, 'Policy not found')
+   assert_equal(policy.id, level.policy_id.id)
+   context = {'active_ids': [invoice.id]}
+   data = {'new_policy_id': policy.id,
+           'new_policy_level_id': level.id}
+   wizard = model('credit.control.policy.changer').create(data, context=context)
+   ctx.wizard = wizard
+
+@then(u'wizard selected move lines should be')
+def impl(ctx):
+    assert_true(ctx.wizard)
+    names = [x.name for x in ctx.wizard.move_line_ids]
+    for line in ctx.table:
+        assert_in(line['name'], names)
+
+@when(u'I confirm the level change')
+def impl(ctx):
+    assert_true(ctx.wizard)
+    ctx.wizard.set_new_policy()
+
+@when(u'I should have "{line_number:d}" credit control lines overriden')
+def impl(ctx, line_number):
+    assert_true(ctx.wizard)
+    move_ids = [x.id for x in ctx.wizard.move_line_ids]
+    overriden = model('credit.control.line').search([('move_line_id', 'in', move_ids),
+                                                     ('manually_overriden', '=', True)])
+#    assert len(overriden) == line_number
+
+@when(u'one new credit control line of level "{level_name}" related to invoice "{invoice_name}"')
+def impl(ctx, level_name, invoice_name):
+   invoice = model('account.invoice').get([('number', '=', invoice_name)])
+   assert_true(invoice, msg='No invoices found')
+   level = model('credit.control.policy.level').get([('name', '=', level_name)])
+   assert_true(level, 'level not found')
+   assert_true(ctx.wizard)
+   move_ids = [x.id for x in ctx.wizard.move_line_ids]
+   created_id = model('credit.control.line').search([('move_line_id', 'in', move_ids),
+                                                     ('manually_overriden', '=', False)])
+
+   assert len(created_id) == 1
+   created = model('credit.control.line').get(created_id[0])
+   ctx.created = created
+   assert_equal(created.policy_level_id.id, level.id)
+   assert_equal(created.invoice_id.id, invoice.id)
+   assert_equal(created.invoice_id.credit_policy_id.id, level.policy_id.id)
+
+@then(u'I force date of generated credit line to "{date}"')
+def impl(ctx, date):
+    assert_true(ctx.created)
+    ctx.created.write({'date': date})
+
+@given(u'the invoice "{invoice_name}" with manual changes')
+def impl(ctx, invoice_name):
+   invoice = model('account.invoice').get([('number', '=', invoice_name)])
+   assert_true(invoice, msg='No invoices found')
+   man_lines = (x for x in invoice.credit_control_line_ids if x.manually_overriden)
+   assert_true(next(man_lines, None), 'No manual change on the invoice')
+   ctx.invoice = invoice
+
+@given(u'the invoice has "{line_number:d}" line of level "{level:d}" for policy "{policy_name}"')
+def impl(ctx, line_number, level, policy_name):
+    assert_true(ctx.invoice)
+    policy = model('credit.control.policy').get([('name', '=', policy_name)])
+    assert_true(policy)
+    lines = model('credit.control.line').search([('invoice_id', '=', ctx.invoice.id),
+                                                 ('level', '=', level),
+                                                 ('policy_id', '=', policy.id)])
+    assert_equal(len(lines), line_number)

=== modified file 'account_credit_control/wizard/__init__.py'
--- account_credit_control/wizard/__init__.py	2012-11-07 12:08:18 +0000
+++ account_credit_control/wizard/__init__.py	2014-05-07 11:50:00 +0000
@@ -22,3 +22,4 @@
 from . import credit_control_marker
 from . import credit_control_printer
 from . import credit_control_communication
+from . import credit_control_policy_changer

=== modified file 'account_credit_control/wizard/credit_control_communication.py'
--- account_credit_control/wizard/credit_control_communication.py	2013-07-12 10:44:13 +0000
+++ account_credit_control/wizard/credit_control_communication.py	2014-05-07 11:50:00 +0000
@@ -80,14 +80,18 @@
         return cr_l_ids
 
     def _generate_comm_from_credit_line_ids(self, cr, uid, line_ids, context=None):
+        """Aggregate credit control line by partner, level, and currency
+        It also generate a communication object per aggregation.
+        """
         if not line_ids:
             return []
         comms = []
-        sql = ("SELECT distinct partner_id, policy_level_id, credit_control_policy_level.level"
+        sql = ("SELECT distinct partner_id, policy_level_id, "
+               " credit_control_line.currency_id, credit_control_policy_level.level"
                " FROM credit_control_line JOIN credit_control_policy_level "
                "   ON (credit_control_line.policy_level_id = credit_control_policy_level.id)"
                " WHERE credit_control_line.id in %s"
-               " ORDER by credit_control_policy_level.level")
+               " ORDER by credit_control_policy_level.level, credit_control_line.currency_id")
 
         cr.execute(sql, (tuple(line_ids),))
         res = cr.dictfetchall()
@@ -101,8 +105,6 @@
             data['partner_id'] = level_assoc['partner_id']
             data['current_policy_level'] = level_assoc['policy_level_id']
             comm_id = self.create(cr, uid, data, context=context)
-
-
             comms.append(self.browse(cr, uid, comm_id, context=context))
         return comms
 

=== modified file 'account_credit_control/wizard/credit_control_emailer_view.xml'
--- account_credit_control/wizard/credit_control_emailer_view.xml	2013-03-18 09:21:33 +0000
+++ account_credit_control/wizard/credit_control_emailer_view.xml	2014-05-07 11:50:00 +0000
@@ -6,15 +6,26 @@
       <field name="model">credit.control.emailer</field>
       <field name="type">form</field>
       <field name="arch" type="xml">
-        <form string="Mailer">
+        <form string="Mailer" version="7.0">
           <separator string="Send emails for the selected lines" colspan="4"/>
           <newline/>
-          <field name="line_ids" colspan="4" nolabel="1" />
+          <notebook>
+            <page string="Lines">
+              <field name="line_ids" colspan="4" nolabel="1" />
+            </page>
+          </notebook>
           <newline/>
-          <group colspan="4">
-            <button special="cancel" string="Cancel" icon='gtk-cancel'/>
-            <button name="email_lines" string="Send the emails" type="object" icon="gtk-execute"/>
-          </group>
+          <footer>
+            <button class="oe_highlight"
+                    name="email_lines"
+                    string="Send the emails"
+                    type="object"/>
+            or
+            <button
+                class="oe_link"
+                special="cancel"
+                string="Cancel"/>
+          </footer>
         </form>
       </field>
     </record>

=== modified file 'account_credit_control/wizard/credit_control_marker_view.xml'
--- account_credit_control/wizard/credit_control_marker_view.xml	2013-03-18 09:21:33 +0000
+++ account_credit_control/wizard/credit_control_marker_view.xml	2014-05-07 11:50:00 +0000
@@ -6,17 +6,33 @@
       <field name="model">credit.control.marker</field>
       <field name="type">form</field>
       <field name="arch" type="xml">
-        <form string="Lines marker">
-          <separator string="Change the state of the selected lines." colspan="4"/>
+        <form string="Lines marker" version="7.0">
+          <separator string="Change the state of the selected lines" colspan="4"/>
+          <newline/>
           <label string="Warning: you will maybe not be able to revert this operation." colspan="4"></label>
           <newline/>
-          <field name="name" colspan="4"/>
-          <field name="line_ids" colspan="4" nolabel="1"/>
-          <newline/>
-          <group colspan="4">
-            <button special="cancel" string="Cancel" icon='gtk-cancel'/>
-            <button name="mark_lines" string="Change Lines' State" type="object" icon="gtk-execute"/>
+          <group>
+            <group><field name="name"/></group>
+            <group></group>
           </group>
+          <notebook>
+            <page string="Lines">
+              <field name="line_ids" colspan="4" nolabel="1"/>
+            </page>
+          </notebook>
+          <newline/>
+          <footer>
+            <button
+                class="oe_highlight"
+                name="mark_lines"
+                string="Change Lines' State"
+                type="object"/>
+            or
+            <button
+                class="oe_link"
+                special="cancel"
+                string="Cancel"/>
+          </footer>
         </form>
       </field>
     </record>

=== added file 'account_credit_control/wizard/credit_control_policy_changer.py'
--- account_credit_control/wizard/credit_control_policy_changer.py	1970-01-01 00:00:00 +0000
+++ account_credit_control/wizard/credit_control_policy_changer.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import logging
+from openerp.tools.translate import _
+from openerp.osv import orm, fields
+logger = logging.getLogger(__name__)
+
+
+class credit_control_policy_changer(orm.TransientModel):
+    """Wizard that is run from invoices and allows to set manually a policy
+    Policy are actually apply to related move lines availabe
+    in selection widget
+
+    """
+    _name = "credit.control.policy.changer"
+    _columns = {
+        'new_policy_id': fields.many2one('credit.control.policy',
+                                         'New Policy to Apply',
+                                         required=True),
+        'new_policy_level_id': fields.many2one('credit.control.policy.level',
+                                               'New level to apply'),
+        # Only used to provide dynamic filtering on form
+        'do_nothing': fields.boolean('No follow  policy'),
+        'move_line_ids': fields.many2many('account.move.line',
+                                          rel='credit_changer_ml_rel',
+                                          string='Move line to change'),
+    }
+
+    def _get_default_lines(self, cr, uid, context=None):
+        """Get default lines for fields move_line_ids
+        of wizard. Only take lines that are on the same account
+        and move of the invoice and not reconciled
+
+        :return: list of compliant move line ids
+
+        """
+        if context is None:
+            context = {}
+        active_ids = context.get('active_ids')
+        selected_line_ids = []
+        inv_model = self.pool['account.invoice']
+        move_line_model = self.pool['account.move.line']
+        if not active_ids:
+            return False
+            # raise ValueError('No active_ids passed in context')
+        for invoice in inv_model.browse(cr, uid, active_ids, context=context):
+            if invoice.type in ('in_invoice', 'in_refund', 'out_refund'):
+                raise orm.except_orm(_('User error'),
+                                     _('Please use wizard on cutomer invoices'))
+
+            domain = [('account_id', '=', invoice.account_id.id),
+                      ('move_id', '=', invoice.move_id.id),
+                      ('reconcile_id', '=', False)]
+            move_ids = move_line_model.search(cr, uid, domain, context=context)
+            selected_line_ids.extend(move_ids)
+        return selected_line_ids
+
+    _defaults = {'move_line_ids': _get_default_lines}
+
+    def onchange_policy_id(self, cr, uid, ids, new_policy_id, context=None):
+        if not new_policy_id:
+            return {}
+        policy = self.pool['credit.control.policy'].browse(cr, uid,
+                                                           new_policy_id,
+                                                           context=context)
+        return {'value': {'do_nothing': policy.do_nothing}}
+
+    def _mark_as_overriden(self, cr, uid, move_lines, context=None):
+        """Mark `move_lines` related credit control line as overriden
+        This is done by setting manually_overriden fields to True
+
+        :param move_lines: move line to mark as overriden
+
+        :retun: list of credit line ids that where marked as overriden
+
+        """
+        credit_model = self.pool['credit.control.line']
+        domain = [('move_line_id', 'in', [x.id for x in move_lines])]
+        credits_ids = credit_model.search(cr, uid, domain, context=context)
+        credit_model.write(cr, uid,
+                           credits_ids,
+                           {'manually_overriden': True},
+                           context)
+        return credits_ids
+
+    def _set_invoice_policy(self, cr, uid, move_line_ids, policy,
+                            context=None):
+        """Force policy on invoice"""
+        invoice_model = self.pool['account.invoice']
+        invoice_ids = set([x.invoice.id for x in move_line_ids if x.invoice])
+        invoice_model.write(cr, uid, list(invoice_ids),
+                            {'credit_policy_id': policy.id},
+                            context=context)
+
+    def _check_accounts_policies(self, cr, uid, lines, policy, context=None):
+        policy_obj = self.pool['credit.control.policy']
+        for line in lines:
+            policy_obj.check_policy_against_account(
+                cr, uid,
+                line.account_id.id,
+                policy.id,
+                context=context
+            )
+        return True
+
+    def set_new_policy(self, cr, uid, wizard_id, context=None):
+        """Set new policy on an invoice.
+
+        This is done by creating a new credit control line
+        related to the move line and the policy setted in
+        the wizard form
+
+        :return: ir.actions.act_windows dict
+
+        """
+        assert len(wizard_id) == 1, "Only one id expected"
+        wizard_id = wizard_id[0]
+
+        credit_line_model = self.pool['credit.control.line']
+        ir_model = self.pool['ir.model.data']
+        ui_act_model = self.pool['ir.actions.act_window']
+        wizard = self.browse(cr, uid, wizard_id, context=context)
+        controlling_date = fields.date.today()
+        self._check_accounts_policies(
+            cr,
+            uid,
+            wizard.move_line_ids,
+            wizard.new_policy_id)
+        self._mark_as_overriden(cr,
+                                uid,
+                                wizard.move_line_ids,
+                                context=context)
+        # As disscused with business expert
+        # draft line should be passed to ignored
+        # if same level as the new one
+        # As it is a manual action
+        # We also ignore rounding tolerance
+        generated_ids = None
+        if not wizard.new_policy_id.do_nothing:
+            generated_ids = credit_line_model.create_or_update_from_mv_lines(
+                cr, uid, [],
+                [x.id for x in wizard.move_line_ids],
+                wizard.new_policy_level_id.id,
+                controlling_date,
+                check_tolerance=False,
+                context=None
+            )
+        self._set_invoice_policy(cr, uid,
+                                 wizard.move_line_ids,
+                                 wizard.new_policy_id,
+                                 context=context)
+        if not generated_ids:
+            return {}
+        view_id = ir_model.get_object_reference(cr, uid,
+                                                "account_credit_control",
+                                                "credit_control_line_action")
+        assert view_id, 'No view found'
+        action =  ui_act_model.read(cr, uid, view_id[1], context=context)
+        action['domain'] = [('id', 'in', generated_ids)]
+        return action

=== added file 'account_credit_control/wizard/credit_control_policy_changer_view.xml'
--- account_credit_control/wizard/credit_control_policy_changer_view.xml	1970-01-01 00:00:00 +0000
+++ account_credit_control/wizard/credit_control_policy_changer_view.xml	2014-05-07 11:50:00 +0000
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="credit_control_policy_changer_form" model="ir.ui.view">
+      <field name="name">credit control policy form</field>
+      <field name="model">credit.control.policy.changer</field>
+      <field name="arch" type="xml">
+        <form version="7.0" string="Set current credit level">
+          <separator string="Change the overdue level of current invoice" colspan="4"/>
+          <label string="This wizard will let you set the overdue policy and level for selected invoices"/>
+          <newline/>
+         <group>
+           <group>
+             <field name="new_policy_id"
+                    on_change="onchange_policy_id(new_policy_id)"/>
+             <field name="do_nothing"
+                    invisible="1"/>
+             <field name="new_policy_level_id"
+                    domain="[('policy_id', '=', new_policy_id)]"
+                    attrs="{'required': [('do_nothing', '=', False)]}"/>
+           </group>
+           <group></group>
+         </group>
+          <notebook colspan="4">
+            <page string="Move lines to affect">
+              <field name="move_line_ids"/>
+            </page>
+          </notebook>
+        <footer>
+            <button class="oe_highlight"
+                    name="set_new_policy"
+                    string="Set new policy"
+                    type="object"/>
+            or
+            <button class="oe_link"
+                    special="cancel"
+                    string="Cancel"/>
+        </footer>
+        </form>
+
+      </field>
+    </record>
+
+    <!-- for button -->
+    <record id="action_wizard_credit_policy_changer" model="ir.actions.act_window">
+      <field name="name">Change current credit policy</field>
+      <field name="res_model">credit.control.policy.changer</field>
+      <field name="src_model">account.invoice</field>
+      <field name="view_type">form</field>
+      <field name="view_mode">form</field>
+      <field name="view_id" ref="credit_control_policy_changer_form"/>
+      <field name="target">new</field>
+      <field name="help">Allows to manually change credit level</field>
+    </record>
+
+    <!-- for menu -->
+    <act_window name="Change current credit policy"
+                res_model="credit.control.policy.changer"
+                src_model="account.invoice"
+                view_mode="form"
+                target="new"
+                key2="client_action_multi"
+                id="action_wizard_credit_policy_changer_menu_action"/>
+
+  </data>
+</openerp>

=== modified file 'account_credit_control/wizard/credit_control_printer.py'
--- account_credit_control/wizard/credit_control_printer.py	2013-06-08 09:34:35 +0000
+++ account_credit_control/wizard/credit_control_printer.py	2014-05-07 11:50:00 +0000
@@ -43,6 +43,7 @@
         'mark_as_sent': fields.boolean('Mark letter lines as sent',
                                        help="Only letter lines will be marked."),
         'report_file': fields.binary('Generated Report', readonly=True),
+        'report_name': fields.char('Report name'),
         'state': fields.char('state', size=32),
         'line_ids': fields.many2many(
             'credit.control.line',
@@ -78,7 +79,9 @@
                                                              context=context)
         report_file = comm_obj._generate_report(cr, uid, comms, context=context)
 
-        form.write({'report_file': base64.b64encode(report_file), 'state': 'done'})
+        form.write({'report_file': base64.b64encode(report_file),
+                    'report_name': 'credit_control_esr_bvr_%s.pdf' % fields.datetime.now(),
+                    'state': 'done'})
 
         if form.mark_as_sent:
             comm_obj._mark_credit_line_as_sent(cr, uid, comms, context=context)

=== modified file 'account_credit_control/wizard/credit_control_printer_view.xml'
--- account_credit_control/wizard/credit_control_printer_view.xml	2013-03-18 09:21:33 +0000
+++ account_credit_control/wizard/credit_control_printer_view.xml	2014-05-07 11:50:00 +0000
@@ -6,21 +6,34 @@
       <field name="model">credit.control.printer</field>
       <field name="type">form</field>
       <field name="arch" type="xml">
-        <form string="Lines report">
-          <separator colspan="4" string="Print the selected lines"/>
-          <newline/>
-          <field name="mark_as_sent" colspan="4" attrs="{'invisible': [('state', '=', 'done')]}"/>
-          <newline/>
+        <form string="Lines report" version="7.0">
+          <separator string="Print the selected lines" colspan="4"/>
+          <newline/>
+          <group>
+            <field name="mark_as_sent"
+                   colspan="4"
+                   attrs="{'invisible': [('state', '=', 'done')]}"/>
+          </group>
+          <newline/>
+          <notebook>
+            <page string="Lines" attrs="{'invisible': [('state', '=', 'done')]}">
           <field name="line_ids" colspan="4" nolabel="1"
             attrs="{'invisible': [('state', '=', 'done')]}" />
-          <field name="report_file" colspan="4" attrs="{'invisible': [('state', '!=', 'done')]}"/>
+            </page>
+          </notebook>
+          <field name="report_name"
+                 invisible="1"/>
+          <field name="report_file"
+                 colspan="4"
+                 filename="report_name"
+                 attrs="{'invisible': [('state', '!=', 'done')]}"/>
           <field name="state" invisible="1" />
           <newline/>
-          <group colspan="4">
+          <footer>
+            <button class="oe_highlight" name="print_lines" string="Print" type="object"  attrs="{'invisible': [('state', '==', 'done')]}"/>
             <button special="cancel" string="Cancel" icon='gtk-cancel' attrs="{'invisible': [('state', '==', 'done')]}"/>
-            <button name="print_lines" string="Print" type="object" icon="gtk-execute" attrs="{'invisible': [('state', '==', 'done')]}"/>
             <button special="cancel" string="Close" icon='gtk-close' attrs="{'invisible': [('state', '!=', 'done')]}"/>
-          </group>
+          </footer>
         </form>
       </field>
     </record>

=== added directory 'account_credit_control_dunning_fees'
=== added file 'account_credit_control_dunning_fees/__init__.py'
--- account_credit_control_dunning_fees/__init__.py	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/__init__.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import model

=== added file 'account_credit_control_dunning_fees/__openerp__.py'
--- account_credit_control_dunning_fees/__openerp__.py	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/__openerp__.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+{'name': 'Credit control dunning fees',
+ 'version': '0.1.0',
+ 'author': 'Camptocamp',
+ 'maintainer': 'Camptocamp',
+ 'category': 'Accounting',
+ 'complexity': 'normal',
+ 'depends': ['account_credit_control'],
+ 'description': """
+Dunning Fees for Credit Control
+===============================
+
+This extention of credit control adds the notion of dunning fees
+on credit control lines.
+
+Configuration
+-------------
+For release 0.1 only fixed fees are supported.
+
+You can specifiy a fixed fees amount, a product and a currency
+on the credit control level form.
+
+The amount will be used as fees values the currency will determine
+the currency of the fee. If the credit control line has not the
+same currency as the fees currency, fees will be converted to
+the credit control line currency.
+
+The product is used to compute taxes in reconciliation process.
+
+Run
+---
+Fees are automatically computed on credit run and saved
+on the generated credit lines.
+
+Fees can be manually edited as long credit line is draft
+
+Credit control Summary report includes a new fees column.
+-------
+Support of fees price list
+
+""",
+ 'website': 'http://www.camptocamp.com',
+ 'data': ['view/policy_view.xml',
+          'view/line_view.xml',
+          'report/report.xml'],
+ 'demo': [],
+ 'test': [],
+ 'installable': True,
+ 'auto_install': False,
+ 'license': 'AGPL-3',
+ 'application': False,
+}

=== added directory 'account_credit_control_dunning_fees/i18n'
=== added file 'account_credit_control_dunning_fees/i18n/account_credit_control_dunning_fees.pot'
--- account_credit_control_dunning_fees/i18n/account_credit_control_dunning_fees.pot	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/i18n/account_credit_control_dunning_fees.pot	2014-05-07 11:50:00 +0000
@@ -0,0 +1,81 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+#	* account_credit_control_dunning_fees
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 7.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-05-07 11:44+0000\n"
+"PO-Revision-Date: 2014-05-07 11:44+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: account_credit_control_dunning_fees
+#: code:_description:0
+#: model:ir.model,name:account_credit_control_dunning_fees.model_credit_control_line
+#, python-format
+msgid "A credit control line"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: code:_description:0
+#: model:ir.model,name:account_credit_control_dunning_fees.model_credit_control_policy_level
+#, python-format
+msgid "A credit control policy level"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: code:_description:0
+#: model:ir.model,name:account_credit_control_dunning_fees.model_credit_control_run
+#, python-format
+msgid "Credit control line generator"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.line,dunning_fees_amount:0
+#: view:credit.control.policy:0
+msgid "Fees"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.policy.level,dunning_fixed_amount:0
+msgid "Fees Fixed Amount"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.policy.level,dunning_product_id:0
+msgid "Fees Product"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.policy.level,dunning_currency_id:0
+msgid "Fees currency"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: selection:credit.control.policy.level,dunning_fees_type:0
+msgid "Fixed"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: view:credit.control.policy:0
+msgid "Mail and reporting"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: code:_description:0
+#: model:ir.model,name:account_credit_control_dunning_fees.model_credit_control_dunning_fees_computer
+#, python-format
+msgid "credit.control.dunning.fees.computer"
+msgstr ""
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.policy.level,dunning_fees_type:0
+msgid "unknown"
+msgstr ""
+

=== added file 'account_credit_control_dunning_fees/i18n/fr.po'
--- account_credit_control_dunning_fees/i18n/fr.po	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/i18n/fr.po	2014-05-07 11:50:00 +0000
@@ -0,0 +1,80 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * account_credit_control_dunning_fees
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 7.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-04-16 07:11+0000\n"
+"PO-Revision-Date: 2014-04-16 07:11+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: account_credit_control_dunning_fees
+#: code:_description:0
+#: model:ir.model,name:account_credit_control_dunning_fees.model_credit_control_line
+#, python-format
+msgid "A credit control line"
+msgstr "Ligne de relance"
+
+#. module: account_credit_control_dunning_fees
+#: code:_description:0
+#: model:ir.model,name:account_credit_control_dunning_fees.model_credit_control_policy_level
+#, python-format
+msgid "A credit control policy level"
+msgstr "Une politique de relance"
+
+#. module: account_credit_control_dunning_fees
+#: code:_description:0
+#: model:ir.model,name:account_credit_control_dunning_fees.model_credit_control_run
+#, python-format
+msgid "Credit control line generator"
+msgstr "Générateur de relance"
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.line,dunning_fees_amount:0
+#: view:credit.control.policy:0
+msgid "Fees"
+msgstr "Frais de relance"
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.policy.level,dunning_fixed_amount:0
+msgid "Fees Fixed Amount"
+msgstr "Montant des frais"
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.policy.level,dunning_product_id:0
+msgid "Fees Product"
+msgstr "Article lié"
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.policy.level,dunning_currency_id:0
+msgid "Fees currency"
+msgstr "Devises"
+
+#. module: account_credit_control_dunning_fees
+#: selection:credit.control.policy.level,dunning_fees_type:0
+msgid "Fixed"
+msgstr "Fixe"
+
+#. module: account_credit_control_dunning_fees
+#: view:credit.control.policy:0
+msgid "Mail and reporting"
+msgstr "Lettres et e-mails"
+
+#. module: account_credit_control_dunning_fees
+#: code:_description:0
+#: model:ir.model,name:account_credit_control_dunning_fees.model_credit_control_dunning_fees_computer
+#, python-format
+msgid "credit.control.dunning.fees.computer"
+msgstr "credit.control.dunning.fees.computer"
+
+#. module: account_credit_control_dunning_fees
+#: field:credit.control.policy.level,dunning_fees_type:0
+msgid "unknown"
+msgstr "inconnu"

=== added directory 'account_credit_control_dunning_fees/model'
=== added file 'account_credit_control_dunning_fees/model/__init__.py'
--- account_credit_control_dunning_fees/model/__init__.py	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/model/__init__.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import line
+from . import policy
+from . import run
+from . import dunning

=== added file 'account_credit_control_dunning_fees/model/dunning.py'
--- account_credit_control_dunning_fees/model/dunning.py	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/model/dunning.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp.osv import orm
+
+
+class FeesComputer(orm.BaseModel):
+    """Model that compute dunnig fees.
+
+    This class does not need any database storage as
+    it contains pure logic.
+
+    It inherits form ``orm.BaseModel`` to benefit of orm facility
+
+    Similar to AbstractModel but log access and actions
+    """
+
+    _name = 'credit.control.dunning.fees.computer'
+    _auto = False
+    _log_access = True
+    _register = True
+    _transient = False
+
+    def _get_compute_fun(self, level_fees_type):
+        """Retrieve function of class that should compute the fees based on type
+
+        :param level_fee_type: type exisiting in model `credit.control.policy.level`
+                               for field dunning_fees_type
+
+        :returns: a function of class :class:`FeesComputer` with following signature
+                 self, cr, uid, credit_line (record), context
+
+        """
+        if level_fees_type == 'fixed':
+            return self.compute_fixed_fees
+        else:
+            raise NotImplementedError('fees type %s is not supported' % level_fees_type)
+
+    def _compute_fees(self, cr, uid, credit_line_ids, context=None):
+        """Compute fees for `credit_line_ids` parameter
+
+        Fees amount is writen on credit line in fields dunning_fees_amount
+
+        :param credit_line_ids: list of `credit.control.line` ids
+
+        :returns: `credit_line_ids` list of `credit.control.line` ids
+
+        """
+        if context is None:
+            context = {}
+        if not credit_line_ids:
+            return credit_line_ids
+        c_model = self.pool['credit.control.line']
+        credit_lines = c_model.browse(cr, uid, credit_line_ids, context=context)
+        for credit in credit_lines:
+            # if there is no dependence between generated credit lines
+            # this could be threaded
+            self._compute(cr, uid, credit, context=context),
+        return credit_line_ids
+
+    def _compute(self, cr, uid, credit_line, context=None):
+        """Compute fees for a given credit line
+
+        Fees amount is writen on credit line in fields dunning_fees_amount
+
+        :param credit_line: credit line record
+
+        :returns: `credit_line` record
+        """
+        fees_type = credit_line.policy_level_id.dunning_fees_type
+        compute = self._get_compute_fun(fees_type)
+        fees = compute(cr, uid, credit_line, context=context)
+        if fees:
+            credit_line.write({'dunning_fees_amount': fees},
+                              context=context)
+        return credit_line
+
+    def compute_fixed_fees(self, cr, uid, credit_line, context=None):
+        """Compute fees amount for fixed fees.
+        Correspond to the fixed dunning fees type
+
+        if currency of the fees is not the same as the currency
+        of the credit line, fees amount is converted to
+        currency of credit line.
+
+        :param credit_line: credit line record
+
+        :return: fees amount float (in credit line currency)
+
+        """
+        currency_model = self.pool['res.currency']
+        credit_currency = credit_line.currency_id
+        level = credit_line.policy_level_id
+        fees_amount = level.dunning_fixed_amount
+        if not fees_amount:
+            return 0.0
+        fees_currency = level.dunning_currency_id
+        if fees_currency == credit_currency:
+            return fees_amount
+        else:
+            return currency_model.compute(cr, uid, fees_currency.id,
+                                          credit_currency.id, fees_amount,
+                                          context=context)

=== added file 'account_credit_control_dunning_fees/model/line.py'
--- account_credit_control_dunning_fees/model/line.py	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/model/line.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp.osv import orm, fields
+
+
+class credit_control_line(orm.Model):
+    """Add dunning_fees_amount_fees field"""
+
+    _inherit = "credit.control.line"
+
+    _columns = {'dunning_fees_amount': fields.float('Fees')}

=== added file 'account_credit_control_dunning_fees/model/policy.py'
--- account_credit_control_dunning_fees/model/policy.py	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/model/policy.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp.osv import orm, fields
+
+
+class credit_control_policy(orm.Model):
+    """ADD dunning fees fields"""
+
+    _inherit = "credit.control.policy.level"
+    _columns = {'dunning_product_id': fields.many2one('product.product',
+                                                      'Fees Product'),
+                'dunning_fixed_amount': fields.float('Fees Fixed Amount'),
+                'dunning_currency_id': fields.many2one('res.currency',
+                                                       'Fees currency'),
+                # planned type are fixed, percent, compound
+                'dunning_fees_type': fields.selection([('fixed', 'Fixed')])}
+
+    _defaults = {'dunning_fees_type': 'fixed'}

=== added file 'account_credit_control_dunning_fees/model/run.py'
--- account_credit_control_dunning_fees/model/run.py	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/model/run.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from openerp.osv import orm, fields
+
+
+class credit_control_run(orm.Model):
+    """Add computation of fees"""
+
+    _inherit = "credit.control.run"
+
+    def  _generate_credit_lines(self, cr, uid, run_id, context=None):
+        """Override method to add fees computation"""
+        credit_line_ids = super(credit_control_run, self)._generate_credit_lines(
+            cr,
+            uid,
+            run_id,
+            context=context
+        )
+        fees_model = self.pool['credit.control.dunning.fees.computer']
+        fees_model._compute_fees(cr, uid, credit_line_ids, context=context)
+        return credit_line_ids

=== added directory 'account_credit_control_dunning_fees/report'
=== added file 'account_credit_control_dunning_fees/report/credit_control_summary.html.mako'
--- account_credit_control_dunning_fees/report/credit_control_summary.html.mako	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/report/credit_control_summary.html.mako	2014-05-07 11:50:00 +0000
@@ -0,0 +1,246 @@
+## -*- coding: utf-8 -*-
+<html>
+  <head>
+    <style type="text/css">
+      ${css}
+body {
+    font-family: helvetica;
+    font-size: 12px;
+}
+
+.custom_text {
+    font-family: helvetica;
+    font-size: 12px;
+}
+
+table {
+    font-family: helvetica;
+    font-size: 12px;
+}
+
+.header {
+    margin-left: 0px;
+    text-align: left;
+    width: 300px;
+    font-size: 12px;
+}
+
+.title {
+    font-size: 16px;
+    font-weight: bold;
+}
+
+.basic_table{
+    text-align: center;
+    border: 1px solid lightGrey;
+    border-collapse: collapse;
+    font-family: helvetica;
+    font-size: 12px;
+}
+
+.basic_table th {
+    border: 1px solid lightGrey;
+    font-size: 11px;
+    font-weight: bold;
+
+}
+
+.basic_table td {
+    border: 1px solid lightGrey;
+    font-size: 12px;
+}
+
+.list_table {
+    border-color: black;
+    text-align: center;
+    border-collapse: collapse;
+}
+
+.list_table td {
+    border-color: gray;
+    border-top: 1px solid gray;
+    text-align: left;
+    font-size: 12px;
+    padding-right: 3px;
+    padding-left: 3px;
+    padding-top: 3px;
+    padding-bottom:3px;
+}
+
+.list_table th {
+    border-bottom: 2px solid black;
+    text-align: left;
+    font-size: 11px;
+    font-weight: bold;
+    padding-right: 3px
+    padding-left: 3px
+}
+
+.list_table thead {
+    display: table-header-group;
+}
+
+.address table {
+    font-size: 11px;
+    border-collapse: collapse;
+    margin: 0px;
+    padding: 0px;
+}
+
+.address .shipping {
+
+}
+
+.address .invoice {
+    margin-top: 10px;
+}
+
+.address .recipient {
+    font-size: 13px;
+    margin-right: 120px;
+    margin-left: 350px;
+    float: right;
+}
+
+
+table .address_title {
+    font-weight: bold;
+}
+
+.address td.name {
+    font-weight: bold;
+}
+
+td.amount, th.amount {
+    text-align: right;
+    padding-right:2px;
+}
+
+h1 {
+    font-size: 16px;
+    font-weight: bold;
+}
+
+tr.line .note {
+    border-style: none;
+    font-size: 9px;
+    padding-left: 10px;
+}
+
+tr.line {
+    margin-bottom: 10px;
+}
+    </style>
+  </head>
+  <body>
+
+    %for comm in objects :
+    <% setLang(comm.get_contact_address().lang) %>
+    <div class="address">
+        <table class="recipient">
+          <%
+             add = comm.get_contact_address()
+          %>
+            %if comm.partner_id.id == add.id:
+              <tr><td class="name">${comm.partner_id.title and comm.partner_id.title.name or ''} ${comm.partner_id.name }</td></tr>
+              <% address_lines = comm.partner_id.contact_address.split("\n") %>
+
+            %else:
+              <tr><td class="name">${comm.partner_id.name or ''}</td></tr>
+              <tr><td>${add.title and add.title.name or ''} ${add.name}</td></tr>
+              <% address_lines = add.contact_address.split("\n")[1:] %>
+            %endif
+            %for part in address_lines:
+                %if part:
+                <tr><td>${part}</td></tr>
+                %endif
+            %endfor
+        </table>
+        <br/>
+        <br/>
+        <br/>
+        <br/>
+
+    </div>
+        <br/>
+        <br/>
+        <br/>
+    <div>
+
+      <h3 style="clear: both; padding-top: 20px;">
+          ${_('Reminder')}: ${comm.current_policy_level.name or '' }
+      </h3>
+
+      <p>${_('Dear')},</p>
+      <p class="custom_text" width="95%">${comm.current_policy_level.custom_text.replace('\n', '<br />')}</p>
+
+      <br/>
+      <br/>
+      <p><b>${_('Summary')}<br/></b></p>
+      <table class="basic_table" style="width: 100%;">
+      <tr>
+        <th width="200">${_('Invoice number')}</th>
+        <th>${_('Invoice date')}</th>
+        <th>${_('Date due')}</th>
+        <th>${_('Invoiced amount')}</th>
+        <th>${_('Open amount')}</th>
+        <th>${_('Fees')}</th>
+        <th>${_('Currency')}</th>
+
+      </tr>
+%for line in comm.credit_control_line_ids:
+      <tr>
+      %if line.invoice_id:
+          <td width="200">${line.invoice_id.number}
+              %if line.invoice_id.name:
+              <br/>
+              ${line.invoice_id.name}
+              %endif
+          </td>
+      %else:
+          <td width="200">${line.move_line_id.name}</td>
+      %endif
+        <td class="date">${line.date_entry}</td>
+        <td class="date">${line.date_due}</td>
+        <td class="amount">${line.amount_due}</td>
+        <td class="amount">${line.balance_due}</td>
+        <td class="amount">${line.dunning_fees_amount}</td>
+        <td class="amount">${line.currency_id.name or comm.company_id.currency_id.name}</td>
+      </tr>
+%endfor
+      </table>
+      <br/>
+      <br/>
+<%doc>
+      <!-- uncomment to have info after summary -->
+      <p>${_('If you have any question, do not hesitate to contact us.')}</p>
+
+      <p>${comm.user_id.name} ${comm.user_id.email and '<%s>'%(comm.user_id.email) or ''}<br/>
+      ${comm.company_id.name}<br/>
+      % if comm.company_id.street:
+      ${comm.company_id.street or ''}<br/>
+
+      % endif
+
+      % if comm.company_id.street2:
+      ${comm.company_id.street2}<br/>
+      % endif
+      % if comm.company_id.city or comm.company_id.zip:
+      ${comm.company_id.zip or ''} ${comm.company_id.city or ''}<br/>
+      % endif
+      % if comm.company_id.country_id:
+      ${comm.company_id.state_id and ('%s, ' % comm.company_id.state_id.name) or ''} ${comm.company_id.country_id.name or ''}<br/>
+      % endif
+      % if comm.company_id.phone:
+      Phone: ${comm.company_id.phone}<br/>
+      % endif
+      % if comm.company_id.website:
+      ${comm.company_id.website or ''}<br/>
+      % endif
+</%doc>
+
+      <p style="page-break-after:always"></p>
+    %endfor
+
+  </body>
+</html>

=== added file 'account_credit_control_dunning_fees/report/report.xml'
--- account_credit_control_dunning_fees/report/report.xml	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/report/report.xml	2014-05-07 11:50:00 +0000
@@ -0,0 +1,12 @@
+<openerp>
+  <data>
+    <report auto="False"
+            id="account_credit_control.report_webkit_html"
+            model="credit.control.communication"
+            name="credit_control_summary"
+            file="account_credit_control_dunning_fees/report/credit_control_summary.html.mako"
+            string="Credit Summary"
+            report_type="webkit"
+            webkit_header="report_webkit.ir_header_webkit_basesample0"/>
+  </data>
+</openerp>

=== added directory 'account_credit_control_dunning_fees/security'
=== added directory 'account_credit_control_dunning_fees/tests'
=== added file 'account_credit_control_dunning_fees/tests/__init__.py'
--- account_credit_control_dunning_fees/tests/__init__.py	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/tests/__init__.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import test_fees_generation
+
+checks = [test_fees_generation]

=== added file 'account_credit_control_dunning_fees/tests/test_fees_generation.py'
--- account_credit_control_dunning_fees/tests/test_fees_generation.py	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/tests/test_fees_generation.py	2014-05-07 11:50:00 +0000
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Nicolas Bessi
+#    Copyright 2014 Camptocamp SA
+#
+#    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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from mock import MagicMock
+import openerp.tests.common as test_common
+
+
+class FixedFeesTester(test_common.TransactionCase):
+
+    def setUp(self):
+        """Initialaize credit control level mock to test fees computations"""
+        super(FixedFeesTester, self).setUp()
+        self.currency_model = self.registry('res.currency')
+        self.euro = self.currency_model.search(self.cr, self.uid,
+                                               [('name', '=', 'EUR')])
+        self.assertTrue(self.euro)
+        self.euro = self.registry('res.currency').browse(self.cr,
+                                                         self.uid,
+                                                         self.euro[0])
+
+        self.usd = self.currency_model.search(self.cr, self.uid,
+                                              [('name', '=', 'USD')])
+        self.assertTrue(self.usd)
+        self.usd = self.registry('res.currency').browse(self.cr,
+                                                        self.uid,
+                                                        self.usd[0])
+
+        self.euro_level = MagicMock(name='Euro policy level')
+        self.euro_level.dunning_fixed_amount = 5.0
+        self.euro_level.dunning_currency_id = self.euro
+        self.euro_level.dunning_type = 'fixed'
+
+        self.usd_level = MagicMock(name='Euro policy level')
+        self.usd_level.dunning_fixed_amount = 5.0
+        self.usd_level.dunning_currency_id = self.usd
+        self.usd_level.dunning_type = 'fixed'
+        self.dunning_model = self.registry('credit.control.dunning.fees.computer')
+
+    def test_type_getter(self):
+        """Test that correct compute function is returned for "fixed" type"""
+        c_fun = self.dunning_model._get_compute_fun('fixed')
+        self.assertEqual(c_fun, self.dunning_model.compute_fixed_fees)
+
+    def test_unknow_type(self):
+        """Test that non implemented error is raised if invalide fees type"""
+        with self.assertRaises(NotImplementedError):
+            self.dunning_model._get_compute_fun('bang')
+
+    def test_computation_same_currency(self):
+        """Test that fees are correctly computed with same currency"""
+        credit_line = MagicMock(name='Euro credit line')
+        credit_line.policy_level_id = self.euro_level
+        credit_line.currency_id = self.euro
+        fees = self.dunning_model.compute_fixed_fees(self.cr, self.uid,
+                                                     credit_line,
+                                                     {})
+        self.assertEqual(fees, self.euro_level.dunning_fixed_amount)
+
+    def test_computation_different_currency(self):
+        """Test that fees are correctly computed with different currency"""
+        credit_line = MagicMock(name='USD credit line')
+        credit_line.policy_level_id = self.euro_level
+        credit_line.currency_id = self.usd
+        fees = self.dunning_model.compute_fixed_fees(self.cr, self.uid,
+                                                     credit_line,
+                                                     {})
+        self.assertNotEqual(fees, self.euro_level.dunning_fixed_amount)
+
+    def test_no_fees(self):
+        """Test that fees are not generated if no amount defined on level"""
+        credit_line = MagicMock(name='USD credit line')
+        credit_line.policy_level_id = self.euro_level
+        self.euro_level.dunning_fixed_amount = 0.0
+        credit_line.currency_id = self.usd
+        fees = self.dunning_model.compute_fixed_fees(self.cr, self.uid,
+                                                     credit_line,
+                                                     {})
+        self.assertEqual(fees, 0.0)

=== added directory 'account_credit_control_dunning_fees/view'
=== added file 'account_credit_control_dunning_fees/view/line_view.xml'
--- account_credit_control_dunning_fees/view/line_view.xml	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/view/line_view.xml	2014-05-07 11:50:00 +0000
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+
+    <record id="add_fees_on_credit_control_line" model="ir.ui.view">
+      <field name="name">add fees on credit control line</field>
+      <field name="model">credit.control.line</field>
+      <field name="inherit_id" ref="account_credit_control.credit_control_line_tree" />
+      <field name="arch" type="xml">
+        <field name="balance_due" position="after">
+          <field name="dunning_fees_amount"
+                 attrs="{'readonly': [('state', '!=', 'draft')]}"/>
+        </field>
+      </field>
+    </record>
+
+    <record id="add_fees_on_credit_control_line_from" model="ir.ui.view">
+      <field name="name">add fees on credit control line form</field>
+      <field name="model">credit.control.line</field>
+      <field name="inherit_id" ref="account_credit_control.credit_control_line_form"/>
+      <field name="arch" type="xml">
+        <field name="balance_due" position="after">
+          <field name="dunning_fees_amount"
+                 attrs="{'readonly': [('state', '!=', 'draft')]}"/>
+        </field>
+      </field>
+    </record>
+
+  </data>
+</openerp>

=== added file 'account_credit_control_dunning_fees/view/policy_view.xml'
--- account_credit_control_dunning_fees/view/policy_view.xml	1970-01-01 00:00:00 +0000
+++ account_credit_control_dunning_fees/view/policy_view.xml	2014-05-07 11:50:00 +0000
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+    <record id="add_dunning_fees_on_policy" model="ir.ui.view">
+      <field name="name">add dunning fees on policy</field>
+      <field name="model">credit.control.policy</field>
+      <field name="inherit_id" ref="account_credit_control.credit_control_policy_form" />
+      <field name="arch" type="xml">
+        <page string="Mail and reporting" position="after">
+          <page string="Fees">
+            <group>
+              <group>
+                <field name="dunning_fixed_amount"/>
+                <field name="dunning_product_id"
+                       attrs="{'required': [('dunning_fixed_amount', '!=', False)]}"/>
+                <field name="dunning_currency_id"
+                       attrs="{'required': [('dunning_fixed_amount', '!=', False)]}"/>
+              </group>
+            </group>
+          </page>
+        </page>
+      </field>
+    </record>
+
+  </data>
+</openerp>


Follow ups