chore: initial commit of existing codebase
This commit is contained in:
commit
11e94889b2
292 files changed
+38586
No files matched your search
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
class GenerateItemTemplate extends Command
|
||||
{
|
||||
protected $signature = 'items:template {--output= : Save path}';
|
||||
protected $description = 'Generate a clean Excel template for importing inventory items';
|
||||
|
||||
private array $columns = [
|
||||
['item_name *', 'SUPERBOND F5 White 25kg', 36],
|
||||
['unit_of_measure', 'BAG', 16],
|
||||
['category', 'finished_good', 20],
|
||||
['cost_price', '0.00', 14],
|
||||
['minimum_stock_level', '0', 20],
|
||||
['description', '', 36],
|
||||
['is_active', 'yes', 12],
|
||||
];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$outputPath = $this->option('output') ?? storage_path('app/items_template.xlsx');
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Items');
|
||||
|
||||
// Headers
|
||||
foreach ($this->columns as $i => [$label]) {
|
||||
$sheet->getCell(Coordinate::stringFromColumnIndex($i + 1) . '1')->setValue($label);
|
||||
}
|
||||
|
||||
$lastCol = Coordinate::stringFromColumnIndex(count($this->columns));
|
||||
$sheet->getStyle("A1:{$lastCol}1")->applyFromArray([
|
||||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF'], 'size' => 11],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '0f172a']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'FFFFFF']]],
|
||||
]);
|
||||
$sheet->getRowDimension(1)->setRowHeight(22);
|
||||
|
||||
// Example rows
|
||||
$examples = [
|
||||
['SUPERBOND F5 White 25kg', 'BAG', 'finished_good', '0.00', '0', '', 'yes'],
|
||||
['SUPERBOND F5 Grey 25kg', 'BAG', 'finished_good', '0.00', '0', '', 'yes'],
|
||||
['Pentaproof 20 P', 'KG', 'raw_material', '1.09', '0', '', 'yes'],
|
||||
['JOLES YELLOW OCHRE', 'KG', 'raw_material', '1.32', '0', '', 'yes'],
|
||||
['Silica Sand', 'KG', 'raw_material', '0.01', '0', '', 'yes'],
|
||||
];
|
||||
|
||||
foreach ($examples as $rowIdx => $values) {
|
||||
$excelRow = $rowIdx + 2;
|
||||
foreach ($values as $colIdx => $value) {
|
||||
$sheet->getCell(Coordinate::stringFromColumnIndex($colIdx + 1) . $excelRow)->setValue($value);
|
||||
}
|
||||
}
|
||||
|
||||
$lastDataRow = count($examples) + 1;
|
||||
$sheet->getStyle("A2:{$lastCol}{$lastDataRow}")->applyFromArray([
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'F8FAFC']],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'CBD5E1']]],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
// Notes
|
||||
$notesRow = $lastDataRow + 2;
|
||||
$sheet->getCell("A{$notesRow}")->setValue(
|
||||
'* Required. category must be: finished_good, raw_material, or wip. Items with "Not in Use" in the name are imported as inactive. Delete example rows before importing.'
|
||||
);
|
||||
$sheet->mergeCells("A{$notesRow}:{$lastCol}{$notesRow}");
|
||||
$sheet->getStyle("A{$notesRow}")->applyFromArray([
|
||||
'font' => ['italic' => true, 'color' => ['rgb' => '6B7280'], 'size' => 9],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FEF9C3']],
|
||||
]);
|
||||
|
||||
// Column widths
|
||||
foreach ($this->columns as $i => [, , $width]) {
|
||||
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($i + 1))->setWidth($width);
|
||||
}
|
||||
|
||||
$dir = dirname($outputPath);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
(new Xlsx($spreadsheet))->save($outputPath);
|
||||
|
||||
$this->info("Template saved to: {$outputPath}");
|
||||
$this->line("Usage: php artisan items:import \"<path-to-file.xlsx>\"");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
class GenerateSupplierTemplate extends Command
|
||||
{
|
||||
protected $signature = 'suppliers:template
|
||||
{--output= : Save path (default: storage/app/suppliers_template.xlsx)}';
|
||||
|
||||
protected $description = 'Generate a clean Excel template for importing suppliers';
|
||||
|
||||
/** [header label, example value, width] */
|
||||
private array $columns = [
|
||||
['Supplier ID', 'SUP-1006', 16],
|
||||
['Company Name *', 'SAFETY CHEMICAL TRADING W.L.L', 38],
|
||||
['Category', 'Chemical', 22],
|
||||
['Contact Person', 'Ali Hassan', 24],
|
||||
['Primary Email', '[email protected]', 30],
|
||||
['Secondary Email', '', 26],
|
||||
['Phone 1', '+97333188311', 18],
|
||||
['Phone 2', '', 18],
|
||||
['WhatsApp Number', '+97334214947', 18],
|
||||
['Address', 'Manama, Bahrain', 32],
|
||||
['Website', 'https://example.com', 28],
|
||||
['Credit (Y/N)', 'Y', 14],
|
||||
['Credit Days', '30', 14],
|
||||
['Tax Number', 'TRN100000000006', 22],
|
||||
['Is Active', 'Yes', 12],
|
||||
['Remarks / Key Details','', 38],
|
||||
];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$outputPath = $this->option('output')
|
||||
?? storage_path('app/suppliers_template.xlsx');
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Suppliers');
|
||||
|
||||
$lastCol = Coordinate::stringFromColumnIndex(count($this->columns));
|
||||
|
||||
// ── Header row ────────────────────────────────────────────────────────
|
||||
foreach ($this->columns as $i => [$label]) {
|
||||
$sheet->getCell(Coordinate::stringFromColumnIndex($i + 1) . '1')->setValue($label);
|
||||
}
|
||||
|
||||
$sheet->getStyle("A1:{$lastCol}1")->applyFromArray([
|
||||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF'], 'size' => 11],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '1D4ED8']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'FFFFFF']]],
|
||||
]);
|
||||
$sheet->getRowDimension(1)->setRowHeight(22);
|
||||
|
||||
// ── Example rows ──────────────────────────────────────────────────────
|
||||
$examples = [
|
||||
['SUP-1006', 'SAFETY CHEMICAL TRADING W.L.L', 'Chemical', 'Ali Hassan', '[email protected]', '', '+973 3318 8311', '', '+973 3421 4947', 'Manama, Bahrain', '', 'Y', '30', 'TRN100000000006', 'Yes', ''],
|
||||
['SUP-1007', 'The prosperity trading & Cont SPC', 'Chemical', 'Sara Al-Ali', '[email protected]', '', '+973 17730001', '', '', 'Riffa, Bahrain', '', '', '', 'TRN100000000007', 'Yes', ''],
|
||||
['SUP-1008', 'Al Eradah chemicals', 'Chemical', 'Mohammed Naser', '[email protected]', '', '+973 1741 3720', '', '', 'Hamad Town, Bahrain','', '', '', '', 'Yes', ''],
|
||||
];
|
||||
|
||||
foreach ($examples as $rowIdx => $values) {
|
||||
$excelRow = $rowIdx + 2;
|
||||
foreach ($values as $colIdx => $value) {
|
||||
$sheet->getCell(Coordinate::stringFromColumnIndex($colIdx + 1) . $excelRow)->setValue($value);
|
||||
}
|
||||
}
|
||||
|
||||
$lastDataRow = count($examples) + 1;
|
||||
$sheet->getStyle("A2:{$lastCol}{$lastDataRow}")->applyFromArray([
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'EFF6FF']],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'BFDBFE']]],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
$sheet->getStyle("B2:B{$lastDataRow}")->applyFromArray([
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'DBEAFE']],
|
||||
]);
|
||||
for ($r = 2; $r <= $lastDataRow; $r++) {
|
||||
$sheet->getRowDimension($r)->setRowHeight(18);
|
||||
}
|
||||
|
||||
// ── Notes row ─────────────────────────────────────────────────────────
|
||||
$notesRow = $lastDataRow + 2;
|
||||
$sheet->getCell("A{$notesRow}")->setValue(
|
||||
'* Required. Delete example rows before importing. Duplicate Company Names are skipped. ' .
|
||||
'Supplier ID is optional — leave blank and one will be auto-assigned. ' .
|
||||
'Category, Secondary Email, Phone 2, WhatsApp, Website, Credit, Remarks are informational only.'
|
||||
);
|
||||
$sheet->mergeCells("A{$notesRow}:{$lastCol}{$notesRow}");
|
||||
$sheet->getStyle("A{$notesRow}")->applyFromArray([
|
||||
'font' => ['italic' => true, 'color' => ['rgb' => '6B7280'], 'size' => 9],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FEF9C3']],
|
||||
]);
|
||||
$sheet->getRowDimension($notesRow)->setRowHeight(30);
|
||||
$sheet->getStyle("A{$notesRow}")->getAlignment()->setWrapText(true);
|
||||
|
||||
// ── Column widths ─────────────────────────────────────────────────────
|
||||
foreach ($this->columns as $i => [, , $width]) {
|
||||
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($i + 1))->setWidth($width);
|
||||
}
|
||||
|
||||
$dir = dirname($outputPath);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
(new Xlsx($spreadsheet))->save($outputPath);
|
||||
|
||||
$this->info("Template saved to: {$outputPath}");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Console\Command;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
|
||||
class ImportSuppliers extends Command
|
||||
{
|
||||
protected $signature = 'suppliers:import
|
||||
{file : Full path to the Excel file (.xlsx or .xls)}
|
||||
{--dry-run : Preview what would be imported without saving}';
|
||||
|
||||
protected $description = 'Import suppliers from an Excel file. Supports both the MRF comparison format and the clean suppliers template. Duplicate names are skipped automatically.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$filePath = $this->argument('file');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
$this->error("File not found: {$filePath}");
|
||||
$this->line("Tip: Use an absolute path, e.g. php artisan suppliers:import \"C:\\path\\to\\file.xlsx\"");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("Reading: " . basename($filePath));
|
||||
|
||||
try {
|
||||
$spreadsheet = IOFactory::load($filePath);
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Could not open file: " . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$format = $this->detectFormat($spreadsheet);
|
||||
$this->line("Detected format: <comment>{$format}</comment>");
|
||||
|
||||
$suppliers = $format === 'mrf'
|
||||
? $this->extractFromMrf($spreadsheet)
|
||||
: $this->extractFromTemplate($spreadsheet);
|
||||
|
||||
if (empty($suppliers)) {
|
||||
$this->warn("No suppliers found in file.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->line(str_pad('', 60, '-'));
|
||||
$this->line(sprintf(" %-30s %-10s %s", 'Name', 'Status', 'Notes'));
|
||||
$this->line(str_pad('', 60, '-'));
|
||||
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($suppliers as $data) {
|
||||
$name = trim($data['name'] ?? '');
|
||||
|
||||
if (empty($name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = Supplier::whereRaw('LOWER(name) = ?', [strtolower($name)])->exists();
|
||||
|
||||
if ($exists) {
|
||||
$this->line(sprintf(" %-30s <fg=yellow>SKIP</> already in database", substr($name, 0, 30)));
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
Supplier::create([
|
||||
'name' => $name,
|
||||
'contact_person' => $data['contact_person'] ?? null ?: null,
|
||||
'email' => $data['email'] ?? null ?: null,
|
||||
'phone' => $data['phone'] ?? null ?: null,
|
||||
'address' => $data['address'] ?? null ?: null,
|
||||
'tax_number' => $data['tax_number'] ?? null ?: null,
|
||||
'is_active' => $this->parseBoolean($data['is_active'] ?? 'yes'),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->line(sprintf(" %-30s <fg=green>%s</> %s",
|
||||
substr($name, 0, 30),
|
||||
$dryRun ? 'PREVIEW' : 'IMPORTED',
|
||||
isset($data['email']) && $data['email'] ? $data['email'] : ''
|
||||
));
|
||||
$imported++;
|
||||
}
|
||||
|
||||
$this->line(str_pad('', 60, '-'));
|
||||
$this->newLine();
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info("DRY RUN complete — nothing was saved.");
|
||||
$this->line("Would import: {$imported} | Would skip: {$skipped}");
|
||||
} else {
|
||||
$this->info("Import complete!");
|
||||
$this->line("Imported: <fg=green>{$imported}</> | Skipped (duplicates): <fg=yellow>{$skipped}</>");
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the file is an MRF comparison sheet or a clean supplier template.
|
||||
* MRF sheets have "S.No" in cell A4.
|
||||
*/
|
||||
private function detectFormat(\PhpOffice\PhpSpreadsheet\Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$a4 = strtolower(trim((string) $sheet->getCell('A4')->getValue()));
|
||||
|
||||
return $a4 === 's.no' ? 'mrf' : 'template';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract supplier names from a Material Purchase Request comparison sheet.
|
||||
* Suppliers appear as column headers in row 4, starting from column G.
|
||||
*/
|
||||
private function extractFromMrf(\PhpOffice\PhpSpreadsheet\Spreadsheet $spreadsheet): array
|
||||
{
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$headerRow = 4;
|
||||
$startCol = 7; // Column G
|
||||
$stopWords = ['comments if any', 'lowest price', 'avg price', 'recommendation'];
|
||||
$suppliers = [];
|
||||
|
||||
for ($col = $startCol; $col <= 50; $col++) {
|
||||
$cellCoord = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col) . $headerRow;
|
||||
$value = trim((string) $sheet->getCell($cellCoord)->getValue());
|
||||
|
||||
if (empty($value)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (in_array(strtolower($value), $stopWords)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$suppliers[] = ['name' => $value];
|
||||
}
|
||||
|
||||
return $suppliers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract supplier rows from the clean import template.
|
||||
* Row 1 must be headers matching: name, contact_person, email, phone, address, tax_number, is_active
|
||||
*/
|
||||
private function extractFromTemplate(\PhpOffice\PhpSpreadsheet\Spreadsheet $spreadsheet): array
|
||||
{
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$rows = $sheet->toArray(null, true, true, false);
|
||||
$suppliers = [];
|
||||
|
||||
if (empty($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Normalise header row: strip asterisks/underscores, lowercase
|
||||
$headers = array_map(
|
||||
fn($h) => strtolower(trim(str_replace(['*', '_'], [' ', ' '], (string) $h))),
|
||||
$rows[0]
|
||||
);
|
||||
|
||||
// Aliases: normalised header text → internal field name (supports both old and new formats)
|
||||
$aliases = [
|
||||
'name' => 'name',
|
||||
'company name' => 'name',
|
||||
'contact person' => 'contact_person',
|
||||
'email' => 'email',
|
||||
'primary email' => 'email',
|
||||
'phone' => 'phone',
|
||||
'phone 1' => 'phone',
|
||||
'address' => 'address',
|
||||
'tax number' => 'tax_number',
|
||||
'is active' => 'is_active',
|
||||
];
|
||||
|
||||
$map = [];
|
||||
foreach ($headers as $idx => $header) {
|
||||
if (isset($aliases[$header]) && !isset($map[$aliases[$header]])) {
|
||||
$map[$aliases[$header]] = $idx;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($map['name'])) {
|
||||
$this->error('Template is missing a recognised name column ("name", "Company Name") in row 1.');
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach (array_slice($rows, 1) as $row) {
|
||||
$name = trim((string) ($row[$map['name']] ?? ''));
|
||||
|
||||
// Skip empty rows and the notes/instruction row
|
||||
if (empty($name) || str_starts_with($name, '*')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = ['name' => $name];
|
||||
|
||||
foreach (['contact_person', 'email', 'phone', 'address', 'tax_number', 'is_active'] as $field) {
|
||||
if (isset($map[$field])) {
|
||||
$entry[$field] = trim((string) ($row[$map[$field]] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
$suppliers[] = $entry;
|
||||
}
|
||||
|
||||
return $suppliers;
|
||||
}
|
||||
|
||||
private function parseBoolean(string $value): bool
|
||||
{
|
||||
return in_array(strtolower(trim($value)), ['yes', 'true', '1', 'active', 'y']);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user