1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# Author: Nicolas Bessi
5
# Copyright 2012 Camptocamp SA
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Affero General Public License as
9
# published by the Free Software Foundation, either version 3 of the
10
# License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Affero General Public License for more details.
17
# You should have received a copy of the GNU Affero General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
##############################################################################
21
from datetime import datetime
23
from openerp.osv.orm import Model, fields
24
from openerp.tools.translate import _
25
from openerp.addons.account_credit_control import run
27
class AccountAccount(Model):
28
"""Add a link to a credit control policy on account account"""
31
def _check_account_type_compatibility(self, cursor, uid, acc_ids, context=None):
32
"""We check that account is of type reconcile"""
33
if not isinstance(acc_ids, list):
35
for acc in self.browse(cursor, uid, acc_ids, context):
36
if acc.credit_policy_id and not acc.reconcile:
40
_inherit = "account.account"
41
_description = """Add a link to a credit policy"""
42
_columns = {'credit_policy_id': fields.many2one('credit.control.policy',
43
'Credit control policy',
44
help=("Define global credit policy"
45
"order is account partner invoice")),
47
'credit_control_line_ids': fields.one2many('credit.control.line',
49
string='Credit Lines',
52
_constraints = [(_check_account_type_compatibility,
53
_('You can not set a credit policy on a non reconciliable account'),
54
['credit_policy_id'])]
56
class AccountInvoice(Model):
57
"""Add a link to a credit control policy on account account"""
59
_inherit = "account.invoice"
60
_description = """Add a link to a credit policy"""
61
_columns = {'credit_policy_id': fields.many2one('credit.control.policy',
62
'Credit control policy',
63
help=("Define global credit policy"
64
"order is account partner invoice")),
66
'credit_control_line_ids': fields.one2many('credit.control.line',
68
string='Credit Lines',
71
def action_move_create(self, cursor, uid, ids, context=None):
72
"""We ensure writing of invoice id in move line because
73
Trigger field may not work without account_voucher addon"""
74
res = super(AccountInvoice, self).action_move_create(cursor, uid, ids, context=context)
75
for inv in self.browse(cursor, uid, ids, context=context):
77
for line in inv.move_id.line_id:
78
line.write({'invoice_id': inv.id})
82
class AccountMoveLine(Model):
83
"""Add a function that compute the residual amount using a follow up date
84
Add relation between move line and invoicex"""
86
_inherit = "account.move.line"
87
# Store fields has strange behavior with voucher module we had to overwrite invoice
90
# def _invoice_id(self, cursor, user, ids, name, arg, context=None):
91
# #Code taken from OpenERP account addon
92
# invoice_obj = self.pool.get('account.invoice')
95
# res[line_id] = False
96
# cursor.execute('SELECT l.id, i.id ' \
97
# 'FROM account_move_line l, account_invoice i ' \
98
# 'WHERE l.move_id = i.move_id ' \
102
# for line_id, invoice_id in cursor.fetchall():
103
# res[line_id] = invoice_id
104
# invoice_ids.append(invoice_id)
105
# invoice_names = {False: ''}
106
# for invoice_id, name in invoice_obj.name_get(cursor, user, invoice_ids, context=context):
107
# invoice_names[invoice_id] = name
108
# for line_id in res.keys():
109
# invoice_id = res[line_id]
110
# res[line_id] = (invoice_id, invoice_names[invoice_id])
113
# def _get_invoice(self, cursor, uid, ids, context=None):
115
# for line in self.pool.get('account.invoice').browse(cursor, uid, ids, context=context):
117
# ids = [x.id for x in line.move_id.line_id or []]
118
# return list(result)
120
# _columns = {'invoice_id': fields.function(_invoice_id, string='Invoice',
121
# type='many2one', relation='account.invoice',
122
# store={'account.invoice': (_get_invoice, ['move_id'], 20)})}
124
_columns = {'invoice_id': fields.many2one('account.invoice', 'Invoice')}
126
def _get_payment_and_credit_lines(self, moveline_array, lookup_date):
129
for line in moveline_array:
130
if self._should_exlude_line(line):
132
if line.account_id.type == 'receivable' and line.debit:
133
credit_lines.append(line)
135
if line.reconcile_partial_id:
136
payment_lines.append(line)
137
credit_lines.sort(key=operator.attrgetter('date'))
138
payment_lines.sort(key=operator.attrgetter('date'))
139
return (credit_lines, payment_lines)
141
def _validate_line_currencies(self, credit_lines):
142
"""Raise an excpetion if there is lines with different currency"""
143
if len(credit_lines) == 0:
145
currency = credit_lines[0].currency_id.id
146
if not all(obj.currency_id.id == currency for obj in credit_lines):
147
raise Exception('Not all line of move line are in the same currency')
149
def _get_value_amount(self, mv_line_br):
150
if mv_line_br.currency_id:
151
return mv_line_br.amount_currency
153
return mv_line_br.debit - mv_line_br.credit
155
def _validate_partial(self, credit_lines):
156
if len(credit_lines) == 0:
159
line_with_partial = 0
160
for line in credit_lines:
161
if not line.reconcile_partial_id:
162
line_with_partial += 1
163
if line_with_partial and line_with_partial != len(credit_lines):
164
raise Exception('Can not compute credit line if multiple'
165
' lines are not all linked to a partial')
167
def _get_applicable_payment_lines(self, credit_line, payment_lines):
168
applicable_payment = []
169
for pay_line in payment_lines:
170
if datetime.strptime(pay_line.date, "%Y-%m-%d").date() \
171
<= datetime.strptime(credit_line.date, "%Y-%m-%d").date():
172
applicable_payment.append(pay_line)
173
return applicable_payment
175
def _compute_partial_reconcile_residual(self, move_lines, lookup_date, move_id, memoizer):
176
""" Compute open amount of multiple credit lines linked to multiple payment lines"""
177
credit_lines, payment_lines = self._get_payment_and_credit_lines(move_lines, lookup_date, memoizer)
178
self._validate_line_currencies(credit_lines)
179
self._validate_line_currencies(payment_lines)
180
self._validate_partial(credit_lines)
181
# memoizer structure move_id : {move_line_id: open_amount}
182
# paymnent line and credit line are sorted by date
184
for credit_line in credit_lines:
185
applicable_payment = self._get_applicable_payment_lines(credit_line, payment_lines)
187
for pay_line in applicable_payment:
188
paid_amount += self._get_value_amount(pay_line)
189
balance_amount = self._get_value_amount(credit_lines) - (paid_amount + rest)
190
memoizer[move_id][credit_line.id] = balance_amount
191
if balance_amount < 0.0:
192
rest = balance_amount
197
def _compute_fully_open_amount(self, move_lines, lookup_date, move_id, memoizer):
198
for move_line in move_lines:
199
memoizer[move_id][move_line.id] = self._get_value_amount(move_line)
203
def _amount_residual_from_date(self, cursor, uid, mv_line_br, lookup_date, context=None):
205
Code from function _amount_residual of account/account_move_line.py does not take
206
in account mulitple line payment and reconciliation. We have to rewrite it
207
Code computes residual amount at lookup date for mv_line_br in entry
209
memoizer = run.memoizers['credit_line_residuals']
210
move_id = mv_line_br.move_id.id
211
if mv_line_br.move_id.id in memoizer:
212
pass # get back value
214
memoizer[move_id] = {}
215
move_lines = mv_line_br.move_id.line_id
216
if mv_line_br.reconcile_partial_id:
217
self._compute_partial_reconcile_residual(move_lines, lookup_date, move_id, memoizer)
219
self._compute_fully_open_amount(move_lines, lookup_date, move_id, memoizer)
220
return memoizer[move_id][mv_line_br.id]