# 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.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from itertools import chain, groupby
from operator import itemgetter
import datetime
from collections import defaultdict
from sql import Table
from trytond.modules.purchase_trade.service import ContractFactory
import requests
import io
import base64
import logging
import json
import re
import html


logger = logging.getLogger(__name__)

class Location(metaclass=PoolMeta):
    __name__ = 'stock.location'

    @classmethod
    def getLocationByName(cls, location, type):
        location = location.upper()
        loc = cls.search([('name', '=', location),('type', '=', type)], limit=1)
        if loc:
            return loc[0].id
        else:
            loc = cls()
            loc.name = location
            loc.type = type
            cls.save([loc])
            return loc
        
    @classmethod
    def get_transit_id(cls):
        return cls.getLocationByName('TRANSIT','storage')
        
    def is_transit(self):
        if self.name == 'Transit':
            return True

class Move(metaclass=PoolMeta):
    __name__ = 'stock.move'

    bldate = fields.Date("BL date")
    lotqt = fields.One2Many('lot.qt','lot_move',"Lots")
    lot = fields.Many2One('lot.lot',"Lot")

    def get_linked_transit_move(self):
        if self.from_location.is_transit():
            Move = Pool().get('stock.move')
            Location = Pool().get('stock.location')
            moves = Move.search([('lot','=',self.lot),('to_location','=',Location.get_transit_id())],order=[('id', 'DESC')],limit=1)
            return moves[0] if moves else None

    @classmethod
    def validate(cls, moves):
        super(Move, cls).validate(moves)
        Lot = Pool().get('lot.lot')
        LotMove = Pool().get('lot.move')
        for move in moves:
            if move.lot:
                l = Lot(move.lot.id)
                lm = LotMove.search([('lot','=',l.id),('move','=',move)])
                if len(lm) == 0:
                    lm = LotMove.search(['lot','>',0],order=[('sequence','DESC')])
                    new_lm = LotMove()
                    new_lm.move = move.id
                    new_lm.lot = l.id
                    new_lm.sequence = 1
                    if lm:
                        new_lm.sequence = lm[0].sequence + 1
                    
                    LotMove.save([new_lm])

class InvoiceLine(metaclass=PoolMeta):
    __name__ = 'account.invoice.line'

    lot = fields.Many2One('lot.lot',"Lot")
    fee = fields.Many2One('fee.fee',"Fee")

    @classmethod
    def validate(cls, lines):
        super(InvoiceLine, cls).validate(lines)
        Lot = Pool().get('lot.lot')
        for line in lines:
            if line.lot and line.quantity > 0:
                l = Lot(line.lot.id)
                if line.description == 'Pro forma':
                    if line.invoice_type == 'in':
                        l.invoice_line_prov = line.id
                        l.lot_pur_inv_state = 'prov'
                    else:
                        l.sale_invoice_line_prov = line.id
                        l.lot_sale_inv_state = 'prov'
                elif line.description == 'Final':
                    if line.invoice_type == 'in':
                        l.invoice_line = line.id
                        l.lot_pur_inv_state = 'invoiced'
                    else:
                        l.sale_invoice_line = line.id
                        l.lot_sale_inv_state = 'invoiced'
                Lot.save([l])

class ShipmentInternal(metaclass=PoolMeta):
    __name__ = 'stock.shipment.internal'
    fees = fields.One2Many('fee.fee','shipment_internal',"Fees")

sof_json = [
            {
                "date": "2025-02-02",
                "time": "19:00",
                "event": "S/B FOR ARRIVAL - PROCEED TO CAK NO.2 ANCHORAGE",
                "end_time": "2025-02-02 21:30",
                "duration": {"iso_8601": "PT26H30M", "text": "2h 30m"},
            },
            {
                "date": "2025-02-02",
                "time": "21:30",
                "event": "DROP ANCHOR AT CAK NO.2 ANCHOR / ARR. CAN NOR TENDERED",
                "end_time": "2025-02-02 21:40",
                "duration": {"iso_8601": "-PT23H50M", "text": "10m"},
            },
            {
                "date": "2025-02-02",
                "time": "21:40",
                "event": "BROUGHT UP ANCHOR/F.W.E",
                "end_time": "2025-02-03 07:20",
                "duration": {"iso_8601": "PT9H40M", "text": "9h 40m"},
            },
            {
                "date": "2025-02-03",
                "time": "07:20",
                "event": "S.B.E. FOR SHIFTING",
                "end_time": "2025-02-03 07:45",
                "duration": {"iso_8601": "PT0H25M", "text": "25m"},
            },
            {
                "date": "2025-02-03",
                "time": "07:45",
                "event": "ANCHOR AWEIGHT",
                "end_time": "2025-02-03 10:45",
                "duration": {"iso_8601": "PT3H0M", "text": "3h"},
            },
            {
                "date": "2025-02-03",
                "time": "10:45",
                "event": "SEA PILOT ON BOARD",
                "end_time": "2025-02-03 14:00",
                "duration": {"iso_8601": "PT3H15M", "text": "3h 15m"},
            },
            {
                "date": "2025-02-03",
                "time": "14:00",
                "event": "EXCHANGED SEA PILOT TO RIVER PILOT",
                "end_time": "2025-02-03 17:45",
                "duration": {"iso_8601": "PT3H45M", "text": "3h 45m"},
            },
            {
                "date": "2025-02-03",
                "time": "17:45",
                "event": "DROP ANCHOR AT NANTONS DANGEROUS ANCHORAGE",
                "end_time": "2025-02-03 18:00",
                "duration": {"iso_8601": "PT0H15M", "text": "15m"},
            },
            {
                "date": "2025-02-03",
                "time": "18:00",
                "event": "BROUGHT UP ANCHOR/F.W.E/PILOT OFF",
                "end_time": "2025-02-04 06:30",
                "duration": {"iso_8601": "PT12H30M", "text": "12h 30m"},
            },
            {
                "date": "2025-02-04",
                "time": "06:30",
                "event": "S.B.E. FOR BERTHING",
                "end_time": "2025-02-04 07:10",
                "duration": {"iso_8601": "PT0H40M", "text": "40m"},
            },
            {
                "date": "2025-02-04",
                "time": "07:10",
                "event": "PILOT ON BOARD",
                "end_time": "2025-02-04 07:15",
                "duration": {"iso_8601": "PT0H5M", "text": "5m"},
            },
            {
                "date": "2025-02-04",
                "time": "07:15",
                "event": "ANCHOR AWEIGHT",
                "end_time": "2025-02-04 09:20",
                "duration": {"iso_8601": "PT2H5M", "text": "2h 5m"},
            },
            {
                "date": "2025-02-04",
                "time": "09:20",
                "event": "FWD TUG MADE FAST",
                "end_time": "2025-02-04 09:35",
                "duration": {"iso_8601": "PT0H15M", "text": "15m"},
            },
            {
                "date": "2025-02-04",
                "time": "09:35",
                "event": "FIRST LINE ASHORE",
                "end_time": "2025-02-04 08:50",
                "duration": {
                    "iso_8601": "-PT0H45M",
                    "text": "Incohérence temporelle",
                    "alert": "Vérifier l'heure de TUG OFF",
                },
            },
            {
                "date": "2025-02-04",
                "time": "08:50",
                "event": "TUG OFF",
                "end_time": "2025-02-04 09:55",
                "duration": {"iso_8601": "PT1H5M", "text": "1h 5m"},
            },
            {
                "date": "2025-02-04",
                "time": "09:55",
                "event": "ALL LINE MADE FAST/F.W.E / NOR ACCEPTED",
                "end_time": "2025-02-04 10:15",
                "duration": {"iso_8601": "PT0H20M", "text": "20m"},
            },
            {
                "date": "2025-02-04",
                "time": "10:15",
                "event": "GANGWAY DOWN/PILOT OFF",
                "end_time": "2025-02-04 10:20",
                "duration": {"iso_8601": "PT0H5M", "text": "5m"},
            },
            {
                "date": "2025-02-04",
                "time": "10:20",
                "event": "AGENT / BAMBRIATION ON BOARD",
                "end_time": "2025-02-04 11:10",
                "duration": {"iso_8601": "PT0H50M", "text": "50m"},
            },
            {
                "date": "2025-02-04",
                "time": "11:10",
                "event": "FREE-PRACTIQUE QUANTED",
                "end_time": "2025-02-04 11:40",
                "duration": {"iso_8601": "PT0H30M", "text": "30m"},
            },
            {
                "date": "2025-02-04",
                "time": "11:40",
                "event": "SURVEYOR & LOADING MASTER ON BOARD",
                "end_time": "2025-02-04 11:40",
                "duration": {
                    "iso_8601": "PT0H0M",
                    "text": "0m",
                    "note": "Début de SAFETY MEETING",
                },
            },
            {
                "date": "2025-02-04",
                "time": "11:40",
                "event": "SAFETY MEETING",
                "end_time": "2025-02-04 12:20",
                "duration": {"iso_8601": "PT0H40M", "text": "40m"},
            },
            {
                "date": "2025-02-04",
                "time": "11:45",
                "event": "TANK INSPECTION",
                "end_time": "2025-02-04 12:30",
                "duration": {"iso_8601": "PT0H45M", "text": "45m"},
            },
            {
                "date": "2025-02-04",
                "time": "13:10",
                "event": 'CARGO HOSE CONNECTED (3 x 8")',
                "end_time": "2025-02-04 13:25",
                "duration": {"iso_8601": "PT0H15M", "text": "15m"},
            },
            {
                "date": "2025-02-04",
                "time": "13:25",
                "event": "LEAKAGE TESTED",
                "end_time": "2025-02-04 13:45",
                "duration": {"iso_8601": "PT0H20M", "text": "20m"},
            },
            {
                "date": "2025-02-04",
                "time": "14:15",
                "event": "COMMENCED LOADING SULPHURIC ACID",
                "end_time": "2025-02-05 08:50",
                "duration": {"iso_8601": "PT18H35M", "text": "18h 35m"},
            },
            {
                "date": "2025-02-05",
                "time": "08:50",
                "event": "COMPLETED LOADING SULPHURIC ACID",
                "end_time": "2025-02-05 09:00",
                "duration": {"iso_8601": "PT0H10M", "text": "10m"},
            },
            {
                "date": "2025-02-05",
                "time": "08:50",
                "event": "AIR BLOWING",
                "end_time": "2025-02-05 09:00",
                "duration": {"iso_8601": "PT0H10M", "text": "10m"},
            },
            {
                "date": "2025-02-05",
                "time": "09:00",
                "event": "VILLAGING, SAMPLING AND CALCULATION",
                "end_time": "2025-02-05 11:00",
                "duration": {"iso_8601": "PT2H0M", "text": "2h"},
            },
            {
                "date": "2025-02-05",
                "time": "10:40",
                "event": 'HOSE OFF (308")',
                "end_time": "2025-02-05 11:40",
                "duration": {"iso_8601": "PT1H0M", "text": "1h"},
            },
            {
                "date": "2025-02-05",
                "time": "11:40",
                "event": "CARGO DOCUMENT COMPLETED",
                "end_time": "2025-02-05 11:40",
                "duration": {"iso_8601": "PT0H0M", "text": "Événement final"},
            },
        ]

