# 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 decimal import Decimal
from itertools import chain

from sql.functions import CharLength

from trytond.i18n import gettext
from trytond.model import (
    Index, ModelSQL, ModelView, Workflow, fields, sequence_ordered)
from trytond.model.exceptions import AccessError, RequiredValidationError
from trytond.modules.company.model import (
    employee_field, reset_employee, set_employee)
from trytond.modules.currency.fields import Monetary
from trytond.modules.product import price_digits, round_price
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
from trytond.tools import firstline, grouped_slice
from trytond.transaction import Transaction
from trytond.wizard import Wizard


class Configuration(metaclass=PoolMeta):
    __name__ = 'purchase.configuration'
    purchase_requisition_sequence = fields.MultiValue(fields.Many2One(
            'ir.sequence', "Purchase Requisition Sequence", required=True,
            domain=[
                ('company', 'in',
                    [Eval('context', {}).get('company', -1), None]),
                ('sequence_type', '=',
                    Id('purchase_requisition',
                        'sequence_type_purchase_requisition')),
                ]))

    @classmethod
    def multivalue_model(cls, field):
        pool = Pool()
        if field == 'purchase_requisition_sequence':
            return pool.get('purchase.configuration.sequence')
        return super(Configuration, cls).multivalue_model(field)

    @classmethod
    def default_purchase_requisition_sequence(cls, **pattern):
        return cls.multivalue_model('purchase_requisition_sequence'
            ).default_purchase_requisition_sequence()


class ConfigurationSequence(metaclass=PoolMeta):
    __name__ = 'purchase.configuration.sequence'
    purchase_requisition_sequence = fields.Many2One(
        'ir.sequence', "Purchase Requisition Sequence", required=True,
        domain=[
            ('company', 'in', [Eval('company', -1), None]),
            ('sequence_type', '=',
                Id('purchase_requisition',
                    'sequence_type_purchase_requisition')),
            ])

    @classmethod
    def default_purchase_requisition_sequence(cls):
        pool = Pool()
        ModelData = pool.get('ir.model.data')
        try:
            return ModelData.get_id(
                'purchase_requisition', 'sequence_purchase_requisition')
        except KeyError:
            return None


