# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
import os
import unicodedata
import uuid
from io import BytesIO
from itertools import groupby

import genshi
import genshi.template
# XXX fix: https://genshi.edgewall.org/ticket/582
from genshi.template.astutil import ASTCodeGenerator, ASTTransformer
from lxml import etree
from sql import Literal, Null

from trytond.config import config
from trytond.i18n import gettext
from trytond.model import (
    Index, ModelSQL, ModelView, Unique, Workflow, dualmethod, fields)
from trytond.model.exceptions import AccessError
from trytond.modules.account_payment.exceptions import ProcessError
from trytond.modules.company import CompanyReport
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, If
from trytond.tools import (
    grouped_slice, is_full_text, lstrip_wildcard, reduce_ids, sortable_values)
from trytond.transaction import Transaction

from .sepa_handler import CAMT054

if not hasattr(ASTCodeGenerator, 'visit_NameConstant'):
    def visit_NameConstant(self, node):
        if node.value is None:
            self._write('None')
        elif node.value is True:
            self._write('True')
        elif node.value is False:
            self._write('False')
        else:
            raise Exception("Unknown NameConstant %r" % (node.value,))
    ASTCodeGenerator.visit_NameConstant = visit_NameConstant
if not hasattr(ASTTransformer, 'visit_NameConstant'):
    # Re-use visit_Name because _clone is deleted
    ASTTransformer.visit_NameConstant = ASTTransformer.visit_Name

if config.getboolean('account_payment_sepa', 'filestore', default=False):
    file_id = 'message_file_id'
    store_prefix = config.get(
        'account_payment_sepa', 'store_prefix', default=None)
else:
    file_id = None
    store_prefix = None

INITIATOR_IDS = [
    (None, ''),
    ('eu_at_02', "SEPA Creditor Identifier"),
    ('be_vat', "Belgian Enterprise Number"),
    ('es_vat', "Spanish VAT Number"),
    ]


class Journal(metaclass=PoolMeta):
    __name__ = 'account.payment.journal'
    company_party = fields.Function(fields.Many2One(
            'party.party', "Company Party",
            context={
                'company': Eval('company', -1),
                },
            depends={'company'}),
        'on_change_with_company_party')
    sepa_bank_account_number = fields.Many2One('bank.account.number',
        'Bank Account Number', states={
            'required': Eval('process_method') == 'sepa',
            'invisible': Eval('process_method') != 'sepa',
            },
        domain=[
            ('type', '=', 'iban'),
            ('account.owners', '=', Eval('company_party')),
            ])
    sepa_payable_flavor = fields.Selection([
            (None, ''),
            ('pain.001.001.03', 'pain.001.001.03'),
            ('pain.001.001.05', 'pain.001.001.05'),
            ('pain.001.003.03', 'pain.001.003.03'),
            ], 'Payable Flavor', states={
            'required': Eval('process_method') == 'sepa',
            'invisible': Eval('process_method') != 'sepa',
            },
        translate=False)
    sepa_receivable_flavor = fields.Selection([
            (None, ''),
            ('pain.008.001.02', 'pain.008.001.02'),
            ('pain.008.001.04', 'pain.008.001.04'),
            ('pain.008.003.02', 'pain.008.003.02'),
            ], 'Receivable Flavor', states={
            'required': Eval('process_method') == 'sepa',
            'invisible': Eval('process_method') != 'sepa',
            },
        translate=False)
    sepa_payable_initiator_id = fields.Selection(
        INITIATOR_IDS, "SEPA Payable Initiator Identifier",
        states={
            'invisible': Eval('process_method') != 'sepa',
            },
        help="The identifier used for the initiating party.")
    sepa_receivable_initiator_id = fields.Selection(
        INITIATOR_IDS, "SEPA Receivable Initiator Identifier",
        states={
            'invisible': Eval('process_method') != 'sepa',
            },
        help="The identifier used for the initiating party.")
    sepa_batch_booking = fields.Boolean('Batch Booking', states={
            'invisible': Eval('process_method') != 'sepa',
            })
    sepa_charge_bearer = fields.Selection([
            ('DEBT', 'Debtor'),
            ('CRED', 'Creditor'),
            ('SHAR', 'Shared'),
            ('SLEV', 'Service Level'),
            ], 'Charge Bearer', states={
            'required': Eval('process_method') == 'sepa',
            'invisible': Eval('process_method') != 'sepa',
            })

    @classmethod
    def __setup__(cls):
        super(Journal, cls).__setup__()
        sepa_method = ('sepa', 'SEPA')
        if sepa_method not in cls.process_method.selection:
            cls.process_method.selection.append(sepa_method)

    @classmethod
    def __register__(cls, module_name):
        cursor = Transaction().connection.cursor()
        sql_table = cls.__table__()
        super().__register__(module_name)

        # Migration from 5.4: sepa identifier merged into eu_at_02
        for name in {'payable', 'receivable'}:
            column = getattr(sql_table, 'sepa_%s_initiator_id' % name)
            cursor.execute(*sql_table.update(
                    columns=[column],
                    values=['eu_at_02'],
                    where=column == 'sepa'))

        # Migration from 6.8: es_nif renamed into es_vat
        cursor.execute(*sql_table.update(
                [sql_table.sepa_payable_initiator_id],
                ['es_vat'],
                where=sql_table.sepa_payable_initiator_id == 'es_nif'))
        cursor.execute(*sql_table.update(
                [sql_table.sepa_receivable_initiator_id],
                ['es_vat'],
                where=sql_table.sepa_receivable_initiator_id == 'es_nif'))

    @classmethod
    def default_company_party(cls):
        pool = Pool()
        Company = pool.get('company.company')
        company_id = cls.default_company()
        if company_id:
            return Company(company_id).party.id

    @fields.depends('company')
    def on_change_with_company_party(self, name=None):
        return self.company.party if self.company else None

    @staticmethod
    def default_sepa_charge_bearer():
        return 'SLEV'