class ContainerType(ModelSQL, ModelView):
    "Container Type"
    __name__ = 'stock.container.type'

    code = fields.Char('Code', required=True)
    name = fields.Char('Description', required=True)

    teu = fields.Numeric('TEU Factor', digits=(5, 2), required=True)

    is_reefer = fields.Boolean('Reefer')
    is_special = fields.Boolean('Special Equipment')

class ShipmentContainer(ModelSQL, ModelView):
    "Shipment Container"
    __name__ = 'stock.shipment.container'

    shipment = fields.Many2One(
        'stock.shipment.in', 'Shipment', required=True
    )

    container_type = fields.Many2One(
        'stock.container.type', 'Container Type', required=True
    )

    container_no = fields.Char('Container Number')
    quantity = fields.Integer('Quantity', required=True)
    seal_no = fields.Char('Seal Number')
    is_reefer = fields.Boolean('Reefer')

class ShipmentWR(ModelSQL,ModelView):
    "Shipment WR"
    __name__ = "shipment.wr"
    shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
    wr = fields.Many2One('weight.report',"WR")

class ShipmentIn(metaclass=PoolMeta):
    __name__ = 'stock.shipment.in'

    from_location = fields.Many2One('stock.location', 'From location')
    to_location = fields.Many2One('stock.location', 'To location')
    transport_type = fields.Selection([
        ('vessel', 'Vessel'),
        ('truck', 'Truck'),
        ('other', 'Other'),
    ], 'Transport type')
    vessel = fields.Many2One('trade.vessel',"Vessel")
    info = fields.Function(fields.Text("Info",states={'invisible': ~Eval('info',False)}),'get_info')
    anim = fields.Function(fields.Text(""),'get_anim')
    carte = fields.Function(fields.Text("",states={'invisible': ~Eval('info',False)}),'get_imo')
    fees = fields.One2Many('fee.fee','shipment_in',"Fees")
    lotqt = fields.One2Many('lot.qt','lot_shipment_in',"Lots")
    quantity = fields.Function(fields.Numeric("Quantity"),'get_quantity')
    unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
    bl_date = fields.Date("BL date")
    bl_number = fields.Char("BL number")
    etl = fields.Date("Est. Loading")
    eta = fields.Date("🏗️ETA POL")
    etad = fields.Date("⚓ETA")
    atad = fields.Date("Act. Destination")
    etd = fields.Date("ETD")
    unloaded = fields.Date("Act. Discharge")
    booking = fields.Char("Booking Nb")
    booking_date =fields.Date("Booking date")
    ref = fields.Char("Our reference")
    note = fields.Text("Notes")
    dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
    himself = fields.Function(fields.Many2One('stock.shipment.in',"Shipment"),'get_sh')
    sof = fields.One2Many('sof.statement', 'shipment',"Demurrage calculations")
    del_from = fields.Date("Delivery period from")
    del_to = fields.Date("to")
    estimated_date = fields.One2Many('pricing.estimated','shipment_in',"Estimated date")
    carrier_ = fields.Many2One('party.party',"Carrier")
    start_date = fields.Date("Start date")
    travel_nb = fields.Char("Travel nb")
    receive_date = fields.Date("Reception date")
    receive_nb = fields.Char("Reception nb")
    cargo_mode = fields.Selection([
        ('bulk', 'Bulk'),
        ('container', 'Container'),
    ], 'Cargo Mode', required=True)

    vessel_type = fields.Function(fields.Many2One('stock.vessel.type',"Vessel type"),'get_vessel_type')
    container = fields.One2Many(
        'stock.shipment.container',
        'shipment',
        'Container'
    )
    shipment_wr = fields.One2Many('shipment.wr','shipment_in',"WR")
    controller = fields.Many2One('party.party',"Controller")
    send_instruction = fields.Boolean("Send instruction")
    instructions = fields.Text("Instructions")
    add_bl = fields.Boolean("Add BL")
    add_invoice = fields.Boolean("Add invoice")
    returned_id = fields.Char("Returned ID")
    result = fields.Char("Result",readonly=True)

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._buttons.update({
            'compute': {},
            'send': {},
        })

    def get_vessel_type(self,name=None):
        if self.vessel:
            return self.vessel.vessel_type

    def get_rec_name(self, name=None):
        if self.number:
            return self.number + '[' + (self.vessel.vessel_name if self.vessel else '') +  (('-' + self.travel_nb) if self.travel_nb else '') + ']'
        else:
            return str(self.id)

    def create_fee(self,controller):
        Fee = Pool().get('fee.fee')
        Product = Pool().get('product.product')
        fee = Fee()
        fee.shipment_in = self.id
        fee.supplier = controller
        fee.type = 'budgeted'
        fee.p_r = 'pay'
        fee.mode = 'perqt'
        fee.currency = 1
        fee.product = Product.get_by_name('Reweighing')
        fee.price = controller.get_sla_cost(self.to_location)
        Fee.save([fee])

    def get_controller(self):
        ControllerCategory = Pool().get('party.category')
        PartyCategory = Pool().get('party.party-party.category')
        cc = ControllerCategory.search(['name','=','CONTROLLER'])
        if cc:
            cc = cc[0]
            controllers = PartyCategory.search(['category','=',cc.id])
            for c in controllers:
                if c.party.IsAvailableForControl(self):
                    return c.party
    
    def get_instructions_html(self,inv_date,inv_nb):
        vessel = self.vessel.vessel_name if self.vessel else ""
        lines = [
            "<p>Hi,</p>",
            "<p>Please find details below for the requested control</p>",
        ]
        lines.append(
                "<p>"
                f"<strong>BL number:</strong> {self.bl_number} | "
                f"<strong>Vessel:</strong> {vessel} | "
                f"<strong>ETA:</strong> {self.etad}"
                "</p>"
            )

        if self.incoming_moves:
            tot_net = sum([m.lot.get_current_quantity() for m in self.incoming_moves])
            tot_gross = sum([m.lot.get_current_gross_quantity() for m in self.incoming_moves])
            tot_bale = sum([m.lot.lot_qt for m in self.incoming_moves])
            customer = self.incoming_moves[0].lot.sale_line.sale.party.name if self.incoming_moves[0].lot.sale_line else ""
            unit = self.incoming_moves[0].lot.lot_unit_line.symbol
            lines.append("<p>"
                f"<strong>Customer:</strong> {customer} | "
                f"<strong>Invoice Nb:</strong> {inv_nb} | "
                f"<strong>Invoice Date:</strong> {inv_date}"
                "</p>"
            )
            lines.append(
                "<p>"
                f"<strong>Nb Bales:</strong> {tot_bale} | "
                f"<strong>Net Qt:</strong> {tot_net} {unit} | "
                f"<strong>Gross Qt:</strong> {tot_gross} {unit}"
                "</p>"
            )

        return "".join(lines)

    # def get_instructions(self):
    #     lines = [
    #         "Hi,",
    #         "",
    #         "Please find details below for the requested control",
    #         f"BL number: {self.bl_number}",
    #         ""
    #     ]

    #     if self.incoming_moves:
    #         for m in self.incoming_moves:
    #             if m.lot:
    #                 lines.append(
    #                     f"Lot nb: {m.lot.lot_name} | "
    #                     f"Net Qt: {m.lot.get_current_quantity()} {m.lot.lot_unit.symbol} | "
    #                     f"Gross Qt: {m.lot.get_current_gross_quantity()} {m.lot.lot_unit.symbol}"
    #                 )

    #     return "\n".join(lines)
            
    def _create_lots_from_fintrade(self):
        t = Table('freight_booking_lots')
        cursor = Transaction().connection.cursor()
        query = t.select(
            t.BOOKING_NUMBER,
            t.LOT_NUMBER,
            t.LOT_NBR_BALES,
            t.LOT_GROSS_WEIGHT,
            t.LOT_NET_WEIGHT,
            t.LOT_UOM,
            t.LOT_QUALITY,
            t.CUSTOMER,
            t.SELL_PRICE_CURRENCY,
            t.SELL_PRICE_UNIT,
            t.SELL_PRICE,
            t.SALE_INVOICE,
            t.SELL_INV_AMOUNT,
            t.SALE_INVOICE_DATE,
            t.SELL_PREMIUM,
            t.SALE_CONTRACT_NUMBER,
            t.SALE_DECLARATION_KEY,
            t.SHIPMENT_CHUNK_KEY,
            where=(t.BOOKING_NUMBER == int(self.reference))
        )
        cursor.execute(*query)
        rows = cursor.fetchall()
        logger.info("ROWS:%s",rows)
        inv_date = None
        inv_nb = None
        if rows:
            sale_line = None
            for row in rows:
                logger.info("ROW:%s",row)
                #Purchase & Sale creation
                LotQt = Pool().get('lot.qt')
                Lot = Pool().get('lot.lot')
                LotAdd = Pool().get('lot.add.line')
                Currency = Pool().get('currency.currency')
                Product = Pool().get('product.product')
                Party = Pool().get('party.party')
                Uom = Pool().get('product.uom')
                Sale = Pool().get('sale.sale')
                SaleLine = Pool().get('sale.line')
                dec_key = str(row[16]).strip()
                chunk_key = str(row[17]).strip()
                lot_unit = str(row[5]).strip().lower()
                product = str(row[6]).strip().upper()
                lot_net_weight = Decimal(row[4])
                logger.info("LOT_NET_WEIGHT:%s",lot_net_weight)
                lot_gross_weight = Decimal(row[3])
                lot_bales = Decimal(row[2])
                lot_number = row[1] 
                customer = str(row[7]).strip().upper()
                sell_price_currency = str(row[8]).strip().upper()
                sell_price_unit = str(row[9]).strip().lower()
                inv_date = str(row[13]).strip()
                inv_nb = str(row[11]).strip()
                sell_price = Decimal(row[10])
                premium = Decimal(row[14])
                reference = Decimal(row[15])
                logger.info("DECLARATION_KEY:%s",dec_key)
                declaration = SaleLine.search(['note','=',dec_key])
                if declaration:
                    sale_line = declaration[0]
                    logger.info("WITH_DEC:%s",sale_line)
                    vlot = sale_line.lots[0]
                    lqt = LotQt.search([('lot_s','=',vlot.id)])
                    if lqt:
                        for lq in lqt:
                            if lq.lot_p:
                                logger.info("VLOT_P:%s",lq.lot_p)
                                sale_line.quantity_theorical += round(lot_net_weight,2) 
                                SaleLine.save([sale_line])
                                lq.lot_p.updateVirtualPart(round(lot_net_weight,2),self,lq.lot_s)
                                vlot.set_current_quantity(round(lot_net_weight,2),round(lot_gross_weight,2),1)
                                Lot.save([vlot])
                else:
                    sale = Sale()
                    sale_line = SaleLine()
                    sale.party = Party.getPartyByName(customer,'CLIENT')
                    logger.info("SALE_PARTY:%s",sale.party)
                    sale.reference = reference
                    sale.from_location = self.from_location
                    sale.to_location = self.to_location
                    sale.company = 6
                    if sale.party.addresses:
                        sale.invoice_address = sale.party.addresses[0]
                        sale.shipment_address = sale.party.addresses[0]
                    
                    if sell_price_currency == 'USC':
                        sale.currency = Currency.get_by_name('USD')
                        sale_line.enable_linked_currency = True
                        sale_line.linked_currency = 1
                        sale_line.linked_unit = Uom.get_by_name(sell_price_unit)
                        sale_line.linked_price = round(sell_price,4)
                        sale_line.unit_price = sale_line.get_price_linked_currency()
                    else:
                        sale.currency = Currency.get_by_name(sell_price_currency)
                        sale_line.unit_price = round(sell_price,4)
                        sale_line.unit = Uom.get_by_name(sell_price_unit)
                        sale_line.premium = premium
                    Sale.save([sale])
                    sale_line.sale = sale.id
                    sale_line.quantity = round(lot_net_weight,2)
                    sale_line.quantity_theorical = round(lot_net_weight,2)
                    sale_line.product = Product.get_by_name('BRAZIL COTTON')
                    logger.info("PRODUCT:%s",sale_line.product)
                    sale_line.unit = Uom.get_by_name(lot_unit)
                    sale_line.price_type = 'priced'
                    sale_line.created_by_code = False
                    sale_line.note = dec_key
                    SaleLine.save([sale_line])

                    #need to link the virtual part to the shipment
                    lqt = LotQt.search([('lot_s','=',sale_line.lots[0])])
                    if lqt:
                        lqt[0].lot_shipment_in = self
                        LotQt.save(lqt)
                        logger.info("SALE_LINKED_TO_SHIPMENT:%s",self)

                    ContractStart = Pool().get('contracts.start')
                    ContractDetail = Pool().get('contract.detail')
                    ct = ContractStart()
                    d = ContractDetail()
                    ct.type = 'Purchase'
                    ct.matched = True
                    ct.shipment_in = self
                    ct.lot = sale_line.lots[0]
                    ct.product = sale_line.product
                    ct.unit = sale_line.unit
                    d.party = Party.getPartyByName('FAIRCOT')
                    if sale_line.enable_linked_currency:
                        d.currency_unit = str(sale_line.linked_currency.id) + '_' + str(sale_line.linked_unit.id)
                    else:
                        d.currency_unit = str(sale.currency.id) + '_' + str(sale_line.unit.id)
                    d.quantity = sale_line.quantity
                    d.unit = sale_line.unit
                    d.price = sale_line.unit_price
                    d.price_type = 'priced'
                    d.crop = None
                    d.tol_min = 0
                    d.tol_max = 0
                    d.incoterm = None
                    d.reference = str(sale.id)
                    d.from_location = sale.from_location
                    d.to_location = sale.to_location
                    d.del_period = None
                    d.from_del = None
                    d.to_del = None
                    ct.contracts = [d]
                    ContractFactory.create_contracts(
                        ct.contracts,
                        type_=ct.type,
                        ct=ct,
                    )
            
                #Lots creation
                vlot = sale_line.lots[0]
                lqt = LotQt.search([('lot_s','=',vlot.id),('lot_p','>',0)])
                if lqt and vlot.lot_quantity > 0:
                    lqt = lqt[0]
                    l = LotAdd()
                    l.lot_qt = lot_bales
                    l.lot_unit = Uom.get_by_name('bale')
                    l.lot_unit_line = Uom.get_by_name(lot_unit)
                    l.lot_quantity = round(lot_net_weight,2)
                    l.lot_gross_quantity = round(lot_gross_weight,2)
                    l.lot_premium = premium
                    l.lot_chunk_key = int(chunk_key)
                    logger.info("ADD_LOT:%s",int(chunk_key))
                    LotQt.add_physical_lots(lqt,[l])

            return inv_date,inv_nb

    def html_to_text(self,html_content):
        text = re.sub(r"<br\s*/?>", "\n", html_content, flags=re.IGNORECASE)
        text = re.sub(r"</p\s*>", "\n\n", text, flags=re.IGNORECASE)
        text = re.sub(r"<[^>]+>", "", text)
        return html.unescape(text).strip()

    @classmethod
    @ModelView.button
    def send(cls, shipments):
        Date = Pool().get('ir.date')
        Attachment = Pool().get('ir.attachment')
        
        for sh in shipments:
            sh.result = "Email not sent"
            attachment = []
            if sh.add_bl:
                attachments = Attachment.search([
                    ('resource', '=', 'stock.shipment.in,' + str(sh.id)),
                ])
                if attachments:
                    content_b64 = base64.b64encode(attachments[0].data).decode('ascii')
                    attachment = [
                            {
                                "filename": attachments[0].name,
                                "content": content_b64,
                                "content_type": "application/pdf"
                            }
                        ]
            
            if sh.controller:
                Contact = Pool().get('party.contact_mechanism')
                contact = Contact.search(['party','=',sh.controller.id])
                if contact:
                    payload = {
                        "to": [contact[0].value],
                        "subject": "Request for control",
                        "body": sh.html_to_text(sh.instructions),
                        "attachments": attachment,
                        "meta": {
                            "shipment": sh.bl_number,
                            "controller": sh.controller.id
                        }
                    }

                    response = requests.post(
                        "http://automation-service:8006/mail",
                        json=payload,
                        timeout=10
                    )

                    response.raise_for_status()
                    data = response.json()
                    logger.info("SEND_FROM_SHIPMENT:%s",data)
                    now = datetime.datetime.now()
                    sh.result = f"Email sent on {now.strftime('%d/%m/%Y %H:%M')}"
                    sh.save()

    @classmethod
    @ModelView.button
    def compute(cls, shipments):
        Sof = Pool().get('sof.statement')       
        for sh in shipments:
            if sh.sof:
                for s in sh.sof:
                    if s.laytime_clause_hour:
                        t = s.laytime_clause_hour  # instance de datetime.time
                        delta = datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second)
                        s.laytime_commenced = s.notice_of_readiness_time + delta
                        s.laytime_allowed = round(s.quantity / s.pumping_rate_o,2)
                        s.laytime_completed = s.hoses_disconnected
                        #s.laytime_completed = s.laytime_commenced + datetime.timedelta(hours=s.laytime_allowed)
                        total_time = (s.laytime_completed - s.laytime_commenced).total_seconds() / 3600.0
                        logger.info("COMPUTE_DEDUCTION3:%s",total_time)
                        s.actual_time_used = round(total_time,2)
                        s.deductions = round(sum([e.duration for e in s.sof_events if e.deductible == True]),2)
                        effective_time = round(total_time - s.deductions,2)
                        s.laytime_used = effective_time
                        s.laytime_balance = round(s.laytime_allowed - s.laytime_used,2)
                        penalty = Decimal(s.laytime_balance) * (s.demurrage_rate_o / 24)
                        s.compensation_amount = round(penalty, 2)
                        Sof.save([s])

    def get_vessel_url(self):
        return f"https://www.vesselfinder.com/en/vessels/VOS-TRAVELLER-IMO-{self.vessel.vessel_imo}"
        #return URL(f"https://www.vesselfinder.com/en/vessels/VOS-TRAVELLER-IMO-{self.vessel.vessel_imo}", target='new')

    def get_imo(self,name):
        if self.vessel:
            return f"imo:{self.vessel.vessel_imo}"

    def get_sh(self, name):
        return self.id

    @classmethod
    def default_transport_type(cls):
        return 'vessel'

    @classmethod
    def default_dashboard(cls):
        return 1

    def get_quantity(self,name=None):
        if self.incoming_moves:
            return sum([(e.quantity if e.quantity else 0) for e in self.incoming_moves])

    def get_unit(self,name=None):
        if self.incoming_moves:
            return self.incoming_moves[0].unit

    def get_info(self,name):
        if self.vessel:
            vessel = Pool().get('trade.vessel')(self.vessel)
            return vessel.get_info()

    def get_anim(self,name):
        gif_url = "http://vps107.geneva.hosting:8000/images/tanker.gif"
        anim = f'<br><img src="{gif_url}" alt="loading..." style="margin-top:10px; width:80px;">'
        return anim
        
    def extract_sof_data_and_events_from_json(self, sof_json):
        def parse_datetime(date_str, time_str):
            dt = datetime.datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
            return dt 
    
        def find_first_event(keywords):
            for item in sof_json:
                if any(kw.lower() in item['event'].lower() for kw in keywords):
                    return parse_datetime(item['date'], item['time']) - datetime.timedelta(hours=1)
            return None

        def find_last_event(keywords):
            for item in reversed(sof_json):
                if any(kw.lower() in item['event'].lower() for kw in keywords):
                    return parse_datetime(item['date'], item['time'])
            return None

        events = []
        for item in sof_json:
            try:
                start = parse_datetime(item['date'], item['time'])
                end_str = item.get("end_time")
                end = datetime.datetime.strptime(end_str, "%Y-%m-%d %H:%M") if end_str else None

                description = item["event"]
                duration = (end - start).total_seconds() / 3600 if end else 0
                deductible = any(word in description.lower() for word in ['rain', 'waiting', 'no loading'])

                events.append({
                    'start': start,
                    'start_date': start.date(),
                    'start_time': start.time(),
                    'end': end,
                    'end_date': end.date() if end else None,
                    'end_time': end.time() if end else None,
                    'duration': duration,
                    'description': description,
                    'deductible': deductible,
                })
            except Exception as e:
                logger.error("Error parsing SOF event: %s | %s", item, e)

        # Extraction par mots-clés (inchangé)
        arrival = find_first_event(["arrived", "anchor at", "proceed to anchorage"])
        nor = find_first_event(["nor tendered", "nor accepted"])
        start_pump = find_first_event(["start pumping", "commenced loading"])
        end_pump = find_first_event(["end pumping", "completed loading"])
        hoses_connected = find_first_event(["hose connected"])
        hoses_disconnected = find_first_event(["hose off", "hose disconnected"])
        sailing_time = find_first_event(["sailing", "sb for sailing", "sea pilot on board"])

        return {
            'arrival_time': arrival,
            'notice_of_readiness_time': nor,
            'start_pumping': start_pump,
            'end_pumping': end_pump,
            'hoses_connected': hoses_connected,
            'hoses_disconnected': hoses_disconnected,
            'sailing_time': sailing_time,
            'events': events,
        }

    @classmethod
    def validate(cls, shipments):
        super(ShipmentIn, cls).validate(shipments)
        Lot = Pool().get('lot.lot')
        StockMove = Pool().get('stock.move')
        Sof = Pool().get('sof.statement')
        SoFEvent = Pool().get("sof.event")
        for sh in shipments:
            lots = Lot.search(['lot_shipment_in','=',sh.id])
            if not lots:
                if sh.lotqt:
                    lots = [sh.lotqt[0].lot_p]
            if lots:
                if sh.state == 'received':
                    for lot in lots:
                        if lot.lot_type == 'physic':
                            if sh.to_location.type == 'storage':
                                lot.lot_status = 'stock'
                            elif sh.to_location.type == 'supplier':
                                lot.lot_status = 'destination'
                            elif sh.to_location.type == 'customer':
                                lot.lot_status = 'delivered'
                            lot.lot_shipment_in = None
                            lot.lot_av = 'available'
                            Lot.save([lot])
                elif sh.incoming_moves:
                    for m in sh.incoming_moves:
                        if sh.bl_date:
                            m.bldate = sh.bl_date
                            StockMove.save([m])
                        for lot in lots:
                            if lot.lot_type == 'physic': 
                                lot.lot_status = 'transit'
                                Lot.save([lot])
                #update line valuation
                Pnl = Pool().get('valuation.valuation')
                for lot in lots:
                    Pnl.generate(lot.line if lot.line else lot.sale_line)
            if sh.sof:
                for sof in sh.sof:
                    if sof.chart:
                        sof.laytime_type = 'nor'
                        sof.laytime_clause_hour = datetime.time(6,0)
                        sof.demurrage_rate = 26000
                        sof.pumping_rate = 350
                        sof.laytime_type_o = 'nor'
                        sof.laytime_clause_hour_o = datetime.time(6,0)
                        sof.demurrage_rate_o = 22000
                        sof.pumping_rate_o = 300
                        sof.timebar_day = 60
                        sof.timebar_warn = 0
                        Sof.save([sof])
                    if sof.sof:
                        extracted_sof = sh.extract_sof_data_and_events_from_json(sof_json)
                        sof.arrival_time = extracted_sof['arrival_time']
                        sof.notice_of_readiness_time = extracted_sof['notice_of_readiness_time']
                        sof.start_pumping = extracted_sof['start_pumping']
                        sof.end_pumping = extracted_sof['end_pumping']
                        sof.hoses_connected = extracted_sof['hoses_connected']
                        sof.hoses_disconnected = extracted_sof['hoses_disconnected']
                        sof.sailing_time = extracted_sof['sailing_time']
                        Sof.save([sof])
                        for e in extracted_sof["events"]:
                            SoFEvent.create(
                                [
                                    {
                                        "statement": sof.id,
                                        "start": e["start"],
                                        "start_date": e["start_date"],
                                        "start_time": e["start_time"],
                                        "end": e["end"],
                                        "end_date": e["end_date"],
                                        "end_time": e["end_time"],
                                        "duration": e["duration"],
                                        "description": e["description"],
                                        "deductible": e["deductible"],
                                    }
                                ]
                            )

