~factorlibre/sepa-tools/sepa-tools

« back to all changes in this revision

Viewing changes to account_payment_sepa_direct_debit/account_banking_sdd.py

  • Committer: Ignacio Ibeas - Acysos S.L.
  • Date: 2014-02-09 15:05:50 UTC
  • Revision ID: ignacio@acysos.com-20140209150550-uug8yon3vcuh3rlj
[ADD] New SEPA modules for account_payment_extension

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
##############################################################################
 
2
#
 
3
#    SEPA Direct Debit module for OpenERP
 
4
#    Copyright (C) 2013 Akretion (http://www.akretion.com)
 
5
#    @author: Alexis de Lattre <alexis.delattre@akretion.com>
 
6
#
 
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.
 
11
#
 
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.
 
16
#
 
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/>.
 
19
#
 
20
##############################################################################
 
21
 
 
22
from openerp.osv import orm, fields
 
23
from openerp.tools.translate import _
 
24
from openerp.addons.decimal_precision import decimal_precision as dp
 
25
from unidecode import unidecode
 
26
from datetime import datetime
 
27
from dateutil.relativedelta import relativedelta
 
28
import logging
 
29
 
 
30
NUMBER_OF_UNUSED_MONTHS_BEFORE_EXPIRY = 36
 
31
 
 
32
logger = logging.getLogger(__name__)
 
33
 
 
34
 
 
35
class banking_export_sdd(orm.Model):
 
36
    '''SEPA Direct Debit export'''
 
37
    _name = 'banking.export.sdd'
 
38
    _description = __doc__
 
39
    _rec_name = 'filename'
 
40
 
 
41
    def _generate_filename(self, cr, uid, ids, name, arg, context=None):
 
42
        res = {}
 
43
        for sepa_file in self.browse(cr, uid, ids, context=context):
 
44
            ref = sepa_file.payment_order_ids[0].reference
 
45
            if ref:
 
46
                label = unidecode(ref.replace('/', '-'))
 
47
            else:
 
48
                label = 'error'
 
49
            res[sepa_file.id] = 'sdd_%s.xml' % label
 
50
        return res
 
51
 
 
52
    _columns = {
 
53
        'payment_order_ids': fields.many2many(
 
54
            'payment.order',
 
55
            'account_payment_order_sdd_rel',
 
56
            'banking_export_sepa_id', 'account_order_id',
 
57
            'Payment Orders',
 
58
            readonly=True),
 
59
        'nb_transactions': fields.integer(
 
60
            'Number of Transactions', readonly=True),
 
61
        'total_amount': fields.float(
 
62
            'Total Amount', digits_compute=dp.get_precision('Account'),
 
63
            readonly=True),
 
64
        'batch_booking': fields.boolean(
 
65
            'Batch Booking', readonly=True,
 
66
            help="If true, the bank statement will display only one credit "
 
67
            "line for all the direct debits of the SEPA file ; if false, "
 
68
            "the bank statement will display one credit line per direct "
 
69
            "debit of the SEPA file."),
 
70
        'charge_bearer': fields.selection([
 
71
            ('SLEV', 'Following Service Level'),
 
72
            ('SHAR', 'Shared'),
 
73
            ('CRED', 'Borne by Creditor'),
 
74
            ('DEBT', 'Borne by Debtor'),
 
75
            ], 'Charge Bearer', readonly=True,
 
76
            help="Following service level : transaction charges are to be "
 
77
            "applied following the rules agreed in the service level and/or "
 
78
            "scheme (SEPA Core messages must use this). Shared : "
 
79
            "transaction charges on the creditor side are to be borne by "
 
80
            "the creditor, transaction charges on the debtor side are to be "
 
81
            "borne by the debtor. Borne by creditor : all transaction "
 
82
            "charges are to be borne by the creditor. Borne by debtor : "
 
83
            "all transaction charges are to be borne by the debtor."),
 
84
        'create_date': fields.datetime('Generation Date', readonly=True),
 
85
        'file': fields.binary('SEPA File', readonly=True),
 
86
        'filename': fields.function(
 
87
            _generate_filename, type='char', size=256,
 
88
            string='Filename', readonly=True, store=True),
 
89
        'state': fields.selection([
 
90
            ('draft', 'Draft'),
 
91
            ('sent', 'Sent'),
 
92
            ('done', 'Reconciled'),
 
93
            ], 'State', readonly=True),
 
94
    }
 
