Apuntes DAM
Volver al inicio

Ejercicios resueltos de Sistemas de Gestión Empresarial

Los 25 ejercicios de Sistemas de Gestión Empresarial de la web, tema a tema: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Sistemas ERP-CRM

1. El embudo de ventas del CRM

Medio · Sistemas ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/sistemas-erp-crm

Cada línea de la entrada es una oportunidad del CRM con el formato «nombre;etapa;importe». Las etapas, en orden, y su probabilidad de cierre son: Nuevo 10 %, Calificado 30 %, Propuesta 50 %, Negociación 75 %, Ganado 100 % y Perdido 0 %. Muestra, para cada etapa en ese orden (aunque no tenga oportunidades), «Etapa: N oportunidades, importe €» con el importe total con 2 decimales. Después, «Previsión ponderada: X €» (la suma de importe × probabilidad de las oportunidades abiertas, es decir, sin contar Ganado ni Perdido) y «Tasa de conversión: X %» (ganadas entre ganadas más perdidas, con 1 decimal; si no hay ninguna cerrada, «Tasa de conversión: -»).

Código de partida (python)
import sys ETAPAS = [("Nuevo", 10), ("Calificado", 30), ("Propuesta", 50), ("Negociación", 75), ("Ganado", 100), ("Perdido", 0)] lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO: recuento por etapa, previsión ponderada y tasa de conversión

Ejemplo: Embudo completo

Entrada

Bodega Ruiz;Propuesta;12000
Hotel Mar;Nuevo;3000
Clínica Sol;Ganado;8000
Taller Paco;Perdido;1500
Academia Luz;Negociación;6000
Gimnasio Up;Ganado;4500

Salida esperada

Nuevo: 1 oportunidades, 3000.00 €
Calificado: 0 oportunidades, 0.00 €
Propuesta: 1 oportunidades, 12000.00 €
Negociación: 1 oportunidades, 6000.00 €
Ganado: 2 oportunidades, 12500.00 €
Perdido: 1 oportunidades, 1500.00 €
Previsión ponderada: 10800.00 €
Tasa de conversión: 66.7 %

Ejemplo: Ninguna cerrada

Entrada

A;Calificado;1000
B;Calificado;2500.5

Salida esperada

Nuevo: 0 oportunidades, 0.00 €
Calificado: 2 oportunidades, 3500.50 €
Propuesta: 0 oportunidades, 0.00 €
Negociación: 0 oportunidades, 0.00 €
Ganado: 0 oportunidades, 0.00 €
Perdido: 0 oportunidades, 0.00 €
Previsión ponderada: 1050.15 €
Tasa de conversión: -

2. Coste total de propiedad a varios años

Difícil · Sistemas ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/sistemas-erp-crm

La primera línea es «años N». Cada línea siguiente describe una opción de ERP: «nombre;licencia_usuario_mes;usuarios;implantación;formación;hardware;mantenimiento_anual». El coste total de propiedad (TCO) es: licencia × usuarios × 12 × años + implantación + formación + hardware + mantenimiento_anual × años. Muestra las opciones ordenadas de menor a mayor TCO (a igualdad, por nombre) con el formato «nombre: X €» (2 decimales), y después «Recomendada: nombre» (la más barata) y «Ahorro frente a la más cara: X € (Y %)», con el porcentaje respecto a la más cara y 1 decimal. Si solo hay una opción, no muestres la línea de ahorro.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO: calcula el TCO de cada opción y recomienda

Ejemplo: Tres opciones a 5 años

Entrada

años 5
Odoo Enterprise;31.1;20;18000;4000;0;2500
Business Central;70;20;25000;5000;0;3000
Odoo Community on-premise;0;20;30000;4000;6000;7000

Salida esperada

Odoo Enterprise: 71820.00 €
Odoo Community on-premise: 75000.00 €
Business Central: 129000.00 €
Recomendada: Odoo Enterprise
Ahorro frente a la más cara: 57180.00 € (44.3 %)

Ejemplo: Una sola opción

Entrada

años 3
Holded;15;4;500;200;0;0

Salida esperada

Holded: 2860.00 €
Recomendada: Holded

3. Dimensionar servidores para Odoo

Muy difícil · Sistemas ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/sistemas-erp-crm

Cada línea es un escenario: «nombre;usuarios_concurrentes;núcleos;ram_gb». Aplica estas reglas: workers = techo(concurrentes / 6), con un mínimo de 2; además hay 1 worker de cron. Núcleos necesarios = techo(workers / 2). Memoria necesaria (MB) = (workers + 1) × 250 + 2048 de PostgreSQL + 1024 del sistema. Para cada escenario muestra, en este orden: «== nombre», «workers = N», «max_cron_threads = 1», «CPU: suficiente» o «CPU: insuficiente (faltan N núcleos)», «Memoria: suficiente» o «Memoria: insuficiente (faltan N MB)» y, si algo es insuficiente, «Recomendado: X», el servidor más pequeño del catálogo que cubra ambas cosas: S (2 núcleos, 4 GB), M (4, 8), L (8, 16), XL (16, 32); si ninguno llega, «Recomendado: varios servidores».

Código de partida (python)
import sysimport math CATALOGO = [("S", 2, 4), ("M", 4, 8), ("L", 8, 16), ("XL", 16, 32)] lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO: dimensiona cada escenario

Ejemplo: Servidor justo y otro corto

Entrada

Oficina;20;4;8
Almacén;40;2;4

Salida esperada

== Oficina
workers = 4
max_cron_threads = 1
CPU: suficiente
Memoria: suficiente
== Almacén
workers = 7
max_cron_threads = 1
CPU: insuficiente (faltan 2 núcleos)
Memoria: insuficiente (faltan 976 MB)
Recomendado: M

Ejemplo: Pocos usuarios y memoria justa

Entrada

Tienda;3;1;4

Salida esperada

== Tienda
workers = 2
max_cron_threads = 1
CPU: suficiente
Memoria: suficiente

Ejemplo: Solo falta memoria

Entrada

Taller;12;2;3

Salida esperada

== Taller
workers = 2
max_cron_threads = 1
CPU: suficiente
Memoria: insuficiente (faltan 750 MB)
Recomendado: S

Implantación de un ERP-CRM

