| @@ -0,0 +1,47 @@ | |||
| <?php | |||
| namespace App\Files\PDF; | |||
| use App\Files\TmpFile; | |||
| abstract class PDFFile extends TmpFile | |||
| { | |||
| protected const DIR = ['pdf']; | |||
| private string $appFileName = ""; | |||
| public function __construct(?string $id = null) | |||
| { | |||
| parent::__construct($id); | |||
| } | |||
| /** | |||
| * @override | |||
| */ | |||
| public function getFileExtension(): string | |||
| { | |||
| return "pdf"; | |||
| } | |||
| /** | |||
| * @override | |||
| */ | |||
| public function getMimeType(): string | |||
| { | |||
| return "application/pdf"; | |||
| } | |||
| /** | |||
| * @override | |||
| */ | |||
| public function getAppFileName() | |||
| { | |||
| return $this->appFileName; | |||
| } | |||
| public function setAppFileName(string $fileName) | |||
| { | |||
| $this->appFileName = $fileName; | |||
| return $this; | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| <?php | |||
| namespace App\Files\PDF; | |||
| class Receipt extends PDFFile | |||
| { | |||
| protected const DIR = [ | |||
| ...parent::DIR, | |||
| 'receipt' | |||
| ]; | |||
| /** | |||
| * @override | |||
| */ | |||
| protected function getFileTypeName(): string | |||
| { | |||
| return "ReceiptPDF"; | |||
| } | |||
| } | |||
| @@ -35,6 +35,8 @@ class TmpFile | |||
| protected string $uuid; | |||
| protected string $content; | |||
| public function __construct(?string $id = null) | |||
| { | |||
| @@ -59,16 +61,26 @@ class TmpFile | |||
| return "tmp"; | |||
| } | |||
| protected function getFileExtension(): string | |||
| public function getFileExtension(): string | |||
| { | |||
| return "tmp"; | |||
| } | |||
| final protected function getFileName(): string | |||
| public function getMimeType(): string | |||
| { | |||
| return "txt/plain"; | |||
| } | |||
| final public function getFileName(): string | |||
| { | |||
| return sprintf("%s_%s.%s", $this->getFileTypeName(), $this->uuid, $this->getFileExtension()); | |||
| } | |||
| public function getAppFileName() | |||
| { | |||
| return $this->getFileName(); | |||
| } | |||
| public function getId(): string | |||
| { | |||
| return $this->uuid; | |||
| @@ -93,16 +105,25 @@ class TmpFile | |||
| public function put(string $content) | |||
| { | |||
| Storage::put($this->getPath(), $content); | |||
| $this->content = $content; | |||
| return $this; | |||
| } | |||
| public function get() | |||
| public function load() | |||
| { | |||
| return Storage::get($this->getPath()); | |||
| $this->content = Storage::get($this->getPath()); | |||
| return $this; | |||
| } | |||
| public function append(string $content) | |||
| { | |||
| Storage::append($this->getPath(), $content); | |||
| $this->content .= $content; | |||
| return $this; | |||
| } | |||
| public function get(): string | |||
| { | |||
| return $this->content; | |||
| } | |||
| public function download(string $name = "download") | |||
| @@ -122,6 +143,7 @@ class TmpFile | |||
| if ($ret) info(sprintf("ファイル削除:%s ", $this->getFullPath())); | |||
| return; | |||
| } else { | |||
| $this->content = ""; | |||
| DeleteFile::dispatch($this) | |||
| ->delay($delay); | |||
| return; | |||
| @@ -4,6 +4,7 @@ namespace App\Kintone; | |||
| use App\Exceptions\AppCommonException; | |||
| use App\Exceptions\ConfigException; | |||
| use App\Files\TmpFile; | |||
| use App\Kintone\Models\KintoneModel; | |||
| use Exception; | |||
| use Illuminate\Database\Eloquent\ModelNotFoundException; | |||
| @@ -403,16 +404,25 @@ class KintoneAccess | |||
| /** | |||
| * ファイルアップロード | |||
| * | |||
| * @param UploadedFile $file | |||
| * @param UploadedFile|TmpFile $file | |||
| * @return string fileKey | |||
| */ | |||
| public function filePut(UploadedFile $file): string | |||
| public function filePut(UploadedFile|TmpFile $file): string | |||
| { | |||
| $content = file_get_contents($file); | |||
| $content = ""; | |||
| $sendFileName = ""; | |||
| if ($file instanceof UploadedFile) { | |||
| $content = file_get_contents($file); | |||
| $sendFileName = sprintf("file.%s", $file->extension()); | |||
| } else if ($file instanceof TmpFile) { | |||
| $content = $file->get(); | |||
| $sendFileName = $file->getAppFileName(); | |||
| } | |||
| $response = Http::withHeaders([ | |||
| "X-Cybozu-API-Token" => $this->apiToken, | |||
| ]) | |||
| ->attach("file", $content, sprintf("file.%s", $file->extension())) | |||
| ->attach("file", $content, $sendFileName) | |||
| ->post($this->getFileUrl()); | |||
| if ($response->failed()) { | |||
| @@ -2,15 +2,18 @@ | |||
| namespace App\Kintone\Models; | |||
| use App\Exceptions\AppCommonException; | |||
| use App\Exceptions\ConfigException; | |||
| use App\Files\TmpFile; | |||
| use App\Kintone\File; | |||
| use App\Kintone\KintoneAccess; | |||
| use App\Kintone\KintoneAccessStore; | |||
| use App\Kintone\KintoneRecordQuery; | |||
| use App\Kintone\Models\SubTable\SubTableData; | |||
| use App\Util\DateUtil; | |||
| use Illuminate\Http\UploadedFile; | |||
| use Illuminate\Support\Arr; | |||
| use Illuminate\Support\Carbon; | |||
| use Illuminate\Support\Collection; | |||
| use Illuminate\Support\Str; | |||
| use LogicException; | |||
| use stdClass; | |||
| @@ -89,6 +92,13 @@ abstract class KintoneModel | |||
| { | |||
| return static::getAccess()->find($recordId); | |||
| } | |||
| /** | |||
| * @return static | |||
| */ | |||
| public static function first(?KintoneRecordQuery $query = null) | |||
| { | |||
| return static::getAccess()->first($query); | |||
| } | |||
| public static function getDropDownOptions(string $fieldCode): array | |||
| { | |||
| @@ -112,8 +122,12 @@ abstract class KintoneModel | |||
| protected ?stdClass $dataOrigin = null; | |||
| protected stdClass $data; | |||
| const FIELD_RECORD_NO = "レコード番号"; | |||
| protected const FIELDS = []; | |||
| protected const SUB_TABLES = []; | |||
| protected const FIELD_NAMES = []; | |||
| protected const RELATIONS = []; | |||
| @@ -148,36 +162,15 @@ abstract class KintoneModel | |||
| public function set(string $fieldCode, $value) | |||
| { | |||
| $field = Arr::get(static::FIELDS, $fieldCode); | |||
| if ($field instanceof FieldType) { | |||
| $this->setData($fieldCode, $field, $value); | |||
| } else if (is_array($field)) { | |||
| $this->setTable($fieldCode, $value); | |||
| $type = $this->getFieldType($fieldCode); | |||
| if ($type) { | |||
| $this->setData($fieldCode, $value); | |||
| data_set($this->changed, $fieldCode, true); | |||
| } | |||
| data_set($this->changed, $fieldCode, true); | |||
| return $this; | |||
| } | |||
| private function setTable(string $fieldCode, array $table) | |||
| { | |||
| foreach ($table as $index => $row) { | |||
| $row = data_get($row, "value"); | |||
| $this->setTableRow($fieldCode, $index, $row); | |||
| } | |||
| } | |||
| private function setTableRow(string $fieldCode, int $index, array $row) | |||
| { | |||
| foreach ($row as $columnFieldCode => $column) { | |||
| $value = $column["value"]; | |||
| $type = static::FIELDS[$fieldCode][$columnFieldCode]; | |||
| $insertKey = sprintf("%s.%d.%s", $fieldCode, $index, $columnFieldCode); | |||
| $this->setData($insertKey, $type, $value); | |||
| } | |||
| } | |||
| private function setData(string $path, FieldType $type, $value) | |||
| private function setData(string $path, $value) | |||
| { | |||
| data_set($this->data, $path, $value); | |||
| @@ -207,43 +200,73 @@ abstract class KintoneModel | |||
| continue; | |||
| } | |||
| $type = FieldType::tryFrom($type); | |||
| if ($type === null) continue; | |||
| data_set($this->data, $fieldCode, $this->readData($ele, $fieldCode)); | |||
| } | |||
| if ($this->recordId === null) { | |||
| throw new LogicException(sprintf("レコード番号取得失敗 :%s", static::class)); | |||
| } | |||
| $ret = $this->setDataCustom($data); | |||
| if ($ret) { | |||
| $this->clean(); | |||
| } | |||
| return $ret; | |||
| } | |||
| private function readData(array $data, string $fieldCode = "") | |||
| { | |||
| $type = data_get($data, "type"); | |||
| $value = data_get($data, "value"); | |||
| if ($type === null || $value === null) { | |||
| return null; | |||
| } | |||
| $type = FieldType::tryFrom($type); | |||
| if ($type !== null) { | |||
| if (in_array($type, [FieldType::DATETIME, FieldType::DATE])) { | |||
| if ($value) { | |||
| data_set($this->data, $fieldCode, DateUtil::parse($value)); | |||
| return DateUtil::parse($value); | |||
| } else { | |||
| data_set($this->data, $fieldCode, null); | |||
| return null; | |||
| } | |||
| continue; | |||
| } | |||
| if ($type === FieldType::NUMBER) { | |||
| return intval($value); | |||
| } | |||
| if ($type === FieldType::FILE) { | |||
| $ret = []; | |||
| foreach ($value as $f) { | |||
| $ret[] = new File($f); | |||
| } | |||
| data_set($this->data, $fieldCode, $ret); | |||
| continue; | |||
| return $ret; | |||
| } | |||
| if ($type === FieldType::SUBTABLE) { | |||
| continue; | |||
| if ($type === FieldType::SUBTABLE && $this->getFieldType($fieldCode)) { | |||
| $ret = collect(); | |||
| foreach ($value as $v) { | |||
| $rowRet = | |||
| $rowArray = data_get($v, 'value'); | |||
| foreach ($rowArray as $subFieldCode => $ele) { | |||
| $rowRet[$subFieldCode] = $this->readData($ele); | |||
| } | |||
| $ret->push($this->getSubTableData($fieldCode, $rowRet)); | |||
| } | |||
| return $ret; | |||
| } | |||
| // 以外はそのまま格納 | |||
| data_set($this->data, $fieldCode, $value); | |||
| return $value; | |||
| } | |||
| if ($this->recordId === null) { | |||
| throw new LogicException(sprintf("レコード番号取得失敗 :%s", static::class)); | |||
| } | |||
| return null; | |||
| } | |||
| $ret = $this->setDataCustom($data); | |||
| if ($ret) { | |||
| $this->clean(); | |||
| private function getSubTableData(string $fieldCode, array $data): SubTableData | |||
| { | |||
| $className = data_get(static::SUB_TABLES, $fieldCode, ""); | |||
| if (class_exists($className)) { | |||
| return new $className($data); | |||
| } | |||
| return $ret; | |||
| throw new LogicException("キントーン設定不備"); | |||
| } | |||
| @@ -273,28 +296,54 @@ abstract class KintoneModel | |||
| } | |||
| $path = sprintf("%s.value", $fieldCode); | |||
| if ($type === FieldType::DATETIME) { | |||
| $data = $this->getDate($fieldCode); | |||
| if ($data) { | |||
| data_set($ret, $path, $data->toIso8601ZuluString()); | |||
| } else { | |||
| data_set($ret, $path, ""); | |||
| } | |||
| continue; | |||
| data_set($ret, $path, $this->getApiLayoutValue($fieldCode)); | |||
| } | |||
| return array_merge($ret, $this->getApiLayoutCustom()); | |||
| } | |||
| private function getApiLayoutValue(string $fieldCode) | |||
| { | |||
| $ret = []; | |||
| $data = $this->get($fieldCode); | |||
| $type = $this->getFieldType($fieldCode); | |||
| if ($type === FieldType::DATETIME) { | |||
| $data = $this->getDate($fieldCode); | |||
| if ($data) { | |||
| return $data->toIso8601ZuluString(); | |||
| } else { | |||
| return ""; | |||
| } | |||
| if ($type === FieldType::DATE) { | |||
| $data = $this->getDate($fieldCode); | |||
| if ($data) { | |||
| data_set($ret, $path, $data->toDateString()); | |||
| } else { | |||
| data_set($ret, $path, ""); | |||
| } | |||
| if ($type === FieldType::DATE) { | |||
| $data = $this->getDate($fieldCode); | |||
| if ($data) { | |||
| return $data->toDateString(); | |||
| } else { | |||
| return ""; | |||
| } | |||
| } | |||
| if ($data instanceof Collection) { | |||
| $ret = []; | |||
| foreach ($data as $ele) { | |||
| if ($ele instanceof SubTableData) { | |||
| $obj = []; | |||
| $obj['value'] = $ele->toArray(); | |||
| $ret[] = $obj; | |||
| } | |||
| continue; | |||
| } | |||
| data_set($ret, $path, data_get($this->data, $fieldCode)); | |||
| return $ret; | |||
| } | |||
| return data_get($this->data, $fieldCode); | |||
| } | |||
| return array_merge($ret, $this->getApiLayoutCustom()); | |||
| private function getFieldType(string $fieldCode): ?FieldType | |||
| { | |||
| return data_get(static::FIELDS, $fieldCode); | |||
| } | |||
| public function get(string $key) | |||
| @@ -346,6 +395,46 @@ abstract class KintoneModel | |||
| return $this->createdAt; | |||
| } | |||
| /** | |||
| * @param string $fieldCode | |||
| * @param Collection<int, UploadedFile|TmpFile> $files | |||
| * @param bool $override | |||
| * @return static | |||
| */ | |||
| public function setFiles(string $fieldCode, Collection $files, bool $override = true): static | |||
| { | |||
| if ($override) { | |||
| $this->set($fieldCode, []); | |||
| } | |||
| foreach ($files as $file) { | |||
| $this->addFile($fieldCode, $file); | |||
| } | |||
| return $this; | |||
| } | |||
| public function addFile(string $fieldCode, UploadedFile|TmpFile $file) | |||
| { | |||
| if ($file instanceof UploadedFile) { | |||
| $name = sprintf("image_%d.%s", Str::uuid(), $file->extension()); | |||
| $contentType = $file->getClientMimeType(); | |||
| } else { | |||
| $name = $file->getAppFileName(); | |||
| $contentType = $file->getMimeType(); | |||
| } | |||
| $access = $this->getAccess(); | |||
| $field = $this->get($fieldCode); | |||
| array_push($field, [ | |||
| 'fileKey' => $access->filePut($file), | |||
| 'name' => $name, | |||
| 'contentType' => $contentType, | |||
| ]); | |||
| $this->set($fieldCode, $field); | |||
| return $this; | |||
| } | |||
| public function save() | |||
| { | |||
| $access = static::getAccess(); | |||
| @@ -0,0 +1,51 @@ | |||
| <?php | |||
| namespace App\Kintone\Models; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractReserve\TargetRoom; | |||
| use Illuminate\Support\Collection; | |||
| /** | |||
| * アプリ名 領収証 | |||
| * @property string receiptNo | |||
| * @property int customerCode | |||
| * @property string customerName | |||
| * @property string parkingName | |||
| * @property string receiptCustomerName | |||
| * @property string receiptPurpose | |||
| * @property int receiptTotalAmount | |||
| */ | |||
| class Receipt extends KintoneModel | |||
| { | |||
| const CONFIG_KEY = "KINTONE_APP_RECEIPT"; | |||
| const FIELD_RECEIPT_NO = "領収証番号"; | |||
| const FIELD_CUSTOMER_CODE = "顧客コード"; | |||
| const FIELD_CUSTOMER_NAME = "顧客名"; | |||
| const FIELD_RECEIPT_CUSTOMER_NAME = "宛名"; | |||
| const FIELD_RECEIPT_PURPOSE = "但し書き"; | |||
| const FIELD_RECEIPT_TOTAL_AMOUNT = "合計"; | |||
| const FIELD_RECEIPT_PDF_FILE = "領収証PDF"; | |||
| protected const FIELDS = [ | |||
| ...parent::FIELDS, | |||
| self::FIELD_RECEIPT_NO => FieldType::SINGLE_LINE_TEXT, | |||
| self::FIELD_CUSTOMER_CODE => FieldType::NUMBER, | |||
| self::FIELD_CUSTOMER_NAME => FieldType::SINGLE_LINE_TEXT, | |||
| self::FIELD_RECEIPT_CUSTOMER_NAME => FieldType::SINGLE_LINE_TEXT, | |||
| self::FIELD_RECEIPT_PURPOSE => FieldType::SINGLE_LINE_TEXT, | |||
| self::FIELD_RECEIPT_TOTAL_AMOUNT => FieldType::NUMBER, | |||
| self::FIELD_RECEIPT_PDF_FILE => FieldType::FILE, | |||
| ]; | |||
| protected const FIELD_NAMES = [ | |||
| ...parent::FIELD_NAMES, | |||
| ]; | |||
| protected const RELATIONS = [ | |||
| Customer::class, | |||
| PaymentPlan::class, | |||
| ]; | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| <?php | |||
| namespace App\Kintone\Models\SubTable; | |||
| abstract class SubTableData | |||
| { | |||
| public function __construct(protected array $data) | |||
| { | |||
| } | |||
| abstract public function toArray(): array; | |||
| } | |||
| @@ -0,0 +1,70 @@ | |||
| <?php | |||
| namespace App\Logic; | |||
| use App\Files\PDF\Receipt as ReceiptReceipt; | |||
| use App\Kintone\Models\Receipt; | |||
| use PDF; | |||
| class ReceiptManager | |||
| { | |||
| private ?Receipt $receipt = null; | |||
| public function __construct(?int $recordNo = null) | |||
| { | |||
| if ($recordNo) { | |||
| $this->load($recordNo); | |||
| } else { | |||
| $this->receipt = new Receipt(); | |||
| } | |||
| } | |||
| private function load(int $recordNo) | |||
| { | |||
| $this->receipt = Receipt::find($recordNo); | |||
| } | |||
| public function getReceipt(): Receipt | |||
| { | |||
| return $this->receipt; | |||
| } | |||
| public function makePdf(): ReceiptReceipt | |||
| { | |||
| $pdf = PDF::loadView("pdf/receipt", $this->getPdfData()) | |||
| // ->setPaper("A4") | |||
| ->setOption('page-width', 210) | |||
| ->setOption('page-height', 148) | |||
| ->setOrientation("Portrait") | |||
| ->setOption('encoding', 'utf-8'); | |||
| $file = new ReceiptReceipt(); | |||
| $file->setAppFileName($this->makeFileName($file)) | |||
| ->put($pdf->output()); | |||
| return $file; | |||
| } | |||
| private function makeFileName(ReceiptReceipt $file) | |||
| { | |||
| return sprintf( | |||
| "領収証_%s_%s_%s.%s", | |||
| $this->receipt->receiptNo, | |||
| $this->receipt->customerName, | |||
| $this->receipt->receiptPurpose, | |||
| $file->getFileExtension(), | |||
| ); | |||
| } | |||
| private function getPdfData() | |||
| { | |||
| return [ | |||
| 'receiptDate' => "2023/10/17", | |||
| 'receiptCustomerName' => $this->receipt->receiptCustomerName, | |||
| 'receiptTotalAmount' => number_format($this->receipt->receiptTotalAmount), | |||
| 'taxTotalAmount' => number_format(100), | |||
| 'receiptPurpose' => $this->receipt->receiptPurpose, | |||
| ]; | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="ja"> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <title>領収証</title> | |||
| <link rel="stylesheet" href="{{ resource_path('views/pdf/receipt.css') }}"> | |||
| </head> | |||
| <body> | |||
| <div id="title"> | |||
| 領収証 | |||
| </div> | |||
| <div id="receiptCustomerName" class="receiptCustomerName">{{ $receiptCustomerName }}様</div> | |||
| <div id="receiptDate" class="receiptCustomerName">領収日 {{ $receiptDate }}</div> | |||
| <div id="receiptTotalAmount">金額{{ $receiptTotalAmount }}(税込)</div> | |||
| <div id="taxTotalAmount">税率10% ¥{{ $taxTotalAmount }}</div> | |||
| <div id="receiptPurpose"> | |||
| <p> | |||
| 但し {{ $receiptPurpose }} として | |||
| </p> | |||
| <p> | |||
| 上記正に領収いたしました | |||
| </p> | |||
| </div> | |||
| <div id="company"> | |||
| <p class="name">一般財団法人 京都市都市整備公社</p> | |||
| <p class="zipCode">〒600-8421</p> | |||
| <p class="address1">京都市下京区小道通烏丸西入童侍者町167番</p> | |||
| <p class="invoiceNo">登録番号 T7 1300 0501 2806</p> | |||
| </div> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,67 @@ | |||
| body { | |||
| position: relative; | |||
| font-size: 5mm | |||
| } | |||
| body div { | |||
| position: absolute; | |||
| } | |||
| #title { | |||
| top : 10mm; | |||
| left: 100mm; | |||
| font-size: 10mm; | |||
| font-weight: bold; | |||
| letter-spacing: 10mm; | |||
| } | |||
| .receiptCustomerName { | |||
| top : 30mm; | |||
| } | |||
| #receiptCustomerName { | |||
| left: 100mm; | |||
| } | |||
| #receiptDate { | |||
| left: 150mm; | |||
| } | |||
| #receiptTotalAmount { | |||
| border-top: solid 1px; | |||
| border-bottom: solid 1px; | |||
| padding-top: 2mm;; | |||
| padding-bottom: 2mm; | |||
| top: 50mm; | |||
| left: 20mm; | |||
| width: 170mm; | |||
| } | |||
| #taxTotalAmount { | |||
| border-bottom: solid 1px; | |||
| padding-bottom: 2mm; | |||
| top: 65mm; | |||
| left: 140mm; | |||
| width: 50mm; | |||
| } | |||
| #receiptPurpose { | |||
| top: 70mm; | |||
| left: 20mm; | |||
| } | |||
| #company { | |||
| top: 130mm; | |||
| left: 150mm; | |||
| font-size: 4mm; | |||
| } | |||
| #company p { | |||
| margin: 1mm; | |||
| } | |||
| #company .name { | |||
| font-size: 5mm; | |||
| } | |||