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:
2
odoo-addons/skeen_ventas/__init__.py
Normal file
2
odoo-addons/skeen_ventas/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
25
odoo-addons/skeen_ventas/__manifest__.py
Normal file
25
odoo-addons/skeen_ventas/__manifest__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'SKEEN Ventas y Cortes de Caja',
|
||||
'version': '1.0.0',
|
||||
'category': 'Healthcare',
|
||||
'summary': 'Ventas, adeudos y cortes de caja para SKEEN Derma Experts',
|
||||
'description': """
|
||||
Módulo de ventas y cortes de caja para clínica dermatológica.
|
||||
Gestiona órdenes de venta, adeudos y cierres de caja diarios.
|
||||
""",
|
||||
'author': 'Consultoria Alcaraz Salazar, S.A.S.',
|
||||
'website': 'https://skeen.mx',
|
||||
'depends': ['base', 'skeen_citas', 'skeen_pagos', 'skeen_monedero'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/sequences.xml',
|
||||
'views/venta_views.xml',
|
||||
'views/corte_views.xml',
|
||||
'views/menu_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
22
odoo-addons/skeen_ventas/data/sequences.xml
Normal file
22
odoo-addons/skeen_ventas/data/sequences.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="seq_skeen_venta" model="ir.sequence">
|
||||
<field name="name">SKEEN Venta</field>
|
||||
<field name="code">skeen.venta</field>
|
||||
<field name="prefix">VENTA-</field>
|
||||
<field name="padding">4</field>
|
||||
<field name="number_next">1</field>
|
||||
<field name="number_increment">1</field>
|
||||
</record>
|
||||
|
||||
<record id="seq_skeen_corte_caja" model="ir.sequence">
|
||||
<field name="name">SKEEN Corte de Caja</field>
|
||||
<field name="code">skeen.corte.caja</field>
|
||||
<field name="prefix">CORTE-</field>
|
||||
<field name="padding">4</field>
|
||||
<field name="number_next">1</field>
|
||||
<field name="number_increment">1</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
3
odoo-addons/skeen_ventas/models/__init__.py
Normal file
3
odoo-addons/skeen_ventas/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import venta
|
||||
from . import tipo_cambio
|
||||
25
odoo-addons/skeen_ventas/models/tipo_cambio.py
Normal file
25
odoo-addons/skeen_ventas/models/tipo_cambio.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class SkeenTipoCambio(models.Model):
|
||||
_name = 'skeen.tipo.cambio'
|
||||
_description = 'Tipo de Cambio USD/MXN'
|
||||
_order = 'date desc, id desc'
|
||||
_rec_name = 'rate'
|
||||
|
||||
date = fields.Date(string='Fecha', required=True, default=fields.Date.today, index=True)
|
||||
rate = fields.Float(string='Tipo de Cambio (MXN por 1 USD)', required=True, digits=(12, 4))
|
||||
source = fields.Char(string='Fuente', default='manual', help='manual, banxico, dof, etc.')
|
||||
notes = fields.Char(string='Notas')
|
||||
|
||||
_sql_constraints = [
|
||||
('date_unique', 'unique(date)', 'Ya existe un tipo de cambio registrado para esa fecha.'),
|
||||
('rate_positive', 'CHECK(rate > 0)', 'El tipo de cambio debe ser mayor a cero.'),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def get_current_rate(self):
|
||||
rec = self.search([], order='date desc, id desc', limit=1)
|
||||
return rec.rate if rec else 0.0
|
||||
290
odoo-addons/skeen_ventas/models/venta.py
Normal file
290
odoo-addons/skeen_ventas/models/venta.py
Normal file
@@ -0,0 +1,290 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SkeenVenta(models.Model):
|
||||
_name = 'skeen.venta'
|
||||
_description = 'Venta SKEEN'
|
||||
_order = 'create_date desc'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
# Referencia
|
||||
name = fields.Char(string='Referencia', readonly=True, copy=False)
|
||||
|
||||
# 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)
|
||||
|
||||
# Origen
|
||||
cita_id = fields.Many2one('skeen.cita', string='Cita Origen')
|
||||
|
||||
# Líneas de venta
|
||||
line_ids = fields.One2many('skeen.venta.line', 'venta_id', string='Líneas')
|
||||
|
||||
# Totales
|
||||
subtotal = fields.Float(string='Subtotal', compute='_compute_totals', store=True)
|
||||
discount = fields.Float(string='Descuento', default=0.0)
|
||||
tax = fields.Float(string='Impuesto', default=0.0)
|
||||
total = fields.Float(string='Total', compute='_compute_totals', store=True)
|
||||
amount_paid = fields.Float(string='Pagado', default=0.0)
|
||||
amount_due = fields.Float(string='Adeudo', compute='_compute_totals', store=True)
|
||||
|
||||
# Estado
|
||||
state = fields.Selection([
|
||||
('draft', 'Borrador'),
|
||||
('confirmed', 'Confirmada'),
|
||||
('paid', 'Pagada'),
|
||||
('partial', 'Parcial'),
|
||||
('cancelled', 'Cancelada'),
|
||||
], string='Estado', default='draft', tracking=True)
|
||||
|
||||
# Monedero
|
||||
points_redeemed = fields.Integer(string='Puntos Redimidos', default=0)
|
||||
points_discount = fields.Float(string='Descuento por Puntos', default=0.0)
|
||||
|
||||
# Devolución
|
||||
refunded = fields.Boolean(string='Devuelta', default=False)
|
||||
refund_amount = fields.Float(string='Monto Devuelto', default=0.0)
|
||||
refund_reason = fields.Text(string='Motivo Devolución')
|
||||
refunded_at = fields.Datetime(string='Fecha Devolución')
|
||||
|
||||
# Fechas
|
||||
date = fields.Date(string='Fecha', required=True, default=fields.Date.today)
|
||||
create_date = fields.Datetime(string='Fecha Creación', readonly=True, default=fields.Datetime.now)
|
||||
|
||||
# Notas
|
||||
notes = fields.Text(string='Notas')
|
||||
|
||||
@api.depends('line_ids.subtotal', 'discount', 'tax', 'amount_paid', 'points_discount')
|
||||
def _compute_totals(self):
|
||||
for venta in self:
|
||||
subtotal = sum(line.subtotal for line in venta.line_ids)
|
||||
venta.subtotal = subtotal
|
||||
venta.total = max(0.0, subtotal - venta.discount - venta.points_discount + venta.tax)
|
||||
venta.amount_due = max(0.0, venta.total - venta.amount_paid)
|
||||
|
||||
@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.venta') or 'VENTA-001'
|
||||
return super(SkeenVenta, self).create(vals_list)
|
||||
|
||||
def action_confirm(self):
|
||||
self.write({'state': 'confirmed'})
|
||||
|
||||
def action_mark_paid(self):
|
||||
for venta in self:
|
||||
venta.write({
|
||||
'state': 'paid',
|
||||
'amount_paid': venta.total,
|
||||
})
|
||||
if venta.cita_id:
|
||||
venta.cita_id.write({
|
||||
'payment_state': 'paid',
|
||||
'amount_paid': venta.total,
|
||||
})
|
||||
# Acumular puntos
|
||||
monedero = self.env['skeen.monedero'].search([('partner_id', '=', venta.partner_id.id)], limit=1)
|
||||
if not monedero:
|
||||
monedero = self.env['skeen.monedero'].create({'partner_id': venta.partner_id.id})
|
||||
points = int(venta.total / 10)
|
||||
if points > 0:
|
||||
monedero.add_points(points, description=f'Venta {venta.name}', reference=venta.name)
|
||||
|
||||
def action_cancel(self):
|
||||
self.write({'state': 'cancelled'})
|
||||
|
||||
def action_refund(self, amount=None, reason=''):
|
||||
"""Devolución total o parcial de una venta. Reversible: no borra nada.
|
||||
|
||||
- Reduce amount_paid en `amount` (o en todo lo pagado si amount es None).
|
||||
- Revierte puntos del monedero en proporción ($10 = 1 pt), con piso en 0.
|
||||
- Estado resultante: cancelled si no queda pagado; partial si queda parcial; paid si seguía cubierto.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.state not in ('confirmed', 'paid', 'partial'):
|
||||
raise ValidationError(_('Solo se pueden devolver ventas confirmadas o pagadas'))
|
||||
if self.amount_paid <= 0:
|
||||
raise ValidationError(_('La venta no tiene monto pagado para devolver'))
|
||||
|
||||
refund_amount = float(amount or 0)
|
||||
if refund_amount <= 0:
|
||||
refund_amount = self.amount_paid # devolución total de lo pagado
|
||||
refund_amount = min(refund_amount, self.amount_paid)
|
||||
if refund_amount <= 0:
|
||||
raise ValidationError(_('Monto de devolución inválido'))
|
||||
|
||||
new_amount_paid = round(self.amount_paid - refund_amount, 2)
|
||||
|
||||
# Revertir puntos acumulados en proporción a lo devuelto ($10 MXN = 1 punto)
|
||||
points_to_reverse = int(refund_amount / 10)
|
||||
if points_to_reverse > 0:
|
||||
monedero = self.env['skeen.monedero'].search([('partner_id', '=', self.partner_id.id)], limit=1)
|
||||
if monedero and monedero.points > 0:
|
||||
monedero.adjust_points(
|
||||
-points_to_reverse,
|
||||
description=f'Devolución venta {self.name}',
|
||||
reference=self.name,
|
||||
)
|
||||
|
||||
# Estado resultante
|
||||
if new_amount_paid <= 0:
|
||||
new_state = 'cancelled'
|
||||
elif new_amount_paid < self.total:
|
||||
new_state = 'partial'
|
||||
else:
|
||||
new_state = 'paid'
|
||||
|
||||
self.write({
|
||||
'amount_paid': new_amount_paid,
|
||||
'state': new_state,
|
||||
'refunded': True,
|
||||
'refund_amount': self.refund_amount + refund_amount,
|
||||
'refund_reason': reason or self.refund_reason or '',
|
||||
'refunded_at': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
# Reflejar en la cita origen si existe
|
||||
if self.cita_id:
|
||||
if new_amount_paid <= 0:
|
||||
cita_state = 'not_paid'
|
||||
elif new_amount_paid < self.total:
|
||||
cita_state = 'partial'
|
||||
else:
|
||||
cita_state = 'paid'
|
||||
self.cita_id.write({'amount_paid': new_amount_paid, 'payment_state': cita_state})
|
||||
|
||||
return {
|
||||
'refund_amount': refund_amount,
|
||||
'points_reversed': points_to_reverse,
|
||||
'new_state': new_state,
|
||||
'new_amount_paid': new_amount_paid,
|
||||
}
|
||||
|
||||
def action_apply_wallet(self, points):
|
||||
"""Redime puntos del monedero como descuento en la venta"""
|
||||
self.ensure_one()
|
||||
monedero = self.env['skeen.monedero'].search([('partner_id', '=', self.partner_id.id)], limit=1)
|
||||
if not monedero or monedero.points < points:
|
||||
raise ValidationError(_('Puntos insuficientes'))
|
||||
|
||||
# Máximo 50% del total
|
||||
max_discount = self.total * 0.5
|
||||
point_value = min(points, int(max_discount))
|
||||
|
||||
monedero.redeem_points(point_value, description=f'Redención venta {self.name}', reference=self.name)
|
||||
self.write({
|
||||
'points_redeemed': point_value,
|
||||
'points_discount': point_value * 1.0,
|
||||
})
|
||||
return True
|
||||
|
||||
|
||||
class SkeenVentaLine(models.Model):
|
||||
_name = 'skeen.venta.line'
|
||||
_description = 'Línea de Venta SKEEN'
|
||||
|
||||
venta_id = fields.Many2one('skeen.venta', string='Venta', required=True, ondelete='cascade')
|
||||
service_id = fields.Many2one('skeen.servicio', string='Servicio', required=True)
|
||||
description = fields.Char(string='Descripción')
|
||||
quantity = fields.Float(string='Cantidad', default=1.0, required=True)
|
||||
price_unit = fields.Float(string='Precio Unitario', required=True)
|
||||
subtotal = fields.Float(string='Subtotal', compute='_compute_subtotal', store=True)
|
||||
# Artículo recetado por médico (base para comisiones)
|
||||
is_prescribed = fields.Boolean(string='Recetado', default=False,
|
||||
help='Marcar si este artículo fue recetado por un médico (entra al cálculo de comisiones).')
|
||||
prescribed_by_id = fields.Many2one('hr.employee', string='Recetado por')
|
||||
|
||||
@api.depends('quantity', 'price_unit')
|
||||
def _compute_subtotal(self):
|
||||
for line in self:
|
||||
line.subtotal = line.quantity * line.price_unit
|
||||
|
||||
@api.onchange('service_id')
|
||||
def _onchange_service(self):
|
||||
if self.service_id:
|
||||
self.price_unit = self.service_id.price
|
||||
self.description = self.service_id.name
|
||||
|
||||
|
||||
class SkeenCorteCaja(models.Model):
|
||||
_name = 'skeen.corte.caja'
|
||||
_description = 'Corte de Caja SKEEN'
|
||||
_order = 'date desc'
|
||||
|
||||
name = fields.Char(string='Referencia', readonly=True, copy=False)
|
||||
date = fields.Date(string='Fecha', required=True, default=fields.Date.today, index=True)
|
||||
user_id = fields.Many2one('res.users', string='Usuario', default=lambda self: self.env.uid)
|
||||
|
||||
# Apertura
|
||||
opening_cash = fields.Float(string='Efectivo Apertura', default=0.0)
|
||||
|
||||
# Ventas del día
|
||||
venta_ids = fields.One2many('skeen.venta', 'date', string='Ventas del Día',
|
||||
domain=[('state', 'in', ('confirmed', 'paid', 'partial'))])
|
||||
|
||||
# Totales por método de pago
|
||||
total_cash = fields.Float(string='Efectivo', compute='_compute_totals', store=True)
|
||||
total_card = fields.Float(string='Tarjeta', compute='_compute_totals', store=True)
|
||||
total_transfer = fields.Float(string='Transferencia', compute='_compute_totals', store=True)
|
||||
total_other = fields.Float(string='Otros', compute='_compute_totals', store=True)
|
||||
total_sales = fields.Float(string='Total Ventas', compute='_compute_totals', store=True)
|
||||
|
||||
# Cierre
|
||||
closing_cash = fields.Float(string='Efectivo Cierre', default=0.0)
|
||||
difference = fields.Float(string='Diferencia', compute='_compute_difference', store=True)
|
||||
|
||||
# Estado
|
||||
state = fields.Selection([
|
||||
('open', 'Abierto'),
|
||||
('closed', 'Cerrado'),
|
||||
], string='Estado', default='open')
|
||||
|
||||
notes = fields.Text(string='Notas')
|
||||
|
||||
@api.depends('date')
|
||||
def _compute_totals(self):
|
||||
for corte in self:
|
||||
ventas = self.env['skeen.venta'].search([
|
||||
('date', '=', corte.date),
|
||||
('state', 'in', ('confirmed', 'paid', 'partial')),
|
||||
])
|
||||
# Sumar también pagos por método
|
||||
pagos = self.env['skeen.pago'].search([
|
||||
('payment_date', '>=', corte.date.strftime('%Y-%m-%d 00:00:00')),
|
||||
('payment_date', '<=', corte.date.strftime('%Y-%m-%d 23:59:59')),
|
||||
('state', '=', 'completed'),
|
||||
])
|
||||
cash = sum(p.amount for p in pagos if p.payment_method == 'cash')
|
||||
card = sum(p.amount for p in pagos if p.payment_method in ('card', 'stripe', 'mercadopago'))
|
||||
transfer = sum(p.amount for p in pagos if p.payment_method == 'transfer')
|
||||
other = sum(p.amount for p in pagos if p.payment_method not in ('cash', 'card', 'stripe', 'mercadopago', 'transfer'))
|
||||
|
||||
corte.total_cash = cash
|
||||
corte.total_card = card
|
||||
corte.total_transfer = transfer
|
||||
corte.total_other = other
|
||||
corte.total_sales = cash + card + transfer + other
|
||||
|
||||
@api.depends('opening_cash', 'total_cash', 'closing_cash')
|
||||
def _compute_difference(self):
|
||||
for corte in self:
|
||||
expected = corte.opening_cash + corte.total_cash
|
||||
corte.difference = corte.closing_cash - expected
|
||||
|
||||
@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.corte.caja') or 'CORTE-001'
|
||||
return super(SkeenCorteCaja, self).create(vals_list)
|
||||
|
||||
def action_close(self):
|
||||
self.write({'state': 'closed'})
|
||||
|
||||
def action_reopen(self):
|
||||
self.write({'state': 'open'})
|
||||
5
odoo-addons/skeen_ventas/security/ir.model.access.csv
Normal file
5
odoo-addons/skeen_ventas/security/ir.model.access.csv
Normal file
@@ -0,0 +1,5 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_skeen_venta_user,Acceso a Ventas SKEEN,model_skeen_venta,base.group_user,1,1,1,1
|
||||
access_skeen_venta_line_user,Acceso a Líneas de Venta SKEEN,model_skeen_venta_line,base.group_user,1,1,1,1
|
||||
access_skeen_corte_caja_user,Acceso a Cortes de Caja SKEEN,model_skeen_corte_caja,base.group_user,1,1,1,1
|
||||
access_skeen_tipo_cambio_user,Acceso a Tipo de Cambio SKEEN,model_skeen_tipo_cambio,base.group_user,1,1,1,1
|
||||
|
63
odoo-addons/skeen_ventas/views/corte_views.xml
Normal file
63
odoo-addons/skeen_ventas/views/corte_views.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_skeen_corte_caja_tree" model="ir.ui.view">
|
||||
<field name="name">skeen.corte.caja.tree</field>
|
||||
<field name="model">skeen.corte.caja</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-muted="state=='closed'">
|
||||
<field name="name" />
|
||||
<field name="date" />
|
||||
<field name="user_id" />
|
||||
<field name="opening_cash" />
|
||||
<field name="total_cash" />
|
||||
<field name="total_card" />
|
||||
<field name="total_sales" />
|
||||
<field name="closing_cash" />
|
||||
<field name="difference" />
|
||||
<field name="state" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_skeen_corte_caja_form" model="ir.ui.view">
|
||||
<field name="name">skeen.corte.caja.form</field>
|
||||
<field name="model">skeen.corte.caja</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_close" string="Cerrar Caja" type="object" invisible="state != 'open'" class="oe_highlight" />
|
||||
<button name="action_reopen" string="Reabrir" type="object" invisible="state != 'closed'" />
|
||||
<field name="state" widget="statusbar" />
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name" />
|
||||
<field name="date" />
|
||||
<field name="user_id" />
|
||||
<field name="opening_cash" />
|
||||
<field name="closing_cash" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="total_cash" />
|
||||
<field name="total_card" />
|
||||
<field name="total_transfer" />
|
||||
<field name="total_other" />
|
||||
<field name="total_sales" />
|
||||
<field name="difference" />
|
||||
</group>
|
||||
</group>
|
||||
<field name="notes" />
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_skeen_corte_caja" model="ir.actions.act_window">
|
||||
<field name="name">Cortes de Caja</field>
|
||||
<field name="res_model">skeen.corte.caja</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
8
odoo-addons/skeen_ventas/views/menu_views.xml
Normal file
8
odoo-addons/skeen_ventas/views/menu_views.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<menuitem id="menu_skeen_ventas_root" name="Ventas" parent="skeen_citas.menu_skeen_root" sequence="30" />
|
||||
<menuitem id="menu_skeen_venta" name="Ventas" parent="menu_skeen_ventas_root" action="action_skeen_venta" sequence="10" />
|
||||
<menuitem id="menu_skeen_corte_caja" name="Cortes de Caja" parent="menu_skeen_ventas_root" action="action_skeen_corte_caja" sequence="20" />
|
||||
</data>
|
||||
</odoo>
|
||||
73
odoo-addons/skeen_ventas/views/venta_views.xml
Normal file
73
odoo-addons/skeen_ventas/views/venta_views.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Vista tree de ventas -->
|
||||
<record id="view_skeen_venta_tree" model="ir.ui.view">
|
||||
<field name="name">skeen.venta.tree</field>
|
||||
<field name="model">skeen.venta</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-success="state=='paid'" decoration-warning="state=='partial'" decoration-muted="state=='cancelled'">
|
||||
<field name="name" />
|
||||
<field name="partner_id" />
|
||||
<field name="date" />
|
||||
<field name="total" />
|
||||
<field name="amount_paid" />
|
||||
<field name="amount_due" />
|
||||
<field name="state" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vista form de ventas -->
|
||||
<record id="view_skeen_venta_form" model="ir.ui.view">
|
||||
<field name="name">skeen.venta.form</field>
|
||||
<field name="model">skeen.venta</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_confirm" string="Confirmar" type="object" invisible="state != 'draft'" class="oe_highlight" />
|
||||
<button name="action_mark_paid" string="Marcar Pagada" type="object" invisible="state not in ('confirmed', 'partial')" class="oe_highlight" />
|
||||
<button name="action_cancel" string="Cancelar" type="object" invisible="state not in ('draft', 'confirmed', 'partial')" />
|
||||
<field name="state" widget="statusbar" />
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name" />
|
||||
<field name="partner_id" />
|
||||
<field name="cita_id" />
|
||||
<field name="date" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="subtotal" />
|
||||
<field name="discount" />
|
||||
<field name="points_discount" />
|
||||
<field name="tax" />
|
||||
<field name="total" />
|
||||
<field name="amount_paid" />
|
||||
<field name="amount_due" />
|
||||
</group>
|
||||
</group>
|
||||
<field name="line_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="service_id" />
|
||||
<field name="description" />
|
||||
<field name="quantity" />
|
||||
<field name="price_unit" />
|
||||
<field name="subtotal" />
|
||||
</tree>
|
||||
</field>
|
||||
<field name="notes" />
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_skeen_venta" model="ir.actions.act_window">
|
||||
<field name="name">Ventas</field>
|
||||
<field name="res_model">skeen.venta</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user