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
|
#!/usr/bin/env python
#-*- encoding:utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2011 TeMPO Consulting, MSF. All Rights Reserved
# Developer: Olivier DOSSMANN
#
# 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 osv import osv
from osv import fields
from tools.translate import _
class account_invoice(osv.osv):
_name = 'account.invoice'
_inherit = 'account.invoice'
def _search_imported_state(self, cr, uid, ids, name, args, context=None):
"""
Search invoice regarding their imported_state field. Check _get_imported_state for more information.
"""
res = [('id', 'not in', [])]
if args and args[0] and len(args[0]) == 3:
if args[0][1] != '=':
raise osv.except_osv(_('Error'), _('Operator not supported yet!'))
# Search all imported invoice
sql = """SELECT INV_ID, INV_TOTAL, abs(SUM(absl.amount))
FROM (
SELECT inv.id AS INV_ID, inv.amount_total AS INV_TOTAL, aml.id AS AML
FROM account_invoice inv, account_move_line aml, account_move am
WHERE inv.move_id = am.id
AND aml.move_id = am.id
AND inv.state = 'open'
ORDER BY inv.id
) AS move_lines, imported_invoice imp, account_bank_statement_line absl
WHERE imp.move_line_id = move_lines.AML
AND imp.st_line_id = absl.id
GROUP BY INV_ID, INV_TOTAL"""
# Fetch second args (type of import)
s = args[0][2]
# Complete SQL query if needed
if s == 'imported':
sql += """ HAVING INV_TOTAL = abs(SUM(absl.amount))"""
elif s == 'partial':
sql += """ HAVING INV_TOTAL != abs(SUM(absl.amount))"""
# finish SQL query
sql += """ ORDER BY INV_ID;"""
# execution
cr.execute(sql)
sql_res = cr.fetchall()
# process regarding second args
if s in ['partial', 'imported']:
res = [('id', 'in', [x and x[0] for x in sql_res])]
else:
res = [('id', 'not in', [x and x[0] for x in sql_res])]
return res
def _get_imported_state(self, cr, uid, ids, field_name=None, arg=None, context=None):
"""
Different states:
- imported: imported_invoice_line_ids exists for this invoice (so register lines are linked to it) and invoice state is paid
- partial: imported_invoice_line_ids exists for this invoice (so that register lines are linked to it) and invoice state is open (so not totally paid)
- not: no imported_invoice_line_ids on this invoice (so no link to a register)
- unknown: default state
"""
if not context:
context = {}
res = {}
for inv in self.browse(cr, uid, ids, context):
res[inv.id] = 'none'
if inv.move_id:
absl_ids = self.pool.get('account.bank.statement.line').search(cr, uid, [('imported_invoice_line_ids', 'in', [x.id for x in inv.move_id.line_id])])
if absl_ids:
res[inv.id] = 'imported'
if isinstance(absl_ids, (int, long)):
absl_ids = [absl_ids]
if inv.amount_total != sum([x and abs(x.amount) or 0.0 for x in self.pool.get('account.bank.statement.line').browse(cr, uid, absl_ids)]):
res[inv.id] = 'partial'
continue
res[inv.id] = 'not'
return res
def _get_down_payment_ids(self, cr, uid, ids, field_name=None, arg=None, context=None):
"""
Search down payment journal items for given invoice
"""
# Some checks
if not context:
context = {}
res = {}
for inv in self.browse(cr, uid, ids):
res[inv.id] = []
for p in inv.purchase_ids:
res[inv.id] += [x and x.id for x in p.down_payment_ids]
return res
_columns = {
'imported_state': fields.function(_get_imported_state, fnct_search=_search_imported_state, method=True, store=False, type='selection', selection=[('none', 'None'), ('imported', 'Imported'), ('not', 'Not Imported'), ('partial', 'Partially Imported')], string='Imported Status'),
'down_payment_ids': fields.function(_get_down_payment_ids, type="one2many", obj='account.move.line', method=True, string='Down payments'),
}
def create_down_payments(self, cr, uid, ids, amount, context=None):
"""
Create down payments for given invoices
"""
# Some verifications
if isinstance(ids, (int, long)):
ids = [ids]
if not amount:
raise osv.except_osv(_('Warning'), _('Amount for Down Payment is missing!'))
# Prepare some values
res = []
# Browse all elements
for inv in self.browse(cr, uid, ids):
# some verification
if amount > inv.amount_total:
raise osv.except_osv(_('Error'), _('Given down payment amount is superior to given invoice. Please check both.'))
# prepare some values
total = 0.0
to_use = [] # should contains tuple with: down payment line id, amount
# Create down payment until given amount is reached
# browse all invoice purchase, then all down payment attached to purchases
for po in inv.purchase_ids:
# Order by id all down payment in order to have them in creation order
dp_ids = self.pool.get('account.move.line').search(cr, uid, [('down_payment_id', '=', po.id)], order='date ASC, id ASC')
for dp in self.pool.get('account.move.line').browse(cr, uid, dp_ids):
# verify that total is not superior to demanded amount
if total >= amount:
continue
diff = 0.0
# Take only line that have a down_payment_amount not superior or equal to line amount
if not dp.down_payment_amount > dp.amount_currency:
if amount > (abs(dp.amount_currency) - abs(dp.down_payment_amount)):
diff = (abs(dp.amount_currency) - abs(dp.down_payment_amount))
else:
diff = amount
# Have a tuple containing line id and amount to use for create a payment on invoice
to_use.append((dp.id, diff))
# Increment processed total
total += diff
# Create counterparts and reconcile them
for el in to_use:
# create down payment counterpart on dp account
dp_info = self.pool.get('account.move.line').browse(cr, uid, el[0])
# first create the move
vals = {
'journal_id': dp_info.statement_id and dp_info.statement_id.journal_id and dp_info.statement_id.journal_id.id or False,
'period_id': inv.period_id.id,
'date': inv.date_invoice,
'partner_id': inv.partner_id.id,
'ref': ':'.join(['%s' % (x.name or '') for x in inv.purchase_ids]),
}
move_id = self.pool.get('account.move').create(cr, uid, vals)
# then 2 lines for this move
vals.update({
'move_id': move_id,
'partner_type_mandatory': True,
'currency_id': inv.currency_id.id,
'name': 'Down payment for ' + ':'.join(['%s' % (x.name or '') for x in inv.purchase_ids]),
'document_date': inv.document_date,
})
# create dp counterpart line
dp_account = dp_info and dp_info.account_id and dp_info.account_id.id or False
debit = 0.0
credit = el[1]
if amount < 0:
credit = 0.0
debit = el[1]
vals.update({
'account_id': dp_account or False,
'debit_currency': debit,
'credit_currency': credit,
})
dp_counterpart_id = self.pool.get('account.move.line').create(cr, uid, vals)
# create supplier line
vals.update({
'account_id': inv.account_id.id,
'debit_currency': credit, # opposite of dp counterpart line
'credit_currency': debit, # opposite of dp counterpart line
})
supplier_line_id = self.pool.get('account.move.line').create(cr, uid, vals)
# post move
self.pool.get('account.move').post(cr, uid, [move_id])
# and reconcile down payment counterpart
self.pool.get('account.move.line').reconcile_partial(cr, uid, [el[0], dp_counterpart_id], type='manual')
# and reconcile invoice and supplier_line
to_reconcile = [supplier_line_id]
for line in inv.move_id.line_id:
if line.account_id.id == inv.account_id.id:
to_reconcile.append(line.id)
if not len(to_reconcile) > 1:
raise osv.except_osv(_('Error'), _('Did not achieve invoice reconciliation with down payment.'))
self.pool.get('account.move.line').reconcile_partial(cr, uid, to_reconcile)
# add amount of invoice down_payment line on purchase order to keep used amount
current_amount = self.pool.get('account.move.line').read(cr, uid, el[0], ['down_payment_amount']).get('down_payment_amount')
self.pool.get('account.move.line').write(cr, uid, [el[0]], {'down_payment_amount': current_amount + el[1]})
# add payment to result
res.append(dp_counterpart_id)
return res
def check_down_payments(self, cr, uid, ids, context=None):
"""
Verify that PO have down payments.
If not, check that no Down Payment in temp state exists in registers.
If yes, launch down payment creation and attach it to invoice.
"""
# Some verification
if not context:
context = {}
if isinstance(ids, (int, long)):
ids = [ids]
# Browse all invoice and check PO
for inv in self.browse(cr, uid, ids):
total_payments = 0.0
# Check that no register lines not hard posted are linked to these PO
st_lines = self.pool.get('account.bank.statement.line').search(cr, uid, [('state', 'in', ['draft', 'temp']), ('down_payment_id', 'in', [x.id for x in inv.purchase_ids])])
if st_lines:
raise osv.except_osv(_('Warning'), _('You cannot validate the invoice because some related down payments are not hard posted.'))
for po in inv.purchase_ids:
for dp in po.down_payment_ids:
if abs(dp.down_payment_amount) < abs(dp.amount_currency):
total_payments += (dp.amount_currency - dp.down_payment_amount)
if total_payments == 0.0:
continue
elif (inv.amount_total - total_payments) > 0.0:
# Attach a down payment to this invoice
self.create_down_payments(cr, uid, inv.id, total_payments)
elif (inv.amount_total - total_payments) <= 0.0:
# In this case, down payment permits to pay entirely invoice, that's why the down payment equals invoice total
self.create_down_payments(cr, uid, inv.id, inv.amount_total)
return True
def _direct_invoice_updated(self, cr, uid, ids, context=None):
"""
User has updated the direct invoice. The (parent) statement line needs to be updated, and then
the move lines deleted and re-created. Ticket utp917. Sheer madness.
"""
# get object handles
account_bank_statement_line = self.pool.get('account.bank.statement.line') #absl
direct_invoice = self.browse(cr, uid, ids, context=context)[0]
# get statement line id
absl = direct_invoice.register_line_ids[0]
if (direct_invoice.document_date != absl.document_date) or (direct_invoice.partner_id != absl.partner_id):
account_bank_statement_line.write(cr, uid, [absl.id], {'document_date': direct_invoice.document_date, \
'partner_id': direct_invoice.partner_id.id , \
'account_id': direct_invoice.account_id.id}, # UFTP-166: Saved also the account change to reg line
context=context)
if (direct_invoice.reference != absl.ref):
account_bank_statement_line.write(cr, uid, [absl.id], {'ref': direct_invoice.reference }, context=context)
# Delete moves
# existing seqnums are saved into context here. utp917
account_bank_statement_line.unlink_moves(cr, uid, [absl.id], context=context)
# Re-create moves and temp post them.
# account_bank_statement_line.write(cr, uid, [absl.id], {'state': 'draft'}, context=context)
account_bank_statement_line.button_temp_posting(cr, uid, [absl.id], context=context)
# remove seqnums from context
context.pop("seqnums",None)
# fix the reference UFTP-167
self.fix_aal_aml_reference(cr, uid, ids[0], context=context)
return True
def fix_aal_aml_reference(self, cr, uid, id, context=None):
# fix the reference UFTP-167
aml_obj = self.pool.get('account.move.line')
aal_obj = self.pool.get('account.analytic.line')
# 1. find the moves associated with the invoice - account_invoice.move_id
move_id = self.browse(cr, uid, id, context=context).move_id.id
# 2. get all the move_lines for that move_id with an account_move_line.invoice_line_id <> null
aml_ids = aml_obj.search(cr, uid, [('move_id', '=', move_id),('invoice_line_id','!=',False)], context=context)
move_lines = aml_obj.browse(cr, uid, aml_ids, context=context)
# 3. get the corresponding invoice_line
# 4. if the ref is not blank than update it
for move_line in move_lines:
ail = move_line.invoice_line_id
if ail.reference:
# must write to 'reference' to have 'ref' update: very confusing.
aml_obj.write(cr, uid, move_line.id, {'reference': ail.reference}, context=context)
# update analytic lines. move_id is actually move_line_id
aal_ids = aal_obj.search(cr, uid, [('move_id','=',move_line.id)], context=context)
aal_obj.write(cr, uid, aal_ids, {'reference': ail.reference}, context=context)
def action_open_invoice(self, cr, uid, ids, context=None, *args):
"""
Add down payment check after others verifications
"""
# Some verifications
if not context:
context = {}
if isinstance(ids, (int, long)):
ids = [ids]
# Browse invoice and all invoice lines to detect a non-valid line
self._check_analytic_distribution_state(cr, uid, ids)
# Default behaviour
res = super(account_invoice, self).action_open_invoice(cr, uid, ids, context)
to_check = []
for inv in self.read(cr, uid, ids, ['purchase_ids']):
# Create down payments for invoice that come from a purchase
if inv.get('purchase_ids', []):
to_check.append(inv.get('id'))
self.check_down_payments(cr, uid, to_check)
return res
def button_close_direct_invoice(self, cr, uid, ids, context=None):
"""
Check analytic distribution before closing pop-up
"""
if not context:
context = {}
if isinstance(ids, (int, long)):
ids = [ids]
self._check_analytic_distribution_state(cr, uid, ids, context)
self._direct_invoice_updated(cr, uid, ids, context)
if context.get('from_register', False):
return {'type': 'ir.actions.act_window_close'}
return True
account_invoice()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|