class PurchaseRequisition(Workflow, ModelSQL, ModelView):
    "Purchase Requisition"
    __name__ = 'purchase.requisition'
    _rec_name = 'number'
    _states = {
        'readonly': Eval('state') != 'draft',
        }

    company = fields.Many2One(
        'company.company', "Company", required=True,
        states={
            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
            })
    number = fields.Char('Number', readonly=True)
    description = fields.Char('Description', states=_states)
    employee = fields.Many2One(
        'company.employee', 'Employee', required=True, states=_states)
    supply_date = fields.Date(
        'Supply Date',
        states={
            'required': ~Eval('state').in_(['draft', 'cancelled']),
            'readonly': _states['readonly'],
            })
    warehouse = fields.Many2One(
        'stock.location', 'Warehouse',
        domain=[
            ('type', '=', 'warehouse'),
            ],
        states=_states)
    currency = fields.Many2One(
        'currency.currency', 'Currency',
        states={
            'readonly': (_states['readonly']
                | (Eval('lines', [0]) & Eval('currency'))),
            })
    total_amount = fields.Function(
        Monetary("Total", currency='currency', digits='currency'),
        'get_amount')
    total_amount_cache = Monetary(
        "Total Cache", currency='currency', digits='currency')
    lines = fields.One2Many(
        'purchase.requisition.line', 'requisition', 'Lines',
        states=_states)

    approved_by = employee_field(
        "Approved By", states=['approved', 'processing', 'done', 'cancelled'])
    rejected_by = employee_field(
        "Rejected By", states=['rejected', 'processing', 'done', 'cancelled'])
    state = fields.Selection([
            ('draft', "Draft"),
            ('waiting', "Waiting"),
            ('rejected', "Rejected"),
            ('approved', "Approved"),
            ('processing', "Processing"),
            ('done', "Done"),
            ('cancelled', "Cancelled"),
            ], "State", readonly=True, required=True, sort=False)

    del _states

    @classmethod
    def __setup__(cls):
        cls.number.search_unaccented = False
        super(PurchaseRequisition, cls).__setup__()
        t = cls.__table__()
        cls._sql_indexes.add(
            Index(
                t,
                (t.state, Index.Equality()),
                where=t.state.in_([
                        'draft', 'waiting', 'approved', 'processing'])))
        cls._transitions |= set((
                ('cancelled', 'draft'),
                ('rejected', 'draft'),
                ('draft', 'cancelled'),
                ('draft', 'waiting'),
                ('waiting', 'draft'),
                ('waiting', 'rejected'),
                ('waiting', 'approved'),
                ('approved', 'processing'),
                ('approved', 'draft'),
                ('processing', 'done'),
                ('done', 'processing'),
                ))
        cls._buttons.update({
                'cancel': {
                    'invisible': Eval('state') != 'draft',
                    'depends': ['state'],
                    },
                'draft': {
                    'invisible': ~Eval('state').in_(
                        ['cancelled', 'waiting', 'approved', 'rejected']),
                    'icon': If(Eval('state').in_(['cancelled', 'rejected']),
                        'tryton-undo',
                        'tryton-back'),
                    'depends': ['state'],
                    },
                'wait': {
                    'pre_validate': [('supply_date', '!=', None)],
                    'invisible': ((Eval('state') != 'draft')
                        | ~Eval('lines', [])),
                    'readonly': ~Eval('lines', []),
                    'depends': ['state'],
                    },
                'approve': {
                    'invisible': Eval('state') != 'waiting',
                    'depends': ['state'],
                    },
                'process': {
                    'invisible': ~Eval('state').in_(
                        ['approved', 'processing']),
                    'icon': If(Eval('state') == 'approved',
                        'tryton-forward', 'tryton-refresh'),
                    'depends': ['state'],
                    },
                'reject': {
                    'invisible': Eval('state') != 'waiting',
                    'depends': ['state'],
                    },
                })
        # The states where amounts are cached
        cls._states_cached = ['approved', 'done', 'rejected',
            'processing', 'cancelled']

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

        super().__register__(module_name)

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

    @classmethod
    def order_number(cls, tables):
        table, _ = tables[None]
        return [CharLength(table.number), table.number]

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

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

    @classmethod
    def default_employee(cls):
        return Transaction().context.get('employee')

    @classmethod
    def default_warehouse(cls):
        Location = Pool().get('stock.location')
        return Location.get_default_warehouse()

    @classmethod
    def default_currency(cls):
        Company = Pool().get('company.company')
        company = Transaction().context.get('company')
        if company is not None and company >= 0:
            return Company(company).currency.id

    @fields.depends('lines', 'currency')
    def on_change_with_total_amount(self):
        self.total_amount = Decimal(0)
        if self.lines:
            for line in self.lines:
                self.total_amount += getattr(line, 'amount', None) or 0
        if self.currency:
            self.total_amount = self.currency.round(self.total_amount)
        return self.total_amount

    @classmethod
    def store_cache(cls, requisitions):
        for requisition in requisitions:
            requisition.total_amount_cache = requisition.total_amount
        cls.save(requisitions)

    @classmethod
    def get_amount(cls, requisitions, name):
        total_amount = {}

        # Sort cached first and re-instantiate to optimize cache management
        requisitions = sorted(requisitions,
            key=lambda r: r.state in cls._states_cached, reverse=True)
        requisitions = cls.browse(requisitions)
        for requisition in requisitions:
            if (requisition.state in cls._states_cached
                    and requisition.total_amount_cache is not None):
                total_amount[requisition.id] = requisition.total_amount_cache
            else:
                total_amount[requisition.id] = (
                    requisition.on_change_with_total_amount())
        return total_amount

    @classmethod
    def create_requests(cls, requisitions):
        pool = Pool()
        Request = pool.get('purchase.request')
        requests = []
        for requisition in requisitions:
            for line in requisition.lines:
                request = line.compute_request()
                if request:
                    requests.append(request)
        if requests:
            Request.save(requests)

    @classmethod
    def view_attributes(cls):
        return super().view_attributes() + [
            ('/tree', 'visual',
                If(Eval('state') == 'cancelled', 'muted', '')),
            ]

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

        config = Config(1)
        vlist = [v.copy() for v in vlist]
        default_company = cls.default_company()
        for values in vlist:
            if values.get('number') is None:
                values['number'] = config.get_multivalue(
                    'purchase_requisition_sequence',
                    company=values.get('company', default_company)).get()
        return super(PurchaseRequisition, cls).create(vlist)

    @classmethod
    def delete(cls, requisitions):
        # Cancel before delete
        cls.cancel(requisitions)
        for requisition in requisitions:
            if requisition.state != 'cancelled':
                raise AccessError(
                    gettext('purchase_requisition.msg_delete_cancel',
                        requisition=requisition.rec_name))
        super(PurchaseRequisition, cls).delete(requisitions)

    def check_for_waiting(self):
        if not self.warehouse:
            for line in self.lines:
                if line.product and line.product.type in {'goods', 'assets'}:
                    raise RequiredValidationError(
                        gettext('purchase_requisition.msg_warehouse_required',
                            requisition=self.rec_name))

    @classmethod
    def copy(cls, requisitions, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('number', None)
        default.setdefault('supply_date', None)
        default.setdefault('approved_by')
        default.setdefault('rejected_by')
        default.setdefault('total_amount_cache')
        return super(PurchaseRequisition, cls).copy(
            requisitions, default=default)

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

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    @reset_employee('approved_by', 'rejected_by')
    def draft(cls, requisitions):
        cls.write(requisitions, {
                'total_amount_cache': None,
                })

    @classmethod
    @ModelView.button
    @Workflow.transition('waiting')
    def wait(cls, requisitions):
        for requisition in requisitions:
            requisition.check_for_waiting()

    @classmethod
    @ModelView.button
    @Workflow.transition('rejected')
    @set_employee('rejected_by')
    def reject(cls, requisitions):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('approved')
    @set_employee('approved_by')
    def approve(cls, requisitions):
        pool = Pool()
        Configuration = pool.get('purchase.configuration')
        transaction = Transaction()
        context = transaction.context
        cls.store_cache(requisitions)
        config = Configuration(1)
        with transaction.set_context(
                queue_scheduled_at=config.purchase_process_after,
                queue_batch=context.get('queue_batch', True)):
            cls.__queue__.process(requisitions)

    @classmethod
    @Workflow.transition('processing')
    def proceed(cls, requisitions):
        pass

    @classmethod
    @Workflow.transition('done')
    def do(cls, requisitions):
        pass

    @classmethod
    @ModelView.button
    def process(cls, requisitions):
        done = []
        process = []
        requisitions = [r for r in requisitions
            if r.state in {'approved', 'processing', 'done'}]
        cls.create_requests(requisitions)
        for requisition in requisitions:
            if requisition.is_done():
                if requisition.state != 'done':
                    done.append(requisition)
            elif requisition.state != 'processing':
                process.append(requisition)
        if process:
            cls.proceed(process)
        if done:
            cls.do(done)

    def is_done(self):
        return all(
            r.purchase and r.purchase.state in {'cancelled', 'confirmed'}
            for l in self.lines for r in l.purchase_requests)


class PurchaseRequisitionLine(sequence_ordered(), ModelSQL, ModelView):
    "Purchase Requisition Line"
    __name__ = 'purchase.requisition.line'
    _states = {
        'readonly': Eval('purchase_requisition_state') != 'draft',
        }

    requisition = fields.Many2One(
        'purchase.requisition', 'Requisition',
        ondelete='CASCADE', required=True)
    supplier = fields.Many2One('party.party', 'Supplier', states=_states)
    product = fields.Many2One(
        'product.product', 'Product',
        ondelete='RESTRICT',
        domain=[
            If((Eval('purchase_requisition_state') == 'draft')
                & ~(Eval('quantity', 0) < 0),
                ('purchasable', '=', True),
                ()),
            ],
        states=_states)
    product_uom_category = fields.Function(
        fields.Many2One(
            'product.uom.category', "Product UoM Category",
            help="The category of Unit of Measure for the product."),
        'on_change_with_product_uom_category')
    description = fields.Text("Description", states=_states)
    summary = fields.Function(fields.Char('Summary'), 'on_change_with_summary')
    quantity = fields.Float(
        "Quantity", digits='unit', required=True, states=_states)
    unit = fields.Many2One(
        'product.uom', 'Unit', ondelete='RESTRICT',
        states={
            'required': Bool(Eval('product')),
            'readonly': _states['readonly'],
            })
    unit_price = Monetary(
        'Unit Price', currency='currency', digits=price_digits, states=_states)
    currency = fields.Function(fields.Many2One('currency.currency',
        'Currency'), 'on_change_with_currency')
    amount = fields.Function(Monetary(
            "Amount", currency='currency', digits='currency'),
        'on_change_with_amount')
    purchase_requests = fields.One2Many(
        'purchase.request', 'origin', 'Purchase Request', readonly=True)
    purchase_requisition_state = fields.Function(fields.Selection(
            'get_purchase_requisition_states', "Purchase Requisition State"),
        'on_change_with_purchase_requisition_state')

    del _states

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('requisition')
        unit_categories = cls._unit_categories()
        cls.unit.domain = [
            If(Bool(Eval('product_uom_category')),
                ('category', 'in', [Eval(c) for c in unit_categories]),
                ('category', '!=', -1)),
            ]

    @classmethod
    def _unit_categories(cls):
        return ['product_uom_category']

    @fields.depends('product')
    def on_change_with_product_uom_category(self, name=None):
        return self.product.default_uom_category if self.product else None

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

    @classmethod
    def get_purchase_requisition_states(cls):
        pool = Pool()
        Requisition = pool.get('purchase.requisition')
        return Requisition.fields_get(['state'])['state']['selection']

    @fields.depends('requisition', '_parent_requisition.state')
    def on_change_with_purchase_requisition_state(self, name=None):
        if self.requisition:
            return self.requisition.state

    @fields.depends('product', 'unit', 'quantity', 'supplier')
    def on_change_product(self):
        if not self.product:
            return

        category = self.product.purchase_uom.category
        if not self.unit or self.unit.category != category:
            self.unit = self.product.purchase_uom

    @fields.depends('description')
    def on_change_with_summary(self, name=None):
        return firstline(self.description or '')

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

    def get_rec_name(self, name):
        pool = Pool()
        Lang = pool.get('ir.lang')
        if self.product:
            lang = Lang.get()
            return (lang.format_number_symbol(
                    self.quantity or 0, self.unit, digits=self.unit.digits)
                + ' %s @ %s' % (
                    self.product.rec_name, self.requisition.rec_name))
        else:
            return self.requisition.rec_name

    def _get_purchase_request_product_supplier_pattern(self):
        pattern = {
            'company': self.requisition.company.id,
            }
        if self.supplier:
            pattern['party'] = self.supplier.id
        return pattern

    @property
    def request_unit(self):
        unit = self.unit
        if (self.product
                and self.product.purchase_uom.category == self.unit.category):
            unit = self.product.purchase_uom
        return unit

    @property
    def request_quantity(self):
        pool = Pool()
        Uom = pool.get('product.uom')
        quantity = self.quantity
        request_unit = self.request_unit
        if (self.product
                and request_unit
                and request_unit.category == self.unit.category):
            quantity = Uom.compute_qty(
                self.unit, self.quantity, request_unit, round=True)
        return quantity

    @property
    def request_unit_price(self):
        return self.unit_price

    def compute_request(self):
        """
        Return the value of the purchase request which will answer to
        the needed quantity at the given date.
        """
        pool = Pool()
        Uom = pool.get('product.uom')
        Request = pool.get('purchase.request')

        if self.purchase_requests:
            return

        supply_date = self.requisition.supply_date
        supplier = None
        purchase_date = None

        if self.product:
            supplier, purchase_date = Request.find_best_supplier(
                self.product, supply_date,
                **self._get_purchase_request_product_supplier_pattern())
        elif self.supplier:
            lead_time = self.supplier.get_multivalue(
                'supplier_lead_time', company=self.requisition.company.id)
            if lead_time is not None:
                purchase_date = supply_date - lead_time

        unit = self.request_unit
        quantity = self.request_quantity
        if (self.product
                and self.product.purchase_uom.category == self.unit.category):
            unit = self.product.purchase_uom
            quantity = Uom.compute_qty(
                self.unit, self.quantity, unit, round=True)

        return Request(
            product=self.product,
            description=self.description,
            party=supplier or self.supplier,
            quantity=quantity,
            unit=unit,
            computed_quantity=self.quantity,
            computed_unit=self.unit,
            purchase_date=purchase_date,
            supply_date=supply_date,
            company=self.requisition.company,
            warehouse=self.requisition.warehouse,
            origin=self,
            )

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


class PurchaseRequest(metaclass=PoolMeta):
    __name__ = 'purchase.request'

    @classmethod
    def _get_origin(cls):
        return (super(PurchaseRequest, cls)._get_origin()
            | {'purchase.requisition.line'})

    @property
    def currency(self):
        pool = Pool()
        RequisitionLine = pool.get('purchase.requisition.line')
        currency = super(PurchaseRequest, self).currency
        if (isinstance(self.origin, RequisitionLine)
                and self.origin.requisition.currency):
            return self.origin.requisition.currency
        return currency


class HandlePurchaseCancellationException(metaclass=PoolMeta):
    __name__ = 'purchase.request.handle.purchase.cancellation'

    def transition_reset(self):
        pool = Pool()
        Requisition = pool.get('purchase.requisition')
        RequisitionLine = pool.get('purchase.requisition.line')

        state = super(
            HandlePurchaseCancellationException, self).transition_reset()

        requests = self.records
        requisition_ids = list({r.origin.requisition.id for r in requests
                if isinstance(r.origin, RequisitionLine)})
        Requisition.process(Requisition.browse(requisition_ids))
        return state


class CreatePurchase(Wizard):
    'Create Purchase'
    __name__ = 'purchase.request.create_purchase'

    def _group_purchase_line_key(self, request):
        pool = Pool()
        RequisitionLine = pool.get('purchase.requisition.line')
        key = super(CreatePurchase, self)._group_purchase_line_key(request)
        if isinstance(request.origin, RequisitionLine):
            unit_price = request.origin.request_unit_price
            if unit_price:
                key += (('unit_price', unit_price),)
        return key

    @classmethod
    def compute_purchase_line(cls, key, requests, purchase):
        pool = Pool()
        RequisitionLine = pool.get('purchase.requisition.line')
        Uom = pool.get('product.uom')

        line = super().compute_purchase_line(key, requests, purchase)

        key_values = dict(key)
        if (key_values.get('unit_price') is not None
                and any(
                    isinstance(r.origin, RequisitionLine) for r in requests)):
            line.unit_price = round_price(
                Uom.compute_price(
                    key_values.get('unit', line.unit),
                    key_values['unit_price'],
                    line.unit))
        return line


class Purchase(metaclass=PoolMeta):
    __name__ = 'purchase.purchase'

    @classmethod
    def _process_requisition(cls, purchases):
        pool = Pool()
        Request = pool.get('purchase.request')
        Requisition = pool.get('purchase.requisition')

        requests = []
        for sub_purchases in grouped_slice(purchases):
            requests.append(Request.search([
                        ('purchase_line.purchase.id', 'in',
                            [x.id for x in sub_purchases]),
                        ('origin', 'like', 'purchase.requisition.line,%'),
                        ]))
        requests = list(chain(*requests))

        if requests:
            requisition_ids = list(set(req.origin.requisition
                for req in requests))
            Requisition.process(Requisition.browse(requisition_ids))

    @classmethod
    @ModelView.button
    @Workflow.transition('confirmed')
    def confirm(cls, purchases):
        super(Purchase, cls).confirm(purchases)
        cls._process_requisition(purchases)

    @classmethod
    @ModelView.button
    @Workflow.transition('cancelled')
    def cancel(cls, purchases):
        super(Purchase, cls).cancel(purchases)
        cls._process_requisition(purchases)