95
 
 
96
    _defaults = {
 
97
        'state': 'draft',
 
98
    }
 
99
 
 
100
 
 
101
class sdd_mandate(orm.Model):
 
102
    '''SEPA Direct Debit Mandate'''
 
103
    _name = 'sdd.mandate'
 
104
    _description = __doc__
 
105
    _rec_name = 'unique_mandate_reference'
 
106
    _inherit = ['mail.thread']
 
107
    _order = 'signature_date desc'
 
108
    _track = {
 
109
        'state': {
 
110
            'account_banking_sepa_direct_debit.mandate_valid':
 
111
            lambda self, cr, uid, obj, ctx=None:
 
112
            obj['state'] == 'valid',
 
113
            'account_banking_sepa_direct_debit.mandate_expired':
 
114
            lambda self, cr, uid, obj, ctx=None:
 
115
            obj['state'] == 'expired',
 
116
            'account_banking_sepa_direct_debit.mandate_cancel':
 
117
            lambda self, cr, uid, obj, ctx=None:
 
118
            obj['state'] == 'cancel',
 
119
            },
 
120
        'recurrent_sequence_type': {
 
121
            'account_banking_sepa_direct_debit.recurrent_sequence_type_first':
 
122
            lambda self, cr, uid, obj, ctx=None:
 
123
            obj['recurrent_sequence_type'] == 'first',
 
124
            'account_banking_sepa_direct_debit.'
 
125
            'recurrent_sequence_type_recurring':
 
126
            lambda self, cr, uid, obj, ctx=None:
 
127
            obj['recurrent_sequence_type'] == 'recurring',
 
128
            'account_banking_sepa_direct_debit.recurrent_sequence_type_final':
 
129
            lambda self, cr, uid, obj, ctx=None:
 
130
            obj['recurrent_sequence_type'] == 'final',
 
131
            }
 
132
        }
 
133
 
 
134
    _columns = {
 
135
        'partner_bank_id': fields.many2one(
 
136
            'res.partner.bank', 'Bank Account', track_visibility='onchange'),
 
137
        'partner_id': fields.related(
 
138
            'partner_bank_id', 'partner_id', type='many2one',
 
139
            relation='res.partner', string='Partner', readonly=True),
 
140
        'company_id': fields.many2one('res.company', 'Company', required=True),
 
141
        'unique_mandate_reference': fields.char(
 
142
            'Unique Mandate Reference', size=35, readonly=True,
 
143
            track_visibility='always'),
 
144
        'type': fields.selection([
 
145
            ('recurrent', 'Recurrent'),
 
146
            ('oneoff', 'One-Off'),
 
147
            ], 'Type of Mandate', required=True, track_visibility='always'),
 
148
        'recurrent_sequence_type': fields.selection([
 
149
            ('first', 'First'),
 
150
            ('recurring', 'Recurring'),
 
151
            ('final', 'Final'),
 
152
            ], 'Sequence Type for Next Debit', track_visibility='onchange',
 
153
            help="This field is only used for Recurrent mandates, not for "
 
154
            "One-Off mandates."),
 
155
        'signature_date': fields.date(
 
156
            'Date of Signature of the Mandate', track_visibility='onchange'),
 
157
        'scan': fields.binary('Scan of the Mandate'),
 
158
        'last_debit_date': fields.date(
 
159
            'Date of the Last Debit', readonly=True),
 
160
        'state': fields.selection([
 
161
            ('draft', 'Draft'),
 
162
            ('valid', 'Valid'),
 
163
            ('expired', 'Expired'),
 
164
            ('cancel', 'Cancelled'),
 
165
            ], 'Status',
 
166
            help="Only valid mandates can be used in a payment line. A "
 
167
            "cancelled mandate is a mandate that has been cancelled by "
 
168
            "the customer. A one-off mandate expires after its first use. "
 
169
            "A recurrent mandate expires after it's final use or if it "
 
170
            "hasn't been used for 36 months."),
 
171
        'payment_line_ids': fields.one2many(
 
172
            'payment.line', 'sdd_mandate_id', "Related Payment Lines"),
 
173
        'sepa_migrated': fields.boolean(
 
174
            'Migrated to SEPA', track_visibility='onchange',
 
175
            help="If this field is not active, the mandate section of the "
 
176
            "next direct debit file that include this mandate will contain "
 
177
            "the 'Original Mandate Identification' and the 'Original "
 
178
            "Creditor Scheme Identification'. This is required in a few "
 
179
            "countries (Belgium for instance), but not in all countries. "
 
180
            "If this is not required in your country, you should keep this "
 
181
            "field always active."),
 
182
        'original_mandate_identification': fields.char(
 
183
            'Original Mandate Identification', size=35,
 
184
            track_visibility='onchange',
 
185
            help="When the field 'Migrated to SEPA' is not active, this "
 
186
            "field will be used as the Original Mandate Identification in "
 
187
            "the Direct Debit file."),
 
188
        }
 
