# 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 trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
from trytond.exceptions import UserWarning, UserError
from trytond.transaction import Transaction, inactive_records
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
import datetime
import logging
import json

logger = logging.getLogger(__name__)

class Lot(ModelSQL, ModelView):
    "Lot"
    __name__ = 'lot.lot'
    _rec_name = 'lot_name'
    
    lot_name = fields.Char("Lot")
    number = fields.Char("Number", readonly=True)
    lot_qt = fields.Float("Quantity", required=False,
        states={
            'readonly': Eval('lot_type') == 'virtual',
            })
    lot_unit = fields.Many2One('product.uom', "Unit", required=False,
        states={
            'readonly': Eval('lot_type') == 'virtual',
            })
    lot_product = fields.Many2One('product.product', "Product")
    lot_type = fields.Selection([
        ('virtual', 'Open'),
        ('physic', 'Physic'),
        ('loss', 'Loss'),
    ], 'Qt type')
    lot_status = fields.Selection([
        ('forecast', 'Forecast'),
        ('loading', 'Loading'),
        ('transit', 'Transit'),
        ('destination', 'Destination'),
        ('stock', 'Stock'),
        ('delivered', 'Delivered')
    ], 'Where')
    lot_av = fields.Selection([
        ('available', 'Available'),
        ('reserved', 'Reserved'),
        ('locked', 'Locked'),
        ('prov', 'Prov. inv'),
        ('invoiced', 'Invoiced')
    ], 'State')
    lot_quantity = fields.Function(fields.Numeric("Net weight",digits='line_unit'),'get_current_quantity')
    lot_premium = fields.Numeric("Premium", digits='line_unit')
    lot_premium_sup = fields.Numeric("Sup prem", digits='line_unit')
    lot_premium_tpl = fields.Numeric("Tpl prem", digits='line_unit')
    lot_premium_sale = fields.Numeric("Prem/Disc", digits='line_unit')
    lot_shipment_in = fields.Many2One('stock.shipment.in', "Shipment In")
    lot_shipment_out = fields.Many2One('stock.shipment.out', "Shipment Out")
    lot_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment Internal")
    lot_gross_quantity = fields.Function(fields.Numeric("Gross weight",digits='line_unit'),'get_current_gross_quantity')
    lot_parent = fields.Many2One('lot.lot',"Parent")
    lot_childs = fields.One2Many('lot.lot','lot_parent',"Childs")
    lot_himself = fields.Many2One('lot.lot',"Lot")
    lot_container = fields.Char("Container")
    lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
    lot_price = fields.Function(fields.Numeric("Price", digits='line_unit'),'get_lot_price')
    lot_price_sale = fields.Function(fields.Numeric("Price", digits='line_unit'),'get_lot_sale_price')
    lot_state = fields.Many2One('lot.qt.type',"Qt state")
    lot_hist = fields.One2Many('lot.qt.hist','lot',"Qt hist")
    lot_pur_inv_state = fields.Selection([
        (None, ''),
        ('prov', 'Prov'),
        ('invoiced', 'Final')
    ], 'Pur. inv')
    lot_sale_inv_state = fields.Selection([
        (None, ''),
        ('prov', 'Prov'),
        ('invoiced', 'Final')
    ], 'Sale inv')
    lot_price_ct_symbol = fields.Function(fields.Char(""),'get_price_ct_symbol')
    lot_price_ct_symbol_sale = fields.Function(fields.Char(""),'get_price_ct_symbol_sale')
    lot_price_ct_symbol_premium = fields.Function(fields.Char(""),'get_price_ct_symbol_premium')
    line_unit = fields.Function(fields.Many2One('product.uom',"Line unit"),'get_unit_line')
    lot_shipment_origin = fields.Function(
        fields.Reference(
            selection=[
                ("stock.shipment.in", "In"),
                ("stock.shipment.out", "Out"),
                ("stock.shipment.internal", "Internal"),
            ],
            string="Shipment",
        ),
        "get_shipment_origin",
    )
    
    lot_role = fields.Selection([
        ('normal', 'Normal'),
        ('technical', 'Technical'),
    ], 'Role', required=True)

    split_operations = fields.One2Many(
        'lot.split.merge', 'source_lot', 'Split/Merge Ops'
    )

    split_graph = fields.Function(
        fields.Text('Split Graph'),
        'get_split_graph'
    )


    @classmethod
    def default_lot_role(cls):
        return 'normal'

    # ---------------------------------------------------------------------
    # Technical helpers
    # ---------------------------------------------------------------------

    def _check_split_allowed(self):
        if self.lot_role != 'normal':
            raise UserError('Only normal lots can be split.')
        if self.lot_type != 'physic':
            raise UserError('Only physical lots can be split.')
        if self.IsDelivered():
            raise UserError('Delivered lots cannot be split.')

    def _create_technical_parent(self):
        Lot = Pool().get('lot.lot')
        parent = Lot(
            lot_name=f'TECH-{self.lot_name}',
            lot_role='technical',
            lot_type='virtual',
            lot_product=self.lot_product,
            lot_status=self.lot_status,
            lot_av='locked',
        )
        Lot.save([parent])
        return parent

    def _clone_hist_with_ratio(self, ratio):
        LotQtHist = Pool().get('lot.qt.hist')
        hist = []
        for h in self.lot_hist:
            hist.append(
                LotQtHist(
                    quantity_type=h.quantity_type,
                    quantity=h.quantity * ratio,
                    gross_quantity=h.gross_quantity * ratio,
                )
            )
        return hist

    def split_by_weight(self, splits):
        Date = Pool().get('ir.date')
        self._check_split_allowed()

        total_weight = sum(s['quantity'] for s in splits)
        lot_weight = self.get_current_quantity()

        diff = (lot_weight - total_weight).quantize(Decimal('0.00001'))

        if diff < 0:
            raise UserError('Split weight exceeds lot weight.')

        if diff != 0:
            raise UserError(
                f'Split does not fully consume the lot weight.\n'
                f'Remaining: {diff}'
            )

        Lot = Pool().get('lot.lot')
        Split = Pool().get('lot.split.merge')

        children = []
        # Parent becomes technical
        self.lot_av = 'locked'
        self.lot_role = 'technical'
        self.lot_name = 'TECH-' + self.lot_name
        line = self.line
        self.line = None
        Lot.save([self])

        for s in splits:
            ratio = s['quantity'] / lot_weight

            lot_qt = None
            if self.lot_qt:
                lot_qt = (Decimal(self.lot_qt) * ratio).quantize(
                    Decimal('0.00001'), rounding=ROUND_HALF_UP
                )

            child = Lot(
                lot_name=s.get('name'),
                lot_role='normal',
                lot_type='physic',
                lot_qt=float(lot_qt) if lot_qt is not None else None,
                lot_unit=self.lot_unit,
                lot_product=self.lot_product,
                lot_status=self.lot_status,
                lot_av=self.lot_av,
                lot_unit_line=self.lot_unit_line,
            )

            with Transaction().set_context(_skip_function_fields=True):
                Lot.save([child])
                child.set_current_quantity(s['quantity'], s['quantity'], 1)
                child.lot_parent = self
                child.line = line
                Lot.save([child])

            children.append(child)

        ops = []
        for c in children:
            ops.append(
                Split(
                    operation='split',
                    source_lot=self,
                    target_lot=c,
                    lot_qt=c.lot_qt,
                    quantity=c.get_current_quantity(),
                    operation_date=Date.today(),
                )
            )
        Split.save(ops)

        return children

    @classmethod
    def merge_lots(cls, lots):
        Date = Pool().get('ir.date')
        if len(lots) < 2:
            raise UserError('At least two lots are required for merge.')

        # parent = lots[0].lot_parent
        # if not parent or parent.lot_role != 'technical':
        #     raise UserError('Lots must share a technical parent.')

        product = lots[0].lot_product
        for l in lots:
            if l.lot_product != product:
                raise UserError('Cannot merge lots of different products.')

        Lot = Pool().get('lot.lot')
        Split = Pool().get('lot.split.merge')

        total_qt = sum([l.lot_qt or 0 for l in lots])
        total_weight = sum([l.get_current_quantity() for l in lots])

        ops = []
        parents_name = ""
        for l in lots:
            l.lot_av = 'locked'
            l.lot_role = 'technical'
            l.line = None
            parents_name += l.lot_name + "/"
            ops.append(
                Split(
                    operation='merge',
                    source_lot=l,
                    target_lot=merged,
                    lot_qt=l.lot_qt,
                    quantity=l.get_current_quantity(),
                    operation_date=Date.today(),
                )
            )

        Lot.save(lots)
        Split.save(ops)

        merged = Lot(
            lot_name='MERGE-' + parents_name,
            lot_role='normal',
            lot_type='physic',
            lot_product=product,
            lot_qt=total_qt,
            lot_unit=lots[0].lot_unit,
            lot_unit_line=lots[0].lot_unit_line,
        )

        merged.set_current_quantity(total_weight, total_weight, 1)
        Lot.save([merged])

        return merged

    def get_split_graph(self, name=None):
        Split = Pool().get('lot.split.merge')
        
        nodes = {}
        edges = []
        
        def add_lot_node(lot):
            if lot.id in nodes:
                return
            nodes[lot.id] = {
                'id': f'lot.lot {lot.id}',
                'label': f'{lot.lot_name}\n({lot.lot_role})',
                'data_model': 'lot.lot',
                'data_id': lot.id,
                'shape': 'box' if lot.lot_role == 'technical' else 'ellipse',
                'color': {
                    'background': '#FFD966' if lot.lot_role == 'technical' else '#A7C7E7',
                    'border': '#555',
                },
            }
        
        def walk(lot):
            add_lot_node(lot)
            ops = Split.search([('source_lot', '=', lot.id)])
            for op in ops:
                tgt = op.target_lot
                add_lot_node(tgt)
                edges.append({
                    'from': f'lot.lot {lot.id}',
                    'to': f'lot.lot {tgt.id}',
                    'label': op.operation if op.operation else '',
                    'arrows': 'to',
                    'color': {
                        'color': '#267F82',
                        'highlight': '#1D5F62',
                        'hover': '#1D5F62'
                    }
                })
                walk(tgt)
        
        walk(self)
        
        # Calcul des relations parent-enfant
        from collections import defaultdict
        
        children_map = defaultdict(list)
        parent_map = {}
        
        for edge in edges:
            from_id = edge['from']
            to_id = edge['to']
            children_map[from_id].append(to_id)
            parent_map[to_id] = from_id
        
        # Trouver la racine (nœud sans parent)
        all_node_ids = {node['id'] for node in nodes.values()}
        root_candidates = all_node_ids - set(parent_map.keys())
        
        if root_candidates:
            root_id = list(root_candidates)[0]
        else:
            root_id = f'lot.lot {self.id}'
        
        # Assigner les niveaux hiérarchiques
        levels = {}
        
        def assign_levels(node_id, level=0):
            if node_id in levels and levels[node_id] >= level:
                return
            levels[node_id] = level
            for child in children_map.get(node_id, []):
                assign_levels(child, level + 1)
        
        assign_levels(root_id)
        
        # Grouper les nœuds par niveau
        by_level = defaultdict(list)
        for node_id, level in levels.items():
            by_level[level].append(node_id)
        
        # Calculer les positions - IMPORTANT: utiliser des valeurs positives plus grandes
        # Calculer les positions pour un graphe HORIZONTAL
        X_SPACING = 180  # espace entre les niveaux (gauche → droite)
        Y_SPACING = 100  # espace entre les nœuds d’un même niveau

        max_nodes_in_level = max(len(nodes) for nodes in by_level.values()) if by_level else 1
        total_height = max_nodes_in_level * Y_SPACING

        for level, node_ids in by_level.items():
            level_height = len(node_ids) * Y_SPACING
            start_y = (total_height - level_height) / 2  # Centrer verticalement

            for i, node_id in enumerate(node_ids):
                node_num = int(node_id.split()[-1])

                # Niveau → X
                x_pos = level * X_SPACING + 300

                # Répartition verticale
                y_pos = start_y + (i * Y_SPACING) + 100

                nodes[node_num]['x'] = x_pos
                nodes[node_num]['y'] = y_pos
                nodes[node_num]['fixed'] = {
                    'x': True,
                    'y': True
                }

        
        # S'assurer que tous les nœuds ont des positions
        for node in nodes.values():
            if 'x' not in node:
                node['x'] = 0
                node['y'] = 0
                node['fixed'] = {'x': True, 'y': True}
        
        data = {
            'nodes': list(nodes.values()),
            'edges': edges,
            'physics': {
                'enabled': False  # Désactivé car positions fixes
            }
        }
        
        return 'visjson:' + json.dumps(data)

    def get_shipment_origin(self, name):
        if self.lot_shipment_in:
            return 'stock.shipment.in,' + str(self.lot_shipment_in.id)
        elif self.lot_shipment_out:
            return 'stock.shipment.out,' + str(self.lot_shipment_out.id)
        elif self.lot_shipment_internal:
            return 'stock.shipment.internal,' + str(self.lot_shipment_internal.id)
        return None

    def get_unit_line(self,name):
        if self.line:
            return self.line.unit
        return self.lot_unit_line
    def get_lot_inv(self):
        if self.invoice_line:
            return self.invoice_line.invoice.origin
        if self.invoice_line_prov:
            return self.invoice_line_prov.invoice.origin
    
    @classmethod
    def __setup__(cls):
        super(Lot, cls).__setup__()
        #cls._order.insert(0, ('lot_shipment', 'ASC'))

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

    @classmethod
    def default_lot_av(cls):
        return 'available'
    
    @classmethod
    def default_lot_status(cls):
        return 'forecast'

    @classmethod
    def default_lot_type(cls):
        return 'physic'
    
    @classmethod
    def default_lot_gross_quantity(cls):
        return Decimal(0)

    @classmethod
    def default_lot_type(cls):
        return 'receive'

    @classmethod
    def default_lot_state(cls):
        LotQtType = Pool().get('lot.qt.type')
        lqt = LotQtType.search([('sequence','=',1)])
        if lqt:
            return lqt[0].id 

    def get_price_ct_symbol(self,name):
        if self.line:
            return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)

    def get_price_ct_symbol_sale(self,name):
        if self.sale_line:
            return str(self.sale_line.currency.symbol) + "/" + str(self.sale_line.unit.symbol)

    def get_price_ct_symbol_premium(self,name):
        if self.line:
            if self.line.enable_linked_currency:
                return str(self.line.linked_currency.name) + "/" + str(self.line.linked_unit.symbol)
            else:
                return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)

    def get_hist_quantity(self,state_id=0):
        qt = Decimal(0)
        gross_qt = Decimal(0)
        if self.lot_state:
            if self.lot_hist:
                if state_id != 0:
                    st = state_id
                else:
                    st = self.lot_state.id
                lot = [e for e in self.lot_hist if e.quantity_type.id == st][0]
                qt = round(lot.quantity,5)
                gross_qt = round(lot.gross_quantity,5)
        return qt, gross_qt
    
    def get_virtual_diff(self):
        Uom = Pool().get('product.uom')
        line = self.line if self.line else self.sale_line
        if line:
            if line.lots:
                physic_sum = Decimal(0)
                for l in line.lots:
                    if l.lot_type == 'physic' :
                        factor = None
                        rate = None
                        if l.lot_unit_line.category.id != l.line.unit.category.id:
                            factor = 1
                            rate = 1
                        physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit, True, factor, rate)),5)
                return line.quantity_theorical - physic_sum

    def get_current_quantity(self,name=None):
        # if self.lot_type == 'physic':
        qt, gross_qt = self.get_hist_quantity()
        return qt
        # else:
        #     return self.get_virtual_diff()

    def get_current_quantity_converted(self,state_id=0,unit=None):
        Uom = Pool().get('product.uom')
        if not unit:
            unit = self.line.unit if self.line else self.sale_line.unit
        qt, gross_qt = self.get_hist_quantity(state_id)
        factor = None
        rate = None
        if self.lot_unit_line.category.id != unit.category.id:
            factor = 1
            rate = 1
        return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(qt), unit, True, factor, rate)),5)
    
    def get_current_gross_quantity_converted(self,state_id=0,unit=None):
        Uom = Pool().get('product.uom')
        if not unit:
            unit = self.line.unit if self.line else self.sale_line.unit
        qt, gross_qt = self.get_hist_quantity(state_id)
        factor = None
        rate = None
        if self.lot_unit_line.category.id != unit.category.id:
            factor = 1
            rate = 1
        return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(gross_qt), unit, True, factor, rate)),5)
        
    def get_current_gross_quantity(self,name=None):
        if self.lot_type == 'physic':
            qt, gross_qt = self.get_hist_quantity()
            return gross_qt
        else:
            return None
        
    def add_quantity_to_hist(self,net,gross,qt_type):
        LotQtHist = Pool().get('lot.qt.hist')
        lqh = LotQtHist()
        lqh.quantity_type = qt_type
        lqh.quantity = net
        logger.info("ADD_QUANTITY_TO_HIST:%s",gross)
        lqh.gross_quantity = gross
        lqh.lot = self
        return lqh
            
    def set_current_quantity(self, net, gross, seq=1):
        LotQtType = Pool().get('lot.qt.type')
        lqtt = LotQtType.search([('sequence', '=', seq)])
        if not lqtt:
            return
        
        lot_hist = list(getattr(self, 'lot_hist', []) or [])
        existing = [e for e in lot_hist if e.quantity_type == lqtt[0]]

        if existing:
            hist = existing[0]
            hist.quantity = net
            logger.info("SET_CURRENT_HIST:%s",gross)
            hist.gross_quantity = gross
        else:
            lot_hist.append(self.add_quantity_to_hist(net, gross, lqtt[0]))

        self.lot_hist = lot_hist
        self.lot_state = lqtt[0]
        
    def get_unit(self,name):
        if self.line:
            return self.line.unit
        if self.sale_line:
            return self.sale_line.unit

    def get_lot_price(self,name=None):
        price = Decimal(0)
        if self.line:
            if self.line.enable_linked_currency and self.line.linked_price and self.line.linked_currency and self.line.price_type == 'priced':
                return self.line.get_price_linked_currency((self.lot_premium if self.lot_premium else 0))
            else:
                return self.line.get_price((self.lot_premium if self.lot_premium else 0))
                
    def get_lot_sale_price(self,name=None):
        price = Decimal(0)
        if self.sale_line:
            # if self.line.enable_linked_currency and self.line.linked_price and self.line.linked_currency and self.line.price_type == 'priced':
            #     return self.line.get_price_linked_currency((self.lot_premium if self.lot_premium else 0))
            # else:
            return self.sale_line.get_price((self.lot_premium if self.lot_premium else 0))
        
    def get_sale_amount(self,name):
        round_context = getcontext()
        round_context.rounding = ROUND_HALF_UP
        price = self.get_lot_sale_price()
        return None

    def get_amount(self,name):
        round_context = getcontext()
        round_context.rounding = ROUND_HALF_UP
        price = self.get_lot_price()
        return None
    
