Guía de Desarrollo: Módulo "Software ERP" 3.0

¿Prefieres no copiar y pegar? Descarga el módulo completo listo para instalar.

⬇️ Descargar software_erp.rar
Archivo comprimido (RAR) - Incluye Manifest, Modelos, Vistas y Demo

Esta 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.

Compatibilidad: Odoo 18.0 (Uso de etiqueta list y propiedades invisible modernas).

1. El Manifiesto (__manifest__.py)

Configuración del módulo y carga de archivos en el orden correcto.

software_erp/__manifest__.py
{
    '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"
}
        

2. Los Modelos (Lógica Python)

Incluye la lógica de Packs Iniciales, la Bolsa de Horas Global y la Calculadora Inteligente de facturas.

software_erp/models/software_module.py
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)
        

3. Seguridad (Permisos)

Habilitamos el acceso a las 3 tablas (Module, Commit, Settlement).

software_erp/security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_software_module,access_software_module,model_software_erp_module,base.group_user,1,1,1,1 access_software_commit,access_software_commit,model_software_erp_commit,base.group_user,1,1,1,1 access_software_settlement,access_software_settlement,model_software_erp_settlement,base.group_user,1,1,1,1

4. Vistas (XML Odoo 18)

Interfaz limpia con Semáforo de horas, selectores condicionales y Galería.

software_erp/views/software_views.xml
<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>
        

5. Datos de Demostración

Datos precargados con historial de 2025 para probar la calculadora.

software_erp/demo/demo_data.xml
<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>
        

GUIA: Ciclo de Vida (Parar, Actualizar y Arrancar)

Usa esta secuencia de comandos cada vez que modifiques código Python o XML y necesites aplicar los cambios.

# 1. DETENER EL SERVICIO (Obligatorio para liberar el puerto) sudo systemctl stop odoo18.service

# 2. ENTRAR AL DIRECTORIO Y ACTIVAR ENTORNO VIRTUAL cd /opt/odoo18/odoo source /opt/odoo18/dev-venv/bin/activate

# 3. EJECUTAR ACTUALIZACIÓN DEL MÓDULO (Modo Pruebas) # Esto lanza Odoo en la terminal. Si ves errores rojos, revísalos. python3 odoo-bin -c debian/odoo.conf -u software_erp -d odoo18

# --> Abre el navegador y prueba tus cambios. # --> Cuando termines, pulsa CTRL + C para detener.

# 4. REACTIVAR EL SERVICIO (Modo Producción) # Una vez verificado que el módulo funciona bien, levanta el servicio de fondo: sudo systemctl start odoo18.service # (Opcional) Comprobar que arrancó bien: sudo systemctl status odoo18.service

← Volver a la Portada