189
 
 
190
    _defaults = {
 
191
        'company_id': lambda self, cr, uid, context:
 
192
        self.pool['res.company']._company_default_get(
 
193
            cr, uid, 'sdd.mandate', context=context),
 
194
        'unique_mandate_reference': lambda self, cr, uid, ctx:
 
195
        self.pool['ir.sequence'].get(cr, uid, 'sdd.mandate.reference'),
 
196
        'state': 'draft',
 
197
        'sepa_migrated': True,
 
198
    }
 
199
 
 
200
    _sql_constraints = [(
 
201
        'mandate_ref_company_uniq',
 
202
        'unique(unique_mandate_reference, company_id)',
 
203
        'A Mandate with the same reference already exists for this company !'
 
204
        )]
 
205
 
 
206
    def _check_sdd_mandate(self, cr, uid, ids):
 
207
        for mandate in self.browse(cr, uid, ids):
 
208
            if (mandate.signature_date and
 
209
                    mandate.signature_date >
 
210
                    datetime.today().strftime('%Y-%m-%d')):
 
211
                raise orm.except_orm(
 
212
                    _('Error:'),
 
213
                    _("The date of signature of mandate '%s' is in the "
 
214
                        "future !")
 
215
                    % mandate.unique_mandate_reference)
 
216
            if mandate.state == 'valid' and not mandate.signature_date:
 
217
                raise orm.except_orm(
 
218
                    _('Error:'),
 
219
                    _("Cannot validate the mandate '%s' without a date of "
 
220
                        "signature.")
 
221
                    % mandate.unique_mandate_reference)
 
222
            if mandate.state == 'valid' and not mandate.partner_bank_id:
 
223
                raise orm.except_orm(
 
224
                    _('Error:'),
 
225
                    _("Cannot validate the mandate '%s' because it is not "
 
226
                        "attached to a bank account.")
 
227
                    % mandate.unique_mandate_reference)
 
228
 
 
229
            if (mandate.signature_date and mandate.last_debit_date and
 
230
                    mandate.signature_date > mandate.last_debit_date):
 
231
                raise orm.except_orm(
 
232
                    _('Error:'),
 
233
                    _("The mandate '%s' can't have a date of last debit "
 
234
                        "before the date of signature.")
 
235
                    % mandate.unique_mandate_reference)
 
236
            if (mandate.type == 'recurrent'
 
237
                    and not mandate.recurrent_sequence_type):
 
238
                raise orm.except_orm(
 
239
                    _('Error:'),
 
240
                    _("The recurrent mandate '%s' must have a sequence type.")
 
241
                    % mandate.unique_mandate_reference)
 
242
            if (mandate.type == 'recurrent' and not mandate.sepa_migrated
 
243
                    and mandate.recurrent_sequence_type != 'first'):
 
244
                raise orm.except_orm(
 
245
                    _('Error:'),
 
246
                    _("The recurrent mandate '%s' which is not marked as "
 
247
                        "'Migrated to SEPA' must have its recurrent sequence "
 
248
                        "type set to 'First'.")
 
249
                    % mandate.unique_mandate_reference)
 
250
            if (mandate.type == 'recurrent' and not mandate.sepa_migrated
 
251
                    and not mandate.original_mandate_identification):
 
