¿Prefieres no copiar y pegar? Descarga el módulo completo listo para instalar.
⬇️ Descargar software_erp.rarEsta guía contiene el código final...
Esta guía contiene el código final para el módulo de gestión de proyectos, incluyendo facturación mensual, bonos de horas, semáforos de estado y lógica de costes avanzada.
list y propiedades invisible modernas).
Configuración del módulo y carga de archivos en el orden correcto.
{
'name': "FireApp ERP",
'version': "18.0.2.0",
'summary': "Gestión Avanzada de Proyectos y Facturación",
'description': "ERP para Software Labs con gestión de Bonos y Mantenimiento.",
'author': "Aaron Gomez",
'category': "Services/Project",
'website': "pi5appserver.ddns.net",
'maintainer': "AaronSGomez",
'sequence': 1,
# Archivos Estructurales (Siempre se cargan)
'data': [
'security/ir.model.access.csv',
'views/software_views.xml',
],
# Datos de Ejemplo (Solo si 'Load Demo Data' está activo)
'demo': [
'demo/demo_data.xml',
],
'application': True,
'installable': True,
'license': "LGPL-3"
}
Incluye la lógica de Packs Iniciales, la Bolsa de Horas Global y la Calculadora Inteligente de facturas.
from odoo import models, fields, api
class SoftwareErpModule(models.Model):
_name = 'software.erp.module'
_description = 'Software ERP Module'
name = fields.Char(string="Nombre del Proyecto", required=True)
version = fields.Char(string="Versión Actual")
creation_date = fields.Date(string="Fecha Inicio", default=fields.Date.today)
screenshot = fields.Image(string="Captura", max_width=1920, max_height=1080)
description = fields.Text(string="Descripción Técnica")
# --- CONTRATO INICIAL ---
pack_type = fields.Selection([
('basic', 'Pack Básico (200€)'),
('standard', 'Pack Estándar (500€)'),
('premium', 'Pack Premium (1000€)'),
('custom', 'A Medida')
], string="Pack Inicial", default='custom')
base_price = fields.Float(string="Precio Pack (€)", default=0.0)
included_hours = fields.Float(string="Horas Pack Inicial", default=0.0)
# --- TARIFAS ---
hourly_rate = fields.Float(string="Precio Hora Extra (€)", default=50.0)
recurring_price = fields.Float(string="Cuota Mantenimiento (€)", default=18.0)
# --- BOLSA DE HORAS GLOBAL ---
total_bonus_hours = fields.Float(string="Horas Extra Compradas", compute="_compute_global_balance", store=True)
total_capacity = fields.Float(string="Capacidad Total", compute="_compute_global_balance", store=True)
total_consumed = fields.Float(string="Total Consumido", compute="_compute_global_balance", store=True)
# SEMÁFORO: Horas que quedan
hours_remaining = fields.Float(string="Horas Restantes", compute="_compute_global_balance", store=True)
project_cost = fields.Float(string="Coste Total", compute="_compute_global_balance", store=True)
# RELACIONES
commit_ids = fields.One2many('software.erp.commit', 'module_id', string="Commits")
settlement_ids = fields.One2many('software.erp.settlement', 'module_id', string="Facturación")
@api.onchange('pack_type')
def _on_pack_change(self):
if self.pack_type == 'basic':
self.base_price = 200.0; self.included_hours = 10.0; self.hourly_rate = 60.0
elif self.pack_type == 'standard':
self.base_price = 500.0; self.included_hours = 30.0; self.hourly_rate = 50.0
elif self.pack_type == 'premium':
self.base_price = 1000.0; self.included_hours = 80.0; self.hourly_rate = 40.0
@api.depends('commit_ids.hours_spent', 'base_price', 'included_hours', 'settlement_ids.new_pack_hours')
def _compute_global_balance(self):
for record in self:
consumed = sum(c.hours_spent for c in record.commit_ids)
bonuses = sum(s.new_pack_hours for s in record.settlement_ids)
capacity = record.included_hours + bonuses
record.total_consumed = consumed
record.total_bonus_hours = bonuses
record.total_capacity = capacity
record.hours_remaining = capacity - consumed
# Coste estimado (Informativo)
record.project_cost = record.base_price + max(0, consumed - capacity) * record.hourly_rate
class SoftwareSettlement(models.Model):
_name = 'software.erp.settlement'
_description = 'Liquidación Mensual'
_order = 'date_start desc'
name = fields.Char(compute="_compute_name", store=True)
module_id = fields.Many2one('software.erp.module', required=True)
date_start = fields.Date(required=True)
date_end = fields.Date(required=True)
monthly_fee = fields.Float(string="Cuota Base")
# VENTA DE BONOS
bonus_type = fields.Selection([
('none', 'Sin Bono Extra'),
('small', 'Bono 10h (150€)'),
('medium', 'Bono 20h (280€)'),
('large', 'Bono 50h (600€)'),
], default='none', string="Vender Bono")
new_pack_price = fields.Float(string="Precio Bono")
new_pack_hours = fields.Float(string="Horas Bono")
# RESULTADOS CÁLCULO
hours_month = fields.Float(readonly=True)
hours_discounted = fields.Float(readonly=True)
hours_billable = fields.Float(readonly=True)
total_amount = fields.Float(readonly=True)
state = fields.Selection([('draft', 'Borrador'), ('done', 'Cerrado')], default='draft')
@api.depends('date_start')
def _compute_name(self):
for r in self:
r.name = f"Factura {r.date_start.strftime('%m/%Y')}" if r.date_start else 'Nueva'
@api.onchange('bonus_type')
def _on_bonus_change(self):
if self.bonus_type == 'none': self.new_pack_price = 0; self.new_pack_hours = 0
elif self.bonus_type == 'small': self.new_pack_price = 150; self.new_pack_hours = 10
elif self.bonus_type == 'medium': self.new_pack_price = 280; self.new_pack_hours = 20
elif self.bonus_type == 'large': self.new_pack_price = 600; self.new_pack_hours = 50
def action_calculate(self):
for record in self:
if record.monthly_fee == 0.0: record.monthly_fee = record.module_id.recurring_price
# 1. Calcular Horas del Mes
commits_month = self.env['software.erp.commit'].search([
('module_id', '=', record.module_id.id), ('date', '>=', record.date_start),
('date', '<=', record.date_end)])
hours_this_month = sum(c.hours_spent for c in commits_month)
# 2. Calcular Bolsa Disponible (Histórico)
commits_before = self.env['software.erp.commit'].search([
('module_id', '=', record.module_id.id), ('date', '<', record.date_start)])
consumed_before = sum(c.hours_spent for c in commits_before)
other_settlements = record.module_id.settlement_ids - record
bonus_before = sum(s.new_pack_hours for s in other_settlements)
capacity_now = record.module_id.included_hours + bonus_before + record.new_pack_hours
saldo_disponible = max(0, capacity_now - consumed_before)
# 3. Aplicar descuentos
hours_covered = min(hours_this_month, saldo_disponible)
hours_to_pay = hours_this_month - hours_covered
record.hours_month = hours_this_month
record.hours_discounted = hours_covered
record.hours_billable = hours_to_pay
record.total_amount = record.monthly_fee + record.new_pack_price +
(hours_to_pay * record.module_id.hourly_rate)
record.state = 'done'
class SoftwareCommit(models.Model):
_name = 'software.erp.commit'
_description = 'Commit Diario'
name = fields.Char(required=True)
date = fields.Date(default=fields.Date.today)
category = fields.Selection([('analysis', 'Análisis'),
('development', 'Desarrollo'), ('qa', 'Pruebas'), ('maintenance', 'Mantenimiento')], required=True)
hours_spent = fields.Float()
module_id = fields.Many2one('software.erp.module', required=True)
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
Habilitamos el acceso a las 3 tablas (Module, Commit, Settlement).
Interfaz limpia con Semáforo de horas, selectores condicionales y Galería.
<odoo>
<record id="view_software_module_form" model="ir.ui.view">
<field name="name">software.erp.module.form</field>
<field name="model">software.erp.module</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<h1><field name="name" placeholder="Nombre del Proyecto"/></h1>
</div>
<group>
<group string="Contrato">
<field name="pack_type" widget="radio" options="{'horizontal': true}"/>
<field name="included_hours" widget="float_time" string="Pack Inicial"/>
<field name="total_bonus_hours" widget="float_time" string="+ Bonos Comprados"/>
<field name="total_capacity" widget="float_time" class="oe_subtotal_footer_separator"/>
</group>
<group string="Estado Bolsa">
<field name="total_consumed" widget="float_time"/>
<field name="hours_remaining" widget="float_time" decoration-success="hours_remaining > 0" decoration-danger="hours_remaining < 0" style="font-size: 1.5em; font-weight: bold;"/>
<div class="oe_inline text-muted" invisible="hours_remaining < 0">Disponibles</div>
<div class="oe_inline text-danger" invisible="hours_remaining >= 0">Excedidas</div>
</group>
</group>
<notebook>
<page string="Trabajo Diario">
<field name="commit_ids">
<list editable="bottom" decoration-info="category == 'analysis'" decoration-success="category == 'development'">
<field name="date"/>
<field name="user_id"/>
<field name="category"/>
<field name="name"/>
<field name="hours_spent" sum="Total"/>
</list>
</field>
</page>
<page string="Facturación Mensual">
<field name="settlement_ids">
<list editable="bottom">
<field name="date_start"/>
<field name="date_end"/>
<field name="monthly_fee" widget="monetary" optional="show"/>
<field name="bonus_type"/>
<field name="new_pack_price" widget="monetary" force_save="1" invisible="bonus_type == 'none'"/>
<field name="new_pack_hours" widget="float_time" force_save="1" invisible="bonus_type == 'none'" decoration-success="1"/>
<field name="hours_month" optional="show"/>
<field name="hours_discounted" optional="show" decoration-success="1"/>
<field name="hours_billable" optional="show" decoration-danger="1"/>
<field name="total_amount" widget="monetary" decoration-bf="1"/>
<field name="state" invisible="1"/>
<button name="action_calculate" type="object" icon="fa-calculator"/>
</list>
</field>
</page>
<page string="Galería">
<group><field name="screenshot" widget="image" options='{"zoom": true, "size": [0, 0]}' class="oe_avatar_big"/></group>
</page>
<page string="Config">
<group><field name="hourly_rate"/><field name="recurring_price"/></group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_software_module_tree" model="ir.ui.view">
<field name="name">software.erp.module.list</field>
<field name="model">software.erp.module</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="pack_type"/>
<field name="hours_remaining" widget="float_time" decoration-danger="hours_remaining < 0"/>
<field name="project_cost" widget="monetary"/>
</list>
</field>
</record>
<record id="action_software_module" model="ir.actions.act_window">
<field name="name">Gestor de Proyectos</field>
<field name="res_model">software.erp.module</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_software_root" name="Software ERP" sequence="10"/>
<menuitem id="menu_software_main" parent="menu_software_root" name="Desarrollo" sequence="1"/>
<menuitem id="menu_software_module" parent="menu_software_main" name="Proyectos" action="action_software_module"/>
</odoo>
Datos precargados con historial de 2025 para probar la calculadora.
<odoo>
<data noupdate="1">
<!-- PROYECTO 1: BARATO -->
<record id="demo_proj_web" model="software.erp.module">
<field name="name">Web Corporativa 2025</field>
<field name="pack_type">basic</field>
<field name="recurring_price">15.00</field>
<field name="hourly_rate">40.00</field>
</record>
<!-- Factura con Bono -->
<record id="settlement_web_feb" model="software.erp.settlement">
<field name="module_id" ref="demo_proj_web"/>
<field name="date_start">2025-02-01</field>
<field name="date_end">2025-02-28</field>
<field name="monthly_fee">15.00</field>
<field name="bonus_type">small</field>
<field name="new_pack_price">150.00</field>
<field name="new_pack_hours">10.00</field>
<field name="state">done</field>
<field name="total_amount">165.00</field>
</record>
<!-- PROYECTO 2: PREMIUM -->
<record id="demo_proj_erp" model="software.erp.module">
<field name="name">Intranet RRHH</field>
<field name="pack_type">custom</field>
<field name="base_price">2000.00</field>
<field name="included_hours">100.00</field>
<field name="recurring_price">32.00</field>
<field name="hourly_rate">60.00</field>
</record>
<!-- Historial de commits -->
<record id="commit_erp_1" model="software.erp.commit">
<field name="name">Desarrollo Backend</field>
<field name="module_id" ref="demo_proj_erp"/>
<field name="date">2025-05-12</field>
<field name="hours_spent">30.0</field>
<field name="category">development</field>
</record>
</data>
</odoo>
Usa esta secuencia de comandos cada vez que modifiques código Python o XML y necesites aplicar los cambios.