def remove_comment(stream):
    for kind, data, pos in stream:
        if kind is genshi.core.COMMENT:
            continue
        yield kind, data, pos


loader = genshi.template.TemplateLoader(
    os.path.join(os.path.dirname(__file__), 'template'),
    auto_reload=True)


class Group(metaclass=PoolMeta):
    __name__ = 'account.payment.group'
    sepa_messages = fields.One2Many('account.payment.sepa.message', 'origin',
        'SEPA Messages', readonly=True,
        domain=[('company', '=', Eval('company', -1))],
        states={
            'invisible': ~Eval('sepa_messages'),
            })
    sepa_id = fields.Char("SEPA ID", readonly=True, size=35,
        states={
            'invisible': ~Eval('sepa_id'),
            })

    @classmethod
    def __setup__(cls):
        super(Group, cls).__setup__()
        cls._buttons.update({
                'sepa_generate_message': {
                    'invisible': Eval('process_method') != 'sepa',
                    'depends': ['process_method'],
                    },
                })

    def get_sepa_template(self):
        if self.kind == 'payable':
            return loader.load('%s.xml' % self.journal.sepa_payable_flavor)
        elif self.kind == 'receivable':
            return loader.load('%s.xml' % self.journal.sepa_receivable_flavor)

    def process_sepa(self):
        pool = Pool()
        Payment = pool.get('account.payment')
        Mandate = pool.get('account.payment.sepa.mandate')
        if self.kind == 'receivable':
            payments = list(self.payments)
            mandates = Payment.get_sepa_mandates(payments)
            Mandate.lock(list(filter(None, mandates)))
            sequence_types = {}
            for payment, mandate in zip(payments, mandates):
                if not mandate:
                    raise ProcessError(
                        gettext('account_payment_sepa'
                            '.msg_payment_process_no_mandate',
                            payment=payment.rec_name))
                sequence_type = sequence_types.get(mandate)
                if not sequence_type:
                    sequence_type = mandate.sequence_type
                    if sequence_type == 'FRST':
                        sequence_types[mandate] = 'RCUR'
                    else:
                        sequence_types[mandate] = sequence_type
                payment.sepa_mandate = mandate
                payment.sepa_mandate_sequence_type = sequence_type
            Payment.save(payments)
        for payment in self.payments:
            if not payment.sepa_bank_account_number:
                raise ProcessError(
                    gettext('account_payment_sepa'
                        '.msg_payment_process_no_iban',
                        payment=payment.rec_name))
            if not payment.sepa_bank_account_number.account.bank:
                bank_account = payment.sepa_bank_account_number.account
                raise ProcessError(
                    gettext('account_payment_sepa'
                        '.msg_payment_process_no_bank',
                        payment=payment.rec_name,
                        bank_account=bank_account.rec_name))
        to_write = []
        for key, payments in self.sepa_payments:
            to_write.append(payments)
            to_write.append({
                    'sepa_info_id': self.sepa_group_payment_id(key),
                    })
        if to_write:
            Payment.write(*to_write)
        self.sepa_id = uuid.uuid4().hex
        self.sepa_generate_message(_save=False)

    @dualmethod
    @ModelView.button
    def sepa_generate_message(cls, groups, _save=True):
        pool = Pool()
        Message = pool.get('account.payment.sepa.message')
        for group in groups:
            if group.journal.process_method != 'sepa':
                continue
            tmpl = group.get_sepa_template()
            if not tmpl:
                raise NotImplementedError
            if not group.sepa_messages:
                group.sepa_messages = ()
            message = tmpl.generate(group=group,
                datetime=datetime, normalize=unicodedata.normalize,
                ).filter(remove_comment).render().encode('utf8')
            message = Message(message=message, type='out', state='waiting',
                company=group.company)
            group.sepa_messages += (message,)
        if _save:
            cls.save(groups)

    @property
    def sepa_initiating_party(self):
        return self.company.party

    def sepa_group_payment_key(self, payment):
        key = (
            ('payment_info', payment.sepa_info_id),
            ('date', payment.date),
            )
        if self.kind == 'receivable':
            key += (('sequence_type', payment.sepa_mandate_sequence_type),)
            key += (('scheme', payment.sepa_mandate.scheme),)
        return key

    def sepa_group_payment_id(self, key):
        return key['payment_info'] or uuid.uuid4().hex

    @property
    def sepa_payments(self):
        pool = Pool()
        Payment = pool.get('account.payment')
        keyfunc = self.sepa_group_payment_key
        # re-browse to align cache
        payments = Payment.browse(sorted(
                self.payments, key=sortable_values(keyfunc)))
        for key, grouped_payments in groupby(payments, key=keyfunc):
            yield dict(key), list(grouped_payments)

    @property
    def sepa_message_id(self):
        return self.sepa_id or self.number

    @classmethod
    def search_rec_name(cls, name, clause):
        _, operator, operand, *extra = clause
        if operator.startswith('!') or operator.startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        code_value = operand
        if operator.endswith('like') and is_full_text(operand):
            code_value = lstrip_wildcard(operand)
        domain = super().search_rec_name(name, clause)
        return [bool_op,
            domain,
            ('sepa_id', operator, code_value, *extra),
            ]


