# 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 collections import defaultdict
from decimal import Decimal
from itertools import groupby

from sql import Literal, Null, Union
from sql.aggregate import Sum
from sql.conditionals import Coalesce
from sql.functions import CharLength
from sql.operators import Equal

from trytond import backend
from trytond.i18n import gettext
from trytond.model import (
    ChatMixin, DeactivableMixin, Exclude, Index, ModelSQL, ModelView, Workflow,
    fields)
from trytond.model.exceptions import AccessError
from trytond.modules.account.tax import TaxableMixin
from trytond.modules.currency.fields import Monetary
from trytond.modules.product import price_digits, round_price
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, Id, If
from trytond.tools import sqlite_apply_types
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard

from .exceptions import SaleValidationError, SessionValidationError


class POS(ModelSQL, ModelView):
    __name__ = 'sale.point'
    name = fields.Char("Name", required=True)
    company = fields.Many2One('company.company', "Company", required=True)
    tax_included = fields.Boolean(
        "Tax Included",
        help="Check if unit Price includes tax.")
    sequence = fields.Many2One(
        'ir.sequence.strict', "Sequence", required=True,
        domain=[
            ('company', '=', Eval('company', -1)),
            ('sequence_type', '=', Id('sale_point', 'sequence_type_sale')),
            ])

    address = fields.Many2One('party.address', "Address")

    storage_location = fields.Many2One(
        'stock.location', "Storage Location", required=True,
        domain=[('type', '=', 'storage')],
        help="The location from where goods are taken when sold.")
    return_location = fields.Many2One(
        'stock.location', "Return Location",
        domain=[('type', '=', 'storage')],
        help="The location where goods are put when returned.\n"
        "If empty the storage location is used.")
    customer_location = fields.Many2One(
        'stock.location', "To Location", required=True,
        domain=[('type', '=', 'customer')],
        help="The location where goods are put when sold.")

    journal = fields.Many2One(
        'account.journal', "Journal", required=True,
        domain=[
            ('type', '=', 'revenue'),
            ])

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

    @classmethod
    def default_tax_included(cls):
        return True

    @classmethod
    def check_modification(cls, mode, points, values=None, external=False):
        pool = Pool()
        Sale = pool.get('sale.point.sale')

        super().check_modification(
            mode, points, values=values, external=external)

        if mode == 'write':
            if 'tax_included' in values:
                if Sale.search([
                            ('point', 'in',
                                list(map(int, points))),
                            ],
                        limit=1, order=[]):
                    raise AccessError(gettext(
                            'sale_point.'
                            'msg_point_change_tax_included'))


