Merge pull request #62 from guewen/8.0-account_credit_control-migr

Migration of account_credit_control
This commit is contained in:
Nicolas Bessi (nbessi)
2014-12-02 14:35:36 +01:00
67 changed files with 4136 additions and 6460 deletions

View File

@@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
class AccountAccount(orm.Model):
"""Add a link to a credit control policy on account.account"""
_inherit = "account.account"
_columns = {
'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):
if default is None:
default = {}
else:
default = default.copy()
default['credit_control_line_ids'] = False
return super(AccountAccount, self).copy_data(
cr, uid, id, default=default, context=context)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Vincent Renaville
# Copyright 2013 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
from openerp.tools.translate import _
class AccountInvoice(orm.Model):
"""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 policy are not copied"""
if default is None:
default = {}
else:
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:
cc_nondraft_line_ids = cc_line_obj.search(
cr, uid,
[('invoice_id', '=', invoice_id),
('state', '!=', 'draft')],
context=context)
if cc_nondraft_line_ids:
raise orm.except_orm(
_('Error!'),
_('You cannot cancel this invoice.\n'
'A payment reminder has already been '
'sent to the customer.\n'
'You must create a credit note and '
'issue a new invoice.')
)
cc_draft_line_ids = cc_line_obj.search(
cr, uid,
[('invoice_id', '=', invoice_id),
('state', '=', 'draft')],
context=context)
cc_line_obj.unlink(cr, uid,
cc_draft_line_ids,
context=context)
return super(AccountInvoice, self).action_cancel(cr, uid, ids,
context=context)

View File

@@ -1,292 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from openerp.osv import orm, fields
from openerp.tools.translate import _
logger = logging.getLogger('credit.line.control')
class CreditControlLine(orm.Model):
"""A credit control line describes an amount due by a customer for a due date.
A line is created once the due date of the payment is exceeded.
It is created in "draft" and some actions are available (send by email,
print, ...)
"""
_name = "credit.control.line"
_description = "A credit control line"
_rec_name = "id"
_order = "date DESC"
_columns = {
'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',
required=True,
readonly=True,
states={'draft': [('readonly', False)]}
),
'date_entry': fields.related(
'move_line_id', 'date',
type='date',
string='Entry date',
store=True, readonly=True
),
'date_sent': fields.date(
'Sent date',
readonly=True,
states={'draft': [('readonly', False)]}
),
'state': fields.selection(
[('draft', 'Draft'),
('ignored', 'Ignored'),
('to_be_sent', 'Ready To Send'),
('sent', 'Done'),
('error', 'Error'),
('email_error', 'Emailing Error')],
'State', required=True, readonly=True,
help=("Draft lines need to be triaged.\n"
"Ignored lines are lines for which we do "
"not want to send something.\n"
"Draft and ignored lines will be "
"generated again on the next run.")
),
'channel': fields.selection(
[('letter', 'Letter'),
('email', 'Email')],
'Channel', 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 incl.',
required=True,
readonly=True
),
'balance_due': fields.float(
'Due balance', required=True,
readonly=True
),
'mail_message_id': fields.many2one(
'mail.mail',
'Sent Email',
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='integer',
relation='credit.control.policy',
string='Level',
store=True,
readonly=True
),
'manually_overridden': fields.boolean('Manually overridden')
}
_defaults = {'state': 'draft'}
def _prepare_from_move_line(self, cr, uid, move_line,
level, controlling_date, open_amount,
context=None):
"""Create credit control line"""
data = {}
data['date'] = controlling_date
data['date_due'] = move_line.date_maturity
data['state'] = 'draft'
data['channel'] = level.channel
data['invoice_id'] = (move_line.invoice.id if
move_line.invoice else False)
data['partner_id'] = move_line.partner_id.id
data['amount_due'] = (move_line.amount_currency or move_line.debit or
move_line.credit)
data['balance_due'] = open_amount
data['policy_level_id'] = level.id
data['company_id'] = move_line.company_id.id
data['move_line_id'] = move_line.id
return data
def create_or_update_from_mv_lines(self, cr, uid, ids, lines,
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')
user = self.pool.get('res.users').browse(cr, uid, uid)
currency_ids = currency_obj.search(cr, uid, [], context=context)
tolerance = {}
tolerance_base = user.company_id.credit_control_tolerance
for c_id in currency_ids:
tolerance[c_id] = currency_obj.compute(
cr, uid,
c_id,
user.company_id.currency_id.id,
tolerance_base,
context=context)
level = level_obj.browse(cr, uid, level_id, context)
line_ids = []
for line in ml_obj.browse(cr, uid, lines, context):
open_amount = line.amount_residual_currency
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
def unlink(self, cr, uid, ids, context=None, check=True):
for line in self.browse(cr, uid, ids, context=context):
if line.state != 'draft':
raise orm.except_orm(
_('Error !'),
_('You are not allowed to delete a credit control '
'line that is not in draft state.')
)
return super(CreditControlLine, self).unlink(cr, uid, ids,
context=context)

View File

@@ -1,78 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
class ResPartner(orm.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',
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:
default = default.copy()
default['credit_control_line_ids'] = False
return super(ResPartner, self).copy_data(
cr, uid, id, default=default, context=context)

View File

@@ -1,464 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp.tools.translate import _
class CreditControlPolicy(orm.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 which should not '
'generate lines or are obsolete'
),
'company_id': fields.many2one('res.company', 'Company'),
'account_ids': fields.many2many(
'account.account',
string='Accounts',
required=True,
domain="[('type', '=', 'receivable')]",
help="This policy will be active only"
" for the selected accounts"
),
'active': fields.boolean('Active'),
}
_defaults = {
'active': True,
}
def _move_lines_domain(self, cr, uid, policy, controlling_date,
context=None):
"""Build the default domain for searching move lines"""
account_ids = [a.id for a in policy.account_ids]
return [('account_id', 'in', account_ids),
('date_maturity', '<=', controlling_date),
('reconcile_id', '=', False),
('partner_id', '!=', False)]
def _due_move_lines(self, cr, uid, policy, controlling_date, context=None):
""" Get the due move lines for the policy of the company.
The set of ids will be reduced and extended according
to the specific policies defined on partners and invoices.
Do not use direct SQL in order to respect security rules.
Assume that only the receivable lines have a maturity date and that
accounts used in the policy are reconcilable.
"""
move_l_obj = self.pool.get('account.move.line')
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
if user.company_id.credit_policy_id.id != policy.id:
return set()
domain_line = self._move_lines_domain(cr, uid, policy,
controlling_date,
context=context)
return set(move_l_obj.search(cr, uid, domain_line, context=context))
def _move_lines_subset(self, cr, uid, policy, controlling_date,
model, move_relation_field, context=None):
""" Get the move lines related to one model for a policy.
Do not use direct SQL in order to respect security rules.
Assume that only the receivable lines have a maturity date and that
accounts used in the policy are reconcilable.
The policy relation field must be named credit_policy_id.
:param browse_record policy: policy
:param str controlling_date: date of credit control
:param str model: name of the model where is defined a credit_policy_id
:param str move_relation_field: name of the field in account.move.line
which is a many2one to `model`
:return: set of ids to add in the process, set of ids to remove from
the process
"""
# MARK possible place for a good optimisation
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)
to_add_ids = set()
to_remove_ids = set()
# The lines which are linked to this policy have to be included in the
# run for this policy.
# If another object override the credit_policy_id (ie. invoice after
add_obj_ids = my_obj.search(
cr, uid,
[('credit_policy_id', '=', policy.id)],
context=context)
if add_obj_ids:
domain = list(default_domain)
domain.append((move_relation_field, 'in', add_obj_ids))
to_add_ids = set(move_l_obj.search(cr, uid, domain,
context=context))
# The lines which are linked to another policy do not have to be
# included in the run for this policy.
neg_obj_ids = my_obj.search(
cr, uid,
[('credit_policy_id', '!=', policy.id),
('credit_policy_id', '!=', False)],
context=context)
if neg_obj_ids:
domain = list(default_domain)
domain.append((move_relation_field, 'in', neg_obj_ids))
to_remove_ids = set(move_l_obj.search(cr, uid, domain,
context=context))
return to_add_ids, to_remove_ids
def _get_partner_related_lines(self, cr, uid, policy, controlling_date,
context=None):
""" Get the move lines for a policy related to a partner.
:param browse_record policy: policy
:param str controlling_date: date of credit control
:param str model: name of the model where is defined a credit_policy_id
:param str move_relation_field: name of the field in account.move.line
which is a many2one to `model`
:return: set of ids to add in the process, set of ids to remove from
the process
"""
return self._move_lines_subset(cr, uid, policy, controlling_date,
'res.partner', 'partner_id',
context=context)
def _get_invoice_related_lines(self, cr, uid, policy, controlling_date,
context=None):
""" Get the move lines for a policy related to an invoice.
:param browse_record policy: policy
:param str controlling_date: date of credit control
:param str model: name of the model where is defined a credit_policy_id
:param str move_relation_field: name of the field in account.move.line
which is a many2one to `model`
:return: set of ids to add in the process, set of ids to remove from
the process
"""
return self._move_lines_subset(cr, uid, policy, controlling_date,
'account.invoice', 'invoice',
context=context)
def _get_move_lines_to_process(self, cr, uid, policy_id, controlling_date,
context=None):
"""Build a list of move lines ids to include in a run
for a policy at a given date.
:param int/long policy: id of the policy
:param str controlling_date: date of credit control
:return: set of ids to include in the run
"""
assert not (isinstance(policy_id, list) and len(policy_id) > 1), \
"policy_id: only one id expected"
if isinstance(policy_id, list):
policy_id = policy_id[0]
policy = self.browse(cr, uid, policy_id, context=context)
# there is a priority between the lines, depicted by the calls below
# warning, side effect method called on lines
lines = self._due_move_lines(cr, uid, policy, controlling_date,
context=context)
add_ids, remove_ids = self._get_partner_related_lines(cr, uid, policy,
controlling_date,
context=context)
lines = lines.union(add_ids).difference(remove_ids)
add_ids, remove_ids = self._get_invoice_related_lines(cr, uid, policy,
controlling_date,
context=context)
lines = lines.union(add_ids).difference(remove_ids)
return lines
def _lines_different_policy(self, cr, uid, policy_id, lines, context=None):
""" Return a set of move lines ids for which there is an existing credit line
but with a different policy.
"""
different_lines = set()
if not lines:
return different_lines
assert not (isinstance(policy_id, list) and len(policy_id) > 1), \
"policy_id: only one id expected"
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"
" AND manually_overridden IS false",
(policy_id, tuple(lines)))
res = cr.fetchall()
if res:
different_lines.update([x[0] for x in res])
return different_lines
def check_policy_against_account(self, cr, uid, account_id, policy_id,
context=None):
"""Ensure that the policy corresponds 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"""
_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,
translate=True),
'level': fields.integer('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 days)', required='True'),
'email_template_id': fields.many2one('email.template',
'Email Template',
required=True),
'channel': fields.selection([('letter', 'Letter'),
('email', 'Email')],
'Channel', required=True),
'custom_text': fields.text('Custom Message',
required=True,
translate=True),
'custom_mail_text': fields.text('Custom Mail Message',
required=True, translate=True),
}
def _check_level_mode(self, cr, uid, rids, context=None):
""" The smallest level of a policy cannot be computed on the
"previous_date". Return False if this happens. """
if isinstance(rids, (int, long)):
rids = [rids]
for level in self.browse(cr, uid, rids, context):
smallest_level_id = self.search(
cr, uid,
[('policy_id', '=', level.policy_id.id)],
order='level asc', limit=1, context=context)
smallest_level = self.browse(cr, 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, cr, 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(
cr,
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
# ----- sql time related methods ---------
def _net_days_get_boundary(self):
return (" (mv_line.date_maturity + %(delay)s)::date <= "
"date(%(controlling_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(%(controlling_date)s)")
def _previous_date_get_boundary(self):
return "(cr_line.date + %(delay)s)::date <= date(%(controlling_date)s)"
def _get_sql_date_boundary_for_computation_mode(self, cr, uid, level,
controlling_date,
context=None):
"""Return a where clauses statement for the given
controlling 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_move_line_ids(self, cr, uid, level, controlling_date,
lines, context=None):
"""Retrieve all the move lines that are linked to a first level.
We use Raw SQL for performance. Security rule where applied in
policy object when the first set of lines were retrieved"""
level_lines = set()
if not lines:
return level_lines
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 id\n"
" 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 overridden
# have to be generated again for the previous level
" AND NOT manually_overridden\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
)
data_dict = {'controlling_date': controlling_date,
'line_ids': tuple(lines),
'delay': level.delay_days}
cr.execute(sql, data_dict)
res = cr.fetchall()
if res:
level_lines.update([x[0] for x in res])
return level_lines
def _get_other_level_move_line_ids(self, cr, uid, level, controlling_date,
lines, context=None):
""" Retrieve the move lines for other levels than first level.
"""
level_lines = set()
if not lines:
return level_lines
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"
" AND state != 'ignored'"
" AND NOT manually_overridden"
" 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 overridden
# have to be generated again for the previous level
" AND NOT manually_overridden\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,
controlling_date,
context
)
previous_level_id = self._previous_level(cr, uid, level,
context=context)
previous_level = self.browse(cr, uid, previous_level_id,
context=context)
data_dict = {'controlling_date': controlling_date,
'line_ids': tuple(lines),
'delay': level.delay_days,
'previous_level': previous_level.level}
# print cr.mogrify(sql, data_dict)
cr.execute(sql, data_dict)
res = cr.fetchall()
if res:
level_lines.update([x[0] for x in res])
return level_lines
def get_level_lines(self, cr, uid, level_id, controlling_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 = set()
level = self.browse(cr, uid, level_id, context=context)
if self._previous_level(cr, uid, level, context=context) is None:
method = self._get_first_level_move_line_ids
else:
method = self._get_other_level_move_line_ids
matching_lines.update(method(cr, uid, level,
controlling_date, lines,
context=context))
return matching_lines

View File

@@ -1 +0,0 @@
from . import credit_control_summary

View File

@@ -1,244 +0,0 @@
## -*- 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>${_('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.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>

View File

@@ -1,16 +0,0 @@
<openerp>
<data>
<report auto="False"
id="report_webkit_html"
model="credit.control.communication"
name="credit_control_summary"
file="account_credit_control/report/credit_control_summary.html.mako"
string="Credit Summary"
report_type="webkit"
webkit_header="report_webkit.ir_header_webkit_basesample0"/>
<record model="ir.actions.report.xml" id="report_webkit_html">
<field name="precise_mode" eval="True"/>
</record>
</data>
</openerp>

View File

@@ -1,189 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from openerp.osv import orm, fields
from openerp.tools.translate import _
logger = logging.getLogger('credit.control.run')
class CreditControlRun(orm.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('Controlling Date', required=True),
'policy_ids': fields.many2many(
'credit.control.policy',
rel="credit_run_policy_rel",
id1='run_id', id2='policy_id',
string='Policies',
readonly=True,
states={'draft': [('readonly', False)]}
),
'report': fields.text('Report', readonly=True),
'state': fields.selection([('draft', 'Draft'),
('done', 'Done')],
string='State',
required=True,
readonly=True),
'manual_ids': fields.many2many(
'account.move.line',
rel="credit_runreject_rel",
string='Lines to handle manually',
help=('If a credit control line has been generated'
'on a policy and the policy has been changed '
'in the meantime, it has to be handled '
'manually'),
readonly=True
),
}
def copy_data(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
else:
default = default.copy()
default.update({
'report': False,
'manual_ids': False,
})
return super(CreditControlRun, self).copy_data(
cr, uid, id, default=default, context=context)
def _get_policies(self, cr, uid, context=None):
return self.pool['credit.control.policy'].search(cr, uid, [],
context=context)
_defaults = {'state': 'draft',
'policy_ids': _get_policies}
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
"""
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 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):
""" Generate credit control lines. """
cr_line_obj = self.pool.get('credit.control.line')
assert not (isinstance(run_id, list) and len(run_id) > 1), \
"run_id: only one id expected"
if isinstance(run_id, list):
run_id = run_id[0]
run = self.browse(cr, uid, run_id, context=context)
manually_managed_lines = set() # line who changed policy
credit_line_ids = [] # generated lines
run._check_run_date(run.date, context=context)
policies = run.policy_ids
if not policies:
raise orm.except_orm(_('Error'),
_('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)
manually_managed_lines.update(manual_lines)
policy_generated_ids = []
if lines:
# policy levels are sorted by level
# so iteration is in the correct order
for level in reversed(policy.level_ids):
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))
credit_line_ids += policy_generated_ids
else:
report += _(
"Policy \"%s\" has not generated any "
"Credit Control Lines.\n" % policy.name
)
vals = {'state': 'done',
'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
Lock the ``credit_control_run`` Postgres table to avoid concurrent
calls of this method.
"""
try:
cr.execute('SELECT id FROM credit_control_run'
' LIMIT 1 FOR UPDATE NOWAIT')
except Exception:
# In case of exception openerp will do a rollback
# for us and free the lock
raise orm.except_orm(_('Error'),
_('A credit control run is already running'
' in background, please try later.'))
self._generate_credit_lines(cr, uid, run_id, context)
return True

View File

@@ -1,99 +0,0 @@
# -*- coding: utf-8 -*-
# flake8: noqa
from support import *
import datetime
@step('I import invoice "{inv_name}" using import invoice button')
def impl(ctx, inv_name):
invoice = model('account.invoice').get([('name', '=', inv_name)])
assert invoice
bank_statement = ctx.found_item
for line in bank_statement.line_ids:
line.unlink()
lines = model('account.move.line').browse([('move_id', '=', invoice.move_id.id),
('account_id', '=', invoice.account_id.id)])
wizard = model('account.statement.from.invoice.lines').create({'line_ids': lines})
wizard.populate_statement({'statement_id': bank_statement.id})
@given(u'I should have a "account.bank.statement.line" with name: "{name}" and amount: "{amount}"')
def impl(ctx, name, amount):
assert ctx.found_item
line = model('account.bank.statement.line').get([('name', '=', name),
('amount', '=', amount),
('statement_id', '=', ctx.found_item.id)])
assert line
ctx.line = line
@given(u'I set the voucher paid amount to "{amount}"')
def impl(ctx, amount):
assert ctx.line
voucher = model('account.voucher').get(ctx.line.voucher_id.id)
assert voucher
vals = voucher.onchange_amount(float(amount),
voucher.payment_rate,
voucher.partner_id.id,
voucher.journal_id.id if voucher.journal_id else False,
voucher.currency_id.id if voucher.currency_id else False,
voucher.type,
voucher.date,
voucher.payment_rate,
voucher.company_id.id if voucher.company_id else false)
vals = vals['value']
vals.update({'amount': ctx.line.voucher_id.amount})
voucher_line_ids = []
voucher_line_dr_ids = []
v_l_obj = model('account.voucher.line')
for v_line_vals in vals.get('line_cr_ids', []) or []:
v_line_vals['voucher_id'] = voucher.id
voucher_line_ids.append(v_l_obj.create(v_line_vals).id)
vals['line_cr_ids'] = voucher_line_ids
for v_line_vals in vals.get('line_dr_ids', []) or []:
v_line_vals['voucher_id'] = voucher.id
voucher_line_dr_ids.append(v_l_obj.create(v_line_vals).id)
vals['line_dr_ids'] = voucher_line_ids
voucher.write(vals)
ctx.vals = vals
ctx.voucher = voucher
@given(u'I save the voucher')
def impl(ctx):
assert True
@given(u'I modify the line amount to "{amount}"')
@then(u'I modify the line amount to "{amount}"')
def impl(ctx, amount):
assert ctx.line
# we have to change voucher amount before chaning statement line amount
if ctx.line.voucher_id:
model('account.voucher').write([ctx.line.voucher_id.id],
{'amount': float(amount)})
ctx.line.write({'amount': float(amount)})
@step('My invoice "{inv_name}" is in state "{state}" reconciled with a residual amount of "{amount:f}"')
def impl(ctx, inv_name, state, amount):
invoice = model('account.invoice').get([('name', '=', inv_name)])
assert_almost_equal(invoice.residual, amount)
assert_equal(invoice.state, state)
@step('I modify the bank statement line amount to {amount:f}')
def impl(ctx, amount):
line = ctx.found_item.voucher_id.line_cr_ids[0]
#ctx.voucher = model('account.voucher').get(ctx.found_item.voucher_id.id)
ctx.found_item.on_change('onchange_amount', 'amount', (), amount)
@then(u'I set bank statement end-balance')
@given(u'I set bank statement end-balance')
def impl(ctx):
assert ctx.found_item, "No statement found"
ctx.found_item.write({'balance_end_real': ctx.found_item.balance_end})
assert ctx.found_item.balance_end == ctx.found_item.balance_end_real
@when(u'I confirm bank statement')
def impl(ctx):
assert ctx.found_item
assert_equal(ctx.found_item._model._name, 'account.bank.statement')
ctx.found_item.button_confirm_bank()

View File

@@ -1,229 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import netsvc
import logging
from openerp.osv.orm import TransientModel, fields
logger = logging.getLogger('credit.control.line.mailing')
class CreditCommunication(TransientModel):
"""Shell class 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_control_line_ids': 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')
}
def _get_comp(self, cr, uid, context=None):
return self.pool.get('res.company')._company_default_get(
cr, uid,
'credit.control.policy',
context=context
)
_defaults = {
'company_id': _get_comp,
'user_id': lambda s, cr, uid, c: uid
}
def get_email(self, cr, uid, com_id, context=None):
"""Return a valid email for customer"""
if isinstance(com_id, list):
assert len(com_id) == 1, "get_email only support one id as param."
com_id = com_id[0]
form = self.browse(cr, uid, com_id, context=context)
contact = form.get_contact_address()
return contact.email
def get_contact_address(self, cr, uid, com_id, context=None):
pmod = self.pool['res.partner']
if isinstance(com_id, list):
com_id = com_id[0]
form = self.browse(cr, uid, com_id, context=context)
part = form.partner_id
add_ids = part.address_get(adr_pref=['invoice']) or {}
add_id = add_ids.get('invoice', add_ids.get('default', False))
return pmod.browse(cr, uid, add_id, context)
def _get_credit_lines(self, cr, 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(cr,
uid,
[('id', 'in', line_ids),
('partner_id', '=', partner_id),
('policy_level_id', '=', level_id)],
context=context)
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_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, "
" credit_control_line.currency_id"
)
cr.execute(sql, (tuple(line_ids),))
res = cr.dictfetchall()
for level_assoc in res:
data = {}
data['credit_control_line_ids'] = [
(
6, 0, self._get_credit_lines(
cr, 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(cr, uid, data, context=context)
comms.append(self.browse(cr, uid, comm_id, context=context))
return comms
def _generate_emails(self, cr, uid, comms, context=None):
"""Generate email message using template related to level"""
cr_line_obj = self.pool.get('credit.control.line')
email_temp_obj = self.pool.get('email.template')
email_message_obj = self.pool.get('mail.mail')
att_obj = self.pool.get('ir.attachment')
email_ids = []
essential_fields = ['subject',
'body_html',
'email_from',
'email_to']
for comm in comms:
# we want to use a local cr in order to send the maximum
# of email
template = comm.current_policy_level.email_template_id.id
email_values = {}
cl_ids = [cl.id for cl in comm.credit_control_line_ids]
email_values = email_temp_obj.generate_email(cr, uid,
template,
comm.id,
context=context)
email_values['body_html'] = email_values['body']
email_values['type'] = 'email'
email_id = email_message_obj.create(cr, uid, email_values,
context=context)
state = 'sent'
# The mail will not be send, however it will be in the pool, in an
# error state. So we create it, link it with
# the credit control line
# and put this latter in a `email_error` state we not that we have
# a problem with the email
if any(not email_values.get(field) for field in essential_fields):
state = 'email_error'
cr_line_obj.write(
cr, uid, cl_ids,
{'mail_message_id': email_id,
'state': state},
context=context
)
att_ids = []
for att in email_values.get('attachments', []):
attach_fname = att[0]
attach_datas = att[1]
data_attach = {
'name': attach_fname,
'datas': attach_datas,
'datas_fname': attach_fname,
'res_model': 'mail.mail',
'res_id': email_id,
'type': 'binary',
}
att_ids.append(
att_obj.create(cr, uid, data_attach,
context=context)
)
email_message_obj.write(cr, uid, [email_id],
{'attachment_ids': [(6, 0, att_ids)]},
context=context)
email_ids.append(email_id)
return email_ids
def _generate_report(self, cr, 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(cr, uid, ids, {}, {})
return result
def _mark_credit_line_as_sent(self, cr, uid, comms, context=None):
line_ids = []
for comm in comms:
line_ids += [x.id for x in comm.credit_control_line_ids]
l_obj = self.pool.get('credit.control.line')
l_obj.write(cr, uid, line_ids, {'state': 'sent'}, context=context)
return line_ids

View File

@@ -1,89 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp.tools.translate import _
class CreditControlEmailer(orm.TransientModel):
"""Send emails for each selected credit control lines."""
_name = "credit.control.emailer"
_description = """Mass credit line emailer"""
_rec_name = 'id'
def _get_line_ids(self, cr, uid, context=None):
if context is None:
context = {}
res = False
if (context.get('active_model') == 'credit.control.line' and
context.get('active_ids')):
res = self._filter_line_ids(
cr, uid,
context['active_ids'],
context=context
)
return res
_columns = {
'line_ids': fields.many2many(
'credit.control.line',
string='Credit Control Lines',
domain=[('state', '=', 'to_be_sent'),
('channel', '=', 'email')]
),
}
_defaults = {
'line_ids': _get_line_ids,
}
def _filter_line_ids(self, cr, uid, active_ids, context=None):
"""filter lines to use in the wizard"""
line_obj = self.pool.get('credit.control.line')
domain = [('state', '=', 'to_be_sent'),
('id', 'in', active_ids),
('channel', '=', 'email')]
return line_obj.search(cr, uid, domain, context=context)
def email_lines(self, cr, uid, wiz_id, context=None):
assert not (isinstance(wiz_id, list) and len(wiz_id) > 1), \
"wiz_id: only one id expected"
comm_obj = self.pool.get('credit.control.communication')
if isinstance(wiz_id, list):
wiz_id = wiz_id[0]
form = self.browse(cr, uid, wiz_id, context)
if not form.line_ids:
raise orm.except_orm(
_('Error'),
_('No credit control lines selected.')
)
line_ids = [l.id for l in form.line_ids]
filtered_ids = self._filter_line_ids(
cr, uid, line_ids, context
)
comms = comm_obj._generate_comm_from_credit_line_ids(
cr, uid, filtered_ids, context=context
)
comm_obj._generate_emails(cr, uid, comms, context=context)
return {}

View File

@@ -1,109 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp.tools.translate import _
class CreditControlMarker(orm.TransientModel):
"""Change the state of lines in mass"""
_name = 'credit.control.marker'
_description = 'Mass marker'
def _get_line_ids(self, cr, uid, context=None):
if context is None:
context = {}
res = False
if (context.get('active_model') == 'credit.control.line' and
context.get('active_ids')):
res = self._filter_line_ids(
cr, uid,
context['active_ids'],
context=context
)
return res
_columns = {
'name': fields.selection([('ignored', 'Ignored'),
('to_be_sent', 'Ready To Send'),
('sent', 'Done')],
'Mark as',
required=True),
'line_ids': fields.many2many(
'credit.control.line',
string='Credit Control Lines',
domain="[('state', '!=', 'sent')]"),
}
_defaults = {
'name': 'to_be_sent',
'line_ids': _get_line_ids,
}
def _filter_line_ids(self, cr, uid, active_ids, context=None):
"""get line to be marked filter done lines"""
line_obj = self.pool.get('credit.control.line')
domain = [('state', '!=', 'sent'), ('id', 'in', active_ids)]
return line_obj.search(cr, uid, domain, context=context)
def _mark_lines(self, cr, 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(cr, uid, filtered_ids,
{'state': state},
context=context)
return filtered_ids
def mark_lines(self, cr, uid, wiz_id, context=None):
"""Write state of selected credit lines to the one in entry
done credit line will be ignored"""
assert not (isinstance(wiz_id, list) and len(wiz_id) > 1), \
"wiz_id: only one id expected"
if isinstance(wiz_id, list):
wiz_id = wiz_id[0]
form = self.browse(cr, uid, wiz_id, context)
if not form.line_ids:
raise orm.except_orm(
_('Error'),
_('No credit control lines selected.')
)
line_ids = [l.id for l in form.line_ids]
filtered_ids = self._filter_line_ids(cr, uid, line_ids, context)
if not filtered_ids:
raise orm.except_orm(
_('Information'),
_('No lines will be changed. '
'All the selected lines are already done.')
)
self._mark_lines(cr, uid, filtered_ids, form.name, context)
return {'domain': unicode([('id', 'in', filtered_ids)]),
'view_type': 'form',
'view_mode': 'tree,form',
'view_id': False,
'res_model': 'credit.control.line',
'type': 'ir.actions.act_window'}

View File

@@ -1,182 +0,0 @@
# -*- 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',
required=True),
# 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_overridden(self, cr, uid, move_lines, context=None):
"""Mark `move_lines` related credit control line as overridden
This is done by setting manually_overridden fields to True
:param move_lines: move line to mark as overridden
:retun: list of credit line ids that where marked as overridden
"""
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_overridden': 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_overridden(
cr,
uid,
wizard.move_line_ids,
context=context
)
# As disscused with business expert
# draft lines 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
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

View File

@@ -1,113 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import base64
from openerp.osv import orm, fields
from openerp.tools.translate import _
class CreditControlPrinter(orm.TransientModel):
"""Print lines"""
_name = "credit.control.printer"
_rec_name = 'id'
_description = 'Mass printer'
def _get_line_ids(self, cr, uid, context=None):
if context is None:
context = {}
res = False
if context.get('active_model') != 'credit.control.line':
return res
res = context.get('active_ids', False)
return res
_columns = {
'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',
string='Credit Control Lines'),
}
_defaults = {
'mark_as_sent': True,
'line_ids': _get_line_ids,
}
def _filter_line_ids(self, cr, uid, active_ids, context=None):
"""filter lines to use in the wizard"""
line_obj = self.pool.get('credit.control.line')
domain = [('state', '=', 'to_be_sent'),
('id', 'in', active_ids),
('channel', '=', 'letter')]
return line_obj.search(cr, uid, domain, context=context)
def _credit_line_predicate(self, cr, uid, line_record, context=None):
return True
def _get_line_ids(self, cr, uid, lines, predicate, context=None):
return [l.id for l in lines if predicate(cr, uid, l, context)]
def print_lines(self, cr, uid, wiz_id, context=None):
assert not (isinstance(wiz_id, list) and len(wiz_id) > 1), \
"wiz_id: only one id expected"
comm_obj = self.pool.get('credit.control.communication')
if isinstance(wiz_id, list):
wiz_id = wiz_id[0]
form = self.browse(cr, uid, wiz_id, context)
if not form.line_ids and not form.print_all:
raise orm.except_orm(_('Error'),
_('No credit control lines selected.'))
line_ids = self._get_line_ids(cr,
uid,
form.line_ids,
self._credit_line_predicate,
context=context)
comms = comm_obj._generate_comm_from_credit_line_ids(cr, uid, line_ids,
context=context)
report_file = comm_obj._generate_report(cr, uid, comms,
context=context)
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)
return {'type': 'ir.actions.act_window',
'res_model': 'credit.control.printer',
'view_mode': 'form',
'view_type': 'form',
'res_id': form.id,
'views': [(False, 'form')],
'target': 'new',
}

View File

@@ -0,0 +1,29 @@
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.
Configure a tolerance for the Credit control and a default policy
applied on all partners in each company, under the Accounting tab.
You are able to specify a particular policy for one partner or one invoice.
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)

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012 Camptocamp SA
# Copyright 2012-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
@@ -26,5 +26,4 @@ from . import partner
from . import policy
from . import company
from . import wizard
from . import report
from . import invoice

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012 Camptocamp SA
# Copyright 2012-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
@@ -24,44 +24,13 @@
'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.
Configure a tolerance for the Credit control and a default policy
applied on all partners in each company, under the Accounting tab.
You are able to specify a particular policy for one partner or one invoice.
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)
""",
'depends': ['base',
'account',
'email_template',
],
'website': 'http://www.camptocamp.com',
'data': ["report/report.xml",
"report/report_credit_control_summary.xml",
"data.xml",
"line_view.xml",
"account_view.xml",
@@ -74,9 +43,9 @@ On each generated line, you have many choices:
"wizard/credit_control_printer_view.xml",
"wizard/credit_control_policy_changer_view.xml",
"security/ir.model.access.csv"],
'demo_xml': ["credit_control_demo.xml"],
'demo': ["credit_control_demo.xml"],
'tests': [],
'installable': False,
'installable': True,
'license': 'AGPL-3',
'application': True
}

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012 Camptocamp SA
# Copyright 2012-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
@@ -18,24 +18,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import time
from openerp.report import report_sxw
from openerp import models, fields
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,
})
class AccountAccount(models.Model):
""" Add a link to a credit control policy on account.account """
report_sxw.report_sxw(
'report.credit_control_summary',
'credit.control.communication',
'addons/account_credit_control/report/credit_control_summary.html.mako',
parser=CreditSummaryReport
)
_inherit = "account.account"
credit_control_line_ids = fields.One2many('credit.control.line',
'account_id',
string='Credit Lines',
readonly=True)

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012 Camptocamp SA
# Copyright 2012-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
@@ -18,24 +18,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp import models, fields
class ResCompany(orm.Model):
"""Add credit control parameters"""
class ResCompany(models.Model):
""" Add credit control parameters """
_inherit = 'res.company'
_columns = {
'credit_control_tolerance': fields.float('Credit Control Tolerance'),
# This is not a property on the partner because we cannot search
# on fields.property (subclass fields.function).
'credit_policy_id': fields.many2one(
'credit.control.policy',
'Credit Control Policy',
help=("The Credit Control Policy used on partners"
" by default. This setting can be overridden"
" on partners or invoices.")
),
}
_defaults = {"credit_control_tolerance": 0.1}
credit_control_tolerance = fields.Float(string='Credit Control Tolerance',
default=0.1)
# This is not a property on the partner because we cannot search
# on fields.property (subclass fields.function).
credit_policy_id = fields.Many2one('credit.control.policy',
string='Credit Control Policy',
help="The Credit Control Policy used "
"on partners by default. "
"This setting can be overridden"
" on partners or invoices.")

View File

@@ -9,9 +9,9 @@
<field name="model_id" ref="model_credit_control_communication"/>
<field name="auto_delete" eval="True"/>
<field name="lang">${object.get_contact_address().lang or 'en_US'}</field>
<field name="report_template" ref="report_webkit_html"/>
<field name="report_template" ref="credit_control_summary"/>
<field name="body_html"><![CDATA[
Dear ${object.get_contact_address().name or ''}
Dear ${object.contact_address.name or ''}
<br/>
<br/>
${object.current_policy_level.custom_mail_text}
@@ -63,7 +63,7 @@ Thank you in advance for your anticipated cooperation in this matter.
Best regards
</field>
<field name="custom_mail_text">Our records indicate that we have not received the payment of the above mentioned invoice (copy attached for your convenience).
<field name="custom_mail_text">Our records indicate that we have not received the payment of the invoices mentioned in the attached document.
If it has already been sent, please disregard this notice. If not, please proceed with payment within 10 days.
@@ -89,7 +89,7 @@ Thank you in advance for your anticipated cooperation in this matter.
Best regards
</field>
<field name="custom_mail_text">Our records indicate that we have not yet received the payment of the above mentioned invoice (copy attached for your convenience) despite our first reminder.
<field name="custom_mail_text">Our records indicate that we have not yet received the payment of the invoices mentioned in the attached document despite our first reminder.
If it has already been sent, please disregard this notice. If not, please proceed with payment within 5 days.
Thank you in advance for your anticipated cooperation in this matter.
@@ -120,7 +120,7 @@ Best regards
Best regards
</field>
<field name="custom_mail_text">Our records indicate that we still have not received the payment of the above mentioned invoice (copy attached) despite our two reminders.
<field name="custom_mail_text">Our records indicate that we still have not received the payment of the invoices mentioned in the attached document despite our two reminders.
If payment have already been sent, please disregard this notice. If not, please proceed with payment.
If your payment has not been received in the next 5 days, your file will be transfered to our debt collection agency.
@@ -155,7 +155,7 @@ Best regards
Best regards
</field>
<field name="custom_mail_text">Our records indicate that we have not received the payment of the above mentioned invoice (copy attached for your convenience).
<field name="custom_mail_text">Our records indicate that we have not received the payment of the invoices mentioned in the attached document.
If it has already been sent, please disregard this notice. If not, please proceed with payment within 10 days.
Thank you in advance for your anticipated cooperation in this matter.
@@ -186,7 +186,7 @@ Best regards
Best regards
</field>
<field name="custom_mail_text">Our records indicate that we still have not received the payment of the above mentioned invoice (copy attached) despite our reminder.
<field name="custom_mail_text">Our records indicate that we still have not received the payment of the invoices mentioned in the attached document despite our reminder.
If payment have already been sent, please disregard this notice. If not, please proceed with payment.
If your payment has not been received in the next 5 days, your file will be transfered to our debt

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Vincent Renaville
# Copyright 2013 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 import models, fields, api, _
class AccountInvoice(models.Model):
"""Check on cancelling of an invoice"""
_inherit = 'account.invoice'
credit_policy_id = fields.Many2one(
'credit.control.policy',
string='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,
copy=False,
)
credit_control_line_ids = fields.One2many(
'credit.control.line', 'invoice_id',
string='Credit Lines',
readonly=True,
copy=False,
)
@api.multi
def action_cancel(self):
"""Prevent to cancel invoice related to credit line"""
# We will search if this invoice is linked with credit
cc_line_obj = self.env['credit.control.line']
for invoice in self:
nondraft_domain = [('invoice_id', '=', invoice.id),
('state', '!=', 'draft')]
cc_nondraft_lines = cc_line_obj.search(nondraft_domain)
if cc_nondraft_lines:
raise api.Warning(
_('You cannot cancel this invoice.\n'
'A payment reminder has already been '
'sent to the customer.\n'
'You must create a credit note and '
'issue a new invoice.')
)
draft_domain = [('invoice_id', '=', invoice.id),
('state', '=', 'draft')]
cc_draft_line = cc_line_obj.search(draft_domain)
cc_draft_line.unlink()
return super(AccountInvoice, self).action_cancel()

View File

@@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012-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 import models, fields, api, _
logger = logging.getLogger('credit.line.control')
class CreditControlLine(models.Model):
""" A credit control line describes an amount due by a customer for a due date.
A line is created once the due date of the payment is exceeded.
It is created in "draft" and some actions are available (send by email,
print, ...)
"""
_name = "credit.control.line"
_description = "A credit control line"
_rec_name = "id"
_order = "date DESC"
date = fields.Date(string='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(string='Due date',
required=True,
readonly=True,
states={'draft': [('readonly', False)]})
date_entry = fields.Date(string='Entry date',
related='move_line_id.date',
store=True,
readonly=True)
date_sent = fields.Date(string='Sent date',
readonly=True,
states={'draft': [('readonly', False)]})
state = fields.Selection([('draft', 'Draft'),
('ignored', 'Ignored'),
('to_be_sent', 'Ready To Send'),
('sent', 'Done'),
('error', 'Error'),
('email_error', 'Emailing Error')],
'State',
required=True,
readonly=True,
default='draft',
help="Draft lines need to be triaged.\n"
"Ignored lines are lines for which we do "
"not want to send something.\n"
"Draft and ignored lines will be "
"generated again on the next run.")
channel = fields.Selection([('letter', 'Letter'),
('email', 'Email')],
string='Channel',
required=True,
readonly=True,
states={'draft': [('readonly', False)]})
invoice_id = fields.Many2one('account.invoice',
string='Invoice',
readonly=True)
partner_id = fields.Many2one('res.partner',
string='Partner',
required=True)
amount_due = fields.Float(string='Due Amount Tax incl.',
required=True, readonly=True)
balance_due = fields.Float(string='Due balance', required=True,
readonly=True)
mail_message_id = fields.Many2one('mail.mail', string='Sent Email',
readonly=True)
move_line_id = fields.Many2one('account.move.line',
string='Move line',
required=True,
readonly=True)
account_id = fields.Many2one('account.account',
related='move_line_id.account_id',
store=True,
readonly=True)
currency_id = fields.Many2one('res.currency',
related='move_line_id.currency_id',
store=True,
readonly=True)
company_id = fields.Many2one('res.company',
related='move_line_id.company_id',
store=True,
readonly=True)
# we can allow a manual change of policy in draft state
policy_level_id = fields.Many2one('credit.control.policy.level',
string='Overdue Level',
required=True,
readonly=True,
states={'draft': [('readonly', False)]})
policy_id = fields.Many2one('credit.control.policy',
related='policy_level_id.policy_id',
store=True,
readonly=True)
level = fields.Integer('credit.control.policy.level',
related='policy_level_id.level',
store=True,
readonly=True)
manually_overridden = fields.Boolean(string='Manually overridden')
@api.model
def _prepare_from_move_line(self, move_line, level, controlling_date,
open_amount):
""" Create credit control line """
data = {}
data['date'] = controlling_date
data['date_due'] = move_line.date_maturity
data['state'] = 'draft'
data['channel'] = level.channel
data['invoice_id'] = (move_line.invoice.id if
move_line.invoice else False)
data['partner_id'] = move_line.partner_id.id
data['amount_due'] = (move_line.amount_currency or move_line.debit or
move_line.credit)
data['balance_due'] = open_amount
data['policy_level_id'] = level.id
data['company_id'] = move_line.company_id.id
data['move_line_id'] = move_line.id
return data
@api.model
def create_or_update_from_mv_lines(self, lines, level, controlling_date,
check_tolerance=True):
""" 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 recordset
:param level: credit.control.policy.level record
: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: recordset of created credit lines
"""
currency_obj = self.env['res.currency']
user = self.env.user
currencies = currency_obj.search([])
tolerance = {}
tolerance_base = user.company_id.credit_control_tolerance
user_currency = user.company_id.currency_id
for currency in currencies:
tolerance[currency.id] = currency.compute(tolerance_base,
user_currency)
new_lines = self.browse()
for move_line in lines:
open_amount = move_line.amount_residual_currency
cur_tolerance = tolerance.get(move_line.currency_id.id,
tolerance_base)
if check_tolerance and open_amount < cur_tolerance:
continue
vals = self._prepare_from_move_line(move_line,
level,
controlling_date,
open_amount)
line = self.create(vals)
new_lines += line
# 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_drafts = self.search([('move_line_id', '=', move_line.id),
('policy_level_id', '=', level.id),
('state', '=', 'draft'),
('id', '!=', line.id)])
if previous_drafts:
previous_drafts.write({'state': 'ignored'})
return new_lines
@api.multi
def unlink(self):
for line in self:
if line.state != 'draft':
raise api.Warning(
_('You are not allowed to delete a credit control '
'line that is not in draft state.')
)
return super(CreditControlLine, self).unlink()

View File

@@ -6,23 +6,41 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Control Credit Lines">
<field name="date"/>
<field name="date_due"/>
<field name="date_sent"/>
<field name="level"/>
<field name="manually_overridden"/>
<field name="state"/>
<field name="channel"/>
<field name="invoice_id"/>
<field name="partner_id"/>
<field name="amount_due"/>
<field name="balance_due"/>
<field name="currency_id"/>
<field name="move_line_id"/>
<field name="account_id"/>
<field name="policy_level_id"/>
<field name="policy_id"/>
<field name="mail_message_id"/>
<header>
<field name="state" widget="statusbar"
statusbar_visible="draft,to_be_sent,sent"
statusbar_colors='{"error":"red","email_error":"red"}'/>
</header>
<sheet>
<group>
<group>
<field name="partner_id"/>
<field name="policy_level_id"/>
<field name="policy_id"/>
<field name="level"/>
<field name="manually_overridden"/>
</group>
<group>
<field name="date"/>
<field name="date_due"/>
<field name="channel"/>
<field name="date_sent"/>
<field name="mail_message_id"/>
</group>
</group>
<group>
<group>
<field name="invoice_id"/>
<field name="move_line_id"/>
<field name="account_id"/>
</group>
<group>
<field name="amount_due"/>
<field name="balance_due"/>
<field name="currency_id"/>
</group>
</group>
</sheet>
</form>
</field>
</record>

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012 Camptocamp SA
# Copyright 2012-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
@@ -18,14 +18,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp import models, fields
class Mail(orm.Model):
class Mail(models.Model):
_inherit = 'mail.mail'
# use HTML fields instead of text
_columns = {
'body_html': fields.html('Rich-text Contents',
help="Rich-text/HTML message"),
}
body_html = fields.Html('Rich-text Contents',
help="Rich-text/HTML message")

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012-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 import models, fields, api
class ResPartner(models.Model):
""" Add a settings on the credit control policy to use on the partners,
and links to the credit control lines.
"""
_inherit = "res.partner"
credit_policy_id = fields.Many2one(
'credit.control.policy',
string='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)
@api.constrains('credit_policy_id')
def _check_credit_policy(self):
""" Ensure that policy on partner are limited to the account policy """
for partner in self:
if (not partner.property_account_receivable or
not partner.credit_policy_id):
continue
account = partner.property_account_receivable
policy = partner.credit_policy_id
try:
policy.check_policy_against_account(account)
except api.Warning as err:
# constrains should raise ValidationError exceptions
raise api.ValidationError(err)

View File

@@ -0,0 +1,406 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012-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 import models, fields, api, _
class CreditControlPolicy(models.Model):
""" Define a policy of reminder """
_name = "credit.control.policy"
_description = """Define a reminder policy"""
name = fields.Char('Name', required=True)
level_ids = fields.One2many('credit.control.policy.level',
'policy_id',
string='Policy Levels')
do_nothing = fields.Boolean('Do nothing',
help='For policies which should not '
'generate lines or are obsolete')
company_id = fields.Many2one('res.company', string='Company')
account_ids = fields.Many2many(
'account.account',
string='Accounts',
required=True,
domain="[('type', '=', 'receivable')]",
help="This policy will be active only"
" for the selected accounts",
)
active = fields.Boolean('Active', default=True)
@api.multi
def _move_lines_domain(self, controlling_date):
""" Build the default domain for searching move lines """
self.ensure_one()
return [('account_id', 'in', self.account_ids.ids),
('date_maturity', '<=', controlling_date),
('reconcile_id', '=', False),
('partner_id', '!=', False)]
@api.multi
@api.returns('account.move.line')
def _due_move_lines(self, controlling_date):
""" Get the due move lines for the policy of the company.
The set of ids will be reduced and extended according
to the specific policies defined on partners and invoices.
Do not use direct SQL in order to respect security rules.
Assume that only the receivable lines have a maturity date and that
accounts used in the policy are reconcilable.
"""
self.ensure_one()
move_l_obj = self.env['account.move.line']
user = self.env.user
if user.company_id.credit_policy_id.id != self.id:
return move_l_obj.browse()
domain_line = self._move_lines_domain(controlling_date)
return move_l_obj.search(domain_line)
@api.multi
@api.returns('account.move.line')
def _move_lines_subset(self, controlling_date, model, move_relation_field):
""" Get the move lines related to one model for a policy.
Do not use direct SQL in order to respect security rules.
Assume that only the receivable lines have a maturity date and that
accounts used in the policy are reconcilable.
The policy relation field must be named credit_policy_id.
:param str controlling_date: date of credit control
:param str model: name of the model where is defined a credit_policy_id
:param str move_relation_field: name of the field in account.move.line
which is a many2one to `model`
:return: recordset to add in the process, recordset to remove from
the process
"""
self.ensure_one()
# MARK possible place for a good optimisation
my_obj = self.env[model]
move_l_obj = self.env['account.move.line']
default_domain = self._move_lines_domain(controlling_date)
to_add = move_l_obj.browse()
to_remove = move_l_obj.browse()
# The lines which are linked to this policy have to be included in the
# run for this policy.
# If another object override the credit_policy_id (ie. invoice after
add_objs = my_obj.search([('credit_policy_id', '=', self.id)])
if add_objs:
domain = list(default_domain)
domain.append((move_relation_field, 'in', add_objs.ids))
to_add = move_l_obj.search(domain)
# The lines which are linked to another policy do not have to be
# included in the run for this policy.
neg_objs = my_obj.search([('credit_policy_id', '!=', self.id),
('credit_policy_id', '!=', False)])
if neg_objs:
domain = list(default_domain)
domain.append((move_relation_field, 'in', neg_objs.ids))
to_remove = move_l_obj.search(domain)
return to_add, to_remove
@api.multi
@api.returns('account.move.line')
def _get_partner_related_lines(self, controlling_date):
""" Get the move lines for a policy related to a partner.
:param str controlling_date: date of credit control
:param str model: name of the model where is defined a credit_policy_id
:param str move_relation_field: name of the field in account.move.line
which is a many2one to `model`
:return: recordset to add in the process, recordset to remove from
the process
"""
return self._move_lines_subset(controlling_date, 'res.partner',
'partner_id')
@api.multi
@api.returns('account.move.line')
def _get_invoice_related_lines(self, controlling_date):
""" Get the move lines for a policy related to an invoice.
:param str controlling_date: date of credit control
:param str model: name of the model where is defined a credit_policy_id
:param str move_relation_field: name of the field in account.move.line
which is a many2one to `model`
:return: recordset to add in the process, recordset to remove from
the process
"""
return self._move_lines_subset(controlling_date, 'account.invoice',
'invoice')
@api.multi
@api.returns('account.move.line')
def _get_move_lines_to_process(self, controlling_date):
""" Build a list of move lines ids to include in a run
for a policy at a given date.
:param str controlling_date: date of credit control
:return: recordset to include in the run
"""
self.ensure_one()
# there is a priority between the lines, depicted by the calls below
lines = self._due_move_lines(controlling_date)
to_add, to_remove = self._get_partner_related_lines(controlling_date)
lines = (lines | to_add) - to_remove
to_add, to_remove = self._get_invoice_related_lines(controlling_date)
lines = (lines | to_add) - to_remove
return lines
@api.multi
@api.returns('account.move.line')
def _lines_different_policy(self, lines):
""" Return a set of move lines ids for which there is an
existing credit line but with a different policy.
"""
self.ensure_one()
move_line_obj = self.env['account.move.line']
different_lines = move_line_obj.browse()
if not lines:
return different_lines
cr = self.env.cr
cr.execute("SELECT move_line_id FROM credit_control_line"
" WHERE policy_id != %s and move_line_id in %s"
" AND manually_overridden IS false",
(self.id, tuple(lines.ids)))
res = cr.fetchall()
if res:
return move_line_obj.browse([row[0] for row in res])
return different_lines
@api.multi
def check_policy_against_account(self, account):
""" Ensure that the policy corresponds to account relation """
policies = self.search([])
allowed = [x for x in policies
if account in x.account_ids or x.do_nothing]
if self not in allowed:
allowed_names = u"\n".join(x.name for x in allowed)
raise api.Warning(
_('You can only use a policy set on '
'account %s.\n'
'Please choose one of the following '
'policies:\n %s') % (account.name, allowed_names)
)
return True
class CreditControlPolicyLevel(models.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"""
name = fields.Char(string='Name', required=True, translate=True)
policy_id = fields.Many2one('credit.control.policy',
string='Related Policy',
required=True)
level = fields.Integer(string='Level', required=True)
computation_mode = fields.Selection(
[('net_days', 'Due Date'),
('end_of_month', 'Due Date, End Of Month'),
('previous_date', 'Previous Reminder')],
string='Compute Mode',
required=True
)
delay_days = fields.Integer(string='Delay (in days)', required=True)
email_template_id = fields.Many2one('email.template',
string='Email Template',
required=True)
channel = fields.Selection([('letter', 'Letter'),
('email', 'Email')],
string='Channel',
required=True)
custom_text = fields.Text(string='Custom Message',
required=True,
translate=True)
custom_mail_text = fields.Text(string='Custom Mail Message',
required=True, translate=True)
_sql_constraint = [('unique level',
'UNIQUE (policy_id, level)',
'Level must be unique per policy')]
@api.one
@api.constrains('level', 'computation_mode')
def _check_level_mode(self):
""" The smallest level of a policy cannot be computed on the
"previous_date".
"""
smallest_level = self.search([('policy_id', '=', self.policy_id.id)],
order='level asc', limit=1)
if smallest_level.computation_mode == 'previous_date':
return api.ValidationError(_('The smallest level can not be of '
'type Previous Reminder'))
@api.multi
def _previous_level(self):
""" 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
:return: previous level or None if there is no previous level
"""
self.ensure_one()
previous_levels = self.search([('policy_id', '=', self.policy_id.id),
('level', '<', self.level)],
order='level desc',
limit=1)
if not previous_levels:
return None
return previous_levels
# ----- sql time related methods ---------
@staticmethod
def _net_days_get_boundary():
return (" (mv_line.date_maturity + %(delay)s)::date <= "
"date(%(controlling_date)s)")
@staticmethod
def _end_of_month_get_boundary():
return ("(date_trunc('MONTH', (mv_line.date_maturity + %(delay)s))+"
"INTERVAL '1 MONTH - 1 day')::date"
"<= date(%(controlling_date)s)")
@staticmethod
def _previous_date_get_boundary():
return "(cr_line.date + %(delay)s)::date <= date(%(controlling_date)s)"
@api.multi
def _get_sql_date_boundary_for_computation_mode(self, controlling_date):
""" Return a where clauses statement for the given controlling
date and computation mode of the level
"""
self.ensure_one()
fname = "_%s_get_boundary" % (self.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, )
)
# -----------------------------------------
@api.multi
@api.returns('account.move.line')
def _get_first_level_move_lines(self, controlling_date, lines):
""" Retrieve all the move lines that are linked to a first level.
We use Raw SQL for performance. Security rule where applied in
policy object when the first set of lines were retrieved
"""
self.ensure_one()
move_line_obj = self.env['account.move.line']
if not lines:
return move_line_obj.browse()
cr = self.env.cr
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 id\n"
" 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 overridden
# have to be generated again for the previous level
" AND NOT manually_overridden\n"
" AND state NOT IN ('draft', 'ignored'))"
" AND (mv_line.debit IS NOT NULL AND mv_line.debit != 0.0)\n")
sql += " AND"
_get_sql_date_part = self._get_sql_date_boundary_for_computation_mode
sql += _get_sql_date_part(controlling_date)
data_dict = {'controlling_date': controlling_date,
'line_ids': tuple(lines.ids),
'delay': self.delay_days}
cr.execute(sql, data_dict)
res = cr.fetchall()
if res:
return move_line_obj.browse([row[0] for row in res])
return move_line_obj.browse()
@api.multi
@api.returns('account.move.line')
def _get_other_level_move_lines(self, controlling_date, lines):
""" Retrieve the move lines for other levels than first level.
"""
self.ensure_one()
move_line_obj = self.env['account.move.line']
if not lines:
return move_line_obj.browse()
cr = self.env.cr
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"
" AND state != 'ignored'"
" AND NOT manually_overridden"
" 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 overridden
# have to be generated again for the previous level
" AND NOT manually_overridden\n"
" AND cr_line.state NOT IN ('draft', 'ignored')\n"
" AND mv_line.id in %(line_ids)s\n")
sql += " AND "
_get_sql_date_part = self._get_sql_date_boundary_for_computation_mode
sql += _get_sql_date_part(controlling_date)
previous_level = self._previous_level()
data_dict = {'controlling_date': controlling_date,
'line_ids': tuple(lines.ids),
'delay': self.delay_days,
'previous_level': previous_level.level}
# print cr.mogrify(sql, data_dict)
cr.execute(sql, data_dict)
res = cr.fetchall()
if res:
return move_line_obj.browse([row[0] for row in res])
return move_line_obj.browse()
@api.multi
@api.returns('account.move.line')
def get_level_lines(self, controlling_date, lines):
""" get all move lines in entry lines that match the current level """
self.ensure_one()
move_line_obj = self.env['account.move.line']
matching_lines = move_line_obj.browse()
if self._previous_level() is None:
method = self._get_first_level_move_lines
else:
method = self._get_other_level_move_lines
matching_lines |= method(controlling_date, lines)
return matching_lines

