🎉 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:
@@ -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>
|
||||
Reference in New Issue
Block a user