|
|
|
|
@@ -7,7 +7,7 @@ import base64
|
|
|
|
|
import json
|
|
|
|
|
import re
|
|
|
|
|
from datetime import date, datetime, timedelta
|
|
|
|
|
from .auth import sign_token, current_user, require_role, verify_token
|
|
|
|
|
from .auth import sign_token, current_user, require_role, forbid_roles, verify_token
|
|
|
|
|
from ..roles import ROLE_SELECTION, ROLE_KEYS, MENU_ROLES_KEY, get_menu_roles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -41,6 +41,9 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
AGENDA_START = 8.0 # 8:00 AM
|
|
|
|
|
AGENDA_END = 19.0 # 7:00 PM
|
|
|
|
|
|
|
|
|
|
# Tope de seguridad de la exportación completa de pacientes (hoy ~7.2k)
|
|
|
|
|
EXPORT_MAX = 100000
|
|
|
|
|
|
|
|
|
|
def _valid_agenda_time(self, value):
|
|
|
|
|
"""Valida hora de inicio dentro del horario de agenda (8:00-19:00)."""
|
|
|
|
|
try:
|
|
|
|
|
@@ -56,6 +59,28 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
"""Usuario del frontend que hizo la petición (o None)."""
|
|
|
|
|
return getattr(request, 'frontend_user', None)
|
|
|
|
|
|
|
|
|
|
def _puede_gestionar_usuarios(self):
|
|
|
|
|
"""Admin del sistema o gerencia: gestión de usuarios y roles.
|
|
|
|
|
|
|
|
|
|
El rango de roles no distingue gerencia de recepción (ambos 50), así que
|
|
|
|
|
el permiso se comprueba por rol explícito, como en el resto del API.
|
|
|
|
|
"""
|
|
|
|
|
u = self._fu_user()
|
|
|
|
|
return bool(u and u.role in ('admin', 'gerencia'))
|
|
|
|
|
|
|
|
|
|
def _puede_gestionar_roles(self):
|
|
|
|
|
"""Editar la matriz de permisos: admin o gerencia."""
|
|
|
|
|
u = self._fu_user()
|
|
|
|
|
return bool(u and u.role in ('admin', 'gerencia'))
|
|
|
|
|
|
|
|
|
|
def _puede_asignar_admin(self):
|
|
|
|
|
"""Repartir el rol Administrador queda reservado al propio admin.
|
|
|
|
|
|
|
|
|
|
Gerencia gestiona usuarios y la matriz, pero no puede crear más admins.
|
|
|
|
|
"""
|
|
|
|
|
u = self._fu_user()
|
|
|
|
|
return bool(u and u.role == 'admin')
|
|
|
|
|
|
|
|
|
|
def _auditar(self, module, entity, field, old_value=None, new_value=None,
|
|
|
|
|
entity_label='', notes=''):
|
|
|
|
|
"""Registra un cambio de configuración en skeen.config.audit.
|
|
|
|
|
@@ -108,9 +133,13 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
return request.env.ref('base.user_admin', raise_if_not_found=False) or User.browse()
|
|
|
|
|
|
|
|
|
|
def _frontend_user_dict(self, user):
|
|
|
|
|
"""to_public_dict + datos del empleado vinculado (rol medico)."""
|
|
|
|
|
"""to_public_dict + datos del empleado vinculado.
|
|
|
|
|
|
|
|
|
|
Aplica a los roles clínicos (médico y cosmetología): la agenda usa
|
|
|
|
|
employee_id para preseleccionar la columna propia.
|
|
|
|
|
"""
|
|
|
|
|
d = user.to_public_dict()
|
|
|
|
|
if user.role == 'medico':
|
|
|
|
|
if user.role in ('medico', 'cosmetologia'):
|
|
|
|
|
emp = self._linked_employee(user)
|
|
|
|
|
d['employee_id'] = emp.id if emp else None
|
|
|
|
|
d['employee_name'] = emp.name if emp else None
|
|
|
|
|
@@ -163,18 +192,22 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
return json_response({'status': 'success'})
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/auth/users', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('admin')
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
def auth_users_list(self, **kw):
|
|
|
|
|
try:
|
|
|
|
|
if not self._puede_gestionar_usuarios():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
|
|
|
|
users = request.env['skeen.frontend.user'].sudo().search([], order='name, login')
|
|
|
|
|
return json_response({'status': 'success', 'users': [u.to_public_dict() | {'active': u.active} for u in users]})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return json_response({'status': 'error', 'message': str(e)}, 500)
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/auth/users', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('admin')
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
def auth_users_create(self, **kw):
|
|
|
|
|
try:
|
|
|
|
|
if not self._puede_gestionar_usuarios():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
|
|
|
|
data = _parse_json_body()
|
|
|
|
|
login = (data.get('login') or '').strip().lower()
|
|
|
|
|
name = (data.get('name') or '').strip()
|
|
|
|
|
@@ -186,6 +219,8 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
return json_response({'status': 'error', 'message': 'La contraseña debe tener al menos 8 caracteres'}, 400)
|
|
|
|
|
if role not in ROLE_KEYS:
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Rol inválido'}, 400)
|
|
|
|
|
if role == 'admin' and not self._puede_asignar_admin():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Solo un administrador puede crear administradores'}, 403)
|
|
|
|
|
User = request.env['skeen.frontend.user'].sudo()
|
|
|
|
|
if User.search([('login', '=', login)], limit=1):
|
|
|
|
|
return json_response({'status': 'error', 'message': 'El usuario ya existe'}, 409)
|
|
|
|
|
@@ -198,19 +233,44 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
return json_response({'status': 'error', 'message': str(e)}, 500)
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/auth/users/<int:user_id>', type='http', auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('admin')
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
def auth_users_update(self, user_id, **kw):
|
|
|
|
|
try:
|
|
|
|
|
if not self._puede_gestionar_usuarios():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
|
|
|
|
user = request.env['skeen.frontend.user'].sudo().browse(user_id)
|
|
|
|
|
if not user.exists():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Usuario no encontrado'}, 404)
|
|
|
|
|
data = _parse_json_body()
|
|
|
|
|
vals = {}
|
|
|
|
|
if 'name' in data:
|
|
|
|
|
vals['name'] = data['name']
|
|
|
|
|
name = (data.get('name') or '').strip()
|
|
|
|
|
if not name:
|
|
|
|
|
return json_response({'status': 'error', 'message': 'El nombre no puede estar vacío'}, 400)
|
|
|
|
|
vals['name'] = name
|
|
|
|
|
if 'login' in data:
|
|
|
|
|
login = (data.get('login') or '').strip().lower()
|
|
|
|
|
if not login:
|
|
|
|
|
return json_response({'status': 'error', 'message': 'El usuario no puede estar vacío'}, 400)
|
|
|
|
|
if not re.match(r'^[a-z0-9._-]{3,}$', login):
|
|
|
|
|
return json_response({
|
|
|
|
|
'status': 'error',
|
|
|
|
|
'message': 'El usuario debe tener al menos 3 caracteres (letras, números, punto, guion o guion bajo)',
|
|
|
|
|
}, 400)
|
|
|
|
|
duplicado = request.env['skeen.frontend.user'].sudo().search(
|
|
|
|
|
[('login', '=', login), ('id', '!=', user_id)], limit=1)
|
|
|
|
|
if duplicado:
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Ese usuario ya existe'}, 409)
|
|
|
|
|
vals['login'] = login
|
|
|
|
|
if 'role' in data:
|
|
|
|
|
if data['role'] not in ROLE_KEYS:
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Rol inválido'}, 400)
|
|
|
|
|
if not self._puede_gestionar_roles():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Sin permiso para cambiar el rol'}, 403)
|
|
|
|
|
if data['role'] == 'admin' and not self._puede_asignar_admin():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Solo un administrador puede asignar el rol Administrador'}, 403)
|
|
|
|
|
if user.role == 'admin' and data['role'] != 'admin' and not self._puede_asignar_admin():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Solo un administrador puede quitar el rol Administrador'}, 403)
|
|
|
|
|
vals['role'] = data['role']
|
|
|
|
|
if 'active' in data:
|
|
|
|
|
vals['active'] = bool(data['active'])
|
|
|
|
|
@@ -241,9 +301,11 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
return json_response({'status': 'error', 'message': str(e)}, 500)
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/auth/users/<int:user_id>', type='http', auth='none', methods=['DELETE'], csrf=False)
|
|
|
|
|
@require_role('admin')
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
def auth_users_delete(self, user_id, **kw):
|
|
|
|
|
try:
|
|
|
|
|
if not self._puede_gestionar_usuarios():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
|
|
|
|
user = request.env['skeen.frontend.user'].sudo().browse(user_id)
|
|
|
|
|
if not user.exists():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Usuario no encontrado'}, 404)
|
|
|
|
|
@@ -273,9 +335,11 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
return json_response({'status': 'error', 'message': str(e)}, 500)
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/roles/permissions', type='http', auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('admin')
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
def roles_permissions_put(self, **kw):
|
|
|
|
|
try:
|
|
|
|
|
if not self._puede_gestionar_roles():
|
|
|
|
|
return json_response({'status': 'error', 'message': 'Sin permiso'}, 403)
|
|
|
|
|
data = _parse_json_body()
|
|
|
|
|
matrix = data.get('matrix')
|
|
|
|
|
if not isinstance(matrix, dict):
|
|
|
|
|
@@ -1313,6 +1377,48 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return json_response({'status': 'error', 'message': str(e)}, 500)
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/patients/export', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('lectura')
|
|
|
|
|
def export_patients(self, **kw):
|
|
|
|
|
"""Todos los pacientes activos que cumplen los filtros, SIN paginar.
|
|
|
|
|
|
|
|
|
|
Pensado para exportaciones (marketing y quien tenga permiso de lectura):
|
|
|
|
|
devuelve ~7k filas de una sola vez (~3 MB de JSON), que es manejable.
|
|
|
|
|
`limit` sólo existe como salvaguarda ante un catálogo runaway; por
|
|
|
|
|
defecto no hay tope práctico (EXPORT_MAX).
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
domain = [('is_patient', '=', True)]
|
|
|
|
|
if kw.get('search'):
|
|
|
|
|
search = kw['search']
|
|
|
|
|
domain += ['|', '|', ('name', 'ilike', search), ('phone', 'ilike', search), ('email', 'ilike', search)]
|
|
|
|
|
if kw.get('incomplete') == '1':
|
|
|
|
|
domain += [('expediente_completion', '<', 100)]
|
|
|
|
|
if kw.get('vip') == '1':
|
|
|
|
|
domain.append(('is_vip', '=', True))
|
|
|
|
|
if kw.get('doctor_id'):
|
|
|
|
|
domain.append(('primary_doctor_id', '=', int(kw['doctor_id'])))
|
|
|
|
|
if kw.get('recent') == '1':
|
|
|
|
|
seis = (date.today() - timedelta(days=183)).strftime('%Y-%m-%d')
|
|
|
|
|
domain.append(('last_visit', '>=', seis))
|
|
|
|
|
elif kw.get('recent') == '0':
|
|
|
|
|
seis = (date.today() - timedelta(days=183)).strftime('%Y-%m-%d')
|
|
|
|
|
domain += ['|', ('last_visit', '=', False), ('last_visit', '<', seis)]
|
|
|
|
|
Pacientes = request.env['res.partner'].sudo()
|
|
|
|
|
total = Pacientes.search_count(domain)
|
|
|
|
|
tope = int(kw.get('limit') or self.EXPORT_MAX)
|
|
|
|
|
patients = Pacientes.search(domain, limit=tope, order='name asc')
|
|
|
|
|
return json_response({
|
|
|
|
|
'status': 'success',
|
|
|
|
|
'patients': [self._patient_to_dict(p) for p in patients],
|
|
|
|
|
'total': len(patients),
|
|
|
|
|
'total_disponible': total,
|
|
|
|
|
'truncado': len(patients) < total,
|
|
|
|
|
'limite': tope,
|
|
|
|
|
})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return json_response({'status': 'error', 'message': str(e)}, 500)
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/patients', type='http', auth='none', methods=['POST'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
def create_patient(self, **kw):
|
|
|
|
|
@@ -1409,7 +1515,35 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
vals = {k: data[k] for k in allowed if k in data}
|
|
|
|
|
if data.get('photo'):
|
|
|
|
|
vals['image_1920'] = data['photo']
|
|
|
|
|
patient.write(vals)
|
|
|
|
|
# Campos sensibles que este endpoint nunca toca (el dinero no se
|
|
|
|
|
# edita por aquí: monedero y saldos se gestionan en sus módulos).
|
|
|
|
|
if vals:
|
|
|
|
|
previos = {}
|
|
|
|
|
for k in vals:
|
|
|
|
|
v = getattr(patient, k, None)
|
|
|
|
|
previos[k] = (v.name if hasattr(v, 'name') else v)
|
|
|
|
|
patient.write(vals)
|
|
|
|
|
cambios = []
|
|
|
|
|
for k, nuevo in vals.items():
|
|
|
|
|
anterior = previos.get(k)
|
|
|
|
|
nuevo_cmp = getattr(patient, k, None)
|
|
|
|
|
nuevo_cmp = (nuevo_cmp.name if hasattr(nuevo_cmp, 'name') else nuevo_cmp)
|
|
|
|
|
if str(anterior or '') != str(nuevo_cmp or ''):
|
|
|
|
|
cambios.append(k)
|
|
|
|
|
self._auditar('pacientes', f'paciente:{patient.id}', k, anterior, nuevo_cmp,
|
|
|
|
|
entity_label=patient.name,
|
|
|
|
|
notes='Edición de expediente')
|
|
|
|
|
if cambios:
|
|
|
|
|
try:
|
|
|
|
|
autor = self._frontend_res_user()
|
|
|
|
|
patient.sudo().message_post(
|
|
|
|
|
body='Expediente editado desde el frontend: ' + ', '.join(cambios),
|
|
|
|
|
author_id=autor.id if autor else None,
|
|
|
|
|
message_type='notification',
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
# El rastro principal es la auditoría; el chatter es extra
|
|
|
|
|
pass
|
|
|
|
|
return json_response({'status': 'success', 'patient': self._patient_to_dict(patient)})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return json_response({'status': 'error', 'message': str(e)}, 500)
|
|
|
|
|
@@ -1586,6 +1720,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/patients/<int:patient_id>/pay_adeudo', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def pay_patient_adeudo(self, patient_id, **kw):
|
|
|
|
|
"""Registra un pago del adeudo (parcial o total) y devuelve el recibo.
|
|
|
|
|
|
|
|
|
|
@@ -3169,6 +3304,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/payments', type='http', auth='none', methods=['POST'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def create_payment(self, **kw):
|
|
|
|
|
try:
|
|
|
|
|
data = _parse_json_body()
|
|
|
|
|
@@ -3185,6 +3321,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/payments/<int:payment_id>/confirm', type='http', auth='none', methods=['POST'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def confirm_payment(self, payment_id, **kw):
|
|
|
|
|
try:
|
|
|
|
|
payment = request.env['skeen.pago'].sudo().browse(payment_id)
|
|
|
|
|
@@ -3247,6 +3384,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/sales', type='http', auth='none', methods=['POST'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def create_sale(self, **kw):
|
|
|
|
|
try:
|
|
|
|
|
data = _parse_json_body()
|
|
|
|
|
@@ -3287,6 +3425,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/sales/<int:sale_id>/pay', type='http', auth='none', methods=['POST'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def pay_sale(self, sale_id, **kw):
|
|
|
|
|
try:
|
|
|
|
|
sale = request.env['skeen.venta'].sudo().browse(sale_id)
|
|
|
|
|
@@ -3319,6 +3458,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/sales/<int:sale_id>/refund', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def refund_sale(self, sale_id, **kw):
|
|
|
|
|
try:
|
|
|
|
|
sale = request.env['skeen.venta'].sudo().browse(sale_id)
|
|
|
|
|
@@ -3334,6 +3474,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/sales/<int:sale_id>/cancel', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def cancel_sale(self, sale_id, **kw):
|
|
|
|
|
"""Cancelación de venta con nota de crédito (motivo obligatorio).
|
|
|
|
|
|
|
|
|
|
@@ -3480,6 +3621,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
# ============================================================
|
|
|
|
|
@http.route('/skeen/frontend/v1/pos/checkout', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def pos_checkout(self, **kw):
|
|
|
|
|
"""Crea la venta confirmada, aplica canje de puntos y/o registra el pago.
|
|
|
|
|
|
|
|
|
|
@@ -3672,6 +3814,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/cash_closings', type='http', auth='none', methods=['POST'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def create_cash_closing(self, **kw):
|
|
|
|
|
try:
|
|
|
|
|
data = _parse_json_body()
|
|
|
|
|
@@ -3689,6 +3832,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/cash_closings/<int:closing_id>/close', type='http', auth='none', methods=['POST'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def close_cash_closing(self, closing_id, **kw):
|
|
|
|
|
try:
|
|
|
|
|
closing = request.env['skeen.corte.caja'].sudo().browse(closing_id)
|
|
|
|
|
@@ -5094,6 +5238,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/inventory/items', type='http', auth='none', methods=['POST'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def inventory_create(self, **kw):
|
|
|
|
|
try:
|
|
|
|
|
data = _parse_json_body()
|
|
|
|
|
@@ -5119,6 +5264,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/inventory/items/<int:item_id>', type='http', auth='none', methods=['PUT'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def inventory_update(self, item_id, **kw):
|
|
|
|
|
try:
|
|
|
|
|
item = request.env['skeen.inventario.item'].sudo().browse(item_id)
|
|
|
|
|
@@ -5150,6 +5296,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/inventory/items/<int:item_id>/moves', type='http', auth='none', methods=['POST'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def inventory_move_create(self, item_id, **kw):
|
|
|
|
|
try:
|
|
|
|
|
item = request.env['skeen.inventario.item'].sudo().browse(item_id)
|
|
|
|
|
@@ -5212,6 +5359,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/inventario/compras', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def inventario_compra(self, **kw):
|
|
|
|
|
"""Registrar compra de consumible (sube stock; actualiza costo si viene)"""
|
|
|
|
|
try:
|
|
|
|
|
@@ -5300,6 +5448,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/inventario/bajas', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def inventario_baja(self, **kw):
|
|
|
|
|
"""Registrar baja/merma de consumible (baja stock)"""
|
|
|
|
|
try:
|
|
|
|
|
@@ -5376,6 +5525,7 @@ class SkeenFrontendController(http.Controller):
|
|
|
|
|
|
|
|
|
|
@http.route('/skeen/frontend/v1/inventario/conteos', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
|
|
|
@require_role('recepcion')
|
|
|
|
|
@forbid_roles('marketing')
|
|
|
|
|
def create_conteo(self, **kw):
|
|
|
|
|
"""Registra un conteo físico y aplica los ajustes por diferencia.
|
|
|
|
|
|
|
|
|
|
|