[CHG] cleanings

(lp:c2c-addons/6.1  rev 89.1.5)
This commit is contained in:
Guewen Baconnier @ Camptocamp
2012-10-23 13:36:28 +02:00
parent 5ccf999f6f
commit a51a427cca
2 changed files with 181 additions and 133 deletions

View File

@@ -21,6 +21,7 @@
from openerp.osv.orm import Model, fields from openerp.osv.orm import Model, fields
from openerp.tools.translate import _ from openerp.tools.translate import _
class CreditControlPolicy(Model): class CreditControlPolicy(Model):
"""Define a policy of reminder""" """Define a policy of reminder"""
@@ -33,104 +34,135 @@ class CreditControlPolicy(Model):
'Policy Levels'), 'Policy Levels'),
'do_nothing' : fields.boolean('Do nothing', 'do_nothing' : fields.boolean('Do nothing',
help=('For policies who should not ' help=('For policies which should not '
'generate lines or are obsolete')), 'generate lines or are obsolete')),
'company_id' : fields.many2one('res.company', 'Company') 'company_id' : fields.many2one('res.company', 'Company')
} }
def _get_account_related_lines(self, cursor, uid, policy_id, controlling_date, lines, context=None): def _get_account_related_lines(self, cursor, uid, policy_id, controlling_date, context=None):
""" We get all the lines related to accounts with given credit policy. """ Get the move lines for a policy.
We try not to use direct SQL in order to respect security rules.
As we define the first set it is important, The date is used to do a prefilter. Do not use direct SQL in order to respect security rules.
!!!We take the asumption that only receivable lines have a maturity date
and account must be reconcillable""" Assume that only the receivable lines have a maturity date and that
context = context or {} accounts used in the policy are reconcilable.
"""
if context is None:
context = {}
move_l_obj = self.pool.get('account.move.line') move_l_obj = self.pool.get('account.move.line')
account_obj = self.pool.get('account.account') account_obj = self.pool.get('account.account')
acc_ids = account_obj.search(cursor, uid, [('credit_policy_id', '=', policy_id)]) acc_ids = account_obj.search(
cursor, uid, [('credit_policy_id', '=', policy_id)], context=context)
if not acc_ids: if not acc_ids:
return lines return []
move_ids = move_l_obj.search(cursor, uid, [('account_id', 'in', acc_ids), lines = move_l_obj.search(cursor, uid,
[('account_id', 'in', acc_ids),
('date_maturity', '<=', controlling_date), ('date_maturity', '<=', controlling_date),
('reconcile_id', '=', False), ('reconcile_id', '=', False),
('partner_id', '!=', False)]) ('partner_id', '!=', False)],
context=context)
lines += move_ids
return lines return lines
def _get_sum_reduce_range(self, cursor, uid, policy_id, controlling_date, lines,
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.
def _get_sum_reduce_range(self, cursor, uid, policy_id, controlling_date, lines, model,
move_relation_field, context=None):
""" We get all the lines related to the model with given credit policy.
We also reduce from the global set (lines) the move line to be excluded.
We try not to use direct SQL in order to respect security rules.
As we define the first set it is important.
The policy relation field MUST be named credit_policy_id The policy relation field MUST be named credit_policy_id
and the model must have a relation
with account move line. Warning: side effect on ``lines``
!!! We take the asumption that only receivable lines have a maturity date
and account must be reconcillable""" """
# MARK possible place for a good optimisation # MARK possible place for a good optimisation
context = context or {} if context is None:
context = {}
my_obj = self.pool.get(model) my_obj = self.pool.get(model)
move_l_obj = self.pool.get('account.move.line') move_l_obj = self.pool.get('account.move.line')
add_obj_ids = my_obj.search(cursor, uid, [('credit_policy_id', '=', policy_id)]) add_lines = []
neg_lines = []
add_obj_ids = my_obj.search(
cursor, uid, [('credit_policy_id', '=', policy_id)], context=context)
if add_obj_ids: if add_obj_ids:
add_lines = move_l_obj.search(cursor, uid, [(move_relation_field, 'in', add_obj_ids), add_lines = move_l_obj.search(
cursor, uid,
[(move_relation_field, 'in', add_obj_ids),
('date_maturity', '<=', controlling_date), ('date_maturity', '<=', controlling_date),
('partner_id', '!=', False), ('partner_id', '!=', False),
('reconcile_id', '=', False)]) ('reconcile_id', '=', False)],
context=context)
lines = list(set(lines + add_lines)) lines = list(set(lines + add_lines))
# we get all the lines that must be excluded at partner_level # we get all the lines that must be excluded at partner_level
# from the global set (even the one included at account level) # from the global set (even the one included at account level)
neg_obj_ids = my_obj.search(cursor, uid, [('credit_policy_id', '!=', policy_id), neg_obj_ids = my_obj.search(
('credit_policy_id', '!=', False)]) cursor, uid,
[('credit_policy_id', '!=', policy_id),
('credit_policy_id', '!=', False)],
context=context)
if neg_obj_ids: if neg_obj_ids:
# should we add ('id', 'in', lines) in domain ? it may give a veeery long SQL... # should we add ('id', 'in', lines) in domain ? it may give a veeery long SQL...
neg_lines = move_l_obj.search(cursor, uid, [(move_relation_field, 'in', neg_obj_ids), neg_lines = move_l_obj.search(
cursor, uid,
[(move_relation_field, 'in', neg_obj_ids),
('date_maturity', '<=', controlling_date), ('date_maturity', '<=', controlling_date),
('partner_id', '!=', False), ('partner_id', '!=', False),
('reconcile_id', '=', False)]) ('reconcile_id', '=', False)],
context=context)
if neg_lines: if neg_lines:
lines = list(set(lines) - set(neg_lines)) lines = list(set(lines) - set(neg_lines))
return lines return lines
def _get_partner_related_lines(self, cursor, uid, policy_id, controlling_date, lines, context=None): def _get_partner_related_lines(self, cursor, uid, policy_id, controlling_date, lines, context=None):
""" Get the move lines for a policy related to a partner.
Warning: side effect on ``lines``
"""
return self._get_sum_reduce_range(cursor, uid, policy_id, controlling_date, lines, return self._get_sum_reduce_range(cursor, uid, policy_id, controlling_date, lines,
'res.partner', 'partner_id', context=context) 'res.partner', 'partner_id', context=context)
def _get_invoice_related_lines(self, cursor, uid, policy_id, controlling_date, lines, context=None): def _get_invoice_related_lines(self, cursor, uid, policy_id, controlling_date, lines, context=None):
""" Get the move lines for a policy related to an invoice.
Warning: side effect on ``lines``
"""
return self._get_sum_reduce_range(cursor, uid, policy_id, controlling_date, lines, return self._get_sum_reduce_range(cursor, uid, policy_id, controlling_date, lines,
'account.invoice', 'invoice', context=context) 'account.invoice', 'invoice', context=context)
def _get_moves_line_to_process(self, cursor, uid, policy_id, controlling_date, context=None): def _get_moves_line_to_process(self, cursor, uid, policy_id, controlling_date, context=None):
"""Retrive all the move line to be procces for current policy. """Retrive all the move line to be procces for current policy.
This function is planned to be use only on one id. This function is planned to be use only on one id.
Priority of inclustion, exlusion is account, partner, invoice""" Priority of inclustion, exlusion is account, partner, invoice"""
context = context or {} if context is None:
context = {}
lines = [] lines = []
assert not (isinstance(policy_id, list) and len(policy_id) > 1), \
"policy_id: only one id expected"
if isinstance(policy_id, list): if isinstance(policy_id, list):
policy_id = policy_id[0] policy_id = policy_id[0]
# order of call MUST be respected priority is account, partner, invoice # there is a priority between the lines, depicted by the calls below
lines = self._get_account_related_lines(cursor, uid, policy_id, lines += self._get_account_related_lines(
controlling_date, lines, context=context) cursor, uid, policy_id, controlling_date, context=context)
lines = self._get_partner_related_lines(cursor, uid, policy_id, # warning, side effect method called on lines
controlling_date, lines, context=context) lines = self._get_partner_related_lines(
lines = self._get_invoice_related_lines(cursor, uid, policy_id, cursor, uid, policy_id, controlling_date, lines, context=context)
controlling_date, lines, context=context) lines = self._get_invoice_related_lines(
cursor, uid, policy_id, controlling_date, lines, context=context)
return lines return lines
def _check_lines_policies(self, cursor, uid, policy_id, lines, context=None): def _check_lines_policies(self, cursor, uid, policy_id, lines, context=None):
""" Check if there is credit line related to same move line but """ Check if there is credit line related to same move line but
related to an other policy""" related to an other policy"""
context = context or {} if context is None:
context = {}
if not lines: if not lines:
return [] return []
assert not (isinstance(policy_id, list) and len(policy_id) > 1), \
"policy_id: only one id expected"
if isinstance(policy_id, list): if isinstance(policy_id, list):
policy_id = policy_id[0] policy_id = policy_id[0]
cursor.execute("SELECT move_line_id FROM credit_control_line" cursor.execute("SELECT move_line_id FROM credit_control_line"
@@ -143,7 +175,6 @@ class CreditControlPolicy(Model):
return [] return []
class CreditControlPolicyLevel(Model): class CreditControlPolicyLevel(Model):
"""Define a policy level. A level allows to determine if """Define a policy level. A level allows to determine if
a move line is due and the level of overdue of the line""" a move line is due and the level of overdue of the line"""
@@ -151,48 +182,48 @@ class CreditControlPolicyLevel(Model):
_name = "credit.control.policy.level" _name = "credit.control.policy.level"
_order = 'level' _order = 'level'
_description = """A credit control policy level""" _description = """A credit control policy level"""
_columns = {'policy_id': fields.many2one('credit.control.policy', _columns = {
'policy_id': fields.many2one('credit.control.policy',
'Related Policy', required=True), 'Related Policy', required=True),
'name': fields.char('Name', size=128, required=True), 'name': fields.char('Name', size=128, required=True),
'level': fields.float('level', required=True), 'level': fields.float('Level', required=True),
'computation_mode': fields.selection([('net_days', 'Due date'), 'computation_mode': fields.selection([('net_days', 'Due Date'),
('end_of_month', 'Due Date: end of Month'), ('end_of_month', 'Due Date, End Of Month'),
('previous_date', 'Previous reminder')], ('previous_date', 'Previous Reminder')],
'Compute mode', 'Compute Mode',
required=True), required=True),
'delay_days': fields.integer('Delay in day', required='True'), 'delay_days': fields.integer('Delay (in days)', required='True'),
'mail_template_id': fields.many2one('email.template', 'Mail template', 'mail_template_id': fields.many2one('email.template', 'Mail Template',
required=True), required=True),
'canal': fields.selection([('manual', 'Manual'), 'canal': fields.selection([('manual', 'Manual'),
('mail', 'Mail')], ('mail', 'Mail')],
'Canal', required=True), 'Canal', required=True),
'custom_text': fields.text('Custom message', required=True, translate=True), 'custom_text': fields.text('Custom Message', required=True, translate=True),
} }
def _check_level_mode(self, cursor, uid, rids, context=None): def _check_level_mode(self, cursor, uid, rids, context=None):
"""We check that the smallest level is not based """ The smallest level of a policy cannot be computed on the
on a level using previous_date mode""" "previous_date". Return False if this happens. """
if not isinstance(rids, list): if isinstance(rids, (int, long)):
rids = [rids] rids = [rids]
for level in self.browse(cursor, uid, rids, context): for level in self.browse(cursor, uid, rids, context):
smallest_level_id = self.search(cursor, uid, [('policy_id', '=', level.policy_id.id)], smallest_level_id = self.search(
cursor, uid,
[('policy_id', '=', level.policy_id.id)],
order='level asc', limit=1, context=context) order='level asc', limit=1, context=context)
smallest_level = self.browse(cursor, uid, smallest_level_id[0], context) smallest_level = self.browse(cursor, uid, smallest_level_id[0], context)
if smallest_level.computation_mode == 'previous_date': if smallest_level.computation_mode == 'previous_date':
return False return False
return True return True
_sql_constraint = [('unique level', _sql_constraint = [('unique level',
'UNIQUE (policy_id, level)', 'UNIQUE (policy_id, level)',
'Level must be unique per policy')] 'Level must be unique per policy')]
_constraints = [(_check_level_mode, _constraints = [(_check_level_mode,
'The smallest level can not be of type Previous reminder', 'The smallest level can not be of type Previous Reminder',
['level'])] ['level'])]
def _previous_level(self, cursor, uid, policy_level, context=None): def _previous_level(self, cursor, uid, policy_level, context=None):
@@ -214,7 +245,7 @@ class CreditControlPolicyLevel(Model):
context=context) context=context)
return previous_level_ids[0] if previous_level_ids else None return previous_level_ids[0] if previous_level_ids else None
# ----- time related functions --------- # ----- sql time related methods ---------
def _net_days_get_boundary(self): def _net_days_get_boundary(self):
return " (mv_line.date_maturity + %(delay)s)::date <= date(%(controlling_date)s)" return " (mv_line.date_maturity + %(delay)s)::date <= date(%(controlling_date)s)"
@@ -242,9 +273,9 @@ class CreditControlPolicyLevel(Model):
def _get_first_level_lines(self, cursor, uid, level, controlling_date, lines, context=None): def _get_first_level_lines(self, cursor, uid, level, controlling_date, lines, context=None):
if not lines: if not lines:
return [] return []
"""Retrieve all the line that are linked to a frist level. """Retrieve all the move lines that are linked to a first level.
We use Raw SQL for perf. Security rule where applied in We use Raw SQL for performance. Security rule where applied in
policy object when line where retrieved""" policy object when the first set of lines were retrieved"""
sql = ("SELECT DISTINCT mv_line.id\n" sql = ("SELECT DISTINCT mv_line.id\n"
" FROM account_move_line mv_line\n" " FROM account_move_line mv_line\n"
" WHERE mv_line.id in %(line_ids)s\n" " WHERE mv_line.id in %(line_ids)s\n"
@@ -261,11 +292,9 @@ class CreditControlPolicyLevel(Model):
return [] return []
return [x[0] for x in res] return [x[0] for x in res]
def _get_other_level_lines(self, cursor, uid, level, controlling_date, lines, context=None): def _get_other_level_lines(self, cursor, uid, level, controlling_date, lines, context=None):
# We filter line that have a level smaller than current one """ Retrieve the move lines for other levels than first level.
# TODO if code fits need refactor _get_first_level_lines and _get_other_level_lines """
# Code is not DRY
if not lines: if not lines:
return [] return []
sql = ("SELECT mv_line.id\n" sql = ("SELECT mv_line.id\n"

View File

@@ -28,6 +28,8 @@ from openerp.osv.osv import except_osv
logger = logging.getLogger('Credit Control run') logger = logging.getLogger('Credit Control run')
# beware, do always use the DB lock of `CreditControlRun.generate_credit_lines`
# to use this variable, otherwise you will have race conditions
memoizers = {} memoizers = {}
@@ -37,13 +39,13 @@ class CreditControlRun(Model):
_name = "credit.control.run" _name = "credit.control.run"
_rec_name = 'date' _rec_name = 'date'
_description = """Credit control line generator""" _description = """Credit control line generator"""
_columns = {'date': fields.date('Controlling date', required=True), _columns = {
'date': fields.date('Controlling Date', required=True),
'policy_ids': fields.many2many('credit.control.policy', 'policy_ids': fields.many2many('credit.control.policy',
rel="credit_run_policy_rel", rel="credit_run_policy_rel",
id1='run_id', id2='policy_id',
string='Policies', string='Policies',
readonly=True, readonly=True,
help="If nothing set all policies will be used",
states={'draft': [('readonly', False)]}), states={'draft': [('readonly', False)]}),
'report': fields.text('Report', readonly=True), 'report': fields.text('Report', readonly=True),
@@ -58,41 +60,51 @@ class CreditControlRun(Model):
'manual_ids': fields.many2many('account.move.line', 'manual_ids': fields.many2many('account.move.line',
rel="credit_runreject_rel", rel="credit_runreject_rel",
string='Line to be handled manually', string='Lines to handle manually',
readonly=True), readonly=True),
} }
_defaults = {'state': 'draft'} def _get_policies(self, cursor, uid, context=None):
return self.pool.get('credit.control.policy').\
search(cursor, uid, [], context=context)
def check_run_date(self, cursor, uid, ids, controlling_date, context=None): _defaults = {
'state': 'draft',
'policy_ids': _get_policies,
}
def _check_run_date(self, cursor, uid, ids, controlling_date, context=None):
"""Ensure that there is no credit line in the future using controlling_date""" """Ensure that there is no credit line in the future using controlling_date"""
line_obj = self.pool.get('credit.control.line') line_obj = self.pool.get('credit.control.line')
lines = line_obj.search(cursor, uid, [('date', '>', controlling_date)], lines = line_obj.search(cursor, uid, [('date', '>', controlling_date)],
order='date DESC', limit=1) order='date DESC', limit=1, context=context)
if lines: if lines:
line = line_obj.browse(cursor, uid, lines[0]) line = line_obj.browse(cursor, uid, lines[0], context=context)
raise except_osv(_('A run was already executed in a greater date'), raise except_osv(_('A run has already been executed more recently than %s') % (line.date))
_('Run date should be >= %s') % (line.date)) return True
def _generate_credit_lines(self, cursor, uid, run_id, context=None): def _generate_credit_lines(self, cursor, uid, run_id, context=None):
""" Generate credit line. Function can be a little dryer but """ Generate credit control lines. """
it does almost noting, initalise variable maange error and call
real know how method"""
memoizers['credit_line_residuals'] = {} memoizers['credit_line_residuals'] = {}
cr_line_obj = self.pool.get('credit.control.line') 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): if isinstance(run_id, list):
run_id = run_id[0] run_id = run_id[0]
run = self.browse(cursor, uid, run_id, context=context) run = self.browse(cursor, uid, run_id, context=context)
errors = [] errors = []
manualy_managed_lines = [] #line who changed policy manually_managed_lines = [] # line who changed policy
credit_line_ids = [] # generated lines credit_line_ids = [] # generated lines
run.check_run_date(run.date, context=context) run._check_run_date(run.date, context=context)
policy_ids = run.policy_ids policy_ids = run.policy_ids
if not policy_ids: if not policy_ids:
policy_obj = self.pool.get('credit.control.policy') raise except_osv(
policy_ids_ids = policy_obj.search(cursor, uid, []) _('Error'),
policy_ids = policy_obj.browse(cursor, uid, policy_ids_ids) _('Please select a policy'))
lines = []
for policy in policy_ids: for policy in policy_ids:
if policy.do_nothing: if policy.do_nothing:
continue continue
@@ -100,43 +112,50 @@ class CreditControlRun(Model):
lines = policy._get_moves_line_to_process(run.date, context=context) lines = policy._get_moves_line_to_process(run.date, context=context)
tmp_manual = policy._check_lines_policies(lines, context=context) tmp_manual = policy._check_lines_policies(lines, context=context)
lines = list(set(lines) - set(tmp_manual)) lines = list(set(lines) - set(tmp_manual))
manualy_managed_lines += tmp_manual manually_managed_lines += tmp_manual
if not lines: if not lines:
continue continue
# policy levels are sorted by level so iteration is in the correct order # policy levels are sorted by level so iteration is in the correct order
for level in policy.level_ids: for level in policy.level_ids:
level_lines = level.get_level_lines(run.date, lines) level_lines = level.get_level_lines(run.date, lines, context=context)
# only this write action own a separate cursor # only this write action own a separate cursor
credit_line_ids += cr_line_obj.create_or_update_from_mv_lines( loc_ids, loc_errors = cr_line_obj.create_or_update_from_mv_lines(
cursor, uid, [], level_lines, level.id, run.date, errors=errors, context=context) cursor, uid, [], level_lines, level.id, run.date, context=context)
credit_line_ids += loc_ids
errors += loc_errors
lines = list(set(lines) - set(level_lines)) lines = list(set(lines) - set(level_lines))
except except_osv, exc: except except_osv, exc:
# TODO: check if rollback on cursor is safe ?
cursor.rollback() cursor.rollback()
error_type, error_value, trbk = sys.exc_info() error_type, error_value, trbk = sys.exc_info()
st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value) st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value)
st += ''.join(traceback.format_tb(trbk, 30)) st += ''.join(traceback.format_tb(trbk, 30))
logger.error(st) logger.error(st)
self.write(cursor, uid, [run.id], {'report':st, 'state': 'error'}) self.write(cursor, uid,
[run.id],
{'report':st,
'state': 'error'},
context=context)
return False return False
vals = {'report': u"Number of generated lines : %s \n" % (len(credit_line_ids),), vals = {'report': u"Number of generated lines : %s \n" % (len(credit_line_ids),),
'state': 'done', 'state': 'done',
'manual_ids': [(6, 0, manualy_managed_lines)]} 'manual_ids': [(6, 0, manually_managed_lines)]}
if errors: if errors:
vals['report'] += u"Following line generation errors appends:" vals['report'] += u"Following line generation errors appends:"
vals['report'] += u"----\n".join(errors) vals['report'] += u"----\n".join(errors)
vals['state'] = 'done' vals['state'] = 'done'
run.write(vals) run.write(vals, context=context)
# lines will correspond to line that where not treated # lines will correspond to line that where not treated
return lines return lines
def generate_credit_lines(self, cursor, uid, run_id, context=None): def generate_credit_lines(self, cursor, uid, run_id, context=None):
"""Generate credit control lines""" """Generate credit control lines
Lock the ``credit_control_run`` Postgres table to avoid concurrent
calls of this method.
"""
context = context or {} context = context or {}
# we do a little magical tips in order to ensure non concurrent run
# of the function generate_credit_lines
try: try:
cursor.execute('SELECT id FROM credit_control_run' cursor.execute('SELECT id FROM credit_control_run'
' LIMIT 1 FOR UPDATE NOWAIT' ) ' LIMIT 1 FOR UPDATE NOWAIT' )