class POSSale(Workflow, ModelSQL, ModelView, TaxableMixin, ChatMixin):
    __name__ = 'sale.point.sale'
    _rec_name = 'number'

    _states = {
        'readonly': Eval('state') != 'open',
        }

    company = fields.Many2One(
        'company.company', "Company", required=True, states=_states)
    employee = fields.Many2One(
        'company.employee', "Employee",
        domain=[
            ('company', '=', Eval('company', -1)),
            ],
        states=_states)
    point = fields.Many2One(
        'sale.point', "Point", required=True, ondelete='RESTRICT',
        states={
            'readonly': ((Eval('id', 0) > 0)
                | Bool(Eval('lines', [0]))
                | _states['readonly']),
            },
        domain=[
            ('company', '=', Eval('company', -1)),
            ])
    number = fields.Char(
        "Number", readonly=True,
        states={
            'required': Eval('state').in_(['done', 'posted']),
            })
    date = fields.Date("Date", required=True, states=_states)
    lines = fields.One2Many(
        'sale.point.sale.line', 'sale', "Lines",
        states={
            'readonly': (~Eval('point')
                | _states['readonly']),
            })
    payments = fields.One2Many(
        'sale.point.payment', 'sale', "Payments", states=_states)
    total_tax = fields.Function(Monetary(
            "Total Tax", currency='currency', digits='currency'),
        'on_change_with_total_tax')
    total = fields.Function(Monetary(
            "Total", currency='currency', digits='currency'),
        'on_change_with_total')
    amount_paid = fields.Function(Monetary(
            "Paid", currency='currency', digits='currency'),
        'on_change_with_amount_paid')
    amount_to_pay = fields.Function(Monetary(
            "To Pay", currency='currency', digits='currency'),
        'on_change_with_amount_to_pay')
    state = fields.Selection([
            ('open', "Open"),
            ('done', "Done"),
            ('posted', "Posted"),
            ('cancelled', "Cancelled"),
            ], "State", readonly=True, sort=False)

    move = fields.Many2One(
        'account.move', "Move", readonly=True,
        states={
            'invisible': ~Eval('move'),
            })

    currency = fields.Function(fields.Many2One(
            'currency.currency', "Currency"), 'on_change_with_currency')

    del _states

    @classmethod
    def __setup__(cls):
        cls.number.search_unaccented = False
        super().__setup__()
        t = cls.__table__()
        cls._sql_indexes.add(
            Index(
                t, (t.state, Index.Equality(cardinality='low')),
                where=t.state.in_(['open', 'done'])))
        cls._order = [
            ('date', 'DESC'),
            ('id', 'DESC'),
            ]
        cls._transitions |= {
            ('open', 'done'),
            ('done', 'posted'),
            ('done', 'open'),
            ('open', 'cancelled'),
            ('cancelled', 'open'),
            }
        cls._buttons.update(
            pay={
                'invisible': Eval('state') != 'open',
                'readonly': ~Eval('amount_to_pay'),
                'depends': ['amount_to_pay'],
                },
            cancel={
                'invisible': Eval('state') != 'open',
                'depends': ['state'],
                },
            open={
                'invisible': ~Eval('state').in_(['done', 'cancelled']),
                'depends': ['state'],
                },
            process={
                'invisible': Eval('state') != 'open',
                'readonly': (
                    Bool(Eval('amount_to_pay', 0))
                    | ~Eval('lines', [-1])),
                'depends': ['state', 'amount_to_pay'],
                },
            post={
                'invisible': Eval('state') != 'done',
                'depends': ['state'],
                },
            )

    @classmethod
    def order_number(cls, tables):
        table, _ = tables[None]
        return [
            ~((table.state == 'cancelled') & (table.number == Null)),
            CharLength(table.number), table.number]

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

    @classmethod
    def default_employee(cls):
        User = Pool().get('res.user')

        if Transaction().context.get('employee'):
            return Transaction().context['employee']
        else:
            user = User(Transaction().user)
            if user.employee:
                return user.employee.id

    @classmethod
    def default_date(cls):
        return Pool().get('ir.date').today()

    @classmethod
    def default_state(cls):
        return 'open'

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

    @fields.depends('lines')
    def on_change_with_total_tax(self, name=None):
        return sum(line.tax_amount for line in self.lines)

    @fields.depends('company', 'lines')
    def on_change_with_total(self, name=None):
        return sum(line.gross_amount for line in self.lines)

    @fields.depends('payments')
    def on_change_with_amount_paid(self, name=None):
        return sum(p.amount or Decimal('0') for p in self.payments)

    @fields.depends(methods=[
            'on_change_with_total', 'on_change_with_amount_paid'])
    def on_change_with_amount_to_pay(self, name=None):
        return (self.on_change_with_total()
            - self.on_change_with_amount_paid())

    @classmethod
    def copy(cls, sales, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('employee')
        default.setdefault('number')
        default.setdefault('payments')
        default.setdefault('move')
        return super().copy(sales, default=default)

    @classmethod
    def check_modification(cls, mode, sales, values=None, external=False):
        super().check_modification(
            mode, sales, values=values, external=external)
        if mode == 'delete':
            for sale in sales:
                if sale.number:
                    raise AccessError(gettext(
                            'sale_point.msg_sale_delete_numbered',
                            sale=sale.rec_name))

    @classmethod
    def validate(cls, sales):
        super().validate(sales)
        for sale in sales:
            if sale.state in {'done', 'posted'}:
                if sale.amount_to_pay:
                    raise SaleValidationError(gettext(
                            'sale_point.msg_sale_amount_to_pay_done_posted',
                            sale=sale.rec_name))
            elif sale.state == 'cancelled':
                if sale.amount_paid:
                    raise SaleValidationError(gettext(
                            'sale_point.msg_sale_amount_paid_cancelled',
                            sale=sale.rec_name))

    @classmethod
    @ModelView.button_action('sale_point.wizard_pay')
    def pay(cls, sales):
        pass

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

    @classmethod
    @ModelView.button
    @Workflow.transition('open')
    def open(cls, sales):
        pass

    @classmethod
    @ModelView.button
    def process(cls, sales):
        cls.do([sale for sale in sales if not sale.amount_to_pay])

    @classmethod
    @Workflow.transition('done')
    def do(cls, sales):
        sales_without_number = (sale for sale in sales if not sale.number)
        for (company, sequence), c_sales in groupby(
                sales_without_number,
                key=lambda s: (s.company, s.point.sequence)):
            c_sales = list(c_sales)
            with Transaction().set_context(company=company.id):
                for sale, number in zip(
                        c_sales, sequence.get_many(len(c_sales))):
                    sale.number = number
        cls.save(sales)

    @classmethod
    @ModelView.button
    @Workflow.transition('posted')
    def post(cls, sales):
        pool = Pool()
        StockMove = pool.get('stock.move')
        AccountMove = pool.get('account.move')

        stock_moves = []
        account_moves = []
        for sale in sales:
            assert not sale.move
            for line in sale.lines:
                move = line.get_stock_move()
                if move:
                    stock_moves.append(move)
            account_move = sale.get_account_move()
            sale.move = account_move
            account_moves.append(account_move)
        if stock_moves:
            StockMove.save(stock_moves)
            StockMove.do(stock_moves)
        if account_moves:
            AccountMove.save(account_moves)
            AccountMove.post(account_moves)
        cls.save(sales)

    def get_account_move(self):
        'Return account move'
        pool = Pool()
        Move = pool.get('account.move')
        Period = pool.get('account.period')

        period = Period.find(self.company, date=self.date)
        lines = []
        for line in self.lines:
            move_lines = line.get_account_move_lines()
            if move_lines:
                lines.extend(move_lines)

        move_lines = self.get_tax_move_lines()
        if move_lines:
            lines.extend(move_lines)

        for payment in self.payments:
            move_lines = payment.get_account_move_lines()
            if move_lines:
                lines.extend(move_lines)

        move = Move()
        move.journal = self.point.journal
        move.period = period
        move.date = self.date
        move.origin = self
        move.company = self.company
        move.lines = lines
        return move

    def get_tax_move_lines(self):
        "Return account move lines for taxes"
        pool = Pool()
        Line = pool.get('account.move.line')
        TaxLine = pool.get('account.tax.line')

        lines = []
        taxes = self._get_taxes().values()

        # Distribute rounding error from tax computation
        tax_amount = sum(t.amount for t in taxes)
        difference = self.total_tax - tax_amount
        if difference:
            remaining = difference
            for tax in taxes:
                if tax.amount:
                    if tax_amount:
                        ratio = tax.amount / tax_amount
                    else:
                        ratio = 1 / len(taxes)
                    value = self.currency.round(difference * ratio)
                    tax.amount += value
                    remaining -= value
            # Add remaining rounding error to the first tax
            if remaining:
                for tax in taxes:
                    if tax.amount:
                        tax.amount += remaining
                        break

        for tax in taxes:
            amount = tax.amount
            if not amount:
                continue
            line = Line()
            line.description = tax.tax.description
            if amount >= 0:
                line.debit, line.credit = Decimal(0), amount
            else:
                line.debit, line.credit = -amount, 0
            line.account = tax.account
            tax_line = TaxLine()
            tax_line.amount = amount
            tax_line.type = 'tax'
            tax_line.tax = tax.tax
            line.tax_lines = [tax_line]
            lines.append(line)
        return lines

    @property
    @fields.depends('lines')
    def taxable_lines(self):
        return sum((line.taxable_lines for line in self.lines), [])

    @property
    @fields.depends('date')
    def tax_date(self):
        return self.date or super().tax_date


class POSSaleLine(ModelSQL, ModelView, TaxableMixin):
    __name__ = 'sale.point.sale.line'

    _states = {
        'readonly': Eval('sale_state') != 'open',
        }

    sale = fields.Many2One(
        'sale.point.sale', "Sale", required=True, ondelete='CASCADE',
        states=_states)
    product = fields.Many2One(
        'product.product', "Product", required=True,
        domain=[
            If((Eval('sale_state') == 'open')
                & ~(Eval('quantity', 0) < 0),
                ('salable', '=', True),
                ()),
            ],
        context={
            'company': Eval('company'),
            },
        states=_states, depends={'company'})
    quantity = fields.Float(
        "Quantity", digits='unit', required=True, states=_states)
    unit = fields.Function(
        fields.Many2One('product.uom', "Unit"), 'on_change_with_unit')
    unit_list_price = fields.Numeric(
        "Unit List Price", digits=price_digits, required=True,
        states={
            'readonly': True,  # Allow client to sent value
            })
    unit_gross_price = fields.Numeric(
        "Unit Gross Price", digits=price_digits, required=True,
        states={
            'readonly': True,  # Allow client to sent value
            })
    unit_price = fields.Function(fields.Numeric(
            "Unit Price", digits=price_digits,
            depends={'unit_list_price', 'unit_gross_price'}),
        'on_change_with_unit_price')
    amount = fields.Function(Monetary(
            "Amount", currency='currency', digits='currency'),
        'on_change_with_amount')

    moves = fields.One2Many(
        'stock.move', 'origin', 'Moves', readonly=True,
        states={
            'invisible': ~Eval('moves', []),
            })

    sale_state = fields.Function(fields.Selection(
            'get_sale_states', "Sale State"), 'on_change_with_sale_state')
    company = fields.Function(
        fields.Many2One('company.company', "Company"),
        'on_change_with_company', searcher='search_company')
    currency = fields.Function(
        fields.Many2One('currency.currency', "Currency"),
        'on_change_with_currency')

    del _states

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('sale')

    @fields.depends('product')
    def on_change_with_unit(self, name=None):
        # TODO packaging UOM
        return self.product.sale_uom if self.product else None

    @classmethod
    def get_sale_states(cls):
        pool = Pool()
        Sale = pool.get('sale.point.sale')
        return Sale.fields_get(['state'])['state']['selection']

    @fields.depends('sale', '_parent_sale.state')
    def on_change_with_sale_state(self, name=None):
        if self.sale:
            return self.sale.state

    @fields.depends(
        'product', 'sale', '_parent_sale.point', '_parent_sale.date',
        methods=['on_change_with_unit_price', 'on_change_with_amount'])
    def on_change_product(self):
        pool = Pool()
        Tax = pool.get('account.tax')

        self.unit_gross_price = None
        self.unit_list_price = None
        if self.product and self.sale and self.sale.point:
            if (self.sale.point.tax_included
                    and self.product.gross_price_used is not None):
                self.unit_gross_price = self.product.gross_price_used
                self.unit_list_price = round_price(Tax.reverse_compute(
                        self.unit_gross_price, self.taxes,
                        date=self.sale.date))
            elif self.product.list_price_used is not None:
                self.unit_list_price = self.product.list_price_used
                taxes = Tax.compute(
                    self.taxes, self.unit_list_price, 1,
                    date=self.sale.date)
                tax_amount = sum(t['amount'] for t in taxes)
                self.unit_gross_price = (
                    self.unit_list_price + tax_amount).quantize(
                        Decimal(1)
                        / 10 ** self.__class__.unit_gross_price.digits[1])
        self.unit_price = self.on_change_with_unit_price()
        self.amount = self.on_change_with_amount()

    @fields.depends('unit_list_price', 'unit_gross_price',
        'sale', '_parent_sale.point')
    def on_change_with_unit_price(self, name=None):
        if self.sale and self.sale.point:
            if self.sale.point.tax_included:
                return self.unit_gross_price
            else:
                return self.unit_list_price
        else:
            return

    @fields.depends('currency', 'quantity', 'unit_price')
    def on_change_with_amount(self, name=None):
        amount = (Decimal(str(self.quantity or 0))
            * (self.unit_price or Decimal('0')))
        if self.currency:
            return self.currency.round(amount)
        return amount

    @fields.depends('sale', '_parent_sale.company')
    def on_change_with_company(self, name=None):
        return self.sale.company if self.sale else None

    @classmethod
    def search_company(cls, name, clause):
        return [('sale.company',) + tuple(clause[1:])]

    @fields.depends('sale', '_parent_sale.currency')
    def on_change_with_currency(self, name=None):
        return self.sale.currency if self.sale else None

    @property
    def untax_amount(self):
        amount = (Decimal(str(self.quantity or 0))
            * (self.unit_list_price or Decimal('0')))
        if getattr(self, 'currency', None):
            amount = self.currency.round(amount)
        return amount

    @property
    def tax_amount(self):
        amount = self.gross_amount - self.untax_amount
        if getattr(self, 'currency', None):
            amount = self.currency.round(amount)
        return amount

    @property
    def gross_amount(self):
        amount = (Decimal(str(self.quantity or 0))
            * (self.unit_gross_price or Decimal('0')))
        if getattr(self, 'currency', None):
            amount = self.currency.round(amount)
        return amount

    def get_rec_name(self, name):
        return '%s @ %s' % (self.product.rec_name, self.sale.rec_name)

    @classmethod
    def search_rec_name(cls, name, clause):
        _, operator, value = clause
        if operator.startswith('!') or operator.startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
            ('sale.rec_name', *clause[1:]),
            ('product.rec_name', *clause[1:]),
            ]

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

    def get_stock_move(self):
        'Return stock move'
        pool = Pool()
        Move = pool.get('stock.move')

        if self.product.type == 'service':
            return

        move = Move()
        move.quantity = abs(self.quantity)
        move.unit = self.unit
        move.product = self.product
        move.from_location = self.from_location
        move.to_location = self.to_location
        move.company = self.company
        move.unit_price = self.unit_list_price
        move.currency = self.currency
        move.effective_date = self.sale.date
        move.origin = self
        return move

    @property
    def from_location(self):
        if self.quantity >= 0:
            return self.sale.point.storage_location
        else:
            return self.sale.point.customer_location

    @property
    def to_location(self):
        if self.quantity >= 0:
            return self.sale.point.customer_location
        else:
            return self.sale.point.return_location

    def get_account_move_lines(self):
        "Return account move lines"
        pool = Pool()
        Line = pool.get('account.move.line')
        TaxLine = pool.get('account.tax.line')

        line = Line()
        if self.quantity >= 0:
            line.debit, line.credit = Decimal(0), self.untax_amount
        else:
            line.debit, line.credit = -self.untax_amount, Decimal(0)
        with Transaction().set_context(company=self.company.id):
            line.account = self.product.account_revenue_used
        line.origin = self

        tax_lines = []
        for tax in self._get_taxes().values():
            tax_line = TaxLine()
            tax_line.amount = tax.base
            tax_line.type = 'base'
            tax_line.tax = tax.tax
            tax_lines.append(tax_line)
        line.tax_lines = tax_lines
        return [line]

    @property
    def taxes(self):
        pool = Pool()
        Product = pool.get('product.product')
        with Transaction().set_context(company=self.company.id):
            return Product(self.product).customer_taxes_used

    @property
    @fields.depends(
        'taxes', 'unit_list_price', 'quantity', methods=['tax_date'])
    def taxable_lines(self):
        return [
            (self.taxes, self.unit_list_price, self.quantity, self.tax_date)]

    @property
    def tax_type(self):
        if self.quantity >= 0:
            return 'invoice'
        else:
            return 'credit_note'

    @property
    @fields.depends('sale', '_parent_sale.date')
    def tax_date(self):
        return self.sale.date or super().tax_date


