diff --git a/account_credit_control/__init__.py b/account_credit_control/__init__.py
new file mode 100644
index 000000000..40d22fe09
--- /dev/null
+++ b/account_credit_control/__init__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+from . import run
+from . import line
+from . import account
+from . import partner
+from . import policy
+from . import company
+import wizard
+import report
diff --git a/account_credit_control/__openerp__.py b/account_credit_control/__openerp__.py
new file mode 100644
index 000000000..f22c22db9
--- /dev/null
+++ b/account_credit_control/__openerp__.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+{'name' : 'Account Credit Control',
+ 'version' : '0.1',
+ 'author' : 'Camptocamp',
+ 'maintainer': 'Camptocamp',
+ 'category': 'Finance',
+ 'complexity': "normal",
+ 'depends' : ['base', 'account', 'email_template', 'report_webkit'],
+ 'description': """
+Credit Control
+==============
+
+Configuration
+-------------
+
+Configure the policies and policy levels in ``Accounting > Configuration >
+Credit Control > Credit Policies``.
+You can define as many policy levels as you need.
+
+Usage
+-----
+
+Menu entries are located in ``Accounting > Periodical Processing > Credit
+Control``.
+
+Create a new "run" in the ``Credit Control Run`` menu with the controlling date.
+Then, use the ``Compute credit lines`` button. All the credit control lines will
+be generated. You can find them in the ``Credit Control Lines`` menu.
+
+On each generated line, you have many choices:
+ * Send a email
+ * Print a letter
+ * Change the state (so you can ignore or reopen lines)
+ """,
+ 'website': 'http://www.camptocamp.com',
+ 'init_xml': ["data.xml"],
+ 'update_xml': ["line_view.xml",
+ "account_view.xml",
+ "partner_view.xml",
+ "policy_view.xml",
+ "run_view.xml",
+ "company_view.xml",
+ "wizard/credit_control_mailer_view.xml",
+ "wizard/credit_control_marker_view.xml",
+ "wizard/credit_control_printer_view.xml",
+ "report/report.xml",
+ "security/ir.model.access.csv",
+ ],
+ 'demo_xml': ["credit_control_demo.xml"],
+ 'tests': [],
+ 'installable': True,
+ 'license': 'AGPL-3',
+ 'application': True
+}
+
diff --git a/account_credit_control/account.py b/account_credit_control/account.py
new file mode 100644
index 000000000..b71928fab
--- /dev/null
+++ b/account_credit_control/account.py
@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+from datetime import datetime
+import operator
+from openerp.osv.orm import Model, fields
+from openerp.tools.translate import _
+from openerp.addons.account_credit_control import run
+
+class AccountAccount(Model):
+ """Add a link to a credit control policy on account account"""
+
+
+ def _check_account_type_compatibility(self, cursor, uid, acc_ids, context=None):
+ """We check that account is of type reconcile"""
+ if not isinstance(acc_ids, list):
+ acc_ids = [acc_ids]
+ for acc in self.browse(cursor, uid, acc_ids, context):
+ if acc.credit_policy_id and not acc.reconcile:
+ return False
+ return True
+
+ _inherit = "account.account"
+ _description = """Add a link to a credit policy"""
+ _columns = {'credit_policy_id': fields.many2one('credit.control.policy',
+ 'Credit control policy',
+ help=("Define global credit policy"
+ "order is account partner invoice")),
+
+ 'credit_control_line_ids': fields.one2many('credit.control.line',
+ 'account_id',
+ string='Credit Lines',
+ readonly=True)}
+
+ _constraints = [(_check_account_type_compatibility,
+ _('You can not set a credit policy on a non reconciliable account'),
+ ['credit_policy_id'])]
+
+class AccountInvoice(Model):
+ """Add a link to a credit control policy on account account"""
+
+ _inherit = "account.invoice"
+ _description = """Add a link to a credit policy"""
+ _columns = {'credit_policy_id': fields.many2one('credit.control.policy',
+ 'Credit control policy',
+ help=("Define global credit policy"
+ "order is account partner invoice")),
+
+ 'credit_control_line_ids': fields.one2many('credit.control.line',
+ 'account_id',
+ string='Credit Lines',
+ readonly=True)}
+
+ def action_move_create(self, cursor, uid, ids, context=None):
+ """We ensure writing of invoice id in move line because
+ Trigger field may not work without account_voucher addon"""
+ res = super(AccountInvoice, self).action_move_create(cursor, uid, ids, context=context)
+ for inv in self.browse(cursor, uid, ids, context=context):
+ if inv.move_id:
+ for line in inv.move_id.line_id:
+ line.write({'invoice_id': inv.id})
+ return res
+
+
+class AccountMoveLine(Model):
+ """Add a function that compute the residual amount using a follow up date
+ Add relation between move line and invoicex"""
+
+ _inherit = "account.move.line"
+ # Store fields has strange behavior with voucher module we had to overwrite invoice
+
+
+ # def _invoice_id(self, cursor, user, ids, name, arg, context=None):
+ # #Code taken from OpenERP account addon
+ # invoice_obj = self.pool.get('account.invoice')
+ # res = {}
+ # for line_id in ids:
+ # res[line_id] = False
+ # cursor.execute('SELECT l.id, i.id ' \
+ # 'FROM account_move_line l, account_invoice i ' \
+ # 'WHERE l.move_id = i.move_id ' \
+ # 'AND l.id IN %s',
+ # (tuple(ids),))
+ # invoice_ids = []
+ # for line_id, invoice_id in cursor.fetchall():
+ # res[line_id] = invoice_id
+ # invoice_ids.append(invoice_id)
+ # invoice_names = {False: ''}
+ # for invoice_id, name in invoice_obj.name_get(cursor, user, invoice_ids, context=context):
+ # invoice_names[invoice_id] = name
+ # for line_id in res.keys():
+ # invoice_id = res[line_id]
+ # res[line_id] = (invoice_id, invoice_names[invoice_id])
+ # return res
+
+ # def _get_invoice(self, cursor, uid, ids, context=None):
+ # result = set()
+ # for line in self.pool.get('account.invoice').browse(cursor, uid, ids, context=context):
+ # if line.move_id:
+ # ids = [x.id for x in line.move_id.line_id or []]
+ # return list(result)
+
+ # _columns = {'invoice_id': fields.function(_invoice_id, string='Invoice',
+ # type='many2one', relation='account.invoice',
+ # store={'account.invoice': (_get_invoice, ['move_id'], 20)})}
+
+ _columns = {'invoice_id': fields.many2one('account.invoice', 'Invoice')}
+
+ def _get_payment_and_credit_lines(self, moveline_array, lookup_date):
+ credit_lines = []
+ payment_lines = []
+ for line in moveline_array:
+ if self._should_exlude_line(line):
+ continue
+ if line.account_id.type == 'receivable' and line.debit:
+ credit_lines.append(line)
+ else:
+ if line.reconcile_partial_id:
+ payment_lines.append(line)
+ credit_lines.sort(key=operator.attrgetter('date'))
+ payment_lines.sort(key=operator.attrgetter('date'))
+ return (credit_lines, payment_lines)
+
+ def _validate_line_currencies(self, credit_lines):
+ """Raise an excpetion if there is lines with different currency"""
+ if len(credit_lines) == 0:
+ return True
+ currency = credit_lines[0].currency_id.id
+ if not all(obj.currency_id.id == currency for obj in credit_lines):
+ raise Exception('Not all line of move line are in the same currency')
+
+ def _get_value_amount(self, mv_line_br):
+ if mv_line_br.currency_id:
+ return mv_line_br.amount_currency
+ else:
+ return mv_line_br.debit - mv_line_br.credit
+
+ def _validate_partial(self, credit_lines):
+ if len(credit_lines) == 0:
+ return True
+ else:
+ line_with_partial = 0
+ for line in credit_lines:
+ if not line.reconcile_partial_id:
+ line_with_partial += 1
+ if line_with_partial and line_with_partial != len(credit_lines):
+ raise Exception('Can not compute credit line if multiple'
+ ' lines are not all linked to a partial')
+
+ def _get_applicable_payment_lines(self, credit_line, payment_lines):
+ applicable_payment = []
+ for pay_line in payment_lines:
+ if datetime.strptime(pay_line.date, "%Y-%m-%d").date() \
+ <= datetime.strptime(credit_line.date, "%Y-%m-%d").date():
+ applicable_payment.append(pay_line)
+ return applicable_payment
+
+ def _compute_partial_reconcile_residual(self, move_lines, lookup_date, move_id, memoizer):
+ """ Compute open amount of multiple credit lines linked to multiple payment lines"""
+ credit_lines, payment_lines = self._get_payment_and_credit_lines(move_lines, lookup_date, memoizer)
+ self._validate_line_currencies(credit_lines)
+ self._validate_line_currencies(payment_lines)
+ self._validate_partial(credit_lines)
+ # memoizer structure move_id : {move_line_id: open_amount}
+ # paymnent line and credit line are sorted by date
+ rest = 0.0
+ for credit_line in credit_lines:
+ applicable_payment = self._get_applicable_payment_lines(credit_line, payment_lines)
+ paid_amount = 0.0
+ for pay_line in applicable_payment:
+ paid_amount += self._get_value_amount(pay_line)
+ balance_amount = self._get_value_amount(credit_lines) - (paid_amount + rest)
+ memoizer[move_id][credit_line.id] = balance_amount
+ if balance_amount < 0.0:
+ rest = balance_amount
+ else:
+ rest = 0.0
+ return memoizer
+
+ def _compute_fully_open_amount(self, move_lines, lookup_date, move_id, memoizer):
+ for move_line in move_lines:
+ memoizer[move_id][move_line.id] = self._get_value_amount(move_line)
+ return memoizer
+
+
+ def _amount_residual_from_date(self, cursor, uid, mv_line_br, lookup_date, context=None):
+ """
+ Code from function _amount_residual of account/account_move_line.py does not take
+ in account mulitple line payment and reconciliation. We have to rewrite it
+ Code computes residual amount at lookup date for mv_line_br in entry
+ """
+ memoizer = run.memoizers['credit_line_residuals']
+ move_id = mv_line_br.move_id.id
+ if mv_line_br.move_id.id in memoizer:
+ pass # get back value
+ else:
+ memoizer[move_id] = {}
+ move_lines = mv_line_br.move_id.line_id
+ if mv_line_br.reconcile_partial_id:
+ self._compute_partial_reconcile_residual(move_lines, lookup_date, move_id, memoizer)
+ else:
+ self._compute_fully_open_amount(move_lines, lookup_date, move_id, memoizer)
+ return memoizer[move_id][mv_line_br.id]
+
diff --git a/account_credit_control/account_view.xml b/account_credit_control/account_view.xml
new file mode 100644
index 000000000..fa68c94b2
--- /dev/null
+++ b/account_credit_control/account_view.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ account.followup.form.view
+ account.account
+
+ form
+
+
+
+
+
+
+
+
+
+
+ invoice.followup.form.view
+ account.invoice
+
+ form
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/account_credit_control/company.py b/account_credit_control/company.py
new file mode 100644
index 000000000..632ff95e7
--- /dev/null
+++ b/account_credit_control/company.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+from openerp.osv.orm import Model, fields
+
+class ResCompany(Model):
+ _inherit = "res.company"
+
+ _columns = {"credit_control_tolerance": fields.float('Credit Tolerance')}
+
+ _defaults = {"credit_control_tolerance": 0.1}
diff --git a/account_credit_control/company_view.xml b/account_credit_control/company_view.xml
new file mode 100644
index 000000000..2adb3f185
--- /dev/null
+++ b/account_credit_control/company_view.xml
@@ -0,0 +1,15 @@
+
+
+
+ credit.control.company.form
+ res.company
+ form
+
+
+
+
+
+
+
+
+
diff --git a/account_credit_control/credit_control_demo.xml b/account_credit_control/credit_control_demo.xml
new file mode 100644
index 000000000..96734fbea
--- /dev/null
+++ b/account_credit_control/credit_control_demo.xml
@@ -0,0 +1,33 @@
+
+
+
+ X11002-a
+ B2B Debtors - (test)
+
+ receivable
+
+
+
+
+
+
+ X11002-b
+ B2C Debtors - (test)
+
+ receivable
+
+
+
+
+
+
+ X11002-c
+ New Debtors - (test)
+
+ receivable
+
+
+
+
+
+
diff --git a/account_credit_control/data.xml b/account_credit_control/data.xml
new file mode 100644
index 000000000..30ce5b0ca
--- /dev/null
+++ b/account_credit_control/data.xml
@@ -0,0 +1,173 @@
+
+
+
+
+ Credit Control demo mail
+ noreply@localhost
+ Credit Control Invoice (${object.current_policy_level.level or 'n/a' })
+ ${object.get_mail() or ''}
+
+
+
+ %if mode != 'pdf':
+
+
+ %endif
+
+
+
Dear ${object.partner_id.name or ''},
+
+
${object.current_policy_level.level.custom_text}
+
+
+
Summary
+
+
date due
+
Amount due
+
Amount balance
+
Invoice number
+
+%for line in object.credit_lines:
+
+
${line.date_due}
+
${line.amount_due}
+
${line.balance_due}
+ %if line.invoice_id:
+
${line.invoice_id.number}
+ %else:
+
n/a
+ %endif
+%endfor
+
+
+
+
+
If you have any question, do not hesitate to contact us.
+
+
+
Thank you for choosing ${object.company_id.name}!
+
+ -- more info here --
+
${object.user_id.name} ${object.user_id.user_email and '<%s>'%(object.user_id.user_email) or ''}
+ ${object.company_id.name}
+ % if object.company_id.street:
+ ${object.company_id.street or ''}
+
+ % endif
+
+ % if object.company_id.street2:
+ ${object.company_id.street2}
+ % endif
+ % if object.company_id.city or object.company_id.zip:
+ ${object.company_id.zip or ''} ${object.company_id.city or ''}
+ % endif
+ % if object.company_id.country_id:
+ ${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}
+ % endif
+ % if object.company_id.phone:
+ Phone: ${object.company_id.phone}
+ % endif
+ % if object.company_id.website:
+ ${object.company_id.website or ''}
+ % endif
+ ]]>
+
+
+
+
+ No follow
+
+
+
+
+
+ 3 time policy
+
+
+
+ 10 days net
+
+ net_days
+
+
+
+ mail
+ Replace this text in level by your message
+
+
+
+ 30 days end of month
+
+ end_of_month
+
+
+
+ mail
+ Replace this text in level by your message
+
+
+
+ 10 days sommation
+
+ previous_date
+
+
+
+ manual
+ Replace this text in level by your message
+
+
+
+
+ 2 time policy
+
+
+
+ 30 days end of month
+
+ end_of_month
+
+
+
+ mail
+ Replace this text in level by your message
+
+
+
+ 60 days sommation
+
+ previous_date
+
+
+
+ manual
+ Replace this text in level by your message
+
+
+
+ Credit Control Manager
+
+
+
+
+ Credit Control User
+
+
+
+
+ Credit Control Info
+
+
+
+
+
diff --git a/account_credit_control/line.py b/account_credit_control/line.py
new file mode 100644
index 000000000..db5486833
--- /dev/null
+++ b/account_credit_control/line.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+import logging
+
+from openerp.osv.orm import Model, fields
+import pooler
+#from datetime import datetime
+
+logger = logging.getLogger('credit.line.control')
+
+class CreditControlLine (Model):
+ """A credit control line decribe a line of amount due by a customer.
+ It is linked to all required financial account.
+ It has various state draft open to be send send. For more information about
+ usage please read __openerp__.py file"""
+
+ _name = "credit.control.line"
+ _description = """A credit control line"""
+ _rec_name = "id"
+
+ _columns = {'date': fields.date('Controlling date', required=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',
+ required=True,
+ readonly=True,
+ states={'draft': [('readonly', False)]}),
+
+ 'date_sent': fields.date('Sent date',
+ readonly=True,
+ states={'draft': [('readonly', False)]}),
+
+ 'state': fields.selection([('draft', 'Draft'),
+ ('to_be_sent', 'To be sent'),
+ ('sent', 'Done'),
+ ('error', 'Error'),
+ ('mail_error', 'Mailing Error')],
+ 'State', required=True, readonly=True),
+
+ 'canal': fields.selection([('manual', 'Manual'),
+ ('mail', 'Mail')],
+ 'Canal', required=True,
+ readonly=True,
+ states={'draft': [('readonly', False)]}),
+
+ 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
+ 'partner_id': fields.many2one('res.partner', "Partner", required=True),
+ 'amount_due': fields.float('Due Amount Tax inc.', required=True, readonly=True),
+ 'balance_due': fields.float('Due balance', required=True, readonly=True),
+ 'mail_message_id': fields.many2one('mail.message', 'Sent mail', readonly=True),
+
+ 'move_line_id': fields.many2one('account.move.line', 'Move line',
+ required=True, readonly=True),
+
+ 'account_id': fields.related('move_line_id', 'account_id', type='many2one',
+ relation='account.account', string='Account',
+ store=True, readonly=True),
+
+ 'currency_id': fields.related('move_line_id', 'currency_id', type='many2one',
+ relation='res.currency', string='Currency',
+ store=True, readonly=True),
+
+ 'company_id': fields.related('move_line_id', 'company_id', type='many2one',
+ relation='res.company', string='Company',
+ store=True, readonly=True),
+
+ # we can allow a manual change of policy in draft state
+ 'policy_level_id':fields.many2one('credit.control.policy.level',
+ 'Overdue Level', required=True, readonly=True,
+ states={'draft': [('readonly', False)]}),
+
+ 'policy_id': fields.related('policy_level_id',
+ 'policy_id',
+ type='many2one',
+ relation='credit.control.policy',
+ string='Policy',
+ store=True,
+ readonly=True),
+
+ 'level': fields.related('policy_level_id',
+ 'level',
+ type='float',
+ relation='credit.control.policy',
+ string='Level',
+ store=True,
+ readonly=True),}
+
+
+ _defaults = {'state': 'draft'}
+
+ def _update_from_mv_line(self, cursor, uid, ids, mv_line_br, level,
+ lookup_date, context=None):
+ """hook function to update line if required"""
+ context = context or {}
+ return []
+
+ def _create_from_mv_line(self, cursor, uid, ids, mv_line_br,
+ level, lookup_date, context=None):
+ """Create credit line"""
+ acc_line_obj = self.pool.get('account.move.line')
+ context = context or {}
+ data_dict = {}
+ data_dict['date'] = lookup_date
+ data_dict['date_due'] = mv_line_br.date_maturity
+ data_dict['state'] = 'draft'
+ data_dict['canal'] = level.canal
+ data_dict['invoice_id'] = (mv_line_br.invoice_id and mv_line_br.invoice_id.id
+ or False)
+ data_dict['partner_id'] = mv_line_br.partner_id.id
+ data_dict['amount_due'] = (mv_line_br.amount_currency or mv_line_br.debit
+ or mv_line_br.credit)
+ data_dict['balance_due'] = acc_line_obj._amount_residual_from_date(cursor, uid, mv_line_br,
+ lookup_date, context=context)
+ data_dict['policy_level_id'] = level.id
+ data_dict['company_id'] = mv_line_br.company_id.id
+ data_dict['move_line_id'] = mv_line_br.id
+ return [self.create(cursor, uid, data_dict)]
+
+
+ def create_or_update_from_mv_lines(self, cursor, uid, ids, lines,
+ level_id, lookup_date, errors=None, context=None):
+ """Create or update line base on levels"""
+ context = context or {}
+ currency_obj = self.pool.get('res.currency')
+ level_obj = self.pool.get('credit.control.policy.level')
+ ml_obj = self.pool.get('account.move.line')
+ level = level_obj.browse(cursor, uid, level_id, context)
+ current_lvl = level.level
+ credit_line_ids = []
+ user = self.pool.get('res.users').browse(cursor, uid, uid)
+ tolerance_base = user.company_id.credit_control_tolerance
+ tolerance = {}
+ currency_ids = currency_obj.search(cursor, uid, [])
+
+ acc_line_obj = self.pool.get('account.move.line')
+ for c_id in currency_ids:
+ tmp = currency_obj.compute(cursor, uid, c_id,
+ user.company_id.currency_id.id, tolerance_base)
+ tolerance[c_id] = tmp
+
+ existings = self.search(cursor, uid, [('move_line_id', 'in', lines),
+ ('level', '=', current_lvl)])
+ db, pool = pooler.get_db_and_pool(cursor.dbname)
+ for line in ml_obj.browse(cursor, uid, lines, context):
+ # we want to create as many line as possible
+ local_cr = db.cursor()
+ try:
+ if line.id in existings:
+ # does nothing just a hook
+ credit_line_ids += self._update_from_mv_line(local_cr, uid, ids,
+ line, level, lookup_date,
+ context=context)
+ else:
+ # as we use memoizer pattern this has almost no cost to get it
+ # multiple time
+ open_amount = acc_line_obj._amount_residual_from_date(cursor, uid, line,
+ lookup_date, context=context)
+
+ if open_amount > tolerance.get(line.currency_id.id, tolerance_base):
+ credit_line_ids += self._create_from_mv_line(local_cr, uid, ids,
+ line, level, lookup_date,
+ context=context)
+ except Exception, exc:
+ logger.error(exc)
+ if errors:
+ errors.append(unicode(exc)) #obj-c common pattern
+ local_cr.rollback()
+ finally:
+ local_cr.commit()
+ local_cr.close()
+ return credit_line_ids
diff --git a/account_credit_control/line_view.xml b/account_credit_control/line_view.xml
new file mode 100644
index 000000000..8c68366d0
--- /dev/null
+++ b/account_credit_control/line_view.xml
@@ -0,0 +1,145 @@
+
+
+
+ credit.control.line.form
+ credit.control.line
+ form
+
+
+
+
+
+
+ Credit Control Lines
+ credit.control.lines
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ credit.control.line.tree
+ credit.control.line
+ tree
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Credit Control Lines
+ ir.actions.act_window
+ credit.control.line
+
+ form
+ tree,form
+
+
+
+
+
+
+
+
+
+
diff --git a/account_credit_control/partner.py b/account_credit_control/partner.py
new file mode 100644
index 000000000..5938a3891
--- /dev/null
+++ b/account_credit_control/partner.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+from openerp.osv.orm import Model, fields
+
+
+class ResPartner(Model):
+ """Add a settings on the credit control policy to use on the partners,
+ and links to the credit control lines."""
+
+ _inherit = "res.partner"
+ _columns = {
+ 'credit_policy_id': fields.many2one('credit.control.policy',
+ 'Credit Control Policy',
+ help=("The Credit Control policy"
+ "used for this user. This "
+ "setting can be forced on the "
+ "invoice. If nothing is defined, "
+ "it will use the account "
+ "setting.")),
+ 'credit_control_line_ids': fields.one2many('credit.control.line',
+ 'invoice_id',
+ string='Credit Lines',
+ readonly=True)
+ }
+
diff --git a/account_credit_control/partner_view.xml b/account_credit_control/partner_view.xml
new file mode 100644
index 000000000..5ac108447
--- /dev/null
+++ b/account_credit_control/partner_view.xml
@@ -0,0 +1,25 @@
+
+
+
+ partner.credit_control.form.view
+ res.partner
+
+ form
+
+
+
+
+
+
+
+
+
+
+
diff --git a/account_credit_control/policy.py b/account_credit_control/policy.py
new file mode 100644
index 000000000..2c10dd2f4
--- /dev/null
+++ b/account_credit_control/policy.py
@@ -0,0 +1,310 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+from openerp.osv.orm import Model, fields
+from openerp.tools.translate import _
+
+class CreditControlPolicy(Model):
+ """Define a policy of reminder"""
+
+ _name = "credit.control.policy"
+ _description = """Define a reminder policy"""
+ _columns = {'name': fields.char('Name', required=True, size=128),
+
+ 'level_ids' : fields.one2many('credit.control.policy.level',
+ 'policy_id',
+ 'Policy Levels'),
+
+ 'do_nothing' : fields.boolean('Do nothing',
+ help=('For policies who should not '
+ 'generate lines or are obsolete')),
+
+ 'company_id' : fields.many2one('res.company', 'Company')
+ }
+
+
+ def _get_account_related_lines(self, cursor, uid, policy_id, lookup_date, lines, context=None):
+ """ We get all the lines related to accounts with given credit policy.
+ We try not to use direct SQL in order to respect security rules.
+ As we define the first set it is important, The date is used to do a prefilter.
+ !!!We take the asumption that only receivable lines have a maturity date
+ and account must be reconcillable"""
+ context = context or {}
+ move_l_obj = self.pool.get('account.move.line')
+ account_obj = self.pool.get('account.account')
+ acc_ids = account_obj.search(cursor, uid, [('credit_policy_id', '=', policy_id)])
+ if not acc_ids:
+ return lines
+ move_ids = move_l_obj.search(cursor, uid, [('account_id', 'in', acc_ids),
+ ('date_maturity', '<=', lookup_date),
+ ('reconcile_id', '=', False),
+ ('partner_id', '!=', False)])
+
+ lines += move_ids
+ return lines
+
+
+ def _get_sum_reduce_range(self, cursor, uid, policy_id, lookup_date, lines, model,
+ move_relation_field, context=None):
+ """ We get all the lines related to the model with given credit policy.
+ We also reduce from the global set (lines) the move line to be excluded.
+ We try not to use direct SQL in order to respect security rules.
+ As we define the first set it is important.
+ The policy relation field MUST be named credit_policy_id
+ and the model must have a relation
+ with account move line.
+ !!! We take the asumption that only receivable lines have a maturity date
+ and account must be reconcillable"""
+ # MARK possible place for a good optimisation
+ context = context or {}
+ my_obj = self.pool.get(model)
+ move_l_obj = self.pool.get('account.move.line')
+ add_obj_ids = my_obj.search(cursor, uid, [('credit_policy_id', '=', policy_id)])
+ if add_obj_ids:
+ add_lines = move_l_obj.search(cursor, uid, [(move_relation_field, 'in', add_obj_ids),
+ ('date_maturity', '<=', lookup_date),
+ ('partner_id', '!=', False),
+ ('reconcile_id', '=', False)])
+ lines = list(set(lines + add_lines))
+ # we get all the lines that must be excluded at partner_level
+ # from the global set (even the one included at account level)
+ neg_obj_ids = my_obj.search(cursor, uid, [('credit_policy_id', '!=', policy_id),
+ ('credit_policy_id', '!=', False)])
+ if neg_obj_ids:
+ # should we add ('id', 'in', lines) in domain ? it may give a veeery long SQL...
+ neg_lines = move_l_obj.search(cursor, uid, [(move_relation_field, 'in', neg_obj_ids),
+ ('date_maturity', '<=', lookup_date),
+ ('partner_id', '!=', False),
+ ('reconcile_id', '=', False)])
+ if neg_lines:
+ lines = list(set(lines) - set(neg_lines))
+ return lines
+
+
+ def _get_partner_related_lines(self, cursor, uid, policy_id, lookup_date, lines, context=None):
+ return self._get_sum_reduce_range(cursor, uid, policy_id, lookup_date, lines,
+ 'res.partner', 'partner_id', context=context)
+
+
+ def _get_invoice_related_lines(self, cursor, uid, policy_id, lookup_date, lines, context=None):
+ return self._get_sum_reduce_range(cursor, uid, policy_id, lookup_date, lines,
+ 'account.invoice', 'invoice', context=context)
+
+
+ def _get_moves_line_to_process(self, cursor, uid, policy_id, lookup_date, context=None):
+ """Retrive all the move line to be procces for current policy.
+ This function is planned to be use only on one id.
+ Priority of inclustion, exlusion is account, partner, invoice"""
+ context = context or {}
+ lines = []
+ if isinstance(policy_id, list):
+ policy_id = policy_id[0]
+ # order of call MUST be respected priority is account, partner, invoice
+ lines = self._get_account_related_lines(cursor, uid, policy_id,
+ lookup_date, lines, context=context)
+ lines = self._get_partner_related_lines(cursor, uid, policy_id,
+ lookup_date, lines, context=context)
+ lines = self._get_invoice_related_lines(cursor, uid, policy_id,
+ lookup_date, lines, context=context)
+ return lines
+
+ def _check_lines_policies(self, cursor, uid, policy_id, lines, context=None):
+ """ Check if there is credit line related to same move line but
+ related to an other policy"""
+ context = context or {}
+ if not lines:
+ return []
+ if isinstance(policy_id, list):
+ policy_id = policy_id[0]
+ cursor.execute("SELECT move_line_id FROM credit_control_line"
+ " WHERE policy_id != %s and move_line_id in %s",
+ (policy_id, tuple(lines)))
+ res = cursor.fetchall()
+ if res:
+ return [x[0] for x in res]
+ else:
+ return []
+
+
+
+class CreditControlPolicyLevel(Model):
+ """Define a policy level. A level allows to determine if
+ a move line is due and the level of overdue of the line"""
+
+ _name = "credit.control.policy.level"
+ _order = 'level'
+ _description = """A credit control policy level"""
+ _columns = {'policy_id': fields.many2one('credit.control.policy',
+ 'Related Policy', required=True),
+ 'name': fields.char('Name', size=128, required=True),
+ 'level': fields.float('level', required=True),
+
+ 'computation_mode': fields.selection([('net_days', 'Due date'),
+ ('end_of_month', 'Due Date: end of Month'),
+ ('previous_date', 'Previous reminder')],
+ 'Compute mode',
+ required=True),
+
+ 'delay_days': fields.integer('Delay in day', required='True'),
+ 'mail_template_id': fields.many2one('email.template', 'Mail template',
+ required=True),
+ 'canal': fields.selection([('manual', 'Manual'),
+ ('mail', 'Mail')],
+ 'Canal', required=True),
+ 'custom_text': fields.text('Custom message', required=True, translate=True),
+ }
+
+
+ def _check_level_mode(self, cursor, uid, rids, context=None):
+ """We check that the smallest level is not based
+ on a level using previous_date mode"""
+ if not isinstance(rids, list):
+ rids = [rids]
+ for level in self.browse(cursor, uid, rids, context):
+ smallest_level_id = self.search(cursor, uid, [('policy_id', '=', level.policy_id.id)],
+ order='level asc', limit=1, context=context)
+ smallest_level = self.browse(cursor, uid, smallest_level_id[0], context)
+ if smallest_level.computation_mode == 'previous_date':
+ return False
+ return True
+
+
+
+ _sql_constraint = [('unique level',
+ 'UNIQUE (policy_id, level)',
+ 'Level must be unique per policy')]
+
+ _constraints = [(_check_level_mode,
+ 'The smallest level can not be of type Previous reminder',
+ ['level'])]
+
+ def _previous_level(self, cursor, uid, policy_level, context=None):
+ """ For one policy level, returns the id of the previous level
+
+ If there is no previous level, it returns None, it means that's the
+ first policy level
+
+ :param browse_record policy_level: policy level
+ :return: previous level id or None if there is no previous level
+ """
+ previous_level_ids = self.search(
+ cursor,
+ uid,
+ [('policy_id', '=', policy_level.policy_id.id),
+ ('level', '<', policy_level.level)],
+ order='level desc',
+ limit=1,
+ context=context)
+ return previous_level_ids[0] if previous_level_ids else None
+
+ # ----- time related functions ---------
+
+ def _net_days_get_boundary(self):
+ return " (mv_line.date_maturity + %(delay)s)::date <= date(%(lookup_date)s)"
+
+ def _end_of_month_get_boundary(self):
+ return ("(date_trunc('MONTH', (mv_line.date_maturity + %(delay)s))+INTERVAL '1 MONTH - 1 day')::date"
+ "<= date(%(lookup_date)s)")
+
+ def _previous_date_get_boundary(self):
+ return "(cr_line.date + %(delay)s)::date <= date(%(lookup_date)s)"
+
+ def _get_sql_date_boundary_for_computation_mode(self, cursor, uid, level, lookup_date, context=None):
+ """Return a where clauses statement for the given
+ lookup date and computation mode of the level"""
+ fname = "_%s_get_boundary" % (level.computation_mode,)
+ if hasattr(self, fname):
+ fnc = getattr(self, fname)
+ return fnc()
+ else:
+ raise NotImplementedError(_('Can not get function for computation mode: '
+ '%s is not implemented') % (fname,))
+
+ # -----------------------------------------
+
+ def _get_first_level_lines(self, cursor, uid, level, lookup_date, lines, context=None):
+ if not lines:
+ return []
+ """Retrieve all the line that are linked to a frist level.
+ We use Raw SQL for perf. Security rule where applied in
+ policy object when line where retrieved"""
+ sql = ("SELECT DISTINCT mv_line.id\n"
+ " FROM account_move_line mv_line\n"
+ " WHERE mv_line.id in %(line_ids)s\n"
+ " AND NOT EXISTS (SELECT cr_line.id from credit_control_line cr_line\n"
+ " WHERE cr_line.move_line_id = mv_line.id)")
+ sql += " AND" + self._get_sql_date_boundary_for_computation_mode(
+ cursor, uid, level, lookup_date, context)
+ data_dict = {'lookup_date': lookup_date, 'line_ids': tuple(lines),
+ 'delay': level.delay_days}
+
+ cursor.execute(sql, data_dict)
+ res = cursor.fetchall()
+ if not res:
+ return []
+ return [x[0] for x in res]
+
+
+ def _get_other_level_lines(self, cursor, uid, level, lookup_date, lines, context=None):
+ # We filter line that have a level smaller than current one
+ # TODO if code fits need refactor _get_first_level_lines and _get_other_level_lines
+ # Code is not DRY
+ if not lines:
+ return []
+ sql = ("SELECT mv_line.id\n"
+ " FROM account_move_line mv_line\n"
+ " JOIN credit_control_line cr_line\n"
+ " ON (mv_line.id = cr_line.move_line_id)\n"
+ " 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"
+ " ORDER BY credit_control_line.level desc limit 1)\n"
+ " AND cr_line.level = %(level)s\n"
+ " AND mv_line.id in %(line_ids)s\n")
+ sql += " AND " + self._get_sql_date_boundary_for_computation_mode(
+ cursor, uid, level, lookup_date, context)
+ previous_level_id = self._previous_level(
+ cursor, uid, level, context=context)
+ previous_level = self.browse(
+ cursor, uid, previous_level_id, context=context)
+ data_dict = {'lookup_date': lookup_date, 'line_ids': tuple(lines),
+ 'delay': level.delay_days, 'level': previous_level.level}
+
+ cursor.execute(sql, data_dict)
+ res = cursor.fetchall()
+ if not res:
+ return []
+ return [x[0] for x in res]
+
+ def get_level_lines(self, cursor, uid, level_id, lookup_date, lines, context=None):
+ """get all move lines in entry lines that match the current level"""
+ assert not (isinstance(level_id, list) and len(level_id) > 1), "level_id: only one id expected"
+ if isinstance(level_id, list):
+ level_id = level_id[0]
+ matching_lines = []
+ level = self.browse(cursor, uid, level_id, context=context)
+ if self._previous_level(cursor, uid, level, context=context) is None:
+ matching_lines += self._get_first_level_lines(
+ cursor, uid, level, lookup_date, lines, context=context)
+ else:
+ matching_lines += self._get_other_level_lines(
+ cursor, uid, level, lookup_date, lines, context=context)
+
+ return matching_lines
+
diff --git a/account_credit_control/policy_view.xml b/account_credit_control/policy_view.xml
new file mode 100644
index 000000000..6113f0439
--- /dev/null
+++ b/account_credit_control/policy_view.xml
@@ -0,0 +1,118 @@
+
+
+
+
+ credit.control.policy.form
+ credit.control.policy
+ form
+
+
+
+
+
+
+ credit.control.policy.tree
+ credit.control.policy
+ tree
+
+
+
+
+
+
+
+
+
+
+
+ Credit Policies
+ ir.actions.act_window
+ credit.control.policy
+
+ form
+ tree,form
+
+
+
+
+
+
+ credit.mangement.policy.level.form
+ credit.control.policy.level
+ form
+
+
+
+
+
+ %for comm in objects :
+ ${setLang(comm.partner_id.lang)}
+ <%
+ current_uri = '%s_policy_template' % (comm.partner_id.lang)
+ if not context.lookup.has_template(current_uri):
+ context.lookup.put_string(current_uri, comm.current_policy_level.mail_template_id.body_html)
+ %>
+ <%include file="${current_uri}" args="object=comm,mode='pdf'"/>
+
+ %endfor
+
+
diff --git a/account_credit_control/report/credit_control_summary.py b/account_credit_control/report/credit_control_summary.py
new file mode 100644
index 000000000..156857f92
--- /dev/null
+++ b/account_credit_control/report/credit_control_summary.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+import time
+
+from openerp.report import report_sxw
+
+class CreditSummaryReport(report_sxw.rml_parse):
+ def __init__(self, cr, uid, name, context):
+ super(CreditSummaryReport, self).__init__(cr, uid, name, context=context)
+ self.localcontext.update({
+ 'time': time,
+ 'cr':cr,
+ 'uid': uid,
+ })
+
+report_sxw.report_sxw('report.credit_control_summary',
+ 'credit.control.communication',
+ 'addons/account_credit_control/report/credit_control_summary.html.mako',
+ parser=CreditSummaryReport)
diff --git a/account_credit_control/report/report.xml b/account_credit_control/report/report.xml
new file mode 100644
index 000000000..04e84f277
--- /dev/null
+++ b/account_credit_control/report/report.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/account_credit_control/run.py b/account_credit_control/run.py
new file mode 100644
index 000000000..80e58798e
--- /dev/null
+++ b/account_credit_control/run.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+import sys
+import traceback
+import logging
+
+from openerp.osv.orm import Model, fields
+from openerp.tools.translate import _
+from openerp.osv.osv import except_osv
+
+logger = logging.getLogger('Credit Control run')
+
+memoizers = {}
+
+
+class CreditControlRun(Model):
+ """Credit Control run generate all credit control lines and reject"""
+
+ _name = "credit.control.run"
+ _rec_name = 'date'
+ _description = """Credit control line generator"""
+ _columns = {'date': fields.date('Lookup date', required=True),
+ 'policy_ids': fields.many2many('credit.control.policy',
+ rel="credit_run_policy_rel",
+ string='Policies',
+ readonly=True,
+ help="If nothing set all policies will be used",
+
+ states={'draft': [('readonly', False)]}),
+
+ 'report': fields.text('Report', readonly=True),
+
+ 'state': fields.selection([('draft', 'Draft'),
+ ('running', 'Running'),
+ ('done', 'Done'),
+ ('error', 'Error')],
+ string='State',
+ required=True,
+ readonly=True),
+
+ 'manual_ids': fields.many2many('account.move.line',
+ rel="credit_runreject_rel",
+ string='Line to be handled manually',
+ readonly=True),
+ }
+
+ _defaults = {'state': 'draft'}
+
+ def check_run_date(self, cursor, uid, ids, lookup_date, context=None):
+ """Ensure that there is no credit line in the future using lookup_date"""
+ line_obj = self.pool.get('credit.control.line')
+ lines = line_obj.search(cursor, uid, [('date', '>', lookup_date)],
+ order='date DESC', limit=1)
+ if lines:
+ line = line_obj.browse(cursor, uid, lines[0])
+ raise except_osv(_('A run was already executed in a greater date'),
+ _('Run date should be >= %s') % (line.date))
+
+
+ def _generate_credit_lines(self, cursor, uid, run_id, context=None):
+ """ Generate credit line. Function can be a little dryer but
+ it does almost noting, initalise variable maange error and call
+ real know how method"""
+ memoizers['credit_line_residuals'] = {}
+ cr_line_obj = self.pool.get('credit.control.line')
+ if isinstance(run_id, list):
+ run_id = run_id[0]
+ run = self.browse(cursor, uid, run_id, context=context)
+ errors = []
+ manualy_managed_lines = [] #line who changed policy
+ credit_line_ids = [] # generated lines
+ run.check_run_date(run.date, context=context)
+ policy_ids = run.policy_ids
+ if not policy_ids:
+ policy_obj = self.pool.get('credit.control.policy')
+ policy_ids_ids = policy_obj.search(cursor, uid, [])
+ policy_ids = policy_obj.browse(cursor, uid, policy_ids_ids)
+ for policy in policy_ids:
+ if policy.do_nothing:
+ continue
+ try:
+ lines = policy._get_moves_line_to_process(run.date, context=context)
+ tmp_manual = policy._check_lines_policies(lines, context=context)
+ lines = list(set(lines) - set(tmp_manual))
+ manualy_managed_lines += tmp_manual
+ if not lines:
+ continue
+ # policy levels are sorted by level so iteration is in the correct order
+ for level in policy.level_ids:
+ level_lines = level.get_level_lines(run.date, lines)
+ #only this write action own a separate cursor
+ credit_line_ids += cr_line_obj.create_or_update_from_mv_lines(
+ cursor, uid, [], level_lines, level.id, run.date, errors=errors, context=context)
+
+ lines = list(set(lines) - set(level_lines))
+ except except_osv, exc:
+ cursor.rollback()
+ error_type, error_value, trbk = sys.exc_info()
+ st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value)
+ st += ''.join(traceback.format_tb(trbk, 30))
+ logger.error(st)
+ self.write(cursor, uid, [run.id], {'report':st, 'state': 'error'})
+ return False
+ vals = {'report': u"Number of generated lines : %s \n" % (len(credit_line_ids),),
+ 'state': 'done',
+ 'manual_ids': [(6, 0, manualy_managed_lines)]}
+ if errors:
+ vals['report'] += u"Following line generation errors appends:"
+ vals['report'] += u"----\n".join(errors)
+ vals['state'] = 'done'
+ run.write(vals)
+ # lines will correspond to line that where not treated
+ return lines
+
+
+
+ def generate_credit_lines(self, cursor, uid, run_id, context=None):
+ """Generate credit control lines"""
+ context = context or {}
+ # we do a little magical tips in order to ensure non concurrent run
+ # of the function generate_credit_lines
+ try:
+ cursor.execute('SELECT id FROM credit_control_run'
+ ' LIMIT 1 FOR UPDATE NOWAIT' )
+ except Exception, exc:
+ cursor.rollback()
+ raise except_osv(_('A credit control run is already running'
+ ' in background please try later'),
+ str(exc))
+ # in case of exception openerp will do a rollback for us and free the lock
+ return self._generate_credit_lines(cursor, uid, run_id, context)
+
diff --git a/account_credit_control/run_view.xml b/account_credit_control/run_view.xml
new file mode 100644
index 000000000..cdd49b401
--- /dev/null
+++ b/account_credit_control/run_view.xml
@@ -0,0 +1,65 @@
+
+
+
+
+ credit.control.run.tree
+ credit.control.run
+ tree
+
+
+
+
+
+
+
+
+
+ credit.control.run.form
+ credit.control.run
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Credit Control Run
+ ir.actions.act_window
+ credit.control.run
+
+ form
+ tree,form
+
+
+
+
+
+
+
+
diff --git a/account_credit_control/security/ir.model.access.csv b/account_credit_control/security/ir.model.access.csv
new file mode 100644
index 000000000..a909d3b51
--- /dev/null
+++ b/account_credit_control/security/ir.model.access.csv
@@ -0,0 +1,27 @@
+"id","perm_create","perm_unlink","group_id/id","name","model_id/id","perm_read","perm_write"
+"account_credit_control.ir_model_access_270",1,1,"group_account_credit_control_manager","credit_control_manager_line","account_credit_control.model_credit_control_line",1,1
+"account_credit_control.ir_model_access_271",1,1,"group_account_credit_control_user","credit_control_user_line","account_credit_control.model_credit_control_line",1,1
+"account_credit_control.ir_model_access_272",0,0,"group_account_credit_control_info","credit_control_info_line","account_credit_control.model_credit_control_line",1,0
+"account_credit_control.ir_model_access_273",1,1,"group_account_credit_control_manager","credit_control_manager_mail_template","email_template.model_email_template",1,1
+"account_credit_control.ir_model_access_274",1,1,"group_account_credit_control_manager","credit_control_manager_mail_template_preview","email_template.model_email_template_preview",1,1
+"account_credit_control.ir_model_access_275",1,1,"group_account_credit_control_manager","credit_control_manager_mail_message","mail.model_mail_message",1,1
+"account_credit_control.ir_model_access_276",1,0,"group_account_credit_control_user","credit_control_user_mail_message","mail.model_mail_message",1,1
+"account_credit_control.ir_model_access_277",0,0,"group_account_credit_control_info","credit_control_info_mail_message","mail.model_mail_message",1,0
+"account_credit_control.ir_model_access_278",1,1,"group_account_credit_control_manager","credit_control_manager_comm","account_credit_control.model_credit_control_communication",1,1
+"account_credit_control.ir_model_access_279",1,1,"base.group_sale_salesman","credit_control_user_comm","account_credit_control.model_credit_control_communication",1,1
+"account_credit_control.ir_model_access_280",0,0,"group_account_credit_control_info","credit_control_info_comm","account_credit_control.model_credit_control_communication",1,0
+"account_credit_control.ir_model_access_281",1,1,"group_account_credit_control_manager","credit_control_mananger_run","account_credit_control.model_credit_control_run",1,1
+"account_credit_control.ir_model_access_282",1,1,"group_account_credit_control_user","credit_control_user_run","account_credit_control.model_credit_control_run",1,1
+"account_credit_control.ir_model_access_283",0,0,"group_account_credit_control_info","credit_control_info_run","account_credit_control.model_credit_control_run",1,0
+"account_credit_control.ir_model_access_284",1,1,"group_account_credit_control_manager","credit_control_manager_policy","account_credit_control.model_credit_control_policy",1,1
+"account_credit_control.ir_model_access_285",0,0,"group_account_credit_control_user","credit_control_user_policy","account_credit_control.model_credit_control_policy",1,0
+"account_credit_control.ir_model_access_286",0,0,"group_account_credit_control_info","credit_control_info_policy","account_credit_control.model_credit_control_policy",1,0
+"account_credit_control.ir_model_access_287",1,1,"group_account_credit_control_manager","credit_control_manager_level","account_credit_control.model_credit_control_policy_level",1,1
+"account_credit_control.ir_model_access_288",0,0,"group_account_credit_control_user","credit_control_user_level","account_credit_control.model_credit_control_policy_level",1,0
+"account_credit_control.ir_model_access_289",0,0,"group_account_credit_control_info","credit_control_info_level","account_credit_control.model_credit_control_policy_level",1,0
+"account_credit_control.ir_model_access_290",1,1,"group_account_credit_control_manager","credit_control_manager_mailer","account_credit_control.model_credit_control_mailer",1,1
+"account_credit_control.ir_model_access_291",1,1,"group_account_credit_control_user","credit_control_user_mailer","account_credit_control.model_credit_control_mailer",1,1
+"account_credit_control.ir_model_access_292",1,1,"group_account_credit_control_manager","credit_control_manager_marker","account_credit_control.model_credit_control_marker",1,1
+"account_credit_control.ir_model_access_293",1,1,"group_account_credit_control_user","credit_control_user_marker","account_credit_control.model_credit_control_marker",1,1
+"account_credit_control.ir_model_access_294",1,1,"group_account_credit_control_manager","credit_control_manager_printer","account_credit_control.model_credit_control_printer",1,1
+"account_credit_control.ir_model_access_295",1,1,"group_account_credit_control_user","credit_control_user_printer","account_credit_control.model_credit_control_printer",1,1
diff --git a/account_credit_control/wizard/__init__.py b/account_credit_control/wizard/__init__.py
new file mode 100644
index 000000000..00a0cadd3
--- /dev/null
+++ b/account_credit_control/wizard/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+from . import credit_control_mailer
+from . import credit_control_marker
+from . import credit_control_printer
+from . import credit_control_communication
diff --git a/account_credit_control/wizard/credit_control_communication.py b/account_credit_control/wizard/credit_control_communication.py
new file mode 100644
index 000000000..5cb79909b
--- /dev/null
+++ b/account_credit_control/wizard/credit_control_communication.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+from openerp.osv.orm import TransientModel, fields
+from openerp.osv.osv import except_osv
+from openerp.tools.translate import _
+import netsvc
+import logging
+
+logger = logging.getLogger('credit.control.line mailing')
+
+
+class CreditCommunication(TransientModel):
+ """Shell calss used to provide a base model to email template and reporting.
+ Il use this approche in version 7 a browse record will exist even if not saved"""
+ _name = "credit.control.communication"
+ _description = "credit control communication"
+ _rec_name = 'partner_id'
+ _columns = {'partner_id': fields.many2one('res.partner', 'Partner', required=True),
+
+ 'current_policy_level': fields.many2one('credit.control.policy.level',
+ 'Level', required=True),
+
+ 'credit_lines': fields.many2many('credit.control.line',
+ rel='comm_credit_rel',
+ string='Credit Lines'),
+
+ 'company_id': fields.many2one('res.company', 'Company',
+ required=True),
+
+ 'user_id': fields.many2one('res.users', 'User')}
+
+ _defaults = {'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(
+ cr, uid, 'credit.control.policy', context=c),
+ 'user_id': lambda s, cr, uid, c: uid}
+
+ def get_address(self, cursor, uid, com_id, context=None):
+ """Return a valid address for customer"""
+
+ context = context or {}
+ if isinstance(com_id, list):
+ com_id = com_id[0]
+ current = self.browse(cursor, uid, com_id, context=context)
+ part_obj = self.pool.get('res.partner')
+ adds = part_obj.address_get(cursor, uid, [current.partner_id.id],
+ adr_pref=['invoice', 'default'])
+
+ add = adds.get('invoice', adds.get('default'))
+ if not add:
+ raise except_osv(_('No address for partner %s') % (current.partner_id.name),
+ _('Please set one'))
+ add_obj = self.pool.get('res.partner.address')
+ return add_obj.browse(cursor, uid, add, context=context)
+
+
+ def get_mail(self, cursor, uid, com_id, context=None):
+ """Return a valid email for customer"""
+ context = context or {}
+ if isinstance(com_id, list):
+ com_id = com_id[0]
+ current = self.browse(cursor, uid, com_id, context=context)
+ email = current.get_address().email
+ if not email:
+ raise except_osv(_('No invoicing or default email for partner %s') %
+ (current.partner_id.name),
+ _('Please set one'))
+ return email
+
+ def _get_credit_lines(self, cursor, uid, line_ids, partner_id, level_id, context=None):
+ """Return credit lines related to a partner and a policy level"""
+ cr_line_obj = self.pool.get('credit.control.line')
+ cr_l_ids = cr_line_obj.search(cursor,
+ uid,
+ [('id', 'in', line_ids),
+ ('partner_id', '=', partner_id),
+ ('policy_level_id', '=', level_id)],
+ context=context)
+ #return cr_line_obj.browse(cursor, uid, cr_l_ids, context=context)
+ return cr_l_ids
+
+ def _generate_comm_from_credit_line_ids(self, cursor, uid, line_ids, context=None):
+ if not line_ids:
+ return []
+ comms = []
+ sql = ("SELECT distinct partner_id, policy_level_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")
+
+ cursor.execute(sql, (tuple(line_ids),))
+ res = cursor.dictfetchall()
+ for level_assoc in res:
+ data = {}
+ data['credit_lines'] = [(6, 0, self._get_credit_lines(cursor, uid, line_ids,
+ level_assoc['partner_id'],
+ level_assoc['policy_level_id'],
+ context=context))]
+ data['partner_id'] = level_assoc['partner_id']
+ data['current_policy_level'] = level_assoc['policy_level_id']
+ comm_id = self.create(cursor, uid, data, context=context)
+
+
+ comms.append(self.browse(cursor, uid, comm_id, context=context))
+ return comms
+
+ def _generate_mails(self, cursor, uid, comms, context=None):
+ """Generate mail message using template related to level"""
+ cr_line_obj = self.pool.get('credit.control.line')
+ mail_temp_obj = self.pool.get('email.template')
+ mail_message_obj = self.pool.get('mail.message')
+ mail_ids = []
+ for comm in comms:
+ # we want to use a local cursor in order to send the maximum
+ # of email
+ try:
+ template = comm.current_policy_level.mail_template_id.id
+
+ mvalues = mail_temp_obj.generate_email(cursor, uid,
+ template,
+ comm.id,
+ context=context)
+ essential_values = ['subject', 'body_html',
+ 'email_from', 'email_to']
+ for val in essential_values:
+ if not mvalues.get(val):
+ raise Exception('Mail generation error with %s', val)
+ mail_id = mail_message_obj.create(cursor, uid, mvalues, context=context)
+
+ cl_ids = [cl.id for cl in comm.credit_lines]
+
+ # we do not use local cusros else we have a lock
+ cr_line_obj.write(cursor, uid, cl_ids,
+ {'mail_message_id': mail_id,
+ 'state': 'sent'})
+ mail_ids.append(mail_id)
+ except Exception, exc:
+ logger.error(exc)
+ cursor.rollback()
+ cl_ids = [cl.id for cl in comm.credit_lines]
+ # we do not use local cusros else we have a lock
+ cr_line_obj.write(cursor, uid, cl_ids,
+ {'state': 'mail_error'})
+ finally:
+ cursor.commit()
+ return mail_ids
+
+ def _generate_report(self, cursor, uid, comms, context=None):
+ """Will generate a report by inserting mako template of related policy template"""
+ service = netsvc.LocalService('report.credit_control_summary')
+ ids = [x.id for x in comms]
+ result, format = service.create(cursor, uid, ids, {}, {})
+ return result
+
+ def _mark_credit_line_as_sent(self, cursor, uid, comms, context=None):
+ line_ids = []
+ for comm in comms:
+ line_ids += [x.id for x in comm.credit_lines]
+ l_obj = self.pool.get('credit.control.line')
+ l_obj.write(cursor, uid, line_ids, {'state': 'sent'}, context=context)
+ return line_ids
diff --git a/account_credit_control/wizard/credit_control_mailer.py b/account_credit_control/wizard/credit_control_mailer.py
new file mode 100644
index 000000000..d64197612
--- /dev/null
+++ b/account_credit_control/wizard/credit_control_mailer.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+from openerp.osv.orm import TransientModel, fields
+from openerp.osv.osv import except_osv
+from openerp.tools.translate import _
+
+class CreditControlMailer(TransientModel):
+ """Change the state of lines in mass"""
+
+ _name = "credit.control.mailer"
+ _description = """Mass credit line mailer"""
+ _rec_name = 'id'
+
+ _columns = {'mail_all': fields.boolean('Mail all ready lines')}
+
+
+ def _get_lids(self, cursor, uid, mail_all, active_ids, context=None):
+ """get line to be marked filter done lines"""
+ # TODO DRY with printer
+ line_obj = self.pool.get('credit.control.line')
+ if mail_all:
+ domain = [('state', '=', 'to_be_sent'),
+ ('canal', '=', 'mail')]
+ else:
+ domain = [('state', '=', 'to_be_sent'),
+ ('id', 'in', active_ids),
+ ('canal', '=', 'mail')]
+ return line_obj.search(cursor, uid, domain, context=context)
+
+
+ def mail_lines(self, cursor, uid, wiz_id, context=None):
+ comm_obj = self.pool.get('credit.control.communication')
+ context = context or {}
+ if isinstance(wiz_id, list):
+ wiz_id = wiz_id[0]
+ current = self.browse(cursor, uid, wiz_id, context)
+ lines_ids = context.get('active_ids')
+
+ if not lines_ids and not current.mail_all:
+ raise except_osv(_('Not lines ids are selected'),
+ _('You may check "Mail all ready lines"'))
+ filtered_ids = self._get_lids(cursor, uid, current.mail_all, lines_ids, context)
+ comms = comm_obj._generate_comm_from_credit_line_ids(cursor, uid, filtered_ids,
+ context=context)
+ comm_obj._generate_mails(cursor, uid, comms, context=context)
+ return {}
diff --git a/account_credit_control/wizard/credit_control_mailer_view.xml b/account_credit_control/wizard/credit_control_mailer_view.xml
new file mode 100644
index 000000000..6b8f5425e
--- /dev/null
+++ b/account_credit_control/wizard/credit_control_mailer_view.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ credit.line.mailer.form
+ credit.control.mailer
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mail lines
+ credit.control.mailer
+ form
+ form
+
+ new
+ Mail all marked lines
+
+
+
+
+
diff --git a/account_credit_control/wizard/credit_control_marker.py b/account_credit_control/wizard/credit_control_marker.py
new file mode 100644
index 000000000..8960852bd
--- /dev/null
+++ b/account_credit_control/wizard/credit_control_marker.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+from openerp.osv.orm import TransientModel, fields
+from openerp.osv.osv import except_osv
+from openerp.tools.translate import _
+
+class CreditControlMarker(TransientModel):
+ """Change the state of lines in mass"""
+
+ _name = "credit.control.marker"
+ _description = """Mass marker"""
+ _columns = {'name': fields.selection([('to_be_sent', 'To be sent'),
+ ('sent', 'Done')],
+ 'Mark as', required=True),
+
+ 'mark_all': fields.boolean('Mark all draft lines')}
+
+ _defaults = {'name': 'to_be_sent'}
+
+ def _get_lids(self, cursor, uid, mark_all, active_ids, context=None):
+ """get line to be marked filter done lines"""
+ line_obj = self.pool.get('credit.control.line')
+ if mark_all:
+ domain = [('state', '=', 'draft')]
+ else:
+ domain = [('state', '!=', 'sent'), ('id', 'in', active_ids)]
+ return line_obj.search(cursor, uid, domain, context=context)
+
+ def _mark_lines(self, cursor, uid, filtered_ids, state, context=None):
+ """write hook"""
+ line_obj = self.pool.get('credit.control.line')
+ if not state:
+ raise ValueError(_('state can not be empty'))
+ line_obj.write(cursor, uid, filtered_ids, {'state': state})
+ return filtered_ids
+
+
+
+ def mark_lines(self, cursor, uid, wiz_id, context=None):
+ """Write state of selected credit lines to the one in entry
+ done credit line will be ignored"""
+ context = context or {}
+ if isinstance(wiz_id, list):
+ wiz_id = wiz_id[0]
+ current = self.browse(cursor, uid, wiz_id, context)
+ lines_ids = context.get('active_ids')
+
+ if not lines_ids and not current.mark_all:
+ raise except_osv(_('Not lines ids are selected'),
+ _('You may check "Mark all draft lines"'))
+ filtered_ids = self._get_lids(cursor, uid, current.mark_all, lines_ids, context)
+ if not filtered_ids:
+ raise except_osv(_('No lines will be changed'),
+ _('All selected lines are allready done'))
+
+ # hook function a simple write should be enought
+ self._mark_lines(cursor, uid, filtered_ids, current.name, context)
+
+ return {'domain': "[('id','in',%s)]" % (filtered_ids,),
+ 'name': _('%s marked line') % (current.name,),
+ 'view_type': 'form',
+ 'view_mode': 'tree,form',
+ 'view_id': False,
+ 'res_model': 'credit.control.line',
+ 'type': 'ir.actions.act_window'}
diff --git a/account_credit_control/wizard/credit_control_marker_view.xml b/account_credit_control/wizard/credit_control_marker_view.xml
new file mode 100644
index 000000000..6898c1ca1
--- /dev/null
+++ b/account_credit_control/wizard/credit_control_marker_view.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ credit.line.marker.form
+ credit.control.marker
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mark lines
+ credit.control.marker
+ credit.control.line
+ form
+ form
+
+ new
+ Mark all lines. You can the send marked lines
+
+
+
+
diff --git a/account_credit_control/wizard/credit_control_printer.py b/account_credit_control/wizard/credit_control_printer.py
new file mode 100644
index 000000000..55ff06590
--- /dev/null
+++ b/account_credit_control/wizard/credit_control_printer.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Nicolas Bessi
+# Copyright 2012 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 .
+#
+##############################################################################
+import base64
+
+from openerp.osv.orm import TransientModel, fields
+from openerp.osv.osv import except_osv
+from openerp.tools.translate import _
+
+class CreditControlPrinter(TransientModel):
+ """Change the state of lines in mass"""
+
+ _name = "credit.control.printer"
+ _rec_name = 'id'
+ _description = """Mass printer"""
+ _columns = {'mark_as_sent': fields.boolean('Mark lines as send',
+ help="Lines to emailed will be ignored"),
+ 'print_all': fields.boolean('Print all ready lines'),
+ 'report_file': fields.binary('Generated Report'),
+ 'state': fields.char('state', size=32)}
+
+ def _get_lids(self, cursor, uid, print_all, active_ids, context=None):
+ """get line to be marked filter done lines"""
+ # TODO Dry with mailer maybe in comm
+ line_obj = self.pool.get('credit.control.line')
+ if print_all:
+ domain = [('state', '=', 'to_be_sent'),
+ ('canal', '=', 'manual')]
+ else:
+ domain = [('state', '=', 'to_be_sent'),
+ ('id', 'in', active_ids),
+ ('canal', '=', 'manual')]
+ return line_obj.search(cursor, uid, domain, context=context)
+
+
+ def print_lines(self, cursor, uid, wiz_id, context=None):
+ comm_obj = self.pool.get('credit.control.communication')
+ context = context or {}
+ if isinstance(wiz_id, list):
+ wiz_id = wiz_id[0]
+ current = self.browse(cursor, uid, wiz_id, context)
+ lines_ids = context.get('active_ids')
+ if not lines_ids and not current.print_all:
+ raise except_osv(_('Not lines ids are selected'),
+ _('You may check "Print all ready lines"'))
+ if current.print_all:
+ filtered_ids = self._get_lids(cursor, uid, current.print_all, lines_ids, context)
+ else:
+ filtered_ids = lines_ids
+ comms = comm_obj._generate_comm_from_credit_line_ids(cursor, uid, filtered_ids,
+ context=context)
+ report_file = comm_obj._generate_report(cursor, uid, comms, context=context)
+ current.write({'report_file': base64.b64encode(report_file), 'state': 'done'})
+ if current.mark_as_sent:
+ filtered_ids = self._get_lids(cursor, uid, False, lines_ids, context)
+ comm_obj._mark_credit_line_as_sent(cursor, uid, comms, context=context)
+ return False
diff --git a/account_credit_control/wizard/credit_control_printer_view.xml b/account_credit_control/wizard/credit_control_printer_view.xml
new file mode 100644
index 000000000..4c1906c64
--- /dev/null
+++ b/account_credit_control/wizard/credit_control_printer_view.xml
@@ -0,0 +1,47 @@
+
+
+
+
+ credit.line.printer.form
+ credit.control.printer
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Print lines
+ credit.control.printer
+ credit.control.line
+ form
+ form
+
+ new
+ Print all lines
+
+
+
+