class QtType(ModelSQL, ModelView):
    "Type"
    __name__ = 'lot.qt.type'
    name = fields.Char("Name")
    sequence = fields.Integer("Sequence")

class QtHist(ModelSQL, ModelView):
    "Quantities"
    __name__ = 'lot.qt.hist'

    lot = fields.Many2One(
        'lot.lot', "Lot", ondelete='CASCADE',
        )
    quantity_type = fields.Many2One('lot.qt.type',"Type")
    quantity = fields.Numeric("Net weight",digits=(1,5))
    gross_quantity = fields.Numeric("Gross weight",digits=(1,5))

class LotSplitMerge(ModelSQL, ModelView):
    "Lot Split Merge"
    __name__ = 'lot.split.merge'

    operation = fields.Selection([
        ('split', 'Split'),
        ('merge', 'Merge'),
    ], 'Operation', required=True)

    source_lot = fields.Many2One(
        'lot.lot', 'Source Lot', required=True, ondelete='CASCADE'
    )
    target_lot = fields.Many2One(
        'lot.lot', 'Target Lot', required=True, ondelete='CASCADE'
    )

    lot_qt = fields.Float('Elements count')
    quantity = fields.Numeric('Weight', digits=(16, 5))

    operation_date = fields.Date('Date', required=True)
    reversed_by = fields.Many2One(
        'lot.split.merge', 'Reversed By'
    )

