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