class Payment(metaclass=PoolMeta):
    __name__ = 'account.payment'

    sepa_mandate = fields.Many2One('account.payment.sepa.mandate', 'Mandate',
        ondelete='RESTRICT',
        states={
            'readonly': Eval('state') != 'draft',
            'invisible': ((Eval('process_method') != 'sepa')
                | (Eval('kind') != 'receivable')),
            },
        domain=[
            ('party', '=', Eval('party', -1)),
            ('company', '=', Eval('company', -1)),
            If(Eval('state') == 'draft',
                ('state', '=', 'validated'),
                (),
                )
            ])
    sepa_mandate_sequence_type = fields.Char('Mandate Sequence Type',
        readonly=True)
    sepa_return_reason_code = fields.Char('Return Reason Code', readonly=True,
        states={
            'invisible': ((Eval('process_method') != 'sepa')
                | (~Eval('sepa_return_reason_code')
                    & (Eval('state') != 'failed'))),
            })
    sepa_return_reason_information = fields.Text('Return Reason Information',
        readonly=True,
        states={
            'invisible': ((Eval('process_method') != 'sepa')
                | (~Eval('sepa_return_reason_information')
                    & (Eval('state') != 'failed'))),
            })
    sepa_end_to_end_id = fields.Function(fields.Char('SEPA End To End ID'),
        'get_sepa_end_to_end_id', searcher='search_end_to_end_id')
    sepa_instruction_id = fields.Function(fields.Char('SEPA Instruction ID'),
        'get_sepa_instruction_id', searcher='search_sepa_instruction_id')
    sepa_info_id = fields.Char("SEPA Info ID", readonly=True, size=35,
        states={
            'invisible': ~Eval('sepa_info_id'),
            })

    @classmethod
    def copy(cls, payments, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('sepa_mandate_sequence_type', None)
        return super(Payment, cls).copy(payments, default=default)

    @classmethod
    def get_sepa_mandates(cls, payments):
        mandates = []
        for payment in payments:
            if payment.sepa_mandate:
                if payment.sepa_mandate.is_valid:
                    mandate = payment.sepa_mandate
                else:
                    mandate = None
            else:
                for mandate in payment.party.sepa_mandates_for(payment):
                    if mandate.is_valid:
                        break
                else:
                    mandate = None
            mandates.append(mandate)
        return mandates

    def get_sepa_end_to_end_id(self, name):
        return str(self.id)

    @classmethod
    def search_end_to_end_id(cls, name, domain):
        table = cls.__table__()
        _, operator, value = domain
        cast = cls.sepa_end_to_end_id._field.sql_type().base
        Operator = fields.SQL_OPERATORS[operator]
        query = table.select(table.id,
            where=Operator(table.id.cast(cast), value))
        return [('id', 'in', query)]

    get_sepa_instruction_id = get_sepa_end_to_end_id
    search_sepa_instruction_id = search_end_to_end_id

    @property
    def sepa_remittance_information(self):
        if self.description:
            return self.description
        elif self.line and self.line.move_origin:
            return getattr(self.line.move_origin, 'rec_name', '')

    @property
    def sepa_bank_account_number(self):
        if self.kind == 'receivable':
            if self.sepa_mandate:
                return self.sepa_mandate.account_number
        else:
            for account in self.party.bank_accounts:
                for number in account.numbers:
                    if number.type == 'iban':
                        return number

    @property
    def rejected(self):
        return (self.state == 'failed'
            and self.sepa_return_reason_code
            and self.sepa_return_reason_information == '/RTYP/RJCT')

    def _get_clearing_move(self, date=None):
        if not date:
            date = Transaction().context.get('date_value')
        return super()._get_clearing_move(date=date)

    @classmethod
    def view_attributes(cls):
        return super().view_attributes() + [
            ('//separator[@id="sepa_return_reason"]', 'states', {
                    'invisible': ((Eval('process_method') != 'sepa')
                        | (Eval('state') != 'failed')),
                    }),
            ]

    @classmethod
    def search_rec_name(cls, name, clause):
        _, operator, operand, *extra = clause
        if operator.startswith('!') or operator.startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        code_value = operand
        if operator.endswith('like') and is_full_text(operand):
            code_value = lstrip_wildcard(operand)
        domain = super().search_rec_name(name, clause)
        return [bool_op,
            domain,
            ('sepa_info_id', operator, code_value, *extra),
            ]


class Mandate(Workflow, ModelSQL, ModelView):
    'SEPA Mandate'
    __name__ = 'account.payment.sepa.mandate'
    party = fields.Many2One(
        'party.party', "Party", required=True,
        states={
            'readonly': Eval('state').in_(
                ['requested', 'validated', 'cancelled']),
            },
        context={
            'company': Eval('company', -1),
            },
        depends={'company'})
    account_number = fields.Many2One('bank.account.number', 'Account Number',
        ondelete='RESTRICT',
        states={
            'readonly': (
                Eval('state').in_(['validated', 'cancelled'])
                | ~Eval('party')),
            'required': Eval('state') == 'validated',
            },
        domain=[
            ('type', '=', 'iban'),
            ('account.owners', '=', Eval('party')),
            ])
    identification = fields.Char('Identification', size=35,
        states={
            'readonly': Eval('identification_readonly', True),
            'required': Eval('state') == 'validated',
            })
    identification_readonly = fields.Function(fields.Boolean(
            'Identification Readonly'), 'get_identification_readonly')
    company = fields.Many2One(
        'company.company', "Company", required=True,
        states={
            'readonly': Eval('state') != 'draft',
            })
    type = fields.Selection([
            ('recurrent', 'Recurrent'),
            ('one-off', 'One-off'),
            ], 'Type',
        states={
            'readonly': Eval('state').in_(['validated', 'cancelled']),
            })
    sequence_type_rcur = fields.Boolean(
        "Always use RCUR",
        states={
            'invisible': Eval('type') == 'one-off',
            })
    scheme = fields.Selection([
            ('CORE', 'Core'),
            ('B2B', 'Business to Business'),
            ], 'Scheme', required=True,
        states={
            'readonly': Eval('state').in_(['validated', 'cancelled']),
            })
    scheme_string = scheme.translated('scheme')
    signature_date = fields.Date('Signature Date',
        states={
            'readonly': Eval('state').in_(['validated', 'cancelled']),
            'required': Eval('state') == 'validated',
            })
    state = fields.Selection([
            ('draft', 'Draft'),
            ('requested', 'Requested'),
            ('validated', 'Validated'),
            ('cancelled', 'Cancelled'),
            ], "State", readonly=True, sort=False)
    payments = fields.One2Many('account.payment', 'sepa_mandate', 'Payments')
    has_payments = fields.Function(fields.Boolean('Has Payments'),
        'get_has_payments')
    is_first_payment = fields.Function(
        fields.Boolean("Is First Payment"), 'get_is_first_payment')

    @classmethod
    def __setup__(cls):
        super(Mandate, cls).__setup__()
        cls._transitions |= set((
                ('draft', 'requested'),
                ('requested', 'validated'),
                ('validated', 'cancelled'),
                ('requested', 'cancelled'),
                ('requested', 'draft'),
                ))
        cls._buttons.update({
                'cancel': {
                    'invisible': ~Eval('state').in_(
                        ['requested', 'validated']),
                    'depends': ['state'],
                    },
                'draft': {
                    'invisible': Eval('state') != 'requested',
                    'depends': ['state'],
                    },
                'request': {
                    'invisible': Eval('state') != 'draft',
                    'depends': ['state'],
                    },
                'validate_mandate': {
                    'invisible': Eval('state') != 'requested',
                    'depends': ['state'],
                    },
                })
        t = cls.__table__()
        cls._sql_constraints = [
            ('identification_unique', Unique(t, t.company, t.identification),
                'account_payment_sepa.msg_mandate_unique_id'),
            ]
        cls._sql_indexes.add(
            Index(
                t, (t.state, Index.Equality()),
                where=t.state.in_(['draft', 'requested'])))

    @classmethod
    def __register__(cls, module_name):
        cursor = Transaction().connection.cursor()
        table = cls.__table__()

        super().__register__(module_name)

        # Migration from 5.6: rename state canceled to cancelled
        cursor.execute(*table.update(
                [table.state], ['cancelled'],
                where=table.state == 'canceled'))

    @staticmethod
    def default_company():
        return Transaction().context.get('company')

    @fields.depends('company')
    def on_change_company(self):
        self.identification_readonly = self.default_identification_readonly(
            company=self.company.id if self.company else None)

    @staticmethod
    def default_type():
        return 'recurrent'

    @classmethod
    def default_sequence_type_rcur(cls):
        return False

    @staticmethod
    def default_scheme():
        return 'CORE'

    @staticmethod
    def default_state():
        return 'draft'

    @classmethod
    def default_identification_readonly(cls, **pattern):
        pool = Pool()
        Configuration = pool.get('account.configuration')
        config = Configuration(1)
        return bool(config.get_multivalue('sepa_mandate_sequence', **pattern))

    def get_identification_readonly(self, name):
        return bool(self.identification)

    def get_rec_name(self, name):
        name = '(%s)' % self.id
        if self.identification:
            name = self.identification
        if self.account_number:
            name += ' @ %s' % self.account_number.rec_name
        return name

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
            ('identification',) + tuple(clause[1:]),
            ('account_number',) + tuple(clause[1:]),
            ]

    @classmethod
    def create(cls, vlist):
        pool = Pool()
        Configuration = pool.get('account.configuration')

        config = Configuration(1)
        vlist = [v.copy() for v in vlist]
        default_company = cls.default_company()
        for values in vlist:
            if (config.sepa_mandate_sequence
                    and not values.get('identification')):
                values['identification'] = config.get_multivalue(
                    'sepa_mandate_sequence',
                    company=values.get('company', default_company)).get()
            # Prevent raising false unique constraint
            if values.get('identification') == '':
                values['identification'] = None
        return super(Mandate, cls).create(vlist)

    @classmethod
    def write(cls, *args):
        actions = iter(args)
        args = []
        for mandates, values in zip(actions, actions):
            # Prevent raising false unique constraint
            if values.get('identification') == '':
                values = values.copy()
                values['identification'] = None
            args.extend((mandates, values))
        super(Mandate, cls).write(*args)

    @classmethod
    def copy(cls, mandates, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('payments', [])
        default.setdefault('signature_date', None)
        default.setdefault('identification', None)
        return super(Mandate, cls).copy(mandates, default=default)

    @property
    def is_valid(self):
        if self.state == 'validated' and self.account_number.active:
            if self.type == 'one-off':
                if not self.has_payments:
                    return True
            else:
                return True
        return False

    @property
    def sequence_type(self):
        if self.type == 'one-off':
            return 'OOFF'
        elif not self.sequence_type_rcur and self.is_first_payment:
            return 'FRST'
        # TODO manage FNAL
        else:
            return 'RCUR'

    @classmethod
    def get_has_payments(cls, mandates, name):
        pool = Pool()
        Payment = pool.get('account.payment')
        payment = Payment.__table__()
        cursor = Transaction().connection.cursor()

        has_payments = dict.fromkeys([m.id for m in mandates], False)
        for sub_ids in grouped_slice(mandates):
            red_sql = reduce_ids(payment.sepa_mandate, sub_ids)
            cursor.execute(*payment.select(payment.sepa_mandate, Literal(True),
                    where=red_sql,
                    group_by=payment.sepa_mandate))
            has_payments.update(cursor)

        return has_payments

    @classmethod
    def get_is_first_payment(cls, mandates, name):
        pool = Pool()
        Payment = pool.get('account.payment')
        payment = Payment.__table__()
        cursor = Transaction().connection.cursor()

        is_first = dict.fromkeys([m.id for m in mandates], True)
        for sub_ids in grouped_slice(mandates):
            red_sql = reduce_ids(payment.sepa_mandate, sub_ids)
            cursor.execute(*payment.select(
                    payment.sepa_mandate, Literal(False),
                    where=red_sql
                    & (payment.sepa_mandate_sequence_type != Null)
                    & ~(  # Same as property rejected
                        (payment.state == 'failed')
                        & ((payment.sepa_return_reason_code != Null)
                            | (payment.sepa_return_reason_code != ''))
                        & (payment.sepa_return_reason_information
                            == '/RTYP/RJCT')),
                    group_by=payment.sepa_mandate))
            is_first.update(cursor)
        return is_first

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, mandates):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('requested')
    def request(cls, mandates):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('validated')
    def validate_mandate(cls, mandates):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('cancelled')
    def cancel(cls, mandates):
        # TODO must be automaticaly cancelled 13 months after last collection
        pass

    @classmethod
    def delete(cls, mandates):
        for mandate in mandates:
            if mandate.state not in ('draft', 'cancelled'):
                raise AccessError(
                    gettext('account_payment_sepa'
                        '.msg_mandate_delete_draft_cancelled',
                        mandate=mandate.rec_name))
        super(Mandate, cls).delete(mandates)


