🎉 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,2 @@
from . import models
from . import wizard
@@ -0,0 +1,20 @@
{
'name': 'Reporte de Inventario con Precio y Costo',
'version': '16.0.1.0.0',
'category': 'Inventory/Reporting',
'summary': 'Añade un reporte nativo de inventario agrupado por punto de venta con costo y precio.',
'description': """
Reporte de inventario basado en stock_quant que agrega costo estándar y precio de lista.
Accesible desde Inventario > Informes > Inventario por Sucursal.
""",
'author': 'Antigravity',
'depends': ['stock', 'product'],
'data': [
'security/ir.model.access.csv',
'wizard/inventory_history_wizard_views.xml',
'views/inventory_report_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
}
@@ -0,0 +1,2 @@
from . import inventory_report
from . import inventory_history_data
@@ -0,0 +1,17 @@
from odoo import models, fields
class HazardInventoryHistoryData(models.TransientModel):
_name = "hazard.inventory.history.data"
_description = "Datos Históricos de Inventario"
wizard_id = fields.Many2one('hazard.inventory.history.wizard', string="Wizard", ondelete='cascade')
location_id = fields.Many2one('stock.location', string='Ubicación', readonly=True)
product_id = fields.Many2one('product.product', string='Producto', readonly=True)
product_tmpl_id = fields.Many2one('product.template', string='Plantilla de Producto', readonly=True)
categ_id = fields.Many2one('product.category', string='Categoría', readonly=True)
quantity = fields.Float(string='Cantidad', readonly=True)
standard_price = fields.Float(string='Costo Unitario', readonly=True)
list_price = fields.Float(string='Precio Venta Unitario', readonly=True)
total_cost = fields.Float(string='Costo Total', readonly=True)
total_sale = fields.Float(string='Valor Venta Total', readonly=True)
@@ -0,0 +1,55 @@
from odoo import models, fields, tools
class HazardInventoryReport(models.Model):
_name = "hazard.inventory.report"
_description = "Reporte de Inventario con Precio y Costo"
_auto = False
location_id = fields.Many2one('stock.location', string='Ubicación', readonly=True)
product_id = fields.Many2one('product.product', string='Producto', readonly=True)
product_tmpl_id = fields.Many2one('product.template', string='Plantilla de Producto', readonly=True)
categ_id = fields.Many2one('product.category', string='Categoría', readonly=True)
quantity = fields.Float(string='Cantidad', readonly=True)
standard_price = fields.Float(string='Costo Unitario', readonly=True)
list_price = fields.Float(string='Precio Venta Unitario', readonly=True)
total_cost = fields.Float(string='Costo Total', readonly=True)
total_sale = fields.Float(string='Valor Venta Total', readonly=True)
def _query(self):
return """
SELECT
sq.id AS id,
sq.location_id,
sq.product_id,
pp.product_tmpl_id,
pt.categ_id,
sq.quantity,
COALESCE(
(SELECT value_float
FROM ir_property
WHERE name = 'standard_price'
AND (res_id = 'product.product,' || pp.id OR res_id = 'product.template,' || pt.id)
ORDER BY res_id DESC, id DESC LIMIT 1),
0
) AS standard_price,
COALESCE(pt.list_price, 0) AS list_price,
(sq.quantity * COALESCE(
(SELECT value_float
FROM ir_property
WHERE name = 'standard_price'
AND (res_id = 'product.product,' || pp.id OR res_id = 'product.template,' || pt.id)
ORDER BY res_id DESC, id DESC LIMIT 1),
0
)) AS total_cost,
(sq.quantity * COALESCE(pt.list_price, 0)) AS total_sale
FROM stock_quant sq
JOIN stock_location sl ON sq.location_id = sl.id
JOIN product_product pp ON sq.product_id = pp.id
JOIN product_template pt ON pp.product_tmpl_id = pt.id
WHERE sl.usage = 'internal'
AND sq.quantity != 0
"""
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute("""CREATE or REPLACE VIEW %s as (%s)""" % (self._table, self._query()))
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Search View -->
<record id="hazard_inventory_report_view_search" model="ir.ui.view">
<field name="name">hazard.inventory.report.search</field>
<field name="model">hazard.inventory.report</field>
<field name="arch" type="xml">
<search string="Búsqueda de Inventario">
<field name="location_id"/>
<field name="product_id"/>
<field name="categ_id"/>
<group expand="0" string="Agrupar por">
<filter string="Ubicación (Sucursal)" name="group_by_location" context="{'group_by':'location_id'}"/>
<filter string="Producto" name="group_by_product" context="{'group_by':'product_id'}"/>
<filter string="Categoría" name="group_by_categ" context="{'group_by':'categ_id'}"/>
</group>
</search>
</field>
</record>
<!-- Tree View -->
<record id="hazard_inventory_report_view_tree" model="ir.ui.view">
<field name="name">hazard.inventory.report.tree</field>
<field name="model">hazard.inventory.report</field>
<field name="arch" type="xml">
<tree string="Reporte de Inventario" create="false" edit="false" delete="false">
<field name="location_id"/>
<field name="product_id"/>
<field name="categ_id" optional="hide"/>
<field name="quantity" sum="Total Cantidad"/>
<field name="standard_price" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="list_price" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="total_cost" sum="Costo Total" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="total_sale" sum="Venta Total" widget="monetary" options="{'currency_field': 'currency_id'}"/>
</tree>
</field>
</record>
<!-- Pivot View -->
<record id="hazard_inventory_report_view_pivot" model="ir.ui.view">
<field name="name">hazard.inventory.report.pivot</field>
<field name="model">hazard.inventory.report</field>
<field name="arch" type="xml">
<pivot string="Reporte Dinámico de Inventario" sample="1" disable_linking="True">
<field name="location_id" type="row"/>
<field name="product_id" type="row"/>
<field name="quantity" type="measure"/>
<field name="total_cost" type="measure"/>
<field name="total_sale" type="measure"/>
</pivot>
</field>
</record>
<!-- Action -->
<record id="action_hazard_inventory_report" model="ir.actions.act_window">
<field name="name">Inventario por Sucursal (Costo y Precio)</field>
<field name="res_model">hazard.inventory.report</field>
<field name="view_mode">tree,pivot</field>
<field name="context">{'search_default_group_by_location': 1}</field>
</record>
<!-- Menu Item (Under Inventory -> Reporting) -->
<menuitem id="menu_hazard_inventory_report"
name="Inventario por Sucursal"
parent="stock.menu_warehouse_report"
action="action_hazard_inventory_report"
sequence="150"/>
</odoo>
@@ -0,0 +1 @@
from . import inventory_history_wizard
@@ -0,0 +1,82 @@
from odoo import models, fields, api, _
class HazardInventoryHistoryWizard(models.TransientModel):
_name = "hazard.inventory.history.wizard"
_description = "Asistente para Reporte Histórico de Inventario"
date = fields.Date(string="Fecha del Reporte", required=True, default=fields.Date.context_today)
def action_generate_report(self):
# 1. Clear previous data for this wizard instance just in case
self.env['hazard.inventory.history.data'].search([('wizard_id', '=', self.id)]).unlink()
# 2. Run the SQL query to get stock at date
query = """
INSERT INTO hazard_inventory_history_data (
wizard_id, location_id, product_id, product_tmpl_id, categ_id,
quantity, standard_price, list_price, total_cost, total_sale, create_uid, create_date, write_uid, write_date
)
SELECT
%s AS wizard_id,
loc.id AS location_id,
prod.id AS product_id,
tmpl.id AS product_tmpl_id,
tmpl.categ_id AS categ_id,
SUM(sml.qty) AS quantity,
COALESCE(
(SELECT value_float
FROM ir_property
WHERE name = 'standard_price'
AND (res_id = 'product.product,' || prod.id OR res_id = 'product.template,' || tmpl.id)
ORDER BY res_id DESC, id DESC LIMIT 1),
0
) AS standard_price,
COALESCE(tmpl.list_price, 0) AS list_price,
(SUM(sml.qty) * COALESCE(
(SELECT value_float
FROM ir_property
WHERE name = 'standard_price'
AND (res_id = 'product.product,' || prod.id OR res_id = 'product.template,' || tmpl.id)
ORDER BY res_id DESC, id DESC LIMIT 1),
0
)) AS total_cost,
(SUM(sml.qty) * COALESCE(tmpl.list_price, 0)) AS total_sale,
%s as create_uid,
now() at time zone 'UTC' as create_date,
%s as write_uid,
now() at time zone 'UTC' as write_date
FROM (
SELECT product_id, location_dest_id as location_id, qty_done as qty
FROM stock_move_line
WHERE state = 'done' AND date <= %s
UNION ALL
SELECT product_id, location_id as location_id, -qty_done as qty
FROM stock_move_line
WHERE state = 'done' AND date <= %s
) sml
JOIN stock_location loc ON sml.location_id = loc.id
JOIN product_product prod ON sml.product_id = prod.id
JOIN product_template tmpl ON prod.product_tmpl_id = tmpl.id
WHERE loc.usage = 'internal'
GROUP BY loc.id, prod.id, tmpl.id, tmpl.categ_id
HAVING SUM(sml.qty) != 0
"""
target_datetime = str(self.date) + ' 23:59:59'
self.env.cr.execute(query, (self.id, self.env.uid, self.env.uid, target_datetime, target_datetime))
return {
'name': _('Inventario Histórico al %s' % self.date.strftime('%d/%m/%Y')),
'type': 'ir.actions.act_window',
'res_model': 'hazard.inventory.history.data',
'view_mode': 'pivot,tree',
'domain': [('wizard_id', '=', self.id)],
'context': {
'search_default_location_id': 1, # This will group by location if we add search view, else ignored
}
}
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Wizard Form View -->
<record id="view_hazard_inventory_history_wizard" model="ir.ui.view">
<field name="name">hazard.inventory.history.wizard.form</field>
<field name="model">hazard.inventory.history.wizard</field>
<field name="arch" type="xml">
<form string="Reporte Histórico de Inventario">
<group>
<field name="date"/>
</group>
<footer>
<button name="action_generate_report" string="Generar Reporte" type="object" class="btn-primary"/>
<button string="Cancelar" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Action for the Wizard -->
<record id="action_hazard_inventory_history_wizard" model="ir.actions.act_window">
<field name="name">Inventario Histórico (A una fecha)</field>
<field name="res_model">hazard.inventory.history.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Menu Item (Under Inventory -> Reporting) -->
<menuitem id="menu_hazard_inventory_history"
name="Inventario Histórico"
parent="stock.menu_warehouse_report"
action="action_hazard_inventory_history_wizard"
sequence="155"/>
<!-- Search View for the Data Model -->
<record id="hazard_inventory_history_data_view_search" model="ir.ui.view">
<field name="name">hazard.inventory.history.data.search</field>
<field name="model">hazard.inventory.history.data</field>
<field name="arch" type="xml">
<search string="Búsqueda de Inventario Histórico">
<field name="location_id"/>
<field name="product_id"/>
<field name="categ_id"/>
<group expand="0" string="Agrupar por">
<filter string="Ubicación (Sucursal)" name="group_by_location" context="{'group_by':'location_id'}"/>
<filter string="Producto" name="group_by_product" context="{'group_by':'product_id'}"/>
<filter string="Categoría" name="group_by_categ" context="{'group_by':'categ_id'}"/>
</group>
</search>
</field>
</record>
<!-- Tree View for the Data Model -->
<record id="hazard_inventory_history_data_view_tree" model="ir.ui.view">
<field name="name">hazard.inventory.history.data.tree</field>
<field name="model">hazard.inventory.history.data</field>
<field name="arch" type="xml">
<tree string="Reporte Histórico de Inventario" create="false" edit="false" delete="false">
<field name="location_id"/>
<field name="product_id"/>
<field name="categ_id" optional="hide"/>
<field name="quantity" sum="Total Cantidad"/>
<field name="standard_price" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="list_price" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="total_cost" sum="Costo Total" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="total_sale" sum="Venta Total" widget="monetary" options="{'currency_field': 'currency_id'}"/>
</tree>
</field>
</record>
<!-- Pivot View for the Data Model -->
<record id="hazard_inventory_history_data_view_pivot" model="ir.ui.view">
<field name="name">hazard.inventory.history.data.pivot</field>
<field name="model">hazard.inventory.history.data</field>
<field name="arch" type="xml">
<pivot string="Reporte Histórico Dinámico" sample="1" disable_linking="True">
<field name="location_id" type="row"/>
<field name="product_id" type="row"/>
<field name="quantity" type="measure"/>
<field name="total_cost" type="measure"/>
<field name="total_sale" type="measure"/>
</pivot>
</field>
</record>
</odoo>