#This file is part of Tryton.  The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from dateutil.relativedelta import relativedelta
from trytond.model import ModelView, ModelSQL, fields
from trytond.wizard import Wizard, StateView, StateTransition, StateAction, \
    Button
from trytond.tools import datetime_strftime
from trytond.pyson import Eval, If, PYSONEncoder
from trytond.transaction import Transaction
from trytond.pool import Pool

__all__ = ['FiscalYear',
    'BalanceNonDeferralStart', 'BalanceNonDeferral',
    'CloseFiscalYearStart', 'CloseFiscalYear']

STATES = {
    'readonly': Eval('state') == 'close',
}
DEPENDS = ['state']


class FiscalYear(ModelSQL, ModelView):
    'Fiscal Year'
    __name__ = 'account.fiscalyear'
    name = fields.Char('Name', size=None, required=True, depends=DEPENDS)
    code = fields.Char('Code', size=None)
    start_date = fields.Date('Starting Date', required=True, states=STATES,
        domain=[('start_date', '<=', Eval('end_date', None))],
        depends=DEPENDS + ['end_date'])
    end_date = fields.Date('Ending Date', required=True, states=STATES,
        domain=[('end_date', '>=', Eval('start_date', None))],
        depends=DEPENDS + ['start_date'])
    periods = fields.One2Many('account.period', 'fiscalyear', 'Periods',
            states=STATES, depends=DEPENDS)
    state = fields.Selection([
        ('open', 'Open'),
        ('close', 'Close'),
        ], 'State', readonly=True, required=True)
    post_move_sequence = fields.Many2One('ir.sequence', 'Post Move Sequence',
            required=True, domain=[('code', '=', 'account.move'),
                ['OR',
                    ('company', '=', Eval('company')),
                    ('company', '=', None)
                ]],
            context={
                'code': 'account.move',
                'company': Eval('company'),
            },
            depends=['company'])
    company = fields.Many2One('company.company', 'Company', required=True,
        domain=[
            ('id', If(Eval('context', {}).contains('company'), '=', '!='),
                Eval('context', {}).get('company', -1)),
            ], select=True)

    @classmethod
    def __setup__(cls):
        super(FiscalYear, cls).__setup__()
        cls._order.insert(0, ('start_date', 'ASC'))
        cls._error_messages.update({
                'change_post_move_sequence': ('You can not change the post '
                    'move sequence in fiscal year "%s".'),
                'no_fiscalyear_date': 'No fiscal year defined for "%s".',
                'fiscalyear_overlaps': ('Fiscal year "%(first)s" and '
                    '"%(second)s" overlap.'),
                'different_post_move_sequence': ('Fiscal year "%(first)s" and '
                    '"%(second)s" have the same post move sequence.'),
                'account_balance_not_zero': ('The balance of the account "%s" '
                    'must be zero.'),
                'close_error': ('You can not close fiscal year "%s" until you '
                    'close all previous fiscal years.'),
                'reopen_error': ('You can not reopen fiscal year "%s" until '
                    'you reopen all later fiscal years.'),
                })
        cls._buttons.update({
                'create_period': {
                    'invisible': ((Eval('state') != 'open')
                        | Eval('periods', [0])),
                    },
                'create_period_3': {
                    'invisible': ((Eval('state') != 'open')
                        | Eval('periods', [0])),
                    },
                'close': {
                    'invisible': Eval('state') != 'open',
                    },
                'reopen': {
                    'invisible': Eval('state') != 'close',
                    },
                })

    @staticmethod
    def default_state():
        return 'open'

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

    @classmethod
    def validate(cls, years):
        super(FiscalYear, cls).validate(years)
        for year in years:
            year.check_dates()
            year.check_post_move_sequence()

    def check_dates(self):
        cursor = Transaction().cursor
        table = self.__table__()
        cursor.execute(*table.select(table.id,
                where=(((table.start_date <= self.start_date)
                        & (table.end_date >= self.start_date))
                    | ((table.start_date <= self.end_date)
                        & (table.end_date >= self.end_date))
                    | ((table.start_date >= self.start_date)
                        & (table.end_date <= self.end_date)))
                & (table.company == self.company.id)
                & (table.id != self.id)))
        second_id = cursor.fetchone()
        if second_id:
            second = self.__class__(second_id[0])
            self.raise_user_error('fiscalyear_overlaps', {
                    'first': self.rec_name,
                    'second': second.rec_name,
                    })

    def check_post_move_sequence(self):
        years = self.search([
                ('post_move_sequence', '=', self.post_move_sequence.id),
                ('id', '!=', self.id),
                ])
        if years:
            self.raise_user_error('different_post_move_sequence', {
                    'first': self.rec_name,
                    'second': years[0].rec_name,
                    })

    @classmethod
    def write(cls, *args):
        actions = iter(args)
        args = []
        for fiscalyears, values in zip(actions, actions):
            if values.get('post_move_sequence'):
                for fiscalyear in fiscalyears:
                    if (fiscalyear.post_move_sequence
                            and fiscalyear.post_move_sequence.id !=
                            values['post_move_sequence']):
                        cls.raise_user_error('change_post_move_sequence', (
                                fiscalyear.rec_name,))
            values = values.copy()
            if 'periods' in values:
                operator = ['delete', 'remove', 'create', 'write', 'add']
                values['periods'].sort(lambda x, y:
                    cmp(operator.index(x[0]), operator.index(y[0])))
            args.extend((fiscalyears, values))
        super(FiscalYear, cls).write(*args)

    @classmethod
    def delete(cls, fiscalyears):
        Period = Pool().get('account.period')
        Period.delete([p for f in fiscalyears for p in f.periods])
        super(FiscalYear, cls).delete(fiscalyears)

    @classmethod
    @ModelView.button
    def create_period(cls, fiscalyears, interval=1):
        '''
        Create periods for the fiscal years with month interval
        '''
        Period = Pool().get('account.period')
        to_create = []
        for fiscalyear in fiscalyears:
            period_start_date = fiscalyear.start_date
            while period_start_date < fiscalyear.end_date:
                period_end_date = period_start_date + \
                    relativedelta(months=interval - 1) + \
                    relativedelta(day=31)
                if period_end_date > fiscalyear.end_date:
                    period_end_date = fiscalyear.end_date
                name = datetime_strftime(period_start_date, '%Y-%m')
                if name != datetime_strftime(period_end_date, '%Y-%m'):
                    name += ' - ' + datetime_strftime(period_end_date, '%Y-%m')
                to_create.append({
                    'name': name,
                    'start_date': period_start_date,
                    'end_date': period_end_date,
                    'fiscalyear': fiscalyear.id,
                    'type': 'standard',
                    })
                period_start_date = period_end_date + relativedelta(days=1)
        if to_create:
            Period.create(to_create)

    @classmethod
    @ModelView.button
    def create_period_3(cls, fiscalyears):
        '''
        Create periods for the fiscal years with 3 months interval
        '''
        cls.create_period(fiscalyears, interval=3)

    @classmethod
    def find(cls, company_id, date=None, exception=True):
        '''
        Return the fiscal year for the company_id
            at the date or the current date.
        If exception is set the function will raise an exception
            if any fiscal year is found.
        '''
        pool = Pool()
        Lang = pool.get('ir.lang')
        Date = pool.get('ir.date')

        if not date:
            date = Date.today()
        fiscalyears = cls.search([
            ('start_date', '<=', date),
            ('end_date', '>=', date),
            ('company', '=', company_id),
            ], order=[('start_date', 'DESC')], limit=1)
        if not fiscalyears:
            if exception:
                language = Transaction().language
                languages = Lang.search([('code', '=', language)])
                if not languages:
                    languages = Lang.search([('code', '=', 'en_US')])
                language, = languages
                formatted = Lang.strftime(date, language.code, language.date)
                cls.raise_user_error('no_fiscalyear_date', (formatted,))
            else:
                return None
        return fiscalyears[0].id

    def _process_account(self, account):
        '''
        Process account for a fiscal year closed
        '''
        Currency = Pool().get('currency.currency')
        Deferral = Pool().get('account.account.deferral')

        if account.kind == 'view':
            return
        if not account.deferral:
            if not Currency.is_zero(self.company.currency, account.balance):
                self.raise_user_error('account_balance_not_zero',
                        error_args=(account.rec_name,))
        else:
            Deferral.create([{
                        'account': account.id,
                        'fiscalyear': self.id,
                        'debit': account.debit,
                        'credit': account.credit,
                        }])

    @classmethod
    @ModelView.button
    def close(cls, fiscalyears):
        '''
        Close a fiscal year
        '''
        pool = Pool()
        Period = pool.get('account.period')
        Account = pool.get('account.account')

        for fiscalyear in fiscalyears:
            if cls.search([
                        ('end_date', '<=', fiscalyear.start_date),
                        ('state', '=', 'open'),
                        ('company', '=', fiscalyear.company.id),
                        ]):
                cls.raise_user_error('close_error', (fiscalyear.rec_name,))

            #First close the fiscalyear to be sure
            #it will not have new period created between.
            cls.write([fiscalyear], {
                'state': 'close',
                })
            periods = Period.search([
                    ('fiscalyear', '=', fiscalyear.id),
                    ])
            Period.close(periods)

            with Transaction().set_context(fiscalyear=fiscalyear.id,
                    date=None, cumulate=True):
                accounts = Account.search([
                        ('company', '=', fiscalyear.company.id),
                        ])
            for account in accounts:
                fiscalyear._process_account(account)

    @classmethod
    @ModelView.button
    def reopen(cls, fiscalyears):
        '''
        Re-open a fiscal year
        '''
        Deferral = Pool().get('account.account.deferral')

        for fiscalyear in fiscalyears:
            if cls.search([
                        ('start_date', '>=', fiscalyear.end_date),
                        ('state', '=', 'close'),
                        ('company', '=', fiscalyear.company.id),
                        ]):
                cls.raise_user_error('reopen_error')

            deferrals = Deferral.search([
                ('fiscalyear', '=', fiscalyear.id),
                ])
            Deferral.delete(deferrals)

            cls.write([fiscalyear], {
                'state': 'open',
                })

    @classmethod
    def search_rec_name(cls, name, clause):
        return ['OR',
            ('code',) + tuple(clause[1:]),
            (cls._rec_name,) + tuple(clause[1:]),
            ]