class POSCashSession(Workflow, ModelSQL, ModelView):
    __name__ = 'sale.point.cash.session'

    point = fields.Many2One(
        'sale.point', "Point", required=True, ondelete='CASCADE')
    previous_session = fields.Many2One(
        'sale.point.cash.session', "Previous Session",
        readonly=True, ondelete='RESTRICT',
        domain=[
            ('point', '=', Eval('point', -1)),
            ])
    next_session = fields.One2One(
        'sale.point.cash.session.relation', 'previous', 'next', "Next Session",
        readonly=True,
        domain=[
            ('point', '=', Eval('point', -1)),
            ])
    payments = fields.One2Many(
        'sale.point.payment', 'session', "Payments", readonly=True,
        domain=[
            ('sale.point', '=', Eval('point', -1)),
            ])
    transfers = fields.One2Many(
        'sale.point.cash.transfer', 'session', "Transfers",
        domain=[
            ('point', '=', Eval('point', -1)),
            ],
        states={
            'readonly': ~Eval('point') | (Eval('state') != 'open'),
            })
    start_amount = fields.Function(
        Monetary("Start Amount", currency='currency', digits='currency'),
        'get_start_amount')
    balance = fields.Function(Monetary(
            "Balance", currency='currency', digits='currency'),
        'get_balance')
    end_amount = Monetary(
        "End Amount", currency='currency', digits='currency', required=True,
        states={
            'required': Eval('state').in_(['closed', 'posted']),
            'readonly': Eval('state') != 'open',
            })
    state = fields.Selection([
            ('open', "Open"),
            ('closed', "Closed"),
            ('posted', "Posted"),
            ], "State", readonly=True, required=True, sort=False)

    currency = fields.Function(fields.Many2One(
            'currency.currency', "Currency"),
        'on_change_with_currency')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('point_cash_session_open_unique',
                Exclude(t, (t.point, Equal),
                    where=t.state == 'open'),
                'sale_point.msg_point_cash_session_open_unique'),
            ('point_cash_session_previous_unique',
                Exclude(t, (Coalesce(t.previous_session, -t.point), Equal)),
                'sale_point.msg_cash_session_previous_unique'),
            ]
        cls._sql_indexes.add(
            Index(
                t,
                (t.state, Index.Equality(cardinality='low')),
                where=t.state == 'open'))
        cls._transitions |= {
            ('open', 'closed'),
            ('closed', 'open'),
            ('closed', 'posted'),
            }
        cls._buttons.update(
            open={
                'invisible': Eval('state') != 'closed',
                'depends': ['state'],
                },
            close={
                'pre_validate': [
                    ('end_amount', '!=', None),
                    ],
                'invisible': Eval('state') != 'open',
                'depends': ['state'],
                },
            post={
                'invisible': Eval('state') != 'closed',
                'depends': ['state'],
                },
            )

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        table_h = cls.__table_handler__(module_name)
        # Migration from 6.2: add point to previous session uniqueness.
        table_h.drop_constraint('previous_session_unique')

    @classmethod
    def default_state(cls):
        return 'open'

    @fields.depends('point')
    def on_change_point(self):
        if self.point:
            last = self.__class__.search([
                    ('point', '=', self.point.id),
                    ('next_session', '=', None),
                    ], limit=1)
            if last:
                self.previous_session, = last

    def get_start_amount(self, name):
        if self.previous_session:
            return self.previous_session.end_amount
        else:
            return Decimal(0)

    @classmethod
    def get_balance(cls, sessions, name):
        pool = Pool()
        Payment = pool.get('sale.point.payment')
        Transfer = pool.get('sale.point.cash.transfer')
        payment = Payment.__table__()
        transfer = Transfer.__table__()
        cursor = Transaction().connection.cursor()
        balances = defaultdict(Decimal)
        session_ids = [s.id for s in sessions]
        query = Union(
            payment.select(
                payment.session.as_('session'),
                Sum(payment.amount).as_('amount'),
                where=fields.SQL_OPERATORS['in'](
                    payment.session, session_ids),
                group_by=[payment.session]),
            transfer.select(
                transfer.session.as_('session'),
                Sum(transfer.amount).as_('amount'),
                where=fields.SQL_OPERATORS['in'](
                    transfer.session, session_ids),
                group_by=[transfer.session]),
            all_=True)
        query = query.select(
            query.session, Sum(query.amount).as_('amount'),
            group_by=[query.session])
        if backend.name == 'sqlite':
            sqlite_apply_types(query, [None, 'NUMERIC'])
        cursor.execute(*query)
        balances.update(cursor)
        for session in sessions:
            balances[session.id] = session.currency.round(balances[session.id])
        return balances

    @classmethod
    def default_end_amount(cls):
        return Decimal(0)

    @fields.depends('point')
    def on_change_with_currency(self, name=None):
        return self.point.company.currency if self.point else None

    @classmethod
    @ModelView.button
    @Workflow.transition('open')
    def open(cls, sessions):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('closed')
    def close(cls, sessions):
        pool = Pool()
        Lang = pool.get('ir.lang')
        for session in sessions:
            if session.end_amount - session.start_amount != session.balance:
                lang = Lang.get()
                raise SessionValidationError(gettext(
                        'sale_point.msg_cash_session_wrong_end_amount',
                        session=session.rec_name,
                        end_amount=lang.currency(
                            session.end_amount, session.currency),
                        amount=lang.currency(
                            session.start_amount + session.balance,
                            session.currency)))

    @classmethod
    def on_modification(cls, mode, sessions, field_names=None):
        super().on_modification(mode, sessions, field_names=field_names)
        if mode == 'delete':
            cls.open(sessions)

    @classmethod
    def check_modification(cls, mode, sessions, values=None, external=False):
        super().check_modification(
            mode, sessions, values=values, external=external)
        if mode == 'delete':
            for session in sessions:
                if session.state != 'open':
                    raise AccessError(gettext(
                            'sale_point.msg_cash_session_delete_open',
                            session=session.rec_name))

    @classmethod
    @ModelView.button
    @Workflow.transition('posted')
    def post(cls, sessions):
        pool = Pool()
        Transfer = pool.get('sale.point.cash.transfer')
        Transfer.post(sum((s.transfers for s in sessions), ()))

    @classmethod
    def get_current(cls, point):
        sessions = cls.search([
                ('point', '=', point.id),
                ('state', '=', 'open'),
                ], limit=1)
        if sessions:
            session, = sessions
        else:
            cls.lock()
            last = cls.search([
                    ('point', '=', point.id),
                    ('next_session', '=', None),
                    ], limit=1)
            if last:
                last, = last
            else:
                last = None
            session = cls(point=point, previous_session=last)
            session.save()
        return session


