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:
197
odoo-addons/skeen_pagos/models/pago.py
Normal file
197
odoo-addons/skeen_pagos/models/pago.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
import requests
|
||||
import json
|
||||
|
||||
|
||||
class SkeenPago(models.Model):
|
||||
_name = 'skeen.pago'
|
||||
_description = 'Pago de Servicio'
|
||||
_order = 'create_date desc'
|
||||
|
||||
# Referencia
|
||||
name = fields.Char(string='Referencia', readonly=True, copy=False)
|
||||
|
||||
# Paciente y Cita
|
||||
partner_id = fields.Many2one('res.partner', string='Paciente', required=True)
|
||||
cita_id = fields.Many2one('skeen.cita', string='Cita')
|
||||
|
||||
# Monto
|
||||
amount = fields.Float(string='Monto', required=True)
|
||||
currency_id = fields.Many2one('res.currency', string='Moneda', default=lambda self: self.env.ref('base.MXN'))
|
||||
|
||||
# Método de pago
|
||||
payment_method = fields.Selection([
|
||||
('stripe', 'Stripe'),
|
||||
('mercadopago', 'MercadoPago'),
|
||||
('cash', 'Efectivo'),
|
||||
('card', 'Tarjeta'),
|
||||
('transfer', 'Transferencia'),
|
||||
], string='Método de Pago', required=True)
|
||||
|
||||
# Estado
|
||||
state = fields.Selection([
|
||||
('pending', 'Pendiente'),
|
||||
('processing', 'Procesando'),
|
||||
('completed', 'Completado'),
|
||||
('failed', 'Fallido'),
|
||||
('refunded', 'Reembolsado'),
|
||||
], string='Estado', default='pending')
|
||||
|
||||
# Datos del proveedor de pago
|
||||
provider_reference = fields.Char(string='Referencia Proveedor')
|
||||
provider_response = fields.Text(string='Respuesta Proveedor')
|
||||
|
||||
# URLs
|
||||
payment_url = fields.Char(string='URL de Pago')
|
||||
success_url = fields.Char(string='URL de Éxito')
|
||||
cancel_url = fields.Char(string='URL de Cancelación')
|
||||
|
||||
# Fechas
|
||||
create_date = fields.Datetime(string='Fecha Creación', readonly=True, default=fields.Datetime.now)
|
||||
payment_date = fields.Datetime(string='Fecha de Pago')
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('name'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('skeen.pago') or 'PAGO-001'
|
||||
return super(SkeenPago, self).create(vals_list)
|
||||
|
||||
def action_generate_stripe_link(self):
|
||||
"""Generar link de pago Stripe"""
|
||||
self.ensure_one()
|
||||
stripe_key = self.env['ir.config_parameter'].sudo().get_param('skeen.stripe_secret_key')
|
||||
if not stripe_key:
|
||||
raise ValidationError(_('Stripe no está configurado!'))
|
||||
|
||||
# Crear sesión de checkout
|
||||
headers = {
|
||||
'Authorization': f'Bearer {stripe_key}',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
data = {
|
||||
'payment_method_types[]': 'card',
|
||||
'line_items[0][price_data][currency]': 'mxn',
|
||||
'line_items[0][price_data][product_data][name]': f'Servicio SKEEN - {self.cita_id.servicio_id.name}',
|
||||
'line_items[0][price_data][unit_amount]': int(self.amount * 100),
|
||||
'line_items[0][quantity]': 1,
|
||||
'mode': 'payment',
|
||||
'success_url': f'https://app.skeen.mx/pagos/exitoso?ref={self.name}',
|
||||
'cancel_url': f'https://app.skeen.mx/pagos/cancelado?ref={self.name}',
|
||||
'metadata[payment_id]': str(self.id),
|
||||
'metadata[patient_phone]': self.partner_id.phone or '',
|
||||
}
|
||||
|
||||
response = requests.post('https://api.stripe.com/v1/checkout/sessions', headers=headers, data=data)
|
||||
if response.status_code == 200:
|
||||
session = response.json()
|
||||
self.write({
|
||||
'payment_url': session['url'],
|
||||
'provider_reference': session['id'],
|
||||
'provider_response': json.dumps(session),
|
||||
'state': 'processing',
|
||||
})
|
||||
else:
|
||||
self.write({
|
||||
'provider_response': response.text,
|
||||
'state': 'failed',
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
def action_generate_mercadopago_link(self):
|
||||
"""Generar link de pago MercadoPago"""
|
||||
self.ensure_one()
|
||||
mp_token = self.env['ir.config_parameter'].sudo().get_param('skeen.mercadopago_access_token')
|
||||
if not mp_token:
|
||||
raise ValidationError(_('MercadoPago no está configurado!'))
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {mp_token}',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
data = {
|
||||
'items': [{
|
||||
'title': f'Servicio SKEEN - {self.cita_id.servicio_id.name}',
|
||||
'quantity': 1,
|
||||
'unit_price': int(self.amount),
|
||||
}],
|
||||
'back_urls': {
|
||||
'success': f'https://app.skeen.mx/pagos/exitoso?ref={self.name}',
|
||||
'failure': f'https://app.skeen.mx/pagos/fallido?ref={self.name}',
|
||||
'pending': f'https://app.skeen.mx/pagos/pendiente?ref={self.name}',
|
||||
},
|
||||
'notification_url': 'https://skeen.mx/webhooks/mercadopago',
|
||||
'metadata': {
|
||||
'payment_id': str(self.id),
|
||||
'patient_phone': self.partner_id.phone or '',
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post('https://api.mercadopago.com/checkout/preferences', headers=headers, json=data)
|
||||
if response.status_code == 201:
|
||||
preference = response.json()
|
||||
self.write({
|
||||
'payment_url': preference['init_point'],
|
||||
'provider_reference': preference['id'],
|
||||
'provider_response': json.dumps(preference),
|
||||
'state': 'processing',
|
||||
})
|
||||
else:
|
||||
self.write({
|
||||
'provider_response': response.text,
|
||||
'state': 'failed',
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
def action_confirm_payment(self):
|
||||
"""Confirmar pago recibido"""
|
||||
self.ensure_one()
|
||||
self.write({
|
||||
'state': 'completed',
|
||||
'payment_date': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
# Actualizar estado de pago de la cita
|
||||
if self.cita_id:
|
||||
self.cita_id.write({
|
||||
'payment_state': 'paid',
|
||||
'amount_paid': self.amount,
|
||||
})
|
||||
|
||||
# Acumular puntos en monedero
|
||||
monedero = self.env['skeen.monedero'].search([('partner_id', '=', self.partner_id.id)], limit=1)
|
||||
if not monedero:
|
||||
monedero = self.env['skeen.monedero'].create({
|
||||
'partner_id': self.partner_id.id,
|
||||
})
|
||||
|
||||
points = int(self.amount / 10) # 1 punto por cada $10
|
||||
monedero.add_points(
|
||||
points=points,
|
||||
description=f'Pago de servicio - {self.cita_id.servicio_id.name if self.cita_id else "N/A"}',
|
||||
reference=self.name,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_refund(self):
|
||||
"""Procesar reembolso"""
|
||||
self.ensure_one()
|
||||
self.write({'state': 'refunded'})
|
||||
|
||||
# Restar puntos del monedero
|
||||
monedero = self.env['skeen.monedero'].search([('partner_id', '=', self.partner_id.id)], limit=1)
|
||||
if monedero:
|
||||
points = int(self.amount / 10)
|
||||
monedero.redeem_points(
|
||||
points=points,
|
||||
description=f'Reembolso - {self.name}',
|
||||
reference=self.name,
|
||||
)
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user