198 lines
6.4 KiB
PHP
198 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace Modules\SupplyChain\Services;
|
|
|
|
use App\Contact;
|
|
use App\Utils\ProductUtil;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Modules\SupplyChain\Models\RfqLine;
|
|
use Modules\SupplyChain\Models\RfqRequest;
|
|
use Modules\SupplyChain\Models\RfqResponse;
|
|
|
|
class RfqService
|
|
{
|
|
public function __construct(
|
|
protected ProductUtil $productUtil
|
|
) {
|
|
}
|
|
|
|
public function create(
|
|
int $businessId,
|
|
array $data,
|
|
array $lines,
|
|
?array $supplierContactIds = null,
|
|
?int $createdBy = null
|
|
): RfqRequest {
|
|
return DB::transaction(function () use ($businessId, $data, $lines, $supplierContactIds, $createdBy) {
|
|
$refCount = $this->productUtil->setAndGetReferenceCount('sc_rfq');
|
|
$refNo = $data['ref_no'] ?? $this->productUtil->generateReferenceNumber('sc_rfq', $refCount);
|
|
|
|
$rfq = RfqRequest::create([
|
|
'business_id' => $businessId,
|
|
'ref_no' => $refNo,
|
|
'title' => $data['title'],
|
|
'status' => $data['status'] ?? 'draft',
|
|
'due_date' => $data['due_date'] ?? null,
|
|
'holding_entity_id' => $data['holding_entity_id'] ?? null,
|
|
'notes' => $data['notes'] ?? null,
|
|
'created_by' => $createdBy ?? auth()->id(),
|
|
]);
|
|
|
|
foreach ($lines as $line) {
|
|
RfqLine::create([
|
|
'rfq_id' => $rfq->id,
|
|
'product_id' => $line['product_id'],
|
|
'variation_id' => $line['variation_id'] ?? null,
|
|
'qty' => $line['qty'],
|
|
'specs' => $line['specs'] ?? null,
|
|
]);
|
|
}
|
|
|
|
$this->inviteSuppliers($rfq, $supplierContactIds);
|
|
|
|
return $rfq->fresh(['lines', 'responses.contact']);
|
|
});
|
|
}
|
|
|
|
public function update(RfqRequest $rfq, array $data, ?array $lines = null): RfqRequest
|
|
{
|
|
return DB::transaction(function () use ($rfq, $data, $lines) {
|
|
$rfq->update([
|
|
'title' => $data['title'] ?? $rfq->title,
|
|
'status' => $data['status'] ?? $rfq->status,
|
|
'due_date' => $data['due_date'] ?? $rfq->due_date,
|
|
'holding_entity_id' => $data['holding_entity_id'] ?? $rfq->holding_entity_id,
|
|
'notes' => $data['notes'] ?? $rfq->notes,
|
|
]);
|
|
|
|
if ($lines !== null) {
|
|
$rfq->lines()->delete();
|
|
foreach ($lines as $line) {
|
|
RfqLine::create([
|
|
'rfq_id' => $rfq->id,
|
|
'product_id' => $line['product_id'],
|
|
'variation_id' => $line['variation_id'] ?? null,
|
|
'qty' => $line['qty'],
|
|
'specs' => $line['specs'] ?? null,
|
|
]);
|
|
}
|
|
}
|
|
|
|
return $rfq->fresh(['lines', 'responses.contact']);
|
|
});
|
|
}
|
|
|
|
public function inviteSuppliers(RfqRequest $rfq, ?array $contactIds = null): int
|
|
{
|
|
if ($contactIds === null) {
|
|
$contactIds = Contact::where('business_id', $rfq->business_id)
|
|
->whereIn('type', ['supplier', 'both'])
|
|
->pluck('id')
|
|
->all();
|
|
}
|
|
|
|
$created = 0;
|
|
foreach ($contactIds as $contactId) {
|
|
$response = RfqResponse::firstOrCreate(
|
|
['rfq_id' => $rfq->id, 'contact_id' => (int) $contactId],
|
|
['status' => 'pending']
|
|
);
|
|
if ($response->wasRecentlyCreated) {
|
|
$created++;
|
|
}
|
|
}
|
|
|
|
return $created;
|
|
}
|
|
|
|
public function submitResponse(
|
|
RfqRequest $rfq,
|
|
int $contactId,
|
|
float $quotedPrice,
|
|
int $leadDays,
|
|
?string $notes = null
|
|
): RfqResponse {
|
|
return RfqResponse::updateOrCreate(
|
|
['rfq_id' => $rfq->id, 'contact_id' => $contactId],
|
|
[
|
|
'quoted_price' => $quotedPrice,
|
|
'lead_days' => $leadDays,
|
|
'status' => 'submitted',
|
|
'notes' => $notes,
|
|
]
|
|
);
|
|
}
|
|
|
|
public function award(RfqRequest $rfq, int $contactId): RfqRequest
|
|
{
|
|
return DB::transaction(function () use ($rfq, $contactId) {
|
|
$rfq->responses()->update(['status' => 'rejected']);
|
|
RfqResponse::where('rfq_id', $rfq->id)
|
|
->where('contact_id', $contactId)
|
|
->update(['status' => 'awarded']);
|
|
|
|
$rfq->update(['status' => 'awarded']);
|
|
|
|
return $rfq->fresh(['lines', 'responses.contact']);
|
|
});
|
|
}
|
|
|
|
public function open(RfqRequest $rfq): RfqRequest
|
|
{
|
|
$rfq->update(['status' => 'open']);
|
|
|
|
return $rfq->fresh(['lines', 'responses.contact']);
|
|
}
|
|
|
|
public function close(RfqRequest $rfq): RfqRequest
|
|
{
|
|
$rfq->update(['status' => 'closed']);
|
|
|
|
return $rfq->fresh(['lines', 'responses.contact']);
|
|
}
|
|
|
|
/**
|
|
* Create RFQ from MRP shortage lines.
|
|
*
|
|
* @param array<int, array<string, mixed>> $shortages
|
|
*/
|
|
public function createFromMrpShortages(
|
|
int $businessId,
|
|
array $shortages,
|
|
?string $title = null,
|
|
?array $supplierContactIds = null
|
|
): RfqRequest {
|
|
$lines = [];
|
|
foreach ($shortages as $shortage) {
|
|
$productId = ! empty($shortage['product_id'])
|
|
? (int) $shortage['product_id']
|
|
: null;
|
|
|
|
if (! $productId && ! empty($shortage['variation_id'])) {
|
|
$productId = (int) \App\Variation::where('id', (int) $shortage['variation_id'])->value('product_id');
|
|
}
|
|
|
|
if (empty($shortage['variation_id']) || ! $productId) {
|
|
continue;
|
|
}
|
|
$lines[] = [
|
|
'product_id' => $productId,
|
|
'variation_id' => (int) $shortage['variation_id'],
|
|
'qty' => (float) ($shortage['shortage_qty'] ?? $shortage['qty'] ?? 0),
|
|
'specs' => $shortage['item_name'] ?? null,
|
|
];
|
|
}
|
|
|
|
return $this->create(
|
|
$businessId,
|
|
[
|
|
'title' => $title ?? __('supplychain::lang.rfq_from_mrp'),
|
|
'status' => 'open',
|
|
'due_date' => now()->addDays(14)->toDateString(),
|
|
],
|
|
$lines,
|
|
$supplierContactIds
|
|
);
|
|
}
|
|
}
|