View File

@@ -7,10 +7,12 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Credit control policy">
<field name="name"/>
<field name="do_nothing"/>
<field name="company_id"/>
<field name="active"/>
<group>
<field name="name"/>
<field name="do_nothing"/>
<field name="company_id"/>
<field name="active"/>
</group>
<notebook colspan="4">
<page string="Policy levels">
<field name="level_ids" nolabel="1" colspan="4" >

View File

@@ -0,0 +1,15 @@
<openerp>
<data>
<report
id="credit_control_summary"
model="credit.control.communication"
string="Credit Control Summary"
report_type="qweb-pdf"
name="account_credit_control.report_credit_control_summary"
file="account_credit_control.report_credit_control_summary"
auto="False"
/>
</data>
</openerp>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_credit_control_summary_document">
<t t-call="report.external_layout">
<div class="page">
<div class="row" id="address">
<div class="col-xs-5 col-xs-offset-7">
<address t-field="o.contact_address"
t-field-options='{"widget": "contact",
"fields": ["address", "name"],
"no_marker": true}' />
</div>
</div>
<h2 id="policy_level">
<span>Reminder</span> <span t-field="o.current_policy_level.name"/>
</h2>
<p>Dear,</p>
<p t-field="o.current_policy_level.custom_text"/>
<h3>Summary</h3>
<table class="table table-condensed" id="summary_table">
<thead>
<tr>
<th>Invoice number</th>
<th>Invoice date</th>
<th>Date due</th>
<th class="text-right">Invoiced amount</th>
<th class="text-right">Open amount</th>
</tr>
</thead>
<tbody>
<tr t-foreach="o.credit_control_line_ids" t-as="l">
<t t-if="l.invoice_id">
<td><span t-field="l.invoice_id.number"/>
<t t-if="l.invoice_id.name">
<span t-field="l.invoice_id.name"/>
</t>
</td>
</t>
<t t-if="not l.invoice_id">
<td><span t-field="l.move_line_id.name"/></td>
</t>
<td>
<span t-field="l.date_entry"/>
</td>
<td>
<span t-field="l.date_due"/>
</td>
<td class="text-right">
<span t-field="l.amount_due"/>
</td>
<td class="text-right">
<span t-field="l.balance_due"
t-field-options='{"widget": "monetary",
"display_currency": "l.currency_id or l.company_id.currency_id"}'/>
</td>
</tr>
</tbody>
</table>
</div>
</t>
</template>
<template id="report_credit_control_summary">
<t t-call="report.html_container">
<t t-foreach="doc_ids" t-as="doc_id">
<t t-raw="translate_doc(doc_id, doc_model, 'partner_id.lang', 'account_credit_control.report_credit_control_summary_document')"/>
</t>
</t>
</template>
</data>
</openerp>