4. ¿Puede este usuario hacerlo?

Medio · Implantación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/implantacion-erp

En Odoo, los derechos de acceso (ir.model.access) dan a cada grupo permisos de lectura (r), escritura (w), creación (c) y borrado (d) sobre cada modelo, y un usuario tiene la suma de los permisos de todos sus grupos. La entrada tiene tres secciones. En [accesos], líneas «grupo modelo permisos» (permisos es una combinación de rwcd, o «-» si ninguno; puede haber varias líneas del mismo grupo y modelo, que se suman). En [usuarios], «usuario grupo1,grupo2…». En [consultas], «usuario acción modelo», con acción leer, escribir, crear o borrar. Para cada consulta muestra «usuario acción modelo: PERMITIDO» o «DENEGADO». Un usuario que no existe no tiene ningún permiso.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()]LETRA = {"leer": "r", "escribir": "w", "crear": "c", "borrar": "d"} # TODO: lee las secciones y responde a cada consulta

Ejemplo: Comercial y administración

Entrada

[accesos]
ventas_usuario sale.order rwc
ventas_usuario res.partner rw
ventas_admin sale.order rwcd
contabilidad account.move rwcd
empleado res.partner r
[usuarios]
ana ventas_usuario,empleado
luis contabilidad,empleado
marta ventas_admin
[consultas]
ana crear sale.order
ana borrar sale.order
marta borrar sale.order
luis leer res.partner
luis escribir res.partner
ana escribir res.partner
marta leer account.move

Salida esperada

ana crear sale.order: PERMITIDO
ana borrar sale.order: DENEGADO
marta borrar sale.order: PERMITIDO
luis leer res.partner: PERMITIDO
luis escribir res.partner: DENEGADO
ana escribir res.partner: PERMITIDO
marta leer account.move: DENEGADO

Ejemplo: Permisos vacíos y usuario desconocido

Entrada

[accesos]
lectura product.product r
nadie product.product -
[usuarios]
pepe nadie
eva lectura,nadie
[consultas]
pepe leer product.product
eva leer product.product
eva borrar product.product
juan leer product.product

Salida esperada

pepe leer product.product: DENEGADO
eva leer product.product: PERMITIDO
eva borrar product.product: DENEGADO
juan leer product.product: DENEGADO

5. Limpiar los clientes antes de migrarlos

Difícil · Implantación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/implantacion-erp

Antes de importar los clientes del sistema antiguo hay que limpiarlos. La entrada es un CSV con cabecera «nombre;nif;email;telefono» (la línea 1). Para cada fila, normaliza: nombre sin espacios sobrantes; NIF en mayúsculas y sin guiones ni espacios; correo en minúsculas; teléfono sin espacios, puntos ni guiones y sin el prefijo +34 o 0034. Después valida, en este orden: el NIF (DNI de 8 cifras y letra, o NIE que empieza por X, Y o Z —que valen 0, 1 y 2— seguido de 7 cifras y letra; la letra es TABLA[número % 23]); el correo (algo@algo.dominio con al menos 2 letras en el dominio); y el teléfono (9 cifras empezando por 6, 7, 8 o 9). Si falla, «ERROR línea N: NIF no válido» (o «correo no válido», «teléfono no válido»). Si es válido pero su NIF ya apareció en una fila válida anterior, «DUPLICADO línea N: NIF (ya en la línea M)». Si no, «OK nif;nombre;email;telefono». Al final, «Válidos: A · Errores: B · Duplicados: C». Las líneas se numeran como en el fichero, contando la cabecera.

Código de partida (python)
import sysimport re TABLA = "TRWAGMYFPDXBNJZSQVHLCKE" lineas = [l for l in sys.stdin.read().splitlines() if l.strip()] # TODO: valida y normaliza cada línea (la primera es la cabecera)

Ejemplo: Clientes variados

Entrada

nombre;nif;email;telefono
  Lucía   Pérez Gómez ;12345678z; Lucia.Perez@Correo.ES ;+34 612 345 678
Talleres Norte;X1234567L;info@talleresnorte.es;942.123.456
Juan Ruiz;12345678A;juan@correo.es;612345678
Marta Díaz;87654321x;marta@correo;612000111
Pedro Gil;00000000T;pedro@correo.es;512345678
L. Pérez;12345678-Z;lucia@otra.es;0034 699 000 111

Salida esperada

OK 12345678Z;Lucía Pérez Gómez;lucia.perez@correo.es;612345678
OK X1234567L;Talleres Norte;info@talleresnorte.es;942123456
ERROR línea 4: NIF no válido
ERROR línea 5: correo no válido
ERROR línea 6: teléfono no válido
DUPLICADO línea 7: 12345678Z (ya en la línea 2)
Válidos: 2 · Errores: 3 · Duplicados: 1

Ejemplo: Todo correcto

Entrada

nombre;nif;email;telefono
Ana;Y1234567X;ana@a.es;700000000
Bea;Z1234567R;bea@b.com;800 00 00 00

Salida esperada

OK Y1234567X;Ana;ana@a.es;700000000
OK Z1234567R;Bea;bea@b.com;800000000
Válidos: 2 · Errores: 0 · Duplicados: 0

6. Rotación de copias abuelo-padre-hijo

Muy difícil · Implantación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/implantacion-erp

La estrategia de rotación abuelo-padre-hijo (GFS) conserva copias de seguridad a distintas escalas sin llenar el disco. La entrada son fechas de copias (AAAA-MM-DD), una por línea, desordenadas y quizá repetidas. Se conservan: las 7 copias más recientes (diaria); la copia más reciente de cada una de las 4 semanas ISO más recientes que tengan copias (semanal); y la copia más reciente de cada uno de los 12 meses más recientes que tengan copias (mensual). Muestra todas las fechas, de la más reciente a la más antigua, con «fecha conservar (motivos)» —los motivos en el orden diaria, semanal, mensual, separados por «, »— o «fecha borrar», y al final «Conservadas: N · Borradas: M».

Código de partida (python)
import sysfrom datetime import date fechas = [date.fromisoformat(l.strip()) for l in sys.stdin.read().split() if l.strip()] # TODO: decide qué copias conservar (diaria, semanal, mensual) y cuáles borrar