class SplitLine(ModelView):
    "Split Line"
    __name__ = 'lot.split.wizard.line'

    name = fields.Char('Lot name')
    lot_qt = fields.Float('Quantity')
    weight = fields.Numeric('Weight', digits=(16,5))

class SplitWizardStart(ModelView):
    "Split Line Start"
    __name__ = 'lot.split.wizard.start'

    mode = fields.Selection([
        ('equal', 'Equal parts'),
        ('manual', 'Manual'),
    ], 'Mode', required=True)

    parts = fields.Integer(
        'Parts',
        states={'required': Eval('mode') == 'equal'}
    )

    create_remainder = fields.Boolean(
        'Create remainder lot',
        help='If checked, a remainder lot will be created when the split '
             'does not exactly match the original weight.'
    )

    lines = fields.One2Many(
        'lot.split.wizard.line',
        None,
        'Lines',
        states={'invisible': Eval('mode') != 'manual'}
    )

class SplitWizard(Wizard):
    "Lot Split Wizard"
    __name__ = 'lot.split.wizard'

    start = StateView(
        'lot.split.wizard.start',
        'lot.split_wizard_start_view',
        [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Split', 'split', 'tryton-ok', default=True),
        ]
    )

    split = StateTransition()
    def transition_split(self):
        Lot = Pool().get('lot.lot')
        lot = Lot(Transaction().context['active_id'])

        if self.start.mode == 'equal':
            part = lot.get_current_quantity() / self.start.parts
            splits = [
                {'quantity': part, 'name': f'{lot.lot_name}-{i+1}'}
                for i in range(self.start.parts)
            ]
        else:
            splits = [
                {'quantity': l.weight, 'lot_qt': l.lot_qt, 'name': l.name}
                for l in self.start.lines
            ]

        lot.split_by_weight(splits)
        return 'end'

class SplitWizard(Wizard):
    __name__ = 'lot.split.wizard'

    start = StateView(
        'lot.split.wizard.start',
        'lot.split_wizard_start_view',
        [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Split', 'split', 'tryton-ok', default=True),
        ]
    )

    split = StateTransition()

    def transition_split(self):
        Lot = Pool().get('lot.lot')
        lot = Lot(Transaction().context['active_id'])

        total_weight = lot.get_current_quantity().quantize(
            Decimal('0.00001'), rounding=ROUND_HALF_UP
        )

        splits = []
        allocated = Decimal('0.0')

        # -------------------------------------------------
        # EQUAL MODE
        # -------------------------------------------------
        if self.start.mode == 'equal':
            if not self.start.parts or self.start.parts <= 0:
                raise UserError('Number of parts must be greater than zero.')

            base = (total_weight / self.start.parts).quantize(
                Decimal('0.00001'), rounding=ROUND_HALF_UP
            )

            for i in range(self.start.parts):
                qty = base
                if i == self.start.parts - 1:
                    qty = total_weight - allocated  # absorbe le reste

                splits.append({
                    'quantity': qty,
                    'lot_qt': lot.lot_qt / self.start.parts if lot.lot_qt else None,
                    'name': f'{lot.lot_name}-{i + 1}',
                })
                allocated += qty

        # -------------------------------------------------
        # MANUAL MODE
        # -------------------------------------------------
        else:
            for line in self.start.lines:
                if not line.weight or line.weight <= 0:
                    raise UserError('Each line must have a positive weight.')

                splits.append({
                    'quantity': line.weight,
                    'lot_qt': line.lot_qt,
                    'name': line.name,
                })
                allocated += line.weight

            diff = (total_weight - allocated).quantize(
                Decimal('0.00001'), rounding=ROUND_HALF_UP
            )

            if diff != 0:
                if self.start.create_remainder and diff > 0:
                    splits.append({
                        'quantity': diff,
                        'lot_qt': None,
                        'name': f'{lot.lot_name}-REMAINDER',
                    })
                else:
                    raise UserError(
                        f'Split does not match the lot weight.\n'
                        f'Remaining difference: {diff}'
                    )

        lot.split_by_weight(splits)
        return 'end'

class MergeLotsStart(ModelView):
    "Merge Lots Start"
    __name__ = 'lot.merge.wizard.start'

    confirm = fields.Boolean('Confirm merge')


class MergeLots(Wizard):
    "Merge Lots"
    __name__ = 'lot.merge.wizard'

    start = StateView(
        'lot.merge.wizard.start',
        'lot.lot_merge_wizard_start_view',
        [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Merge', 'merge', 'tryton-ok', default=True),
        ]
    )

    merge = StateTransition()

    def transition_merge(self):
        Lot = Pool().get('lot.lot')
        ids = Transaction().context['active_ids']
        lots = Lot.browse(ids)

        Lot.merge_lots(lots)
        return 'end'