View File

@@ -0,0 +1,158 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012-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 import models, fields, api, _
logger = logging.getLogger('credit.control.run')
class CreditControlRun(models.Model):
""" Credit Control run generate all credit control lines and reject """
_name = "credit.control.run"
_rec_name = 'date'
_description = "Credit control line generator"
date = fields.Date(string='Controlling Date', required=True,
readonly=True,
states={'draft': [('readonly', False)]})
@api.model
def _get_policies(self):
return self.env['credit.control.policy'].search([])
policy_ids = fields.Many2many(
'credit.control.policy',
rel="credit_run_policy_rel",
id1='run_id', id2='policy_id',
string='Policies',
readonly=True,
states={'draft': [('readonly', False)]},
default=_get_policies,
)
report = fields.Text(string='Report', readonly=True, copy=False)
state = fields.Selection([('draft', 'Draft'),
('done', 'Done')],
string='State',
required=True,
readonly=True,
default='draft')
manual_ids = fields.Many2many(
'account.move.line',
rel="credit_runreject_rel",
string='Lines to handle manually',
help='If a credit control line has been generated'
'on a policy and the policy has been changed '
'in the meantime, it has to be handled '
'manually',
readonly=True,
copy=False,
)
@api.multi
def _check_run_date(self, controlling_date):
""" Ensure that there is no credit line in the future
using controlling_date
"""
runs = self.search([('date', '>', controlling_date)],
order='date DESC', limit=1)
if runs:
raise api.Warning(_('A run has already been executed more '
'recently than %s') % (runs.date))
line_obj = self.env['credit.control.line']
lines = line_obj.search([('date', '>', controlling_date)],
order='date DESC', limit=1)
if lines:
raise api.Warning(_('A credit control line more '
'recent than %s exists at %s') %
(controlling_date, lines.date))
@api.multi
@api.returns('credit.control.line')
def _generate_credit_lines(self):
""" Generate credit control lines. """
self.ensure_one()
cr_line_obj = self.env['credit.control.line']
move_line_obj = self.env['account.move.line']
manually_managed_lines = move_line_obj.browse()
self._check_run_date(self.date)
policies = self.policy_ids
if not policies:
raise api.Warning(_('Please select a policy'))
report = ''
generated = cr_line_obj.browse()
for policy in policies:
if policy.do_nothing:
continue
lines = policy._get_move_lines_to_process(self.date)
manual_lines = policy._lines_different_policy(lines)
lines -= manual_lines
manually_managed_lines |= manual_lines
policy_lines_generated = cr_line_obj.browse()
if lines:
# policy levels are sorted by level
# so iteration is in the correct order
create = cr_line_obj.create_or_update_from_mv_lines
for level in reversed(policy.level_ids):
level_lines = level.get_level_lines(self.date, lines)
policy_lines_generated += create(level_lines,
level,
self.date)
generated |= policy_lines_generated
if policy_lines_generated:
report += (_("Policy \"%s\" has generated %d Credit "
"Control Lines.\n") %
(policy.name, len(policy_lines_generated)))
else:
report += _(
"Policy \"%s\" has not generated any "
"Credit Control Lines.\n" % policy.name
)
vals = {'state': 'done',
'report': report,
'manual_ids': [(6, 0, manually_managed_lines)]}
self.write(vals)
return generated
@api.multi
def generate_credit_lines(self):
""" Generate credit control lines
Lock the ``credit_control_run`` Postgres table to avoid concurrent
calls of this method.
"""
try:
self.env.cr.execute('SELECT id FROM credit_control_run'
' LIMIT 1 FOR UPDATE NOWAIT')
except Exception:
# In case of exception openerp will do a rollback
# for us and free the lock
raise api.Warning(_('A credit control run is already running'
' in background, please try later.'))
self._generate_credit_lines()
return True

