Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
This commit is contained in:
1
odoo-addons/skeen_monedero/__init__.py
Normal file
1
odoo-addons/skeen_monedero/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
23
odoo-addons/skeen_monedero/__manifest__.py
Normal file
23
odoo-addons/skeen_monedero/__manifest__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'SKEEN Monedero',
|
||||
'version': '1.0.0',
|
||||
'category': 'Healthcare',
|
||||
'summary': 'Monedero digital de fidelidad para SKEEN Derma Experts',
|
||||
'description': """
|
||||
Sistema de puntos de fidelidad para pacientes.
|
||||
1 punto = $1 MXN. Acumulación por compras, redención por descuentos.
|
||||
""",
|
||||
'author': 'Consultoria Alcaraz Salazar, S.A.S.',
|
||||
'website': 'https://skeen.mx',
|
||||
'depends': ['base', 'skeen_pacientes'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/monedero_views.xml',
|
||||
'views/menu_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
4
odoo-addons/skeen_monedero/models/__init__.py
Normal file
4
odoo-addons/skeen_monedero/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import monedero
|
||||
from . import res_partner
|
||||
131
odoo-addons/skeen_monedero/models/monedero.py
Normal file
131
odoo-addons/skeen_monedero/models/monedero.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SkeenMonedero(models.Model):
|
||||
_name = 'skeen.monedero'
|
||||
_description = 'Monedero Digital de Fidelidad'
|
||||
_order = 'create_date desc'
|
||||
|
||||
# Paciente
|
||||
partner_id = fields.Many2one('res.partner', string='Paciente', required=True,
|
||||
domain=[('is_patient', '=', True)])
|
||||
partner_phone = fields.Char(related='partner_id.phone', string='Teléfono', readonly=True)
|
||||
|
||||
# Puntos
|
||||
points = fields.Integer(string='Puntos', default=0)
|
||||
equivalent_mxn = fields.Float(string='Equivalente MXN', compute='_compute_equivalent')
|
||||
|
||||
# Historial de transacciones
|
||||
transaction_ids = fields.One2many('skeen.monedero.transaction', 'monedero_id', string='Transacciones')
|
||||
|
||||
# Estado
|
||||
active = fields.Boolean(string='Activo', default=True)
|
||||
|
||||
@api.depends('points')
|
||||
def _compute_equivalent(self):
|
||||
for monedero in self:
|
||||
monedero.equivalent_mxn = monedero.points * 1.0 # 1 punto = $1 MXN
|
||||
|
||||
def get_balance(self, phone):
|
||||
"""Obtener saldo por teléfono (para API)"""
|
||||
partner = self.env['res.partner'].search([('phone', '=', phone)], limit=1)
|
||||
if not partner:
|
||||
return {'points': 0, 'equivalent_mxn': 0, 'history': []}
|
||||
|
||||
monedero = self.search([('partner_id', '=', partner.id)], limit=1)
|
||||
if not monedero:
|
||||
return {'points': 0, 'equivalent_mxn': 0, 'history': []}
|
||||
|
||||
history = []
|
||||
for tx in monedero.transaction_ids:
|
||||
history.append({
|
||||
'date': tx.create_date.strftime('%d/%m/%Y'),
|
||||
'type': tx.type,
|
||||
'points': tx.points,
|
||||
'description': tx.description,
|
||||
})
|
||||
|
||||
return {
|
||||
'points': monedero.points,
|
||||
'equivalent_mxn': monedero.equivalent_mxn,
|
||||
'history': history,
|
||||
}
|
||||
|
||||
def add_points(self, points, description='', reference=''):
|
||||
"""Agregar puntos al monedero"""
|
||||
self.ensure_one()
|
||||
self.write({'points': self.points + points})
|
||||
self.env['skeen.monedero.transaction'].create({
|
||||
'monedero_id': self.id,
|
||||
'type': 'accrual',
|
||||
'points': points,
|
||||
'description': description or 'Acumulación de puntos',
|
||||
'reference': reference,
|
||||
})
|
||||
return True
|
||||
|
||||
def redeem_points(self, points, description='', reference=''):
|
||||
"""Redimir puntos del monedero"""
|
||||
self.ensure_one()
|
||||
if points > self.points:
|
||||
raise ValidationError(_('No tiene suficientes puntos!'))
|
||||
|
||||
self.write({'points': self.points - points})
|
||||
self.env['skeen.monedero.transaction'].create({
|
||||
'monedero_id': self.id,
|
||||
'type': 'redemption',
|
||||
'points': -points,
|
||||
'description': description or 'Redención de puntos',
|
||||
'reference': reference,
|
||||
})
|
||||
return True
|
||||
|
||||
def adjust_points(self, delta, description='', reference=''):
|
||||
"""Ajuste de puntos (puede ser negativo). Hace piso en 0 para no dejar saldo negativo.
|
||||
|
||||
Usado por devoluciones para revertir puntos acumulados por una venta.
|
||||
Devuelve el delta efectivamente aplicado (puede ser menor si se pisó en 0).
|
||||
"""
|
||||
self.ensure_one()
|
||||
delta = int(delta or 0)
|
||||
new_points = self.points + delta
|
||||
effective_delta = delta
|
||||
if new_points < 0:
|
||||
effective_delta = -self.points # solo revertir hasta 0
|
||||
new_points = 0
|
||||
self.write({'points': new_points})
|
||||
self.env['skeen.monedero.transaction'].create({
|
||||
'monedero_id': self.id,
|
||||
'type': 'adjustment',
|
||||
'points': effective_delta,
|
||||
'description': description or 'Ajuste de puntos',
|
||||
'reference': reference,
|
||||
})
|
||||
return effective_delta
|
||||
|
||||
|
||||
class SkeenMonederoTransaction(models.Model):
|
||||
_name = 'skeen.monedero.transaction'
|
||||
_description = 'Transacción de Monedero'
|
||||
_order = 'create_date desc'
|
||||
|
||||
monedero_id = fields.Many2one('skeen.monedero', string='Monedero', required=True, ondelete='cascade')
|
||||
partner_id = fields.Many2one(related='monedero_id.partner_id', string='Paciente', readonly=True)
|
||||
|
||||
# Transacción
|
||||
type = fields.Selection([
|
||||
('accrual', 'Acumulación'),
|
||||
('redemption', 'Redención'),
|
||||
('adjustment', 'Ajuste'),
|
||||
('expiration', 'Vencimiento'),
|
||||
], string='Tipo', required=True)
|
||||
|
||||
points = fields.Integer(string='Puntos', required=True)
|
||||
description = fields.Text(string='Descripción')
|
||||
reference = fields.Char(string='Referencia')
|
||||
|
||||
# Fecha
|
||||
create_date = fields.Datetime(string='Fecha', readonly=True, default=fields.Datetime.now)
|
||||
17
odoo-addons/skeen_monedero/models/res_partner.py
Normal file
17
odoo-addons/skeen_monedero/models/res_partner.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
# Relación inversa con monedero
|
||||
monedero_ids = fields.One2many('skeen.monedero', 'partner_id', string='Monederos')
|
||||
wallet_points = fields.Integer(string='Puntos Monedero', compute='_compute_wallet_points', store=True)
|
||||
|
||||
@api.depends('monedero_ids.points')
|
||||
def _compute_wallet_points(self):
|
||||
for partner in self:
|
||||
monedero = partner.monedero_ids[:1]
|
||||
partner.wallet_points = monedero.points if monedero else 0
|
||||
3
odoo-addons/skeen_monedero/security/ir.model.access.csv
Normal file
3
odoo-addons/skeen_monedero/security/ir.model.access.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_skeen_monedero_user,Acceso a Monedero SKEEN,model_skeen_monedero,base.group_user,1,1,1,1
|
||||
access_skeen_monedero_transaction_user,Acceso a Transacciones Monedero SKEEN,model_skeen_monedero_transaction,base.group_user,1,1,1,1
|
||||
|
8
odoo-addons/skeen_monedero/views/menu_views.xml
Normal file
8
odoo-addons/skeen_monedero/views/menu_views.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Menú Monedero -->
|
||||
<menuitem id="menu_skeen_monedero" name="Monedero" parent="skeen_citas.menu_skeen_root" sequence="20"/>
|
||||
|
||||
<menuitem id="menu_skeen_monedero_list" name="Monederos" parent="menu_skeen_monedero"
|
||||
action="action_skeen_monedero_tree" sequence="10"/>
|
||||
</odoo>
|
||||
56
odoo-addons/skeen_monedero/views/monedero_views.xml
Normal file
56
odoo-addons/skeen_monedero/views/monedero_views.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Vista de lista de monederos -->
|
||||
<record id="view_skeen_monedero_tree" model="ir.ui.view">
|
||||
<field name="name">skeen.monedero.tree</field>
|
||||
<field name="model">skeen.monedero</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_phone"/>
|
||||
<field name="points"/>
|
||||
<field name="equivalent_mxn"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista de formulario de monedero -->
|
||||
<record id="view_skeen_monedero_form" model="ir.ui.view">
|
||||
<field name="name">skeen.monedero.form</field>
|
||||
<field name="model">skeen.monedero</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_phone" readonly="1"/>
|
||||
<field name="points"/>
|
||||
<field name="equivalent_mxn" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="transaction_ids">
|
||||
<tree>
|
||||
<field name="create_date"/>
|
||||
<field name="type"/>
|
||||
<field name="points"/>
|
||||
<field name="description"/>
|
||||
<field name="reference"/>
|
||||
</tree>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Acción para monederos -->
|
||||
<record id="action_skeen_monedero_tree" model="ir.actions.act_window">
|
||||
<field name="name">Monederos</field>
|
||||
<field name="res_model">skeen.monedero</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user