#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 trytond.model import ModelView, ModelSQL, Workflow, fields
from trytond.wizard import Wizard, StateTransition, StateView, Button
from trytond.pyson import Eval, Bool, If, Id
from trytond.pool import Pool
from trytond.transaction import Transaction

BOM_CHANGES = ['bom', 'product', 'quantity', 'uom', 'warehouse', 'location',
    'company', 'inputs', 'outputs']


class Production(Workflow, ModelSQL, ModelView):
    "Production"
    _name = 'production'
    _description = __doc__
    _rec_name = 'code'

    code = fields.Char('Code', select=True, readonly=True)
    reference = fields.Char('Reference', select=1,
        states={
            'readonly': ~Eval('state').in_(['request', 'draft']),
            },
        depends=['state'])
    planned_date = fields.Date('Planned Date',
        states={
            'readonly': ~Eval('state').in_(['request', 'draft']),
            },
        depends=['state'])
    effective_date = fields.Date('Effective Date', readonly=True)
    company = fields.Many2One('company.company', 'Company', required=True,
        states={
            'readonly': ~Eval('state').in_(['request', 'draft']),
            },
        depends=['state'])
    warehouse = fields.Many2One('stock.location', 'Warehouse', required=True,
        domain=[
            ('type', '=', 'warehouse'),
            ],
        states={
            'readonly': (~Eval('state').in_(['request', 'draft'])
                | Eval('inputs', True) | Eval('outputs', True)),
            },
        on_change=['warehouse'],
        depends=['state'])
    location = fields.Many2One('stock.location', 'Location', required=True,
        domain=[
            ('type', '=', 'production'),
            ],
        states={
            'readonly': (~Eval('state').in_(['request', 'draft'])
                | Eval('inputs', True) | Eval('outputs', True)),
            },
        depends=['state'])
    product = fields.Many2One('product.product', 'Product',
        domain=[
            ('type', '!=', 'service'),
            ],
        states={
            'readonly': ~Eval('state').in_(['request', 'draft']),
            },
        on_change=BOM_CHANGES)
    bom = fields.Many2One('production.bom', 'BOM',
        domain=[
            ('output_products', '=', Eval('product', 0)),
            ],
        states={
            'readonly': ~Eval('state').in_(['request', 'draft']),
            'invisible': ~Eval('product'),
            },
        on_change=BOM_CHANGES,
        depends=['product'])
    uom_category = fields.Function(fields.Many2One(
            'product.uom.category', 'Uom Category',
            on_change_with=['product']), 'get_uom_category')
    uom = fields.Many2One('product.uom', 'Uom',
        domain=[
            ('category', '=', Eval('uom_category')),
            ],
        states={
            'readonly': ~Eval('state').in_(['request', 'draft']),
            'required': Bool(Eval('bom')),
            'invisible': ~Eval('product'),
            },
        on_change=BOM_CHANGES,
        depends=['uom_category'])
    unit_digits = fields.Function(fields.Integer('Unit Digits',
            on_change_with=['uom']), 'get_unit_digits')
    quantity = fields.Float('Quantity',
        digits=(16, Eval('unit_digits', 2)),
        states={
            'readonly': ~Eval('state').in_(['request', 'draft']),
            'required': Bool(Eval('bom')),
            'invisible': ~Eval('product'),
            },
        on_change=BOM_CHANGES,
        depends=['unit_digits'])
    cost = fields.Function(fields.Numeric('Cost', digits=(16, 4),
            readonly=True, on_change_with=['inputs']), 'get_cost')
    inputs = fields.One2Many('stock.move', 'production_input', 'Inputs',
        domain=[
            ('from_location', 'child_of', [Eval('warehouse')], 'parent'),
            ('to_location', '=', Eval('location')),
            ],
        states={
            'readonly': (~Eval('state').in_(['request', 'draft', 'waiting'])
                | ~Eval('location')),
            },
        depends=['warehouse', 'location'])
    outputs = fields.One2Many('stock.move', 'production_output', 'Outputs',
        domain=[
            ('from_location', '=', Eval('location')),
            ('to_location', 'child_of', [Eval('warehouse')], 'parent'),
            ],
        states={
            'readonly': (Eval('state').in_(['done', 'cancel'])
                | ~Eval('location')),
            },
        depends=['warehouse', 'location'])
    state = fields.Selection([
            ('request', 'Request'),
            ('draft', 'Draft'),
            ('waiting', 'Waiting'),
            ('assigned', 'Assigned'),
            ('running', 'Running'),
            ('done', 'Done'),
            ('cancel', 'Canceled'),
            ], 'State', readonly=True)

    def __init__(self):
        super(Production, self).__init__()
        self._constraints += [
            ('check_cost', 'missing_cost'),
            ]
        self._error_messages.update({
                'missing_cost': 'It misses some cost on the outputs!',
                })
        self._transitions |= set((
                ('request', 'draft'),
                ('draft', 'waiting'),
                ('waiting', 'assigned'),
                ('assigned', 'running'),
                ('running', 'done'),
                ('running', 'waiting'),
                ('assigned', 'waiting'),
                ('waiting', 'waiting'),
                ('waiting', 'draft'),
                ('request', 'cancel'),
                ('draft', 'cancel'),
                ('waiting', 'cancel'),
                ('assigned', 'cancel'),
                ('cancel', 'draft'),
                ))
        self._buttons.update({
                'cancel': {
                    'invisible': ~Eval('state').in_(['request', 'draft',
                            'assigned']),
                    },
                'draft': {
                    'invisible': ~Eval('state').in_(['request', 'waiting',
                            'cancel']),
                    'icon': If(Eval('state') == 'cancel',
                        'tryton-clear',
                        If(Eval('state') == 'request',
                            'tryton-go-next',
                            'tryton-go-previous')),
                    },
                'wait': {
                    'invisible': ~Eval('state').in_(['draft', 'assigned',
                            'waiting', 'running']),
                    'icon': If(Eval('state').in_(['assigned', 'running']),
                        'tryton-go-previous',
                        If(Eval('state') == 'waiting',
                            'tryton-clear',
                            'tryton-go-next')),
                    },
                'run': {
                    'invisible': Eval('state') != 'assigned',
                    },
                'done': {
                    'invisible': Eval('state') != 'running',
                    },
                'assign_try': {},
                'assign_force': {},
                })

    def default_state(self):
        return 'draft'

    def default_warehouse(self):
        location_obj = Pool().get('stock.location')
        location_ids = location_obj.search(self.warehouse.domain)
        if len(location_ids) == 1:
            return location_ids[0]

    def default_location(self):
        location_obj = Pool().get('stock.location')
        warehouse_id = self.default_warehouse()
        if warehouse_id:
            warehouse = location_obj.browse(warehouse_id)
            return warehouse.production_location.id

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

    def _move_values(self, from_location, to_location, company, product, uom,
            quantity):
        values = {
            'product': product.id,
            'uom': uom.id,
            'quantity': quantity,
            'from_location': None,
            'to_location': None,
            'company': None,
            'state': 'draft',
            }
        values['currency'] = company.currency.id if company else None
        if from_location:
            values['from_location'] = from_location.id
        if to_location:
            values['to_location'] = to_location.id
        if company:
            values['company'] = company.id
        return values

    def _explode_move_values(self, from_location, to_location, company,
            bom_io, quantity):
        pool = Pool()
        move_obj = pool.get('stock.move')

        values = self._move_values(from_location, to_location, company,
            bom_io.product, bom_io.uom, quantity)
        values['product.rec_name'] = bom_io.product.rec_name
        values['uom.rec_name'] = bom_io.uom.rec_name
        values['unit_price_required'] = \
            move_obj.on_change_with_unit_price_required({
                    'from_location': (from_location.id if from_location else
                        None),
                    'to_location': to_location.id if to_location else None,
                    })
        if from_location:
            values['from_location.rec_name'] = from_location.rec_name
        if to_location:
            values['to_location.rec_name'] = to_location.rec_name
        if company:
            values['company.rec_name'] = company.rec_name
        return values

    def explode_bom(self, values):
        pool = Pool()
        bom_obj = pool.get('production.bom')
        product_obj = pool.get('product.product')
        uom_obj = pool.get('product.uom')
        input_obj = pool.get('production.bom.input')
        output_obj = pool.get('production.bom.output')
        location_obj = pool.get('stock.location')
        company_obj = pool.get('company.company')
        template_obj = pool.get('product.template')

        if not (values.get('bom')
                and values.get('product')
                and values.get('uom')):
            return {}
        inputs = {
            'remove': [r['id'] for r in values.get('inputs') or []],
            'add': [],
            }
        outputs = {
            'remove': [r['id'] for r in values.get('outputs') or []],
            'add': [],
            }
        changes = {
            'inputs': inputs,
            'outputs': outputs,
            'cost': Decimal(0),
            }

        bom = bom_obj.browse(values['bom'])
        product = product_obj.browse(values['product'])
        quantity = values.get('quantity') or 0
        uom = uom_obj.browse(values['uom'])
        if values.get('warehouse'):
            warehouse = location_obj.browse(values['warehouse'])
            storage_location = warehouse.storage_location
        else:
            storage_location = None
        if values.get('location'):
            location = location_obj.browse(values['location'])
        else:
            location = None
        if values.get('company'):
            company = company_obj.browse(values['company'])
        else:
            company = None

        factor = bom_obj.compute_factor(bom, product, quantity, uom)
        for input_ in bom.inputs:
            quantity = input_obj.compute_quantity(input_, factor)
            values = self._explode_move_values(storage_location, location,
                company, input_, quantity)
            if values:
                inputs['add'].append(values)
                quantity = uom_obj.compute_qty(input_.uom, quantity,
                    input_.product.default_uom)
                changes['cost'] += (Decimal(str(quantity)) *
                    input_.product.cost_price)
        if hasattr(product_obj, 'cost_price'):
            digits = product_obj.cost_price.digits
        else:
            digits = template_obj.cost_price.digits
        for output in bom.outputs:
            quantity = output_obj.compute_quantity(output, factor)
            values = self._explode_move_values(location, storage_location,
                company, output, quantity)
            if values:
                values['unit_price'] = Decimal(0)
                if output.product.id == values.get('product') and quantity:
                    values['unit_price'] = Decimal(
                        changes['cost'] / Decimal(str(quantity))
                        ).quantize(Decimal(str(10 ** -digits[1])))
                outputs['add'].append(values)
        return changes

    def on_change_warehouse(self, values):
        location_obj = Pool().get('stock.location')
        changes = {
            'location': None,
            }
        if values.get('warehouse'):
            warehouse = location_obj.browse(values['warehouse'])
            changes['location'] = warehouse.production_location.id
        return changes

    def on_change_product(self, values):
        product_obj = Pool().get('product.product')

        result = {}
        if values.get('product'):
            product = product_obj.browse(values['product'])
            uom_ids = [x.id for x in product.default_uom.category.uoms]
            if (not values.get('uom')
                    or values.get('uom') not in uom_ids):
                result['uom'] = product.default_uom.id
                result['uom.rec_name'] = product.default_uom.rec_name
                result['unit_digits'] = product.default_uom.digits
        else:
            result['bom'] = None
            result['uom'] = None
            result['uom.rec_name'] = ''
            result['unit_digits'] = 2

        values = values.copy()
        if 'uom' in result:
            values['uom'] = result['uom']
        if 'bom' in result:
            values['bom'] = result['bom']
        result.update(self.explode_bom(values))
        return result

    def on_change_with_uom_category(self, values):
        product_obj = Pool().get('product.product')
        if values.get('product'):
            product = product_obj.browse(values['product'])
            return product.default_uom.category.id

    def get_uom_category(self, ids, name):
        res = {}
        for production in self.browse(ids):
            if production.product:
                res[production.id] = production.product.default_uom.category.id
            else:
                res[production.id] = None
        return res

    def on_change_with_unit_digits(self, values):
        uom_obj = Pool().get('product.uom')
        if values.get('uom'):
            uom = uom_obj.browse(values['uom'])
            return uom.digits
        return 2

    def get_unit_digits(self, ids, name):
        digits = {}
        for production in self.browse(ids):
            if production.uom:
                digits[production.id] = production.uom.digits
            else:
                digits[production.id] = 2
        return digits

    def on_change_bom(self, values):
        return self.explode_bom(values)

    def on_change_uom(self, values):
        return self.explode_bom(values)

    def on_change_quantity(self, values):
        return self.explode_bom(values)

    def get_cost(self, ids, name):
        costs = {}
        for production in self.browse(ids):
            costs[production.id] = Decimal(0)
            for input_ in production.inputs:
                if input_.cost_price is not None:
                    cost_price = input_.cost_price
                else:
                    cost_price = input_.product.cost_price
                costs[production.id] += (Decimal(str(input_.internal_quantity))
                        * cost_price)
        return costs

    def on_change_with_cost(self, values):
        pool = Pool()
        product_obj = pool.get('product.product')
        uom_obj = pool.get('product.uom')

        cost = Decimal(0)
        if not values.get('inputs'):
            return cost

        product_ids = list(set(r['product'] for r in values['inputs'] if
                r['product'] is not None))
        id2product = dict((p.id, p) for p in product_obj.browse(product_ids))

        uom_ids = list(set(r['uom'] for r in values['inputs']
                if r['uom'] is not None))
        id2uom = dict((u.id, u) for u in uom_obj.browse(uom_ids))

        for input_ in values['inputs']:
            if (input_['product'] is None
                    or input_['uom'] is None
                    or input_['quantity'] is None):
                continue
            product = id2product[input_['product']]
            quantity = uom_obj.compute_qty(id2uom[input_['uom']],
                input_['quantity'], product.default_uom)
            cost += Decimal(str(quantity)) * product.cost_price
        return cost

    def set_moves(self, production):
        pool = Pool()
        bom_obj = pool.get('production.bom')
        move_obj = pool.get('stock.move')
        input_obj = pool.get('production.bom.input')
        output_obj = pool.get('production.bom.output')
        template_obj = pool.get('product.template')
        product_obj = pool.get('product.product')

        storage_location = production.warehouse.storage_location
        location = production.location
        company = production.company

        if not production.bom:
            if production.product:
                product = production.product
                values = self._move_values(location, storage_location, company,
                    product, production.uom, production.quantity)
                if values:
                    values['production_output'] = production.id
                    values['unit_price'] = Decimal(0)
                    move_obj.create(values)
            self._set_move_planned_date([production.id])
            return

        factor = bom_obj.compute_factor(production.bom, production.product,
            production.quantity, production.uom)
        cost = Decimal(0)
        for input_ in production.bom.inputs:
            quantity = input_obj.compute_quantity(input_, factor)
            product = input_.product
            values = self._move_values(storage_location, location, company,
                product, input_.uom, quantity)
            if values:
                values['production_input'] = production.id
                move_obj.create(values)
                cost += Decimal(str(quantity)) * product.cost_price

        if hasattr(product_obj, 'cost_price'):
            digits = product_obj.cost_price.digits
        else:
            digits = template_obj.cost_price.digits

        for output in production.bom.outputs:
            quantity = output_obj.compute_quantity(output, factor)
            product = output.product
            values = self._move_values(location, storage_location, company,
                product, output.uom, quantity)
            if values:
                values['production_output'] = production.id
                if product == production.product:
                    values['unit_price'] = Decimal(
                        cost / Decimal(str(quantity))
                        ).quantize(Decimal(str(10 ** -digits[1])))
                move_obj.create(values)
        self._set_move_planned_date([production.id])

    def check_cost(self, ids):
        pool = Pool()
        currency_obj = pool.get('currency.currency')

        for production in self.browse(ids):
            if production.state != 'done':
                continue
            cost_price = Decimal(0)
            for output in production.outputs:
                cost_price += (Decimal(str(output.quantity))
                    * output.unit_price)
            if not currency_obj.is_zero(production.company.currency,
                    production.cost - cost_price):
                return False
        return True

    def create(self, values):
        sequence_obj = Pool().get('ir.sequence')
        config_obj = Pool().get('production.configuration')

        values = values.copy()
        config = config_obj.browse(1)
        values['code'] = sequence_obj.get_id(config.production_sequence.id)
        production_id = super(Production, self).create(values)
        self._set_move_planned_date(production_id)
        return production_id

    def write(self, ids, values):
        result = super(Production, self).write(ids, values)
        self._set_move_planned_date(ids)
        return result

    def _get_move_planned_date(self, production):
        "Return the planned dates for input and output moves"
        return production.planned_date, production.planned_date

    def _set_move_planned_date(self, ids):
        "Set planned date of moves for the shipments"
        pool = Pool()
        move_obj = pool.get('stock.move')
        if isinstance(ids, (int, long)):
            ids = [ids]
        for production in self.browse(ids):
            dates = self._get_move_planned_date(production)
            input_date, output_date = dates
            move_obj.write([m.id for m in production.inputs
                    if m.state not in ('assigned', 'done', 'cancel')], {
                    'planned_date': input_date,
                    })
            move_obj.write([m.id for m in production.outputs
                    if m.state not in ('assigned', 'done', 'cancel')], {
                    'planned_date': output_date,
                    })

    @ModelView.button
    @Workflow.transition('cancel')
    def cancel(self, ids):
        pool = Pool()
        move_obj = pool.get('stock.move')
        productions = self.browse(ids)
        move_obj.write([m.id for p in productions
                for m in p.inputs + p.outputs
                if m.state != 'cancel'], {
                'state': 'cancel',
                })

    @ModelView.button
    @Workflow.transition('draft')
    def draft(self, ids):
        pool = Pool()
        move_obj = pool.get('stock.move')
        productions = self.browse(ids)
        move_obj.write([m.id for p in productions
                for m in p.inputs + p.outputs
                if m.state != 'draft'], {
                'state': 'draft',
                })

    @ModelView.button
    @Workflow.transition('waiting')
    def wait(self, ids):
        pool = Pool()
        move_obj = pool.get('stock.move')
        productions = self.browse(ids)
        move_obj.write([m.id for p in productions
                for m in p.inputs + p.outputs
                if m.state not in ('draft', 'done')], {
                'state': 'draft',
                })

    @Workflow.transition('assigned')
    def assign(self, ids):
        pass

    @ModelView.button
    @Workflow.transition('running')
    def run(self, ids):
        pool = Pool()
        move_obj = pool.get('stock.move')
        productions = self.browse(ids)
        move_obj.write([m.id for p in productions
                for m in p.inputs
                if m.state not in ('done', 'cancel')], {
                'state': 'done',
                })

    @ModelView.button
    @Workflow.transition('done')
    def done(self, ids):
        pool = Pool()
        move_obj = pool.get('stock.move')
        date_obj = pool.get('ir.date')
        productions = self.browse(ids)
        move_obj.write([m.id for p in productions
                for m in p.outputs
                if m.state not in ('done', 'cancel')], {
                'state': 'done',
                })
        self.write(ids, {
                'effective_date': date_obj.today(),
                })

    @ModelView.button
    def assign_try(self, ids):
        pool = Pool()
        move_obj = pool.get('stock.move')
        productions = self.browse(ids)
        if move_obj.assign_try([m for p in productions
                    for m in p.inputs]):
            self.assign(ids)
            return True
        else:
            return False

    @ModelView.button
    def assign_force(self, ids):
        pool = Pool()
        move_obj = pool.get('stock.move')
        productions = self.browse(ids)
        move_obj.write([m.id for p in productions for m in p.inputs
                if m.state != 'done'], {
                'state': 'assigned',
                })
        self.assign(ids)