Ejemplo: Un mes de copias diarias

Entrada

2026-09-01
2026-09-02
2026-09-03
2026-09-04
2026-09-05
2026-09-06
2026-09-07
2026-09-08
2026-09-09
2026-09-10
2026-09-11
2026-09-12
2026-09-13
2026-09-14
2026-09-15
2026-09-16
2026-09-17
2026-09-18
2026-09-19
2026-09-20
2026-09-21
2026-09-22
2026-09-23
2026-09-24
2026-09-25
2026-09-26
2026-09-27
2026-09-28
2026-09-29
2026-09-30

Salida esperada

2026-09-30 conservar (diaria, semanal, mensual)
2026-09-29 conservar (diaria)
2026-09-28 conservar (diaria)
2026-09-27 conservar (diaria, semanal)
2026-09-26 conservar (diaria)
2026-09-25 conservar (diaria)
2026-09-24 conservar (diaria)
2026-09-23 borrar
2026-09-22 borrar
2026-09-21 borrar
2026-09-20 conservar (semanal)
2026-09-19 borrar
2026-09-18 borrar
2026-09-17 borrar
2026-09-16 borrar
2026-09-15 borrar
2026-09-14 borrar
2026-09-13 conservar (semanal)
2026-09-12 borrar
2026-09-11 borrar
2026-09-10 borrar
2026-09-09 borrar
2026-09-08 borrar
2026-09-07 borrar
2026-09-06 borrar
2026-09-05 borrar
2026-09-04 borrar
2026-09-03 borrar
2026-09-02 borrar
2026-09-01 borrar
Conservadas: 9 · Borradas: 21

Ejemplo: Copias sueltas de varios meses

Entrada

2026-01-15
2026-01-31
2026-02-28
2026-03-01
2026-03-02
2026-05-20
2026-05-21
2026-05-22

Salida esperada

2026-05-22 conservar (diaria, semanal, mensual)
2026-05-21 conservar (diaria)
2026-05-20 conservar (diaria)
2026-03-02 conservar (diaria, semanal, mensual)
2026-03-01 conservar (diaria, semanal)
2026-02-28 conservar (diaria, mensual)
2026-01-31 conservar (diaria, semanal, mensual)
2026-01-15 borrar
Conservadas: 7 · Borradas: 1

Adaptación de un ERP-CRM

7. Encuentra el fallo: el descuento para casi todos

Fácil · Adaptación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/adaptacion-erp

A partir de 10 unidades se aplica un descuento del 5 %. Con 12 unidades funciona, pero con 9 también da el descuento. Corrígelo.

Código de partida (python)
cantidad = input()if cantidad >= "10":    print("Descuento del 5 %")else:    print("Sin descuento")

Ejemplo: 12 unidades

Entrada

12

Salida esperada

Descuento del 5 %

Ejemplo: 9 unidades

Entrada

9

Salida esperada

Sin descuento

Ejemplo: 100 unidades

Entrada

100

Salida esperada

Descuento del 5 %

8. Encuentra el fallo: el ranking que no existe

Fácil · Adaptación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/adaptacion-erp

El programa ordena los clientes por importe facturado, de mayor a menor, y muestra el ranking. En lugar del ranking da un error. Corrígelo.

Código de partida (python)
clientes = [("Carlos", 558.0), ("Ana", 1079.86), ("Javier", 165.4), ("Lucía", 297.0)]ranking = clientes.sort(key=lambda c: c[1], reverse=True)for posicion, (nombre, importe) in enumerate(ranking, start=1):    print(f"{posicion}. {nombre}: {importe:.2f} €")

Ejemplo: Ranking de facturación

Salida esperada

1. Ana: 1079.86 €
2. Carlos: 558.00 €
3. Lucía: 297.00 €
4. Javier: 165.40 €

9. Encuentra el fallo: el stock que aparece en todos los almacenes

Medio · Adaptación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/adaptacion-erp

Se crea una tabla de stock con 2 almacenes y 3 productos, todo a 0, y se anotan 5 unidades del producto 1 en el almacén 0. Al imprimirla, las 5 unidades aparecen en los dos almacenes. Corrígelo.

Código de partida (python)
almacenes, productos = 2, 3stock = [[0] * productos] * almacenesstock[0][1] = 5for i, fila in enumerate(stock):    print(f"Almacén {i}: {fila}")

Ejemplo: Tabla de stock

Salida esperada

Almacén 0: [0, 5, 0]
Almacén 1: [0, 0, 0]

10. Un evaluador de dominios de Odoo

Muy difícil · Adaptación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/adaptacion-erp

Las reglas de automatización, las reglas de registro y los filtros de Odoo usan dominios: listas de condiciones (campo, operador, valor) en notación prefija (polaca), donde '&' (y) y '|' (o) se aplican a las dos expresiones que les siguen, '!' (no) a la siguiente, y los términos seguidos del nivel superior se unen con Y. La primera línea de la entrada es un dominio escrito como lista de Python; cada línea siguiente es un registro en JSON con su id. Muestra los id de los registros que cumplen el dominio, separados por espacios y en el orden de la entrada, o «ninguno». Operadores: =, !=, >, >=, <, <=, in, not in e ilike (contiene, sin distinguir mayúsculas). Un campo que falta vale None: para = False cumple, y para >, <, >=, <= e ilike no cumple.

Código de partida (python)
import sysimport astimport json lineas = [l for l in sys.stdin.read().splitlines() if l.strip()]dominio = ast.literal_eval(lineas[0])registros = [json.loads(l) for l in lineas[1:]] # TODO: muestra los id de los registros que cumplen el dominio

Ejemplo: AND implícito

Entrada

[('is_company', '=', True), ('city', '=', 'Logroño')]
{"id": 1, "name": "Talleres Norte", "city": "Logroño", "is_company": true, "credit": 1200.5, "country": "ES"}
{"id": 2, "name": "Ana Ruiz", "city": "Haro", "is_company": false, "credit": 0, "country": "ES"}
{"id": 3, "name": "Clínica Sol", "city": "Logroño", "is_company": true, "credit": 0, "country": "ES"}
{"id": 4, "name": "Weinhaus GmbH", "city": "Berlín", "is_company": true, "credit": 5300, "country": "DE"}
{"id": 5, "name": "Pedro Gil", "is_company": false, "credit": 90, "country": "ES"}

Salida esperada

1 3

Ejemplo: OR en notación prefija

Entrada

['|', ('city', '=', 'Haro'), ('credit', '>', 1000)]
{"id": 1, "name": "Talleres Norte", "city": "Logroño", "is_company": true, "credit": 1200.5, "country": "ES"}
{"id": 2, "name": "Ana Ruiz", "city": "Haro", "is_company": false, "credit": 0, "country": "ES"}
{"id": 3, "name": "Clínica Sol", "city": "Logroño", "is_company": true, "credit": 0, "country": "ES"}
{"id": 4, "name": "Weinhaus GmbH", "city": "Berlín", "is_company": true, "credit": 5300, "country": "DE"}
{"id": 5, "name": "Pedro Gil", "is_company": false, "credit": 90, "country": "ES"}

Salida esperada

1 2 4

Ejemplo: Negación, in e ilike

Entrada

['!', ('country', 'in', ['ES', 'PT']), ('name', 'ilike', 'GMBH')]
{"id": 1, "name": "Talleres Norte", "city": "Logroño", "is_company": true, "credit": 1200.5, "country": "ES"}
{"id": 2, "name": "Ana Ruiz", "city": "Haro", "is_company": false, "credit": 0, "country": "ES"}
{"id": 3, "name": "Clínica Sol", "city": "Logroño", "is_company": true, "credit": 0, "country": "ES"}
{"id": 4, "name": "Weinhaus GmbH", "city": "Berlín", "is_company": true, "credit": 5300, "country": "DE"}
{"id": 5, "name": "Pedro Gil", "is_company": false, "credit": 90, "country": "ES"}

Salida esperada

4

Ejemplo: Campo ausente y False

Entrada

[('city', '=', False)]
{"id": 1, "name": "Talleres Norte", "city": "Logroño", "is_company": true, "credit": 1200.5, "country": "ES"}
{"id": 2, "name": "Ana Ruiz", "city": "Haro", "is_company": false, "credit": 0, "country": "ES"}
{"id": 3, "name": "Clínica Sol", "city": "Logroño", "is_company": true, "credit": 0, "country": "ES"}
{"id": 4, "name": "Weinhaus GmbH", "city": "Berlín", "is_company": true, "credit": 5300, "country": "DE"}
{"id": 5, "name": "Pedro Gil", "is_company": false, "credit": 90, "country": "ES"}

Salida esperada

5

Desarrollo de Componentes ERP

11. Encuentra el fallo: el recuento de ventas que se rompe

Fácil · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

El programa cuenta las unidades vendidas de cada producto a partir de las líneas de pedido y las muestra por orden alfabético. Se detiene con un error en la primera línea. Corrígelo.

Código de partida (python)
lineas = ["Portátil", "Ratón", "Ratón", "Webcam", "Ratón", "Portátil"]ventas = {}for producto in lineas:    ventas[producto] += 1for producto, unidades in sorted(ventas.items()):    print(producto, unidades)

Ejemplo: Seis líneas

Salida esperada

Portátil 2
Ratón 3
Webcam 1

12. Un campo calculado: la amortización

Medio · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

En el módulo de equipamiento, el campo calculado valor_actual depende de la fecha de compra, la vida útil y el precio (@api.depends). La primera línea es la fecha de hoy (AAAA-MM-DD); cada línea siguiente, un equipo: «nombre;fecha_compra;vida_util_en_años;precio». Con amortización lineal, años = días transcurridos / 365 y valor = precio × (1 − años / vida útil), sin bajar de 0. Muestra «nombre: valor € (estado, N años)», con el valor con 2 decimales, el estado «amortizado» si el valor es 0 y «en uso» si no, y los años con 1 decimal. Si la fecha de compra es posterior a hoy, «nombre: fecha de compra futura».

Código de partida (python)
import sysfrom datetime import date lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()]hoy = date.fromisoformat(lineas[0]) # TODO: calcula el valor actual y el estado de cada equipo

Ejemplo: Varios equipos

Entrada

2026-10-01
Portátil Dell;2024-10-01;4;1200
Impresora HP;2019-03-15;5;450
Monitor LG;2026-04-01;6;220
Servidor;2027-01-01;5;3000

Salida esperada

Portátil Dell: 600.00 € (en uso, 2.0 años)
Impresora HP: 0.00 € (amortizado, 7.6 años)
Monitor LG: 201.62 € (en uso, 0.5 años)
Servidor: fecha de compra futura

Ejemplo: Recién comprado

Entrada

2026-10-01
Ratón;2026-10-01;3;25

Salida esperada

Ratón: 25.00 € (en uso, 0.0 años)

13. Encuentra el fallo: las líneas que se comparten

Medio · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

Cada pedido debería empezar sin líneas. Ana pide un portátil y Luis dos ratones, pero el programa dice que los dos pedidos tienen 2 líneas. Corrígelo.

Código de partida (python)
class Pedido:    def __init__(self, cliente, lineas=[]):        self.cliente = cliente        self.lineas = lineas     def anadir(self, producto, cantidad):        self.lineas.append((producto, cantidad))  p1 = Pedido("Ana")p1.anadir("Portátil", 1)p2 = Pedido("Luis")p2.anadir("Ratón", 2)print(p1.cliente, len(p1.lineas))print(p2.cliente, len(p2.lineas))

Ejemplo: Dos pedidos

Salida esperada

Ana 1
Luis 1

14. Encuentra el fallo: los agotados que se escapan

Medio · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

El programa quita del inventario los productos sin stock. Deberían quedar el ratón y el teclado, pero también queda la webcam, que tiene 0 unidades. Corrígelo.

Código de partida (python)
productos = [["Auriculares", 0], ["Webcam", 0], ["Ratón", 120], ["Altavoz", 0], ["Teclado", 45]]for p in productos:    if p[1] == 0:        productos.remove(p)print([p[0] for p in productos])

Ejemplo: Inventario

Salida esperada

['Ratón', 'Teclado']

15. Encuentra el fallo: el pedido confirmado que no se factura

Medio · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

El programa lee el estado de un pedido y, si está confirmado, genera la factura. Al escribir «confirmado» dice que todavía no se factura. Corrígelo.