class POSCashSessionRelation(ModelSQL):
    __name__ = 'sale.point.cash.session.relation'

    previous = fields.Many2One('sale.point.cash.session', "Previous")
    next = fields.Many2One('sale.point.cash.session', "Next")

    @classmethod
    def table_query(cls):
        pool = Pool()
        Session = pool.get('sale.point.cash.session')
        session = Session.__table__()
        return session.select(
            session.id.as_('id'),
            session.previous_session.as_('previous'),
            session.id.as_('next'))


class POSPayment(ModelSQL, ModelView):
    __name__ = 'sale.point.payment'

    _states = {
        'readonly': Eval('sale_state') != 'open',
        }

    sale = fields.Many2One(
        'sale.point.sale', "Sale", required=True, ondelete='CASCADE',
        states=_states)
    method = fields.Many2One(
        'sale.point.payment.method', "Method", required=True,
        domain=[
            ('company', '=', Eval('company', -1)),
            If(Eval('amount', 0) < 0,
                ('cash', '=', True),
                ()),
            ],
        states=_states)
    amount = Monetary(
        "Amount", currency='currency', digits='currency', states=_states)
    session = fields.Many2One(
        'sale.point.cash.session', "Session",
        ondelete='RESTRICT', readonly=True,
        domain=[
            ('point', '=', Eval('point', -1)),
            ],
        states={
            'invisible': ~Eval('cash'),
            'required': Eval('cash', False),
            })

    currency = fields.Function(fields.Many2One(
            'currency.currency', "Currency"), 'on_change_with_currency')
    company = fields.Function(
        fields.Many2One('company.company', "Company"),
        'on_change_with_company', searcher='search_company')
    sale_state = fields.Function(fields.Selection(
            'get_sale_states', "Sale State"), 'on_change_with_sale_state')
    point = fields.Function(fields.Many2One(
            'sale.point', "Point"), 'on_change_with_point')
    cash = fields.Function(fields.Boolean("Cash"), 'on_change_with_cash')

    del _states

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('sale')

    @fields.depends('sale', '_parent_sale.amount_to_pay')
    def on_change_sale(self):
        if self.sale:
            self.amount = self.sale.amount_to_pay

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

    @fields.depends('sale', '_parent_sale.company')
    def on_change_with_company(self, name=None):
        return self.sale.company if self.sale else None

    @classmethod
    def search_company(cls, name, clause):
        return [('sale.company',) + tuple(clause[1:])]

    @classmethod
    def get_sale_states(cls):
        pool = Pool()
        Sale = pool.get('sale.point.sale')
        return Sale.fields_get(['state'])['state']['selection']

    @fields.depends('sale', '_parent_sale.state')
    def on_change_with_sale_state(self, name=None):
        if self.sale:
            return self.sale.state

    @fields.depends('sale', '_parent_sale.point')
    def on_change_with_point(self, name=None):
        return self.sale.point if self.sale else None

    @fields.depends('method')
    def on_change_with_cash(self, name=None):
        if self.method:
            return self.method.cash

    @classmethod
    def preprocess_values(cls, mode, values):
        pool = Pool()
        Method = pool.get('sale.point.payment.method')
        Sale = pool.get('sale.point.sale')
        Session = pool.get('sale.point.cash.session')
        values = super().preprocess_values(mode, values)
        if mode == 'create':
            if values.get('method') and values.get('sale'):
                method = Method(values['method'])
                if method.cash:
                    sale = Sale(values['sale'])
                    values['session'] = Session.get_current(sale.point)
        return values

    @classmethod
    def check_modification(cls, mode, payments, values=None, external=False):
        super().check_modification(
            mode, payments, values=values, external=external)
        if mode == 'delete':
            for payment in payments:
                if payment.sale.state != 'open':
                    raise AccessError(gettext(
                            'sale_point.msg_payment_delete_sale_open',
                            payment=payment.rec_name,
                            sale=payment.sale.rec_name))
                if payment.session and payment.session.state != 'open':
                    raise AccessError(gettext(
                            'sale_point.msg_payment_delete_session_open',
                            payment=payment.rec_name,
                            session=payment.session.rec_name))

    def get_account_move_lines(self):
        "Return account move lines"
        pool = Pool()
        Line = pool.get('account.move.line')

        line = Line()
        if self.sale.total >= 0:
            line.debit, line.credit = self.amount, Decimal(0)
        else:
            line.debit, line.credit = Decimal(0), -self.amount
        line.account = self.method.account
        line.origin = self
        return [line]