View File

@@ -6,7 +6,7 @@
<field name="model">credit.control.run</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Credit control run"> <!-- editable="bottom" -->
<tree string="Credit control run">
<field name="date"/>
<field name="state"/>
</tree>
@@ -19,27 +19,33 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Credit control run">
<field name="date"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<newline/>
<notebook colspan="4">
<page string="Policies">
<field name="policy_ids" colspan="4" nolabel="1"/>
</page>
<page string="Report and Errors">
<field name="report" colspan="4" nolabel="1"/>
<separator string="Move lines To be treated manually"/>
<field name="manual_ids" colspan="4" nolabel="1"/>
</page>
</notebook>
<group col="3" colspan="4">
<button name="generate_credit_lines"
string="Compute Credit Control Lines"
colspan="1"
type="object" icon="gtk-execute"
attrs="{'invisible': [('state', '!=', 'draft')]}"/>
</group>
<field name="state"/>
<header>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"
statusbar_colors='{}'/>
</header>
<sheet>
<group>
<field name="date"/>
</group>
<notebook>
<page string="Policies">
<field name="policy_ids" colspan="4" nolabel="1"/>
</page>
<page string="Report and Errors">
<field name="report" colspan="4" nolabel="1"/>
<separator string="Move lines To be treated manually"/>
<field name="manual_ids" colspan="4" nolabel="1"/>
</page>
</notebook>
<group col="3" colspan="4">
<button name="generate_credit_lines"
string="Compute Credit Control Lines"
colspan="1"
type="object" icon="gtk-execute"
attrs="{'invisible': [('state', '!=', 'draft')]}"/>
</group>
</sheet>
</form>
</field>
</record>