Código de partida (python)
estado = input().strip()if estado is "confirmado":    print("Pedido confirmado: se genera la factura")else:    print(f"El pedido está en estado {estado}: todavía no se factura")

Ejemplo: Pedido confirmado

Entrada

confirmado

Salida esperada

Pedido confirmado: se genera la factura

Ejemplo: Pedido en borrador

Entrada

borrador

Salida esperada

El pedido está en estado borrador: todavía no se factura

16. Restricciones de un modelo

Difícil · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

En Odoo, los métodos decorados con @api.constrains lanzan un ValidationError y el registro no se guarda. Simula el modelo de mantenimientos: la primera línea es la fecha de hoy y cada línea siguiente un mantenimiento «equipo;fecha;coste;tipo». Comprueba, en este orden: el coste no es negativo; el tipo es preventivo o correctivo; la fecha no es posterior a hoy; y el equipo no tiene ya otro mantenimiento guardado esa misma fecha (una restricción única sobre equipo y fecha). Para cada error muestra «ValidationError en el registro N: mensaje» (N empieza en 1) con los mensajes «el coste no puede ser negativo», «tipo «X» no permitido», «la fecha no puede ser futura» o «EQUIPO ya tiene un mantenimiento el FECHA». Los registros con error no se guardan (no cuentan para los duplicados). Al final, «Guardados: N».

Código de partida (python)
import sysfrom datetime import date lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()]hoy = date.fromisoformat(lineas[0])TIPOS = ("preventivo", "correctivo") # TODO: valida cada mantenimiento como lo haría @api.constrains

Ejemplo: Mezcla de errores

Entrada

2026-10-01
Portátil Dell;2026-09-10;45.5;preventivo
Impresora HP;2026-09-12;-10;correctivo
Impresora HP;2026-09-12;60;revisión
Servidor;2026-11-02;0;preventivo
Portátil Dell;2026-09-10;20;correctivo
Servidor;2026-10-01;120;correctivo

Salida esperada

ValidationError en el registro 2: el coste no puede ser negativo
ValidationError en el registro 3: tipo «revisión» no permitido
ValidationError en el registro 4: la fecha no puede ser futura
ValidationError en el registro 5: Portátil Dell ya tiene un mantenimiento el 2026-09-10
Guardados: 2

Ejemplo: Todo correcto

Entrada

2026-10-01
A;2026-01-01;0;preventivo
A;2026-01-02;10;preventivo
B;2026-01-01;10;correctivo

Salida esperada

Guardados: 3

17. Secuencias de referencias

Muy difícil · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

Los módulos de Odoo numeran sus registros con secuencias (ir.sequence): un prefijo que puede incluir la fecha, un número con ceros a la izquierda y un reinicio del contador. La entrada tiene tres líneas de configuración —«prefijo=…», «relleno=N» y «reinicio=anual|mensual|nunca»— y después las fechas (AAAA-MM-DD) en que se crean los registros, en orden de creación. En el prefijo, %(year)s es el año con 4 cifras, %(y)s con 2, %(month)s el mes y %(day)s el día, con sus ceros. El contador empieza en 1 y se reinicia cuando cambia el año (anual) o el año y mes (mensual); si se vuelve a un periodo anterior, continúa su propio contador. Muestra «fecha → referencia» por cada registro.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()]patron = lineas[0].split("=", 1)[1]relleno = int(lineas[1].split("=")[1])reinicio = lineas[2].split("=")[1] # TODO: genera la referencia de cada registro creado en las fechas siguientes

Ejemplo: Reinicio anual

Entrada

prefijo=EQ/%(year)s/
relleno=4
reinicio=anual
2025-11-03
2025-12-20
2026-01-02
2026-01-15
2026-02-01

Salida esperada

2025-11-03 → EQ/2025/0001
2025-12-20 → EQ/2025/0002
2026-01-02 → EQ/2026/0001
2026-01-15 → EQ/2026/0002
2026-02-01 → EQ/2026/0003

Ejemplo: Reinicio mensual

Entrada

prefijo=MNT%(y)s%(month)s-
relleno=3
reinicio=mensual
2026-09-29
2026-09-30
2026-10-01
2026-10-01
2026-09-30

Salida esperada

2026-09-29 → MNT2609-001
2026-09-30 → MNT2609-002
2026-10-01 → MNT2610-001
2026-10-01 → MNT2610-002
2026-09-30 → MNT2609-003

18. Encuentra el fallo: los céntimos que se pierden

Muy difícil · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

Las facturas se redondean al céntimo y los medios céntimos van hacia arriba (1,525 € pasa a 1,53 €), como en un tique. El programa redondea 1,525 a 1,52 y 2,675 a 2,67. Corrígelo con el módulo decimal, que ya está importado.

Código de partida (python)
from decimal import Decimal, ROUND_HALF_UP lineas = [("Tornillos", 1.15), ("Arandelas", 1.525), ("Tuercas", 2.675)]for nombre, importe in lineas:    print(nombre, round(importe, 2))

Ejemplo: Tres líneas

Salida esperada

Tornillos 1.15
Arandelas 1.53
Tuercas 2.68

Ejercicios de Sistemas de Gestión Empresarial

19. Totales de una factura

Fácil · Ejercicios de Sistemas de Gestión Empresarial · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/ejercicios-sge

Cada línea de la entrada es una línea de factura con el formato «producto;cantidad;precio;iva» (iva en %: 21, 10 o 4). Muestra la base imponible, la cuota de IVA y el total con 2 decimales, con el formato «Base: 100.00», «IVA: 21.00» y «Total: 121.00».

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO

Ejemplo: Factura con dos tipos de IVA

Entrada

Licencia ERP;3;300;21
Formación (hora);10;60;21
Libro de consulta;2;25;4

Salida esperada

Base: 1550.00
IVA: 317.00
Total: 1867.00

Ejemplo: Una sola línea

Entrada

Soporte anual;1;900;21

Salida esperada

Base: 900.00
IVA: 189.00
Total: 1089.00

20. Stock tras los movimientos

Medio · Ejercicios de Sistemas de Gestión Empresarial · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/ejercicios-sge

