Migrate the main models to the new API

This commit is contained in:
Guewen Baconnier
2014-10-28 16:19:45 +01:00
parent 2538b0c55c
commit caa628c825
7 changed files with 462 additions and 650 deletions

View File

@@ -18,27 +18,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp import models, fields
class AccountAccount(orm.Model):
"""Add a link to a credit control policy on account.account"""
class AccountAccount(models.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)
credit_control_line_ids = fields.One2many('credit.control.line',
'account_id',
string='Credit Lines',
readonly=True)

View File

@@ -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

@@ -20,14 +20,14 @@
##############################################################################
import logging
from openerp.osv import orm, fields
from openerp import models, fields, api
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.
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,
@@ -38,160 +38,105 @@ class CreditControlLine(orm.Model):
_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 = 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_sent': fields.date(
'Sent date',
readonly=True,
states={'draft': [('readonly', False)]}
),
date_entry = fields.Date(string='Entry date',
related='move_line_id.date',
store=True,
readonly=True)
'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.")
),
date_sent = fields.Date(string='Sent date',
readonly=True,
states={'draft': [('readonly', False)]})
'channel': fields.selection(
[('letter', 'Letter'),
('email', 'Email')],
'Channel', required=True,
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.")
'invoice_id': fields.many2one(
'account.invoice',
'Invoice',
readonly=True
),
channel = fields.Selection([('letter', 'Letter'),
('email', 'Email')],
string='Channel',
required=True,
readonly=True,
states={'draft': [('readonly', False)]})
'partner_id': fields.many2one(
'res.partner',
"Partner",
required=True
),
invoice_id = fields.Many2one('account.invoice',
string='Invoice',
readonly=True)
'amount_due': fields.float(
'Due Amount Tax incl.',
required=True,
readonly=True
),
partner_id = fields.Many2one('res.partner',
string='Partner',
required=True)
'balance_due': fields.float(
'Due balance', required=True,
readonly=True
),
amount_due = fields.Float(string='Due Amount Tax incl.',
required=True, readonly=True)
'mail_message_id': fields.many2one(
'mail.mail',
'Sent Email',
readonly=True
),
balance_due = fields.Float(string='Due balance', required=True,
readonly=True)
'move_line_id': fields.many2one(
'account.move.line',
'Move line',
required=True,
readonly=True
),
mail_message_id = fields.Many2one('mail.mail', string='Sent Email',
readonly=True)
'account_id': fields.related(
'move_line_id',
'account_id',
type='many2one',
relation='account.account',
string='Account',
store=True,
readonly=True
),
move_line_id = fields.Many2one('account.move.line',
string='Move line',
required=True,
readonly=True)
'currency_id': fields.related(
'move_line_id',
'currency_id',
type='many2one',
relation='res.currency',
string='Currency',
store=True,
readonly=True
),
account_id = fields.Many2one(related='move_line_id.account_id',
store=True,
readonly=True)
'company_id': fields.related(
'move_line_id', 'company_id',
type='many2one',
relation='res.company',
string='Company',
store=True, readonly=True
),
currency_id = fields.Many2one(related='move_line_id.currency_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',
'Overdue Level',
required=True,
readonly=True,
states={'draft': [('readonly', False)]}
),
company_id = fields.Many2one(related='move_line_id.company_id',
store=True,
readonly=True)
'policy_id': fields.related(
'policy_level_id',
'policy_id',
type='many2one',
relation='credit.control.policy',
string='Policy',
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)]})
'level': fields.related(
'policy_level_id',
'level',
type='integer',
relation='credit.control.policy',
string='Level',
store=True,
readonly=True
),
policy_id = fields.Many2one(related='policy_level_id.policy_id',
store=True,
readonly=True)
'manually_overridden': fields.boolean('Manually overridden')
}
level = fields.Integer(related='policy_level_id.level',
store=True,
readonly=True)
_defaults = {'state': 'draft'}
manually_overridden = fields.Boolean(string='Manually overridden')
def _prepare_from_move_line(self, cr, uid, move_line,
level, controlling_date, open_amount,
context=None):
"""Create credit control line"""
@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
@@ -208,17 +153,17 @@ class CreditControlLine(orm.Model):
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
@api.model
def create_or_update_from_mv_lines(self, lines, level_id, 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 list
:param lines: move.line id recordset
:param level_id: credit.control.policy.level id
:param controlling_date: date string of the credit controlling date.
Generally it should be the same
@@ -228,65 +173,54 @@ class CreditControlLine(orm.Model):
is smaller than company defined
tolerance
:returns: list of created credit line ids
:returns: recordset of created credit lines
"""
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)
currency_obj = self.env['res.currency']
level_obj = self.env['credit.control.policy.level']
user = self.env.user
currencies = currency_obj.search([])
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)
user_currency = user.company_id.currency_id
for currency in currencies:
tolerance[currency.id] = currency.compute(tolerance_base,
user_currency)
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)
level = level_obj.browse(level_id)
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(cr, uid,
line,
vals = self._prepare_from_move_line(move_line,
level,
controlling_date,
open_amount,
context=context)
line_id = self.create(cr, uid, vals, context=context)
line_ids.append(line_id)
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_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)
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 line_ids
return new_lines
def unlink(self, cr, uid, ids, context=None, check=True):
for line in self.browse(cr, uid, ids, context=context):
@api.multi
def unlink(self):
for line in self:
if line.state != 'draft':
raise orm.except_orm(
_('Error !'),
raise api.Warning(
_('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)
return super(CreditControlLine, self).unlink()

View File

@@ -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

@@ -18,61 +18,41 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp import models, fields, api
class ResPartner(orm.Model):
"""Add a settings on the credit control policy to use on the partners,
and links to the credit control lines."""
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"
_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
)
}
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)
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
@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_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)
policy = partner.credit_policy_id
try:
policy.check_policy_against_account(account.id)
except api.Warning as err:
# constrains should raise ValidationError exceptions
raise api.ValidationError(err)

View File

@@ -18,61 +18,47 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp import models, fields, api
from openerp.tools.translate import _
class CreditControlPolicy(orm.Model):
"""Define a policy of reminder"""
class CreditControlPolicy(models.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'
),
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)
'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]
@api.multi
def _move_lines_domain(self, controlling_date):
""" Build the default domain for searching move lines """
self.ensure_one()
account_ids = [a.id for a in self.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):
@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
@@ -83,18 +69,17 @@ class CreditControlPolicy(orm.Model):
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()
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)
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):
@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.
@@ -104,274 +89,243 @@ class CreditControlPolicy(orm.Model):
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
: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.pool.get(model)
move_l_obj = self.pool.get('account.move.line')
my_obj = self.env[model]
move_l_obj = self.env['account.move.line']
default_domain = self._move_lines_domain(controlling_date)
default_domain = self._move_lines_domain(cr, uid,
policy,
controlling_date,
context=context)
to_add_ids = set()
to_remove_ids = set()
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_obj_ids = my_obj.search(
cr, uid,
[('credit_policy_id', '=', policy.id)],
context=context)
if add_obj_ids:
add_objs = my_obj.search([('credit_policy_id', '=', self.id)])
if add_objs:
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))
domain.append((move_relation_field, 'in', add_objs))
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_obj_ids = my_obj.search(
cr, uid,
[('credit_policy_id', '!=', policy.id),
('credit_policy_id', '!=', False)],
context=context)
if neg_obj_ids:
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_obj_ids))
to_remove_ids = set(move_l_obj.search(cr, uid, domain,
context=context))
return to_add_ids, to_remove_ids
domain.append((move_relation_field, 'in', neg_objs))
to_remove = move_l_obj.search(domain)
return to_add, to_remove
def _get_partner_related_lines(self, cr, uid, policy, controlling_date,
context=None):
@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 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
:return: recordset to add in the process, recordset to remove from
the process
"""
return self._move_lines_subset(cr, uid, policy, controlling_date,
'res.partner', 'partner_id',
context=context)
return self._move_lines_subset(controlling_date, 'res.partner',
'partner_id')
def _get_invoice_related_lines(self, cr, uid, policy, controlling_date,
context=None):
@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 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
:return: recordset to add in the process, recordset to remove from
the process
"""
return self._move_lines_subset(cr, uid, policy, controlling_date,
'account.invoice', 'invoice',
context=context)
return self._move_lines_subset(controlling_date, 'account.invoice',
'invoice')
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
@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 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)
:return: recordset to include in the run
"""
self.ensure_one()
# 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)
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
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.
@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.
"""
different_lines = set()
self.ensure_one()
move_line_obj = self.env['account.move.line']
different_lines = move_line_obj.browse()
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 = self._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",
(policy_id, tuple(lines)))
(self.id, tuple(line.id for line in lines)))
res = cr.fetchall()
if res:
different_lines.update([x[0] for x in res])
return move_line_obj.browse([row[0] for row 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)
@api.multi
def check_policy_against_account(self, account_id):
""" Ensure that the policy corresponds to account relation """
account = self.env['account.account'].browse(account_id)
policies = self.search([])
allowed = [x for x in policies
if account in x.account_ids or x.do_nothing]
if policy not in allowed:
if self not in allowed:
allowed_names = u"\n".join(x.name for x in allowed)
raise orm.except_orm(
raise api.Warning(
_('You can only use a policy set on '
'account %s') % account.name,
_("Please choose one of the following "
"policies:\n %s") % allowed_names
'account %s.\n'
'Please choose one of the following '
'policies:\n %s') % (account.name, allowed_names)
)
return True
class CreditControlPolicyLevel(orm.Model):
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"""
_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
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')]
_constraints = [(_check_level_mode,
'The smallest level can not be of type Previous Reminder',
['level'])]
@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'))
def _previous_level(self, cr, uid, policy_level, context=None):
@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
:param browse_record policy_level: policy level
:return: previous level id or None if there is no previous level
:return: previous level 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
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 ---------
def _net_days_get_boundary(self):
@staticmethod
def _net_days_get_boundary():
return (" (mv_line.date_maturity + %(delay)s)::date <= "
"date(%(controlling_date)s)")
def _end_of_month_get_boundary(self):
@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)")
def _previous_date_get_boundary(self):
@staticmethod
def _previous_date_get_boundary():
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,)
@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,)
'%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()
@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 level_lines
return move_line_obj.browse()
cr = self._cr
sql = ("SELECT DISTINCT mv_line.id\n"
" FROM account_move_line mv_line\n"
" WHERE mv_line.id in %(line_ids)s\n"
@@ -385,26 +339,28 @@ class CreditControlPolicyLevel(orm.Model):
" 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
)
_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),
'delay': level.delay_days}
'line_ids': tuple(line.id for line in lines),
'delay': self.delay_days}
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
return move_line_obj.browse([row[0] for row in res])
return move_line_obj.browse()
def _get_other_level_move_line_ids(self, cr, uid, level, controlling_date,
lines, context=None):
@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.
"""
level_lines = set()
self.ensure_one()
move_line_obj = self.env['account.move.line']
if not lines:
return level_lines
return move_line_obj.browse()
cr = self._cr
sql = ("SELECT mv_line.id\n"
" FROM account_move_line mv_line\n"
" JOIN credit_control_line cr_line\n"
@@ -424,41 +380,31 @@ class CreditControlPolicyLevel(orm.Model):
" 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)
_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),
'delay': level.delay_days,
'line_ids': tuple(line.id for line in lines),
'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:
level_lines.update([x[0] for x in res])
return level_lines
return move_line_obj.browse([row[0] for row in res])
return move_line_obj.browse()
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
@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_line_ids
matching_lines.update(method(cr, uid, level,
controlling_date, lines,
context=context))
method = self._get_other_level_move_lines
matching_lines += method(controlling_date, lines)
return matching_lines

View File

@@ -20,143 +20,113 @@
##############################################################################
import logging
from openerp.osv import orm, fields
from openerp import models, fields, api
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"""
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"""
_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)]}
),
_description = "Credit control line generator"
'report': fields.text('Report', readonly=True),
date = fields.Date(string='Controlling Date', required=True)
'state': fields.selection([('draft', 'Draft'),
('done', 'Done')],
string='State',
required=True,
readonly=True),
@api.model
def _get_policies(self):
return self.env['credit.control.policy'].search([])
'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
),
}
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,
)
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
@api.multi
def _check_run_date(self, controlling_date):
""" 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)
runs = self.search([('date', '>', controlling_date)],
order='date DESC', limit=1)
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))
raise api.Warning(_('A run has already been executed more '
'recently than %s') % (runs.date))
line_obj = self.pool['credit.control.line']
lines = line_obj.search(cr, uid, [('date', '>', controlling_date)],
order='date DESC', limit=1, context=context)
line_obj = self.env['credit.control.line']
lines = line_obj.search([('date', '>', controlling_date)],
order='date DESC', limit=1)
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
raise api.Warning(_('A credit control line more '
'recent than %s exists at %s') %
(controlling_date, lines.date))
def _generate_credit_lines(self, cr, uid, run_id, context=None):
@api.multi
@api.returns('credit.control.line')
def _generate_credit_lines(self):
""" 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]
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)
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
policies = self.policy_ids
if not policies:
raise orm.except_orm(_('Error'),
_('Please select a policy'))
raise api.Warning(_('Please select a policy'))
report = ''
generated_ids = []
generated = []
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 = []
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
for level in reversed(policy.level_ids):
level_lines = level.get_level_lines(run.date, lines,
context=context)
policy_generated_ids += \
level_lines = level.get_level_lines(self.date, lines)
policy_lines_generated += \
cr_line_obj.create_or_update_from_mv_lines(
cr, uid,
[],
list(level_lines),
level_lines,
level.id,
run.date,
context=context
self.date,
)
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
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 "
@@ -166,24 +136,24 @@ class CreditControlRun(orm.Model):
vals = {'state': 'done',
'report': report,
'manual_ids': [(6, 0, manually_managed_lines)]}
run.write(vals, context=context)
return generated_ids
self.write(vals)
return generated
def generate_credit_lines(self, cr, uid, run_id, context=None):
"""Generate credit control lines
@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:
cr.execute('SELECT id FROM credit_control_run'
' LIMIT 1 FOR UPDATE NOWAIT')
self._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.'))
raise api.Warning(_('A credit control run is already running'
' in background, please try later.'))
self._generate_credit_lines(cr, uid, run_id, context)
self._generate_credit_lines()
return True