219 lines
8.3 KiB
JavaScript
219 lines
8.3 KiB
JavaScript
"use strict";
|
|
/**
|
|
* ComparativoService - Servicio de comparativos de cotizaciones
|
|
*
|
|
* Gestión de cuadros comparativos para selección de proveedores.
|
|
*
|
|
* @module Purchase
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.ComparativoService = void 0;
|
|
class ComparativoService {
|
|
comparativoRepository;
|
|
proveedorRepository;
|
|
productoRepository;
|
|
constructor(comparativoRepository, proveedorRepository, productoRepository) {
|
|
this.comparativoRepository = comparativoRepository;
|
|
this.proveedorRepository = proveedorRepository;
|
|
this.productoRepository = productoRepository;
|
|
}
|
|
generateCode() {
|
|
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 `CMP-${year}${month}-${random}`;
|
|
}
|
|
async findWithFilters(ctx, filters = {}, page = 1, limit = 20) {
|
|
const skip = (page - 1) * limit;
|
|
const queryBuilder = this.comparativoRepository
|
|
.createQueryBuilder('comp')
|
|
.leftJoinAndSelect('comp.requisicion', 'requisicion')
|
|
.leftJoinAndSelect('comp.createdBy', 'createdBy')
|
|
.where('comp.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
|
.andWhere('comp.deleted_at IS NULL');
|
|
if (filters.requisicionId) {
|
|
queryBuilder.andWhere('comp.requisicion_id = :requisicionId', {
|
|
requisicionId: filters.requisicionId,
|
|
});
|
|
}
|
|
if (filters.status) {
|
|
queryBuilder.andWhere('comp.status = :status', { status: filters.status });
|
|
}
|
|
if (filters.dateFrom) {
|
|
queryBuilder.andWhere('comp.comparison_date >= :dateFrom', { dateFrom: filters.dateFrom });
|
|
}
|
|
if (filters.dateTo) {
|
|
queryBuilder.andWhere('comp.comparison_date <= :dateTo', { dateTo: filters.dateTo });
|
|
}
|
|
queryBuilder
|
|
.orderBy('comp.comparison_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.comparativoRepository.findOne({
|
|
where: {
|
|
id,
|
|
tenantId: ctx.tenantId,
|
|
deletedAt: null,
|
|
},
|
|
});
|
|
}
|
|
async findWithDetails(ctx, id) {
|
|
return this.comparativoRepository.findOne({
|
|
where: {
|
|
id,
|
|
tenantId: ctx.tenantId,
|
|
deletedAt: null,
|
|
},
|
|
relations: [
|
|
'requisicion',
|
|
'createdBy',
|
|
'approvedBy',
|
|
'proveedores',
|
|
'proveedores.productos',
|
|
],
|
|
});
|
|
}
|
|
async create(ctx, dto) {
|
|
const comparativo = this.comparativoRepository.create({
|
|
tenantId: ctx.tenantId,
|
|
createdById: ctx.userId,
|
|
code: this.generateCode(),
|
|
requisicionId: dto.requisicionId,
|
|
name: dto.name,
|
|
comparisonDate: dto.comparisonDate,
|
|
notes: dto.notes,
|
|
status: 'draft',
|
|
});
|
|
return this.comparativoRepository.save(comparativo);
|
|
}
|
|
async addProveedor(ctx, comparativoId, dto) {
|
|
const comparativo = await this.findById(ctx, comparativoId);
|
|
if (!comparativo) {
|
|
throw new Error('Comparativo not found');
|
|
}
|
|
if (comparativo.status !== 'draft' && comparativo.status !== 'in_evaluation') {
|
|
throw new Error('Cannot add suppliers to approved or cancelled comparisons');
|
|
}
|
|
const proveedor = this.proveedorRepository.create({
|
|
tenantId: ctx.tenantId,
|
|
createdById: ctx.userId,
|
|
comparativoId,
|
|
supplierId: dto.supplierId,
|
|
quotationNumber: dto.quotationNumber,
|
|
quotationDate: dto.quotationDate,
|
|
deliveryDays: dto.deliveryDays,
|
|
paymentConditions: dto.paymentConditions,
|
|
evaluationNotes: dto.evaluationNotes,
|
|
});
|
|
return this.proveedorRepository.save(proveedor);
|
|
}
|
|
async addProducto(ctx, proveedorId, dto) {
|
|
const proveedor = await this.proveedorRepository.findOne({
|
|
where: { id: proveedorId, tenantId: ctx.tenantId },
|
|
relations: ['comparativo'],
|
|
});
|
|
if (!proveedor) {
|
|
throw new Error('Supplier entry not found');
|
|
}
|
|
if (proveedor.comparativo.status !== 'draft' && proveedor.comparativo.status !== 'in_evaluation') {
|
|
throw new Error('Cannot add products to approved or cancelled comparisons');
|
|
}
|
|
const producto = this.productoRepository.create({
|
|
tenantId: ctx.tenantId,
|
|
createdById: ctx.userId,
|
|
comparativoProveedorId: proveedorId,
|
|
productId: dto.productId,
|
|
quantity: dto.quantity,
|
|
unitPrice: dto.unitPrice,
|
|
notes: dto.notes,
|
|
});
|
|
return this.productoRepository.save(producto);
|
|
}
|
|
async startEvaluation(ctx, id) {
|
|
const comparativo = await this.findWithDetails(ctx, id);
|
|
if (!comparativo) {
|
|
return null;
|
|
}
|
|
if (comparativo.status !== 'draft') {
|
|
throw new Error('Can only start evaluation on draft comparisons');
|
|
}
|
|
if (!comparativo.proveedores || comparativo.proveedores.length < 2) {
|
|
throw new Error('Need at least 2 suppliers to start evaluation');
|
|
}
|
|
comparativo.status = 'in_evaluation';
|
|
return this.comparativoRepository.save(comparativo);
|
|
}
|
|
async selectWinner(ctx, id, supplierId) {
|
|
const comparativo = await this.findWithDetails(ctx, id);
|
|
if (!comparativo) {
|
|
return null;
|
|
}
|
|
if (comparativo.status !== 'in_evaluation') {
|
|
throw new Error('Can only select winner during evaluation');
|
|
}
|
|
// Verify supplier is in the comparison
|
|
const proveedor = comparativo.proveedores?.find((p) => p.supplierId === supplierId);
|
|
if (!proveedor) {
|
|
throw new Error('Supplier not found in this comparison');
|
|
}
|
|
// Mark all as not selected, then mark winner
|
|
for (const p of comparativo.proveedores || []) {
|
|
p.isSelected = p.supplierId === supplierId;
|
|
await this.proveedorRepository.save(p);
|
|
}
|
|
comparativo.winnerSupplierId = supplierId;
|
|
comparativo.status = 'approved';
|
|
comparativo.approvedById = ctx.userId || '';
|
|
comparativo.approvedAt = new Date();
|
|
return this.comparativoRepository.save(comparativo);
|
|
}
|
|
async cancel(ctx, id) {
|
|
const comparativo = await this.findById(ctx, id);
|
|
if (!comparativo) {
|
|
return null;
|
|
}
|
|
if (comparativo.status === 'approved') {
|
|
throw new Error('Cannot cancel approved comparisons');
|
|
}
|
|
comparativo.status = 'cancelled';
|
|
return this.comparativoRepository.save(comparativo);
|
|
}
|
|
async updateProveedorTotal(ctx, proveedorId) {
|
|
const proveedor = await this.proveedorRepository.findOne({
|
|
where: { id: proveedorId, tenantId: ctx.tenantId },
|
|
relations: ['productos'],
|
|
});
|
|
if (!proveedor) {
|
|
return null;
|
|
}
|
|
const total = proveedor.productos?.reduce((sum, p) => sum + (p.quantity * p.unitPrice), 0) || 0;
|
|
proveedor.totalAmount = total;
|
|
return this.proveedorRepository.save(proveedor);
|
|
}
|
|
async softDelete(ctx, id) {
|
|
const comparativo = await this.findById(ctx, id);
|
|
if (!comparativo) {
|
|
return false;
|
|
}
|
|
if (comparativo.status === 'approved') {
|
|
throw new Error('Cannot delete approved comparisons');
|
|
}
|
|
await this.comparativoRepository.update({ id, tenantId: ctx.tenantId }, { deletedAt: new Date(), deletedById: ctx.userId });
|
|
return true;
|
|
}
|
|
}
|
|
exports.ComparativoService = ComparativoService;
|
|
//# sourceMappingURL=comparativo.service.js.map
|