class FindVessel(Wizard):
    __name__ = 'stock.shipment.in.vf'
    start_state = 'vf'
    vf = StateAction('purchase_trade.url_vessel_finder')

    def do_vf(self, action):
        action['url'] = self.record.get_vessel_url()
        return action, {}

class ShipmentOut(metaclass=PoolMeta):
    __name__ = 'stock.shipment.out'

    from_location = fields.Many2One('stock.location', 'From location')
    to_location = fields.Many2One('stock.location', 'To location')
    transport_type = fields.Selection([
        ('vessel', 'Vessel'),
        ('truck', 'Truck'),
        ('other', 'Other'),
    ], 'Transport type')
    vessel = fields.Many2One('trade.vessel',"Vessel")
    info = fields.Function(fields.Text("Info"),'get_info')
    fees = fields.One2Many('fee.fee','shipment_in',"Fees")
    lotqt = fields.One2Many('lot.qt','lot_shipment_in',"Lots")
    quantity = fields.Function(fields.Numeric("Quantity"),'get_quantity')
    unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
    etl = fields.Date("Est. Loading")
    eta = fields.Date("Est. Arrival")
    etd = fields.Date("Est. Discharge")
    unloaded = fields.Date("Unloaded")
    booking = fields.Char("Booking Nb")
    ref = fields.Char("Reference")
    note = fields.Text("Notes")

    @classmethod
    def default_transport_type(cls):
        return 'vessel'

    def get_quantity(self,name=None):
        if self.incoming_moves:
            return sum([(e.quantity if e.quantity else 0) for e in self.incoming_moves])

    def get_unit(self,name=None):
        if self.incoming_moves:
            return self.incoming_moves[0].unit

    def get_info(self,name):
        if self.vessel:
            vessel = Pool().get('trade.vessel')(self.vessel)
            return vessel.get_info()

from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import PoolMeta, Pool
from trytond.wizard import Wizard, StateView, StateAction, StateTransition, Button
from trytond.transaction import Transaction
from trytond.report import Report
from trytond.i18n import gettext
from trytond.pyson import Eval

import datetime
import io
import re
import PyPDF2

class StatementOfFacts(ModelSQL, ModelView):
    "Statement of Facts for Vessel Discharge"
    __name__ = 'sof.statement'

    # === A. Informations générales ===
    shipment = fields.Many2One('stock.shipment.in', 'Shipment', required=True, ondelete='CASCADE')
    loading_sequence = fields.Char('Loading Sequence')  # ex: "Lot A - Supplier X"

    # === Termes contractuels ===
    laytime_allowed = fields.Float('Laytime Allowed (hours)')
    notice_of_readiness_time = fields.DateTime('Notice of Readiness Time')

    laytime_type = fields.Selection([
        ('nor', 'NOR +'),
        (None, 'Laytime start'),
    ], 'Laytime clause')
    laytime_clause_hour = fields.Time("")
    laytime_start = fields.DateTime('Laytime Start')
    laytime_end = fields.DateTime('Laytime End')
    demurrage_rate = fields.Numeric('Demurrage Rate ($/day)', digits=(16, 2))
    pumping_rate = fields.Float('Pumping Rate (MT/hour)')

    laytime_type_o = fields.Selection([
        ('nor', 'NOR +'),
        (None, 'Laytime start'),
    ], 'Laytime clause')
    laytime_clause_hour_o = fields.Time("")
    laytime_start_o = fields.DateTime('Laytime Start')
    laytime_end_o = fields.DateTime('Laytime End')
    demurrage_rate_o = fields.Numeric('Demurrage Rate ($/day)', digits=(16, 2))
    pumping_rate_o = fields.Float('Pumping Rate (MT/hour)')

    timebar_day = fields.Integer("TimeBar")
    timebar_warn = fields.Integer("Warning TimeBar")
    chart = fields.Many2One('document.incoming',"Charter Party Terms")
    sof = fields.Many2One('document.incoming',"Statement of facts")

    # === B. Événements du SOF ===
    sof_events = fields.One2Many('sof.event', 'statement', "SOF Events")

    arrival_time = fields.DateTime('Arrival Time')
    hoses_connected = fields.DateTime('Hoses Connected')
    start_pumping = fields.DateTime('Start Pumping')
    end_pumping = fields.DateTime('End Pumping')
    hoses_disconnected = fields.DateTime('Hoses Disconnected')
    sailing_time = fields.DateTime('Sailing Time')

    # === C. Résultats du calcul ===
    laytime_commenced = fields.DateTime('Laytime Commenced')
    laytime_completed = fields.DateTime('Laytime Completed')
    actual_time_used = fields.Float('Actual Time Used (hours)', readonly=True)
    deductions = fields.Float('Deductions (hours)', readonly=True)
    laytime_used = fields.Float('Laytime Used (hours)', readonly=True)
    laytime_balance = fields.Float('Laytime Balance (hours)', readonly=True)
    compensation_type = fields.Selection([
        ('demurrage', 'Demurrage'),
        ('despatch', 'Despatch'),
        (None, 'No Compensation'),
    ], 'Compensation Type')
    compensation_amount = fields.Numeric('Compensation Amount ($)', readonly=True, digits=(16, 2))
    quantity = fields.Function(fields.Float("Quantity loaded"),'get_qt')

    notes = fields.Text('Remarks / Notes')

    def get_qt(self,name):
        if self.shipment.incoming_moves:
            return sum([e.quantity for e in self.shipment.incoming_moves])

class SoFEvent(ModelSQL, ModelView):
    "Event from Statement of Facts"
    __name__ = 'sof.event'

    statement = fields.Many2One('sof.statement', "Statement", ondelete='CASCADE')

    start = fields.DateTime("Start Time")
    end = fields.DateTime("End Time")

    start_date = fields.Date("Start Date")
    start_time = fields.Time("Start Time (H)")
    end_date = fields.Date("End Date")
    end_time = fields.Time("End Time (H)")

    duration = fields.Float("Duration")
    description = fields.Char("Description")
    deductible = fields.Boolean("Deduct from Laytime")

class ImportSoFStart(ModelView):
    """Wizard Start View"""
    __name__ = 'import.s'

    pdf = fields.Binary('Statement of Facts PDF', required=True, filename='filename')
    filename = fields.Char('SoF Filename',readonly=True)
    charter = fields.Binary('Charter Party File', required=True, filename='charter_name')
    charter_name = fields.Char('Charter Filename',readonly=True)

class ImportSoFWizard(Wizard):
    "Import Statement of Facts Wizard"
    __name__ = 'sof.import'

    start = StateTransition()

    sof = StateView('import.s', 'purchase_trade.view_sof_import_start', [
        Button('Cancel', 'end', 'tryton-cancel'),
        Button('Import', 'processing', 'tryton-ok')
    ])

    processing = StateTransition()

    def transition_start(self):
        return 'sof'
        
    def parse_pdf_text(self, binary_data):
        text = ""
        with io.BytesIO(binary_data) as pdf_file:
            reader = PyPDF2.PdfReader(pdf_file)
            for page in reader.pages:
                text += page.extract_text()
        return text

    def parse_charter_text(self, binary_data):
        try:
            return binary_data.decode('utf-8')
        except Exception:
            return ''

    def extract_fields_from_text(self,text):
        def extract(pattern):
            match = re.search(pattern, text)
            return match.group(1).strip() if match else None

        vessel = extract(r"Vessel Name:\s*(.+)")
        port = extract(r"Port:\s*(.+)")
        berth = extract(r"Berth:\s*(.+)")
        cargo = extract(r"Cargo:\s*(.+)")
        charter_ref = extract(r"Charter Party Ref:\s*(.+)")
        laytime_allowed = extract(r"Laytime Allowed:\s*(\d+)\s*hours")
        demurrage = extract(r"Demurrage Rate:\s*\$(\d[\d,]*)/day")
        despatch = extract(r"Despatch Rate:\s*\$(\d[\d,]*)/day")
        laytime_start_raw = extract(r"Laytime Start:\s*([\d\-: ]+)")
        laytime_end_raw = extract(r"Laytime End:\s*([\d\-: ]+)")
        quantity_raw = extract(r"Quantity:\s*([\d,]+)\s*MT")
        quantity = float(quantity_raw.replace(',', '')) if quantity_raw else None

        laytime_start = datetime.datetime.strptime(laytime_start_raw, "%Y-%m-%d %H:%M").date() if laytime_start_raw else None
        laytime_end = datetime.datetime.strptime(laytime_end_raw, "%Y-%m-%d %H:%M").date() if laytime_end_raw else None

        return {
            'vessel_name': vessel,
            'port': port,
            'berth': berth,
            'cargo': cargo,
            'quantity': quantity,
            'charter_ref': charter_ref,
            'laytime_allowed': float(laytime_allowed) if laytime_allowed else None,
            'laytime_start': laytime_start,
            'laytime_end': laytime_end,
            'demurrage_rate': float(demurrage.replace(',', '')) if demurrage else None,
            'despatch_rate': float(despatch.replace(',', '')) if despatch else None,
        }

    def extract_sof_events(self, text):
        """
        Extrait les événements datés à partir du texte brut du SOF PDF.
        Format attendu (comme dans ton fichier) :
        '15.05.2024 13:00 - 15.05.2024 18:00 Loading stopped due to rain'
        """
        pattern = re.compile(
            r'(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2})\s*-\s*(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2})\s+(.+)',
            re.MULTILINE
        )
        events = []
        logger.info("TEXT FOR EVENTS:\n%s", text)
        matchs = pattern.finditer(text)
        logger.info("MATCHS:%s",matchs)
        for match in matchs:
            logger.debug("MATCH: %s | %s -> %s", match.group(5), match.group(1), match.group(3))
            start = datetime.datetime.strptime(f"{match.group(1)} {match.group(2)}", "%d.%m.%Y %H:%M")
            end = datetime.datetime.strptime(f"{match.group(3)} {match.group(4)}", "%d.%m.%Y %H:%M")
            description = match.group(5).strip()
            duration = (end - start).total_seconds() / 3600
            deductible = any(word in description.lower() for word in ['rain', 'waiting', 'no loading'])
            events.append({
                'start': start,
                'end': end,
                'duration': duration,
                'description': description,
                'deductible': deductible,
            })
        return events

    def extract_sof_data_and_events(self, text):
        def extract(pattern, fmt=None, default=None):
            match = re.search(pattern, text, re.IGNORECASE)
            if not match:
                logger.warning("Pattern not found: %s", pattern)
                return default
            raw = match.group(1).strip()
            try:
                return datetime.datetime.strptime(raw, fmt) if fmt else raw
            except Exception:
                logger.error("Failed to parse date: %s", raw)
                return default

        # Nouvelles regex plus souples
        arrival = extract(r"vessel.*arrived.*?(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})", "%d.%m.%Y %H:%M")
        nor = extract(r"nor.*tendered.*?(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})", "%d.%m.%Y %H:%M")
        start_pump = extract(r"start.*pumping.*?(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})", "%d.%m.%Y %H:%M")
        end_pump = extract(r"end.*pumping.*?(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})", "%d.%m.%Y %H:%M")

        logger.info("ARRIVAL: %s", arrival)
        logger.info("NOR: %s", nor)
        logger.info("START_PUMP: %s", start_pump)
        logger.info("END_PUMP: %s", end_pump)

        events = self.extract_sof_events(text)

        return {
            'arrival_time': arrival,
            'notice_of_readiness_time': nor,
            'start_pumping': start_pump,
            'end_pumping': end_pump,
            'hoses_connected': None,
            'hoses_disconnected': None,
            'sailing_time': None,
            'events': events,
        }

    def extract_sof_data_and_events_from_json(self, sof_json):
        def parse_datetime(date_str, time_str):
            return datetime.datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")

        def find_first_event(keywords):
            for item in sof_json:
                if any(kw.lower() in item['event'].lower() for kw in keywords):
                    return parse_datetime(item['date'], item['time'])
            return None

        def find_last_event(keywords):
            for item in reversed(sof_json):
                if any(kw.lower() in item['event'].lower() for kw in keywords):
                    return parse_datetime(item['date'], item['time'])
            return None

        events = []
        for item in sof_json:
            try:
                start = parse_datetime(item['date'], item['time'])
                end_str = item.get("end_time")
                end = datetime.datetime.strptime(end_str, "%Y-%m-%d %H:%M") if end_str else None

                description = item["event"]
                duration = (end - start).total_seconds() / 3600 if end else 0
                deductible = any(word in description.lower() for word in ['rain', 'waiting', 'no loading'])

                events.append({
                    'start': start,
                    'start_date': start.date(),
                    'start_time': start.time(),
                    'end': end,
                    'end_date': end.date() if end else None,
                    'end_time': end.time() if end else None,
                    'duration': duration,
                    'description': description,
                    'deductible': deductible,
                })
            except Exception as e:
                logger.error("Error parsing SOF event: %s | %s", item, e)

        # Extraction par mots-clés (inchangé)
        arrival = find_first_event(["arrived", "anchor at", "proceed to anchorage"])
        nor = find_first_event(["nor tendered", "nor accepted"])
        start_pump = find_first_event(["start pumping", "commenced loading"])
        end_pump = find_first_event(["end pumping", "completed loading"])
        hoses_connected = find_first_event(["hose connected"])
        hoses_disconnected = find_first_event(["hose off", "hose disconnected"])
        sailing_time = find_first_event(["sailing", "sb for sailing", "sea pilot on board"])

        return {
            'arrival_time': arrival,
            'notice_of_readiness_time': nor,
            'start_pumping': start_pump,
            'end_pumping': end_pump,
            'hoses_connected': hoses_connected,
            'hoses_disconnected': hoses_disconnected,
            'sailing_time': sailing_time,
            'events': events,
        }

    def transition_processing(self):
        pool = Pool()
        Statement = pool.get("sof.statement")
        SoFEvent = pool.get("sof.event")

        # Lecture des documents
        # text_sof = self.parse_pdf_text(self.sof.pdf)
        # logger.info("SOF TEXT:\n%s", text_sof)
        text_charter = self.parse_charter_text(self.sof.charter)

        # Extraction des champs
        # extracted_main = self.extract_fields_from_text(text_sof)
        # extracted_sof = self.extract_sof_data_and_events(text_sof)
        sof_json = [
            {
                "date": "2025-02-02",
                "time": "19:00",
                "event": "S/B FOR ARRIVAL - PROCEED TO CAK NO.2 ANCHORAGE",
                "end_time": "2025-02-03 21:30",
                "duration": {"iso_8601": "PT26H30M", "text": "26h 30m"},
            },
            {
                "date": "2025-02-02",
                "time": "21:30",
                "event": "DROP ANCHOR AT CAK NO.2 ANCHOR / ARR. CAN NOR TENDERED",
                "end_time": "2025-02-02 21:40",
                "duration": {"iso_8601": "-PT23H50M", "text": "10m"},
            },
            {
                "date": "2025-02-02",
                "time": "21:40",
                "event": "BROUGHT UP ANCHOR/F.W.E",
                "end_time": "2025-02-03 07:20",
                "duration": {"iso_8601": "PT9H40M", "text": "9h 40m"},
            },
            {
                "date": "2025-02-03",
                "time": "07:20",
                "event": "S.B.E. FOR SHIFTING",
                "end_time": "2025-02-03 07:45",
                "duration": {"iso_8601": "PT0H25M", "text": "25m"},
            },
            {
                "date": "2025-02-03",
                "time": "07:45",
                "event": "ANCHOR AWEIGHT",
                "end_time": "2025-02-03 10:45",
                "duration": {"iso_8601": "PT3H0M", "text": "3h"},
            },
            {
                "date": "2025-02-03",
                "time": "10:45",
                "event": "SEA PILOT ON BOARD",
                "end_time": "2025-02-03 14:00",
                "duration": {"iso_8601": "PT3H15M", "text": "3h 15m"},
            },
            {
                "date": "2025-02-03",
                "time": "14:00",
                "event": "EXCHANGED SEA PILOT TO RIVER PILOT",
                "end_time": "2025-02-03 17:45",
                "duration": {"iso_8601": "PT3H45M", "text": "3h 45m"},
            },
            {
                "date": "2025-02-03",
                "time": "17:45",
                "event": "DROP ANCHOR AT NANTONS DANGEROUS ANCHORAGE",
                "end_time": "2025-02-03 18:00",
                "duration": {"iso_8601": "PT0H15M", "text": "15m"},
            },
            {
                "date": "2025-02-03",
                "time": "18:00",
                "event": "BROUGHT UP ANCHOR/F.W.E/PILOT OFF",
                "end_time": "2025-02-04 06:30",
                "duration": {"iso_8601": "PT12H30M", "text": "12h 30m"},
            },
            {
                "date": "2025-02-04",
                "time": "06:30",
                "event": "S.B.E. FOR BERTHING",
                "end_time": "2025-02-04 07:10",
                "duration": {"iso_8601": "PT0H40M", "text": "40m"},
            },
            {
                "date": "2025-02-04",
                "time": "07:10",
                "event": "PILOT ON BOARD",
                "end_time": "2025-02-04 07:15",
                "duration": {"iso_8601": "PT0H5M", "text": "5m"},
            },
            {
                "date": "2025-02-04",
                "time": "07:15",
                "event": "ANCHOR AWEIGHT",
                "end_time": "2025-02-04 09:20",
                "duration": {"iso_8601": "PT2H5M", "text": "2h 5m"},
            },
            {
                "date": "2025-02-04",
                "time": "09:20",
                "event": "FWD TUG MADE FAST",
                "end_time": "2025-02-04 09:35",
                "duration": {"iso_8601": "PT0H15M", "text": "15m"},
            },
            {
                "date": "2025-02-04",
                "time": "09:35",
                "event": "FIRST LINE ASHORE",
                "end_time": "2025-02-04 08:50",
                "duration": {
                    "iso_8601": "-PT0H45M",
                    "text": "Incohérence temporelle",
                    "alert": "Vérifier l'heure de TUG OFF",
                },
            },
            {
                "date": "2025-02-04",
                "time": "08:50",
                "event": "TUG OFF",
                "end_time": "2025-02-04 09:55",
                "duration": {"iso_8601": "PT1H5M", "text": "1h 5m"},
            },
            {
                "date": "2025-02-04",
                "time": "09:55",
                "event": "ALL LINE MADE FAST/F.W.E / NOR ACCEPTED",
                "end_time": "2025-02-04 10:15",
                "duration": {"iso_8601": "PT0H20M", "text": "20m"},
            },
            {
                "date": "2025-02-04",
                "time": "10:15",
                "event": "GANGWAY DOWN/PILOT OFF",
                "end_time": "2025-02-04 10:20",
                "duration": {"iso_8601": "PT0H5M", "text": "5m"},
            },
            {
                "date": "2025-02-04",
                "time": "10:20",
                "event": "AGENT / BAMBRIATION ON BOARD",
                "end_time": "2025-02-04 11:10",
                "duration": {"iso_8601": "PT0H50M", "text": "50m"},
            },
            {
                "date": "2025-02-04",
                "time": "11:10",
                "event": "FREE-PRACTIQUE QUANTED",
                "end_time": "2025-02-04 11:40",
                "duration": {"iso_8601": "PT0H30M", "text": "30m"},
            },
            {
                "date": "2025-02-04",
                "time": "11:40",
                "event": "SURVEYOR & LOADING MASTER ON BOARD",
                "end_time": "2025-02-04 11:40",
                "duration": {
                    "iso_8601": "PT0H0M",
                    "text": "0m",
                    "note": "Début de SAFETY MEETING",
                },
            },
            {
                "date": "2025-02-04",
                "time": "11:40",
                "event": "SAFETY MEETING",
                "end_time": "2025-02-04 12:20",
                "duration": {"iso_8601": "PT0H40M", "text": "40m"},
            },
            {
                "date": "2025-02-04",
                "time": "11:45",
                "event": "TANK INSPECTION",
                "end_time": "2025-02-04 12:30",
                "duration": {"iso_8601": "PT0H45M", "text": "45m"},
            },
            {
                "date": "2025-02-04",
                "time": "13:10",
                "event": 'CARGO HOSE CONNECTED (3 x 8")',
                "end_time": "2025-02-04 13:25",
                "duration": {"iso_8601": "PT0H15M", "text": "15m"},
            },
            {
                "date": "2025-02-04",
                "time": "13:25",
                "event": "LEAKAGE TESTED",
                "end_time": "2025-02-04 13:45",
                "duration": {"iso_8601": "PT0H20M", "text": "20m"},
            },
            {
                "date": "2025-02-04",
                "time": "14:15",
                "event": "COMMENCED LOADING SULPHURIC ACID",
                "end_time": "2025-02-05 08:50",
                "duration": {"iso_8601": "PT18H35M", "text": "18h 35m"},
            },
            {
                "date": "2025-02-05",
                "time": "08:50",
                "event": "COMPLETED LOADING SULPHURIC ACID",
                "end_time": "2025-02-05 09:00",
                "duration": {"iso_8601": "PT0H10M", "text": "10m"},
            },
            {
                "date": "2025-02-05",
                "time": "08:50",
                "event": "AIR BLOWING",
                "end_time": "2025-02-05 09:00",
                "duration": {"iso_8601": "PT0H10M", "text": "10m"},
            },
            {
                "date": "2025-02-05",
                "time": "09:00",
                "event": "VILLAGING, SAMPLING AND CALCULATION",
                "end_time": "2025-02-05 11:00",
                "duration": {"iso_8601": "PT2H0M", "text": "2h"},
            },
            {
                "date": "2025-02-05",
                "time": "10:40",
                "event": 'HOSE OFF (308")',
                "end_time": "2025-02-05 11:40",
                "duration": {"iso_8601": "PT1H0M", "text": "1h"},
            },
            {
                "date": "2025-02-05",
                "time": "11:40",
                "event": "CARGO DOCUMENT COMPLETED",
                "end_time": "2025-02-05 11:40",
                "duration": {"iso_8601": "PT0H0M", "text": "Événement final"},
            },
        ]
        extracted_sof = self.extract_sof_data_and_events_from_json(sof_json)

        # Création du Statement principal
        statement = Statement.create(
            [
                {
                    "shipment": self.records[0].id,
                    "document": self.sof.pdf,
                    "document_name": self.sof.filename,
                    "charter_text": text_charter,
                    # **extracted_main,
                    **{k: v for k, v in extracted_sof.items() if k != "events"},
                }
            ]
        )[0]

        # Création des événements liés
        for e in extracted_sof["events"]:
            SoFEvent.create(
                [
                    {
                        "statement": statement.id,
                        "start": e["start"],
                        "start_date": e["start_date"],
                        "start_time": e["start_time"],
                        "end": e["end"],
                        "end_date": e["end_date"],
                        "end_time": e["end_time"],
                        "duration": e["duration"],
                        "description": e["description"],
                        "deductible": e["deductible"],
                    }
                ]
            )

        return "end"