Production()


class AssignFailed(ModelView):
    'Assign Production'
    _name = 'production.assign.failed'
    _description = __doc__

    moves = fields.Many2Many('stock.move', None, None, 'Moves', readonly=True)

    def default_moves(self):
        pool = Pool()
        production_obj = pool.get('production')
        production_id = Transaction().context.get('active_id')
        if not production_id:
            return []
        production = production_obj.browse(production_id)
        return [m.id for m in production.inputs if m.state == 'draft']

AssignFailed()


class Assign(Wizard):
    'Assign Production'
    _name = 'production.assign'

    start = StateTransition()
    failed = StateView('production.assign.failed',
        'production.assign_failed_view_form', [
            Button('Force Assign', 'force', 'tryton-go-next',
                states={
                    'invisible': ~Id('stock',
                        'group_stock_force_assignment').in_(
                        Eval('context', {}).get('groups', [])),
                    }),
            Button('Ok', 'end', 'tryton-ok', True),
            ])
    force = StateTransition()

    def transition_start(self, session):
        pool = Pool()
        production_obj = pool.get('production')

        if production_obj.assign_try([Transaction().context['active_id']]):
            return 'end'
        else:
            return 'failed'

    def transition_force(self, session):
        pool = Pool()
        production_obj = pool.get('production')

        production_obj.assign_force([Transaction().context['active_id']])
        return 'end'

Assign()