class BalanceNonDeferralStart(ModelView):
    'Balance Non-Deferral'
    __name__ = 'account.fiscalyear.balance_non_deferral.start'
    fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year',
        required=True, domain=[('state', '=', 'open')])
    journal = fields.Many2One('account.journal', 'Journal', required=True,
        domain=[
            ('type', '=', 'situation'),
            ])
    period = fields.Many2One('account.period', 'Period', required=True,
        domain=[
            ('fiscalyear', '=', Eval('fiscalyear')),
            ('type', '=', 'adjustment'),
            ],
        depends=['fiscalyear'])
    credit_account = fields.Many2One('account.account', 'Credit Account',
        required=True,
        domain=[
            ('kind', '!=', 'view'),
            ('company', '=', Eval('context', {}).get('company', -1)),
            ('deferral', '=', True),
            ])
    debit_account = fields.Many2One('account.account', 'Debit Account',
        required=True,
        domain=[
            ('kind', '!=', 'view'),
            ('company', '=', Eval('context', {}).get('company', -1)),
            ('deferral', '=', True),
            ])


class BalanceNonDeferral(Wizard):
    'Balance Non-Deferral'
    __name__ = 'account.fiscalyear.balance_non_deferral'
    start = StateView('account.fiscalyear.balance_non_deferral.start',
        'account.fiscalyear_balance_non_deferral_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Ok', 'balance', 'tryton-ok', default=True),
            ])
    balance = StateAction('account.act_move_line_form')

    def get_move_line(self, account):
        pool = Pool()
        Line = pool.get('account.move.line')
        if account.company.currency.is_zero(account.balance):
            return
        line = Line()
        line.account = account
        if account.balance >= 0:
            line.credit = abs(account.balance)
            line.debit = 0
        else:
            line.credit = 0
            line.debit = abs(account.balance)
        return line

    def get_counterpart_line(self, amount):
        pool = Pool()
        Line = pool.get('account.move.line')
        if self.start.fiscalyear.company.currency.is_zero(amount):
            return
        line = Line()
        if amount >= 0:
            line.credit = abs(amount)
            line.debit = 0
            line.account = self.start.credit_account
        else:
            line.credit = 0
            line.debit = abs(amount)
            line.account = self.start.debit_account
        return line

    def create_move(self):
        pool = Pool()
        Account = pool.get('account.account')
        Move = pool.get('account.move')

        with Transaction().set_context(fiscalyear=self.start.fiscalyear.id,
                date=None, cumulate=False):
            accounts = Account.search([
                    ('company', '=', self.start.fiscalyear.company.id),
                    ('deferral', '=', False),
                    ('kind', '!=', 'view'),
                    ])
        lines = []
        for account in accounts:
            line = self.get_move_line(account)
            if line:
                lines.append(line)
        if not lines:
            return
        amount = sum(l.debit - l.credit for l in lines)
        counter_part_line = self.get_counterpart_line(amount)
        if counter_part_line:
            lines.append(counter_part_line)

        move = Move()
        move.period = self.start.period
        move.journal = self.start.journal
        move.date = self.start.period.start_date
        move.origin = self.start.fiscalyear
        move.lines = lines
        move.save()
        return move

    def do_balance(self, action):
        self.create_move()
        action['pyson_domain'] = PYSONEncoder().encode([
                ('origin', '=', str(self.start.fiscalyear)),
                ])
        return action, {}


class CloseFiscalYearStart(ModelView):
    'Close Fiscal Year'
    __name__ = 'account.fiscalyear.close.start'
    close_fiscalyear = fields.Many2One('account.fiscalyear',
            'Fiscal Year to close', required=True,
            domain=[('state', '!=', 'close')])


class CloseFiscalYear(Wizard):
    'Close Fiscal Year'
    __name__ = 'account.fiscalyear.close'
    start = StateView('account.fiscalyear.close.start',
        'account.fiscalyear_close_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Close', 'close', 'tryton-ok', default=True),
            ])
    close = StateTransition()

    def transition_close(self):
        Fiscalyear = Pool().get('account.fiscalyear')
        Fiscalyear.close([self.start.close_fiscalyear])
        return 'end'
