# 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 sql.functions import CharLength

from trytond.i18n import gettext
from trytond.model import (
    DeactivableMixin, Index, ModelSQL, ModelView, Workflow, fields)
from trytond.model.exceptions import AccessError
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
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, If
from trytond.transaction import Transaction


class Type(DeactivableMixin, ModelSQL, ModelView):
    'Customer Complaint Type'
    __name__ = 'sale.complaint.type'

    name = fields.Char('Name', required=True)
    origin = fields.Many2One('ir.model', 'Origin', required=True,
        domain=[('model', 'in', ['sale.sale', 'sale.line',
                    'account.invoice', 'account.invoice.line'])])


class Complaint(Workflow, ModelSQL, ModelView):
    'Customer Complaint'
    __name__ = 'sale.complaint'
    _rec_name = 'number'

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

    number = fields.Char("Number", readonly=True)
    reference = fields.Char("Reference")
    date = fields.Date('Date', states=_states)
    customer = fields.Many2One(
        'party.party', "Customer", required=True, states=_states,
        context={
            'company': Eval('company', -1),
            },
        depends={'company'})
    company = fields.Many2One(
        'company.company', 'Company', required=True,
        states={
            'readonly': _states['readonly'] | Eval('origin'),
            })
    type = fields.Many2One('sale.complaint.type', 'Type', required=True,
        states=_states)
    origin = fields.Reference('Origin', selection='get_origin',
        domain={
            'sale.sale': [
                If(Eval('customer'),
                    ('party', '=', Eval('customer')),
                    ()),
                ('company', '=', Eval('company')),
                ('state', 'in', ['confirmed', 'processing', 'done']),
                ],
            'sale.line': [
                ('type', '=', 'line'),
                If(Eval('customer'),
                    ('sale.party', '=', Eval('customer')),
                    ()),
                ('sale.company', '=', Eval('company')),
                ('sale.state', 'in', ['confirmed', 'processing', 'done']),
                ],
            'account.invoice': [
                If(Eval('customer'),
                    ('party', '=', Eval('customer')),
                    ()),
                ('company', '=', Eval('company')),
                ('type', '=', 'out'),
                ('state', 'in', ['posted', 'paid']),
                ],
            'account.invoice.line': [
                ('type', '=', 'line'),
                If(Eval('customer'),
                    ('invoice.party', '=', Eval('customer')),
                    ()),
                ('invoice.company', '=', Eval('company')),
                ('invoice.type', '=', 'out'),
                ('invoice.state', 'in', ['posted', 'paid']),
                ],
            },
        states={
            'readonly': ((Eval('state') != 'draft')
                | Bool(Eval('actions', [0]))),
            'required': Bool(Eval('origin_model')),
            },
        depends={'origin_model'})
    origin_id = fields.Function(fields.Integer('Origin ID'),
        'on_change_with_origin_id')
    origin_model = fields.Function(fields.Char('Origin Model'),
        'on_change_with_origin_model')
    description = fields.Text('Description', states=_states)
    actions = fields.One2Many('sale.complaint.action', 'complaint', 'Actions',
        states={
            'readonly': ((Eval('state') != 'draft')
                | (If(~Eval('origin_id', 0), 0, Eval('origin_id', 0)) <= 0)),
            },
        depends={'origin_model'})
    submitted_by = employee_field(
        "Submitted By",
        states=['waiting', 'approved', 'rejected', 'done', 'cancelled'])
    approved_by = employee_field(
        "Approved By",
        states=['approved', 'rejected', 'done', 'cancelled'])
    rejected_by = employee_field(
        "Rejected By",
        states=['approved', 'rejected', 'done', 'cancelled'])
    cancelled_by = employee_field(
        "Cancelled By",
        states=['cancelled'])
    state = fields.Selection([
            ('draft', 'Draft'),
            ('waiting', 'Waiting'),
            ('approved', 'Approved'),
            ('rejected', 'Rejected'),
            ('done', 'Done'),
            ('cancelled', 'Cancelled'),
            ], "State", readonly=True, required=True, sort=False)

    @classmethod
    def __setup__(cls):
        cls.number.search_unaccented = False
        cls.reference.search_unaccented = False
        super(Complaint, cls).__setup__()
        t = cls.__table__()
        cls._sql_indexes.update({
                Index(t, (t.reference, Index.Similarity())),
                Index(
                    t,
                    (t.state, Index.Equality()),
                    where=t.state.in_(['draft', 'waiting', 'approved'])),
                })
        cls._order.insert(0, ('date', 'DESC'))
        cls._transitions |= set((
                ('draft', 'waiting'),
                ('waiting', 'draft'),
                ('waiting', 'approved'),
                ('waiting', 'rejected'),
                ('approved', 'done'),
                ('approved', 'draft'),
                ('draft', 'cancelled'),
                ('waiting', 'cancelled'),
                ('done', 'draft'),
                ('rejected', 'draft'),
                ('cancelled', 'draft'),
                ))
        cls._buttons.update({
                'cancel': {
                    'invisible': ~Eval('state').in_(['draft', 'waiting']),
                    'depends': ['state'],
                    },
                'draft': {
                    'invisible': ~Eval('state').in_(
                        ['waiting', 'done', 'cancelled']),
                    'icon': If(Eval('state').in_(['done', 'cancelled']),
                        'tryton-undo', 'tryton-back'),
                    'depends': ['state'],
                    },
                'wait': {
                    'invisible': ~Eval('state').in_(['draft']),
                    'depends': ['state'],
                    },
                'approve': {
                    'invisible': ~Eval('state').in_(['waiting']),
                    'depends': ['state'],
                    },
                'reject': {
                    'invisible': ~Eval('state').in_(['waiting']),
                    'depends': ['state'],
                    },
                'process': {
                    'invisible': ~Eval('state').in_(['approved']),
                    'depends': ['state'],
                    },
                })

        actions_domains = cls._actions_domains()
        actions_domain = [('action', 'in', actions_domains.pop(None))]
        for model, actions in actions_domains.items():
            actions_domain = If(Eval('origin_model') == model,
                [('action', 'in', actions)], actions_domain)
        cls.actions.domain = [actions_domain]

    @classmethod
    def __register__(cls, module_name):
        table_h = cls.__table_handler__(module_name)

        # Migration from 6.4: rename employee into submitted_by
        if (table_h.column_exist('employee')
                and not table_h.column_exist('submitted_by')):
            table_h.column_rename('employee', 'submitted_by')

        super(Complaint, cls).__register__(module_name)

    @classmethod
    def _actions_domains(cls):
        return {
            None: [],
            'sale.sale': ['sale_return'],
            'sale.line': ['sale_return'],
            'account.invoice': ['credit_note'],
            'account.invoice.line': ['credit_note'],
            }

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

    @staticmethod
    def default_date():
        pool = Pool()
        Date = pool.get('ir.date')
        return Date.today()

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

    @staticmethod
    def default_state():
        return 'draft'

    @fields.depends('type')
    def get_origin(self):
        if self.type:
            origin = self.type.origin
            return [('', ''), (origin.model, origin.name)]
        else:
            return []

    @fields.depends('origin', 'customer')
    def on_change_origin(self):
        pool = Pool()
        Sale = pool.get('sale.sale')
        SaleLine = pool.get('sale.line')
        Invoice = pool.get('account.invoice')
        InvoiceLine = pool.get('account.invoice.line')
        if not self.customer and self.origin and self.origin.id >= 0:
            if isinstance(self.origin, Sale):
                self.customer = self.origin.party
            elif isinstance(self.origin, SaleLine):
                self.customer = self.origin.sale.party
            elif isinstance(self.origin, Invoice):
                self.customer = self.origin.party
            elif isinstance(self.origin, InvoiceLine) and self.origin.invoice:
                self.customer = self.origin.invoice.party

    @fields.depends('origin')
    def on_change_with_origin_id(self, name=None):
        if self.origin:
            return self.origin.id

    @fields.depends('origin')
    def on_change_with_origin_model(self, name=None):
        if self.origin:
            return self.origin.__class__.__name__

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

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

        config = Configuration(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(
                    'complaint_sequence',
                    company=values.get('company', default_company)).get()
        return super(Complaint, cls).create(vlist)

    @classmethod
    def copy(cls, complaints, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('number', None)
        default.setdefault('submitted_by')
        default.setdefault('approved_by')
        default.setdefault('rejected_by')
        default.setdefault('cancelled_by')
        return super(Complaint, cls).copy(complaints, default=default)

    @classmethod
    def delete(cls, complaints):
        for complaint in complaints:
            if complaint.state != 'draft':
                raise AccessError(
                    gettext('sale_complaint.msg_complaint_delete_draft',
                        complaint=complaint.rec_name))
        super(Complaint, cls).delete(complaints)

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

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    @reset_employee(
        'submitted_by', 'approved_by', 'rejected_by', 'cancelled_by')
    def draft(cls, complaints):
        pass

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

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

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

    @classmethod
    @ModelView.button
    @Workflow.transition('done')
    def process(cls, complaints):
        pool = Pool()
        Action = pool.get('sale.complaint.action')
        results = defaultdict(list)
        actions = defaultdict(list)
        for complaint in complaints:
            for action in complaint.actions:
                if action.result:
                    continue
                result = action.do()
                results[result.__class__].append(result)
                actions[result.__class__].append(action)
        for kls, records in results.items():
            kls.save(records)
            for action, record in zip(actions[kls], records):
                action.result = record
        Action.save(sum(list(actions.values()), []))


class Action(ModelSQL, ModelView):
    'Customer Complaint Action'
    __name__ = 'sale.complaint.action'

    _states = {
        'readonly': ((Eval('complaint_state') != 'draft')
            | Bool(Eval('result'))),
        }
    _line_states = {
        'invisible': ~Eval('_parent_complaint', {}
            ).get('origin_model', 'sale.line').in_(
            ['sale.line', 'account.invoice.line']),
        'readonly': _states['readonly'],
        }

    complaint = fields.Many2One(
        'sale.complaint', 'Complaint', required=True, ondelete='CASCADE',
        states=_states)
    action = fields.Selection([
            ('sale_return', 'Create Sale Return'),
            ('credit_note', 'Create Credit Note'),
            ], 'Action', states=_states)

    sale_lines = fields.One2Many(
        'sale.complaint.action-sale.line', 'action', "Sale Lines",
        states={
            'invisible': Eval('_parent_complaint', {}
                ).get('origin_model', 'sale.sale') != 'sale.sale',
            'readonly': _states['readonly'],
            },
        help='Leave empty for all lines.')

    invoice_lines = fields.One2Many(
        'sale.complaint.action-account.invoice.line', 'action',
        "Invoice Lines",
        states={
            'invisible': Eval('_parent_complaint', {}
                ).get('origin_model', 'account.invoice.line'
                ) != 'account.invoice',
            'readonly': _states['readonly'],
            },
        help='Leave empty for all lines.')

    quantity = fields.Float(
        "Quantity", digits='unit',
        states=_line_states,
        help='Leave empty for the same quantity.')
    unit = fields.Function(fields.Many2One('product.uom', 'Unit',
            states=_line_states),
        'on_change_with_unit')
    unit_price = Monetary(
        "Unit Price", currency='currency', digits=price_digits,
        states=_line_states,
        help='Leave empty for the same price.')

    amount = fields.Function(Monetary(
            "Amount", 'currency', digits='currency'),
        'on_change_with_amount')
    currency = fields.Function(fields.Many2One(
            'currency.currency', "Currency"),
        'on_change_with_currency')

    result = fields.Reference('Result', selection='get_result', readonly=True)

    complaint_state = fields.Function(
        fields.Selection('get_complaint_states', "Complaint State"),
        'on_change_with_complaint_state')

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

    @fields.depends('complaint',
        '_parent_complaint.origin_model', '_parent_complaint.origin')
    def on_change_with_unit(self, name=None):
        if (self.complaint
                and self.complaint.origin_model in {
                    'sale.line', 'account.invoice.line'}):
            return self.complaint.origin.unit

    @fields.depends(
        'quantity', 'unit_price', 'currency', 'sale_lines', 'invoice_lines',
        'complaint', '_parent_complaint.origin_model',
        '_parent_complaint.origin')
    def on_change_with_amount(self, name=None):
        if self.complaint:
            if self.complaint.origin_model in {
                    'sale.line', 'account.invoice.line'}:
                if self.quantity is not None:
                    quantity = self.quantity
                else:
                    quantity = self.complaint.origin.quantity
                if self.unit_price is not None:
                    unit_price = self.unit_price
                else:
                    unit_price = self.complaint.origin.unit_price
                amount = Decimal(str(quantity)) * unit_price
                if self.currency:
                    amount = self.currency.round(amount)
                return amount
            elif self.complaint.origin_model == 'sale.sale':
                if not self.sale_lines:
                    if self.complaint and self.complaint.origin:
                        return self.complaint.origin.untaxed_amount
                else:
                    return sum(
                        getattr(l, 'amount', None) or Decimal(0)
                        for l in self.sale_lines)
            elif self.complaint.origin_model == 'account.invoice':
                if not self.invoice_lines:
                    if self.complaint and self.complaint.origin:
                        return self.complaint.origin.untaxed_amount
                else:
                    return sum(
                        getattr(l, 'amount', None) or Decimal(0)
                        for l in self.invoice_lines)

    @fields.depends(
        'complaint',
        '_parent_complaint.origin_model', '_parent_complaint.origin')
    def on_change_with_currency(self, name=None):
        if (self.complaint
                and self.complaint.origin_model in {
                    'sale.sale', 'sale.line',
                    'account.invoice', 'account.invoice.line'}):
            return self.complaint.origin.currency

    @classmethod
    def get_complaint_states(cls):
        pool = Pool()
        Complaint = pool.get('sale.complaint')
        return Complaint.fields_get(['state'])['state']['selection']

    @fields.depends('complaint', '_parent_complaint.state')
    def on_change_with_complaint_state(self, name=None):
        if self.complaint:
            return self.complaint.state

    @classmethod
    def _get_result(cls):
        'Return list of Model names for result Reference'
        return ['sale.sale', 'account.invoice']

    @classmethod
    def get_result(cls):
        pool = Pool()
        Model = pool.get('ir.model')
        get_name = Model.get_name
        models = cls._get_result()
        return [(None, '')] + [(m, get_name(m)) for m in models]

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

    def do(self):
        return getattr(self, 'do_%s' % self.action)()

    def do_sale_return(self):
        pool = Pool()
        Sale = pool.get('sale.sale')
        Line = pool.get('sale.line')

        if isinstance(self.complaint.origin, (Sale, Line)):
            default = {}
            if isinstance(self.complaint.origin, Sale):
                sale = self.complaint.origin
                if self.sale_lines:
                    sale_lines = [l.line for l in self.sale_lines]
                    line2qty = {
                        l.line.id: l.get_quantity() for l in self.sale_lines}
                    line2price = {
                        l.line.id: l.get_unit_price() for l in self.sale_lines}
                    default['quantity'] = lambda o: line2qty.get(o['id'])
                    default['unit_price'] = lambda o: line2price.get(o['id'])
                else:
                    sale_lines = [l for l in sale.lines if l.type == 'line']
            elif isinstance(self.complaint.origin, Line):
                sale_line = self.complaint.origin
                sale = sale_line.sale
                sale_lines = [sale_line]
                if self.quantity is not None:
                    default['quantity'] = self.quantity
                if self.unit_price is not None:
                    default['unit_price'] = self.unit_price
            return_sale, = Sale.copy([sale], default={'lines': None})
            default['sale'] = return_sale.id
            Line.copy(sale_lines, default=default)
        else:
            return
        return_sale.origin = self.complaint
        for line in return_sale.lines:
            if line.type == 'line':
                line.quantity *= -1
        return_sale.lines = return_sale.lines  # Force saving
        return return_sale

    def do_credit_note(self):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        Line = pool.get('account.invoice.line')

        if isinstance(self.complaint.origin, (Invoice, Line)):
            line2qty = line2price = {}
            if isinstance(self.complaint.origin, Invoice):
                invoice = self.complaint.origin
                if self.invoice_lines:
                    invoice_lines = [l.line for l in self.invoice_lines]
                    line2qty = {l.line: l.quantity
                        for l in self.invoice_lines
                        if l.quantity is not None}
                    line2price = {l.line: l.unit_price
                        for l in self.invoice_lines
                        if l.unit_price is not None}
                else:
                    invoice_lines = [
                        l for l in invoice.lines if l.type == 'line']
            elif isinstance(self.complaint.origin, Line):
                invoice_line = self.complaint.origin
                invoice = invoice_line.invoice
                invoice_lines = [invoice_line]
                if self.quantity is not None:
                    line2qty = {invoice_line: self.quantity}
                if self.unit_price is not None:
                    line2price = {invoice_line: self.unit_price}
            with Transaction().set_context(_account_invoice_correction=True):
                credit_note, = Invoice.copy([invoice], default={
                        'lines': [],
                        'taxes': [],
                        })
                # Copy each line one by one to get negative and positive lines
                # following each other
                for invoice_line in invoice_lines:
                    qty = line2qty.get(invoice_line, invoice_line.quantity)
                    unit_price = invoice_line.unit_price - line2price.get(
                        invoice_line, invoice_line.unit_price)
                    Line.copy([invoice_line], default={
                            'invoice': credit_note.id,
                            'quantity': -qty,
                            'origin': str(self.complaint),
                            })
                    credit_line, = Line.copy([invoice_line], default={
                            'invoice': credit_note.id,
                            'quantity': qty,
                            'unit_price': unit_price,
                            'origin': str(self.complaint),
                            })
            credit_note.update_taxes()
        else:
            return
        return credit_note

    @classmethod
    def delete(cls, actions):
        for action in actions:
            if action.result:
                raise AccessError(
                    gettext('sale_complaint.msg_action_delete_result',
                        action=action.rec_name))
        super(Action, cls).delete(actions)


class _Action_Line:
    __slots__ = ()
    _states = {
        'readonly': (
            (Eval('complaint_state') != 'draft')
            | Bool(Eval('_parent_action.result', True))),
        }

    action = fields.Many2One(
        'sale.complaint.action', "Action", ondelete='CASCADE', required=True)
    quantity = fields.Float(
        "Quantity", digits='unit', states=_states)
    unit = fields.Function(
        fields.Many2One('product.uom', "Unit"), 'on_change_with_unit')
    unit_price = Monetary(
        "Unit Price", currency='currency', digits=price_digits,
        states=_states,
        help='Leave empty for the same price.')

    amount = fields.Function(Monetary(
            "Amount", currency='currency', digits='currency'),
        'on_change_with_amount')
    currency = fields.Function(fields.Many2One(
            'currency.currency', "Currency"),
        'on_change_with_currency')

    complaint_state = fields.Function(
        fields.Selection('get_complaint_states', "Complaint State"),
        'on_change_with_complaint_state')
    complaint_origin_id = fields.Function(
        fields.Integer("Complaint Origin ID"),
        'on_change_with_complaint_origin_id')

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

    def on_change_with_unit(self, name=None):
        raise NotImplementedError

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

    def get_quantity(self):
        raise NotImplementedError

    def get_unit_price(self):
        raise NotImplementedError

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

    @classmethod
    def get_complaint_states(cls):
        pool = Pool()
        Complaint = pool.get('sale.complaint')
        return Complaint.fields_get(['state'])['state']['selection']

    @fields.depends('action', '_parent_action.complaint',
        '_parent_action._parent_complaint.state')
    def on_change_with_complaint_state(self, name=None):
        if self.action and self.action.complaint:
            return self.action.complaint.state

    @fields.depends('action', '_parent_action.complaint',
        '_parent_action._parent_complaint.origin_id')
    def on_change_with_complaint_origin_id(self, name=None):
        if self.action and self.action.complaint:
            return self.action.complaint.origin_id


class Action_SaleLine(_Action_Line, ModelView, ModelSQL):
    'Customer Complaint Action - Sale Line'
    __name__ = 'sale.complaint.action-sale.line'

    line = fields.Many2One(
        'sale.line', "Sale Line",
        ondelete='RESTRICT', required=True,
        domain=[
            ('type', '=', 'line'),
            ('sale', '=', Eval('complaint_origin_id', -1)),
            ])

    @fields.depends('line')
    def on_change_with_unit(self, name=None):
        return self.line.unit if self.line else None

    @fields.depends('quantity', 'line')
    def get_quantity(self):
        if self.quantity is not None:
            return self.quantity
        elif self.line:
            return self.line.quantity

    @fields.depends('unit_price', 'line')
    def get_unit_price(self):
        if self.unit_price is not None:
            return self.unit_price
        elif self.line:
            return self.line.unit_price


class Action_InvoiceLine(_Action_Line, ModelView, ModelSQL):
    'Customer Complaint Action - Invoice Line'
    __name__ = 'sale.complaint.action-account.invoice.line'

    line = fields.Many2One(
        'account.invoice.line', 'Invoice Line',
        ondelete='RESTRICT', required=True,
        domain=[
            ('type', '=', 'line'),
            ('invoice', '=', Eval('complaint_origin_id', -1)),
            ])

    @fields.depends('line')
    def on_change_with_unit(self, name=None):
        return self.line.unit if self.line else None

    @fields.depends('quantity', 'line')
    def get_quantity(self):
        if self.quantity is not None:
            return self.quantity
        elif self.line:
            return self.line.quantity

    @fields.depends('unit_price', 'line')
    def get_unit_price(self):
        if self.unit_price is not None:
            return self.unit_price
        elif self.line:
            return self.line.unit_price
