From caa628c825c60c9e785c0349e43252f57cbc2daf Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 28 Oct 2014 16:19:45 +0100 Subject: [PATCH] Migrate the main models to the new API --- account_credit_control/account.py | 26 +- account_credit_control/company.py | 30 +- account_credit_control/line.py | 304 ++++++++------------ account_credit_control/mail.py | 10 +- account_credit_control/partner.py | 82 ++---- account_credit_control/policy.py | 464 +++++++++++++----------------- account_credit_control/run.py | 196 ++++++------- 7 files changed, 462 insertions(+), 650 deletions(-) diff --git a/account_credit_control/account.py b/account_credit_control/account.py index 8fe57e27b..0c00a8e23 100644 --- a/account_credit_control/account.py +++ b/account_credit_control/account.py @@ -18,27 +18,15 @@ # along with this program. If not, see . # ############################################################################## -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) diff --git a/account_credit_control/company.py b/account_credit_control/company.py index 790fc05f1..cd94ef7c8 100644 --- a/account_credit_control/company.py +++ b/account_credit_control/company.py @@ -18,24 +18,20 @@ # along with this program. If not, see . # ############################################################################## -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.") diff --git a/account_credit_control/line.py b/account_credit_control/line.py index 964878fa4..02d1dfbd4 100644 --- a/account_credit_control/line.py +++ b/account_credit_control/line.py @@ -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() diff --git a/account_credit_control/mail.py b/account_credit_control/mail.py index 25614913c..4636c81be 100644 --- a/account_credit_control/mail.py +++ b/account_credit_control/mail.py @@ -18,14 +18,12 @@ # along with this program. If not, see . # ############################################################################## -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") diff --git a/account_credit_control/partner.py b/account_credit_control/partner.py index ebfee0f9e..9255e7ff4 100644 --- a/account_credit_control/partner.py +++ b/account_credit_control/partner.py @@ -18,61 +18,41 @@ # along with this program. If not, see . # ############################################################################## -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) diff --git a/account_credit_control/policy.py b/account_credit_control/policy.py index 0b4a5c8ca..4806cdf95 100644 --- a/account_credit_control/policy.py +++ b/account_credit_control/policy.py @@ -18,61 +18,47 @@ # along with this program. If not, see . # ############################################################################## -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 diff --git a/account_credit_control/run.py b/account_credit_control/run.py index fd32be1dc..e25518f1c 100644 --- a/account_credit_control/run.py +++ b/account_credit_control/run.py @@ -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