🎉 Initial commit - Hazard Odoo 16 project

- Custom modules: hazard_shopify, hazard_inventory_report, hazard_website
- Third-party modules: bi_sql_editor, query_deluxe, sql_request_abstract, l10n_mx_sat_sync_itadmin_ee
- Docker setup: Odoo 16 + PostgreSQL 17
- Utility scripts: backup_hazard.sh, update_module.py
- Agent configuration: .agents/AGENTS.md
- Security: Enterprise modules, credentials, backups and production data excluded via .gitignore
This commit is contained in:
2026-06-30 06:12:35 -06:00
commit 28d9e56f47
350 changed files with 99832 additions and 0 deletions
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="report_product_template_label_thermal2x1" model="ir.actions.report">
<field name="name">Product Label (Thermal 2x1)</field>
<field name="model">product.template</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">hazard_shopify.report_producttemplatelabel_thermal2x1</field>
<field name="report_file">hazard_shopify.report_producttemplatelabel_thermal2x1</field>
<field name="paperformat_id" ref="hazard_shopify.paperformat_label_thermal2x1"/>
<field name="print_report_name">'Products Labels - %s' % (object.name)</field>
<field name="binding_type">report</field>
</record>
<template id="report_producttemplatelabel_thermal2x1">
<t t-call="web.html_container">
<t t-set="barcode_size" t-value="'width: 40mm; height: 8mm;'"/>
<t t-foreach="quantity.items()" t-as="barcode_and_qty_by_product">
<t t-set="product" t-value="barcode_and_qty_by_product[0]"/>
<t t-foreach="barcode_and_qty_by_product[1]" t-as="barcode_and_qty">
<t t-set="barcode" t-value="barcode_and_qty[0]"/>
<t t-foreach="range(barcode_and_qty[1])" t-as="qty">
<div class="page" style="position: relative; width: 51mm; height: 25mm; background-color: white; overflow: hidden; box-sizing: border-box; page-break-inside: avoid; page-break-after: always;">
<!-- Top Text Section -->
<div style="position: absolute; top: 1mm; left: 0; width: 100%; text-align: center;">
<div style="font-family: Arial, sans-serif; font-size: 10px; font-weight: normal; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; padding: 0 1mm;" t-esc="product.name"/>
<t t-if="product.is_product_variant and product.product_template_attribute_value_ids">
<div style="font-family: Arial, sans-serif; font-size: 11px; font-weight: normal; margin-top: 1px;" t-esc="', '.join(product.product_template_attribute_value_ids.mapped('name'))"/>
</t>
</div>
<!-- Bottom Barcode Section -->
<div style="position: absolute; bottom: 1.5mm; left: 0; width: 100%; text-align: center;">
<t t-if="barcode">
<div t-out="barcode" style="padding:0; margin: 0 auto;" t-options="{'widget': 'barcode', 'symbology': 'auto', 'img_style': 'width: 44mm; height: 11mm; display: block; margin: 0 auto;'}"/>
<div style="font-family: Arial, sans-serif; font-size: 11px; font-weight: normal; margin-top: 1px; line-height: 1;" t-esc="barcode"/>
</t>
<t t-elif="product.default_code">
<div t-out="product.default_code" style="padding:0; margin: 0 auto;" t-options="{'widget': 'barcode', 'symbology': 'auto', 'img_style': 'width: 44mm; height: 11mm; display: block; margin: 0 auto;'}"/>
<div style="font-family: Arial, sans-serif; font-size: 11px; font-weight: normal; margin-top: 1px; line-height: 1;" t-esc="product.default_code"/>
</t>
<t t-else="">
<div style="font-family: Arial, sans-serif; font-size: 11px; color: grey;">Sin código</div>
</t>
</div>
</div>
</t>
</t>
</t>
</t>
</template>
</data>
</odoo>
@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- ===================================================================
EXTENSIÓN: Vista de Producto — Integración Shopify
Agrega panel de publicación en Shopify y controles de sincronización.
=================================================================== -->
<record id="product_template_hazard_shopify_view" model="ir.ui.view">
<field name="name">product.template.hazard.shopify.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view"/>
<field name="arch" type="xml">
<!-- ── Botón de estado Shopify en el header (stat button) ── -->
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_push_to_shopify"
type="object"
class="oe_stat_button"
icon="fa-shopping-bag"
groups="hazard_shopify.group_hazard_designer,hazard_shopify.group_hazard_finance"
attrs="{'invisible': [('shopify_published', '=', True)]}"
help="Publicar este producto en Shopify">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value" style="font-size:12px;">Publicar</span>
<span class="o_stat_text">Shopify</span>
</div>
</button>
<button name="action_push_to_shopify"
type="object"
class="oe_stat_button"
icon="fa-check-circle"
groups="hazard_shopify.group_hazard_designer,hazard_shopify.group_hazard_finance"
attrs="{'invisible': [('shopify_published', '=', False)]}"
help="Sincronizar cambios con Shopify">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="shopify_sync_status"
widget="badge"
decoration-success="shopify_sync_status == 'synced'"
decoration-warning="shopify_sync_status == 'pending'"
decoration-danger="shopify_sync_status == 'error'"/>
</span>
<span class="o_stat_text">Shopify</span>
</div>
</button>
</xpath>
<!-- ── Panel de Shopify en pestaña "Información general" ── -->
<xpath expr="//page[@name='general_information']" position="after">
<page string="🛒 Shopify"
name="shopify_info"
groups="hazard_shopify.group_hazard_designer,hazard_shopify.group_hazard_finance">
<group>
<group string="Estado de Publicación">
<field name="shopify_published"
string="¿Publicado en Shopify?"
widget="toggle_button"
readonly="1"/>
<field name="shopify_status"
string="Estado en Shopify"
widget="selection"/>
<field name="shopify_sync_status"
string="Estado de sincronización"
readonly="1"/>
<field name="shopify_synced_at"
string="Primera sincronización"
readonly="1"/>
<field name="shopify_last_pushed_at"
string="Última actualización a Shopify"
readonly="1"/>
</group>
<group string="Información Shopify">
<field name="shopify_config_id"
string="Tienda Shopify"
domain="[('state','=','confirmed')]"
options="{'no_create': True}"/>
<field name="shopify_id"
string="ID Producto Shopify"
readonly="1"
attrs="{'invisible': [('shopify_id', '=', False)]}"/>
</group>
</group>
<!-- Canales de Venta — ancho completo -->
<separator string="Canales de Venta Shopify"
attrs="{'invisible': [('shopify_published', '=', False)]}"/>
<div class="mb-2" attrs="{'invisible': [('shopify_published', '=', False)]}">
<button name="action_refresh_shopify_channels"
type="object"
string="🔄 Leer canales desde Shopify"
class="btn btn-sm btn-info"
groups="hazard_shopify.group_hazard_designer,hazard_shopify.group_hazard_finance"
help="Actualiza esta lista leyendo el estado real desde Shopify sin realizar cambios."/>
</div>
<field name="shopify_publication_ids"
nolabel="1"
attrs="{'invisible': [('shopify_published', '=', False)]}">
<tree editable="bottom" create="false" delete="false">
<field name="publication_name" string="Canal de Venta" readonly="1"/>
<field name="is_published" string="Publicado" widget="boolean_toggle"/>
<field name="publication_id" invisible="1"/>
</tree>
</field>
<!-- Error de sync -->
<group attrs="{'invisible': [('shopify_sync_error', '=', False)]}">
<div class="alert alert-danger" role="alert" style="margin:8px 0;">
<strong>⚠️ Último error de sincronización:</strong>
<field name="shopify_sync_error" readonly="1" nolabel="1"/>
</div>
</group>
<!-- Botones de acción manuales -->
<div class="o_field_widget mt-3">
<button name="action_push_to_shopify"
type="object"
string="📤 Publicar / Actualizar en Shopify"
class="btn btn-primary me-2"
groups="hazard_shopify.group_hazard_designer,hazard_shopify.group_hazard_finance"/>
<button name="action_unpublish_from_shopify"
type="object"
string="🚫 Despublicar de Shopify"
class="btn btn-secondary"
attrs="{'invisible': [('shopify_published', '=', False)]}"
groups="hazard_shopify.group_hazard_finance"
confirm="¿Seguro que quieres ocultar este producto de la tienda Shopify? No se eliminará, solo quedará como borrador."/>
</div>
<!-- Instrucciones para el equipo de diseño -->
<div class="alert alert-info mt-3" role="alert"
attrs="{'invisible': [('shopify_published', '=', True)]}">
<strong>️ ¿Cómo publicar este producto?</strong>
<ol style="margin-top:6px; margin-bottom:0;">
<li>Completa el nombre, descripción, precio y variantes del producto.</li>
<li>Sube la imagen principal del producto.</li>
<li>Presiona el botón <strong>"📤 Publicar / Actualizar en Shopify"</strong>.</li>
<li>El producto aparecerá automáticamente en la tienda de Shopify.</li>
</ol>
</div>
</page>
</xpath>
<!-- ── Restricciones de precios (ya existían, las mantenemos) ── -->
<xpath expr="//field[@name='list_price']" position="attributes">
<attribute name="groups">hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance</attribute>
</xpath>
<xpath expr="//field[@name='standard_price']" position="attributes">
<attribute name="groups">hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance</attribute>
</xpath>
<xpath expr="//field[@name='taxes_id']" position="attributes">
<attribute name="groups">hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance</attribute>
</xpath>
<xpath expr="//field[@name='supplier_taxes_id']" position="attributes">
<attribute name="groups">hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance</attribute>
</xpath>
<xpath expr="//field[@name='default_code']" position="attributes">
<attribute name="groups">hazard_shopify.group_hazard_designer,hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance</attribute>
</xpath>
<xpath expr="//field[@name='barcode']" position="attributes">
<attribute name="groups">hazard_shopify.group_hazard_designer,hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance</attribute>
</xpath>
<xpath expr="//field[@name='barcode']" position="after">
<button name="action_generate_barcode" string="Generar EAN-13" type="object" class="btn btn-sm btn-link" icon="fa-barcode" attrs="{'invisible': [('barcode', '!=', False)]}" groups="hazard_shopify.group_hazard_designer,hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance"/>
</xpath>
</field>
</record>
<!-- EXTENSIÓN: Vista de Variante de Producto — Restricción de Precios para Bodega/Diseñador -->
<record id="product_product_hazard_shopify_view" model="ir.ui.view">
<field name="name">product.product.hazard.shopify.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='lst_price']" position="attributes">
<attribute name="groups">hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance</attribute>
</xpath>
<xpath expr="//field[@name='standard_price']" position="attributes">
<attribute name="groups">hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance</attribute>
</xpath>
<xpath expr="//field[@name='barcode']" position="after">
<button name="action_generate_barcode" string="Generar EAN-13" type="object" class="btn btn-sm btn-link" icon="fa-barcode" attrs="{'invisible': [('barcode', '!=', False)]}" groups="hazard_shopify.group_hazard_designer,hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,226 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- ===================================================================
EXTENSIÓN: Vista de Pedido de Venta — Integración Shopify
Agrega botón de fulfillment e indicadores de estado en sale.order
=================================================================== -->
<record id="sale_order_shopify_fulfillment_view" model="ir.ui.view">
<field name="name">sale.order.shopify.fulfillment.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<!-- ── Botón de fulfillment en el header del pedido ── -->
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_notify_shopify_fulfillment"
type="object"
class="oe_stat_button"
icon="fa-truck"
groups="hazard_shopify.group_hazard_warehouse,hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance"
attrs="{'invisible': [('shopify_order_id', '=', False)]}"
help="Notificar a Shopify que este pedido ha sido enviado">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="shopify_fulfillment_status"
widget="badge"
decoration-success="shopify_fulfillment_status == 'fulfilled'"
decoration-warning="shopify_fulfillment_status == 'partial'"
decoration-danger="shopify_fulfillment_status == 'error'"
decoration-muted="not shopify_fulfillment_status"/>
</span>
<span class="o_stat_text">Shopify</span>
</div>
</button>
<button name="action_view_shopify_refunds"
type="object"
class="oe_stat_button"
icon="fa-undo"
groups="hazard_shopify.group_hazard_warehouse,hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance"
attrs="{'invisible': ['|', ('shopify_order_id', '=', False), ('shopify_refund_status', '=', 'no_refund')]}"
help="Estado de Reembolsos de Shopify">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="shopify_refund_status"
widget="badge"
decoration-danger="shopify_refund_status == 'refunded'"
decoration-warning="shopify_refund_status == 'partially_refunded'"/>
</span>
<span class="o_stat_text">Reembolso</span>
</div>
</button>
</xpath>
<!-- ── Campos de Shopify en pestaña "Otra información" ── -->
<xpath expr="//page[@name='other_information']" position="inside">
<group string="🛒 Shopify"
groups="hazard_shopify.group_hazard_warehouse,hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance"
attrs="{'invisible': [('shopify_order_id', '=', False)]}">
<group>
<field name="shopify_order_id" readonly="1" string="ID Orden Shopify"/>
<field name="shopify_order_number" readonly="1" string="# Orden Shopify"/>
</group>
<group>
<field name="shopify_fulfillment_status" readonly="1" string="Estado Fulfillment"/>
<field name="shopify_fulfillment_id" readonly="1" string="ID Fulfillment Shopify"
attrs="{'invisible': [('shopify_fulfillment_id', '=', False)]}"/>
</group>
<!-- ── Campos de reembolsos y devoluciones ── -->
<group string="🔄 Reembolsos y Devoluciones" col="2">
<field name="shopify_financial_status" readonly="1" string="Estado Financiero"/>
<field name="shopify_refund_status" readonly="1" string="Estado Reembolso"/>
<field name="shopify_refund_amount" readonly="1" string="Monto Reembolsado" widget="monetary"/>
<field name="shopify_refund_ids" readonly="1" string="IDs Reembolsos" attrs="{'invisible': [('shopify_refund_ids', '=', False)]}"/>
</group>
<!-- ── Campos de guía de envío ── -->
<group string="📦 Datos de Envío" col="2">
<field name="shopify_shipping_title" readonly="1" string="Método Envío Shopify"/>
<field name="shopify_carrier_name"
string="Paquetería"
attrs="{'readonly': [('shopify_fulfillment_status', '=', 'fulfilled')]}"/>
<field name="shopify_tracking_number"
string="Número de Guía"
placeholder="Ej: 1234567890"
attrs="{'readonly': [('shopify_fulfillment_status', '=', 'fulfilled')]}"/>
</group>
<div class="col-12 mt-1" attrs="{'invisible': [('shopify_fulfillment_status', '!=', False)]}">
<button name="action_notify_shopify_fulfillment"
type="object"
string="📦 Marcar como Enviado en Shopify"
class="btn btn-primary"
groups="hazard_shopify.group_hazard_warehouse,hazard_shopify.group_hazard_sales,hazard_shopify.group_hazard_finance"
attrs="{'invisible': [('shopify_order_id', '=', False)]}"/>
<span class="text-muted ml-2" style="font-size:0.85em;">
Solo disponible cuando haya entregas validadas en Odoo.
</span>
</div>
</group>
</xpath>
</field>
</record>
<!-- ===================================================================
EXTENSIÓN: Vista de Árbol/Lista de Pedidos y Cotizaciones
Agrega la columna con el número de pedido de Shopify directamente
en la tabla principal.
=================================================================== -->
<record id="view_quotation_tree_shopify" model="ir.ui.view">
<field name="name">sale.quotation.tree.shopify.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_quotation_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="shopify_order_number" string="# Shopify" optional="show"/>
<field name="shopify_financial_status" string="Pago (Shopify)" optional="show" widget="badge"
decoration-success="shopify_financial_status == 'paid'"
decoration-warning="shopify_financial_status in ('pending', 'partially_paid')"
decoration-danger="shopify_financial_status in ('refunded', 'voided')"/>
<field name="shopify_refund_status" string="Reembolso (Shopify)" optional="show" widget="badge"
decoration-danger="shopify_refund_status == 'refunded'"
decoration-warning="shopify_refund_status == 'partially_refunded'"
decoration-muted="shopify_refund_status in ('no_refund', False)"/>
<field name="shopify_fulfillment_status" string="Envío (Shopify)" optional="show" widget="badge"
decoration-success="shopify_fulfillment_status == 'fulfilled'"
decoration-warning="shopify_fulfillment_status == 'partial'"
decoration-info="shopify_fulfillment_status == 'pending'"/>
</xpath>
</field>
</record>
<record id="view_order_tree_shopify" model="ir.ui.view">
<field name="name">sale.order.tree.shopify.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="shopify_order_number" string="# Shopify" optional="show"/>
<field name="shopify_financial_status" string="Pago (Shopify)" optional="show" widget="badge"
decoration-success="shopify_financial_status == 'paid'"
decoration-warning="shopify_financial_status in ('pending', 'partially_paid')"
decoration-danger="shopify_financial_status in ('refunded', 'voided')"/>
<field name="shopify_refund_status" string="Reembolso (Shopify)" optional="show" widget="badge"
decoration-danger="shopify_refund_status == 'refunded'"
decoration-warning="shopify_refund_status == 'partially_refunded'"
decoration-muted="shopify_refund_status in ('no_refund', False)"/>
<field name="shopify_fulfillment_status" string="Envío (Shopify)" optional="show" widget="badge"
decoration-success="shopify_fulfillment_status == 'fulfilled'"
decoration-warning="shopify_fulfillment_status == 'partial'"
decoration-info="shopify_fulfillment_status == 'pending'"/>
</xpath>
</field>
</record>
<!-- ===================================================================
EXTENSIÓN: Vista de Árbol/Lista de Órdenes de Entrega (Stock Picking)
Agrega la columna con el número de pedido de Shopify directamente
en la tabla de albaranes de almacén.
=================================================================== -->
<record id="view_picking_tree_shopify" model="ir.ui.view">
<field name="name">stock.picking.tree.shopify.inherit</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.vpicktree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='origin']" position="after">
<field name="shopify_order_number" string="# Shopify" optional="show"/>
</xpath>
</field>
</record>
<!-- ===================================================================
EXTENSIÓN: Vista de Formulario de Albarán (Stock Picking)
Agrega los badges visuales muy obvios para Bodega sobre el tipo de envío.
=================================================================== -->
<record id="view_picking_form_shopify" model="ir.ui.view">
<field name="name">stock.picking.form.shopify.inherit</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<xpath expr="//header" position="inside">
<button name="action_shopify_ready_for_pickup"
string="Notificar Listo para Retiro"
type="object"
class="oe_highlight btn-primary"
attrs="{'invisible': ['|', '|', '|', ('shopify_order_number', '=', False), ('shopify_delivery_type', '!=', 'pickup'), ('state', '!=', 'assigned'), ('shopify_ready_for_pickup_notified', '=', True)]}"/>
</xpath>
<xpath expr="//header" position="after">
<!-- Banner de advertencia visual interactivo para Almacén -->
<div class="alert alert-info text-center o_form_header"
role="alert"
style="margin-bottom:0px; font-weight:bold; font-size:1.1em; padding:8px 5px;"
attrs="{'invisible': ['|', ('shopify_order_number', '=', False), ('shopify_delivery_type', '!=', 'pickup')]}">
🔵 <strong>[ RETIRO EN OFICINA ]</strong> Este pedido de Shopify se entregará en mano. NO requiere Número de Guía.
</div>
<div class="alert alert-success text-center o_form_header"
role="alert"
style="margin-bottom:0px; font-weight:bold; font-size:1.1em; padding:8px 5px;"
attrs="{'invisible': ['|', ('shopify_order_number', '=', False), ('shopify_delivery_type', '!=', 'delivery')]}">
🚚 <strong>[ ENVÍO CON GUÍA ]</strong> Este pedido se envía por paquetería. <strong>Requiere Número de Guía obligatorio.</strong>
</div>
</xpath>
<xpath expr="//field[@name='partner_id']" position="after">
<field name="shopify_delivery_address"
string="Dirección Completa"
attrs="{'invisible': [('shopify_order_number', '=', False)]}"
readonly="1"/>
<field name="shopify_delivery_phone"
string="Teléfono"
attrs="{'invisible': ['|', ('shopify_order_number', '=', False), ('shopify_delivery_phone', '=', False)]}"
readonly="1"/>
<field name="shopify_delivery_mobile"
string="Celular"
attrs="{'invisible': ['|', ('shopify_order_number', '=', False), ('shopify_delivery_mobile', '=', False)]}"
readonly="1"/>
</xpath>
<xpath expr="//field[@name='origin']" position="after">
<field name="shopify_order_number" readonly="1" string="# Shopify" attrs="{'invisible': [('shopify_order_number', '=', False)]}"/>
<field name="shopify_shipping_title" readonly="1" string="Método Envío Shopify" attrs="{'invisible': [('shopify_order_number', '=', False)]}"/>
<field name="shopify_delivery_type" invisible="1"/>
<field name="shopify_ready_for_pickup_notified" invisible="1"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,808 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Form View -->
<record id="view_shopify_config_form" model="ir.ui.view">
<field name="name">shopify.config.form</field>
<field name="model">shopify.config</field>
<field name="arch" type="xml">
<form string="Shopify">
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Ej: Tienda Principal"/>
</h1>
</div>
<notebook>
<!-- 1. PANEL DE CONTROL (DASHBOARD) -->
<page string="📊 Panel de Control" name="dashboard_main">
<div class="o_horizontal_separator font-weight-bold mb-3" style="font-size: 1.15em; color: #1E3A8A;">🏷️ Resumen de SKUs en Catálogo</div>
<div class="row mb-4 mt-2 border-bottom pb-3">
<div class="col-3 text-center border-right">
<span class="text-muted d-block uppercase small">Total Variantes</span>
<h2 class="mt-0 font-weight-bold"><field name="total_count"/></h2>
</div>
<div class="col-3 text-center border-right text-info">
<span class="text-muted d-block uppercase small">Faltan por Sync</span>
<h2 class="mt-0 font-weight-bold"><field name="pending_count"/></h2>
</div>
<div class="col-3 text-center border-right text-success">
<span class="text-muted d-block uppercase small">Ya en Shopify</span>
<h2 class="mt-0 font-weight-bold"><field name="synced_count"/></h2>
</div>
<div class="col-3 text-center text-danger">
<span class="text-muted d-block uppercase small">Duplicados en Web</span>
<h2 class="mt-0 font-weight-bold"><field name="duplicate_count"/></h2>
</div>
</div>
<div class="o_horizontal_separator font-weight-bold mb-3" style="font-size: 1.15em; color: #0f766e;">🖼️ Estado de Catálogo e Imágenes</div>
<div class="row mb-3 border-bottom pb-3">
<div class="col-4 text-center border-right text-primary">
<span class="text-muted d-block uppercase small">Productos Importados</span>
<h2 class="mt-0 font-weight-bold"><field name="products_total"/></h2>
</div>
<div class="col-4 text-center border-right text-success">
<span class="text-muted d-block uppercase small">✅ Con Imagen</span>
<h2 class="mt-0 font-weight-bold"><field name="products_with_image"/></h2>
</div>
<div class="col-4 text-center text-warning">
<span class="text-muted d-block uppercase small">⏳ Sin Imagen</span>
<h2 class="mt-0 font-weight-bold"><field name="products_without_image"/></h2>
</div>
</div>
<!-- LIVE CONSOLE AND PROGRESS AREA -->
<div class="mb-4 mt-3">
<!-- Progress bar shown only when syncing -->
<div class="row mb-3" attrs="{'invisible': [('is_syncing', '=', False)]}">
<div class="col-12">
<div class="d-flex align-items-center justify-content-between mb-1 text-warning font-weight-bold" style="font-size: 1.1em;">
<span><i class="fa fa-spinner fa-spin mr-1"/> Sincronización en curso...</span>
<span><field name="sync_progress" readonly="1"/>%</span>
</div>
<field name="sync_progress" widget="progressbar" class="mt-1"/>
</div>
</div>
<!-- Monospace Console Log, ALWAYS visible in the middle -->
<div class="d-flex align-items-center justify-content-between mb-2">
<div class="o_horizontal_separator font-weight-bold m-0" style="font-size: 1.15em; color: #1E3A8A;">📝 Bitácora de Procesos (Consola de Operaciones)</div>
<button name="action_release_lock"
string="🔓 Liberar bloqueo"
type="object"
class="btn btn-outline-danger btn-sm py-0"
style="height: 24px; font-size: 11px;"
attrs="{'invisible': [('is_syncing', '=', False)]}"
confirm="¿Liberar el bloqueo de sincronización?"/>
</div>
<field name="sku_simulation_log" readonly="1" widget="text"
style="font-family: 'Courier New', Courier, monospace; font-size: 12px; background-color: #0F172A; color: #38BDF8; border: 1px solid #1E293B; border-radius: 8px; padding: 12px; min-height: 180px; max-height: 300px; overflow-y: auto;"
placeholder="Consola lista. Ejecuta cualquier simulación, stock audit o subida de códigos..."/>
</div>
<!-- GUÍA RÁPIDA HTML -->
<div class="alert alert-light mt-3 border" style="padding:15px; border-radius:8px; background:#F8FAFC; border:1px solid #E2E8F0;">
<h3 style="color:#0F172A; margin-top:0; font-weight:bold; margin-bottom:12px; font-size: 1.25em;">🗺️ Guía Rápida de Operación</h3>
<div class="row">
<div class="col-4">
<div style="background:#EFF6FF; border-left:4px solid #3B82F6; padding:12px; border-radius:4px; height:100%;">
<div style="font-weight:bold; color:#1E3A8A; font-size:13px; margin-bottom:6px;">🏷️ FASE 1: Limpieza de SKUs</div>
<p style="margin:0; font-size:11px; color:#334155; line-height: 1.4;">
1. Sube tu archivo CSV en la pestaña de <b>Conexión y API</b>.<br/>
2. Ve a <b>Asignar SKUs</b> y presiona <b>Simular SKUs</b> para revisar discrepancias.<br/>
3. Selecciona y haz clic en <b>Aplicar SKUs a Shopify</b>.
</p>
</div>
</div>
<div class="col-4">
<div style="background:#ECFDF5; border-left:4px solid #10B981; padding:12px; border-radius:4px; height:100%;">
<div style="font-weight:bold; color:#064E3B; font-size:13px; margin-bottom:6px;">📦 FASE 2: Catálogo y Stock</div>
<p style="margin:0; font-size:11px; color:#334155; line-height: 1.4;">
1. Ve a <b>Importar Catálogo</b> para descargar productos nuevos desde Shopify a Odoo.<br/>
2. Presiona <b>Marcar Discrepancias de Stock</b> para ver diferencias físicas.<br/>
3. Ajusta el stock usando <b>Actualizar Stock Seleccionado</b>.
</p>
</div>
</div>
<div class="col-4">
<div style="background:#FFFBEB; border-left:4px solid #F59E0B; padding:12px; border-radius:4px; height:100%;">
<div style="font-weight:bold; color:#78350F; font-size:13px; margin-bottom:6px;">🔖 FASE 3: Generación EAN-13</div>
<p style="margin:0; font-size:11px; color:#334155; line-height: 1.4;">
1. Ve a <b>Barcodes EAN-13</b>.<br/>
2. Haz clic en <b>1. Generar EAN-13</b> para folios nuevos sin colisiones.<br/>
3. Haz clic en <b>2. Subir a Shopify</b>.<br/>
4. Exporta tu archivo para Zebra/Dymo.
</p>
</div>
</div>
</div>
</div>
</page>
<!-- 2. SINCRONIZACIÓN MANUAL -->
<page string="🚀 Sincronización Manual" name="manual_sync">
<div style="background: #F8FAFC; padding: 20px; border-radius: 8px; border: 1px solid #E2E8F0; margin-bottom: 20px;">
<h3 style="color: #1E3A8A; margin-top: 0;">1. 📥 Sincronizar Catálogo (Productos y Variantes)</h3>
<p><strong>¿Qué hace?:</strong> Se conecta a Shopify y descarga todo tu catálogo. Si encuentra productos agrupados (ej. Playera con tallas S, M, L), construye perfectamente esa misma estructura de variantes dentro de Odoo.</p>
<p><strong>¿Cuándo usarlo?:</strong> Cuando creaste productos nuevos en Shopify y quieres traerlos a Odoo. <em>Nota: Esta descarga es pesada, es normal si la pantalla marca "conexión perdida", Odoo seguirá procesando en el fondo.</em></p>
<p><strong>¿Cuándo NO usarlo?:</strong> Si buscas descargar cantidades de inventario o fotografías, este botón no lo hace para que el proceso sea más rápido.</p>
<button name="action_sync_products"
string="Ejecutar: Sincronizar Catálogo"
type="object"
class="btn-primary mt-2"
confirm="¿Importar todos los productos de Shopify a Odoo? Esto puede tardar varios minutos si el catálogo es grande."/>
</div>
<div style="background: #F8FAFC; padding: 20px; border-radius: 8px; border: 1px solid #E2E8F0; margin-bottom: 20px;">
<h3 style="color: #065F46; margin-top: 0;">2. 🖼️ Descargar Imágenes</h3>
<p><strong>¿Qué hace?:</strong> Revisa qué productos en Odoo no tienen fotografía y las descarga directamente desde Shopify en segundo plano.</p>
<p><strong>¿Cuándo usarlo?:</strong> Después de haber sincronizado el catálogo (Paso 1). Como es un proceso en segundo plano, puedes darle clic y seguir trabajando en otras pantallas sin que Odoo se trabe.</p>
<button name="action_download_product_images"
string="Ejecutar: Descargar Imágenes"
type="object"
class="btn-info mt-2"
confirm="¿Descargar imágenes faltantes desde Shopify? El proceso se ejecutará silenciosamente de fondo."/>
</div>
<div style="background: #FDF2F8; padding: 20px; border-radius: 8px; border: 1px solid #FBCFE8; margin-bottom: 20px;">
<h3 style="color: #9D174D; margin-top: 0;">3. 📦 Importar Stock a Odoo (Shopify Manda)</h3>
<p><strong>¿Qué hace?:</strong> Descarga el inventario exacto que hay en cada sucursal de Shopify y <strong>aplasta</strong> las existencias de Odoo para que cuadren a la fuerza.</p>
<p><strong>¿Cuándo usarlo?:</strong> Durante la configuración inicial para traer tu stock de Shopify a Odoo por primera vez.</p>
<p style="color: #9D174D;"><strong>⚠️ ¿Cuándo NO usarlo?:</strong> ¡NUNCA lo uses en la operación diaria regular! Una vez que Odoo se convierta en tu "Fuente de Verdad" de inventario, Odoo debe mandar el stock a Shopify (usando las reglas), no al revés.</p>
<button name="action_import_inventory_from_shopify"
string="Ejecutar: Importar Stock a Odoo"
type="object"
class="btn-danger mt-2"
confirm="¡PRECAUCIÓN! Esto sobrescribirá las cantidades físicas de Odoo con lo que diga Shopify. ¿Estás seguro?"/>
</div>
</page>
<!-- 3. CONEXIÓN Y API -->
<page string="🔌 Conexión y API" name="connection_api">
<div class="row">
<div class="col-6">
<group string="🔑 Credenciales de Shopify">
<field name="shop_url" placeholder="tienda.myshopify.com"/>
<field name="api_key" string="Client ID" placeholder="803268f..."/>
<field name="api_secret" string="Client Secret" password="1" placeholder="••••••••••••••••"/>
<field name="is_syncing" invisible="1"/>
<field name="barcode_is_pushing" invisible="1"/>
<field name="barcode_error_count" invisible="1"/>
</group>
<div class="alert alert-info py-2 mt-1" style="font-size: 0.82em;">
<i class="fa fa-info-circle mr-1"/>
Ingresa el <strong>Client ID</strong> y <strong>Client Secret</strong> de tu App en Shopify y presiona <strong>Generar Token</strong>. El token dura 24h y se renueva solo.
</div>
<div class="mt-2 mb-3">
<button name="action_get_access_token" string="⚡ Generar Token" type="object" icon="fa-key" class="btn btn-primary mr-2"/>
<button name="action_test_connection" string="Probar Conexión" type="object" icon="fa-check-circle" class="btn btn-info"/>
</div>
<group string="🔒 Token Activo">
<field name="access_token" password="1" readonly="1" string="Access Token"/>
<field name="access_token_expiry" readonly="1" string="Válido hasta"/>
</group>
</div>
<div class="col-6">
<group string="📁 Archivo Maestro (CSV)">
<field name="csv_file" filename="csv_filename" help="Sube el archivo REV2 SKU .csv aquí"/>
<field name="csv_filename" invisible="1" force_save="1"/>
</group>
<div class="alert alert-warning py-2 mt-1" style="font-size: 0.82em;">
<i class="fa fa-exclamation-triangle mr-1"/>
El CSV se utiliza para asignar las nomenclaturas personalizadas de SKU cargadas por el equipo de diseño.
</div>
</div>
</div>
</page>
<!-- HERRAMIENTAS LEGACY -->
<page string="⚙️ Herramientas de Mantenimiento" name="legacy_tools">
<notebook>
<!-- 3. ASIGNAR SKUS -->
<page string="🏷️ Paso 1: Asignar SKUs" name="sku_simulation">
<div style="background: #F8FAFC; padding: 15px; border-radius: 8px; border: 1px solid #E2E8F0; margin-bottom: 15px;">
<div class="row">
<div class="col-12 mb-2">
<div class="d-flex align-items-center">
<i class="fa fa-search mr-2 text-muted"/>
<field name="search_term" placeholder="Buscar por nombre, variante o SKU en todas las pestañas..." nolabel="1" class="w-100"/>
</div>
</div>
</div>
<div class="row">
<div class="col-4">
<div class="o_form_label font-weight-bold mb-1">Filtrar por Categoría:</div>
<field name="filter_category" widget="selection" nolabel="1"/>
</div>
<div class="col-4">
<div class="o_form_label font-weight-bold mb-1">Marcar por Categoría:</div>
<div class="d-flex">
<field name="select_category" placeholder="Categoría..." class="mr-2"/>
<button name="action_select_by_category" string="Marcar" type="object" class="btn btn-primary btn-sm" icon="fa-filter"/>
</div>
</div>
<div class="col-4">
<div class="o_form_label font-weight-bold mb-1">Acciones Rápidas:</div>
<div>
<button name="action_simulate_skus" string="⚙️ Simular SKUs (Paso 1)" type="object" class="btn btn-secondary btn-sm mr-1" icon="fa-refresh" attrs="{'invisible': [('is_syncing', '=', True)]}"/>
<button name="action_apply_skus_to_shopify" string="📤 Aplicar SKUs (Paso 2)" type="object" class="btn btn-warning btn-sm mr-1" icon="fa-cloud-upload" attrs="{'invisible': [('is_syncing', '=', True)]}" confirm="¿Actualizar los SKUs seleccionados en Shopify?"/>
<button name="action_export_sku_barcodes" string="Exportar SKU + Barcode" type="object" class="btn btn-outline-info btn-sm" icon="fa-download"/>
<button name="action_release_lock" string="Liberar bloqueo" type="object" class="btn btn-link text-danger btn-sm d-block mt-1" attrs="{'invisible': [('is_syncing', '=', False)]}" confirm="¿Liberar el bloqueo de sincronización?"/>
</div>
</div>
</div>
</div>
<notebook>
<page string="PASO 0: Duplicados en Shopify" name="sku_duplicates" attrs="{'invisible': [('duplicate_count', '=', 0)]}">
<div class="mb-3 d-flex align-items-center">
<div class="flex-grow-1">
<p class="text-danger font-weight-bold mb-0">
<i class="fa fa-exclamation-triangle mr-1"/>
Se detectaron SKUs repetidos en tu tienda. Se recomienda corregirlos usando el "SKU Sugerido" antes de continuar.
</p>
</div>
<button name="action_export_duplicates" string="Exportar Reporte para Cliente (CSV)" type="object" class="btn btn-outline-primary" icon="fa-download"/>
</div>
<field name="duplicate_report_file" filename="duplicate_report_filename" invisible="1"/>
<field name="duplicate_report_filename" invisible="1"/>
<field name="duplicate_line_ids" search="1">
<tree editable="bottom" decoration-danger="is_shopify_duplicate" default_order="old_sku">
<field name="is_shopify_duplicate" invisible="1"/>
<field name="is_selected" widget="boolean_toggle" string="Sync?"/>
<field name="image_html" string="Imagen" readonly="1"/>
<field name="product_name"/>
<field name="variant_name"/>
<field name="old_sku" string="SKU Duplicado en Web" class="font-weight-bold"/>
<field name="duplicate_with" string="¿Con quién choca?" class="text-danger small"/>
<field name="suggested_sku" string="SKU Sugerido (Corrección)" class="text-primary"/>
<field name="sku_category_code"/>
<field name="sync_state" widget="badge" decoration-info="sync_state == 'draft'"/>
</tree>
</field>
</page>
<page string="PASO 1: Sin SKU" name="sku_pending">
<div class="mb-2 d-flex align-items-center">
<button name="action_select_all" string="Marcar Todos" type="object" class="btn-sm btn-outline-info mr-1" icon="fa-check-square-o"/>
<button name="action_unselect_all" string="Desmarcar Todos" type="object" class="btn-sm btn-outline-secondary" icon="fa-square-o"/>
</div>
<field name="pending_line_ids" search="1">
<tree editable="bottom" decoration-danger="sync_state == 'error' or is_shopify_duplicate">
<field name="is_shopify_duplicate" invisible="1"/>
<field name="is_selected" widget="boolean_toggle" string="Sync?"/>
<field name="image_html" string="Imagen" readonly="1"/>
<field name="product_name" readonly="1"/>
<field name="variant_name" readonly="1"/>
<field name="sku_category_code" readonly="1"/>
<field name="old_sku" readonly="1"/>
<field name="sku_source" widget="badge" decoration-success="sku_source == 'csv'" decoration-info="sku_source == 'logic'" readonly="1"/>
<field name="sync_state" widget="badge" decoration-danger="sync_state == 'error'" decoration-warning="sync_state == 'has_sku'" decoration-info="sync_state == 'draft'" readonly="1"/>
<field name="sync_history" string="Respuesta Shopify" optional="show"/>
<field name="shopify_handle" optional="hide"/>
<field name="shopify_title" optional="hide"/>
</tree>
</field>
</page>
<page string="PASO 2: Con SKU" name="sku_history">
<field name="synced_line_ids" readonly="1" search="1">
<tree decoration-success="sync_state == 'synced'">
<field name="image_html" string="Imagen" readonly="1"/>
<field name="product_name"/>
<field name="variant_name"/>
<field name="sku_category_code"/>
<field name="old_sku" string="SKU en Shopify"/>
<field name="barcode" string="Cód. de Barras" optional="show"/>
<field name="sku_source" widget="badge" decoration-success="sku_source == 'csv'" decoration-info="sku_source == 'logic'"/>
<field name="sync_state" widget="badge" decoration-success="sync_state == 'synced'" decoration-warning="sync_state == 'has_sku'" decoration-info="sync_state == 'draft'"/>
<field name="shopify_handle" optional="hide"/>
<field name="shopify_title" optional="hide"/>
</tree>
</field>
</page>
</notebook>
</page>
<!-- 4. IMPORTAR CATÁLOGO -->
<page string="📦 Paso 2: Importar Catálogo" name="sku_all">
<!-- ═══ PANEL A: CATÁLOGO ═══ -->
<div style="background:#e8f4fd; padding:12px 15px; border-radius:8px; margin-bottom:10px; border-left:4px solid #0d6efd;">
<div style="font-weight:bold; color:#0d6efd; margin-bottom:8px;">
📦 GESTIÓN DE CATÁLOGO — Importar productos nuevos de Shopify a Odoo
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<field name="total_variant_count" readonly="1" nolabel="1" style="display:none;"/>
<span style="font-size:12px; color:#6c757d;">
Total: <field name="total_variant_count" readonly="1" nolabel="1"/> |
En Odoo ✅: <field name="imported_variant_count" readonly="1" nolabel="1"/> |
Pendientes ⏳: <field name="pending_variant_count" readonly="1" nolabel="1"/> |
Seleccionados: <field name="selected_variant_count" readonly="1" nolabel="1"/>
</span>
<button name="action_select_all_pending" type="object"
string="✅ Marcar Pendientes de Importar"
class="btn-outline-primary btn-sm"
icon="fa-check-square-o"/>
<button name="action_unselect_all" type="object"
string="⬜ Desmarcar Todo"
class="btn-outline-secondary btn-sm"
icon="fa-square-o"/>
<button name="action_mass_import_to_odoo" type="object"
string="🚀 IMPORTAR SELECCIONADOS A ODOO"
class="btn-primary btn-sm"
icon="fa-cloud-download"/>
<button name="action_audit_inventory" type="object"
string="🔍 Auditar Catálogo"
class="btn-outline-warning btn-sm"
icon="fa-search"/>
</div>
</div>
<!-- ═══ PANEL B: STOCK ═══ -->
<div style="background:#e8f8f0; padding:12px 15px; border-radius:8px; margin-bottom:12px; border-left:4px solid #198754;">
<div style="font-weight:bold; color:#198754; margin-bottom:8px;">
📊 ACTUALIZAR STOCK — Traer cantidades de Shopify a Odoo (Shopify manda)
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<button name="action_select_stock_discrepancies" type="object"
string="⚠️ Marcar Discrepancias de Stock"
class="btn-outline-success btn-sm"
icon="fa-exclamation-triangle"
help="Selecciona solo los productos que tienen diferencia entre Shopify y Odoo"/>
<button name="action_update_stock_from_shopify" type="object"
string="📥 Actualizar Stock Seleccionado (Shopify → Odoo)"
class="btn-success btn-sm"
icon="fa-refresh"
confirm="¿Actualizar las cantidades en Odoo para los productos seleccionados? Shopify es la fuente de verdad."/>
<button name="action_release_lock" type="object"
string="🔓 Liberar bloqueo"
class="btn-link btn-sm"
style="color:#dc3545;"
confirm="¿Liberar el bloqueo de sincronización?"/>
</div>
</div>
<field name="all_line_ids" readonly="1" search="1">
<tree decoration-info="exists_in_odoo" decoration-muted="exists_in_odoo">
<field name="is_selected" string="Sel?" attrs="{'invisible': [('exists_in_odoo', '=', True)]}"/>
<field name="image_html" string="Imagen" readonly="1"/>
<field name="product_name" readonly="1"/>
<field name="variant_name" readonly="1"/>
<field name="old_sku" readonly="1"/>
<field name="shopify_qty_total" string="Stock Shopify"/>
<field name="odoo_qty_total" string="Stock Odoo"/>
<button name="action_refresh_shopify_stock_info" string="Consultar" type="object" icon="fa-refresh" class="btn-sm btn-secondary"/>
<field name="barcode" string="Cód. de Barras" optional="hide" readonly="1"/>
<field name="exists_in_odoo" string="En Odoo" readonly="1"/>
<button name="action_import_to_odoo" string="Importar" type="object"
class="btn-primary btn-sm" icon="fa-download"
attrs="{'invisible': [('exists_in_odoo', '=', True)]}"/>
<button name="action_sync_inventory_only" string="↓ Act. Stock" type="object"
class="btn-success btn-sm" icon="fa-refresh"
attrs="{'invisible': [('exists_in_odoo', '=', False)]}"
confirm="¿Traer stock de Shopify para este producto?"/>
<field name="sync_state" widget="badge"
decoration-success="sync_state == 'synced'"
decoration-warning="sync_state == 'has_sku'"
decoration-info="sync_state == 'draft'"
readonly="1" optional="hide"/>
<field name="generated_barcode" string="EAN-13" optional="hide" readonly="1"/>
<field name="barcode_sync_state" widget="badge"
decoration-success="barcode_sync_state == 'done'"
decoration-warning="barcode_sync_state == 'pending'"
decoration-danger="barcode_sync_state == 'error'"
optional="hide" readonly="1"/>
<field name="shopify_handle" optional="hide" readonly="1"/>
</tree>
</field>
</page>
<!-- 5. BARCODES EAN-13 -->
<page string="🔖 Paso 3: Barcodes EAN-13" name="ean13_barcodes">
<div class="row text-center mb-3 pb-3" style="border-bottom:1px solid #dee2e6;">
<div class="col-3">
<div class="text-muted small mb-1">Sin generar</div>
<div class="h3 text-secondary"><field name="barcode_none_count" readonly="1"/></div>
</div>
<div class="col-3">
<div class="text-muted small mb-1">Pendientes</div>
<div class="h3 text-warning"><field name="barcode_pending_count" readonly="1"/></div>
</div>
<div class="col-3">
<div class="text-muted small mb-1">Subidos ✓</div>
<div class="h3 text-success"><field name="barcode_pushed_count" readonly="1"/></div>
</div>
<div class="col-3">
<div class="text-muted small mb-1">Errores</div>
<div class="h3 text-danger"><field name="barcode_error_count" readonly="1"/></div>
</div>
</div>
<div class="d-flex align-items-center mb-3">
<div class="o_form_label mr-2 font-weight-bold">Prefijo empresa:</div>
<field name="barcode_prefix" style="width:130px;" class="mr-3"/>
<div class="o_form_label mr-2 text-muted">Progreso:</div>
<field name="barcode_progress" readonly="1" widget="float" digits="[3,1]" class="mr-1"/>
<span class="text-muted mr-3">%</span>
<span attrs="{'invisible': [('barcode_is_pushing', '=', False)]}"
class="badge badge-warning ml-2">
<i class="fa fa-spin fa-spinner mr-1"/>Subiendo barcodes...
</span>
</div>
<div class="mb-3 d-flex" style="gap:8px;">
<button name="action_generate_ean13"
string="1. Generar EAN-13"
type="object"
class="btn btn-primary"
icon="fa-barcode"
attrs="{'invisible': [('barcode_is_pushing', '=', True)]}"/>
<button name="action_push_barcodes_to_shopify"
string="2. Subir a Shopify"
type="object"
class="btn btn-success"
icon="fa-cloud-upload"
attrs="{'invisible': [('barcode_is_pushing', '=', True)]}"
confirm="¿Subir todos los barcodes pendientes a Shopify? El proceso corre en segundo plano."/>
<button name="action_retry_barcode_errors"
string="Reintentar errores"
type="object"
class="btn btn-warning"
icon="fa-refresh"
attrs="{'invisible': [('barcode_error_count', '=', 0)]}"/>
<button name="action_mark_barcodes_done"
string="✓ Marcar como Subidos"
type="object"
class="btn btn-info"
icon="fa-check-square-o"
attrs="{'invisible': [('barcode_pending_count', '=', 0)]}"
confirm="¿Marcar todos los pendientes como subidos correctamente? Hazlo solo si ya verificaste en Shopify."/>
<button name="action_export_barcode_labels"
string="Exportar Etiquetas CSV"
type="object"
class="btn btn-outline-secondary"
icon="fa-file-text-o"/>
<button name="action_export_inventory_excel"
string="Exportar Excel (Inventario + Próximos)"
type="object"
class="btn btn-success"
icon="fa-file-excel-o"/>
</div>
<div class="o_horizontal_separator font-weight-bold mb-3" style="font-size: 1.15em; color: #1E3A8A;">📋 Detalle de Códigos de Barras (EAN-13)</div>
<field name="simulation_line_ids" search="1">
<tree editable="bottom" create="false" delete="false" decoration-success="barcode_sync_state == 'done'" decoration-warning="barcode_sync_state == 'pending'" decoration-danger="barcode_sync_state == 'error'">
<field name="is_selected" widget="boolean_toggle" string="Sel?"/>
<field name="image_html" string="Imagen" readonly="1"/>
<field name="product_name" readonly="1"/>
<field name="variant_name" readonly="1"/>
<field name="old_sku" readonly="1"/>
<field name="barcode" string="Cód. de Barras (Shopify)" readonly="1"/>
<field name="generated_barcode" string="EAN-13 Generado" readonly="1"/>
<field name="barcode_sync_state" widget="badge"
decoration-success="barcode_sync_state == 'done'"
decoration-warning="barcode_sync_state == 'pending'"
decoration-danger="barcode_sync_state == 'error'"
readonly="1"/>
<field name="barcode_sync_error" string="Detalle de Error" readonly="1"/>
</tree>
</field>
<div class="alert alert-info mt-2" role="alert">
<i class="fa fa-info-circle mr-1"/>
Para ver el detalle EAN-13 por producto, ve a la pestaña
<strong>Paso 2: Importar Catálogo</strong> y activa las columnas
<em>EAN-13 Generado</em>, <em>Estado Barcode</em> y <em>Error Barcode</em>
desde el botón de columnas opcionales en el encabezado de la tabla.
</div>
</page>
<!-- PASO 4: IMPORTAR COSTOS -->
<page string="💰 Paso 4: Sincronizar Costos" name="sync_costs">
<div class="row">
<div class="col-md-4 text-center">
<h4>Pendientes</h4>
<h2 class="text-warning"><field name="cost_pending_count" readonly="1"/></h2>
</div>
<div class="col-md-4 text-center">
<h4>Subidos ✓</h4>
<h2 class="text-success"><field name="cost_done_count" readonly="1"/></h2>
</div>
<div class="col-md-4 text-center">
<h4>Errores</h4>
<h2 class="text-danger"><field name="cost_error_count" readonly="1"/></h2>
</div>
</div>
<hr/>
<div class="mb-3">
<field name="cost_is_fetching" invisible="1"/>
<field name="cost_is_pushing" invisible="1"/>
<button name="action_fetch_shopify_costs"
string="1. Consultar Costos"
type="object"
class="btn btn-primary"
icon="fa-download"
attrs="{'invisible': [('cost_is_fetching', '=', True)]}"/>
<span class="text-muted ml-2" attrs="{'invisible': [('cost_is_fetching', '=', False)]}">
<i class="fa fa-spinner fa-spin"/> Consultando Costos en Shopify...
</span>
<button name="action_apply_shopify_costs"
string="2. Aplicar Costos a Odoo"
type="object"
class="btn btn-success ml-2"
icon="fa-cloud-upload"
attrs="{'invisible': ['|', ('cost_pending_count', '=', 0), ('cost_is_pushing', '=', True)]}"/>
<span class="text-muted ml-2" attrs="{'invisible': [('cost_is_pushing', '=', False)]}">
<i class="fa fa-spinner fa-spin"/> Aplicando Costos en Odoo...
</span>
</div>
<div class="o_horizontal_separator font-weight-bold mb-3" style="font-size: 1.15em; color: #1E3A8A;">📋 Detalle de Costos Extraídos</div>
<field name="cost_line_ids" search="1">
<tree editable="bottom" create="false" delete="false" decoration-success="state == 'done'" decoration-warning="state == 'pending'" decoration-danger="state == 'error'">
<field name="product_tmpl_id" readonly="1"/>
<field name="product_id" readonly="1"/>
<field name="default_code" readonly="1"/>
<field name="shopify_inventory_item_id" readonly="1" optional="hide"/>
<field name="current_cost" string="Costo Actual" readonly="1"/>
<field name="new_cost" string="Costo Shopify" readonly="1"/>
<field name="state" widget="badge"
decoration-success="state == 'done'"
decoration-warning="state == 'pending'"
decoration-danger="state == 'error'"
readonly="1"/>
<field name="error_message" readonly="1"/>
</tree>
</field>
</page>
</notebook>
</page>
<!-- 6. SUCURSALES (MAPEO) -->
<page string="🏬 Sucursales (Mapeo)" name="sucursales">
<group>
<group string="Sincronización de Stock">
<button name="action_sync_inventory" string="Sincronizar Stock" type="object" class="btn-primary" icon="fa-refresh"/>
</group>
<group string="Sucursal Principal Shopify">
<field name="shopify_location_name" readonly="1" string="Nombre Sucursal"/>
<field name="shopify_location_id" readonly="1" string="ID Sucursal"/>
<button name="action_fetch_locations" string="🔄 Actualizar Ubicaciones" type="object" class="btn-secondary" icon="fa-refresh"/>
</group>
</group>
<div class="alert alert-info" role="alert">
<strong>Mapeo de Sucursales:</strong> Vincula cada sucursal de Shopify con un almacén o ubicación de Odoo para sincronizar el stock correctamente.
</div>
<field name="location_mapping_ids">
<tree editable="bottom" create="false" delete="false">
<field name="shopify_location_name" readonly="1"/>
<field name="shopify_location_id" readonly="1"/>
<field name="odoo_warehouse_id"/>
<field name="odoo_location_id" domain="[('warehouse_id', '=', odoo_warehouse_id)]"/>
</tree>
</field>
</page>
<!-- 7. REGLAS DE VENTAS -->
<page string="🛒 Reglas de Ventas" name="sales_config">
<group>
<group string="Configuración Financiera">
<field name="order_pricelist_id" required="1"/>
<field name="order_journal_id" required="1"/>
<field name="default_warehouse_id" required="1"/>
<field name="sales_team_id"/>
</group>
<group string="Automatizaciones">
<field name="auto_create_draft_invoice"/>
<field name="auto_workflow_payment" attrs="{'invisible': [('auto_create_draft_invoice', '=', False)]}"/>
<field name="payment_journal_id" attrs="{'invisible': [('auto_workflow_payment', '=', False)], 'required': [('auto_workflow_payment', '=', True)]}"/>
<field name="auto_sync_products"/>
<field name="auto_process_refunds"/>
<field name="shopify_sandbox_mode"/>
</group>
</group>
<group string="Estado de Sincronización">
<field name="last_order_sync_date" string="Buscar pedidos desde" help="Puedes editar esta fecha para buscar ventas pasadas (ej. de la última semana). Si la dejas en blanco, traerá las últimas 100 ventas."/>
<field name="shopify_import_cutoff_date" string="Fecha de Corte de Inventario" help="Pedidos de Shopify anteriores a esta fecha no descontarán stock en Odoo."/>
<button name="action_import_orders" string="📥 IMPORTAR PEDIDOS RECIENTES" type="object" class="btn btn-primary mt-2" attrs="{'invisible': [('is_syncing', '=', True)]}"/>
</group>
<div class="alert alert-warning text-center" role="alert" attrs="{'invisible': [('shopify_sandbox_mode', '=', False)]}" style="font-weight: bold; padding: 8px; font-size: 1.05em;">
🛡️ MODO SANDBOX ACTIVO: Las devoluciones y reembolsos (tanto manuales como automáticos) solo se procesarán para pedidos con productos de prueba (SKU contiene 'PRUEBA'). Los pedidos reales de clientes están protegidos.
</div>
</page>
<!-- 8. TAREAS PROGRAMADAS (CRONS) -->
<page string="⏰ Tareas Programadas (Crons)" name="cron_config">
<!-- Banner Superior Premium -->
<div class="bg-gradient-primary text-white p-4 rounded mb-4" style="background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); margin-bottom: 25px; padding: 15px; color: white !important;">
<div class="d-flex align-items-center">
<div style="font-size: 2.2em; margin-right: 15px; float: left;"></div>
<div style="margin-left: 60px;">
<h3 class="m-0" style="font-weight: 700; color: white; margin-top: 0; margin-bottom: 5px;">Panel de Automatizaciones (Cron Jobs)</h3>
<p class="m-0" style="opacity: 0.9; font-size: 0.95em; color: white; margin: 0;">Configura y monitorea en tiempo real los procesos automáticos en segundo plano que sincronizan Odoo y Shopify de manera continua.</p>
</div>
</div>
<div style="clear: both;"/>
</div>
<group>
<!-- 1. Cron de Pedidos -->
<group string="📥 Importación de Pedidos (Shopify ➔ Odoo)">
<div class="alert alert-info" role="alert" colspan="2" style="margin-bottom: 15px; border-left: 4px solid #17a2b8; padding: 10px; border-radius: 4px; color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; width: 100%; display: block;">
💡 <b>¿Qué hace?</b> Busca y descarga automáticamente nuevos pedidos pagados o autorizados de Shopify a Odoo.<br/>
💡 <b>Caso de uso:</b> Registra las ventas en tiempo real sin intervención manual para que almacén pueda facturar y empacar de inmediato.<br/>
💡 <b>Frecuencia recomendada:</b> 15 o 30 minutos en producción.<br/>
💡 <b>Seguridad:</b> No altera el stock físico de Odoo, solo lee datos de Shopify y escribe órdenes nuevas en Odoo.
</div>
<field name="auto_import_orders" string="Activar Importación Automática" help="Activa la búsqueda y descarga automatizada de nuevos pedidos de Shopify a Odoo en segundo plano."/>
<field name="order_sync_interval" string="Frecuencia de Búsqueda" attrs="{'invisible': [('auto_import_orders', '=', False)]}" help="Intervalo de tiempo con el que se ejecuta la importación de pedidos. Frecuencia recomendada para producción: 15 o 30 minutos."/>
<field name="cron_orders_status" string="Estado de Ejecución" readonly="1" style="font-weight: bold;"/>
</group>
<!-- 2. Cron de Inventario -->
<group string="📤 Sincronización de Inventario (Odoo ➔ Shopify)">
<div class="alert alert-success" role="alert" colspan="2" style="margin-bottom: 15px; border-left: 4px solid #28a745; padding: 10px; border-radius: 4px; color: #155724; background-color: #d4edda; border-color: #c3e6cb; width: 100%; display: block;">
🔥 <b>CRÍTICO DE SEGURIDAD:</b> Envía el inventario físico real de Odoo hacia Shopify para mantenerlos alineados.<br/>
💡 <b>¿Qué hace?</b> Si las existencias de un producto en Odoo llegan a 0 unidades, cambia el botón en Shopify automáticamente a "Agotado" para evitar sobreventas.<br/>
💡 <b>Frecuencia recomendada:</b> 30 o 60 minutos en producción, o 12 horas en fases iniciales.<br/>
⚠️ <b>Regla de Oro:</b> Odoo es la Fuente de Verdad. Este cron reescribe el inventario de Shopify con las unidades físicas de Odoo.
</div>
<field name="cron_sync_inventory_active" string="Activar Envío de Stock" help="CRÍTICO: Activa la actualización automática del inventario desde Odoo hacia Shopify en segundo plano."/>
<field name="cron_sync_inventory_interval" string="Frecuencia de Stock" attrs="{'invisible': [('cron_sync_inventory_active', '=', False)]}" help="Intervalo de ejecución para actualizar las existencias en Shopify. Frecuencia recomendada: 30 o 60 minutos en producción."/>
<field name="cron_inventory_status" string="Estado de Ejecución" readonly="1" style="font-weight: bold;"/>
</group>
</group>
<group>
<!-- 3. Cron de Imágenes -->
<group string="📸 Auto-Descarga de Imágenes (Shopify ➔ Odoo)">
<div class="alert alert-warning" role="alert" colspan="2" style="margin-bottom: 15px; border-left: 4px solid #ffc107; padding: 10px; border-radius: 4px; color: #856404; background-color: #fff3cd; border-color: #ffeeba; width: 100%; display: block;">
📸 <b>¿Qué hace?</b> Busca periódicamente productos mapeados o creados en Odoo que no tengan imagen asignada y descarga su foto de portada desde Shopify.<br/>
💡 <b>Caso de uso:</b> Mantiene tu base de datos de Odoo visualmente completa de forma automática.<br/>
💡 <b>Frecuencia recomendada:</b> 2 o 6 horas.<br/>
💡 <b>Seguridad:</b> Se ejecuta en segundo plano de manera ligera por lotes pequeños para no ralentizar el servidor.
</div>
<field name="auto_import_images" string="Activar Descarga de Fotos" help="Busca automáticamente productos que no tengan imagen en Odoo y descarga sus portadas desde Shopify."/>
<field name="image_sync_interval" string="Frecuencia de Fotos" attrs="{'invisible': [('auto_import_images', '=', False)]}" help="Frecuencia con la que se buscarán nuevas imágenes de Shopify. Se recomienda 2 o 6 horas."/>
<field name="cron_images_status" string="Estado de Ejecución" readonly="1" style="font-weight: bold;"/>
</group>
<!-- 4. Cron de Renovación de Tokens -->
<group string="🔑 Renovación de Tokens de Acceso (OAuth)">
<div class="alert alert-secondary" role="alert" colspan="2" style="margin-bottom: 15px; border-left: 4px solid #6c757d; padding: 10px; border-radius: 4px; color: #383d41; background-color: #e2e3e5; border-color: #d6d8db; width: 100%; display: block;">
🔒 <b>Proceso Técnico Automatizado:</b><br/>
Shopify emite tokens de acceso con una duración máxima de 24 horas.<br/>
Este cron técnico interno renueva automáticamente las llaves de acceso cada 20 horas para prevenir desconexiones de servicio y asegurar que las integraciones nunca expiren.
</div>
<field name="cron_tokens_status" string="Estado de Renovación" readonly="1" style="font-weight: bold;"/>
</group>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Tree View -->
<record id="view_shopify_config_tree" model="ir.ui.view">
<field name="name">shopify.config.tree</field>
<field name="model">shopify.config</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="shop_url"/>
</tree>
</field>
</record>
<!-- Action -->
<record id="action_shopify_config" model="ir.actions.act_window">
<field name="name">Configuración de Shopify</field>
<field name="res_model">shopify.config</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Estructura de Menú Ad Hoc (Nivel Principal) con Icono de Shopify -->
<menuitem id="menu_shopify_root"
name="Shopify Connect"
web_icon="fa fa-shopping-bag,#96bf48,#FFFFFF"
groups="hazard_shopify.group_hazard_finance,base.group_system"
sequence="1"/>
<menuitem id="menu_shopify_dashboard"
name="Panel de Control"
parent="menu_shopify_root"
action="action_shopify_config"
sequence="10"/>
<!-- Mantener rastro del menú anterior por si queremos regresar (Comentado) -->
<!--
<menuitem id="menu_shopify_config_old" name="Configuración de Tiendas" parent="sale.menu_sale_config" action="action_shopify_config" sequence="100"/>
-->
</data>
<!-- Simulation Line Form View (Detalle del Producto) -->
<record id="view_shopify_sku_simulation_line_form" model="ir.ui.view">
<field name="name">shopify.sku.simulation.line.form</field>
<field name="model">shopify.sku.simulation.line</field>
<field name="arch" type="xml">
<form string="Detalle de Producto Shopify">
<header>
<button name="action_refresh_shopify_stock_info" string="🔍 Solo Consultar Stock Shopify" type="object" class="btn-secondary"/>
<button name="action_import_to_odoo" string="Importar a Odoo" type="object" class="btn-primary" attrs="{'invisible': [('exists_in_odoo', '=', True)]}"/>
<button name="action_sync_inventory_only"
string="📥 Traer Stock de Shopify a Odoo"
type="object"
class="btn-info"
attrs="{'invisible': [('exists_in_odoo', '=', False)]}"
confirm="¿Estás seguro? Esto ajustará las cantidades de Odoo según lo que hay actualmente en Shopify para este producto."/>
</header>
<sheet>
<div class="oe_title">
<label for="product_name" string="Producto"/>
<h1><field name="product_name"/></h1>
<h3><field name="variant_name" attrs="{'invisible': [('variant_name', '=', 'Default Title')]}"/></h3>
</div>
<group>
<group string="Identificadores">
<field name="old_sku" string="SKU (Shopify)"/>
<field name="barcode" string="Código de Barras"/>
<field name="shopify_variant_id" groups="base.group_no_one"/>
<field name="shopify_inventory_item_id" groups="base.group_no_one"/>
</group>
<group string="Estado en Odoo">
<field name="exists_in_odoo" readonly="1"/>
<field name="is_imported" readonly="1"/>
<field name="last_sync_date" string="Última Sincronización" readonly="1"/>
<field name="sync_state" widget="badge"/>
</group>
</group>
<group>
<group string="Inventario en Shopify (Real Time)">
<field name="shopify_stock_info" widget="html" nolabel="1" class="text-info font-weight-bold" style="font-size: 1.1em;"/>
</group>
<group string="Inventario en Odoo (Actual)">
<field name="odoo_stock_info" widget="html" nolabel="1" class="text-success font-weight-bold" style="font-size: 1.1em;"/>
</group>
</group>
<notebook>
<page string="Historial de Sincronización">
<field name="sync_history" readonly="1" placeholder="Aquí aparecerán los éxitos y errores de sincronización..."/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Wizard View -->
<record id="view_shopify_location_wizard_form" model="ir.ui.view">
<field name="name">shopify.location.wizard.form</field>
<field name="model">shopify.location.wizard</field>
<field name="arch" type="xml">
<form string="Seleccionar Sucursal Shopify">
<p class="oe_grey">
Selecciona la sucursal de Shopify que quieres vincular con este almacén de Odoo.
</p>
<field name="config_id" invisible="1"/>
<field name="line_ids">
<tree editable="bottom" create="false" delete="false">
<field name="is_selected" widget="boolean_toggle"/>
<field name="name"/>
<field name="shopify_location_id"/>
</tree>
</field>
<footer>
<button name="action_confirm" string="Vincular Sucursal" type="object" class="btn-primary"/>
<button string="Cancelar" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>