erp-construccion-backend/dist/modules/inventory/services/requisicion.service.js

247 lines
9.2 KiB
JavaScript

"use strict";
/**
* RequisicionService - Servicio de requisiciones de obra
*
* Gestión de requisiciones de material con workflow:
* draft -> submitted -> approved -> partially_served -> served
*
* @module Inventory
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RequisicionService = void 0;
class RequisicionService {
requisicionRepository;
lineaRepository;
constructor(requisicionRepository, lineaRepository) {
this.requisicionRepository = requisicionRepository;
this.lineaRepository = lineaRepository;
}
generateNumber() {
const now = new Date();
const year = now.getFullYear().toString().slice(-2);
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
return `REQ-${year}${month}-${random}`;
}
async findWithFilters(ctx, filters = {}, page = 1, limit = 20) {
const skip = (page - 1) * limit;
const queryBuilder = this.requisicionRepository
.createQueryBuilder('req')
.leftJoinAndSelect('req.fraccionamiento', 'fraccionamiento')
.leftJoinAndSelect('req.requestedBy', 'requestedBy')
.where('req.tenant_id = :tenantId', { tenantId: ctx.tenantId })
.andWhere('req.deleted_at IS NULL');
if (filters.fraccionamientoId) {
queryBuilder.andWhere('req.fraccionamiento_id = :fraccionamientoId', {
fraccionamientoId: filters.fraccionamientoId,
});
}
if (filters.status) {
queryBuilder.andWhere('req.status = :status', { status: filters.status });
}
if (filters.priority) {
queryBuilder.andWhere('req.priority = :priority', { priority: filters.priority });
}
if (filters.dateFrom) {
queryBuilder.andWhere('req.requisition_date >= :dateFrom', { dateFrom: filters.dateFrom });
}
if (filters.dateTo) {
queryBuilder.andWhere('req.requisition_date <= :dateTo', { dateTo: filters.dateTo });
}
queryBuilder
.orderBy('req.requisition_date', 'DESC')
.skip(skip)
.take(limit);
const [data, total] = await queryBuilder.getManyAndCount();
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
async findById(ctx, id) {
return this.requisicionRepository.findOne({
where: {
id,
tenantId: ctx.tenantId,
deletedAt: null,
},
});
}
async findWithDetails(ctx, id) {
return this.requisicionRepository.findOne({
where: {
id,
tenantId: ctx.tenantId,
deletedAt: null,
},
relations: [
'fraccionamiento',
'requestedBy',
'approvedBy',
'lineas',
'lineas.concepto',
'lineas.lote',
],
});
}
async create(ctx, dto) {
if (!ctx.userId) {
throw new Error('User ID is required to create a requisition');
}
const requisicion = this.requisicionRepository.create({
tenantId: ctx.tenantId,
createdById: ctx.userId,
requestedById: ctx.userId,
requisitionNumber: this.generateNumber(),
fraccionamientoId: dto.fraccionamientoId,
requisitionDate: dto.requisitionDate,
requiredDate: dto.requiredDate,
priority: dto.priority || 'medium',
destinationWarehouseId: dto.destinationWarehouseId,
notes: dto.notes,
status: 'draft',
});
return this.requisicionRepository.save(requisicion);
}
async addLinea(ctx, requisicionId, dto) {
const requisicion = await this.findById(ctx, requisicionId);
if (!requisicion) {
throw new Error('Requisicion not found');
}
if (requisicion.status !== 'draft') {
throw new Error('Can only add lines to draft requisitions');
}
const linea = this.lineaRepository.create({
tenantId: ctx.tenantId,
createdById: ctx.userId,
requisicionId,
productId: dto.productId,
conceptoId: dto.conceptoId,
loteId: dto.loteId,
quantityRequested: dto.quantityRequested,
unitId: dto.unitId,
notes: dto.notes,
});
return this.lineaRepository.save(linea);
}
async removeLinea(ctx, requisicionId, lineaId) {
const requisicion = await this.findById(ctx, requisicionId);
if (!requisicion) {
return false;
}
if (requisicion.status !== 'draft') {
throw new Error('Can only remove lines from draft requisitions');
}
const result = await this.lineaRepository.delete({
id: lineaId,
requisicionId,
tenantId: ctx.tenantId,
});
return (result.affected ?? 0) > 0;
}
async submit(ctx, id) {
const requisicion = await this.findWithDetails(ctx, id);
if (!requisicion) {
return null;
}
if (requisicion.status !== 'draft') {
throw new Error('Can only submit draft requisitions');
}
if (!requisicion.lineas || requisicion.lineas.length === 0) {
throw new Error('Cannot submit requisition without lines');
}
requisicion.status = 'submitted';
return this.requisicionRepository.save(requisicion);
}
async approve(ctx, id) {
const requisicion = await this.findWithDetails(ctx, id);
if (!requisicion) {
return null;
}
if (requisicion.status !== 'submitted') {
throw new Error('Can only approve submitted requisitions');
}
requisicion.status = 'approved';
requisicion.approvedById = ctx.userId || '';
requisicion.approvedAt = new Date();
// Set approved quantities to requested quantities by default
for (const linea of requisicion.lineas) {
linea.quantityApproved = linea.quantityRequested;
await this.lineaRepository.save(linea);
}
return this.requisicionRepository.save(requisicion);
}
async reject(ctx, id, reason) {
const requisicion = await this.findById(ctx, id);
if (!requisicion) {
return null;
}
if (requisicion.status !== 'submitted') {
throw new Error('Can only reject submitted requisitions');
}
requisicion.status = 'cancelled';
requisicion.rejectionReason = reason;
return this.requisicionRepository.save(requisicion);
}
async cancel(ctx, id) {
const requisicion = await this.findById(ctx, id);
if (!requisicion) {
return null;
}
if (requisicion.status === 'served' || requisicion.status === 'cancelled') {
throw new Error('Cannot cancel served or already cancelled requisitions');
}
requisicion.status = 'cancelled';
return this.requisicionRepository.save(requisicion);
}
async updateServedQuantity(ctx, lineaId, quantityServed) {
const linea = await this.lineaRepository.findOne({
where: { id: lineaId, tenantId: ctx.tenantId },
relations: ['requisicion'],
});
if (!linea || !linea.requisicion) {
return null;
}
if (linea.requisicion.status !== 'approved' && linea.requisicion.status !== 'partially_served') {
throw new Error('Can only serve approved or partially served requisitions');
}
linea.quantityServed = quantityServed;
await this.lineaRepository.save(linea);
// Update requisition status based on all lines
await this.updateRequisitionStatus(ctx, linea.requisicion.id);
return linea;
}
async updateRequisitionStatus(ctx, requisicionId) {
const requisicion = await this.findWithDetails(ctx, requisicionId);
if (!requisicion || !requisicion.lineas) {
return;
}
const allServed = requisicion.lineas.every((l) => l.quantityServed >= (l.quantityApproved || l.quantityRequested));
const someServed = requisicion.lineas.some((l) => l.quantityServed > 0);
if (allServed) {
requisicion.status = 'served';
}
else if (someServed) {
requisicion.status = 'partially_served';
}
await this.requisicionRepository.save(requisicion);
}
async softDelete(ctx, id) {
const requisicion = await this.findById(ctx, id);
if (!requisicion) {
return false;
}
if (requisicion.status !== 'draft' && requisicion.status !== 'cancelled') {
throw new Error('Can only delete draft or cancelled requisitions');
}
await this.requisicionRepository.update({ id, tenantId: ctx.tenantId }, { deletedAt: new Date(), deletedById: ctx.userId });
return true;
}
}
exports.RequisicionService = RequisicionService;
//# sourceMappingURL=requisicion.service.js.map