252
                raise orm.except_orm(
 
253
                    _('Error:'),
 
254
                    _("You must set the 'Original Mandate Identification' "
 
255
                        "on the recurrent mandate '%s' which is not marked "
 
256
                        "as 'Migrated to SEPA'.")
 
257
                    % mandate.unique_mandate_reference)
 
258
        return True
 
259
 
 
260
    _constraints = [
 
261
        (_check_sdd_mandate, "Error msg in raise", [
 
262
            'last_debit_date', 'signature_date', 'state', 'partner_bank_id',
 
263
            'type', 'recurrent_sequence_type', 'sepa_migrated',
 
264
            'original_mandate_identification',
 
265
            ]),
 
266
    ]
 
267
 
 
268
    def mandate_type_change(self, cr, uid, ids, type):
 
269
        if type == 'recurrent':
 
270
            recurrent_sequence_type = 'first'
 
271
        else:
 
272
            recurrent_sequence_type = False
 
273
        res = {'value': {'recurrent_sequence_type': recurrent_sequence_type}}
 
274
        return res
 
275
 
 
276
    def mandate_partner_bank_change(
 
277
            self, cr, uid, ids, partner_bank_id, type, recurrent_sequence_type,
 
278
            last_debit_date, state):
 
279
        res = {'value': {}}
 
280
        if partner_bank_id:
 
281
            partner_bank_read = self.pool['res.partner.bank'].read(
 
282
                cr, uid, partner_bank_id, ['partner_id'])['partner_id']
 
283
            if partner_bank_read:
 
284
                res['value']['partner_id'] = partner_bank_read[0]
 
285
        if (state == 'valid' and partner_bank_id
 
286
                and type == 'recurrent'
 
287
                and recurrent_sequence_type != 'first'):
 
288
            res['value']['recurrent_sequence_type'] = 'first'
 
289
            res['warning'] = {
 
290
                'title': _('Mandate update'),
 
291
                'message': _(
 
292
                    "As you changed the bank account attached to this "
 
293
                    "mandate, the 'Sequence Type' has been set back to "
 
294
                    "'First'."),
 
295
                }
 
296
        return res
 
297
 
 
298
    def validate(self, cr, uid, ids, context=None):
 
299
        to_validate_ids = []
 
300
        for mandate in self.browse(cr, uid, ids, context=context):
 
301
            assert mandate.state == 'draft', 'Mandate should be in draft state'
 
302
            to_validate_ids.append(mandate.id)
 
303
        self.write(
 
304
            cr, uid, to_validate_ids, {'state': 'valid'}, context=context)
 
305
        return True
 
306
 
 
307
    def cancel(self, cr, uid, ids, context=None):
 
308
        to_cancel_ids = []
 
309
        for mandate in self.browse(cr, uid, ids, context=context):
 
310
            assert mandate.state in ('draft', 'valid'),\
 
311
                'Mandate should be in draft or valid state'
 
312
            to_cancel_ids.append(mandate.id)
 
313
        self.write(
 
314
            cr, uid, to_cancel_ids, {'state': 'cancel'}, context=context)
 
315
        return True
 
316
 
 
317
    def _sdd_mandate_set_state_to_expired(self, cr, uid, context=None):
 
318
        logger.info('Searching for SDD Mandates that must be set to Expired')
 
319
        expire_limit_date = datetime.today() + \
 
320
            relativedelta(months=-NUMBER_OF_UNUSED_MONTHS_BEFORE_EXPIRY)
 
321
        expire_limit_date_str = expire_limit_date.strftime('%Y-%m-%d')
 
322
        expired_mandate_ids = self.search(cr, uid, [
 
323
            '|',
 
324
            ('last_debit_date', '=', False),
 
325
            ('last_debit_date', '<=', expire_limit_date_str),
 
326
            ('state', '=', 'valid'),
 
327
            ('signature_date', '<=', expire_limit_date_str),
 
328
            ], context=context)
 
329
        if expired_mandate_ids:
 
330
            self.write(
 
331
                cr, uid, expired_mandate_ids, {'state': 'expired'},
 
332
                context=context)
 
333
            logger.info(
 
334
                'The following SDD Mandate IDs has been set to expired: %s'
 
335
                % expired_mandate_ids)
 