View File

@@ -3,6 +3,12 @@
# OERPScenario, OpenERP Functional Tests
# Copyright 2009 Camptocamp SA
#
#
# The base scenario for the finance data must be executed before this
# one. The finance scenario is included in the oerpscenario base and
# the tag to run it is: @base_finance
#
#
##############################################################################
##############################################################################
# Branch # Module # Processes # System
@@ -30,11 +36,11 @@ Feature: General parameters in order to test the credit control module
"""
@email_params_mailtrap
Scenario: E-MAIL PARAMS WITH EMAIL EATER (http://mailtrap.railsware.com/)
Scenario: E-MAIL PARAMS WITH EMAIL EATER (http://mailtrap.io)
Given I need a "ir.mail_server" with name: mailstrap_testings
And having:
| name | value |
| smtp_host | mailtrap.railsware.com |
| smtp_host | mailtrap.io |
| sequence | 1 |
| smtp_port | 2525 |
| smtp_user | camptocamp1 |

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################
@@ -20,25 +20,7 @@ Feature: Ensure that email credit line generation first pass is correct
@pay_invoice_si_19_part1
Scenario: I pay a part of the first part of the invoice SI 19,
Given I need a "account.bank.statement" with oid: scen.state_control_eur_1
And having:
| name | value |
| name | Bk.St.si_19_part1 |
| date | 2013-03-31 |
| journal_id | by oid: scen.eur_journal |
| period_id | by name: 03/2013 |
And I import invoice "SI_19" using import invoice button
And I should have a "account.bank.statement.line" with name: "SI_19" and amount: "450"
And I set the voucher paid amount to "300"
And I save the voucher
And I should have a "account.bank.statement.line" with name: "SI_19" and amount: "1050"
And I set the voucher paid amount to "0"
And I save the voucher
Then I modify the line amount to "0"
Given I need a "account.bank.statement" with oid: scen.state_control_eur_1
And I set bank statement end-balance
When I confirm bank statement
Given I pay 300.0 on the invoice "SI_19"
Then My invoice "SI_19" is in state "open" reconciled with a residual amount of "1200.0"
@account_credit_control_run_month_mar

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################
@@ -20,61 +20,17 @@ Feature: Ensure that email credit line generation first pass is correct
@pay_invoice_si_16
Scenario: I pay entirely the invoice SI 16, so it should no longer appear in the credit control lines
Given I need a "account.bank.statement" with oid: scen.voucher_statement_si_16
And having:
| name | value |
| name | Bk.St.si_16 |
| date | 2013-05-30 |
| journal_id | by oid: scen.eur_journal |
| period_id | by name: 05/2013 |
And I import invoice "SI_16" using import invoice button
And I set bank statement end-balance
When I confirm bank statement
Given I pay the full amount on the invoice "SI_16"
Then My invoice "SI_16" is in state "paid" reconciled with a residual amount of "0.0"
@pay_invoice_si_17
Scenario: I pay entirely the invoice SI 17, so it should no longer appear in the credit control lines
Given I need a "account.bank.statement" with oid: scen.voucher_statement_si_17
And having:
| name | value |
| name | Bk.St.si_17 |
| date | 2013-05-30 |
| journal_id | by oid: scen.eur_journal |
| period_id | by name: 05/2013 |
And I import invoice "SI_17" using import invoice button
And I should have a "account.bank.statement.line" with name: "SI_17" and amount: "1500"
And I set the voucher paid amount to "1000"
And I save the voucher
And I modify the line amount to "1000"
And I need a "account.bank.statement" with oid: scen.voucher_statement_si_17
And I set bank statement end-balance
When I confirm bank statement
Given I pay 1000.0 on the invoice "SI_17"
Then My invoice "SI_17" is in state "open" reconciled with a residual amount of "500.0"
@pay_invoice_si_18_part1
Scenario: I pay the first part of the invoice SI 18, so it should no longer appear in the credit control lines however, the second move lines should still appears
Given I need a "account.bank.statement" with oid: scen.voucher_statement_si_18
And having:
| name | value |
| name | Bk.St.si_18_part1 |
| date | 2013-05-30 |
| journal_id | by oid: scen.eur_journal |
| period_id | by name: 05/2013 |
And I import invoice "SI_18" using import invoice button
And I should have a "account.bank.statement.line" with name: "SI_18" and amount: "450"
And I set the voucher paid amount to "450"
And I save the voucher
And I modify the line amount to "450"
And I should have a "account.bank.statement.line" with name: "SI_18" and amount: "1050"
And I set the voucher paid amount to "0"
And I save the voucher
Then I modify the line amount to "0"
And I need a "account.bank.statement" with oid: scen.voucher_statement_si_18
And I set bank statement end-balance
When I confirm bank statement
Given I pay 450.0 on the invoice "SI_18"
Then My invoice "SI_18" is in state "open" reconciled with a residual amount of "1050.0"
@account_credit_control_run_month

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################

View File

@@ -1,7 +1,7 @@
###############################################################################
#
# OERPScenario, OpenERP Functional Tests
# Copyright 2012 Camptocamp SA
# Copyright 2012-2014 Camptocamp SA
# Author Nicolas Bessi
##############################################################################

View File

@@ -2,7 +2,7 @@
# flake8: noqa
import time
from behave import given, when
from support import model
from support import model, assert_equal
@given(u'I configure the following accounts on the credit control policy with oid: "{policy_oid}"')
def impl(ctx, policy_oid):

View File

@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from support import model, assert_equal, assert_in, assert_true
# flake8: noqa
@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):

View File

@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# flake8: noqa
from support import model, assert_equal, assert_almost_equal
import datetime
@step('I pay the full amount on the invoice "{inv_name}"')
def impl(ctx, inv_name):
Invoice = model('account.invoice')
invoice = Invoice.get([('name', '=', inv_name)])
assert invoice
ctx.execute_steps("""
When I pay %f on the invoice "%s"
""" % (invoice.residual, inv_name))
@step('I pay {amount:f} on the invoice "{inv_name}"')
def impl(ctx, amount, inv_name):
Partner = model('res.partner')
Invoice = model('account.invoice')
Voucher = model('account.voucher')
VoucherLine = model('account.voucher.line')
Journal = model('account.journal')
invoice = Invoice.get([('name', '=', inv_name)])
assert invoice
journal = Journal.get('scen.eur_journal')
values = {
'partner_id': invoice.partner_id.commercial_partner_id.id,
'reference': invoice.name,
'amount': amount,
'date': invoice.date_invoice,
'currency_id': invoice.currency_id.id,
'company_id': invoice.company_id.id,
'journal_id': journal.id,
}
if invoice.type in ('out_invoice','out_refund'):
values['type'] = 'receipt'
else:
values['type'] = 'payment'
onchange = Voucher.onchange_partner_id([], values['partner_id'],
values['journal_id'],
values['amount'],
values['currency_id'],
values['type'],
values['date'])
values.update(onchange['value'])
onchange = Voucher.onchange_date([], values['date'],
values['currency_id'],
False,
values['amount'],
values['company_id'])
values.update(onchange['value'])
onchange = Voucher.onchange_amount([], values['amount'],
False,
values['partner_id'],
values['journal_id'],
values['currency_id'],
values['type'],
values['date'],
False,
values['company_id'])
values.update(onchange['value'])
values['line_cr_ids'] = False
voucher = Voucher.create(values)
vals = voucher.recompute_voucher_lines(voucher.partner_id.id,
voucher.journal_id.id,
voucher.amount,
voucher.currency_id.id,
voucher.type,
voucher.date)
for line in vals['value']['line_cr_ids']:
line['voucher_id'] = voucher.id
VoucherLine.create(line)
for line in vals['value']['line_dr_ids']:
line['voucher_id'] = voucher.id
VoucherLine.create(line)
voucher.button_proforma_voucher()
# Workaround to force recomputation of the residual.
# Must be removed once this bug is fixed:
# https://github.com/odoo/odoo/issues/3395
invoice.write({'currency_id': invoice.currency_id.id})
@step('My invoice "{inv_name}" is in state "{state}" reconciled with a residual amount of "{amount:f}"')
def impl(ctx, inv_name, state, amount):
invoice = model('account.invoice').get([('name', '=', inv_name)])
assert_almost_equal(invoice.residual, amount)
assert_equal(invoice.state, state)

View File

@@ -2,7 +2,7 @@
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012 Camptocamp SA
# Copyright 2012-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

View File

@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012-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 import models, fields, api
logger = logging.getLogger('credit.control.line.mailing')
class CreditCommunication(models.TransientModel):
"""Shell class 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'
partner_id = fields.Many2one('res.partner', 'Partner', required=True)
current_policy_level = fields.Many2one('credit.control.policy.level',
'Level',
required=True)
credit_control_line_ids = fields.Many2many('credit.control.line',
rel='comm_credit_rel',
string='Credit Lines')
contact_address = fields.Many2one('res.partner',
string='Contact Address',
readonly=True)
@api.model
def _get_company(self):
company_obj = self.env['res.company']
return company_obj._company_default_get('credit.control.policy')
company_id = fields.Many2one('res.company',
string='Company',
default=_get_company,
required=True)
user_id = fields.Many2one('res.users',
default=lambda self: self.env.user,
string='User')
@api.model
@api.returns('self', lambda value: value.id)
def create(self, vals):
if vals.get('partner_id'):
# the computed field does not work in TransientModel,
# just set a value on creation
partner_id = vals['partner_id']
vals['contact_address'] = self._get_contact_address(partner_id).id
return super(CreditCommunication, self).create(vals)
@api.multi
def get_email(self):
""" Return a valid email for customer """
self.ensure_one()
contact = self.contact_address
return contact.email
@api.multi
@api.returns('res.partner')
def get_contact_address(self):
""" Compatibility method, please use the contact_address field """
self.ensure_one()
return self.contact_address
@api.model
@api.returns('res.partner')
def _get_contact_address(self, partner_id):
partner_obj = self.env['res.partner']
partner = partner_obj.browse(partner_id)
add_ids = partner.address_get(adr_pref=['invoice']) or {}
add_id = add_ids['invoice']
return partner_obj.browse(add_id)
@api.model
@api.returns('credit.control.line')
def _get_credit_lines(self, line_ids, partner_id, level_id):
""" Return credit lines related to a partner and a policy level """
cr_line_obj = self.env['credit.control.line']
cr_lines = cr_line_obj.search([('id', 'in', line_ids),
('partner_id', '=', partner_id),
('policy_level_id', '=', level_id)])
return cr_lines
@api.model
def _generate_comm_from_credit_lines(self, lines):
""" Aggregate credit control line by partner, level, and currency
It also generate a communication object per aggregation.
"""
comms = self.browse()
if not lines:
return comms
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, "
" credit_control_line.currency_id"
)
cr = self.env.cr
cr.execute(sql, (tuple(lines.ids), ))
res = cr.dictfetchall()
for level_assoc in res:
data = {}
level_lines = self._get_credit_lines(lines.ids,
level_assoc['partner_id'],
level_assoc['policy_level_id']
)
data['credit_control_line_ids'] = [(6, 0, level_lines.ids)]
data['partner_id'] = level_assoc['partner_id']
data['current_policy_level'] = level_assoc['policy_level_id']
comm = self.create(data)
comms += comm
return comms
@api.multi
@api.returns('mail.mail')
def _generate_emails(self):
""" Generate email message using template related to level """
email_message_obj = self.env['mail.mail']
# Warning: still using the old-api on 'email.template' because
# the method generate_email() does not follow the cr, uid, ids
# convention and the new api wrapper can't translate the call
email_template_obj = self.pool['email.template']
att_obj = self.env['ir.attachment']
emails = email_message_obj.browse()
required_fields = ['subject',
'body_html',
'email_from',
'email_to']
cr, uid, context = self.env.cr, self.env.uid, self.env.context
for comm in self:
template = comm.current_policy_level.email_template_id
email_values = email_template_obj.generate_email(cr, uid,
template.id,
comm.id,
context=context)
email_values['type'] = 'email'
email = email_message_obj.create(email_values)
state = 'sent'
# The mail will not be send, however it will be in the pool, in an
# error state. So we create it, link it with
# the credit control line
# and put this latter in a `email_error` state we not that we have
# a problem with the email
if not all(email_values.get(field) for field in required_fields):
state = 'email_error'
comm.credit_control_line_ids.write({'mail_message_id': email.id,
'state': state})
attachments = att_obj.browse()
for att in email_values.get('attachments', []):
attach_fname = att[0]
attach_datas = att[1]
data_attach = {
'name': attach_fname,
'datas': attach_datas,
'datas_fname': attach_fname,
'res_model': 'mail.mail',
'res_id': email.id,
'type': 'binary',
}
attachments += att_obj.create(data_attach)
email.write({'attachment_ids': [(6, 0, attachments.ids)]})
emails += email
return emails
@api.multi
def _generate_report(self):
""" Will generate a report by inserting mako template
of related policy template
"""
report_name = 'account_credit_control.report_credit_control_summary'
return self.env['report'].get_pdf(self, report_name)
@api.multi
@api.returns('credit.control.line')
def _mark_credit_line_as_sent(self):
line_obj = self.env['credit.control.line']
lines = line_obj.browse()
for comm in self:
lines |= comm.credit_control_line_ids
lines.write({'state': 'sent'})
return lines

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012-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 import models, fields, api, _
class CreditControlEmailer(models.TransientModel):
""" Send emails for each selected credit control lines. """
_name = "credit.control.emailer"
_description = """Mass credit line emailer"""
_rec_name = 'id'
@api.model
def _get_line_ids(self):
context = self.env.context
if not (context.get('active_model') == 'credit.control.line' and
context.get('active_ids')):
return False
line_obj = self.env['credit.control.line']
lines = line_obj.browse(context['active_id'])
return self._filter_lines(lines)
line_ids = fields.Many2many('credit.control.line',
string='Credit Control Lines',
default=_get_line_ids,
domain=[('state', '=', 'to_be_sent'),
('channel', '=', 'email')])
@api.model
@api.returns('credit.control.line')
def _filter_lines(self, lines):
""" filter lines to use in the wizard """
line_obj = self.env['credit.control.line']
domain = [('state', '=', 'to_be_sent'),
('id', 'in', lines.ids),
('channel', '=', 'email')]
return line_obj.search(domain)
@api.multi
def email_lines(self):
self.ensure_one()
if not self.line_ids:
raise api.Warning(_('No credit control lines selected.'))
comm_obj = self.env['credit.control.communication']
filtered_lines = self._filter_lines(self.line_ids)
comms = comm_obj._generate_comm_from_credit_lines(filtered_lines)
comms._generate_emails()
return {'type': 'ir.actions.act_window_close'}

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012-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 import models, fields, api, _
class CreditControlMarker(models.TransientModel):
""" Change the state of lines in mass """
_name = 'credit.control.marker'
_description = 'Mass marker'
@api.model
def _get_line_ids(self):
context = self.env.context
if not (context.get('active_model') == 'credit.control.line' and
context.get('active_ids')):
return False
line_obj = self.env['credit.control.line']
lines = line_obj.browse(context['active_id'])
return self._filter_lines(lines)
name = fields.Selection([('ignored', 'Ignored'),
('to_be_sent', 'Ready To Send'),
('sent', 'Done')],
string='Mark as',
default='to_be_sent',
required=True)
line_ids = fields.Many2many('credit.control.line',
string='Credit Control Lines',
default=_get_line_ids,
domain="[('state', '!=', 'sent')]")
@api.model
@api.returns('credit.control.line')
def _filter_lines(self, lines):
""" get line to be marked filter done lines """
line_obj = self.env['credit.control.line']
domain = [('state', '!=', 'sent'), ('id', 'in', lines.ids)]
return line_obj.search(domain)
@api.model
@api.returns('credit.control.line')
def _mark_lines(self, filtered_lines, state):
""" write hook """
assert state
filtered_lines.write({'state': state})
return filtered_lines
@api.multi
def mark_lines(self):
""" Write state of selected credit lines to the one in entry
done credit line will be ignored """
self.ensure_one()
if not self.line_ids:
raise api.Warning(_('No credit control lines selected.'))
filtered_lines = self._filter_lines(self.line_ids)
if not filtered_lines:
raise api.Warning(_('No lines will be changed. '
'All the selected lines are already done.'))
self._mark_lines(filtered_lines, self.name)
return {'domain': unicode([('id', 'in', filtered_lines.ids)]),
'view_type': 'form',
'view_mode': 'tree,form',
'view_id': False,
'res_model': 'credit.control.line',
'type': 'ir.actions.act_window'}

