# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

import datetime as dt
from collections import defaultdict
from decimal import Decimal

from sql import Null
from sql.functions import CharLength

from trytond.i18n import gettext
from trytond.model import (
    ChatMixin, 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, PoolMeta
from trytond.pyson import Bool, Eval, If
from trytond.tools import grouped_slice
from trytond.transaction import Transaction

from .exceptions import ComplaintSimilarWarning


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

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


class Complaint(Workflow, ModelSQL, ModelView, ChatMixin):
    __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', -1)),
                    ()),
                ('company', '=', Eval('company', -1)),
                ('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', -1)),
                    ()),
                ('company', '=', Eval('company', -1)),
                ('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().__setup__()
        t = cls.__table__()
        cls._sql_indexes.update({
                Index(t, (t.reference, Index.Similarity())),
                Index(
                    t,
                    (t.state, Index.Equality(cardinality='low')),
                    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().__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 [
            ~((table.state == 'cancelled') & (table.number == Null)),
            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.name, 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', '')),
            ]

    def chat_language(self, audience='internal'):
        language = super().chat_language(audience=audience)
        if audience == 'public':
            language = self.customer.lang.code if self.customer.lang else None
        return language

    @classmethod
    def preprocess_values(cls, mode, values):
        pool = Pool()
        Configuration = pool.get('sale.configuration')
        values = super().preprocess_values(mode, values)
        if mode == 'create' and not values.get('number'):
            company_id = values.get('company', cls.default_company())
            if company_id is not None:
                configuration = Configuration(1)
                if sequence := configuration.get_multivalue(
                        'complaint_sequence', company=company_id):
                    values['number'] = sequence.get()
        return values

    @classmethod
    def check_modification(cls, mode, complaints, values=None, external=False):
        super().check_modification(
            mode, complaints, values=values, external=external)
        if mode == 'delete':
            for complaint in complaints:
                if complaint.state != 'draft':
                    raise AccessError(gettext(
                            'sale_complaint.msg_complaint_delete_draft',
                            complaint=complaint.rec_name))

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

    @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):
        cls._check_similar(complaints)

    @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()), []))

    @classmethod
    def _check_similar(cls, complaints):
        pool = Pool()
        Warning = pool.get('res.user.warning')
        for sub_complaints in grouped_slice(complaints):
            sub_complaints = list(sub_complaints)
            domain = list(filter(None,
                    (c._similar_domain() for c in sub_complaints)))
            if not domain:
                continue
            if cls.search(['OR'] + domain, order=[]):
                for complaint in sub_complaints:
                    domain = complaint._similar_domain()
                    if not domain:
                        continue
                    try:
                        similar, = cls.search(domain, limit=1)
                    except ValueError:
                        continue
                    warning_key = Warning.format(
                        'complaint_similar', [complaint])
                    if Warning.check(warning_key):
                        raise ComplaintSimilarWarning(warning_key,
                            gettext('sale_complaint.msg_complaint_similar',
                                similar=similar.rec_name,
                                complaint=complaint.rec_name))

    def _similar_domain(self):
        pool = Pool()
        Sale = pool.get('sale.sale')
        SaleLine = pool.get('sale.line')
        Invoice = pool.get('account.invoice')
        InvoiceLine = pool.get('account.invoice.line')
        domain = ['OR',
            ('origin', '=', str(self.origin)),
            ]
        if isinstance(self.origin, Sale):
            domain.append(('origin.sale', '=', self.origin.id, 'sale.line'))
        elif isinstance(self.origin, SaleLine):
            domain.append(('origin', '=', str(self.origin.sale)))
        elif isinstance(self.origin, Invoice):
            domain.append(
                ('origin.invoice', '=', self.origin.id,
                    'account.invoice.line'))
        elif isinstance(self.origin, InvoiceLine):
            domain.append(('origin', '=', str(self.origin.invoice)))
        return [
            domain,
            ('id', '!=', self.id),
            ]


class Action(ModelSQL, ModelView):
    __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')
    company = fields.Function(
        fields.Many2One('company.company', "Company"),
        'on_change_with_company')

    @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
                elif (self.complaint.origin_model == 'sale.line'
                        and self.complaint.origin.actual_quantity is not None):
                    quantity = self.complaint.origin.actual_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:
                        sale = self.complaint.origin
                        amount = 0
                        for line in sale.lines:
                            if line.type != 'line':
                                continue
                            if line.actual_quantity is not None:
                                quantity = line.actual_quantity
                            else:
                                quantity = line.quantity
                            amount += sale.currency.round(
                                Decimal(str(quantity)) * line.unit_price)
                        return 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

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

    @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']
                    default['quantity'] = lambda o: (
                        o['actual_quantity']
                        if o['actual_quantity'] is not None
                        else o['quantity'])
            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
                else:
                    default['quantity'] = (
                        sale_line.actual_quantity
                        if sale_line.actual_quantity is not None
                        else sale_line.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 check_modification(cls, mode, actions, values=None, external=False):
        super().check_modification(
            mode, actions, values=values, external=external)
        if mode == 'delete':
            for action in actions:
                if action.result:
                    raise AccessError(gettext(
                            'sale_complaint.msg_action_delete_result',
                            action=action.rec_name))


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):
    __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:
            if self.line.actual_quantity is not None:
                return self.line.actual_quantity
            else:
                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):
    __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


class Complaint_PromotionCoupon(metaclass=PoolMeta):
    __name__ = 'sale.complaint'

    @classmethod
    def _actions_domains(cls):
        domains = super()._actions_domains()
        for name, domain in domains.items():
            domain.append('promotion_coupon')
        return domains


class Action_PromotionCoupon(metaclass=PoolMeta):
    __name__ = 'sale.complaint.action'

    _promtion_coupon_states = {
        'invisible': Eval('action') != 'promotion_coupon',
        'required': Eval('action') == 'promotion_coupon',
        }

    promotion_coupon = fields.Many2One(
        'sale.promotion.coupon', "Promotion Coupon",
        domain=[
            ('company', '=', Eval('company', -1)),
            ('number_of_use', '=', 1),
            ('per_party', '=', False),
            ],
        states=_promtion_coupon_states)
    promotion_coupon_number = fields.Char(
        "Number", states=_promtion_coupon_states)
    promotion_coupon_duration = fields.TimeDelta(
        "Duration", states=_promtion_coupon_states)

    del _promtion_coupon_states

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.action.selection.append(('promotion_coupon', "Promotion Coupon"))

    @classmethod
    def default_promotion_coupon_duration(cls):
        return dt.timedelta(days=90)

    @classmethod
    def _get_result(cls):
        return super()._get_result() + ['sale.promotion.coupon.number']

    def do_promotion_coupon(self):
        pool = Pool()
        PromotionCouponNumber = pool.get('sale.promotion.coupon.number')
        Date = pool.get('ir.date')

        with Transaction().set_context(company=self.company.id):
            today = Date.today()

        return PromotionCouponNumber(
            number=self.promotion_coupon_number,
            coupon=self.promotion_coupon,
            company=self.company,
            start_date=today,
            end_date=today + self.promotion_coupon_duration,
            )