La entrada tiene dos partes separadas por una línea «---». Primero, el stock inicial: «producto cantidad». Después, los movimientos: «entrada producto cantidad» o «salida producto cantidad». Una salida mayor que el stock disponible no se hace y se muestra «ERROR: stock insuficiente de producto». Al final muestra el stock de cada producto ordenado alfabéticamente con el formato «producto: cantidad». Un producto que llega por una entrada sin estar en el stock inicial empieza en 0.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO

Ejemplo: Entradas, salidas y un error

Entrada

tornillos 500
tuercas 300
arandelas 50
---
salida tornillos 120
entrada arandelas 200
salida tuercas 400
salida tuercas 100
entrada bisagras 40

Salida esperada

ERROR: stock insuficiente de tuercas
arandelas: 250
bisagras: 40
tornillos: 380
tuercas: 200

Ejemplo: Sin movimientos

Entrada

lector 3
portatil 5
---

Salida esperada

lector: 3
portatil: 5

21. Validar NIF y NIE de clientes

Medio · Ejercicios de Sistemas de Gestión Empresarial · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/ejercicios-sge

Antes de dar de alta clientes en el ERP hay que validar su documento. Cada línea es un NIF (8 cifras y una letra) o un NIE (X, Y o Z, 7 cifras y una letra), en mayúsculas o minúsculas. Muestra «DOCUMENTO válido» o «DOCUMENTO no válido», con el documento en mayúsculas. La letra de control es la posición número % 23 de la cadena TRWAGMYFPDXBNJZSQVHLCKE; en los NIE la X, Y o Z se sustituye por 0, 1 o 2 antes de calcularla.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO

Ejemplo: NIF y NIE variados

Entrada

12345678Z
48123456A
X1234567L
Y7654321A
1234567Z
30567891v

Salida esperada

12345678Z válido
48123456A no válido
X1234567L válido
Y7654321A no válido
1234567Z no válido
30567891V válido

22. Informe de antigüedad de deuda

Difícil · Ejercicios de Sistemas de Gestión Empresarial · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/ejercicios-sge

La primera línea es la fecha del informe (AAAA-MM-DD). Las siguientes son facturas pendientes: «factura;cliente;vencimiento;pendiente». Clasifica el importe pendiente por días de retraso (fecha del informe − vencimiento) en los tramos «No vencido» (0 días o menos), «1-30», «31-60», «61-90» y «Más de 90», y muestra todos los tramos en ese orden con el formato «tramo: importe» (2 decimales). Termina con «Mayor deudor vencido: cliente (importe)», el cliente con más importe vencido (retraso > 0), o «Mayor deudor vencido: ninguno».

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO

Ejemplo: Facturas en todos los tramos

Entrada

2025-06-30
F2025-010;Talleres Ebro;2025-07-15;1200
F2025-004;Calzados Cidacos;2025-06-10;850.5
F2025-002;Talleres Ebro;2025-05-20;300
F2025-001;Hotel Los Cameros;2025-04-15;2100
F2024-090;Calzados Cidacos;2025-01-31;640
F2025-011;Muebles Oja;2025-06-30;99.9

Salida esperada

No vencido: 1299.90
1-30: 850.50
31-60: 300.00
61-90: 2100.00
Más de 90: 640.00
Mayor deudor vencido: Hotel Los Cameros (2100.00)

Ejemplo: Nada vencido

Entrada

2025-03-01
F1;Academia Rioja;2025-03-15;400
F2;Farmacia Central;2025-03-01;150

Salida esperada

No vencido: 550.00
1-30: 0.00
31-60: 0.00
61-90: 0.00
Más de 90: 0.00
Mayor deudor vencido: ninguno

Ejercicios largos

23. Corrector de exámenes tipo test

Difícil · Python · 60 minutos · apuntesdam.com/ejercicios/python/corrector-de-examenes-tipo-test

Una profesora pasa sus exámenes tipo test a un formulario y obtiene un fichero de texto con las respuestas de cada alumno. Quiere un programa que los corrija, ordene las notas y le diga qué preguntas han salido peor, para repasarlas en clase.

Cada pregunta tiene cuatro opciones (A, B, C y D) y el alumno puede dejarla en blanco. Para que no compense contestar al azar, cada fallo resta un tercio de acierto: tres fallos anulan una pregunta bien contestada. Las preguntas en blanco no suman ni restan, y la puntuación de un examen nunca puede ser negativa.

Algunas líneas del fichero llegan mal (sin el punto y coma, con respuestas de más o de menos o con letras que no existen): el programa debe avisar de cada una y corregir las demás.

Requisitos

  • La primera línea es la plantilla: CLAVE seguido de las respuestas correctas, por ejemplo CLAVE ABCDA. El número de preguntas N es la longitud de la clave.

  • Cada línea siguiente es un examen: nombre;respuestas, con una letra (A, B, C o D) o un guion (en blanco) por pregunta. Se ignoran las líneas vacías y los espacios al principio y al final de cada parte.

  • Si una línea no tiene ;, escribe Aviso: la línea L no tiene el formato nombre;respuestas, donde L es su número de línea (la de la clave es la 1). Si las respuestas no tienen exactamente N caracteres o alguno no es A, B, C, D ni guion, escribe Aviso: las respuestas de nombre no son válidas y no se corrigen. En los dos casos ese examen no cuenta.

  • Puntuación de un examen: aciertos − fallos ÷ 3, con un mínimo de 0. La nota es esa puntuación × 10 ÷ N.

  • Escribe Corrección de N preguntas y una línea por alumno, de mayor a menor nota (a igual nota, por orden alfabético): la posición en 2 caracteres alineada a la derecha, un punto y un espacio, el nombre en 22 caracteres alineado a la izquierda y la nota con dos decimales y coma, en 6 caracteres a la derecha.

  • Después: Media: X,XX, Aprobados: A de T (aprueba quien tiene un 5 o más), Pregunta más fallada: P (F fallos) (la de más respuestas incorrectas; en blanco no cuenta como fallo; si hay empate, la de número más bajo; «fallo» en singular si es 1; ninguna si nadie ha fallado nada) y Sin aciertos: … con los números de las preguntas que nadie ha acertado separados por coma y espacio, o ninguna.

  • Si no hay ningún examen válido, después de los avisos escribe solo No hay exámenes que corregir.

Formato de la entrada

  • Línea 1: CLAVE respuestas. Resto de líneas: nombre;respuestas (puede haber líneas vacías o mal escritas).

Ejemplo: Cinco exámenes de diez preguntas

Entrada

CLAVE ABCDABCDAB
Lucía Gómez;ABCDABCDAB
Mario Vega;ABCDABCD--
Nerea Paz;ABCAABCDBB
Olga Ruiz;DCBADCBADC
Pedro Sanz;-----ABCDA

Salida esperada

Corrección de 10 preguntas
 1. Lucía Gómez            10,00
 2. Mario Vega              8,00
 3. Nerea Paz               7,33
 4. Olga Ruiz               0,00
 5. Pedro Sanz              0,00
Media: 5,07
Aprobados: 3 de 5
Pregunta más fallada: 9 (3 fallos)
Sin aciertos: ninguna

Ejemplo: Con líneas mal escritas

Entrada

CLAVE CCBA

Ana Ruiz;CCBB
Bruno Díaz;CCB
Carla Mena;CCXA
sin punto y coma
Diego Sola;CA-B
Elena Toro;C-BC

Salida esperada

Aviso: las respuestas de Bruno Díaz no son válidas y no se corrigen
Aviso: las respuestas de Carla Mena no son válidas y no se corrigen
Aviso: la línea 6 no tiene el formato nombre;respuestas
Corrección de 4 preguntas
 1. Ana Ruiz                6,67
 2. Elena Toro              4,17
 3. Diego Sola              0,83
Media: 3,89
Aprobados: 1 de 3
Pregunta más fallada: 4 (3 fallos)
Sin aciertos: 4
Código de partida (python)
import sys  def formato(numero):    """Dos decimales con coma, como se escriben las notas en España."""    return f"{numero:.2f}".replace(".", ",")  def main():    lineas = sys.stdin.read().split("\n")    clave = lineas[0].split()[1]    n = len(clave)    alumnos = []                  # (nombre, nota) de cada examen válido    fallos = [0] * n              # fallos de cada pregunta    aciertos_pregunta = [0] * n   # aciertos de cada pregunta     for num, linea in enumerate(lineas[1:], start=2):        # TODO: valida la línea, corrige el examen y guarda el nombre y la nota        pass     # TODO: clasificación, media, aprobados y estadísticas de las preguntas  main()

24. Informe de ventas a partir de un CSV

Difícil · Python · 70 minutos · apuntesdam.com/ejercicios/python/informe-de-ventas-csv

Una papelería exporta las ventas de su caja registradora a un fichero CSV, con una línea por venta: fecha, producto, categoría, unidades y precio unitario. El dueño quiere un informe rápido sin abrir una hoja de cálculo.

El fichero lo genera una caja antigua y a veces trae líneas rotas: columnas que faltan, unidades escritas con letras o negativas. Esas líneas no deben estropear el informe: se descartan y se dice cuáles eran.

Para que los totales cuadren al céntimo, el informe se calcula en céntimos (números enteros) y solo se convierte a euros al escribirlo. Sumar precios con decimales como 0.1 + 0.2 en coma flotante da errores pequeños que, con muchas ventas, acaban notándose.

Requisitos

  • La primera línea es la cabecera fecha,producto,categoria,unidades,precio y no se procesa. Las líneas vacías se ignoran sin más.

  • Una línea es válida si tiene exactamente 5 campos separados por comas, las unidades son un entero mayor que 0 y el precio es un número no negativo. El importe de la línea es unidades × precio. Las fechas tienen el formato AAAA-MM-DD.

  • Escribe Ventas totales: X € y, bajo Por categoría:, una línea por categoría de mayor a menor importe (a igualdad, alfabético): dos espacios, el nombre en 16 caracteres a la izquierda, el importe en 12 caracteres a la derecha, dos espacios y el porcentaje sobre el total con un decimal en 5 caracteres a la derecha, seguido de %.

  • Producto más vendido: producto (N unidades): el de más unidades vendidas en total (a igualdad, el primero alfabéticamente; «unidad» en singular si es 1). Mejor día: fecha (X €): el día con más importe (a igualdad, el más antiguo).

  • Bajo Por mes:, una línea por mes en orden cronológico: dos espacios, el mes (AAAA-MM), dos espacios, el importe en 12 caracteres a la derecha, un espacio y una almohadilla # por cada 50 € completos (sin espacios al final de la línea).

  • Por último, Líneas descartadas: N y, si hay alguna, sus números de línea entre paréntesis separados por coma y espacio (la cabecera es la línea 1).

  • Los importes se escriben con punto de millar, coma decimal, dos decimales y el símbolo € (por ejemplo 1.234,50 €). Si no hay ninguna venta válida, escribe solo No hay ventas válidas.

Formato de la entrada

  • Un fichero CSV: la cabecera y una venta por línea, con los campos separados por comas y el precio con punto decimal.

Ejemplo: Una semana de ventas

Entrada

fecha,producto,categoria,unidades,precio
2026-08-28,Cuaderno A4,Papelería,10,2.50
2026-08-28,Ratón inalámbrico,Informática,2,18.90
2026-08-31,Bolígrafo azul,Papelería,25,0.80
2026-09-01,Cuaderno A4,Papelería,15,2.50
2026-09-01,Memoria USB 64 GB,Informática,4,12.00
2026-09-02,Mochila,Complementos,3,34.95
2026-09-02,Bolígrafo azul,Papelería,40,0.80
2026-09-03,Ratón inalámbrico,Informática,1,18.90

Salida esperada

Ventas totales: 324,05 €
Por categoría:
  Papelería           114,50 €   35,3 %
  Complementos        104,85 €   32,4 %
  Informática         104,70 €   32,3 %
Producto más vendido: Bolígrafo azul (65 unidades)
Mejor día: 2026-09-02 (136,85 €)
Por mes:
  2026-08       82,80 € #
  2026-09      241,25 € ####
Líneas descartadas: 0

Ejemplo: Con líneas rotas

Entrada

fecha,producto,categoria,unidades,precio
2026-09-10,Cuaderno A4,Papelería,4,2.50
2026-09-10,Grapadora,Papelería,dos,6.40
2026-09-11,Auriculares,Informática,1,29.99
2026-09-11,Funda portátil,Complementos,1
2026-09-12,Bolígrafo azul,Papelería,-3,0.80
2026-09-12,Bolígrafo azul,Papelería,12,0.80

2026-09-13,Auriculares,Informática,1,29.99

Salida esperada

Ventas totales: 79,58 €
Por categoría:
  Informática          59,98 €   75,4 %
  Papelería            19,60 €   24,6 %
Producto más vendido: Bolígrafo azul (12 unidades)
Mejor día: 2026-09-11 (29,99 €)
Por mes:
  2026-09       79,58 € #
Líneas descartadas: 3 (3, 5, 6)
Código de partida (python)
import sys  def euros(centimos):    """1234.5 € → «1.234,50 €»: puntos de millar y coma decimal."""    texto = f"{centimos / 100:,.2f}"    return texto.replace(",", "X").replace(".", ",").replace("X", ".") + " €"  def leer_venta(linea):    """Devuelve (fecha, producto, categoría, unidades, importe en céntimos) o None si la línea no es válida."""    # TODO    return None  def main():    lineas = sys.stdin.read().split("\n")    # TODO: acumula por categoría, producto, día y mes, y escribe el informe  main()

25. Inventario de almacén con valoración FIFO y reposición

Muy difícil · Python · 90 minutos · apuntesdam.com/ejercicios/python/inventario-con-valoracion-fifo

En un ERP como Odoo, el módulo de inventario no solo cuenta unidades: también valora el almacén. Si un tornillo se ha comprado una vez a 0,05 € y otra a 0,06 €, ¿cuánto valen los que quedan y cuánto costaron los vendidos? La respuesta depende del método de valoración, y uno de los más usados es FIFO (primero en entrar, primero en salir): cada salida se lleva primero las unidades del lote más antiguo.

Los movimientos del almacén llegan de varios terminales y no siempre en orden, así que hay que aplicarlos por fecha. Una salida de más unidades de las que hay se rechaza (es un error de la persona que la registró) y no cambia nada.

Además, el ERP propone pedidos: cada producto tiene un stock mínimo y uno máximo, y cuando queda por debajo del mínimo hay que pedir lo necesario para volver al máximo.

Requisitos

  • Cada línea es PRODUCTO código;nombre;mínimo;máximo, ENTRADA fecha;código;cantidad;coste unitario o SALIDA fecha;código;cantidad, con las fechas como AAAA-MM-DD, cantidades enteras mayores que 0 y el coste con punto decimal. Los productos se declaran antes de sus movimientos.

  • Una línea con otro formato, un número no válido, un producto que no existe (o repetido) o un mínimo mayor que el máximo escribe Aviso: línea N ignorada al leerla.

  • Los movimientos se aplican por fecha y, si coinciden, en el orden de la entrada. Una entrada crea un lote con su cantidad y su coste. Una salida con más unidades que el stock escribe Aviso: salida de C código el fecha rechazada (stock S) y no se aplica; si hay stock, consume unidades del lote más antiguo, y cuando se acaba, del siguiente, sumando su coste al coste de lo vendido.

  • Escribe Inventario: y, por cada producto en el orden en que se declararon, código nombre: stock S · valor V · coste de lo vendido C, donde el valor es la suma de las unidades que quedan por el coste de su lote.

  • Después, Reponer: y una línea por producto con stock menor que su mínimo, código nombre: pedir P (stock S, mínimo m, máximo M) con P = máximo − stock; o Reponer: nada. Para terminar, Valor del almacén: X.

  • Los importes se calculan con decimales exactos, se redondean al céntimo solo al escribirlos (las mitades suben) y se escriben con punto de millar, coma decimal y €. Si no hay ningún producto, No hay productos.

Formato de la entrada

  • Una línea por producto o movimiento: PRODUCTO …, ENTRADA … o SALIDA ….

Ejemplo: Tornillos y tuercas

Entrada

PRODUCTO T01;Tornillo M6;100;500
PRODUCTO T02;Tuerca M6;100;400
ENTRADA 2026-09-03;T01;200;0.06
ENTRADA 2026-09-01;T01;300;0.05
SALIDA 2026-09-05;T01;350
ENTRADA 2026-09-02;T02;150;0.035
SALIDA 2026-09-06;T02;80
SALIDA 2026-09-07;T02;100

Salida esperada

Aviso: salida de 100 T02 el 2026-09-07 rechazada (stock 70)
Inventario:
  T01 Tornillo M6: stock 150 · valor 9,00 € · coste de lo vendido 18,00 €
  T02 Tuerca M6: stock 70 · valor 2,45 € · coste de lo vendido 2,80 €
Reponer:
  T02 Tuerca M6: pedir 330 (stock 70, mínimo 100, máximo 400)
Valor del almacén: 11,45 €

Ejemplo: Líneas incorrectas

Entrada

PRODUCTO A;Arandela;10;50
PRODUCTO A;Repetido;1;2
PRODUCTO B;Bisagra;20;10
ENTRADA 2026-10-01;A;40;0.10
SALIDA 2026-10-01;A;15
SALIDA 2026-10-01;A;30
SALIDA 2026-10-02;Z;1
ENTRADA 2026-10-03;A;-5;0.10
AJUSTE 2026-10-03;A;3

Salida esperada

Aviso: línea 2 ignorada
Aviso: línea 3 ignorada
Aviso: línea 7 ignorada
Aviso: línea 8 ignorada
Aviso: línea 9 ignorada
Aviso: salida de 30 A el 2026-10-01 rechazada (stock 25)
Inventario:
  A Arandela: stock 25 · valor 2,50 € · coste de lo vendido 1,50 €
Reponer: nada
Valor del almacén: 2,50 €
Código de partida (python)
import sysfrom decimal import Decimal, ROUND_HALF_UP  def euros(x):    """Decimal → «1.234,56 €» redondeado al céntimo."""    x = x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)    return f"{x:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + " €"  def main():    productos = {}       # código → datos del producto    movimientos = []     # (fecha, orden de la entrada, tipo, código, cantidad, coste)    # TODO: lee productos y movimientos, aplícalos por fecha con FIFO y escribe el inventario  main()