class POSPaymentMethod(DeactivableMixin, ModelSQL, ModelView):
    __name__ = 'sale.point.payment.method'
    company = fields.Many2One('company.company', "Company", required=True)
    name = fields.Char("Name", required=True, translate=True)
    account = fields.Many2One(
        'account.account', "Account", required=True,
        domain=[
            ('type', '!=', None),
            ('closed', '!=', True),
            ('company', '=', Eval('company', -1)),
            ])
    cash = fields.Boolean("Cash")

    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('cash_company_unique',
                Exclude(t, (t.company, Equal),
                    where=(t.active == Literal(True))
                    & (t.cash == Literal(True))),
                'sale_point.msg_payment_method_cash_unique'),
            ]
        cls._order.insert(0, ('name', 'ASC'))

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


class POSPay(Wizard):
    __name__ = 'sale.point.sale.pay'
    start_state = 'payment'
    payment = StateView(
        'sale.point.payment',
        'sale_point.payment_view_form_wizard', [
            Button("Cancel", 'end', 'tryton-cancel'),
            Button("OK", 'pay', 'tryton-ok', default=True),
            ])
    pay = StateTransition()

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__rpc__['create'].fresh_session = True

    def default_payment(self, fields):
        pool = Pool()
        PaymentMethod = pool.get('sale.point.payment.method')
        default = {
            'sale': self.record.id,
            'amount': self.record.amount_to_pay,
            }
        if default['amount'] < 0:
            methods = PaymentMethod.search([
                    ('company', '=', self.record.company.id),
                    ('cash', '=', True),
                    ])
            if methods:
                method, = methods
                default['method'] = method.id
        return default

    def transition_pay(self):
        self.payment.sale = self.record
        self.payment.save()
        if self.record.amount_to_pay:
            return 'payment'
        else:
            self.model.process([self.record])
            return 'end'


