1
# -*- encoding: utf-8 -*-
2
##############################################################################
4
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (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 General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
##############################################################################
23
This module shows resemblance to both account_bankimport/bankimport.py,
24
account/account_bank_statement.py and account_payment(_export). All hail to
25
the makers. account_bankimport is only referenced for their ideas and the
26
framework of the filters, which they in their turn seem to have derived
29
Modifications are extensive:
31
1. In relation to account/account_bank_statement.py:
32
account.bank.statement is effectively stripped from its account.period
33
association, while account.bank.statement.line is extended with the same
34
association, thereby reflecting real world usage of bank.statement as a
35
list of bank transactions and bank.statement.line as a bank transaction.
37
2. In relation to account/account_bankimport:
38
All filter objects and extensions to res.company are removed. Instead a
39
flexible auto-loading and auto-browsing plugin structure is created,
40
whereby business logic and encoding logic are strictly separated.
41
Both parsers and business logic are rewritten from scratch.
43
The association of account.journal with res.company is replaced by an
44
association of account.journal with res.partner.bank, thereby allowing
45
multiple bank accounts per company and one journal per bank account.
47
The imported bank statement file does not result in a single 'bank
48
statement', but in a list of bank statements by definition of whatever the
49
bank sees as a statement. Every imported bank statement contains at least
50
one bank transaction, which is a modded account.bank.statement.line.
52
3. In relation to account_payment:
53
An additional state was inserted between 'open' and 'done', to reflect a
54
exported bank orders file which was not reported back through statements.
55
The import of statements matches the payments and reconciles them when
56
needed, flagging them 'done'. When no export wizards are found, the
57
default behavior is to flag the orders as 'sent', not as 'done'.
60
from osv import osv, fields
61
from tools.translate import _
63
class account_banking_account_settings(osv.osv):
64
'''Default Journal for Bank Account'''
65
_name = 'account.banking.account.settings'
66
_description = __doc__
68
'company_id': fields.many2one('res.company', 'Company', select=True,
70
'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
71
select=True, required=True),
72
'journal_id': fields.many2one('account.journal', 'Journal',
74
'default_credit_account_id': fields.many2one(
75
'account.account', 'Default credit account', select=True,
76
help=('The account to use when an unexpected payment was signaled. '
77
'This can happen when a direct debit payment is cancelled '
78
'by a customer, or when no matching payment can be found. '
79
' Mind that you can correct movements before confirming them.'
83
'default_debit_account_id': fields.many2one(
84
'account.account', 'Default debit account',
85
select=True, required=True,
86
help=('The account to use when an unexpected payment is received. '
87
'This can be needed when a customer pays in advance or when '
88
'no matching invoice can be found. Mind that you can correct '
89
'movements before confirming them.'
94
def _default_company(self, cursor, uid, context={}):
95
user = self.pool.get('res.users').browse(cursor, uid, uid, context=context)
97
return user.company_id.id
98
return self.pool.get('res.company').search(cursor, uid,
99
[('parent_id', '=', False)]
103
'company_id': _default_company,
105
account_banking_account_settings()
107
class account_banking_imported_file(osv.osv):
108
'''Imported Bank Statements File'''
109
_name = 'account.banking.imported.file'
110
_description = __doc__
112
'company_id': fields.many2one('res.company', 'Company',
113
select=True, readonly=True
115
'date': fields.datetime('Import Date', readonly=False, select=True),
116
'format': fields.char('File Format', size=20, readonly=False),
117
'file': fields.binary('Raw Data', readonly=False),
118
'log': fields.text('Import Log', readonly=False),
119
'user_id': fields.many2one('res.users', 'Responsible User',
120
readonly=False, select=True
122
'state': fields.selection(
123
[('unfinished', 'Unfinished'),
125
('ready', 'Finished'),
126
], 'State', select=True, readonly=True
128
'statement_ids': fields.one2many('account.bank.statement',
129
'banking_id', 'Statements',
134
'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
135
'user_id': lambda self, cursor, uid, context: uid,
137
account_banking_imported_file()
139
class account_bank_statement(osv.osv):
141
Extensions from account_bank_statement:
142
1. Removed period_id (transformed to optional boolean) - as it is no
144
2. Extended 'button_confirm' trigger to cope with the period per
145
statement_line situation.
146
3. Added optional relation with imported statements file
148
_inherit = 'account.bank.statement'
150
'period_id': fields.many2one('account.period', 'Period',
151
required=False, readonly=True),
152
'banking_id': fields.many2one('account.banking.imported.file',
153
'Imported File', readonly=True,
157
'period_id': lambda *a: False,
160
def _get_period(self, cursor, uid, date, context={}):
162
Find matching period for date, not meant for _defaults.
164
period_obj = self.pool.get('account.period')
165
periods = period_obj.find(cursor, uid, dt=date, context=context)
166
return periods and periods[0] or False
168
def button_confirm(self, cursor, uid, ids, context=None):
169
# This is largely a copy of the original code in account
170
# As there is no valid inheritance mechanism for large actions, this
171
# is the only option to add functionality to existing actions.
172
# WARNING: when the original code changes, this trigger has to be
175
res_currency_obj = self.pool.get('res.currency')
176
res_users_obj = self.pool.get('res.users')
177
account_move_obj = self.pool.get('account.move')
178
account_move_line_obj = self.pool.get('account.move.line')
179
account_bank_statement_line_obj = \
180
self.pool.get('account.bank.statement.line')
182
company_currency_id = res_users_obj.browse(cursor, uid, uid,
183
context=context).company_id.currency_id.id
185
for st in self.browse(cursor, uid, ids, context):
186
if not st.state=='draft':
188
end_bal = st.balance_end or 0.0
189
if not (abs(end_bal - st.balance_end_real) < 0.0001):
190
raise osv.except_osv(_('Error !'),
191
_('The statement balance is incorrect !\n') +
192
_('The expected balance (%.2f) is different '
193
'than the computed one. (%.2f)') % (
194
st.balance_end_real, st.balance_end
196
if (not st.journal_id.default_credit_account_id) \
197
or (not st.journal_id.default_debit_account_id):
198
raise osv.except_osv(_('Configration Error !'),
199
_('Please verify that an account is defined in the journal.'))
201
for line in st.move_line_ids:
202
if line.state != 'valid':
203
raise osv.except_osv(_('Error !'),
204
_('The account entries lines are not in valid state.'))
206
for move in st.line_ids:
207
period_id = self._get_period(cursor, uid, move.date, context=context)
208
move_id = account_move_obj.create(cursor, uid, {
209
'journal_id': st.journal_id.id,
210
'period_id': period_id,
212
account_bank_statement_line_obj.write(cursor, uid, [move.id], {
213
'move_ids': [(4, move_id, False)]
220
account_id = st.journal_id.default_credit_account_id.id
222
account_id = st.journal_id.default_debit_account_id.id
223
acc_cur = ((move.amount<=0) and st.journal_id.default_debit_account_id) \
225
amount = res_currency_obj.compute(cursor, uid, st.currency.id,
226
company_currency_id, move.amount, context=context,
228
if move.reconcile_id and move.reconcile_id.line_new_ids:
229
for newline in move.reconcile_id.line_new_ids:
230
amount += newline.amount
237
'partner_id': ((move.partner_id) and move.partner_id.id) or False,
238
'account_id': (move.account_id) and move.account_id.id,
239
'credit': ((amount>0) and amount) or 0.0,
240
'debit': ((amount<0) and -amount) or 0.0,
241
'statement_id': st.id,
242
'journal_id': st.journal_id.id,
243
'period_id': period_id,
244
'currency_id': st.currency.id,
247
amount = res_currency_obj.compute(cursor, uid, st.currency.id,
248
company_currency_id, move.amount, context=context,
251
if move.account_id and move.account_id.currency_id:
252
val['currency_id'] = move.account_id.currency_id.id
253
if company_currency_id==move.account_id.currency_id.id:
254
amount_cur = move.amount
256
amount_cur = res_currency_obj.compute(cursor, uid, company_currency_id,
257
move.account_id.currency_id.id, amount, context=context,
259
val['amount_currency'] = amount_cur
261
torec.append(account_move_line_obj.create(cursor, uid, val , context=context))
263
if move.reconcile_id and move.reconcile_id.line_new_ids:
264
for newline in move.reconcile_id.line_new_ids:
265
account_move_line_obj.create(cursor, uid, {
266
'name': newline.name or move.name,
270
'partner_id': ((move.partner_id) and move.partner_id.id) or False,
271
'account_id': (newline.account_id) and newline.account_id.id,
272
'debit': newline.amount>0 and newline.amount or 0.0,
273
'credit': newline.amount<0 and -newline.amount or 0.0,
274
'statement_id': st.id,
275
'journal_id': st.journal_id.id,
276
'period_id': period_id,
279
# Fill the secondary amount/currency
280
# if currency is not the same than the company
281
amount_currency = False
283
if st.currency.id <> company_currency_id:
284
amount_currency = move.amount
285
currency_id = st.currency.id
287
account_move_line_obj.create(cursor, uid, {
292
'partner_id': ((move.partner_id) and move.partner_id.id) or False,
293
'account_id': account_id,
294
'credit': ((amount < 0) and -amount) or 0.0,
295
'debit': ((amount > 0) and amount) or 0.0,
296
'statement_id': st.id,
297
'journal_id': st.journal_id.id,
298
'period_id': period_id,
299
'amount_currency': amount_currency,
300
'currency_id': currency_id,
303
for line in account_move_line_obj.browse(cursor, uid, [x.id for x in
304
account_move_obj.browse(cursor, uid, move_id, context=context).line_id
306
if line.state != 'valid':
307
raise osv.except_osv(
309
_('Account move line "%s" is not valid')
313
if move.reconcile_id and move.reconcile_id.line_ids:
314
torec += map(lambda x: x.id, move.reconcile_id.line_ids)
316
if abs(move.reconcile_amount-move.amount)<0.0001:
317
account_move_line_obj.reconcile(
318
cursor, uid, torec, 'statement', context
321
account_move_line_obj.reconcile_partial(
322
cursor, uid, torec, 'statement', context
325
# raise osv.except_osv(
327
# _('Unable to reconcile entry "%s": %.2f') %
328
# (move.name, move.amount)
331
if st.journal_id.entry_posted:
332
account_move_obj.write(cursor, uid, [move_id], {'state':'posted'})
334
self.write(cursor, uid, done, {'state':'confirm'}, context=context)
337
account_bank_statement()
339
class account_bank_statement_line(osv.osv):
341
Extension on basic class:
342
1. Extra links to account.period and res.partner.bank for tracing and
344
2. Extra 'trans' field to carry the transaction id of the bank.
345
3. Extra 'international' flag to indicate the missing of a remote
346
account number. Some banks use seperate international banking
347
modules that do not integrate with the standard transaction files.
348
4. Readonly states for most fields except when in draft.
350
_inherit = 'account.bank.statement.line'
351
_description = 'Bank Transaction'
353
def _get_period(self, cursor, uid, context={}):
354
date = context.get('date') and context['date'] or None
355
periods = self.pool.get('account.period').find(cursor, uid, dt=date)
356
return periods and periods[0] or False
358
def _seems_international(self, cursor, uid, context={}):
360
Some banks have seperate international banking modules which do not
361
translate correctly into the national formats. Instead, they
362
leave key fields blank and signal this anomaly with a special
364
With the introduction of SEPA, this may worsen greatly, as SEPA
365
payments are considered to be analogous to international payments
366
by most local formats.
368
# Quick and dirty check: if remote bank account is missing, assume
369
# international transfer
371
context.get('partner_bank_id') and context['partner_bank_id']
373
# Not so dirty check: check if partner_id is set. If it is, check the
374
# default/invoice addresses country. If it is the same as our
375
# company's, its local, else international.
380
'amount': fields.float('Amount', readonly=True,
381
states={'draft': [('readonly', False)]}),
382
'ref': fields.char('Ref.', size=32, readonly=True,
383
states={'draft': [('readonly', False)]}),
384
'name': fields.char('Name', size=64, required=True, readonly=True,
385
states={'draft': [('readonly', False)]}),
386
'date': fields.date('Date', required=True, readonly=True,
387
states={'draft': [('readonly', False)]}),
389
'trans': fields.char('Bank Transaction ID', size=15, required=False,
391
states={'draft':[('readonly', False)]},
393
'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
394
required=False, readonly=True,
395
states={'draft':[('readonly', False)]},
397
'period_id': fields.many2one('account.period', 'Period', required=True,
398
states={'confirm': [('readonly', True)]}),
399
# Not used yet, but usefull in the future.
400
'international': fields.boolean('International Transaction',
402
states={'confirm': [('readonly', True)]},
407
'period_id': _get_period,
408
'international': _seems_international,
411
def onchange_partner_id(self, cursor, uid, line_id, partner_id, type,
412
currency_id, context={}
416
users_obj = self.pool.get('res.users')
417
partner_obj = self.pool.get('res.partner')
419
company_currency_id = users_obj.browse(
420
cursor, uid, uid, context=context
421
).company_id.currency_id.id
424
currency_id = company_currency_id
426
partner = partner_obj.browse(cursor, uid, partner_id, context=context)
427
if partner.supplier and not part.customer:
428
account_id = part.property_account_payable.id
430
elif partner.supplier and not part.customer:
431
account_id = part.property_account_receivable.id
437
return {'value': {'type': type, 'account_id': account_id}}
439
def write(self, cursor, uid, ids, values, context={}):
440
# TODO: Not sure what to do with this, as it seems that most of
441
# this code is related to res_partner_bank and not to this class.
443
bank_obj = self.pool.get('res.partner.bank')
444
statement_line_obj = self.pool.get('account.bank.statement.line')
446
if 'partner_id' in values:
447
bank_account_ids = bank_obj.search(cursor, uid,
448
[('partner_id','=', values['partner_id'])]
450
bank_accounts = bank_obj.browse(cursor, uid, bank_account_ids)
451
import_account_numbers = statement_line_obj.browse(cursor, uid, ids)
453
for accno in bank_accounts:
454
# Allow acc_number and iban to co-exist (SEPA will unite the
455
# two, but - as seen now - in an uneven pace per country)
457
account_numbers.append(accno.acc_number)
459
account_numbers.append(accno.iban)
461
if any([x for x in import_account_numbers if x.bank_accnumber in
463
for accno in import_account_numbers:
464
account_data = _get_account_data(accno.bank_accnumber)
466
bank_id = bank_obj.search(cursor, uid, [
467
('name', '=', account_data['bank_name'])
470
bank_id = bank_obj.create(cursor, uid, {
471
'name': account_data['bank_name'],
472
'bic': account_data['bic'],
478
bank_acc = bank_obj.create(cursor, uid, {
480
'partner_id': values['partner_id'],
482
'acc_number': accno.bank_accnumber,
485
bank_iban = bank_obj.create(cursor, uid, {
487
'partner_id': values['partner_id'],
489
'iban': account_data['iban'],
493
bank_acc = bank_obj.create(cursor, uid, {
495
'partner_id': values['partner_id'],
496
'acc_number': accno.bank_accnumber,
499
return super(account_bank_statement_line, self).write(
500
cursor, uid, ids, values, context
503
account_bank_statement_line()
505
class payment_type(osv.osv):
507
Make description field translatable #, add country context
509
_inherit = 'payment.type'
511
'name': fields.char('Name', size=64, required=True, translate=True,
514
#'country_id': fields.many2one('res.country', 'Country',
516
# help='Use this to limit this type to a specific country'
520
# 'country_id': lambda *a: False,
524
class payment_line(osv.osv):
526
Add extra export_state and date_done fields; make destination bank account
527
mandatory, as it makes no sense to send payments into thin air.
529
_inherit = 'payment.line'
532
'bank_id': fields.many2one('res.partner.bank',
533
'Destination Bank account',
536
'export_state': fields.selection([
538
('open','Confirmed'),
539
('cancel','Cancelled'),
542
], 'State', select=True
544
# Redefined fields: added states
545
'date_done': fields.datetime('Date Confirmed', select=True,
548
'Your Reference', size=64, required=True,
550
'sent': [('readonly', True)],
551
'done': [('readonly', True)]
554
'communication': fields.char(
555
'Communication', size=64, required=True,
556
help=("Used as the message between ordering customer and current "
557
"company. Depicts 'What do you want to say to the recipient"
558
" about this order ?'"
561
'sent': [('readonly', True)],
562
'done': [('readonly', True)]
565
'communication2': fields.char(
566
'Communication 2', size=64,
567
help='The successor message of Communication.',
569
'sent': [('readonly', True)],
570
'done': [('readonly', True)]
573
'move_line_id': fields.many2one(
574
'account.move.line', 'Entry line',
575
domain=[('reconcile_id','=', False),
576
('account_id.type', '=','payable')
578
help=('This Entry Line will be referred for the information of '
579
'the ordering customer.'
582
'sent': [('readonly', True)],
583
'done': [('readonly', True)]
586
'amount_currency': fields.float(
587
'Amount in Partner Currency', digits=(16,2),
589
help='Payment amount in the partner currency',
591
'sent': [('readonly', True)],
592
'done': [('readonly', True)]
595
'currency': fields.many2one(
596
'res.currency', 'Partner Currency', required=True,
598
'sent': [('readonly', True)],
599
'done': [('readonly', True)]
602
'bank_id': fields.many2one(
603
'res.partner.bank', 'Destination Bank account',
605
'sent': [('readonly', True)],
606
'done': [('readonly', True)]
609
'order_id': fields.many2one(
610
'payment.order', 'Order', required=True,
611
ondelete='cascade', select=True,
613
'sent': [('readonly', True)],
614
'done': [('readonly', True)]
617
'partner_id': fields.many2one(
618
'res.partner', string="Partner", required=True,
619
help='The Ordering Customer',
621
'sent': [('readonly', True)],
622
'done': [('readonly', True)]
627
help=("If no payment date is specified, the bank will treat this "
628
"payment line directly"
631
'sent': [('readonly', True)],
632
'done': [('readonly', True)]
635
'state': fields.selection([
637
('structured','Structured')
638
], 'Communication Type', required=True,
640
'sent': [('readonly', True)],
641
'done': [('readonly', True)]
646
'export_state': lambda *a: 'draft',
647
'date_done': lambda *a: False,
651
class payment_order(osv.osv):
653
Enable extra state for payment exports
655
_inherit = 'payment.order'
657
'date_planned': fields.date(
658
'Scheduled date if fixed',
660
'sent': [('readonly', True)],
661
'done': [('readonly', True)]
663
help='Select a date if you have chosen Preferred Date to be fixed.'
665
'reference': fields.char(
666
'Reference', size=128, required=True,
668
'sent': [('readonly', True)],
669
'done': [('readonly', True)]
672
'mode': fields.many2one(
673
'payment.mode', 'Payment mode', select=True, required=True,
675
'sent': [('readonly', True)],
676
'done': [('readonly', True)]
678
help='Select the Payment Mode to be applied.'
680
'state': fields.selection([
682
('open','Confirmed'),
683
('cancel','Cancelled'),
686
], 'State', select=True
688
'line_ids': fields.one2many(
689
'payment.line', 'order_id', 'Payment lines',
691
'sent': [('readonly', True)],
692
'done': [('readonly', True)]
695
'user_id': fields.many2one(
696
'res.users','User', required=True,
698
'sent': [('readonly', True)],
699
'done': [('readonly', True)]
702
'date_prefered': fields.selection([
705
('fixed', 'Fixed date')
706
], "Preferred date", change_default=True, required=True,
708
'sent': [('readonly', True)],
709
'done': [('readonly', True)]
711
help=("Choose an option for the Payment Order:'Fixed' stands for a "
712
"date specified by you.'Directly' stands for the direct "
713
"execution.'Due date' stands for the scheduled date of "
719
def set_to_draft(self, cr, uid, ids, *args):
720
cr.execute("UPDATE payment_line "
721
"SET export_state = 'draft' "
722
"WHERE order_id in (%s)" % (
723
','.join(map(str, ids))
725
return super(payment_order, self).set_to_draft(
729
def action_sent(self, cr, uid, ids, *args):
730
cr.execute("UPDATE payment_line "
731
"SET export_state = 'sent' "
732
"WHERE order_id in (%s)" % (
733
','.join(map(str, ids))
737
def set_done(self, cr, uid, id, *args):
739
Extend standard transition to update childs as well.
741
cr.execute("UPDATE payment_line "
742
"SET export_state = 'done', date_done = '%s' "
743
"WHERE order_id = %s" % (
744
time.strftime('%Y-%m-%d'),
747
return super(payment_order, self).set_done(
753
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: