# 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 ModelSQL, ModelView, fields, sequence_ordered, ModelSingleton from trytond.pyson import Eval from trytond.transaction import Transaction from trytond.pool import Pool, PoolMeta from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction from decimal import getcontext, Decimal, ROUND_HALF_UP import jwt import datetime import logging from trytond.exceptions import UserWarning, UserError import json import shlex from datetime import date logger = logging.getLogger(__name__) keywords = ["duplicate", "create", "add", "invoice", "reception", "pay", "delivery", "match"] class DashboardLoader(Wizard): 'Load Dashboard' __name__ = 'purchase.dashboard.loader' start = StateAction('purchase_trade.act_dashboard_form') def do_start(self, action): pool = Pool() Dashboard = pool.get('purchase.dashboard') user_id = Transaction().user if user_id == 0: user_id = Transaction().context.get('user', user_id) dashboard = Dashboard.search([('user_id', '=', user_id)]) if not dashboard: dashboard, = Dashboard.create([{'user_id': user_id}]) else: dashboard = dashboard[0] action['views'].reverse() return action, {'res_id': [dashboard.id]} class DashboardContext(ModelView): "Dashboard context" __name__ = 'dashboard.context' user = fields.Many2One('res.user', 'User') company = fields.Many2One('company.company', 'Company', required=True) @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_user(): user_id = Transaction().user if user_id == 0: user_id = Transaction().context.get('user', user_id) return user_id class Demos(ModelSQL, ModelView): 'Demos' __name__ = 'demos.demos' title = fields.Char('Title', required=True) category = fields.Char('Category') icon = fields.Char('Icon') # exemple: "🚢" ou "📝" active = fields.Boolean('Active') video_file = fields.Char('File') description = fields.Text('Description') class News(ModelSQL, ModelView): 'News' __name__ = 'news.news' title = fields.Char('Title', required=True) category = fields.Char('Category') icon = fields.Char('Icon') # exemple: "🚢" ou "📝" active = fields.Boolean('Active') publish_date = fields.Date('Publish Date') model = fields.Char('Model') # ex: 'purchase.purchase' model_id = fields.Integer('Record ID') class Dashboard(ModelSQL, ModelView): "Dashbot" __name__ = "purchase.dashboard" stock = fields.One2Many('lot.lot','dashboard',"STOCK") stock2 = fields.One2Many('lot.lot','dashboard',"PRODUCT") stock3 = fields.One2Many('lot.lot','dashboard',"CURVE") transit = fields.One2Many('stock.shipment.in','dashboard',"VESSEL IN TRANSIT") document = fields.One2Many('document.incoming','dashboard',"Documents received") html_content = fields.Function(fields.Text('HTML Content'),'get_news') demos = fields.Function(fields.Text('Demos'),'get_demos') #metabase = fields.Function(fields.Text("",states={'invisible': ~Eval('metabase',False)}),'gen_url') metabase = fields.Text("",states={'invisible': ~Eval('metabase',False)}) chatbot = fields.Text("") input = fields.Text("") bi_id = fields.Integer("") user = fields.Function(fields.Char(""), 'get_user') fake = fields.Char("",readonly=True) res_id = fields.Integer("Res id") action_return = fields.Char("Action return") actions = fields.One2Many('bot.action','dashboard',"Actions") user_request = fields.Char("User request") user_id = fields.Many2One('res.user',"User") tremor = fields.Function(fields.Text(""),'get_tremor') @classmethod def default_bi_id(cls): return 38 @classmethod def default_res_id(cls): return 0 @fields.depends('input') def on_change_input(self): logger.info("ENTERONCHANGE",self.input) Lot = Pool().get('lot.lot') self.metabase = None if self.input is None: self.chatbot = 'chatbot:' + json.dumps([{"type": "bot", "content": "🧠 Hi Admin, how can I help you?"}], ensure_ascii=False) else: dial = json.loads(self.input) last_content = dial[-1]["content"] last_type = dial[-1]["type"] logger.info("ON_CHANGE_INPUT:%s",last_content) message = '' if "display" in last_content.lower() and "clients" in last_content.lower(): dial.append({"type": "bot", "content": "Clients displayed below"}) if self.bi_id != 7: self.bi_id = 7 self.metabase = self.gen_url() elif "display" in last_content.lower() and "sales" in last_content.lower(): dial.append({"type": "bot", "content": "Sales displayed below"}) if self.bi_id != 41: self.bi_id = 41 self.metabase = self.gen_url() elif "display" in last_content.lower() and "products" in last_content.lower(): dial.append({"type": "bot", "content": "Products displayed below"}) if self.bi_id != 38: self.bi_id = 38 self.metabase = self.gen_url() elif "open" in last_content.lower() and "shipment" in last_content.lower(): sh = self.FindInContent('shipment',last_content) if sh: dial.append({"type": "bot", "content": "Ok shipment opened in new tab"}) dial.append({"type": "do", "content": "open,stock.shipment.in," + str(sh.id) + ",0"}) else: dial.append({"type": "bot", "content": "No shipment found"}) elif "where" in last_content.lower() and "vessel" in last_content.lower(): sh = self.FindInContent('shipment',last_content) if sh: dial.append({"type": "bot", "content": "Look at the map below"}) self.metabase = f"imo:{sh.vessel.vessel_imo}" elif "graph" in last_content.lower() and "lot" in last_content.lower(): g,l,i = last_content.split() lot = Lot(i) if lot: dial.append({"type": "bot", "content": "Graph displayed below"}) self.metabase = lot.get_flow_graph() elif "pivot" in last_content.lower() and "lot" in last_content.lower(): g,l,i = last_content.split() lot = Lot(i) if lot: dial.append({"type": "bot", "content": "Pivot displayed below"}) self.metabase = lot.get_pivot() elif "display" in last_content.lower() and "map" in last_content.lower(): dial.append({"type": "bot", "content": "Map displayed below"}) self.metabase = self.get_map() elif any(k in last_content.lower() for k in keywords): logger.info("ACTION_RETURN:%s",self.action_return) if "help" in last_content.lower(): if "duplicate" in last_content.lower(): dial.append({"type": "bot", "content": "duplicate [purchase/sale]* [party]* [qt]* [plan] [period] [price]"}) elif self.action_return: model,res_id, res_nb = self.action_return.split(",") z,keyword = model.split(".") message = { "type": "bot", "content": ( f'Action done. ' f'Open {keyword} {res_nb}' ) } dial.append(message) else: if "match" in last_content.lower(): dial.append({"type": "bot", "content": "Matching done"}) else: dial.append({"type": "bot", "content": "Not done"}) else: dial.append({"type": "bot", "content": "Ok done"}) self.chatbot = 'chatbot:' + json.dumps(dial, ensure_ascii=False) logger.info("EXITONCHANGE",self.chatbot) def get_last_five_fx_rates(self, from_code='USD', to_code='EUR'): """ Retourne (dernier_taux, avant_dernier_taux) pour le couple de devises. """ Currency = Pool().get('currency.currency') CurrencyRate = Pool().get('currency.currency.rate') # Récupérer les devises EUR et USD from_currency = Currency.search([('name', '=', from_code)])[0] to_currency = Currency.search([('name', '=', to_code)])[0] # Recherche des taux de la devise de base (ex: USD) rates = CurrencyRate.search( [('currency', '=', to_currency.id)], order=[('date', 'DESC')], limit=5, ) if not rates: return None, None, None, None, None # Calcul du taux EUR/USD # Si la devise principale de la société est EUR, et que le taux stocké est # "1 USD = X EUR", on veut l'inverse pour avoir EUR/USD f1 = rates[0].rate f2 = rates[1].rate if len(rates) > 1 else None f3 = rates[2].rate if len(rates) > 2 else None f4 = rates[3].rate if len(rates) > 3 else None f5 = rates[4].rate if len(rates) > 4 else None d1 = rates[0].date d2 = rates[1].date if len(rates) > 1 else None d3 = rates[2].date if len(rates) > 2 else None d4 = rates[3].date if len(rates) > 3 else None d5 = rates[4].date if len(rates) > 4 else None # if from_currency != to_currency: # last_rate = 1 / last_rate if last_rate else None # prev_rate = 1 / prev_rate if prev_rate else None return round(1/f1,6), round(1/f2,6) if f2 else None, round(1/f3,6) if f3 else None, round(1/f4,6) if f4 else None, round(1/f5,6) if f5 else None, d1, d2, d3, d4, d5 def get_tremor(self,name): Date = Pool().get('ir.date') Configuration = Pool().get('gr.configuration') config = Configuration.search(['id','>',0])[0] Shipment = Pool().get('stock.shipment.in') DocumentIncoming = Pool().get('document.incoming') Fee = Pool().get('fee.fee') WR = Pool().get('weight.report') if config.automation: shipment = Shipment.search([('state','!=','received')]) shipment_trend = [sh for sh in shipment if sh.create_date == Date.today()] controller = Shipment.search([('controller','!=',None)]) controller_trend = [co for co in controller if co.create_date == Date.today()] instruction = Shipment.search([('result','!=',None)]) instruction_trend = [si for si in instruction if si.create_date == Date.today()] id_received = Shipment.search([('returned_id','!=',None)]) id_received_trend = [i for i in id_received if i.create_date == Date.today()] wr = WR.search([('id','>',0)]) wr_trend = [w for w in wr if w.create_date == Date.today()] so = Fee.search(['id','=',25]) so_trend = [s for s in so if s.create_date == Date.today()] di = DocumentIncoming.search(['id','>',0]) di_trend = [d for d in di if d.create_date == Date.today()] return ( config.dashboard + "/dashboard/index.html?shipment=" + str(len(shipment)) + "&shipment_trend=" + str(len(shipment_trend)) + "&controller=" + str(len(controller)) + "&controller_trend=" + str(len(controller_trend)) + "&instruction=" + str(len(instruction)) + "&instruction_trend=" + str(len(instruction_trend)) + "&wr=" + str(len(wr)) + "&wr_trend=" + str(len(wr_trend)) + "&so=" + str(len(so)) + "&so_trend=" + str(len(so_trend)) + "&di=" + str(len(di)) + "&di_trend=" + str(len(di_trend)) + "&id_received=" + str(len(id_received)) + "&id_received_trend=" + str(len(id_received_trend))) f1,f2,f3,f4,f5,d1,d2,d3,d4,d5 = self.get_last_five_fx_rates() Valuation = Pool().get('valuation.valuation') last_total,last_variation = Valuation.get_totals() pnl_amount = "{:,.0f}".format(round(last_total,0)) pnl_variation = 0 if last_total and last_variation: pnl_variation = "{:,.2f}".format(round((last_variation/last_total)*100,0)) Open = Pool().get('open.position') opens = Open.search(['id','>',0]) exposure = "{:,.0f}".format(round(sum([e.net_exposure for e in opens]),0)) ToPay = Pool().get('account.invoice') topays = ToPay.search(['type','=','in']) amounts = ToPay.get_amount_to_pay(topays) total = sum(amounts.values(), Decimal(0)) topay = "{:,.0f}".format(round(total,0)) ToReceive = Pool().get('account.invoice') toreceives = ToReceive.search(['type','=','out']) amounts = ToPay.get_amount_to_pay(toreceives) total = sum(amounts.values(), Decimal(0)) toreceive = "{:,.0f}".format(round(total,0)) Purchase = Pool().get('purchase.purchase') draft = Purchase.search(['state','=','draft']) draft_p = len(draft) val = Purchase.search(['state','=','quotation']) val_p = len(val) conf = Purchase.search(['state','=','confirmed']) conf_p = len(conf) Sale = Pool().get('sale.sale') draft = Sale.search(['state','=','draft']) draft_s = len(draft) val = Sale.search(['state','=','quotation']) val_s = len(val) conf = Sale.search(['state','=','confirmed']) conf_s = len(conf) draft = Shipment.search(['state','=','draft']) shipment_d = len(draft) val = Shipment.search(['state','=','started']) shipment_s = len(val) conf = Shipment.search(['state','=','received']) shipment_r = len(conf) Lot = Pool().get('lot.lot') lots = Lot.search([('sale_line','!=',None),('line','!=',None),('lot_type','=','physic')]) lot_m = len(lots) val = Lot.search([('sale_line','=',None),('line','!=',None),('lot_type','=','physic')]) lot_a = len(val) Invoice = Pool().get('account.invoice') invs = Invoice.search(['type','=','in']) inv_p = len(invs) invs = Invoice.search([('type','=','in'),('state','=','paid')]) inv_p_p = len(invs) invs = Invoice.search([('type','=','in'),('state','!=','paid')]) inv_p_np = len(invs) invs = Invoice.search(['type','=','out']) inv_s = len(invs) invs = Invoice.search([('type','=','out'),('state','=','paid')]) inv_s_p = len(invs) invs = Invoice.search([('type','=','out'),('state','!=','paid')]) inv_s_np = len(invs) AccountMove = Pool().get('account.move') accs = AccountMove.search([('journal','=',3),('state','!=','posted')]) pay_val = len(accs) accs = AccountMove.search([('journal','=',3),('state','=','posted')]) pay_posted = len(accs) return ( config.dashboard + "/dashboard/index.html?pnl_amount=" + str(pnl_amount) + "&pnl_variation=" + str(pnl_variation) + "&exposure=" + str(exposure) + "&topay=" + str(topay) + "&toreceive=" + str(toreceive) + "&eurusd=" + str(f1) + "&eurusd=" + str(f2) + "&eurusd=" + str(f3) + "&eurusd=" + str(f4) + "&eurusd=" + str(f5) + "&eurusd_date=" + str(d1) + "&eurusd_date=" + str(d2) + "&eurusd_date=" + str(d3) + "&eurusd_date=" + str(d4) + "&eurusd_date=" + str(d5) + "&draft_p=" + str(draft_p) + "&val_p=" + str(val_p) + "&conf_p=" + str(conf_p) + "&draft_s=" + str(draft_s) + "&val_s=" + str(val_s) + "&conf_s=" + str(conf_s) + "&shipment_d=" + str(shipment_d) + "&shipment_s=" + str(shipment_s) + "&shipment_r=" + str(shipment_r) + "&lot_m=" + str(lot_m) + "&lot_a=" + str(lot_a) + "&inv_p=" + str(inv_p) + "&inv_p_p=" + str(inv_p_p) + "&inv_p_np=" + str(inv_p_np) + "&inv_s=" + str(inv_s) + "&inv_s_p=" + str(inv_s_p) + "&inv_s_np=" + str(inv_s_np) + "&pay_val=" + str(pay_val) + "&pay_posted=" + str(pay_posted) ) def get_news(self, name): News = Pool().get('news.news') Date = Pool().get('ir.date') news_list = News.search([('active', '=', True)], limit=5, order=[('publish_date', 'DESC')]) last_rate,prev_rate, = self.get_last_five_fx_rates() if last_rate and prev_rate: variation = ((last_rate - prev_rate) / prev_rate) * 100 if prev_rate else 0 direction = "📈" if variation > 0 else "📉" # 🆕 Création d’une “fake news” locale forex_news = News() forex_news.icon = direction forex_news.category = "Forex" # Détermine la direction et le style couleur if variation > 0: arrow = "⬆️" color = "#28a745" # vert elif variation < 0: arrow = "⬇️" color = "#dc3545" # rouge else: arrow = "➡️" color = "#6c757d" # gris neutre # Contenu HTML enrichi forex_news.title = ( f"EUR/USD: {last_rate:.4f}" f"" f" {variation:+.2f}%" f" " ) forex_news.publish_date = Date.today() forex_news.model = 'currency.currency' # par exemple 'forex.forex' forex_news.model_id = 2 forex_news.active = True # On insère la “news” au début de la liste news_list.insert(0, forex_news) html_parts = [ '