class SofUpdate(Wizard):
    "Update shipment"
    __name__ = "sof.update"

    start = StateTransition()
    
    def transition_start(self):
        Shipment = Pool().get('stock.shipment.in')
        Sof = Pool().get('sof.statement')
        for r in self.records:
            sh = Shipment(r.id)
            sof = Sof()
            sof.chart = 6
            sof.sof = 5
            sh.sof = [sof]
            Shipment.save([sh])

        return 'end'

class AccountMoveLine(metaclass=PoolMeta):
    "Account move line"
    __name__ = 'account.move.line'
    revaluate = fields.Integer("Linked line")
    lot = fields.Many2One('lot.lot',"Lot")
    fee = fields.Many2One('fee.fee',"Fee")

class AccountMove(metaclass=PoolMeta):
    "Account move"
    __name__ = 'account.move'

    def IsBankMove(self):
        PM = Pool().get('account.invoice.payment.method')
        if self.lines:
            for m in self.lines:
                pm = PM.search(['debit_account','=',m.account])
                if pm:
                    return True
                else:
                    pm = PM.search(['credit_account','=',m.account])
                    if pm:
                        return True
        return False

class Account(metaclass=PoolMeta):
    "Account"
    __name__ = 'account.account'

    def get_foreign_currencies(self):
        Currency = Pool().get('currency.currency')
        cursor = Transaction().connection.cursor()
        cursor.execute("""
            SELECT DISTINCT second_currency
            FROM account_move_line
            WHERE account = %s
            AND second_currency IS NOT NULL
        """, [self.id])

        company_currency = self.company.currency
        currencies = []
        for (currency_id,) in cursor.fetchall():
            cur = Currency(currency_id)
            if cur != company_currency:
                currencies.append(cur)

        return currencies
    
    def get_lines_curr(self,curr):
        MoveLine = Pool().get('account.move.line')
        lines = MoveLine.search([('account','=',self.id),('second_currency','=',curr.id),('revaluate','=',None),('reconciliation','=',None)])
        return lines
    
    def get_all_lines_curr(self,curr):
        MoveLine = Pool().get('account.move.line')
        lines = MoveLine.search([('account','=',self.id)])#,('second_currency','=',curr.id)])
        return lines

    def get_lines_curr_reval(self,id):
        MoveLine = Pool().get('account.move.line')
        lines = MoveLine.search([('account','=',self.id),('revaluate','=',id)])
        return lines
    
    def revaluate_fx(self,reval_date,after):
        Currency = Pool().get('currency.currency')
        MoveLine = Pool().get('account.move.line')
        Configuration = Pool().get('account.configuration')
        configuration = Configuration(1)
        Period = Pool().get('account.period')
        Move = Pool().get('account.move')
        currencies = self.get_foreign_currencies()
        logger.info("REVALUATE_ACCOUNT:%s",self.id)
        if currencies:
            for curr in currencies:
                logger.info("REVALUATE_CURR:%s",curr.id)
                if self.fx_eval_detail:
                    lines = self.get_lines_curr(curr)
                    logger.info("REVALUATE_DETAIL:%s",lines)
                    if lines:
                        for l in lines:
                            reval_lines = self.get_lines_curr_reval(l.id)
                            logger.info("REVALUATE_REVAL_LINES:%s",l)
                            with Transaction().set_context(date=reval_date):
                                amount_converted = Currency.compute(curr,l.amount_second_currency, self.company.currency)
                                amount_reval = sum([(e.debit-e.credit) for e in reval_lines])
                                to_add = amount_converted - ((l.debit-l.credit) + amount_reval)
                                if to_add != 0:
                                    second_amount_to_add = Currency.compute(self.company.currency,to_add,curr)
                                    line = MoveLine()
                                    line.account = self.id
                                    line.revaluate = l.id
                                    line.credit = -to_add if to_add < 0 else 0
                                    line.debit = to_add if to_add > 0 else 0
                                    line.lot = l.lot
                                    line.party = l.party
                                    line.maturity_date = reval_date
                                    logger.info("REVALUATE_ACC:%s",line)
                                    line_ = MoveLine()
                                    if to_add < 0:
                                        line_.account = configuration.get_multivalue('currency_exchange_credit_account', company=self.company.id)
                                    else:
                                        line_.account = configuration.get_multivalue('currency_exchange_debit_account', company=self.company.id)
                                    line_.credit = to_add if to_add > 0 else 0
                                    line_.debit = -to_add if to_add < 0 else 0
                                    line_.lot = l.lot
                                    line_.maturity_date = reval_date
                                    logger.info("REVALUATE_EX:%s",line_)
                                    move = Move()
                                    move.journal = configuration.get_multivalue('currency_exchange_journal', company=self.company.id)
                                    period = Period.find(self.company, date=reval_date)
                                    move.date = reval_date
                                    move.period = period
                                    #move.origin = forex
                                    move.company = self.company
                                    move.lines = [line,line_]
                                    Move.save([move])
                    all_lines = self.get_all_lines_curr(curr)
                    sum_second_amount = sum([e.amount_second_currency for e in all_lines if e.amount_second_currency])
                    logger.info("REVALUATE_SUM_SECOND:%s",sum_second_amount)
                    if sum_second_amount == Decimal(0):
                        base_curr_sum = sum([(e.debit-e.credit) for e in all_lines])
                        if abs(base_curr_sum) > 0:
                            line = MoveLine()
                            line.account = self.id
                            line.party = 1904 #TBN
                            line.credit = base_curr_sum if base_curr_sum > 0 else 0
                            line.debit = -base_curr_sum if base_curr_sum < 0 else 0
                            line.maturity_date = reval_date
                            logger.info("REVALUATE_ACC_:%s",line)
                            line_ = MoveLine()
                            if base_curr_sum < 0:
                                line_.account = configuration.get_multivalue('currency_exchange_credit_account', company=self.company.id)
                            else:
                                line_.account = configuration.get_multivalue('currency_exchange_debit_account', company=self.company.id)
                            line_.credit = -base_curr_sum if base_curr_sum < 0 else 0
                            line_.debit = base_curr_sum if base_curr_sum > 0 else 0
                            line_.maturity_date = reval_date
                            logger.info("REVALUATE_EX:%s",line_)
                            move = Move()
                            move.journal = configuration.get_multivalue('currency_exchange_journal', company=self.company.id)
                            period = Period.find(self.company, date=reval_date)
                            move.date = reval_date
                            move.period = period
                            #move.origin = forex
                            move.company = self.company
                            move.lines = [line,line_]
                            Move.save([move])

                else:
                    lines = self.get_all_lines_curr(curr)
                    with Transaction().set_context(date=reval_date):
                        sum_second_amount = sum([e.amount_second_currency for e in lines if e.amount_second_currency])
                        logger.info("REVALUATE_SECOND:%s",sum_second_amount)
                        amount_converted = Currency.compute(curr,sum_second_amount, self.company.currency)
                        logger.info("REVALUATE_SECOND2:%s",amount_converted)
                        total_amount = sum([(e.debit-e.credit) for e in lines])
                        logger.info("REVALUATE_SECOND3:%s",total_amount)
                        to_add = amount_converted - total_amount
                        if to_add != 0:
                            second_amount_to_add = Currency.compute(self.company.currency,to_add,curr)
                            line = MoveLine()
                            line.account = self.id
                            line.credit = -to_add if to_add < 0 else 0
                            line.debit = to_add if to_add > 0 else 0
                            line.party = self.company.party.id
                            line.maturity_date = reval_date
                            logger.info("REVALUATE_ACC:%s",line)
                            line_ = MoveLine()
                            if to_add < 0:
                                line_.account = configuration.get_multivalue('currency_exchange_credit_account', company=self.company.id)
                            else:
                                line_.account = configuration.get_multivalue('currency_exchange_debit_account', company=self.company.id)
                            line_.credit = to_add if to_add > 0 else 0
                            line_.debit = -to_add if to_add < 0 else 0
                            line_.maturity_date = reval_date
                            logger.info("REVALUATE_EX:%s",line_)
                            move = Move()
                            move.journal = configuration.get_multivalue('currency_exchange_journal', company=self.company.id)
                            period = Period.find(self.company, date=reval_date)
                            move.date = reval_date
                            move.period = period
                            #move.origin = forex
                            move.company = self.company
                            move.lines = [line,line_]
                            Move.save([move])

class Revaluate(Wizard):
    'Revaluate'
    __name__ = 'account.revaluate'
    start = StateView(
        'account.revaluate.start',
        'purchase_trade.revaluate_start_view_form', [
            Button("Cancel", 'end', 'tryton-cancel'),
            Button("Revaluate", 'revaluate', 'tryton-ok', default=True),
            ])
    revaluate = StateTransition()

    def transition_revaluate(self):
        Account = Pool().get('account.account')
        accounts = Account.search([('fx_eval','=',True)])
        if accounts:
            for acc in accounts:
                acc.revaluate_fx(self.start.revaluation_date,self.start.delete_after)

        return 'end'

class RevaluateStart(ModelView):
    "Revaluate"
    __name__ = 'account.revaluate.start'
    revaluation_date = fields.Date(
        "Revaluation date")
    delete_after = fields.Boolean(
        "Delete lines after revaluation date")

    @classmethod
    def default_revaluation_date(cls):
        Date = Pool().get('ir.date')
        return Date.today()

    @classmethod
    def default_delete_after(cls):
        return False
