Files
Autoparts-DB/pos/tests/test_service_order_integration.py
consultoria-as 2bdeb2973a
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
fix(audit): corrige errores criticos y mayores, mejora UX/accesibilidad y optimiza rendimiento
- Arregla @require_auth, permisos, race conditions, locks de caja/stock
- Elimina N+1 en layaway, flotilla, dashboard y global_invoice
- Asegura folios atomicos para CFDI, ordenes de servicio y polizas
- Protege client_secret de MercadoLibre en backend
- Conecta botones/filtros de config, customers, accounting e invoicing
- Mejora accesibilidad (labels/aria-label) y estados de carga/vacio
- Limpia accounting.js obsoleto y consolida accounting.v9.js
- Actualiza cache busting a v32 y Service Worker a v32
- Documenta todo en docs/AUDIT_Y_MEJORAS_2026-06-15.md

Tests: 35 passed
2026-06-29 23:54:58 +00:00

242 lines
7.0 KiB
Python

"""Unit tests for service order workshop integration.
These tests use mocked DB cursors and inventory_engine so they do not require
PostgreSQL or network access.
"""
import os
import sys
POS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, POS_DIR)
from unittest import mock # noqa: E402
import pytest # noqa: E402
from services import service_order_engine as engine # noqa: E402
class MockCursor:
"""Simple programmable cursor mock."""
def __init__(self, responses=None):
self.responses = responses or []
self._calls = []
self._response_index = 0
def execute(self, sql, params=None):
self._calls.append((sql, params))
def fetchone(self):
if self._response_index < len(self.responses):
resp = self.responses[self._response_index]
self._response_index += 1
return resp
return None
def fetchall(self):
if self._response_index < len(self.responses):
resp = self.responses[self._response_index]
self._response_index += 1
return resp
return []
def close(self):
pass
class MockConn:
def __init__(self, cursor):
self._cursor = cursor
def cursor(self):
return self._cursor
def commit(self):
pass
def rollback(self):
pass
@pytest.fixture
def conn():
return MockConn(MockCursor())
def test_generate_order_number_first_of_day(conn):
conn._cursor.responses = [(None,)]
number = engine._generate_order_number(conn)
# Format DDMMYYYY-N
assert len(number.split("-")) == 2
assert number.split("-")[1] == "1"
def test_generate_order_number_increments(conn):
from datetime import datetime
today = datetime.utcnow().strftime('%d%m%Y')
conn._cursor.responses = [(f"{today}-42",)]
number = engine._generate_order_number(conn)
assert number == f"{today}-43"
@mock.patch("services.inventory_engine.get_stock", return_value=10)
@mock.patch("services.inventory_engine.record_operation", return_value=123)
def test_reserve_item_inserts_so_reserve_and_updates_quantity(mock_record, mock_stock, conn):
conn._cursor.responses = [
(1, 5, 3, "pending", "SO-2026-0001", 2), # item lookup (branch_id=2)
None, # update
]
result = engine.reserve_item(conn, 7, branch_id=99, employee_id=9)
assert result["reserved"] == 3
mock_stock.assert_called_once_with(conn, 5, 2)
mock_record.assert_called_once()
args = mock_record.call_args.args
assert args[3] == "SO_RESERVE"
assert args[4] == -3
@mock.patch("services.inventory_engine.get_stock", return_value=1)
def test_reserve_item_raises_when_insufficient_stock(mock_stock, conn):
conn._cursor.responses = [
(1, 5, 3, "pending", "SO-2026-0001", 2),
]
with pytest.raises(ValueError, match="Insufficient stock"):
engine.reserve_item(conn, 7, branch_id=99)
@mock.patch("services.inventory_engine.record_operation", return_value=124)
def test_release_item_restores_stock(mock_record, conn):
conn._cursor.responses = [
(1, 5, 2, 2, "SO-2026-0001"), # item lookup (reserved_quantity=2)
None, # update
]
result = engine.release_item(conn, 7, employee_id=9)
assert result["released"] == 2
args = mock_record.call_args.args
assert args[3] == "SO_RELEASE"
assert args[4] == 2
@mock.patch("services.inventory_engine.record_operation", return_value=125)
def test_convert_to_sale_creates_sale_and_consumes_inventory(mock_record, conn):
# Mock get_service_order response
so = {
"id": 1,
"order_number": "SO-2026-0001",
"status": "ready",
"sale_id": None,
"branch_id": 2,
"customer_id": 3,
"items": [
{
"id": 10,
"inventory_id": 5,
"part_number": "BP-123",
"name": "Bujia",
"quantity": 2,
"unit_price": 150.0,
"unit_cost": 80.0,
"status": "pending",
"reserved_quantity": 2,
}
],
"labor": [
{
"description": "Cambio de bujias",
"hours": 1,
"hourly_rate": 250,
"total_cost": 250,
"status": "completed",
}
],
}
cur = conn._cursor
cur.responses = [
(1, "2026-01-01 10:00:00"), # sale insert -> id=1
]
with mock.patch.object(engine, "get_service_order", return_value=so):
result = engine.convert_to_sale(conn, 1, {"payment_method": "efectivo", "sale_type": "cash"}, employee_id=9)
assert result["sale_id"] == 1
assert result["items_count"] == 2
assert result["total"] == pytest.approx(2 * 150 * 1.16 + 250 * 1.16, 0.01)
# inventory operations: SO_RELEASE + SALE
assert mock_record.call_count == 2
first = mock_record.call_args_list[0].args
second = mock_record.call_args_list[1].args
assert first[3] == "SO_RELEASE"
assert first[4] == 2
assert second[3] == "SALE"
assert second[4] == -2
def test_convert_to_sale_raises_when_already_converted(conn):
so = {"status": "ready", "sale_id": 99, "branch_id": 1, "customer_id": 1, "items": [], "labor": []}
conn._cursor.responses = [(1,)]
with (
mock.patch.object(engine, "get_service_order", return_value=so),
pytest.raises(ValueError, match="already converted"),
):
engine.convert_to_sale(conn, 1, {})
def test_convert_to_sale_raises_when_cancelled(conn):
so = {"status": "cancelled", "sale_id": None, "branch_id": 1, "customer_id": 1, "items": [], "labor": []}
conn._cursor.responses = [(1,)]
with (
mock.patch.object(engine, "get_service_order", return_value=so),
pytest.raises(ValueError, match="cancelled"),
):
engine.convert_to_sale(conn, 1, {})
def test_assign_mechanic_updates_employee(conn):
conn._cursor.responses = [(1,), None]
result = engine.assign_mechanic(conn, 1, 7)
assert result["employee_id"] == 7
def test_assign_mechanic_raises_when_order_missing(conn):
conn._cursor.responses = [None]
with pytest.raises(ValueError, match="not found"):
engine.assign_mechanic(conn, 1, 7)
def test_service_catalog_crud(conn):
# create
conn._cursor.responses = [(1,)]
result = engine.create_service_catalog_item(
conn, 1, {"name": "Afinacion", "suggested_hours": 2, "suggested_rate": 300}
)
assert result["id"] == 1
# list
conn._cursor = MockCursor(
[
[(1, 1, "Afinacion", "", 2, 300, True, None, None)],
]
)
items = engine.list_service_catalog(conn)
assert len(items) == 1
assert items[0]["name"] == "Afinacion"
# update
conn._cursor = MockCursor([None])
ok = engine.update_service_catalog_item(conn, 1, {"name": "Afinacion mayor"})
assert ok is True
# delete
conn._cursor = MockCursor([None])
ok = engine.delete_service_catalog_item(conn, 1)
assert ok is True