247 lines
9.2 KiB
JavaScript
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
|