View File

@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# 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 import models, fields, api, _
logger = logging.getLogger(__name__)
class credit_control_policy_changer(models.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"
new_policy_id = fields.Many2one('credit.control.policy',
string='New Policy to Apply',
required=True)
new_policy_level_id = fields.Many2one('credit.control.policy.level',
string='New level to apply',
required=True)
# Only used to provide dynamic filtering on form
do_nothing = fields.Boolean(string='No follow policy')
@api.model
def _get_default_lines(self):
""" 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 lines
"""
context = self.env.context
active_ids = context.get('active_ids')
invoice_obj = self.env['account.invoice']
move_line_obj = self.env['account.move.line']
if not active_ids:
return False
selected_lines = move_line_obj.browse()
for invoice in invoice_obj.browse(active_ids):
if invoice.type in ('in_invoice', 'in_refund', 'out_refund'):
raise api.Warning(_('Please use wizard on customer invoices'))
domain = [('account_id', '=', invoice.account_id.id),
('move_id', '=', invoice.move_id.id),
('reconcile_id', '=', False)]
move_lines = move_line_obj.search(domain)
selected_lines |= move_lines
return selected_lines
move_line_ids = fields.Many2many('account.move.line',
rel='credit_changer_ml_rel',
string='Move line to change',
default=_get_default_lines)
@api.onchange('new_policy_level_id')
def onchange_policy_id(self):
if not self.new_policy_id:
return
self.do_nothing = self.new_policy_id.do_nothing
@api.model
@api.returns('credit.control.line')
def _mark_as_overridden(self, move_lines):
""" Mark `move_lines` related credit control line as overridden
This is done by setting manually_overridden fields to True
:param move_lines: move line to mark as overridden
:return: list of credit lines that where marked as overridden
"""
credit_obj = self.env['credit.control.line']
domain = [('move_line_id', 'in', move_lines.ids)]
credit_lines = credit_obj.search(domain)
credit_lines.write({'manually_overridden': True})
return credit_lines
@api.model
def _set_invoice_policy(self, move_lines, policy):
""" Force policy on invoice """
invoice_obj = self.env['account.invoice']
invoice_ids = set(line.invoice.id for line in move_lines
if line.invoice)
invoices = invoice_obj.browse(invoice_ids)
invoices.write({'credit_policy_id': policy.id})
@api.model
def _check_accounts_policies(self, lines, policy):
accounts = set(line.account_id for line in lines)
for account in accounts:
policy.check_policy_against_account(account)
return True
@api.multi
def set_new_policy(self):
""" 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
"""
self.ensure_one()
credit_line_obj = self.env['credit.control.line']
controlling_date = fields.date.today()
self._check_accounts_policies(self.move_line_ids, self.new_policy_id)
self._mark_as_overridden(self.move_line_ids)
# As disscused with business expert
# draft lines should be passed to ignored
# if same level as the new one
# As it is a manual action
# We also ignore rounding tolerance
create = credit_line_obj.create_or_update_from_mv_lines
generated_lines = create(self.move_line_ids,
self.new_policy_level_id,
controlling_date,
check_tolerance=False)
self._set_invoice_policy(self.move_line_ids, self.new_policy_id)
if not generated_lines:
return {'type': 'ir.actions.act_window_close'}
action_ref = 'account_credit_control.credit_control_line_action'
action = self.env.ref(action_ref)
action = action.read()[0]
action['domain'] = [('id', 'in', generated_lines.ids)]
return action

View File

@@ -11,8 +11,7 @@
<newline/>
<group>
<group>
<field name="new_policy_id"
on_change="onchange_policy_id(new_policy_id)"/>
<field name="new_policy_id"/>
<field name="do_nothing"
invisible="1"/>
<field name="new_policy_level_id"

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012-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 import models, fields, api, _
class CreditControlPrinter(models.TransientModel):
""" Print lines """
_name = "credit.control.printer"
_rec_name = 'id'
_description = 'Mass printer'
@api.model
def _get_line_ids(self):
context = self.env.context
if context.get('active_model') != 'credit.control.line':
return False
return context.get('active_ids', False)
mark_as_sent = fields.Boolean(string='Mark letter lines as sent',
default=True,
help="Only letter lines will be marked.")
line_ids = fields.Many2many('credit.control.line',
string='Credit Control Lines',
default=_get_line_ids)
@api.model
def _credit_line_predicate(self, line):
return True
@api.model
@api.returns('credit.control.line')
def _get_lines(self, lines, predicate):
return lines.filtered(predicate)
@api.multi
def print_lines(self):
self.ensure_one()
comm_obj = self.env['credit.control.communication']
if not self.line_ids and not self.print_all:
raise api.Warning(_('No credit control lines selected.'))
lines = self._get_lines(self.line_ids, self._credit_line_predicate)
comms = comm_obj._generate_comm_from_credit_lines(lines)
if self.mark_as_sent:
comms._mark_credit_line_as_sent()
report_name = 'account_credit_control.report_credit_control_summary'
report_obj = self.env['report'].with_context(active_ids=comms.ids)
return report_obj.get_action(comms, report_name)

View File

@@ -11,28 +11,17 @@
<newline/>
<group>
<field name="mark_as_sent"
colspan="4"
attrs="{'invisible': [('state', '=', 'done')]}"/>
colspan="4"/>
</group>
<newline/>
<notebook>
<page string="Lines" attrs="{'invisible': [('state', '=', 'done')]}">
<field name="line_ids" colspan="4" nolabel="1"
attrs="{'invisible': [('state', '=', 'done')]}" />
<page string="Lines">
<field name="line_ids" colspan="4" nolabel="1"/>
</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/>
<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 special="cancel" string="Close" icon='gtk-close' attrs="{'invisible': [('state', '!=', 'done')]}"/>
<button class="oe_highlight" name="print_lines" string="Print" type="object"/>
<button special="cancel" string="Cancel" icon='gtk-cancel'/>
</footer>
</form>
</field>