-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaccount_invoice.py
1646 lines (1473 loc) · 77.3 KB
/
account_invoice.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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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/>.
#
##############################################################################
import itertools
from lxml import etree
from openerp import models, fields, api, _
from openerp.exceptions import except_orm, Warning, RedirectWarning
from openerp.tools import float_compare
import openerp.addons.decimal_precision as dp
# mapping invoice type to journal type
TYPE2JOURNAL = {
'out_invoice': 'sale',
'in_invoice': 'purchase',
'out_refund': 'sale_refund',
'in_refund': 'purchase_refund',
}
# mapping invoice type to refund type
TYPE2REFUND = {
'out_invoice': 'out_refund', # Customer Invoice
'in_invoice': 'in_refund', # Supplier Invoice
'out_refund': 'out_invoice', # Customer Refund
'in_refund': 'in_invoice', # Supplier Refund
}
MAGIC_COLUMNS = ('id', 'create_uid', 'create_date', 'write_uid', 'write_date')
class account_invoice(models.Model):
_name = "account.invoice"
_inherit = ['mail.thread']
_description = "Invoice"
_order = "number desc, id desc"
_track = {
'type': {
},
'state': {
'account.mt_invoice_paid': lambda self, cr, uid, obj, ctx=None: obj.state == 'paid' and obj.type in ('out_invoice', 'out_refund'),
'account.mt_invoice_validated': lambda self, cr, uid, obj, ctx=None: obj.state == 'open' and obj.type in ('out_invoice', 'out_refund'),
},
}
@api.one
@api.depends('invoice_line.price_subtotal', 'tax_line.amount')
def _compute_amount(self):
self.amount_untaxed = sum(line.price_subtotal for line in self.invoice_line)
self.amount_tax = sum(line.amount for line in self.tax_line)
self.amount_total = self.amount_untaxed + self.amount_tax
@api.model
def _default_journal(self):
inv_type = self._context.get('type', 'out_invoice')
inv_types = inv_type if isinstance(inv_type, list) else [inv_type]
company_id = self._context.get('company_id', self.env.user.company_id.id)
domain = [
('type', 'in', filter(None, map(TYPE2JOURNAL.get, inv_types))),
('company_id', '=', company_id),
]
return self.env['account.journal'].search(domain, limit=1)
@api.model
def _default_currency(self):
journal = self._default_journal()
return journal.currency or journal.company_id.currency_id
@api.model
@api.returns('account.analytic.journal', lambda r: r.id)
def _get_journal_analytic(self, inv_type):
""" Return the analytic journal corresponding to the given invoice type. """
type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'}
journal_type = type2journal.get(inv_type, 'sale')
journal = self.env['account.analytic.journal'].search([('type', '=', journal_type)], limit=1)
if not journal:
raise except_orm(_('No Analytic Journal!'),
_("You must define an analytic journal of type '%s'!") % (journal_type,))
return journal[0]
@api.one
@api.depends('account_id', 'move_id.line_id.account_id', 'move_id.line_id.reconcile_id')
def _compute_reconciled(self):
self.reconciled = self.test_paid()
@api.model
def _get_reference_type(self):
return [('none', _('Free Reference'))]
@api.one
@api.depends(
'state', 'currency_id', 'invoice_line.price_subtotal',
'move_id.line_id.account_id.type',
'move_id.line_id.amount_residual',
# Fixes the fact that move_id.line_id.amount_residual, being not stored and old API, doesn't trigger recomputation
'move_id.line_id.reconcile_id',
'move_id.line_id.amount_residual_currency',
'move_id.line_id.currency_id',
'move_id.line_id.reconcile_partial_id.line_partial_ids.invoice.type',
)
# An invoice's residual amount is the sum of its unreconciled move lines and,
# for partially reconciled move lines, their residual amount divided by the
# number of times this reconciliation is used in an invoice (so we split
# the residual amount between all invoice)
def _compute_residual(self):
self.residual = 0.0
# Each partial reconciliation is considered only once for each invoice it appears into,
# and its residual amount is divided by this number of invoices
partial_reconciliations_done = []
for line in self.sudo().move_id.line_id:
if line.account_id.type not in ('receivable', 'payable'):
continue
if line.reconcile_partial_id and line.reconcile_partial_id.id in partial_reconciliations_done:
continue
# Get the correct line residual amount
if line.currency_id == self.currency_id:
line_amount = line.currency_id and line.amount_residual_currency or line.amount_residual
else:
from_currency = line.company_id.currency_id.with_context(date=line.date)
line_amount = from_currency.compute(line.amount_residual, self.currency_id)
# For partially reconciled lines, split the residual amount
if line.reconcile_partial_id:
partial_reconciliation_invoices = set()
for pline in line.reconcile_partial_id.line_partial_ids:
if pline.invoice and self.type == pline.invoice.type:
partial_reconciliation_invoices.update([pline.invoice.id])
line_amount = self.currency_id.round(line_amount / len(partial_reconciliation_invoices))
partial_reconciliations_done.append(line.reconcile_partial_id.id)
self.residual += line_amount
self.residual = max(self.residual, 0.0)
@api.one
@api.depends(
'move_id.line_id.account_id',
'move_id.line_id.reconcile_id.line_id',
'move_id.line_id.reconcile_partial_id.line_partial_ids',
)
def _compute_move_lines(self):
# Give Journal Items related to the payment reconciled to this invoice.
# Return partial and total payments related to the selected invoice.
self.move_lines = self.env['account.move.line']
if not self.move_id:
return
data_lines = self.move_id.line_id.filtered(lambda l: l.account_id == self.account_id)
partial_lines = self.env['account.move.line']
for data_line in data_lines:
if data_line.reconcile_id:
lines = data_line.reconcile_id.line_id
elif data_line.reconcile_partial_id:
lines = data_line.reconcile_partial_id.line_partial_ids
else:
lines = self.env['account.move.line']
partial_lines += data_line
self.move_lines = lines - partial_lines
@api.one
@api.depends(
'move_id.line_id.reconcile_id.line_id',
'move_id.line_id.reconcile_partial_id.line_partial_ids',
)
def _compute_payments(self):
partial_lines = lines = self.env['account.move.line']
for line in self.move_id.line_id:
if line.account_id != self.account_id:
continue
if line.reconcile_id:
lines |= line.reconcile_id.line_id
elif line.reconcile_partial_id:
lines |= line.reconcile_partial_id.line_partial_ids
partial_lines += line
self.payment_ids = (lines - partial_lines).sorted()
name = fields.Char(string='Reference/Description', index=True,
readonly=True, states={'draft': [('readonly', False)]})
origin = fields.Char(string='Source Document',
help="Reference of the document that produced this invoice.",
readonly=True, states={'draft': [('readonly', False)]})
supplier_invoice_number = fields.Char(string='Supplier Invoice Number',
help="The reference of this invoice as provided by the supplier.",
readonly=True, states={'draft': [('readonly', False)]})
type = fields.Selection([
('out_invoice','Customer Invoice'),
('in_invoice','Supplier Invoice'),
('out_refund','Customer Refund'),
('in_refund','Supplier Refund'),
], string='Type', readonly=True, index=True, change_default=True,
default=lambda self: self._context.get('type', 'out_invoice'),
track_visibility='always')
number = fields.Char(related='move_id.name', store=True, readonly=True, copy=False)
internal_number = fields.Char(string='Invoice Number', readonly=True,
default=False, copy=False,
help="Unique number of the invoice, computed automatically when the invoice is created.")
reference = fields.Char(string='Invoice Reference',
help="The partner reference of this invoice.")
reference_type = fields.Selection('_get_reference_type', string='Payment Reference',
required=True, readonly=True, states={'draft': [('readonly', False)]},
default='none')
comment = fields.Text('Additional Information')
state = fields.Selection([
('draft','Draft'),
('proforma','Pro-forma'),
('proforma2','Pro-forma'),
('open','Open'),
('paid','Paid'),
('cancel','Cancelled'),
], string='Status', index=True, readonly=True, default='draft',
track_visibility='onchange', copy=False,
help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n"
" * The 'Pro-forma' when invoice is in Pro-forma status,invoice does not have an invoice number.\n"
" * The 'Open' status is used when user create invoice,a invoice number is generated.Its in open status till user does not pay invoice.\n"
" * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n"
" * The 'Cancelled' status is used when user cancel invoice.")
sent = fields.Boolean(readonly=True, default=False, copy=False,
help="It indicates that the invoice has been sent.")
date_invoice = fields.Date(string='Invoice Date',
readonly=True, states={'draft': [('readonly', False)]}, index=True,
help="Keep empty to use the current date", copy=False)
date_due = fields.Date(string='Due Date',
readonly=True, states={'draft': [('readonly', False)]}, index=True, copy=False,
help="If you use payment terms, the due date will be computed automatically at the generation "
"of accounting entries. The payment term may compute several due dates, for example 50% "
"now and 50% in one month, but if you want to force a due date, make sure that the payment "
"term is not set on the invoice. If you keep the payment term and the due date empty, it "
"means direct payment.")
partner_id = fields.Many2one('res.partner', string='Partner', change_default=True,
required=True, readonly=True, states={'draft': [('readonly', False)]},
track_visibility='always')
payment_term = fields.Many2one('account.payment.term', string='Payment Terms',
readonly=True, states={'draft': [('readonly', False)]},
help="If you use payment terms, the due date will be computed automatically at the generation "
"of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "
"The payment term may compute several due dates, for example 50% now, 50% in one month.")
period_id = fields.Many2one('account.period', string='Force Period',
domain=[('state', '!=', 'done')], copy=False,
help="Keep empty to use the period of the validation(invoice) date.",
readonly=True, states={'draft': [('readonly', False)]})
account_id = fields.Many2one('account.account', string='Account',
required=True, readonly=True, states={'draft': [('readonly', False)]},
help="The partner account used for this invoice.")
invoice_line = fields.One2many('account.invoice.line', 'invoice_id', string='Invoice Lines',
readonly=True, states={'draft': [('readonly', False)]}, copy=True)
tax_line = fields.One2many('account.invoice.tax', 'invoice_id', string='Tax Lines',
readonly=True, states={'draft': [('readonly', False)]}, copy=True)
move_id = fields.Many2one('account.move', string='Journal Entry',
readonly=True, index=True, ondelete='restrict', copy=False,
help="Link to the automatically generated Journal Items.")
amount_untaxed = fields.Float(string='Subtotal', digits=dp.get_precision('Account'),
store=True, readonly=True, compute='_compute_amount', track_visibility='always')
amount_tax = fields.Float(string='Tax', digits=dp.get_precision('Account'),
store=True, readonly=True, compute='_compute_amount')
amount_total = fields.Float(string='Total', digits=dp.get_precision('Account'),
store=True, readonly=True, compute='_compute_amount')
currency_id = fields.Many2one('res.currency', string='Currency',
required=True, readonly=True, states={'draft': [('readonly', False)]},
default=_default_currency, track_visibility='always')
journal_id = fields.Many2one('account.journal', string='Journal',
required=True, readonly=True, states={'draft': [('readonly', False)]},
default=_default_journal,
domain="[('type', 'in', {'out_invoice': ['sale'], 'out_refund': ['sale_refund'], 'in_refund': ['purchase_refund'], 'in_invoice': ['purchase']}.get(type, [])), ('company_id', '=', company_id)]")
company_id = fields.Many2one('res.company', string='Company', change_default=True,
required=True, readonly=True, states={'draft': [('readonly', False)]},
default=lambda self: self.env['res.company']._company_default_get('account.invoice'))
check_total = fields.Float(string='Verification Total', digits=dp.get_precision('Account'),
readonly=True, states={'draft': [('readonly', False)]}, default=0.0)
reconciled = fields.Boolean(string='Paid/Reconciled',
store=True, readonly=True, compute='_compute_reconciled',
help="It indicates that the invoice has been paid and the journal entry of the invoice has been reconciled with one or several journal entries of payment.")
partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account',
help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Supplier Refund, otherwise a Partner bank account number.',
readonly=True, states={'draft': [('readonly', False)]})
move_lines = fields.Many2many('account.move.line', string='Entry Lines',
compute='_compute_move_lines')
residual = fields.Float(string='Balance', digits=dp.get_precision('Account'),
compute='_compute_residual', store=True,
help="Remaining amount due.")
payment_ids = fields.Many2many('account.move.line', string='Payments',
compute='_compute_payments')
move_name = fields.Char(string='Journal Entry', readonly=True,
states={'draft': [('readonly', False)]}, copy=False)
user_id = fields.Many2one('res.users', string='Salesperson', track_visibility='onchange',
readonly=True, states={'draft': [('readonly', False)]},
default=lambda self: self.env.user)
fiscal_position = fields.Many2one('account.fiscal.position', string='Fiscal Position',
readonly=True, states={'draft': [('readonly', False)]})
commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity',
related='partner_id.commercial_partner_id', store=True, readonly=True,
help="The commercial entity that will be used on Journal Entries for this invoice")
_sql_constraints = [
('number_uniq', 'unique(number, company_id, journal_id, type)',
'Invoice Number must be unique per Company!'),
]
@api.model
def fields_view_get(self, view_id=None, view_type=False, toolbar=False, submenu=False):
context = self._context
def get_view_id(xid, name):
try:
return self.env['ir.model.data'].xmlid_to_res_id('account.' + xid, raise_if_not_found=True)
except ValueError:
try:
return self.env['ir.ui.view'].search([('name', '=', name)], limit=1).id
except Exception:
return False # view not found
if context.get('active_model') == 'res.partner' and context.get('active_ids'):
partner = self.env['res.partner'].browse(context['active_ids'])[0]
if not view_type:
view_id = get_view_id('invoice_tree', 'account.invoice.tree')
view_type = 'tree'
elif view_type == 'form':
if partner.supplier and not partner.customer:
view_id = get_view_id('invoice_supplier_form', 'account.invoice.supplier.form')
elif partner.customer and not partner.supplier:
view_id = get_view_id('invoice_form', 'account.invoice.form')
res = super(account_invoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
# adapt selection of field journal_id
for field in res['fields']:
if field == 'journal_id' and type:
journal_select = self.env['account.journal']._name_search('', [('type', '=', type)], name_get_uid=1)
res['fields'][field]['selection'] = journal_select
doc = etree.XML(res['arch'])
if context.get('type'):
for node in doc.xpath("//field[@name='partner_bank_id']"):
if context['type'] == 'in_refund':
node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
elif context['type'] == 'out_refund':
node.set('domain', "[('partner_id', '=', partner_id)]")
if view_type == 'search':
if context.get('type') in ('out_invoice', 'out_refund'):
for node in doc.xpath("//group[@name='extended filter']"):
doc.remove(node)
if view_type == 'tree':
partner_string = _('Customer')
if context.get('type') in ('in_invoice', 'in_refund'):
partner_string = _('Supplier')
for node in doc.xpath("//field[@name='reference']"):
node.set('invisible', '0')
for node in doc.xpath("//field[@name='partner_id']"):
node.set('string', partner_string)
res['arch'] = etree.tostring(doc)
return res
@api.multi
def invoice_print(self):
""" Print the invoice and mark it as sent, so that we can see more
easily the next step of the workflow
"""
assert len(self) == 1, 'This option should only be used for a single id at a time.'
self.sent = True
return self.env['report'].get_action(self, 'account.report_invoice')
@api.multi
def action_invoice_sent(self):
""" Open a window to compose an email, with the edi invoice template
message loaded by default
"""
assert len(self) == 1, 'This option should only be used for a single id at a time.'
template = self.env.ref('account.email_template_edi_invoice', False)
compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
ctx = dict(
default_model='account.invoice',
default_res_id=self.id,
default_use_template=bool(template),
default_template_id=template.id,
default_composition_mode='comment',
mark_invoice_as_sent=True,
)
return {
'name': _('Compose Email'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(compose_form.id, 'form')],
'view_id': compose_form.id,
'target': 'new',
'context': ctx,
}
@api.multi
def confirm_paid(self):
return self.write({'state': 'paid'})
@api.multi
def unlink(self):
for invoice in self:
if invoice.state not in ('draft', 'cancel'):
raise Warning(_('You cannot delete an invoice which is not draft or cancelled. You should refund it instead.'))
elif invoice.internal_number:
raise Warning(_('You cannot delete an invoice after it has been validated (and received a number). You can set it back to "Draft" state and modify its content, then re-confirm it.'))
return super(account_invoice, self).unlink()
@api.multi
def onchange_partner_id(self, type, partner_id, date_invoice=False,
payment_term=False, partner_bank_id=False, company_id=False):
account_id = False
payment_term_id = False
fiscal_position = False
bank_id = False
if partner_id:
p = self.env['res.partner'].browse(partner_id)
rec_account = p.property_account_receivable
pay_account = p.property_account_payable
if company_id:
if p.property_account_receivable.company_id and \
p.property_account_receivable.company_id.id != company_id and \
p.property_account_payable.company_id and \
p.property_account_payable.company_id.id != company_id:
prop = self.env['ir.property']
rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
res_dom = [('res_id', '=', 'res.partner,%s' % partner_id)]
rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
rec_account = rec_prop.get_by_record(rec_prop)
pay_account = pay_prop.get_by_record(pay_prop)
if not rec_account and not pay_account:
action = self.env.ref('account.action_account_config')
msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
if type in ('out_invoice', 'out_refund'):
account_id = rec_account.id
payment_term_id = p.property_payment_term.id
else:
account_id = pay_account.id
payment_term_id = p.property_supplier_payment_term.id
fiscal_position = p.property_account_position.id
bank_id = p.bank_ids and p.bank_ids[0].id or False
result = {'value': {
'account_id': account_id,
'payment_term': payment_term_id,
'fiscal_position': fiscal_position,
}}
if type in ('in_invoice', 'in_refund'):
result['value']['partner_bank_id'] = bank_id
if payment_term != payment_term_id:
if payment_term_id:
to_update = self.onchange_payment_term_date_invoice(payment_term_id, date_invoice)
result['value'].update(to_update.get('value', {}))
else:
result['value']['date_due'] = False
if partner_bank_id != bank_id:
to_update = self.onchange_partner_bank(bank_id)
result['value'].update(to_update.get('value', {}))
return result
@api.multi
def onchange_journal_id(self, journal_id=False):
if journal_id:
journal = self.env['account.journal'].browse(journal_id)
return {
'value': {
'currency_id': journal.currency.id or journal.company_id.currency_id.id,
'company_id': journal.company_id.id,
}
}
return {}
@api.multi
def onchange_payment_term_date_invoice(self, payment_term_id, date_invoice):
if not date_invoice:
date_invoice = fields.Date.context_today(self)
if not payment_term_id:
# To make sure the invoice due date should contain due date which is
# entered by user when there is no payment term defined
return {'value': {'date_due': self.date_due or date_invoice}}
pterm = self.env['account.payment.term'].browse(payment_term_id)
pterm_list = pterm.compute(value=1, date_ref=date_invoice)[0]
if pterm_list:
return {'value': {'date_due': max(line[0] for line in pterm_list)}}
else:
raise except_orm(_('Insufficient Data!'),
_('The payment term of supplier does not have a payment term line.'))
@api.multi
def onchange_invoice_line(self, lines):
return {}
@api.multi
def onchange_partner_bank(self, partner_bank_id=False):
return {'value': {}}
@api.multi
def onchange_company_id(self, company_id, part_id, type, invoice_line, currency_id):
# TODO: add the missing context parameter when forward-porting in trunk
# so we can remove this hack!
self = self.with_context(self.env['res.users'].context_get())
values = {}
domain = {}
if company_id and part_id and type:
p = self.env['res.partner'].browse(part_id)
if p.property_account_payable and p.property_account_receivable and \
p.property_account_payable.company_id.id != company_id and \
p.property_account_receivable.company_id.id != company_id:
prop = self.env['ir.property']
rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)]
pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)]
res_dom = [('res_id', '=', 'res.partner,%s' % part_id)]
rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom)
pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom)
rec_account = rec_prop.get_by_record(rec_prop)
pay_account = pay_prop.get_by_record(pay_prop)
if not rec_account and not pay_account:
action = self.env.ref('account.action_account_config')
msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
if type in ('out_invoice', 'out_refund'):
acc_id = rec_account.id
else:
acc_id = pay_account.id
values= {'account_id': acc_id}
if self:
if company_id:
for line in self.invoice_line:
if not line.account_id:
continue
if line.account_id.company_id.id == company_id:
continue
accounts = self.env['account.account'].search([('name', '=', line.account_id.name), ('company_id', '=', company_id)])
if not accounts:
action = self.env.ref('account.action_account_config')
msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.')
raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
line.write({'account_id': accounts[-1].id})
else:
for line_cmd in invoice_line or []:
if len(line_cmd) >= 3 and isinstance(line_cmd[2], dict):
line = self.env['account.account'].browse(line_cmd[2]['account_id'])
if line.company_id.id != company_id:
raise except_orm(
_('Configuration Error!'),
_("Invoice line account's company and invoice's company does not match.")
)
if company_id and type:
journal_type = TYPE2JOURNAL[type]
journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)])
if journals:
values['journal_id'] = journals[0].id
journal_defaults = self.env['ir.values'].get_defaults_dict('account.invoice', 'type=%s' % type)
if 'journal_id' in journal_defaults:
values['journal_id'] = journal_defaults['journal_id']
if not values.get('journal_id'):
field_desc = journals.fields_get(['type'])
type_label = next(t for t, label in field_desc['type']['selection'] if t == journal_type)
action = self.env.ref('account.action_account_journal_form')
msg = _('Cannot find any account journal of type "%s" for this company, You should create one.\n Please go to Journal Configuration') % type_label
raise RedirectWarning(msg, action.id, _('Go to the configuration panel'))
domain = {'journal_id': [('id', 'in', journals.ids)]}
return {'value': values, 'domain': domain}
@api.multi
def action_cancel_draft(self):
# go from canceled state to draft state
self.write({'state': 'draft'})
self.delete_workflow()
self.create_workflow()
return True
@api.one
@api.returns('ir.ui.view')
def get_formview_id(self):
""" Update form view id of action to open the invoice """
if self.type == 'in_invoice':
return self.env.ref('account.invoice_supplier_form')
else:
return self.env.ref('account.invoice_form')
@api.multi
def move_line_id_payment_get(self):
# return the move line ids with the same account as the invoice self
if not self.id:
return []
query = """ SELECT l.id
FROM account_move_line l, account_invoice i
WHERE i.id = %s AND l.move_id = i.move_id AND l.account_id = i.account_id
"""
self._cr.execute(query, (self.id,))
return [row[0] for row in self._cr.fetchall()]
@api.multi
def test_paid(self):
# check whether all corresponding account move lines are reconciled
line_ids = self.move_line_id_payment_get()
if not line_ids:
return False
query = "SELECT reconcile_id FROM account_move_line WHERE id IN %s"
self._cr.execute(query, (tuple(line_ids),))
return all(row[0] for row in self._cr.fetchall())
@api.multi
def button_reset_taxes(self):
account_invoice_tax = self.env['account.invoice.tax']
ctx = dict(self._context)
for invoice in self:
self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,))
self.invalidate_cache()
partner = invoice.partner_id
if partner.lang:
ctx['lang'] = partner.lang
for taxe in account_invoice_tax.compute(invoice.with_context(ctx)).values():
account_invoice_tax.create(taxe)
# dummy write on self to trigger recomputations
return self.with_context(ctx).write({'invoice_line': []})
@api.multi
def button_compute(self, set_total=False):
self.button_reset_taxes()
for invoice in self:
if set_total:
invoice.check_total = invoice.amount_total
return True
@api.multi
def _get_analytic_lines(self):
""" Return a list of dict for creating analytic lines for self[0] """
company_currency = self.company_id.currency_id
sign = 1 if self.type in ('out_invoice', 'in_refund') else -1
iml = self.env['account.invoice.line'].move_line_get(self.id)
for il in iml:
if il['account_analytic_id']:
if self.type in ('in_invoice', 'in_refund'):
ref = self.reference
else:
ref = self.number
if not self.journal_id.analytic_journal_id:
raise except_orm(_('No Analytic Journal!'),
_("You have to define an analytic journal on the '%s' journal!") % (self.journal_id.name,))
currency = self.currency_id.with_context(date=self.date_invoice)
il['analytic_lines'] = [(0,0, {
'name': il['name'],
'date': self.date_invoice,
'account_id': il['account_analytic_id'],
'unit_amount': il['quantity'],
'amount': currency.compute(il['price'], company_currency) * sign,
'product_id': il['product_id'],
'product_uom_id': il['uos_id'],
'general_account_id': il['account_id'],
'journal_id': self.journal_id.analytic_journal_id.id,
'ref': ref,
})]
return iml
@api.multi
def action_date_assign(self):
for inv in self:
res = inv.onchange_payment_term_date_invoice(inv.payment_term.id, inv.date_invoice)
if res and res.get('value'):
inv.write(res['value'])
return True
@api.multi
def finalize_invoice_move_lines(self, move_lines):
""" finalize_invoice_move_lines(move_lines) -> move_lines
Hook method to be overridden in additional modules to verify and
possibly alter the move lines to be created by an invoice, for
special cases.
:param move_lines: list of dictionaries with the account.move.lines (as for create())
:return: the (possibly updated) final move_lines to create for this invoice
"""
return move_lines
@api.multi
def check_tax_lines(self, compute_taxes):
account_invoice_tax = self.env['account.invoice.tax']
company_currency = self.company_id.currency_id
if not self.tax_line:
for tax in compute_taxes.values():
account_invoice_tax.create(tax)
else:
tax_key = []
precision = self.env['decimal.precision'].precision_get('Account')
for tax in self.tax_line:
if tax.manual:
continue
key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
tax_key.append(key)
if key not in compute_taxes:
raise except_orm(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !'))
base = compute_taxes[key]['base']
if float_compare(abs(base - tax.base), company_currency.rounding, precision_digits=precision) == 1:
raise except_orm(_('Warning!'), _('Tax base different!\nClick on compute to update the tax base.'))
for key in compute_taxes:
if key not in tax_key:
raise except_orm(_('Warning!'), _('Taxes are missing!\nClick on compute button.'))
@api.multi
def compute_invoice_totals(self, company_currency, ref, invoice_move_lines):
total = 0
total_currency = 0
for line in invoice_move_lines:
if self.currency_id != company_currency:
currency = self.currency_id.with_context(date=self.date_invoice or fields.Date.context_today(self))
line['currency_id'] = currency.id
line['amount_currency'] = line['price']
line['price'] = currency.compute(line['price'], company_currency)
else:
line['currency_id'] = False
line['amount_currency'] = False
line['ref'] = ref
if self.type in ('out_invoice','in_refund'):
total += line['price']
total_currency += line['amount_currency'] or line['price']
line['price'] = - line['price']
else:
total -= line['price']
total_currency -= line['amount_currency'] or line['price']
return total, total_currency, invoice_move_lines
def inv_line_characteristic_hashcode(self, invoice_line):
"""Overridable hashcode generation for invoice lines. Lines having the same hashcode
will be grouped together if the journal has the 'group line' option. Of course a module
can add fields to invoice lines that would need to be tested too before merging lines
or not."""
return "%s-%s-%s-%s-%s" % (
invoice_line['account_id'],
invoice_line.get('tax_code_id', 'False'),
invoice_line.get('product_id', 'False'),
invoice_line.get('analytic_account_id', 'False'),
invoice_line.get('date_maturity', 'False'),
)
def group_lines(self, iml, line):
"""Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
if self.journal_id.group_invoice_lines:
line2 = {}
for x, y, l in line:
tmp = self.inv_line_characteristic_hashcode(l)
if tmp in line2:
am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
line2[tmp]['debit'] = (am > 0) and am or 0.0
line2[tmp]['credit'] = (am < 0) and -am or 0.0
line2[tmp]['tax_amount'] += l['tax_amount']
line2[tmp]['analytic_lines'] += l['analytic_lines']
else:
line2[tmp] = l
line = []
for key, val in line2.items():
line.append((0,0,val))
return line
@api.multi
def action_move_create(self):
""" Creates invoice related analytics and financial move lines """
account_invoice_tax = self.env['account.invoice.tax']
account_move = self.env['account.move']
for inv in self:
if not inv.journal_id.sequence_id:
raise except_orm(_('Error!'), _('Please define sequence on the journal related to this invoice.'))
if not inv.invoice_line:
raise except_orm(_('No Invoice Lines!'), _('Please create some invoice lines.'))
if inv.move_id:
continue
ctx = dict(self._context, lang=inv.partner_id.lang)
if not inv.date_invoice:
inv.with_context(ctx).write({'date_invoice': fields.Date.context_today(self)})
date_invoice = inv.date_invoice
company_currency = inv.company_id.currency_id
# create the analytical lines, one move line per invoice line
iml = inv._get_analytic_lines()
# check if taxes are all computed
compute_taxes = account_invoice_tax.compute(inv)
inv.check_tax_lines(compute_taxes)
# I disabled the check_total feature
if self.env['res.users'].has_group('account.group_supplier_inv_check_total'):
if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding / 2.0):
raise except_orm(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.'))
if inv.payment_term:
total_fixed = total_percent = 0
for line in inv.payment_term.line_ids:
if line.value == 'fixed':
total_fixed += line.value_amount
if line.value == 'procent':
total_percent += line.value_amount
total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
if (total_fixed + total_percent) > 100:
raise except_orm(_('Error!'), _("Cannot create the invoice.\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. In order to avoid rounding issues, the latest line of your payment term must be of type 'balance'."))
# one move line per tax line
iml += account_invoice_tax.move_line_get(inv.id)
if inv.type in ('in_invoice', 'in_refund'):
ref = inv.reference
else:
ref = inv.number
diff_currency = inv.currency_id != company_currency
# create one move line for the total and possibly adjust the other lines amount
total, total_currency, iml = inv.with_context(ctx).compute_invoice_totals(company_currency, ref, iml)
name = inv.name or inv.supplier_invoice_number or '/'
totlines = []
if inv.payment_term:
totlines = inv.with_context(ctx).payment_term.compute(total, date_invoice)[0]
if totlines:
res_amount_currency = total_currency
ctx['date'] = date_invoice
for i, t in enumerate(totlines):
if inv.currency_id != company_currency:
amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
else:
amount_currency = False
# last line: add the diff
res_amount_currency -= amount_currency or 0
if i + 1 == len(totlines):
amount_currency += res_amount_currency
iml.append({
'type': 'dest',
'name': name,
'price': t[1],
'account_id': inv.account_id.id,
'date_maturity': t[0],
'amount_currency': diff_currency and amount_currency,
'currency_id': diff_currency and inv.currency_id.id,
'ref': ref,
})
else:
iml.append({
'type': 'dest',
'name': name,
'price': total,
'account_id': inv.account_id.id,
'date_maturity': inv.date_due,
'amount_currency': diff_currency and total_currency,
'currency_id': diff_currency and inv.currency_id.id,
'ref': ref
})
date = date_invoice
part = self.env['res.partner']._find_accounting_partner(inv.partner_id)
line = [(0, 0, self.line_get_convert(l, part.id, date)) for l in iml]
line = inv.group_lines(iml, line)
journal = inv.journal_id.with_context(ctx)
if journal.centralisation:
raise except_orm(_('User Error!'),
_('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.'))
line = inv.finalize_invoice_move_lines(line)
move_vals = {
'ref': inv.reference or inv.name,
'line_id': line,
'journal_id': journal.id,
'date': inv.date_invoice,
'narration': inv.comment,
'company_id': inv.company_id.id,
}
ctx['company_id'] = inv.company_id.id
period = inv.period_id
if not period:
period = period.with_context(ctx).find(date_invoice)[:1]
if period:
move_vals['period_id'] = period.id
for i in line:
i[2]['period_id'] = period.id
ctx['invoice'] = inv
move = account_move.with_context(ctx).create(move_vals)
# make the invoice point to that move
vals = {
'move_id': move.id,
'period_id': period.id,
'move_name': move.name,
}
inv.with_context(ctx).write(vals)
# Pass invoice in context in method post: used if you want to get the same
# account move reference when creating the same invoice after a cancelled one:
move.post()
self._log_event()
return True
@api.multi
def invoice_validate(self):
return self.write({'state': 'open'})
@api.model
def line_get_convert(self, line, part, date):
return {
'date_maturity': line.get('date_maturity', False),
'partner_id': part,
'name': line['name'][:64],
'date': date,
'debit': line['price']>0 and line['price'],
'credit': line['price']<0 and -line['price'],
'account_id': line['account_id'],
'analytic_lines': line.get('analytic_lines', []),
'amount_currency': line['price']>0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)),
'currency_id': line.get('currency_id', False),
'tax_code_id': line.get('tax_code_id', False),
'tax_amount': line.get('tax_amount', False),
'ref': line.get('ref', False),
'quantity': line.get('quantity',1.00),
'product_id': line.get('product_id', False),
'product_uom_id': line.get('uos_id', False),
'analytic_account_id': line.get('account_analytic_id', False),
}
@api.multi
def action_number(self):
#TODO: not correct fix but required a fresh values before reading it.
self.write({})
for inv in self:
self.write({'internal_number': inv.number})
if inv.type in ('in_invoice', 'in_refund'):
if not inv.reference:
ref = inv.number
else:
ref = inv.reference
else:
ref = inv.number
self._cr.execute(""" UPDATE account_move SET ref=%s
WHERE id=%s AND (ref IS NULL OR ref = '')""",
(ref, inv.move_id.id))
self._cr.execute(""" UPDATE account_move_line SET ref=%s
WHERE move_id=%s AND (ref IS NULL OR ref = '')""",
(ref, inv.move_id.id))
self._cr.execute(""" UPDATE account_analytic_line SET ref=%s
FROM account_move_line
WHERE account_move_line.move_id = %s AND
account_analytic_line.move_id = account_move_line.id""",
(ref, inv.move_id.id))
self.invalidate_cache()
return True
@api.multi
def action_cancel(self):
moves = self.env['account.move']
for inv in self:
if inv.move_id:
moves += inv.move_id
if inv.payment_ids:
for move_line in inv.payment_ids:
if move_line.reconcile_partial_id.line_partial_ids:
raise except_orm(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
# First, set the invoices as cancelled and detach the move ids
self.write({'state': 'cancel', 'move_id': False})
if moves: