forked from OCA/account-financial-tools
-
Notifications
You must be signed in to change notification settings - Fork 3
/
policy.py
406 lines (362 loc) · 16.9 KB
/
policy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi, Guewen Baconnier
# Copyright 2012-2014 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import models, fields, api, _
class CreditControlPolicy(models.Model):
""" Define a policy of reminder """
_name = "credit.control.policy"
_description = """Define a reminder policy"""
name = fields.Char('Name', required=True)
level_ids = fields.One2many('credit.control.policy.level',
'policy_id',
string='Policy Levels')
do_nothing = fields.Boolean('Do nothing',
help='For policies which should not '
'generate lines or are obsolete')
company_id = fields.Many2one('res.company', string='Company')
account_ids = fields.Many2many(
'account.account',
string='Accounts',
required=True,
domain="[('type', '=', 'receivable')]",
help="This policy will be active only"
" for the selected accounts",
)
active = fields.Boolean('Active', default=True)
@api.multi
def _move_lines_domain(self, controlling_date):
""" Build the default domain for searching move lines """
self.ensure_one()
return [('account_id', 'in', self.account_ids.ids),
('date_maturity', '<=', controlling_date),
('reconcile_id', '=', False),
('partner_id', '!=', False)]
@api.multi
@api.returns('account.move.line')
def _due_move_lines(self, controlling_date):
""" Get the due move lines for the policy of the company.
The set of ids will be reduced and extended according
to the specific policies defined on partners and invoices.
Do not use direct SQL in order to respect security rules.
Assume that only the receivable lines have a maturity date and that
accounts used in the policy are reconcilable.
"""
self.ensure_one()
move_l_obj = self.env['account.move.line']
user = self.env.user
if user.company_id.credit_policy_id.id != self.id:
return move_l_obj.browse()
domain_line = self._move_lines_domain(controlling_date)
return move_l_obj.search(domain_line)
@api.multi
@api.returns('account.move.line')
def _move_lines_subset(self, controlling_date, model, move_relation_field):
""" Get the move lines related to one model for a policy.
Do not use direct SQL in order to respect security rules.
Assume that only the receivable lines have a maturity date and that
accounts used in the policy are reconcilable.
The policy relation field must be named credit_policy_id.
:param str controlling_date: date of credit control
:param str model: name of the model where is defined a credit_policy_id
:param str move_relation_field: name of the field in account.move.line
which is a many2one to `model`
:return: recordset to add in the process, recordset to remove from
the process
"""
self.ensure_one()
# MARK possible place for a good optimisation
my_obj = self.env[model]
move_l_obj = self.env['account.move.line']
default_domain = self._move_lines_domain(controlling_date)
to_add = move_l_obj.browse()
to_remove = move_l_obj.browse()
# The lines which are linked to this policy have to be included in the
# run for this policy.
# If another object override the credit_policy_id (ie. invoice after
add_objs = my_obj.search([('credit_policy_id', '=', self.id)])
if add_objs:
domain = list(default_domain)
domain.append((move_relation_field, 'in', add_objs.ids))
to_add = move_l_obj.search(domain)
# The lines which are linked to another policy do not have to be
# included in the run for this policy.
neg_objs = my_obj.search([('credit_policy_id', '!=', self.id),
('credit_policy_id', '!=', False)])
if neg_objs:
domain = list(default_domain)
domain.append((move_relation_field, 'in', neg_objs.ids))
to_remove = move_l_obj.search(domain)
return to_add, to_remove
@api.multi
@api.returns('account.move.line')
def _get_partner_related_lines(self, controlling_date):
""" Get the move lines for a policy related to a partner.
:param str controlling_date: date of credit control
:param str model: name of the model where is defined a credit_policy_id
:param str move_relation_field: name of the field in account.move.line
which is a many2one to `model`
:return: recordset to add in the process, recordset to remove from
the process
"""
return self._move_lines_subset(controlling_date, 'res.partner',
'partner_id')
@api.multi
@api.returns('account.move.line')
def _get_invoice_related_lines(self, controlling_date):
""" Get the move lines for a policy related to an invoice.
:param str controlling_date: date of credit control
:param str model: name of the model where is defined a credit_policy_id
:param str move_relation_field: name of the field in account.move.line
which is a many2one to `model`
:return: recordset to add in the process, recordset to remove from
the process
"""
return self._move_lines_subset(controlling_date, 'account.invoice',
'invoice')
@api.multi
@api.returns('account.move.line')
def _get_move_lines_to_process(self, controlling_date):
""" Build a list of move lines ids to include in a run
for a policy at a given date.
:param str controlling_date: date of credit control
:return: recordset to include in the run
"""
self.ensure_one()
# there is a priority between the lines, depicted by the calls below
lines = self._due_move_lines(controlling_date)
to_add, to_remove = self._get_partner_related_lines(controlling_date)
lines = (lines | to_add) - to_remove
to_add, to_remove = self._get_invoice_related_lines(controlling_date)
lines = (lines | to_add) - to_remove
return lines
@api.multi
@api.returns('account.move.line')
def _lines_different_policy(self, lines):
""" Return a set of move lines ids for which there is an
existing credit line but with a different policy.
"""
self.ensure_one()
move_line_obj = self.env['account.move.line']
different_lines = move_line_obj.browse()
if not lines:
return different_lines
cr = self.env.cr
cr.execute("SELECT move_line_id FROM credit_control_line"
" WHERE policy_id != %s and move_line_id in %s"
" AND manually_overridden IS false",
(self.id, tuple(lines.ids)))
res = cr.fetchall()
if res:
return move_line_obj.browse([row[0] for row in res])
return different_lines
@api.multi
def check_policy_against_account(self, account):
""" Ensure that the policy corresponds to account relation """
policies = self.search([])
allowed = [x for x in policies
if account in x.account_ids or x.do_nothing]
if self not in allowed:
allowed_names = u"\n".join(x.name for x in allowed)
raise api.Warning(
_('You can only use a policy set on '
'account %s.\n'
'Please choose one of the following '
'policies:\n %s') % (account.name, allowed_names)
)
return True
class CreditControlPolicyLevel(models.Model):
"""Define a policy level. A level allows to determine if
a move line is due and the level of overdue of the line"""
_name = "credit.control.policy.level"
_order = 'level'
_description = """A credit control policy level"""
name = fields.Char(string='Name', required=True, translate=True)
policy_id = fields.Many2one('credit.control.policy',
string='Related Policy',
required=True)
level = fields.Integer(string='Level', required=True)
computation_mode = fields.Selection(
[('net_days', 'Due Date'),
('end_of_month', 'Due Date, End Of Month'),
('previous_date', 'Previous Reminder')],
string='Compute Mode',
required=True
)
delay_days = fields.Integer(string='Delay (in days)', required=True)
email_template_id = fields.Many2one('email.template',
string='Email Template',
required=True)
channel = fields.Selection([('letter', 'Letter'),
('email', 'Email')],
string='Channel',
required=True)
custom_text = fields.Text(string='Custom Message',
required=True,
translate=True)
custom_mail_text = fields.Html(string='Custom Mail Message',
required=True, translate=True)
_sql_constraint = [('unique level',
'UNIQUE (policy_id, level)',
'Level must be unique per policy')]
@api.one
@api.constrains('level', 'computation_mode')
def _check_level_mode(self):
""" The smallest level of a policy cannot be computed on the
"previous_date".
"""
smallest_level = self.search([('policy_id', '=', self.policy_id.id)],
order='level asc', limit=1)
if smallest_level.computation_mode == 'previous_date':
return api.ValidationError(_('The smallest level can not be of '
'type Previous Reminder'))
@api.multi
def _previous_level(self):
""" For one policy level, returns the id of the previous level
If there is no previous level, it returns None, it means that's the
first policy level
:return: previous level or None if there is no previous level
"""
self.ensure_one()
previous_levels = self.search([('policy_id', '=', self.policy_id.id),
('level', '<', self.level)],
order='level desc',
limit=1)
if not previous_levels:
return None
return previous_levels
# ----- sql time related methods ---------
@staticmethod
def _net_days_get_boundary():
return (" (mv_line.date_maturity + %(delay)s)::date <= "
"date(%(controlling_date)s)")
@staticmethod
def _end_of_month_get_boundary():
return ("(date_trunc('MONTH', (mv_line.date_maturity + %(delay)s))+"
"INTERVAL '1 MONTH - 1 day')::date"
"<= date(%(controlling_date)s)")
@staticmethod
def _previous_date_get_boundary():
return "(cr_line.date + %(delay)s)::date <= date(%(controlling_date)s)"
@api.multi
def _get_sql_date_boundary_for_computation_mode(self, controlling_date):
""" Return a where clauses statement for the given controlling
date and computation mode of the level
"""
self.ensure_one()
fname = "_%s_get_boundary" % (self.computation_mode, )
if hasattr(self, fname):
fnc = getattr(self, fname)
return fnc()
else:
raise NotImplementedError(
_('Can not get function for computation mode: '
'%s is not implemented') % (fname, )
)
# -----------------------------------------
@api.multi
@api.returns('account.move.line')
def _get_first_level_move_lines(self, controlling_date, lines):
""" Retrieve all the move lines that are linked to a first level.
We use Raw SQL for performance. Security rule where applied in
policy object when the first set of lines were retrieved
"""
self.ensure_one()
move_line_obj = self.env['account.move.line']
if not lines:
return move_line_obj.browse()
cr = self.env.cr
sql = ("SELECT DISTINCT mv_line.id\n"
" FROM account_move_line mv_line\n"
" WHERE mv_line.id in %(line_ids)s\n"
" AND NOT EXISTS (SELECT id\n"
" FROM credit_control_line\n"
" WHERE move_line_id = mv_line.id\n"
# lines from a previous level with a draft or ignored state
# or manually overridden
# have to be generated again for the previous level
" AND NOT manually_overridden\n"
" AND state NOT IN ('draft', 'ignored'))"
" AND (mv_line.debit IS NOT NULL AND mv_line.debit != 0.0)\n")
sql += " AND"
_get_sql_date_part = self._get_sql_date_boundary_for_computation_mode
sql += _get_sql_date_part(controlling_date)
data_dict = {'controlling_date': controlling_date,
'line_ids': tuple(lines.ids),
'delay': self.delay_days}
cr.execute(sql, data_dict)
res = cr.fetchall()
if res:
return move_line_obj.browse([row[0] for row in res])
return move_line_obj.browse()
@api.multi
@api.returns('account.move.line')
def _get_other_level_move_lines(self, controlling_date, lines):
""" Retrieve the move lines for other levels than first level.
"""
self.ensure_one()
move_line_obj = self.env['account.move.line']
if not lines:
return move_line_obj.browse()
cr = self.env.cr
sql = ("SELECT mv_line.id\n"
" FROM account_move_line mv_line\n"
" JOIN credit_control_line cr_line\n"
" ON (mv_line.id = cr_line.move_line_id)\n"
" WHERE cr_line.id = (SELECT credit_control_line.id "
" FROM credit_control_line\n"
" WHERE credit_control_line.move_line_id = mv_line.id\n"
" AND state != 'ignored'"
" AND NOT manually_overridden"
" ORDER BY credit_control_line.level desc limit 1)\n"
" AND cr_line.level = %(previous_level)s\n"
" AND (mv_line.debit IS NOT NULL AND mv_line.debit != 0.0)\n"
# lines from a previous level with a draft or ignored state
# or manually overridden
# have to be generated again for the previous level
" AND NOT manually_overridden\n"
" AND cr_line.state NOT IN ('draft', 'ignored')\n"
" AND mv_line.id in %(line_ids)s\n")
sql += " AND "
_get_sql_date_part = self._get_sql_date_boundary_for_computation_mode
sql += _get_sql_date_part(controlling_date)
previous_level = self._previous_level()
data_dict = {'controlling_date': controlling_date,
'line_ids': tuple(lines.ids),
'delay': self.delay_days,
'previous_level': previous_level.level}
# print cr.mogrify(sql, data_dict)
cr.execute(sql, data_dict)
res = cr.fetchall()
if res:
return move_line_obj.browse([row[0] for row in res])
return move_line_obj.browse()
@api.multi
@api.returns('account.move.line')
def get_level_lines(self, controlling_date, lines):
""" get all move lines in entry lines that match the current level """
self.ensure_one()
move_line_obj = self.env['account.move.line']
matching_lines = move_line_obj.browse()
if self._previous_level() is None:
method = self._get_first_level_move_lines
else:
method = self._get_other_level_move_lines
matching_lines |= method(controlling_date, lines)
return matching_lines