336
        else:
 
337
            logger.info('0 SDD Mandates must be set to Expired')
 
338
        return True
 
339
 
 
340
 
 
341
class res_partner_bank(orm.Model):
 
342
    _inherit = 'res.partner.bank'
 
343
 
 
344
    _columns = {
 
345
        'sdd_mandate_ids': fields.one2many(
 
346
            'sdd.mandate', 'partner_bank_id', 'SEPA Direct Debit Mandates'),
 
347
        }
 
348
 
 
349
 
 
350
class payment_line(orm.Model):
 
351
    _inherit = 'payment.line'
 
352
 
 
353
    _columns = {
 
354
        'sdd_mandate_id': fields.many2one(
 
355
            'sdd.mandate', 'SEPA Direct Debit Mandate',
 
356
            domain=[('state', '=', 'valid')]),
 
357
        }
 
358
 
 
359
    def create(self, cr, uid, vals, context=None):
 
360
        '''If the customer invoice has a mandate, take it
 
361
        otherwise, take the first valid mandate of the bank account'''
 
362
        if context is None:
 
363
            context = {}
 
364
        if not vals:
 
365
            vals = {}
 
366
        partner_bank_id = vals.get('bank_id')
 
367
        move_line_id = vals.get('move_line_id')
 
368
        if (context.get('type') == 'receivable'
 
369
                and 'sdd_mandate_id' not in vals):
 
370
            if move_line_id:
 
371
                line = self.pool['account.move.line'].browse(
 
372
                    cr, uid, move_line_id, context=context)
 
373
                if (line.invoice and line.invoice.type == 'out_invoice'
 
374
                        and line.invoice.sdd_mandate_id):
 
375
                    vals.update({
 
376
                        'sdd_mandate_id': line.invoice.sdd_mandate_id.id,
 
377
                        'bank_id':
 
378
                        line.invoice.sdd_mandate_id.partner_bank_id.id,
 
379
                    })
 
380
            if partner_bank_id and 'sdd_mandate_id' not in vals:
 
381
                mandate_ids = self.pool['sdd.mandate'].search(cr, uid, [
 
382
                    ('partner_bank_id', '=', partner_bank_id),
 
383
                    ('state', '=', 'valid'),
 
384
                    ], context=context)
 
385
                if mandate_ids:
 
386
                    vals['sdd_mandate_id'] = mandate_ids[0]
 
387
        return super(payment_line, self).create(cr, uid, vals, context=context)
 
388
 
 
389
    def _check_mandate_bank_link(self, cr, uid, ids):
 
390
        for payline in self.browse(cr, uid, ids):
 
391
            if (payline.sdd_mandate_id and payline.bank_id
 
392
                    and payline.sdd_mandate_id.partner_bank_id.id !=
 
393
                    payline.bank_id.id):
 
394
                raise orm.except_orm(
 
395
                    _('Error:'),
 
396
                    _("The payment line with reference '%s' has the bank "
 
397
                        "account '%s' which is not attached to the mandate "
 
398
                        "'%s' (this mandate is attached to the bank account "
 
399
                        "'%s').") % (
 
400
                        payline.name,
 
401
                        self.pool['res.partner.bank'].name_get(
 
402
                            cr, uid, [payline.bank_id.id])[0][1],
 
403
                        payline.sdd_mandate_id.unique_mandate_reference,
 
404
                        self.pool['res.partner.bank'].name_get(
 
405
                            cr, uid,
 
406
                            [payline.sdd_mandate_id.partner_bank_id.id])[0][1],
 
407
                    ))
 
408
        return True
 
409
 
 
410
    _constraints = [
 
411
        (_check_mandate_bank_link, 'Error msg in raise',
 
412
            ['sdd_mandate_id', 'bank_id']),
 
413
    ]
 
414
 
 
415
 
 
416
class account_invoice(orm.Model):
 
417
    _inherit = 'account.invoice'
 
418
 
 
419
    _columns = {
 
420
        'sdd_mandate_id': fields.many2one(
 
421
            'sdd.mandate', 'SEPA Direct Debit Mandate',
 
422
            domain=[('state', '=', 'valid')], readonly=True,
 
423
            states={'draft': [('readonly', False)]})
 
424
        }