class POSCashTransfer(Workflow, ModelSQL, ModelView):
    __name__ = 'sale.point.cash.transfer'

    _states = {
        'readonly': Eval('state') == 'posted',
        }

    point = fields.Many2One(
        'sale.point', "Point", required=True, states=_states)
    session = fields.Many2One(
        'sale.point.cash.session', "Session", required=True,
        domain=[
            ('point', '=', Eval('point', -1)),
            If(Eval('state') == 'draft',
                ('state', '=', 'open'),
                ()),
            ],
        states={
            'required': Eval('id', -1) >= 0,
            'readonly': _states['readonly'],
            'invisible': ~Eval('point'),
            })
    type = fields.Many2One(
        'sale.point.cash.transfer.type', "Type", required=True,
        domain=[
            ('company', '=', Eval('company', -1)),
            ])
    amount = Monetary(
        "Amount", currency='currency', digits='currency', states=_states)
    date = fields.Date("Date", required=True)
    state = fields.Selection([
            ('draft', "Draft"),
            ('posted', "Posted"),
            ], "State", readonly=True, required=True, sort=False)

    move = fields.Many2One(
        'account.move', "Move", readonly=True,
        states={
            'invisible': ~Eval('move'),
            })

    company = fields.Function(fields.Many2One(
            'company.company', "Company"),
        'on_change_with_company')
    currency = fields.Function(fields.Many2One(
            'currency.currency', "Currency"),
        'on_change_with_currency')

    del _states

    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_indexes.add(
            Index(
                t,
                (t.state, Index.Equality(cardinality='low')),
                where=t.state == 'draft'))
        cls._transitions |= {
            ('draft', 'posted'),
            }
        cls._buttons.update(
            post={
                'invisible': Eval('state') != 'draft',
                'depends': ['state'],
                },
            )

    @classmethod
    def default_state(cls):
        return 'draft'

    @classmethod
    def default_date(cls):
        return Pool().get('ir.date').today()

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

    @fields.depends('point')
    def on_change_with_currency(self, name=None):
        return self.point.company.currency if self.point else None

    @classmethod
    def check_modification(cls, mode, transfers, values=None, external=False):
        super().check_modification(
            mode, transfers, values=values, external=external)
        if mode == 'delete':
            for transfer in transfers:
                if transfer.state == 'posted':
                    raise AccessError(gettext(
                            'sale_point.msg_transfer_delete_posted',
                            transfer=transfer.rec_name))
                if transfer.session.state != 'open':
                    raise AccessError(gettext(
                            'sale_point.msg_transfer_delete_session_open',
                            transfer=transfer.rec_name,
                            session=transfer.session.rec_name))

    @classmethod
    @ModelView.button
    @Workflow.transition('posted')
    def post(cls, transfers):
        pool = Pool()
        Move = pool.get('account.move')
        PaymentMethod = pool.get('sale.point.payment.method')

        methods = PaymentMethod.search([('cash', '=', True)])
        company2account = {m.company: m.account for m in methods}

        moves = []
        for transfer in transfers:
            assert not transfer.move
            move = transfer.get_move(
                account=company2account.get(transfer.company))
            transfer.move = move
            moves.append(move)
        Move.save(moves)
        Move.post(moves)
        cls.save(transfers)

    def get_move(self, account):
        pool = Pool()
        Line = pool.get('account.move.line')
        Move = pool.get('account.move')
        Period = pool.get('account.period')

        period = Period.find(self.company, date=self.date)
        line = Line()
        if self.amount >= 0:
            line.debit, line.credit = self.amount, Decimal(0)
        else:
            line.debit, line.credit = Decimal(0), -self.amount
        line.account = account
        counterpart = Line()
        counterpart.debit, counterpart.credit = line.credit, line.debit
        counterpart.account = self.type.account

        move = Move()
        move.journal = self.type.journal
        move.period = period
        move.date = self.date
        move.origin = self
        move.company = self.company
        move.lines = [line, counterpart]
        return move

    @classmethod
    def preprocess_values(cls, mode, values):
        pool = Pool()
        Point = pool.get('sale.point')
        Session = pool.get('sale.point.cash.session')
        values = super().preprocess_values(mode, values)
        if mode == 'create':
            if values.get('point') is not None and not values.get('session'):
                point = Point(values['point'])
                values['session'] = Session.get_current(point)
        return values


class POSCashTransferType(ModelSQL, ModelView):
    __name__ = 'sale.point.cash.transfer.type'

    company = fields.Many2One('company.company', "Company", required=True)
    name = fields.Char("Name", required=True, translate=True)
    journal = fields.Many2One(
        'account.journal', "Journal", required=True,
        context={
            'company': Eval('company', -1),
            },
        depends={'company'})
    account = fields.Many2One(
        'account.account', "Account", required=True,
        domain=[
            ('type', '!=', None),
            ('closed', '!=', True),
            ('company', '=', Eval('company', -1)),
            ])

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

# TODO: wizard to create an invoice from sale