class MandateReport(CompanyReport):
    __name__ = 'account.payment.sepa.mandate'

    @classmethod
    def get_context(cls, records, header, data):
        context = super().get_context(records, header, data)
        context['gettext'] = gettext
        return context


class Message(Workflow, ModelSQL, ModelView):
    'SEPA Message'
    __name__ = 'account.payment.sepa.message'
    _states = {
        'readonly': Eval('state') != 'draft',
        }
    message = fields.Binary('Message', filename='filename',
        file_id=file_id, store_prefix=store_prefix,
        states=_states)
    message_file_id = fields.Char("Message File ID", readonly=True)
    filename = fields.Function(fields.Char('Filename'), 'get_filename')
    type = fields.Selection([
            ('in', 'IN'),
            ('out', 'OUT'),
            ], 'Type', required=True, states=_states)
    company = fields.Many2One(
        'company.company', "Company", required=True,
        states={
            'readonly': Eval('state') != 'draft',
            })
    origin = fields.Reference("Origin", selection='get_origin', states=_states)
    state = fields.Selection([
            ('draft', 'Draft'),
            ('waiting', 'Waiting'),
            ('done', 'Done'),
            ('cancelled', 'Cancelled'),
            ], "State", readonly=True, sort=False)

    @classmethod
    def __setup__(cls):
        super(Message, cls).__setup__()
        t = cls.__table__()
        cls._sql_indexes.add(
            Index(
                t, (t.state, Index.Equality()),
                where=t.state.in_(['draft', 'waiting'])))
        cls._transitions |= {
            ('draft', 'waiting'),
            ('waiting', 'done'),
            ('waiting', 'draft'),
            ('draft', 'cancelled'),
            ('waiting', 'cancelled'),
            }
        cls._buttons.update({
                'cancel': {
                    'invisible': ~Eval('state').in_(['draft', 'waiting']),
                    'depends': ['state'],
                    },
                'draft': {
                    'invisible': Eval('state') != 'waiting',
                    'depends': ['state'],
                    },
                'wait': {
                    'invisible': Eval('state') != 'draft',
                    'depends': ['state'],
                    },
                'do': {
                    'invisible': Eval('state') != 'waiting',
                    'depends': ['state'],
                    },
                })

    @classmethod
    def __register__(cls, module_name):
        transaction = Transaction()
        cursor = transaction.connection.cursor()
        table = cls.__table__()

        super(Message, cls).__register__(module_name)

        # Migration from 5.6: rename state canceled to cancelled
        cursor.execute(*table.update(
                [table.state], ['cancelled'],
                where=table.state == 'canceled'))

    @staticmethod
    def default_type():
        return 'in'

    @staticmethod
    def default_company():
        return Transaction().context.get('company')

    @staticmethod
    def default_state():
        return 'draft'

    def get_filename(self, name):
        pool = Pool()
        Group = pool.get('account.payment.group')
        if isinstance(self.origin, Group):
            return self.origin.rec_name + '.xml'

    @staticmethod
    def _get_origin():
        'Return list of Model names for origin Reference'
        return ['account.payment.group']

    @classmethod
    def get_origin(cls):
        IrModel = Pool().get('ir.model')
        get_name = IrModel.get_name
        models = cls._get_origin()
        return [(None, '')] + [(m, get_name(m)) for m in models]

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, messages):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('waiting')
    def wait(cls, messages):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('done')
    def do(cls, messages):
        for message in messages:
            if message.type == 'in':
                message.parse()
            else:
                message.send()

    @classmethod
    @ModelView.button
    @Workflow.transition('cancelled')
    def cancel(cls, messages):
        pass

    @staticmethod
    def _get_handlers():
        pool = Pool()
        Payment = pool.get('account.payment')
        return {
            'urn:iso:std:iso:20022:tech:xsd:camt.054.001.01':
            lambda f: CAMT054(f, Payment),
            'urn:iso:std:iso:20022:tech:xsd:camt.054.001.02':
            lambda f: CAMT054(f, Payment),
            'urn:iso:std:iso:20022:tech:xsd:camt.054.001.03':
            lambda f: CAMT054(f, Payment),
            'urn:iso:std:iso:20022:tech:xsd:camt.054.001.04':
            lambda f: CAMT054(f, Payment),
            }

    @staticmethod
    def get_namespace(message):
        f = BytesIO(message)
        for _, element in etree.iterparse(f, events=('start',)):
            tag = etree.QName(element)
            if tag.localname == 'Document':
                return tag.namespace

    def parse(self):
        f = BytesIO(self.message)
        namespace = self.get_namespace(self.message)
        handlers = self._get_handlers()
        if namespace not in handlers:
            raise  # TODO UserError
        handlers[namespace](f)

    def send(self):
        pass
