diff --git a/hesabixCore/composer.json b/hesabixCore/composer.json index 66aedb9..f44927d 100644 --- a/hesabixCore/composer.json +++ b/hesabixCore/composer.json @@ -16,6 +16,7 @@ "doctrine/orm": "^3.2", "dompdf/dompdf": "^3.0", "melipayamak/php": "^1.0", + "morilog/jalali": "*", "mpdf/mpdf": "^8.2", "nelmio/api-doc-bundle": "^4.35", "nelmio/cors-bundle": "^2.5", diff --git a/hesabixCore/composer.lock b/hesabixCore/composer.lock index fa625a3..c53e754 100644 --- a/hesabixCore/composer.lock +++ b/hesabixCore/composer.lock @@ -4,8 +4,75 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "43db0ad2bb94569ed6d44cabf503210e", + "content-hash": "01b5daf5a6fd011b4eb616e0e4ae18fe", "packages": [ + { + "name": "beberlei/assert", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd", + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=6.0.0", + "yoast/phpunit-polyfills": "^0.1.0" + }, + "suggest": { + "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Assert/functions.php" + ], + "psr-4": { + "Assert\\": "lib/Assert" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "support": { + "issues": "https://github.com/beberlei/assert/issues", + "source": "https://github.com/beberlei/assert/tree/v3.3.3" + }, + "time": "2024-07-15T13:18:35+00:00" + }, { "name": "brick/math", "version": "0.12.3", @@ -66,6 +133,75 @@ ], "time": "2025-02-28T13:11:00+00:00" }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, { "name": "composer/pcre", "version": "3.3.2", @@ -2297,6 +2433,71 @@ ], "time": "2025-03-24T10:02:05+00:00" }, + { + "name": "morilog/jalali", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/morilog/jalali.git", + "reference": "f475f4db7bd540c6abc01126e46824c897ed1e03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/morilog/jalali/zipball/f475f4db7bd540c6abc01126e46824c897ed1e03", + "reference": "f475f4db7bd540c6abc01126e46824c897ed1e03", + "shasum": "" + }, + "require": { + "beberlei/assert": "^3.0", + "nesbot/carbon": "^1.21 || ^2.0 || ^3.0", + "php": "^7.0 | ^8.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Morilog\\Jalali\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Milad Rey", + "email": "miladr@gmail.com" + }, + { + "name": "Morteza Parvini", + "email": "m.parvini@outlook.com" + } + ], + "description": "This Package helps developers to easily work with Jalali (Shamsi or Iranian) dates in PHP applications, based on Jalali (Shamsi) DateTime class.", + "keywords": [ + "Jalali", + "date", + "datetime", + "laravel", + "morilog" + ], + "support": { + "issues": "https://github.com/morilog/jalali/issues", + "source": "https://github.com/morilog/jalali/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://issuehunt.io/r/morilog", + "type": "issuehunt" + } + ], + "time": "2024-05-09T08:44:51+00:00" + }, { "name": "mpdf/mpdf", "version": "v8.2.5", @@ -2714,6 +2915,111 @@ }, "time": "2024-06-24T21:25:28+00:00" }, + { + "name": "nesbot/carbon", + "version": "3.10.2", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.75.0", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^10.5.46", + "squizlabs/php_codesniffer": "^3.13.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-08-02T09:36:06+00:00" + }, { "name": "nikic/php-parser", "version": "v5.4.0", diff --git a/hesabixCore/migrations/Version20241220000000.php b/hesabixCore/migrations/Version20241220000000.php deleted file mode 100644 index 3d12ad1..0000000 --- a/hesabixCore/migrations/Version20241220000000.php +++ /dev/null @@ -1,40 +0,0 @@ -addSql('ALTER TABLE chat_channel ADD member_count INT NOT NULL DEFAULT 0'); - - // Update existing channels with correct member count - $this->addSql(' - UPDATE chat_channel c - SET member_count = ( - SELECT COUNT(*) - FROM chat_channel_member m - WHERE m.channel_id = c.id AND m.is_active = 1 - ) - '); - } - - public function down(Schema $schema): void - { - $this->addSql('ALTER TABLE chat_channel DROP member_count'); - } -} \ No newline at end of file diff --git a/hesabixCore/migrations/Version20250113000000.php b/hesabixCore/migrations/Version20250113000000.php new file mode 100644 index 0000000..73e672a --- /dev/null +++ b/hesabixCore/migrations/Version20250113000000.php @@ -0,0 +1,40 @@ +addSql('ALTER TABLE hesabdari_doc ADD is_preview TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE hesabdari_doc ADD is_approved TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE hesabdari_doc ADD approved_by_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE hesabdari_doc ADD CONSTRAINT FK_HESABDARI_DOC_APPROVED_BY FOREIGN KEY (approved_by_id) REFERENCES user (id)'); + $this->addSql('CREATE INDEX IDX_HESABDARI_DOC_APPROVED_BY ON hesabdari_doc (approved_by_id)'); + + // Set default values for existing documents + $this->addSql('UPDATE hesabdari_doc SET is_preview = 0, is_approved = 1 WHERE is_preview IS NULL'); + } + + public function down(Schema $schema): void + { + // Remove approval fields from HesabdariDoc table + $this->addSql('ALTER TABLE hesabdari_doc DROP FOREIGN KEY FK_HESABDARI_DOC_APPROVED_BY'); + $this->addSql('DROP INDEX IDX_HESABDARI_DOC_APPROVED_BY ON hesabdari_doc'); + $this->addSql('ALTER TABLE hesabdari_doc DROP is_preview, DROP is_approved, DROP approved_by_id'); + } +} diff --git a/hesabixCore/migrations/Version20250113000001.php b/hesabixCore/migrations/Version20250113000001.php new file mode 100644 index 0000000..bb87e08 --- /dev/null +++ b/hesabixCore/migrations/Version20250113000001.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE storeroom_ticket DROP status'); + } + + public function down(Schema $schema): void + { + // Add status field back to storeroom_ticket table + $this->addSql('ALTER TABLE storeroom_ticket ADD status VARCHAR(50) DEFAULT NULL'); + } +} diff --git a/hesabixCore/migrations/Version20250113000002.php b/hesabixCore/migrations/Version20250113000002.php new file mode 100644 index 0000000..794b897 --- /dev/null +++ b/hesabixCore/migrations/Version20250113000002.php @@ -0,0 +1,40 @@ +addSql('ALTER TABLE storeroom_ticket ADD is_preview TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE storeroom_ticket ADD is_approved TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE storeroom_ticket ADD approved_by_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE storeroom_ticket ADD CONSTRAINT FK_STOREROOM_TICKET_APPROVED_BY FOREIGN KEY (approved_by_id) REFERENCES user (id)'); + $this->addSql('CREATE INDEX IDX_STOREROOM_TICKET_APPROVED_BY ON storeroom_ticket (approved_by_id)'); + + // Set default values for existing tickets + $this->addSql('UPDATE storeroom_ticket SET is_preview = 0, is_approved = 1 WHERE is_preview IS NULL'); + } + + public function down(Schema $schema): void + { + // Remove approval fields from storeroom_ticket table + $this->addSql('ALTER TABLE storeroom_ticket DROP FOREIGN KEY FK_STOREROOM_TICKET_APPROVED_BY'); + $this->addSql('DROP INDEX IDX_STOREROOM_TICKET_APPROVED_BY ON storeroom_ticket'); + $this->addSql('ALTER TABLE storeroom_ticket DROP is_preview, DROP is_approved, DROP approved_by_id'); + } +} diff --git a/hesabixCore/migrations/Version20250809100001.php b/hesabixCore/migrations/Version20250809100001.php deleted file mode 100644 index afc4739..0000000 --- a/hesabixCore/migrations/Version20250809100001.php +++ /dev/null @@ -1,30 +0,0 @@ -addSql('CREATE TABLE custom_invoice_template (id INT AUTO_INCREMENT NOT NULL, bid_id INT NOT NULL, submitter_id INT NOT NULL, name VARCHAR(255) NOT NULL, is_public TINYINT(1) NOT NULL, code LONGTEXT NOT NULL, INDEX IDX_CUSTOM_INV_TPL_BID (bid_id), INDEX IDX_CUSTOM_INV_TPL_SUBMITTER (submitter_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_BID FOREIGN KEY (bid_id) REFERENCES business (id)'); - $this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_SUBMITTER FOREIGN KEY (submitter_id) REFERENCES `user` (id)'); - } - - public function down(Schema $schema): void - { - $this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_BID'); - $this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_SUBMITTER'); - $this->addSql('DROP TABLE custom_invoice_template'); - } -} \ No newline at end of file diff --git a/hesabixCore/migrations/Version20250811093832.php b/hesabixCore/migrations/Version20250811093832.php new file mode 100644 index 0000000..48c8143 --- /dev/null +++ b/hesabixCore/migrations/Version20250811093832.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE plug_warranty_serial ADD used TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE plug_warranty_serial ADD used_at VARCHAR(50) DEFAULT NULL'); + $this->addSql('ALTER TABLE plug_warranty_serial ADD used_ticket_code VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE plug_warranty_serial DROP used'); + $this->addSql('ALTER TABLE plug_warranty_serial DROP used_at'); + $this->addSql('ALTER TABLE plug_warranty_serial DROP used_ticket_code'); + } +} + + diff --git a/hesabixCore/migrations/Version20250811123020.php b/hesabixCore/migrations/Version20250811123020.php new file mode 100644 index 0000000..3c9d827 --- /dev/null +++ b/hesabixCore/migrations/Version20250811123020.php @@ -0,0 +1,30 @@ +addSql('ALTER TABLE storeroom_ticket ADD status VARCHAR(50) DEFAULT NULL'); + $this->addSql('ALTER TABLE storeroom_ticket ADD import_workflow_code VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE storeroom_ticket DROP status'); + $this->addSql('ALTER TABLE storeroom_ticket DROP import_workflow_code'); + } +} + + diff --git a/hesabixCore/migrations/Version20250811124530.php b/hesabixCore/migrations/Version20250811124530.php new file mode 100644 index 0000000..50abeaa --- /dev/null +++ b/hesabixCore/migrations/Version20250811124530.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE permission ADD require_two_step_sell TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE permission ADD require_two_step_payment TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE permission ADD require_two_step_store TINYINT(1) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE permission DROP require_two_step_sell'); + $this->addSql('ALTER TABLE permission DROP require_two_step_payment'); + $this->addSql('ALTER TABLE permission DROP require_two_step_store'); + } +} + + diff --git a/hesabixCore/migrations/Version20250815143325.php b/hesabixCore/migrations/Version20250815143325.php new file mode 100644 index 0000000..3008029 --- /dev/null +++ b/hesabixCore/migrations/Version20250815143325.php @@ -0,0 +1,101 @@ +addSql(<<<'SQL' + ALTER TABLE hesabdari_row DROP FOREIGN KEY FK_83B2C6EC2D234F6A + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_83B2C6EC2D234F6A ON hesabdari_row + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE hesabdari_row DROP is_preview, DROP is_approved, DROP approved_by_id + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26F4D9866B8 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_1A5DC26F4D9866B8 ON plug_warranty_serial + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD allocated_to_document_id INT DEFAULT NULL, ADD allocated_at DATETIME DEFAULT NULL, ADD bound_to_item_id INT DEFAULT NULL, ADD bound_at DATETIME DEFAULT NULL, DROP used, DROP used_at, CHANGE date_submit date_submit DATETIME NOT NULL, CHANGE description description LONGTEXT DEFAULT NULL, CHANGE warranty_start_date warranty_start_date DATETIME DEFAULT NULL, CHANGE warranty_end_date warranty_end_date DATETIME DEFAULT NULL, CHANGE status status VARCHAR(20) NOT NULL, CHANGE notes notes LONGTEXT DEFAULT NULL, CHANGE used_ticket_code void_reason VARCHAR(255) DEFAULT NULL, CHANGE bid_id business_id INT NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26FA89DB457 FOREIGN KEY (business_id) REFERENCES business (id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1A5DC26FA89DB457 ON plug_warranty_serial (business_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX idx_status_product ON plug_warranty_serial (status, commodity_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX idx_alloc_doc ON plug_warranty_serial (allocated_to_document_id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial RENAME INDEX uniq_1a5dc26fd948ee2 TO uniq_warranty_serial + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE storeroom_ticket RENAME INDEX idx_storeroom_ticket_approved_by TO IDX_9B4CC0F72D234F6A + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE hesabdari_row ADD is_preview TINYINT(1) DEFAULT NULL, ADD is_approved TINYINT(1) DEFAULT NULL, ADD approved_by_id INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE hesabdari_row ADD CONSTRAINT FK_83B2C6EC2D234F6A FOREIGN KEY (approved_by_id) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE NO ACTION + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_83B2C6EC2D234F6A ON hesabdari_row (approved_by_id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26FA89DB457 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_1A5DC26FA89DB457 ON plug_warranty_serial + SQL); + $this->addSql(<<<'SQL' + DROP INDEX idx_status_product ON plug_warranty_serial + SQL); + $this->addSql(<<<'SQL' + DROP INDEX idx_alloc_doc ON plug_warranty_serial + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD used TINYINT(1) DEFAULT NULL, ADD used_at VARCHAR(50) DEFAULT NULL, DROP allocated_to_document_id, DROP allocated_at, DROP bound_to_item_id, DROP bound_at, CHANGE date_submit date_submit VARCHAR(25) NOT NULL, CHANGE description description VARCHAR(255) DEFAULT NULL, CHANGE warranty_start_date warranty_start_date VARCHAR(25) DEFAULT NULL, CHANGE warranty_end_date warranty_end_date VARCHAR(25) DEFAULT NULL, CHANGE status status VARCHAR(50) DEFAULT NULL, CHANGE notes notes VARCHAR(255) DEFAULT NULL, CHANGE business_id bid_id INT NOT NULL, CHANGE void_reason used_ticket_code VARCHAR(255) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26F4D9866B8 FOREIGN KEY (bid_id) REFERENCES business (id) ON UPDATE NO ACTION ON DELETE NO ACTION + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1A5DC26F4D9866B8 ON plug_warranty_serial (bid_id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial RENAME INDEX uniq_warranty_serial TO UNIQ_1A5DC26FD948EE2 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE storeroom_ticket RENAME INDEX idx_9b4cc0f72d234f6a TO IDX_STOREROOM_TICKET_APPROVED_BY + SQL); + } +} diff --git a/hesabixCore/migrations/Version20250816171207.php b/hesabixCore/migrations/Version20250816171207.php new file mode 100644 index 0000000..5bfe61d --- /dev/null +++ b/hesabixCore/migrations/Version20250816171207.php @@ -0,0 +1,47 @@ +addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD commodity_serial VARCHAR(255) DEFAULT NULL, ADD buyer_id INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26F6C755722 FOREIGN KEY (buyer_id) REFERENCES person (id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_1A5DC26F6C755722 ON plug_warranty_serial (buyer_id) + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26F6C755722 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_1A5DC26F6C755722 ON plug_warranty_serial + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial DROP commodity_serial, DROP buyer_id + SQL); + } +} diff --git a/hesabixCore/migrations/Version20250816185111.php b/hesabixCore/migrations/Version20250816185111.php new file mode 100644 index 0000000..9274184 --- /dev/null +++ b/hesabixCore/migrations/Version20250816185111.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD activation VARCHAR(20) NOT NULL + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial DROP activation + SQL); + } +} diff --git a/hesabixCore/migrations/Version20250816185556.php b/hesabixCore/migrations/Version20250816185556.php new file mode 100644 index 0000000..d6ea780 --- /dev/null +++ b/hesabixCore/migrations/Version20250816185556.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial ADD activation_at VARCHAR(20) NOT NULL + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial DROP activation_at + SQL); + } +} diff --git a/hesabixCore/migrations/Version20250818042052.php b/hesabixCore/migrations/Version20250818042052.php new file mode 100644 index 0000000..67a5631 --- /dev/null +++ b/hesabixCore/migrations/Version20250818042052.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial CHANGE activation_at activation_at DATETIME DEFAULT NULL + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial CHANGE activation_at activation_at VARCHAR(20) NOT NULL + SQL); + } +} diff --git a/hesabixCore/migrations/Version20250818042232.php b/hesabixCore/migrations/Version20250818042232.php new file mode 100644 index 0000000..ea3f647 --- /dev/null +++ b/hesabixCore/migrations/Version20250818042232.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial CHANGE activation_at activation_at DATETIME DEFAULT NULL + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE plug_warranty_serial CHANGE activation_at activation_at VARCHAR(20) DEFAULT NULL + SQL); + } +} diff --git a/hesabixCore/src/Cog/PersonService.php b/hesabixCore/src/Cog/PersonService.php index 61c0ecd..1137a6d 100644 --- a/hesabixCore/src/Cog/PersonService.php +++ b/hesabixCore/src/Cog/PersonService.php @@ -288,6 +288,7 @@ class PersonService if (isset($params['shenasemeli'])) $person->setShenasemeli($params['shenasemeli']); if (isset($params['company'])) $person->setCompany($params['company']); if (isset($params['tags'])) $person->setTags($params['tags']); + if (array_key_exists('prelabel', $params)) { if ($params['prelabel'] != '') { $prelabel = $em->getRepository(\App\Entity\PersonPrelabel::class)->findOneBy(['label' => $params['prelabel']]); diff --git a/hesabixCore/src/Controller/ApprovalController.php b/hesabixCore/src/Controller/ApprovalController.php new file mode 100644 index 0000000..e4f9b85 --- /dev/null +++ b/hesabixCore/src/Controller/ApprovalController.php @@ -0,0 +1,426 @@ +hasRole('settings'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $business = $acc['bid']; + $businessSettings = $entityManager->getRepository(Business::class)->find($business->getId()); + + // بررسی اینکه آیا تأیید دو مرحله‌ای فعال است + if (!$businessSettings->isRequireTwoStepApproval()) { + return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']); + } + + // پیدا کردن حواله انبار + $ticket = $entityManager->getRepository(\App\Entity\StoreroomTicket::class)->findOneBy([ + 'code' => $ticketCode, + 'bid' => $business + ]); + + if (!$ticket) { + return $this->json(['success' => false, 'message' => 'حواله انبار یافت نشد']); + } + + // بررسی مجوز تأیید + $canApprove = $this->canUserApproveStoreroomTicket($user, $businessSettings); + if (!$canApprove) { + return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']); + } + + // تأیید حواله + $ticket->setIsPreview(false); + $ticket->setIsApproved(true); + $ticket->setApprovedBy($user); + + $entityManager->persist($ticket); + $entityManager->flush(); + + // ثبت لاگ + $logService->insert( + 'تأیید حواله انبار', + "حواله انبار {$ticket->getCode()} توسط {$user->getFullName()} تأیید شد", + $user, + $business + ); + + return $this->json([ + 'success' => true, + 'message' => 'حواله انبار با موفقیت تأیید شد' + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'message' => 'خطا در تأیید حواله انبار: ' . $e->getMessage() + ], 500); + } + } + + // تأیید فاکتور فروش + #[Route('/api/approval/approve/sales/{docId}', name: 'api_approval_approve_sales', methods: ['POST'])] + public function approveSalesInvoice( + $docId, + #[CurrentUser] ?User $user, + Access $access, + LogService $logService, + EntityManagerInterface $entityManager + ): Response { + try { + // بررسی دسترسی کاربر + $acc = $access->hasRole('settings'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $business = $acc['bid']; + $businessSettings = $entityManager->getRepository(Business::class)->find($business->getId()); + + // بررسی اینکه آیا تأیید دو مرحله‌ای فعال است + if (!$businessSettings->isRequireTwoStepApproval()) { + return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']); + } + + // پیدا کردن فاکتور فروش + $document = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ + 'code' => $docId, + 'bid' => $business + ]); + + if (!$document) { + return $this->json(['success' => false, 'message' => 'فاکتور فروش یافت نشد']); + } + + // بررسی مجوز تأیید + $canApprove = $this->canUserApproveSalesInvoice($user, $businessSettings); + if (!$canApprove) { + return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']); + } + + // تأیید فاکتور + $document->setIsPreview(false); + $document->setIsApproved(true); + $document->setApprovedBy($user); + + $entityManager->persist($document); + $entityManager->flush(); + + // ثبت لاگ + $logService->insert( + 'تأیید فاکتور فروش', + "فاکتور فروش {$document->getCode()} توسط {$user->getFullName()} تأیید شد", + $user, + $business + ); + + return $this->json([ + 'success' => true, + 'message' => 'فاکتور فروش با موفقیت تأیید شد' + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'message' => 'خطا در تأیید فاکتور فروش: ' . $e->getMessage() + ], 500); + } + } + + // تأیید سند مالی + #[Route('/api/approval/approve/financial/{docId}', name: 'api_approval_approve_financial', methods: ['POST'])] + public function approveFinancialDocument( + $docId, + #[CurrentUser] ?User $user, + Access $access, + LogService $logService, + EntityManagerInterface $entityManager + ): Response { + try { + // بررسی دسترسی کاربر + $acc = $access->hasRole('hasRole'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $business = $acc['bid']; + $businessSettings = $entityManager->getRepository(Business::class)->find($business->getId()); + + // بررسی اینکه آیا تأیید دو مرحله‌ای فعال است + if (!$businessSettings->isRequireTwoStepApproval()) { + return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']); + } + + // پیدا کردن سند مالی + $document = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ + 'id' => $docId, + 'bid' => $business + ]); + + if (!$document) { + return $this->json(['success' => false, 'message' => 'سند مالی یافت نشد']); + } + + // بررسی مجوز تأیید + $canApprove = $this->canUserApproveFinancialDocument($user, $businessSettings); + if (!$canApprove) { + return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این سند را ندارید']); + } + + // تأیید سند + $document->setIsPreview(false); + $document->setIsApproved(true); + $document->setApprovedBy($user); + + $entityManager->persist($document); + $entityManager->flush(); + + // ثبت لاگ + $logService->insert( + 'تأیید سند مالی', + "سند مالی {$document->getCode()} توسط {$user->getFullName()} تأیید شد", + $user, + $business + ); + + return $this->json([ + 'success' => true, + 'message' => 'سند مالی با موفقیت تأیید شد' + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'message' => 'خطا در تأیید سند مالی: ' . $e->getMessage() + ], 500); + } + } + + #[Route('/api/approval/reject/{docId}', name: 'api_approval_reject', methods: ['POST'])] + public function rejectDocument( + $docId, + Request $request, + #[CurrentUser] ?User $user, + Access $access, + LogService $logService, + EntityManagerInterface $entityManager + ): Response { + try { + // بررسی دسترسی کاربر + $acc = $access->hasRole('owner'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $business = $acc['bid']; + $businessSettings = $entityManager->getRepository(Business::class)->find($business->getId()); + + // بررسی اینکه آیا تأیید دو مرحله‌ای فعال است + if (!$businessSettings->isRequireTwoStepApproval()) { + return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']); + } + + // پیدا کردن سند + $document = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ + 'id' => $docId, + 'bid' => $business + ]); + + if (!$document) { + return $this->json(['success' => false, 'message' => 'سند یافت نشد']); + } + + // دریافت دلیل رد + $data = json_decode($request->getContent(), true); + $rejectionReason = $data['reason'] ?? 'دلیل مشخص نشده'; + + // رد سند + $document->setIsPreview(false); + $document->setIsApproved(false); + $document->setApprovedBy(null); + + // ردیف‌ها نیازی به تنظیم جداگانه ندارند - از سند پیروی می‌کنند + + $entityManager->persist($document); + $entityManager->flush(); + + // ثبت لاگ + $logService->insert( + 'رد سند', + "سند {$document->getCode()} توسط {$user->getFullName()} رد شد. دلیل: {$rejectionReason}", + $user, + $business + ); + + return $this->json([ + 'success' => true, + 'message' => 'سند با موفقیت رد شد' + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'message' => 'خطا در رد سند: ' . $e->getMessage() + ], 500); + } + } + + #[Route('/api/approval/check-permission/{docId}', name: 'api_approval_check_permission', methods: ['GET'])] + public function checkApprovalPermission( + $docId, + #[CurrentUser] ?User $user, + Access $access, + EntityManagerInterface $entityManager + ): Response { + try { + $acc = $access->hasRole('settings'); + if (!$acc) { + return $this->json(['canApprove' => false, 'message' => 'دسترسی محدود']); + } + + $business = $acc['bid']; + $businessSettings = $entityManager->getRepository(Business::class)->find($business->getId()); + + // پیدا کردن سند + $document = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ + 'id' => $docId, + 'bid' => $business + ]); + + if (!$document) { + return $this->json(['canApprove' => false, 'message' => 'سند یافت نشد']); + } + + $canApprove = $this->canUserApproveDocument($user, $businessSettings, $document); + + return $this->json([ + 'canApprove' => $canApprove, + 'documentStatus' => [ + 'isPreview' => $document->isPreview(), + 'isApproved' => $document->isApproved(), + 'approvedBy' => $document->getApprovedBy() ? $document->getApprovedBy()->getFullName() : null + ] + ]); + + } catch (\Exception $e) { + return $this->json([ + 'canApprove' => false, + 'message' => 'خطا در بررسی مجوز: ' . $e->getMessage() + ], 500); + } + } + + /** + * بررسی اینکه آیا کاربر می‌تواند سند را تأیید کند + */ + private function canUserApproveDocument(User $user, Business $business, HesabdariDoc $document): bool + { + // مدیر کسب و کار همیشه می‌تواند تأیید کند + if ($user->getEmail() === $business->getOwner()->getEmail()) { + return true; + } + + // بررسی تأییدکنندگان اختصاصی بر اساس نوع سند + $documentType = $this->getDocumentType($document); + + switch ($documentType) { + case 'invoice': + return $business->getInvoiceApprover() === $user->getEmail(); + case 'warehouse': + return $business->getWarehouseApprover() === $user->getEmail(); + case 'financial': + return $business->getFinancialApprover() === $user->getEmail(); + default: + return false; + } + } + + /** + * تشخیص نوع سند + */ + private function getDocumentType(HesabdariDoc $document): string + { + $type = $document->getType(); + + if (strpos($type, 'sell') !== false || strpos($type, 'invoice') !== false) { + return 'invoice'; + } + + if (strpos($type, 'warehouse') !== false || strpos($type, 'storeroom') !== false) { + return 'warehouse'; + } + + if (strpos($type, 'payment') !== false || strpos($type, 'receipt') !== false || strpos($type, 'hesabdari') !== false) { + return 'financial'; + } + + return 'unknown'; + } + + // بررسی مجوز تأیید حواله انبار + private function canUserApproveStoreroomTicket(User $user, Business $business): bool + { + // مدیر کسب و کار همیشه می‌تواند تأیید کند + if ($user->getEmail() === $business->getOwner()->getEmail()) { + return true; + } + + // کاربر تأییدکننده انبار + return $business->getWarehouseApprover() === $user->getEmail(); + } + + // بررسی مجوز تأیید فاکتور فروش + private function canUserApproveSalesInvoice(User $user, Business $business): bool + { + // مدیر کسب و کار همیشه می‌تواند تأیید کند + if ($user->getEmail() === $business->getOwner()->getEmail()) { + return true; + } + + // کاربر تأییدکننده فاکتور فروش + return $business->getInvoiceApprover() === $user->getEmail(); + } + + // بررسی مجوز تأیید سند مالی + private function canUserApproveFinancialDocument(User $user, Business $business): bool + { + // مدیر کسب و کار همیشه می‌تواند تأیید کند + if ($user->getEmail() === $business->getOwner()->getEmail()) { + return true; + } + + // کاربر تأییدکننده اسناد مالی + return $business->getFinancialApprover() === $user->getEmail(); + } +} diff --git a/hesabixCore/src/Controller/BusinessController.php b/hesabixCore/src/Controller/BusinessController.php index 698f066..15b8754 100644 --- a/hesabixCore/src/Controller/BusinessController.php +++ b/hesabixCore/src/Controller/BusinessController.php @@ -246,6 +246,22 @@ class BusinessController extends AbstractController $business->setWalletEnable(false); } } + if (array_key_exists('requireTwoStepApproval', $params)) { + $business->setRequireTwoStepApproval((bool)$params['requireTwoStepApproval']); + } + + // Set approvers + if (array_key_exists('invoiceApprover', $params)) { + $business->setInvoiceApprover($params['invoiceApprover']); + } + + if (array_key_exists('warehouseApprover', $params)) { + $business->setWarehouseApprover($params['warehouseApprover']); + } + + if (array_key_exists('financialApprover', $params)) { + $business->setFinancialApprover($params['financialApprover']); + } //get Money type if (!array_key_exists('arzmain', $params) && $isNew) { @@ -548,6 +564,8 @@ class BusinessController extends AbstractController 'plugWarranty' => true, 'inquiry' => true, 'ai' => true, + 'warehouseManager' => true, + 'importWorkflow' => true, ]; } elseif ($perm) { $result = [ @@ -595,7 +613,16 @@ class BusinessController extends AbstractController 'plugWarranty' => $perm->isPlugWarrantyManager(), 'inquiry' => $perm->isInquiry(), 'ai' => $perm->isAi(), + 'warehouseManager' => $perm->isWarehouseManager(), + 'importWorkflow' => $perm->isImportWorkflow(), ]; + + if ($perm->isWarehouseManager()) { + $result['commodity'] = true; + $result['store'] = true; + $result['plugWarranty'] = true; + $result['permission'] = true; + } } return $this->json($result); } @@ -668,6 +695,8 @@ class BusinessController extends AbstractController $perm->setPlugTaxSettings($params['plugTaxSettings']); $perm->setInquiry($params['inquiry']); $perm->setAi($params['ai']); + $perm->setWarehouseManager($params['warehouseManager'] ?? false); + $perm->setImportWorkflow($params['importWorkflow'] ?? false); $entityManager->persist($perm); $entityManager->flush(); $log->insert('تنظیمات پایه', 'ویرایش دسترسی‌های کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business); diff --git a/hesabixCore/src/Controller/PluginController.php b/hesabixCore/src/Controller/PluginController.php index 02d7908..9d8b63c 100644 --- a/hesabixCore/src/Controller/PluginController.php +++ b/hesabixCore/src/Controller/PluginController.php @@ -498,15 +498,24 @@ class PluginController extends AbstractController 'icon' => ' taxplugin.jpg', 'defaultOn' => null, ], - // [ - // 'name' => 'مدیریت گارانتی', - // 'code' => 'warranty', - // 'timestamp' => '32104000', - // 'timelabel' => 'یک سال', - // 'price' => '200000', - // 'icon' => 'warranty.png', - // 'defaultOn' => null, - // ], + [ + 'name' => 'مدیریت گارانتی', + 'code' => 'warranty', + 'timestamp' => '32104000', + 'timelabel' => 'یک سال', + 'price' => '200000', + 'icon' => 'warranty.png', + 'defaultOn' => null, + ], + [ + 'name' => 'مدیریت واردات کالا', + 'code' => 'import-workflow', + 'timestamp' => '32104000', + 'timelabel' => 'یک سال', + 'price' => '200000', + 'icon' => 'import-workflow.png', + 'defaultOn' => null, + ], ]; $repo = $entityManager->getRepository(PluginProdect::class); diff --git a/hesabixCore/src/Controller/Plugins/PlugImportWorkflowController.php b/hesabixCore/src/Controller/Plugins/PlugImportWorkflowController.php new file mode 100644 index 0000000..f4465bd --- /dev/null +++ b/hesabixCore/src/Controller/Plugins/PlugImportWorkflowController.php @@ -0,0 +1,1111 @@ +hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $status = $request->query->get('status', ''); + $page = (int) $request->query->get('page', 1); + $limit = (int) $request->query->get('limit', 20); + + $repository = $entityManager->getRepository(ImportWorkflow::class); + + if ($status) { + $workflows = $repository->findByStatus($status, $acc['bid']->getId()); + } else { + $workflows = $repository->findByBusiness($acc['bid']->getId()); + } + + $total = count($workflows); + $offset = ($page - 1) * $limit; + $workflows = array_slice($workflows, $offset, $limit); + + $data = []; + foreach ($workflows as $workflow) { + $data[] = [ + 'id' => $workflow->getId(), + 'code' => $workflow->getCode(), + 'title' => $workflow->getTitle(), + 'status' => $workflow->getStatus(), + 'dateSubmit' => $workflow->getDateSubmit(), + 'supplierName' => $workflow->getSupplierName(), + 'totalAmount' => $workflow->getTotalAmount(), + 'currency' => $workflow->getCurrency(), + 'submitter' => $workflow->getSubmitter()->getFullName() + ]; + } + + return $this->json([ + 'Success' => true, + 'ErrorCode' => 0, + 'ErrorMessage' => '', + 'Result' => [ + 'data' => $data, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + #[Route('/api/import-workflow/documents/{docId}/download', name: 'api_import_workflow_document_download', methods: ['GET'])] + public function downloadDocument(string $docId, Request $request, Access $access, EntityManagerInterface $entityManager, FileStorage $storage): \Symfony\Component\HttpFoundation\Response + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $doc = $entityManager->getRepository(ImportWorkflowDocument::class)->find((int)$docId); + if (!$doc) { + throw $this->createNotFoundException('سند یافت نشد'); + } + // Enforce business ownership + $workflow = $doc->getImportWorkflow(); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + throw $this->createAccessDeniedException('دسترسی ندارید'); + } + $abs = $storage->absolutePath((string)$doc->getFilePath()); + if (!is_file($abs) || !is_readable($abs)) { + throw $this->createNotFoundException('فایل یافت نشد'); + } + $response = new \Symfony\Component\HttpFoundation\BinaryFileResponse($abs); + $response->setContentDisposition( + \Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_ATTACHMENT, + $doc->getFileName() ?: basename($abs) + ); + $response->headers->set('Content-Type', $doc->getFileType() ?: 'application/octet-stream'); + return $response; + } + + #[Route('/api/import-workflow/create', name: 'api_import_workflow_create', methods: ['POST'])] + public function create( + Request $request, + Access $access, + Log $log, + Provider $provider, + EntityManagerInterface $entityManager, + #[CurrentUser] $user + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $data = json_decode($request->getContent(), true); + + $workflow = new ImportWorkflow(); + $workflow->setCode($provider->getAccountingCode($request->headers->get('activeBid'), 'ImportWorkflow')); + $workflow->setTitle($data['title'] ?? ''); + $workflow->setBusiness($acc['bid']); + $workflow->setSubmitter($user); + $workflow->setDescription($data['description'] ?? ''); + $workflow->setSupplierName($data['supplierName'] ?? ''); + $workflow->setSupplierCountry($data['supplierCountry'] ?? ''); + $workflow->setSupplierAddress($data['supplierAddress'] ?? ''); + $workflow->setSupplierPhone($data['supplierPhone'] ?? ''); + $workflow->setSupplierEmail($data['supplierEmail'] ?? ''); + $workflow->setTotalAmount($data['totalAmount'] ?? ''); + $workflow->setCurrency($data['currency'] ?? ''); + $workflow->setExchangeRate($data['exchangeRate'] ?? ''); + $workflow->setTotalAmountIRR($data['totalAmountIRR'] ?? ''); + $workflow->setStatus('draft'); + + $entityManager->persist($workflow); + $entityManager->flush(); + + $log->insert('مدیریت واردات', 'پرونده واردات جدید با کد ' . $workflow->getCode() . ' ایجاد شد.', $user, $request->headers->get('activeBid')); + + return $this->json([ + 'Success' => true, + 'ErrorCode' => 0, + 'ErrorMessage' => '', + 'Result' => [ + 'id' => $workflow->getId(), + 'code' => $workflow->getCode() + ] + ]); + } + + #[Route('/api/import-workflow/stats', name: 'api_import_workflow_stats', methods: ['GET'])] + public function stats( + Request $request, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $repository = $entityManager->getRepository(ImportWorkflow::class); + $workflows = $repository->findByBusiness($acc['bid']->getId()); + + $totalWorkflows = count($workflows); + $draftWorkflows = 0; + $processingWorkflows = 0; + $completedWorkflows = 0; + + foreach ($workflows as $workflow) { + switch ($workflow->getStatus()) { + case 'draft': + $draftWorkflows++; + break; + case 'processing': + $processingWorkflows++; + break; + case 'completed': + $completedWorkflows++; + break; + } + } + + return $this->json([ + 'Success' => true, + 'ErrorCode' => 0, + 'ErrorMessage' => '', + 'Result' => [ + 'totalWorkflows' => $totalWorkflows, + 'draftWorkflows' => $draftWorkflows, + 'processingWorkflows' => $processingWorkflows, + 'completedWorkflows' => $completedWorkflows + ] + ]); + } + + #[Route('/api/import-workflow/{id}', name: 'api_import_workflow_get', methods: ['GET'])] + public function get( + string $id, + Request $request, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $workflowId = (int) $id; + $repository = $entityManager->getRepository(ImportWorkflow::class); + $workflow = $repository->findWithDetails($workflowId, $acc['bid']->getId()); + + if (!$workflow) { + return $this->json([ + 'Success' => false, + 'ErrorCode' => 404, + 'ErrorMessage' => 'پرونده واردات یافت نشد', + 'Result' => null + ]); + } + + $data = [ + 'id' => $workflow->getId(), + 'code' => $workflow->getCode(), + 'title' => $workflow->getTitle(), + 'status' => $workflow->getStatus(), + 'dateSubmit' => $workflow->getDateSubmit(), + 'description' => $workflow->getDescription(), + 'supplierName' => $workflow->getSupplierName(), + 'supplierCountry' => $workflow->getSupplierCountry(), + 'supplierAddress' => $workflow->getSupplierAddress(), + 'supplierPhone' => $workflow->getSupplierPhone(), + 'supplierEmail' => $workflow->getSupplierEmail(), + 'totalAmount' => $workflow->getTotalAmount(), + 'currency' => $workflow->getCurrency(), + 'exchangeRate' => $workflow->getExchangeRate(), + 'totalAmountIRR' => $workflow->getTotalAmountIRR(), + 'submitter' => $workflow->getSubmitter()->getFullName(), + 'items' => [], + 'payments' => [], + 'documents' => [], + 'stages' => [], + 'shipping' => [], + 'customs' => [] + ]; + + foreach ($workflow->getItems() as $item) { + $data['items'][] = [ + 'id' => $item->getId(), + 'commodity' => $item->getCommodity() ? [ + 'id' => $item->getCommodity()->getId(), + 'code' => method_exists($item->getCommodity(), 'getCode') ? $item->getCommodity()->getCode() : null, + 'name' => method_exists($item->getCommodity(), 'getName') ? $item->getCommodity()->getName() : null, + ] : null, + 'name' => $item->getName(), + 'productCode' => $item->getProductCode(), + 'brand' => $item->getBrand(), + 'model' => $item->getModel(), + 'originCountry' => $item->getOriginCountry(), + 'quantity' => $item->getQuantity(), + 'unitPrice' => $item->getUnitPrice(), + 'unitPriceIRR' => $item->getUnitPriceIRR(), + 'totalPrice' => $item->getTotalPrice(), + 'totalPriceIRR' => $item->getTotalPriceIRR(), + 'weight' => $item->getWeight(), + 'volume' => $item->getVolume(), + 'description' => $item->getDescription(), + 'specifications' => $item->getSpecifications(), + 'dateSubmit' => $item->getDateSubmit() + ]; + } + + foreach ($workflow->getPayments() as $payment) { + $data['payments'][] = [ + 'id' => $payment->getId(), + 'type' => $payment->getType(), + 'amount' => $payment->getAmount(), + 'currency' => $payment->getCurrency(), + 'amountIRR' => $payment->getAmountIRR(), + 'paymentDate' => $payment->getPaymentDate(), + 'referenceNumber' => $payment->getReferenceNumber(), + 'bankName' => $payment->getBankName(), + 'accountNumber' => $payment->getAccountNumber(), + 'recipientName' => $payment->getRecipientName(), + 'status' => $payment->getStatus(), + 'description' => $payment->getDescription(), + 'receiptNumber' => $payment->getReceiptNumber(), + 'dateSubmit' => $payment->getDateSubmit() + ]; + } + + foreach ($workflow->getDocuments() as $document) { + $data['documents'][] = [ + 'id' => $document->getId(), + 'type' => $document->getType(), + 'title' => $document->getTitle(), + 'filePath' => $document->getFilePath(), // relative storage path + 'fileName' => $document->getFileName(), + 'fileSize' => $document->getFileSize(), + 'fileType' => $document->getFileType(), + 'description' => $document->getDescription(), + 'documentNumber' => $document->getDocumentNumber(), + 'issueDate' => $document->getIssueDate(), + 'status' => $document->getStatus(), + 'dateSubmit' => $document->getDateSubmit() + ]; + } + + foreach ($workflow->getStages() as $stage) { + $data['stages'][] = [ + 'id' => $stage->getId(), + 'stage' => $stage->getStage(), + 'status' => $stage->getStatus(), + 'startDate' => $stage->getStartDate(), + 'endDate' => $stage->getEndDate(), + 'description' => $stage->getDescription(), + 'assignedTo' => $stage->getAssignedTo(), + 'notes' => $stage->getNotes(), + 'dateSubmit' => $stage->getDateSubmit() + ]; + } + + foreach ($workflow->getShipping() as $shipping) { + $data['shipping'][] = [ + 'id' => $shipping->getId(), + 'type' => $shipping->getType(), + 'containerNumber' => $shipping->getContainerNumber(), + 'billOfLading' => $shipping->getBillOfLading(), + 'shippingDate' => $shipping->getShippingDate(), + 'arrivalDate' => $shipping->getArrivalDate(), + 'unloadingDate' => $shipping->getUnloadingDate(), + 'shippingCompany' => $shipping->getShippingCompany(), + 'shippingCompanyPhone' => $shipping->getShippingCompanyPhone(), + 'shippingCompanyEmail' => $shipping->getShippingCompanyEmail(), + 'originPort' => $shipping->getOriginPort(), + 'destinationPort' => $shipping->getDestinationPort(), + 'vesselName' => $shipping->getVesselName(), + 'voyageNumber' => $shipping->getVoyageNumber(), + 'description' => $shipping->getDescription(), + 'status' => $shipping->getStatus(), + 'dateSubmit' => $shipping->getDateSubmit() + ]; + } + + foreach ($workflow->getCustoms() as $customs) { + $data['customs'][] = [ + 'id' => $customs->getId(), + 'declarationNumber' => $customs->getDeclarationNumber(), + 'customsCode' => $customs->getCustomsCode(), + 'clearanceDate' => $customs->getClearanceDate(), + 'customsDuty' => $customs->getCustomsDuty(), + 'valueAddedTax' => $customs->getValueAddedTax(), + 'otherCharges' => $customs->getOtherCharges(), + 'totalCustomsCharges' => $customs->getTotalCustomsCharges(), + 'customsBroker' => $customs->getCustomsBroker(), + 'customsBrokerPhone' => $customs->getCustomsBrokerPhone(), + 'customsBrokerEmail' => $customs->getCustomsBrokerEmail(), + 'warehouseNumber' => $customs->getWarehouseNumber(), + 'warehouseLocation' => $customs->getWarehouseLocation(), + 'description' => $customs->getDescription(), + 'status' => $customs->getStatus(), + 'dateSubmit' => $customs->getDateSubmit() + ]; + } + + return $this->json([ + 'Success' => true, + 'ErrorCode' => 0, + 'ErrorMessage' => '', + 'Result' => $data + ]); + } + + // Documents CRUD + #[Route('/api/import-workflow/{id}/documents/create', name: 'api_import_workflow_document_create', methods: ['POST'])] + public function createDocument(string $id, Request $request, Access $access, EntityManagerInterface $entityManager, FileStorage $storage): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = $request->request->all(); + $doc = new ImportWorkflowDocument(); + $doc->setImportWorkflow($workflow); + $doc->setType($data['type'] ?? 'other'); + $doc->setTitle($data['title'] ?? ''); + $doc->setDocumentNumber($data['documentNumber'] ?? null); + $doc->setIssueDate($this->jalaliToGregorian($data['issueDate']) ?? null); + $doc->setDescription($data['description'] ?? null); + $doc->setStatus('active'); + // handle file + $file = $request->files->get('file'); + if ($file) { + $stored = $storage->store($file, (string)$acc['bid']->getId(), 'import_docs'); + $doc->setFilePath($stored['relativePath']); + $doc->setFileName($stored['originalName']); + $doc->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null); + $doc->setFileType($stored['mime']); + } + $entityManager->persist($doc); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$doc->getId()]]); + } + + #[Route('/api/import-workflow/{id}/documents/{docId}/update', name: 'api_import_workflow_document_update', methods: ['PUT','POST'])] + public function updateDocument(string $id, string $docId, Request $request, Access $access, EntityManagerInterface $entityManager, FileStorage $storage): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $doc = $entityManager->getRepository(ImportWorkflowDocument::class)->find((int)$docId); + if (!$doc || $doc->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'سند یافت نشد','Result'=>null], 404); + } + $data = $request->request->all(); + if (isset($data['type'])) $doc->setType($data['type']); + if (isset($data['title'])) $doc->setTitle($data['title']); + if (isset($data['documentNumber'])) $doc->setDocumentNumber($data['documentNumber']); + if (isset($data['issueDate'])) $doc->setIssueDate($this->jalaliToGregorian($data['issueDate']) ?? null); + if (isset($data['description'])) $doc->setDescription($data['description']); + $file = $request->files->get('file'); + if ($file) { + $stored = $storage->store($file, (string)$acc['bid']->getId(), 'import_docs'); + $doc->setFilePath($stored['relativePath']); + $doc->setFileName($stored['originalName']); + $doc->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null); + $doc->setFileType($stored['mime']); + } + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$doc->getId()]]); + } + + #[Route('/api/import-workflow/{id}/documents/{docId}/delete', name: 'api_import_workflow_document_delete', methods: ['DELETE'])] + public function deleteDocument(string $id, string $docId, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $doc = $entityManager->getRepository(ImportWorkflowDocument::class)->find((int)$docId); + if (!$doc || $doc->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'سند یافت نشد','Result'=>null], 404); + } + $entityManager->remove($doc); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + // Stages CRUD + #[Route('/api/import-workflow/{id}/stages/create', name: 'api_import_workflow_stage_create', methods: ['POST'])] + public function createStage(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + $st = new ImportWorkflowStage(); + $st->setImportWorkflow($workflow); + $st->setStage($data['stage'] ?? ''); + $st->setStatus($data['status'] ?? 'pending'); + $st->setStartDate(isset($data['startDate']) ? ($this->jalaliToGregorian($data['startDate']) ?? null) : null); + $st->setEndDate(isset($data['endDate']) ? ($this->jalaliToGregorian($data['endDate']) ?? null) : null); + $st->setDescription($data['description'] ?? null); + $st->setAssignedTo($data['assignedTo'] ?? null); + $st->setNotes($data['notes'] ?? null); + $entityManager->persist($st); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$st->getId()]]); + } + + #[Route('/api/import-workflow/{id}/stages/{sid}/update', name: 'api_import_workflow_stage_update', methods: ['PUT'])] + public function updateStage(string $id, string $sid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $st = $entityManager->getRepository(ImportWorkflowStage::class)->find((int)$sid); + if (!$st || $st->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'مرحله یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['stage'])) $st->setStage($data['stage']); + if (isset($data['status'])) $st->setStatus($data['status']); + if (isset($data['startDate'])) $st->setStartDate($this->jalaliToGregorian($data['startDate']) ?? null); + if (isset($data['endDate'])) $st->setEndDate($this->jalaliToGregorian($data['endDate']) ?? null); + if (isset($data['description'])) $st->setDescription($data['description']); + if (isset($data['assignedTo'])) $st->setAssignedTo($data['assignedTo']); + if (isset($data['notes'])) $st->setNotes($data['notes']); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$st->getId()]]); + } + + #[Route('/api/import-workflow/{id}/stages/{sid}/delete', name: 'api_import_workflow_stage_delete', methods: ['DELETE'])] + public function deleteStage(string $id, string $sid, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $st = $entityManager->getRepository(ImportWorkflowStage::class)->find((int)$sid); + if (!$st || $st->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'مرحله یافت نشد','Result'=>null], 404); + } + $entityManager->remove($st); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + // Shipping CRUD + #[Route('/api/import-workflow/{id}/shipping/create', name: 'api_import_workflow_shipping_create', methods: ['POST'])] + public function createShipping(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + $sh = new ImportWorkflowShipping(); + $sh->setImportWorkflow($workflow); + $sh->setType($data['type'] ?? 'sea'); + $sh->setContainerNumber($data['containerNumber'] ?? null); + $sh->setBillOfLading($data['billOfLading'] ?? null); + $sh->setShippingDate(isset($data['shippingDate']) ? ($this->jalaliToGregorian($data['shippingDate']) ?? null) : null); + $sh->setArrivalDate(isset($data['arrivalDate']) ? ($this->jalaliToGregorian($data['arrivalDate']) ?? null) : null); + $sh->setUnloadingDate(isset($data['unloadingDate']) ? ($this->jalaliToGregorian($data['unloadingDate']) ?? null) : null); + $sh->setShippingCompany($data['shippingCompany'] ?? null); + $sh->setShippingCompanyPhone($data['shippingCompanyPhone'] ?? null); + $sh->setShippingCompanyEmail($data['shippingCompanyEmail'] ?? null); + $sh->setOriginPort($data['originPort'] ?? null); + $sh->setDestinationPort($data['destinationPort'] ?? null); + $sh->setVesselName($data['vesselName'] ?? null); + $sh->setVoyageNumber($data['voyageNumber'] ?? null); + $sh->setDescription($data['description'] ?? null); + $sh->setStatus($data['status'] ?? 'active'); + $entityManager->persist($sh); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$sh->getId()]]); + } + + #[Route('/api/import-workflow/{id}/shipping/{sid}/update', name: 'api_import_workflow_shipping_update', methods: ['PUT','POST'])] + public function updateShipping(string $id, string $sid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $sh = $entityManager->getRepository(ImportWorkflowShipping::class)->find((int)$sid); + if (!$sh || $sh->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات حمل یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['type'])) $sh->setType($data['type']); + if (isset($data['containerNumber'])) $sh->setContainerNumber($data['containerNumber']); + if (isset($data['billOfLading'])) $sh->setBillOfLading($data['billOfLading']); + if (isset($data['shippingDate'])) $sh->setShippingDate($this->jalaliToGregorian($data['shippingDate']) ?? null); + if (isset($data['arrivalDate'])) $sh->setArrivalDate($this->jalaliToGregorian($data['arrivalDate']) ?? null); + if (isset($data['unloadingDate'])) $sh->setUnloadingDate($this->jalaliToGregorian($data['unloadingDate']) ?? null); + if (isset($data['shippingCompany'])) $sh->setShippingCompany($data['shippingCompany']); + if (isset($data['shippingCompanyPhone'])) $sh->setShippingCompanyPhone($data['shippingCompanyPhone']); + if (isset($data['shippingCompanyEmail'])) $sh->setShippingCompanyEmail($data['shippingCompanyEmail']); + if (isset($data['originPort'])) $sh->setOriginPort($data['originPort']); + if (isset($data['destinationPort'])) $sh->setDestinationPort($data['destinationPort']); + if (isset($data['vesselName'])) $sh->setVesselName($data['vesselName']); + if (isset($data['voyageNumber'])) $sh->setVoyageNumber($data['voyageNumber']); + if (isset($data['description'])) $sh->setDescription($data['description']); + if (isset($data['status'])) $sh->setStatus($data['status']); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$sh->getId()]]); + } + + #[Route('/api/import-workflow/{id}/shipping/{sid}/delete', name: 'api_import_workflow_shipping_delete', methods: ['DELETE'])] + public function deleteShipping(string $id, string $sid, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $sh = $entityManager->getRepository(ImportWorkflowShipping::class)->find((int)$sid); + if (!$sh || $sh->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات حمل یافت نشد','Result'=>null], 404); + } + $entityManager->remove($sh); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + // Customs CRUD + #[Route('/api/import-workflow/{id}/customs/create', name: 'api_import_workflow_customs_create', methods: ['POST'])] + public function createCustoms(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + $c = new ImportWorkflowCustoms(); + $c->setImportWorkflow($workflow); + $c->setDeclarationNumber($data['declarationNumber'] ?? ''); + $c->setCustomsCode($data['customsCode'] ?? null); + $c->setClearanceDate(isset($data['clearanceDate']) ? ($this->jalaliToGregorian($data['clearanceDate']) ?? null) : null); + $c->setCustomsDuty(isset($data['customsDuty']) ? (string)$data['customsDuty'] : null); + $c->setValueAddedTax(isset($data['valueAddedTax']) ? (string)$data['valueAddedTax'] : null); + $c->setOtherCharges(isset($data['otherCharges']) ? (string)$data['otherCharges'] : null); + $c->setTotalCustomsCharges(isset($data['totalCustomsCharges']) ? (string)$data['totalCustomsCharges'] : null); + $c->setCustomsBroker($data['customsBroker'] ?? null); + $c->setCustomsBrokerPhone($data['customsBrokerPhone'] ?? null); + $c->setCustomsBrokerEmail($data['customsBrokerEmail'] ?? null); + $c->setWarehouseNumber($data['warehouseNumber'] ?? null); + $c->setWarehouseLocation($data['warehouseLocation'] ?? null); + $c->setDescription($data['description'] ?? null); + $c->setStatus($data['status'] ?? 'active'); + $entityManager->persist($c); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$c->getId()]]); + } + + #[Route('/api/import-workflow/{id}/customs/{cid}/update', name: 'api_import_workflow_customs_update', methods: ['PUT','POST'])] + public function updateCustoms(string $id, string $cid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $c = $entityManager->getRepository(ImportWorkflowCustoms::class)->find((int)$cid); + if (!$c || $c->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات ترخیص یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['declarationNumber'])) $c->setDeclarationNumber($data['declarationNumber']); + if (isset($data['customsCode'])) $c->setCustomsCode($data['customsCode']); + if (isset($data['clearanceDate'])) $c->setClearanceDate($this->jalaliToGregorian($data['clearanceDate']) ?? null); + if (isset($data['customsDuty'])) $c->setCustomsDuty((string)$data['customsDuty']); + if (isset($data['valueAddedTax'])) $c->setValueAddedTax((string)$data['valueAddedTax']); + if (isset($data['otherCharges'])) $c->setOtherCharges((string)$data['otherCharges']); + if (isset($data['totalCustomsCharges'])) $c->setTotalCustomsCharges((string)$data['totalCustomsCharges']); + if (isset($data['customsBroker'])) $c->setCustomsBroker($data['customsBroker']); + if (isset($data['customsBrokerPhone'])) $c->setCustomsBrokerPhone($data['customsBrokerPhone']); + if (isset($data['customsBrokerEmail'])) $c->setCustomsBrokerEmail($data['customsBrokerEmail']); + if (isset($data['warehouseNumber'])) $c->setWarehouseNumber($data['warehouseNumber']); + if (isset($data['warehouseLocation'])) $c->setWarehouseLocation($data['warehouseLocation']); + if (isset($data['description'])) $c->setDescription($data['description']); + if (isset($data['status'])) $c->setStatus($data['status']); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$c->getId()]]); + } + + #[Route('/api/import-workflow/{id}/customs/{cid}/delete', name: 'api_import_workflow_customs_delete', methods: ['DELETE'])] + public function deleteCustoms(string $id, string $cid, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $c = $entityManager->getRepository(ImportWorkflowCustoms::class)->find((int)$cid); + if (!$c || $c->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات ترخیص یافت نشد','Result'=>null], 404); + } + $entityManager->remove($c); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + // Payments CRUD + #[Route('/api/import-workflow/{id}/payments/create', name: 'api_import_workflow_payment_create', methods: ['POST'])] + public function createPayment(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + $p = new ImportWorkflowPayment(); + $p->setImportWorkflow($workflow); + $p->setType($data['type'] ?? 'other'); + $p->setAmount((string)($data['amount'] ?? '0')); + $p->setCurrency($data['currency'] ?? 'IRR'); + $p->setAmountIRR(isset($data['amountIRR']) ? (string)$data['amountIRR'] : null); + $p->setPaymentDate($this->jalaliToGregorian($data['paymentDate']) ?? date('Y-m-d')); + $p->setReferenceNumber($data['referenceNumber'] ?? null); + $p->setBankName($data['bankName'] ?? null); + $p->setAccountNumber($data['accountNumber'] ?? null); + $p->setRecipientName($data['recipientName'] ?? null); + $p->setStatus($data['status'] ?? 'pending'); + $p->setDescription($data['description'] ?? null); + $p->setReceiptNumber($data['receiptNumber'] ?? null); + $entityManager->persist($p); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$p->getId()]]); + } + + #[Route('/api/import-workflow/{id}/payments/{pid}/update', name: 'api_import_workflow_payment_update', methods: ['PUT'])] + public function updatePayment(string $id, string $pid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $p = $entityManager->getRepository(ImportWorkflowPayment::class)->find((int)$pid); + if (!$p || $p->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرداخت یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['type'])) $p->setType($data['type']); + if (isset($data['amount'])) $p->setAmount((string)$data['amount']); + if (isset($data['currency'])) $p->setCurrency($data['currency']); + if (isset($data['amountIRR'])) $p->setAmountIRR((string)$data['amountIRR']); + if (isset($data['paymentDate'])) $p->setPaymentDate($this->jalaliToGregorian($data['paymentDate']) ?? date('Y-m-d')); + if (isset($data['referenceNumber'])) $p->setReferenceNumber($data['referenceNumber']); + if (isset($data['bankName'])) $p->setBankName($data['bankName']); + if (isset($data['accountNumber'])) $p->setAccountNumber($data['accountNumber']); + if (isset($data['recipientName'])) $p->setRecipientName($data['recipientName']); + if (isset($data['status'])) $p->setStatus($data['status']); + if (isset($data['description'])) $p->setDescription($data['description']); + if (isset($data['receiptNumber'])) $p->setReceiptNumber($data['receiptNumber']); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$p->getId()]]); + } + + #[Route('/api/import-workflow/{id}/payments/{pid}/delete', name: 'api_import_workflow_payment_delete', methods: ['DELETE'])] + public function deletePayment(string $id, string $pid, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $p = $entityManager->getRepository(ImportWorkflowPayment::class)->find((int)$pid); + if (!$p || $p->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرداخت یافت نشد','Result'=>null], 404); + } + $entityManager->remove($p); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + #[Route('/api/import-workflow/{id}/items/create', name: 'api_import_workflow_item_create', methods: ['POST'])] + public function createItem( + string $id, + Request $request, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + + $data = json_decode($request->getContent() ?: '{}', true); + $commodityId = isset($data['commodity_id']) ? (int)$data['commodity_id'] : 0; + if ($commodityId <= 0) { + return $this->json(['Success'=>false,'ErrorCode'=>400,'ErrorMessage'=>'کالا انتخاب نشده است','Result'=>null], 400); + } + $commodity = $entityManager->getRepository(Commodity::class)->find($commodityId); + if (!$commodity || $commodity->getBid()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'کالا یافت نشد','Result'=>null], 404); + } + + $item = new ImportWorkflowItem(); + $item->setImportWorkflow($workflow); + $item->setCommodity($commodity); + $item->setName($data['name'] ?? ($commodity->getName() ?? '')); + $item->setProductCode($data['productCode'] ?? ($commodity->getCode() ?? '')); + $item->setBrand($data['brand'] ?? null); + $item->setModel($data['model'] ?? null); + $item->setOriginCountry($data['originCountry'] ?? null); + $item->setQuantity($data['quantity'] ?? '0'); + $item->setUnitPrice($data['unitPrice'] ?? null); + $item->setUnitPriceIRR($data['unitPriceIRR'] ?? null); + $item->setTotalPrice($data['totalPrice'] ?? null); + $item->setTotalPriceIRR($data['totalPriceIRR'] ?? null); + $item->setWeight($data['weight'] ?? null); + $item->setVolume($data['volume'] ?? null); + $item->setDescription($data['description'] ?? null); + $item->setSpecifications($data['specifications'] ?? null); + + $entityManager->persist($item); + $entityManager->flush(); + + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$item->getId()]]); + } + + #[Route('/api/import-workflow/{id}/items/{itemId}/update', name: 'api_import_workflow_item_update', methods: ['PUT'])] + public function updateItem( + string $id, + string $itemId, + Request $request, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $item = $entityManager->getRepository(ImportWorkflowItem::class)->find((int)$itemId); + if (!$item || $item->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'آیتم یافت نشد','Result'=>null], 404); + } + + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['commodity_id'])) { + $commodity = $entityManager->getRepository(Commodity::class)->find((int)$data['commodity_id']); + if ($commodity && $commodity->getBid()->getId() === $acc['bid']->getId()) { + $item->setCommodity($commodity); + if (!isset($data['name'])) $item->setName($commodity->getName()); + if (!isset($data['productCode'])) $item->setProductCode($commodity->getCode()); + } + } + if (isset($data['name'])) $item->setName($data['name']); + if (isset($data['productCode'])) $item->setProductCode($data['productCode']); + if (isset($data['brand'])) $item->setBrand($data['brand']); + if (isset($data['model'])) $item->setModel($data['model']); + if (isset($data['originCountry'])) $item->setOriginCountry($data['originCountry']); + if (isset($data['quantity'])) $item->setQuantity($data['quantity']); + if (isset($data['unitPrice'])) $item->setUnitPrice($data['unitPrice']); + if (isset($data['unitPriceIRR'])) $item->setUnitPriceIRR($data['unitPriceIRR']); + if (isset($data['totalPrice'])) $item->setTotalPrice($data['totalPrice']); + if (isset($data['totalPriceIRR'])) $item->setTotalPriceIRR($data['totalPriceIRR']); + if (isset($data['weight'])) $item->setWeight($data['weight']); + if (isset($data['volume'])) $item->setVolume($data['volume']); + if (isset($data['description'])) $item->setDescription($data['description']); + if (isset($data['specifications'])) $item->setSpecifications($data['specifications']); + + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$item->getId()]]); + } + + #[Route('/api/import-workflow/{id}/items/{itemId}/delete', name: 'api_import_workflow_item_delete', methods: ['DELETE'])] + public function deleteItem( + string $id, + string $itemId, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $item = $entityManager->getRepository(ImportWorkflowItem::class)->find((int)$itemId); + if (!$item || $item->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'آیتم یافت نشد','Result'=>null], 404); + } + $entityManager->remove($item); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + #[Route('/api/import-workflow/{id}/update', name: 'api_import_workflow_update', methods: ['PUT'])] + public function update( + string $id, + Request $request, + Access $access, + Log $log, + EntityManagerInterface $entityManager, + #[CurrentUser] $user + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $workflowId = (int) $id; + $repository = $entityManager->getRepository(ImportWorkflow::class); + $workflow = $repository->find($workflowId); + + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json([ + 'Success' => false, + 'ErrorCode' => 404, + 'ErrorMessage' => 'پرونده واردات یافت نشد', + 'Result' => null + ]); + } + + $data = json_decode($request->getContent(), true); + + if (isset($data['title'])) $workflow->setTitle($data['title']); + if (isset($data['description'])) $workflow->setDescription($data['description']); + if (isset($data['supplierName'])) $workflow->setSupplierName($data['supplierName']); + if (isset($data['supplierCountry'])) $workflow->setSupplierCountry($data['supplierCountry']); + if (isset($data['supplierAddress'])) $workflow->setSupplierAddress($data['supplierAddress']); + if (isset($data['supplierPhone'])) $workflow->setSupplierPhone($data['supplierPhone']); + if (isset($data['supplierEmail'])) $workflow->setSupplierEmail($data['supplierEmail']); + if (isset($data['totalAmount'])) $workflow->setTotalAmount($data['totalAmount']); + if (isset($data['currency'])) $workflow->setCurrency($data['currency']); + if (isset($data['exchangeRate'])) $workflow->setExchangeRate($data['exchangeRate']); + if (isset($data['totalAmountIRR'])) $workflow->setTotalAmountIRR($data['totalAmountIRR']); + if (isset($data['status'])) $workflow->setStatus($data['status']); + + $workflow->setDateMod(date('Y-m-d H:i:s')); + + $entityManager->flush(); + + $log->insert('مدیریت واردات', 'پرونده واردات با کد ' . $workflow->getCode() . ' ویرایش شد.', $user, $request->headers->get('activeBid')); + + return $this->json([ + 'Success' => true, + 'ErrorCode' => 0, + 'ErrorMessage' => '', + 'Result' => [ + 'id' => $workflow->getId(), + 'code' => $workflow->getCode() + ] + ]); + } + + #[Route('/api/import-workflow/{id}/delete', name: 'api_import_workflow_delete', methods: ['DELETE'])] + public function delete( + string $id, + Request $request, + Access $access, + Log $log, + EntityManagerInterface $entityManager, + #[CurrentUser] $user + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $workflowId = (int) $id; + $repository = $entityManager->getRepository(ImportWorkflow::class); + $workflow = $repository->find($workflowId); + + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json([ + 'Success' => false, + 'ErrorCode' => 404, + 'ErrorMessage' => 'پرونده واردات یافت نشد', + 'Result' => null + ]); + } + + $code = $workflow->getCode(); + $entityManager->remove($workflow); + $entityManager->flush(); + + $log->insert('مدیریت واردات', 'پرونده واردات با کد ' . $code . ' حذف شد.', $user, $request->headers->get('activeBid')); + + return $this->json([ + 'Success' => true, + 'ErrorCode' => 0, + 'ErrorMessage' => '', + 'Result' => 'پرونده واردات با موفقیت حذف شد' + ]); + } + + #[Route('/api/import-workflow/{code}/create-inbound-ticket', name: 'api_import_workflow_create_inbound_ticket', methods: ['POST'])] + public function createInboundTicket( + string $code, + Request $request, + Access $access, + Log $log, + Provider $provider, + EntityManagerInterface $entityManager, + #[CurrentUser] $user + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $params = json_decode($request->getContent() ?: '{}', true); + $storeroomId = $params['storeroom_id'] ?? null; + $personId = $params['person_id'] ?? null; + if (!$storeroomId || !$personId) { + return $this->json(['Success'=>false,'ErrorCode'=>400,'ErrorMessage'=>'پارامترهای ناقص','Result'=>null], 400); + } + + $workflow = $entityManager->getRepository(ImportWorkflow::class)->findOneBy(['code'=>$code, 'business'=>$acc['bid']]); + if (!$workflow) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $storeroom = $entityManager->getRepository(Storeroom::class)->find($storeroomId); + $person = $entityManager->getRepository(Person::class)->find($personId); + if (!$storeroom || !$person) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'انبار یا طرف حساب یافت نشد','Result'=>null], 404); + } + + $ticket = new StoreroomTicket(); + $ticket->setSubmitter($user); + $ticket->setDate($workflow->getDateSubmit()); + $ticket->setBid($acc['bid']); + $ticket->setDateSubmit((string) time()); + $ticket->setDoc(null); + $ticket->setSenderTel(null); + $ticket->setTransfer(null); + $ticket->setYear($acc['year']); + $ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom')); + $ticket->setReceiver($person->getNikename()); + $ticket->setTransferType($entityManager->getRepository(\App\Entity\StoreroomTransferType::class)->findOneBy([])); + $ticket->setReferral(null); + $ticket->setStoreroom($storeroom); + $ticket->setPerson($person); + $ticket->setType('input'); + $ticket->setTypeString('ورود از واردات'); + $ticket->setDes('ورود از پرونده واردات #' . $workflow->getCode()); + // $ticket->setStatus('in_progress'); + $ticket->setImportWorkflowCode($workflow->getCode()); + $entityManager->persist($ticket); + $entityManager->flush(); + + $log->insert('مدیریت واردات', 'ایجاد حواله ورود به انبار از پرونده ' . $workflow->getCode(), $user, $acc['bid']); + + return $this->json([ + 'Success'=>true, + 'ErrorCode'=>0, + 'ErrorMessage'=>'', + 'Result'=>[ + 'ticketCode'=>$ticket->getCode() + ] + ]); + } + + private function jalaliToGregorian(?string $input): ?string + { + if (!$input) { + return null; + } + + $s = trim($input); + + // پشتیبانی از جلالی با جداکننده '/' + if (preg_match('/^(\d{4})\/(\d{2})\/(\d{2})$/', $s, $m)) { + $y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3]; + try { + $g = CalendarUtils::toGregorian($y, $mo, $d); + return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]); + } catch (\Throwable $e) { + return $s; + } + } + + // پشتیبانی از جلالی با جداکننده '-' + if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $s, $m)) { + $y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3]; + // اگر سال جلالی باشد (مثلاً 1400) آن را تبدیل کن، در غیر اینصورت همان مقدار را برگردان + if ($y < 1700) { + try { + $g = CalendarUtils::toGregorian($y, $mo, $d); + return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]); + } catch (\Throwable $e) { + return $s; + } + } + return $s; // احتمالاً میلادی است + } + + return $s; + } +} diff --git a/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php b/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php index 4e8c334..ccb9735 100644 --- a/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php +++ b/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php @@ -2,6 +2,8 @@ namespace App\Controller\Plugins; +use App\Repository\PlugWarrantySerialRepository; +use Morilog\Jalali\CalendarUtils; use Symfony\Component\Routing\Annotation\Route; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; @@ -9,78 +11,466 @@ use Symfony\Component\HttpFoundation\Request; use Doctrine\ORM\EntityManagerInterface; use App\Entity\PlugWarrantySerial; use App\Entity\Commodity; +use App\Entity\HesabdariDoc; +use App\Entity\Business; use App\Service\Access; use App\Service\Log; use App\Service\Provider; use App\Service\Jdate; +use PhpOffice\PhpSpreadsheet\IOFactory; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use App\Service\PluginService; +use App\Service\SMS; +use App\Service\registryMGR; +use Symfony\Component\Validator\Constraints as Assert; +use App\Entity\StoreroomTicket; class PlugWarrantyController extends AbstractController { private $entityManager; - public function __construct(EntityManagerInterface $entityManager) + private function toDate(?string $s): ?\DateTimeImmutable + { + if (!$s) + return null; + try { + return new \DateTimeImmutable($s); + } catch (\Throwable $e) { + return null; + } + } + + #[Route('/api/plugins/warranty/serials/by-storeroom-ticket/{code}', name: 'plugin_warranty_serials_by_storeroom_ticket', methods: ['GET'])] + public function plugin_warranty_serials_by_storeroom_ticket(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, PluginService $pluginService): JsonResponse + { + $acc = $access->hasRole('store'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + if (!$pluginService->isActive('warranty', $acc['bid'])) { + return $this->json(['success' => false, 'message' => 'افزونه گارانتی فعال نیست'], 403); + } + + /** @var StoreroomTicket|null $ticket */ + $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $code, + ]); + if (!$ticket) { + return $this->json(['success' => false, 'message' => 'حواله یافت نشد'], 404); + } + + $doc = $ticket->getDoc(); + if (!$doc) { + return $this->json([ + 'success' => true, + 'ticketActivationCode' => $ticket->getActivationCode(), + 'items' => [] + ]); + } + + $serials = $entityManager->getRepository(PlugWarrantySerial::class)->createQueryBuilder('s') + ->andWhere('s.business = :bid') + ->andWhere('s.allocatedToDocumentId = :docId') + ->setParameter('bid', $acc['bid']) + ->setParameter('docId', $doc->getId()) + ->getQuery() + ->getResult(); + + $items = array_map(function (PlugWarrantySerial $s) use ($entityManager) { + $commodity = $s->getCommodity(); + return [ + 'serialNumber' => $s->getSerialNumber(), + 'commodity' => $commodity ? [ + 'id' => $commodity->getId(), + 'name' => $commodity->getName(), + 'code' => $commodity->getCode(), + ] : null, + 'status' => $s->getStatus(), + 'activation' => $s->getActivation(), + 'activationTicketCode' => $s->getActivationTicketCode(), + 'warrantyEndDate' => $s->getWarrantyEndDate()?->format('Y-m-d'), + ]; + }, $serials); + + return $this->json([ + 'success' => true, + 'ticketActivationCode' => $ticket->getActivationCode(), + 'items' => $items + ]); + } + + private function expiredFlag(?\DateTimeImmutable $end): bool + { + return $end !== null && $end < new \DateTimeImmutable('today'); + } + + public function __construct(EntityManagerInterface $entityManager, private PlugWarrantySerialRepository $repository) { $this->entityManager = $entityManager; } - private function updateExpiredSerials(EntityManagerInterface $entityManager, $businessId): void + #[Route('/api/plugins/warranty/assign/request', name: 'plugin_warranty_assign_request', methods: ['POST'])] + public function plugin_warranty_assign_request(Request $request, EntityManagerInterface $em, Access $access, PluginService $pluginService): JsonResponse { - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - $jdate = new Jdate(); - $today = $jdate->GetTodayDate(); - - $expiredSerials = $repository->createQueryBuilder('s') - ->where('s.bid = :businessId') - ->andWhere('s.status = :status') - ->andWhere('s.warrantyEndDate IS NOT NULL') - ->andWhere('s.warrantyEndDate < :today') - ->setParameter('businessId', $businessId) - ->setParameter('status', 'active') - ->setParameter('today', $today) - ->getQuery() - ->getResult(); - - foreach ($expiredSerials as $serial) { - $serial->setStatus('expired'); - } - - if (!empty($expiredSerials)) { - $entityManager->flush(); + $acc = $access->hasRole('plugWarrantyManager'); + if (!$acc) + throw $this->createAccessDeniedException(); + if (!$pluginService->isActive('warranty', $acc['bid'])) + return $this->json(['success' => false, 'message' => 'افزونه گارانتی فعال نیست'], 403); + + $p = json_decode($request->getContent() ?: '{}', true); + $items = $p['items'] ?? []; // [{commodity_id, count, document_id, document_item_id}] + if (!is_array($items) || empty($items)) + return $this->json(['success' => false, 'message' => 'پارامترهای نامعتبر'], 400); + + $em->beginTransaction(); + try { + $business = $em->getRepository(Business::class)->find($acc['bid']); + if (!$business) + return $this->json(['success' => false, 'message' => 'کسب‌وکار یافت نشد'], 404); + + $result = []; + + foreach ($items as $i) { + $commodityId = (int) ($i['commodity_id'] ?? 0); + $qty = (int) ($i['count'] ?? 0); + $documentId = isset($i['document_id']) ? (int) $i['document_id'] : null; + + if ($commodityId <= 0 || $qty <= 0) + continue; + + // انتخاب N کد available با قفل + $ids = $em->getConnection()->executeQuery( + "SELECT id FROM plug_warranty_serial + WHERE commodity_id = :cid AND status = 'available' + ORDER BY id ASC + FOR UPDATE SKIP LOCKED + LIMIT :lim", + ['cid' => $commodityId, 'lim' => $qty], + ['cid' => \PDO::PARAM_INT, 'lim' => \PDO::PARAM_INT] + )->fetchFirstColumn(); + + if (count($ids) < $qty) { + $em->rollBack(); + return $this->json(['success' => false, 'message' => 'کد گارانتی کافی برای یکی از اقلام موجود نیست'], 409); + } + + $now = new \DateTimeImmutable(); + $em->createQuery('UPDATE App\Entity\PlugWarrantySerial s SET s.status = :st, s.allocatedToDocumentId = :doc, s.allocatedAt = :at WHERE s.id IN (:ids)') + ->setParameter('st', PlugWarrantySerial::STATUS_ALLOCATED) + ->setParameter('doc', $documentId) + ->setParameter('at', $now) + ->setParameter('ids', $ids) + ->execute(); + + $result[] = [ + 'commodity_id' => $commodityId, + 'allocated' => $ids + ]; + } + + $em->commit(); + return $this->json(['success' => true, 'allocated' => $result]); + } catch (\Throwable $e) { + $em->rollBack(); + return $this->json(['success' => false, 'message' => $e->getMessage()], 500); } } - private function checkAndUpdateSerialStatus($serial, EntityManagerInterface $entityManager): void + #[Route('/api/plugins/warranty/assign/scan', name: 'plugin_warranty_assign_scan', methods: ['POST'])] + public function plugin_warranty_assign_scan(Request $request, EntityManagerInterface $entityManager, Access $access, PluginService $pluginService): JsonResponse { - $jdate = new Jdate(); - $today = $jdate->GetTodayDate(); - $warrantyEndDate = $serial->getWarrantyEndDate(); - - if ($serial->getStatus() === 'active' && - $warrantyEndDate && - $warrantyEndDate < $today) { - $serial->setStatus('expired'); + try { + $acc = $access->hasRole('plugWarrantyManager'); + if (!$acc) + throw $this->createAccessDeniedException(); + + if (!$pluginService->isActive('warranty', $acc['bid'])) { + return $this->json(['success' => false, 'message' => 'افزونه گارانتی فعال نیست'], 403); + } + + $params = json_decode($request->getContent() ?: '{}', true); + $serialNumber = $params['serialNumber'] ?? null; + $commodityId = isset($params['commodity_id']) ? (int) $params['commodity_id'] : null; + $documentId = isset($params['document_id']) ? (int) $params['document_id'] : null; + $documentItemId = isset($params['document_item_id']) ? (int) $params['document_item_id'] : 0; + $physicalBoxBarcode = $params['physicalBoxBarcode'] ?? ''; + $productTypeCode = $params['productTypeCode'] ?? ''; + + if (!$serialNumber || !$commodityId || !$documentItemId) { + return $this->json(['success' => false, 'message' => 'پارامترهای ناقص'], 400); + } + + $repo = $entityManager->getRepository(PlugWarrantySerial::class); + /** @var PlugWarrantySerial|null $serial */ + $serial = $repo->findOneBy([ + 'serialNumber' => $serialNumber, + 'business' => $acc['bid'] + ]); + + if (!$serial) { + return $this->json(['success' => false, 'message' => 'کد گارانتی یافت نشد']); + } + + if ($serial->getCommodity()?->getId() !== $commodityId) { + return $this->json(['success' => false, 'message' => 'مغایرت نوع محصول']); + } + + if (!in_array($serial->getStatus(), [PlugWarrantySerial::STATUS_ALLOCATED, PlugWarrantySerial::STATUS_VERIFIED], true)) { + return $this->json(['success' => false, 'message' => 'وضعیت کد برای اسکن معتبر نیست'], 409); + } + + if ($documentId !== null && $serial->getAllocatedToDocumentId() !== $documentId) { + return $this->json(['success' => false, 'message' => 'کد به سند دیگری تخصیص داده شده'], 409); + } + + // در این نسخه، صرفاً صحت را تایید می‌کنیم و وضعیت را verified می‌کنیم + $serial->setStatus(PlugWarrantySerial::STATUS_VERIFIED); + $entityManager->persist($serial); $entityManager->flush(); + + return $this->json(['success' => true, 'message' => 'تأیید شد']); + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'message' => $e->getMessage() + ], 500); + } + } + + #[Route('/api/plugins/warranty/serials/by-invoice/{code}', name: 'plugin_warranty_serials_by_invoice', methods: ['GET'])] + public function plugin_warranty_serials_by_invoice(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, PluginService $pluginService): JsonResponse + { + $acc = $access->hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + if (!$pluginService->isActive('warranty', $acc['bid'])) { + return $this->json(['success' => false, 'message' => 'افزونه گارانتی فعال نیست'], 403); + } + + $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $code, + 'money' => $acc['money'] + ]); + if (!$doc) { + return $this->json(['success' => false, 'message' => 'فاکتور یافت نشد'], 404); + } + + // سریال‌هایی که به این فاکتور تخصیص داده شده‌اند + $serialRepo = $entityManager->getRepository(PlugWarrantySerial::class); + $serials = $serialRepo->createQueryBuilder('s') + ->where('s.business = :bid') + ->andWhere('s.allocatedToDocumentId = :docId') + ->setParameter('bid', $acc['bid']) + ->setParameter('docId', $doc->getId()) + ->getQuery() + ->getResult(); + + $result = array_map(function (PlugWarrantySerial $s) { + return [ + 'serialNumber' => $s->getSerialNumber(), + 'commodity' => $s->getCommodity() ? $s->getCommodity()->getName() : null, + 'status' => $s->getStatus(), + 'warrantyEndDate' => $s->getWarrantyEndDate()?->format('Y-m-d'), + 'expired' => $this->expiredFlag($s->getWarrantyEndDate()) + ]; + }, $serials); + + return $this->json(['success' => true, 'items' => $result]); + } + + #[Route('/api/plugins/warranty/send-serials', name: 'plugin_warranty_send_serials', methods: ['POST'])] + public function plugin_warranty_send_serials(Request $request, Access $access, EntityManagerInterface $entityManager, PluginService $pluginService, SMS $SMS, registryMGR $registryMGR): JsonResponse + { + $acc = $access->hasRole('sell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + if (!$pluginService->isActive('warranty', $acc['bid'])) { + return $this->json(['success' => false, 'message' => 'افزونه گارانتی فعال نیست'], 403); + } + + $data = json_decode($request->getContent() ?: '{}', true); + $invoiceCode = $data['invoiceCode'] ?? null; + $mobile = $data['mobile'] ?? null; + if (!$invoiceCode || !$mobile) { + return $this->json(['success' => false, 'message' => 'پارامترهای ناقص'], 400); + } + + $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $invoiceCode, + 'money' => $acc['money'] + ]); + if (!$doc) { + return $this->json(['success' => false, 'message' => 'فاکتور یافت نشد'], 404); + } + + $serialsResponse = $this->plugin_warranty_serials_by_invoice($invoiceCode, $request, $access, $entityManager, $pluginService); + $payload = json_decode($serialsResponse->getContent(), true); + $serials = $payload['items'] ?? []; + if (empty($serials)) { + return $this->json(['success' => false, 'message' => 'سریالی برای ارسال یافت نشد'], 400); + } + + $serialNumbers = array_map(fn($s) => $s['serialNumber'], $serials); + $text = 'سریال‌های گارانتی: ' . implode('، ', $serialNumbers); + + $SMS->sendByBalance( + [$text], + $registryMGR->get('sms', 'sharefaktor'), + $mobile, + $acc['bid'], + $this->getUser(), + 3 + ); + + return $this->json(['success' => true]); + } + + #[Route('/api/plugins/warranty/serials/preview-import', name: 'plugin_warranty_serial_preview_import', methods: ['POST'])] + public function plugin_warranty_serial_preview_import(Request $request, Access $access): JsonResponse + { + try { + $acc = $access->hasRole('plugWarrantyManager'); + if (!$acc) + throw $this->createAccessDeniedException(); + + /** @var UploadedFile|null $file */ + $file = $request->files->get('file'); + if (!$file instanceof UploadedFile) { + return $this->json(['error' => 'فایل ارسال نشده است'], 400); + } + + $spreadsheet = IOFactory::load($file->getPathname()); + $sheet = $spreadsheet->getActiveSheet(); + $rows = $sheet->toArray(null, true, true, true); + + $preview = []; + $errors = []; + + // انتظار ستون‌های: serialNumber | commodity | description | warrantyStartDate | warrantyEndDate | status + // ردیف اول را عنوان فرض می‌کنیم + $isHeader = true; + foreach ($rows as $rowIndex => $row) { + if ($isHeader) { + $isHeader = false; + continue; + } + $serialNumber = trim((string) ($row['A'] ?? '')); + $commodity = trim((string) ($row['B'] ?? '')); + $description = trim((string) ($row['C'] ?? '')); + $warrantyStartDate = trim((string) ($row['D'] ?? '')); + $warrantyEndDate = trim((string) ($row['E'] ?? '')); + $status = trim((string) ($row['F'] ?? 'available')); + + if ($serialNumber === '' || $commodity === '') { + $errors[] = 'ردیف ' . ($rowIndex) . ': شماره سریال یا محصول خالی است'; + continue; + } + + // نگاشت وضعیت‌های قدیمی به جدید + $map = ['active' => 'available', 'inactive' => 'void', 'expired' => 'void']; + $status = $map[$status] ?? 'available'; + + $preview[] = [ + 'serialNumber' => $serialNumber, + 'commodity' => $commodity, + 'description' => $description, + 'warrantyStartDate' => $warrantyStartDate, + 'warrantyEndDate' => $warrantyEndDate, + 'status' => $status + ]; + } + + return $this->json([ + 'preview' => $preview, + 'errors' => $errors + ]); + } catch (\Exception $e) { + return $this->json([ + 'error' => $e->getMessage() + ], 500); + } + } + + #[Route('/api/plugins/warranty/serials/import-excel', name: 'plugin_warranty_serial_import_excel', methods: ['POST'])] + public function plugin_warranty_serial_import_excel(Request $request, EntityManagerInterface $entityManager, Access $access): JsonResponse + { + try { + $acc = $access->hasRole('plugWarrantyManager'); + if (!$acc) + throw $this->createAccessDeniedException(); + + /** @var UploadedFile|null $file */ + $file = $request->files->get('file'); + if (!$file instanceof UploadedFile) { + return $this->json(['error' => 'فایل ارسال نشده است'], 400); + } + + $spreadsheet = IOFactory::load($file->getPathname()); + $sheet = $spreadsheet->getActiveSheet(); + $rows = $sheet->toArray(null, true, true, true); + + $serials = []; + $isHeader = true; + foreach ($rows as $row) { + if ($isHeader) { + $isHeader = false; + continue; + } + $serialNumber = trim((string) ($row['A'] ?? '')); + $commodity = trim((string) ($row['B'] ?? '')); + $description = trim((string) ($row['C'] ?? '')); + $warrantyStartDate = trim((string) ($row['D'] ?? '')); + $warrantyEndDate = trim((string) ($row['E'] ?? '')); + $status = trim((string) ($row['F'] ?? 'available')); + + if ($serialNumber === '' || $commodity === '') { + continue; + } + + // نگاشت وضعیت‌های قدیمی به جدید + $map = ['active' => 'available', 'inactive' => 'void', 'expired' => 'void']; + $status = $map[$status] ?? 'available'; + + $serials[] = [ + 'serialNumber' => $serialNumber, + 'commodity' => $commodity, + 'description' => $description, + 'warrantyStartDate' => $warrantyStartDate, + 'warrantyEndDate' => $warrantyEndDate, + 'status' => $status + ]; + } + + return $this->json([ + 'serials' => $serials + ]); + } catch (\Exception $e) { + return $this->json([ + 'error' => $e->getMessage() + ], 500); } } #[Route('/api/plugins/warranty/serials', name: 'plugin_warranty_serials', methods: ['GET'])] - public function plugin_warranty_serials(EntityManagerInterface $entityManager, Access $access, Request $request): JsonResponse + public function plugin_warranty_serials(EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Request $request): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); - $this->updateExpiredSerials($entityManager, $acc['bid']); - $page = $request->query->get('page', 1); $limit = $request->query->get('limit', 20); $status = $request->query->get('status'); $commodityId = $request->query->get('commodity_id'); $search = $request->query->get('search'); - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - if ($search) { $serials = $repository->searchSerials($acc['bid'], $search); } elseif ($status) { @@ -92,10 +482,13 @@ class PlugWarrantyController extends AbstractController } $data = []; - foreach($serials as $serial){ + foreach ($serials as $serial) { $commodity = $serial->getCommodity(); $submitter = $serial->getSubmitter(); - + $buyer = $serial->getBuyer(); + + $allocatedToDocument = $serial->getAllocatedToDocumentId() ? $entityManager->getRepository(HesabdariDoc::class)->find($serial->getAllocatedToDocumentId()) : null; + $data[] = [ 'id' => $serial->getId(), 'serialNumber' => $serial->getSerialNumber(), @@ -104,16 +497,28 @@ class PlugWarrantyController extends AbstractController 'name' => $commodity->getName(), 'code' => $commodity->getCode() ] : null, - 'dateSubmit' => $serial->getDateSubmit(), + 'dateSubmit' => $serial->getDateSubmit()->format('Y-m-d H:i:s'), 'description' => $serial->getDescription(), - 'warrantyStartDate' => $serial->getWarrantyStartDate(), - 'warrantyEndDate' => $serial->getWarrantyEndDate(), + 'warrantyStartDate' => $serial->getWarrantyStartDate()?->format('Y-m-d'), + 'warrantyEndDate' => $serial->getWarrantyEndDate()?->format('Y-m-d'), 'status' => $serial->getStatus(), + 'activation' => $serial->getActivation(), 'notes' => $serial->getNotes(), + 'expired' => $this->expiredFlag($serial->getWarrantyEndDate()), 'submitter' => $submitter ? [ 'id' => $submitter->getId(), 'name' => $submitter->getFullName() - ] : null + ] : null, + 'commoditySerial' => $serial->getCommoditySerial(), + 'buyer' => $buyer ? [ + 'id' => $buyer->getId(), + 'code' => $buyer->getCode(), + 'name' => $buyer->getName(), + 'nikename' => $buyer->getNikename(), + 'mobile' => $buyer->getMobile() + ] : null, + 'activationAt' => $serial->getActivationAt()?->format('Y-m-d H:i:s'), + 'allocatedToDocumentCode' => $allocatedToDocument ? $allocatedToDocument->getCode() : null ]; } @@ -130,19 +535,19 @@ class PlugWarrantyController extends AbstractController { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); - + $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ 'id' => $id, - 'bid' => $acc['bid'] + 'business' => $acc['bid'] ]); - - if(!$serial) + + if (!$serial) throw $this->createNotFoundException(); - - $this->checkAndUpdateSerialStatus($serial, $entityManager); - + + $allocatedToDocument = $serial->getAllocatedToDocumentId() ? $entityManager->getRepository(HesabdariDoc::class)->find($serial->getAllocatedToDocumentId()) : null; + $data = [ 'id' => $serial->getId(), 'serialNumber' => $serial->getSerialNumber(), @@ -151,18 +556,30 @@ class PlugWarrantyController extends AbstractController 'name' => $serial->getCommodity()->getName(), 'code' => $serial->getCommodity()->getCode() ], - 'dateSubmit' => $serial->getDateSubmit(), + 'dateSubmit' => $serial->getDateSubmit()->format('Y-m-d H:i:s'), 'description' => $serial->getDescription(), - 'warrantyStartDate' => $serial->getWarrantyStartDate(), - 'warrantyEndDate' => $serial->getWarrantyEndDate(), + 'warrantyStartDate' => $this->jalaliToGregorian($serial->getWarrantyStartDate()?->format('Y-m-d')), + 'warrantyEndDate' => $this->jalaliToGregorian($serial->getWarrantyEndDate()?->format('Y-m-d')), 'status' => $serial->getStatus(), + 'activation' => $serial->getActivation(), 'notes' => $serial->getNotes(), + 'expired' => $this->expiredFlag($serial->getWarrantyEndDate()), 'submitter' => [ 'id' => $serial->getSubmitter()->getId(), 'name' => $serial->getSubmitter()->getFullName() - ] + ], + 'commoditySerial' => $serial->getCommoditySerial(), + 'buyer' => $serial->getBuyer() ? [ + 'id' => $serial->getBuyer()->getId(), + 'code' => $serial->getBuyer()->getCode(), + 'name' => $serial->getBuyer()->getName(), + 'nikename' => $serial->getBuyer()->getNikename(), + 'mobile' => $serial->getBuyer()->getMobile() + ] : null, + 'activationAt' => $serial->getActivationAt()?->format('Y-m-d H:i:s'), + 'allocatedToDocumentCode' => $allocatedToDocument ? $allocatedToDocument->getCode() : null ]; - + return $this->json($data); } catch (\Exception $e) { return $this->json([ @@ -172,11 +589,11 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/serials/add', name: 'plugin_warranty_serial_add', methods: ['POST'])] - public function plugin_warranty_serial_add(Request $request, EntityManagerInterface $entityManager, Access $access, Log $log): JsonResponse + public function plugin_warranty_serial_add(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); $params = []; @@ -184,12 +601,10 @@ class PlugWarrantyController extends AbstractController $params = json_decode($content, true); } - if(!array_key_exists('serialNumber', $params) || !array_key_exists('commodity_id', $params)) + if (!array_key_exists('serialNumber', $params) || !array_key_exists('commodity_id', $params)) throw $this->createAccessDeniedException('پارامترهای ناقص'); - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - - if($repository->isSerialNumberExists($params['serialNumber'], $acc['bid'])) { + if ($repository->isSerialNumberExists($params['serialNumber'], $acc['bid'])) { return $this->json(['error' => 'شماره سریال تکراری است'], 400); } @@ -197,21 +612,27 @@ class PlugWarrantyController extends AbstractController 'id' => $params['commodity_id'], 'bid' => $acc['bid'] ]); - - if(!$commodity) { + + if (!$commodity) { return $this->json(['error' => 'محصول یافت نشد'], 400); } + $business = $entityManager->getRepository(Business::class)->find($acc['bid']); + if (!$business) { + return $this->json(['error' => 'کسب‌وکار یافت نشد'], 404); + } + $serial = new PlugWarrantySerial(); $serial->setSerialNumber($params['serialNumber']); $serial->setCommodity($commodity); - $serial->setBid($acc['bid']); + $serial->setBusiness($business); $serial->setSubmitter($this->getUser()); $serial->setDescription($params['description'] ?? null); - $serial->setWarrantyStartDate($params['warrantyStartDate'] ?? null); - $serial->setWarrantyEndDate($params['warrantyEndDate'] ?? null); - $serial->setStatus($params['status'] ?? 'active'); + $serial->setWarrantyStartDate($this->toDate($this->jalaliToGregorian($params['warrantyStartDate']) ?? null)); + $serial->setWarrantyEndDate($this->toDate($this->jalaliToGregorian($params['warrantyEndDate']) ?? null)); + $serial->setStatus($params['status'] ?? PlugWarrantySerial::STATUS_AVAILABLE); $serial->setNotes($params['notes'] ?? null); + $serial->setActivation('deactive'); $entityManager->persist($serial); $entityManager->flush(); @@ -236,19 +657,19 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/serials/edit/{id}', name: 'plugin_warranty_serial_edit', methods: ['POST'])] - public function plugin_warranty_serial_edit(Request $request, EntityManagerInterface $entityManager, Access $access, $id, Log $log): JsonResponse + public function plugin_warranty_serial_edit(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, $id, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ 'id' => $id, - 'bid' => $acc['bid'] + 'business' => $acc['bid'] ]); - - if(!$serial) + + if (!$serial) throw $this->createNotFoundException(); $params = []; @@ -256,11 +677,10 @@ class PlugWarrantyController extends AbstractController $params = json_decode($content, true); } - if(array_key_exists('serialNumber', $params)) { - $repository = $entityManager->getRepository(PlugWarrantySerial::class); + if (array_key_exists('serialNumber', $params)) { $existingSerial = $repository->createQueryBuilder('p') ->andWhere('p.serialNumber = :serialNumber') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->andWhere('p.id != :id') ->setParameter('serialNumber', $params['serialNumber']) ->setParameter('bid', $acc['bid']) @@ -268,46 +688,50 @@ class PlugWarrantyController extends AbstractController ->getQuery() ->getOneOrNullResult(); - if($existingSerial) { + if ($existingSerial) { return $this->json(['error' => 'شماره سریال تکراری است'], 400); } - + $serial->setSerialNumber($params['serialNumber']); } - if(array_key_exists('commodity_id', $params)) { + if (array_key_exists('commodity_id', $params)) { $commodity = $entityManager->getRepository(Commodity::class)->findOneBy([ 'id' => $params['commodity_id'], 'bid' => $acc['bid'] ]); - - if(!$commodity) { + + if (!$commodity) { return $this->json(['error' => 'محصول یافت نشد'], 400); } - + $serial->setCommodity($commodity); } - if(array_key_exists('description', $params)) { + if (array_key_exists('description', $params)) { $serial->setDescription($params['description']); } - if(array_key_exists('warrantyStartDate', $params)) { - $serial->setWarrantyStartDate($params['warrantyStartDate']); + if (array_key_exists('warrantyStartDate', $params)) { + $serial->setWarrantyStartDate($this->toDate($this->jalaliToGregorian($params['warrantyStartDate']))); } - if(array_key_exists('warrantyEndDate', $params)) { - $serial->setWarrantyEndDate($params['warrantyEndDate']); + if (array_key_exists('warrantyEndDate', $params)) { + $serial->setWarrantyEndDate($this->toDate($this->jalaliToGregorian($params['warrantyEndDate']))); } - if(array_key_exists('status', $params)) { + if (array_key_exists('status', $params)) { $serial->setStatus($params['status']); } - if(array_key_exists('notes', $params)) { + if (array_key_exists('notes', $params)) { $serial->setNotes($params['notes']); } + if (array_key_exists('activation', $params)) { + $serial->setActivation($params['activation']); + } + $entityManager->flush(); $log->insert( @@ -329,19 +753,19 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/serials/{id}', name: 'plugin_warranty_serial_delete', methods: ['DELETE'])] - public function plugin_warranty_serial_delete(EntityManagerInterface $entityManager, Access $access, $id, Log $log): JsonResponse + public function plugin_warranty_serial_delete(EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, $id, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ 'id' => $id, - 'bid' => $acc['bid'] + 'business' => $acc['bid'] ]); - - if(!$serial) + + if (!$serial) throw $this->createNotFoundException(); $serialNumber = $serial->getSerialNumber(); @@ -367,11 +791,11 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/serials/bulk-import', name: 'plugin_warranty_serial_bulk_import', methods: ['POST'])] - public function plugin_warranty_serial_bulk_import(Request $request, EntityManagerInterface $entityManager, Access $access, Log $log): JsonResponse + public function plugin_warranty_serial_bulk_import(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); $params = []; @@ -379,36 +803,40 @@ class PlugWarrantyController extends AbstractController $params = json_decode($content, true); } - if(!array_key_exists('serials', $params) || !is_array($params['serials'])) + if (!array_key_exists('serials', $params) || !is_array($params['serials'])) throw $this->createAccessDeniedException('داده‌های نامعتبر'); - $repository = $entityManager->getRepository(PlugWarrantySerial::class); $commodityRepo = $entityManager->getRepository(Commodity::class); - + $business = $entityManager->getRepository(Business::class)->find($acc['bid']); + + if (!$business) { + return $this->json(['error' => 'کسب‌وکار یافت نشد'], 404); + } + $successCount = 0; $errorCount = 0; $errors = []; - foreach($params['serials'] as $index => $serialData) { + foreach ($params['serials'] as $index => $serialData) { try { - if(!array_key_exists('serialNumber', $serialData) || !array_key_exists('commodity_id', $serialData)) { + if (!array_key_exists('serialNumber', $serialData) || !array_key_exists('commodity_code', $serialData)) { $errors[] = "ردیف " . ($index + 1) . ": پارامترهای ناقص"; $errorCount++; continue; } - if($repository->isSerialNumberExists($serialData['serialNumber'], $acc['bid'])) { + if ($repository->isSerialNumberExists($serialData['serialNumber'], $acc['bid'])) { $errors[] = "ردیف " . ($index + 1) . ": شماره سریال تکراری است"; $errorCount++; continue; } $commodity = $commodityRepo->findOneBy([ - 'id' => $serialData['commodity_id'], + 'code' => $serialData['commodity_code'], 'bid' => $acc['bid'] ]); - - if(!$commodity) { + + if (!$commodity) { $errors[] = "ردیف " . ($index + 1) . ": محصول یافت نشد"; $errorCount++; continue; @@ -417,12 +845,17 @@ class PlugWarrantyController extends AbstractController $serial = new PlugWarrantySerial(); $serial->setSerialNumber($serialData['serialNumber']); $serial->setCommodity($commodity); - $serial->setBid($acc['bid']); + $serial->setBusiness($business); $serial->setSubmitter($this->getUser()); $serial->setDescription($serialData['description'] ?? null); - $serial->setWarrantyStartDate($serialData['warrantyStartDate'] ?? null); - $serial->setWarrantyEndDate($serialData['warrantyEndDate'] ?? null); - $serial->setStatus($serialData['status'] ?? 'active'); + $serial->setWarrantyStartDate($this->toDate($this->jalaliToGregorian($serialData['warrantyStartDate']) ?? null)); + $serial->setWarrantyEndDate($this->toDate($this->jalaliToGregorian($serialData['warrantyEndDate']) ?? null)); + $serial->setActivation('deactive'); + + $incomingStatus = $serialData['status'] ?? null; + $map = ['active' => 'available', 'inactive' => 'void', 'expired' => 'void']; + $serial->setStatus($map[$incomingStatus] ?? PlugWarrantySerial::STATUS_AVAILABLE); + $serial->setNotes($serialData['notes'] ?? null); $entityManager->persist($serial); @@ -458,48 +891,42 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/stats', name: 'plugin_warranty_stats', methods: ['GET'])] - public function plugin_warranty_stats(EntityManagerInterface $entityManager, Access $access): JsonResponse + public function plugin_warranty_stats(EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); - $this->updateExpiredSerials($entityManager, $acc['bid']); - - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - $allSerials = $repository->createQueryBuilder('p') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->setParameter('bid', $acc['bid']) ->getQuery() ->getResult(); - + $totalSerials = count($allSerials); - $activeSerials = 0; - $inactiveSerials = 0; - $expiredSerials = 0; - + $byStatus = [ + PlugWarrantySerial::STATUS_AVAILABLE => 0, + PlugWarrantySerial::STATUS_ALLOCATED => 0, + PlugWarrantySerial::STATUS_VERIFIED => 0, + PlugWarrantySerial::STATUS_BOUND => 0, + PlugWarrantySerial::STATUS_CONSUMED => 0, + PlugWarrantySerial::STATUS_VOID => 0, + ]; + $expired = 0; + foreach ($allSerials as $serial) { - $status = $serial->getStatus(); - switch ($status) { - case 'active': - $activeSerials++; - break; - case 'inactive': - $inactiveSerials++; - break; - case 'expired': - $expiredSerials++; - break; - } + $st = $serial->getStatus(); + if (isset($byStatus[$st])) + $byStatus[$st]++; + if ($this->expiredFlag($serial->getWarrantyEndDate())) + $expired++; } return $this->json([ 'totalSerials' => $totalSerials, - 'activeSerials' => $activeSerials, - 'inactiveSerials' => $inactiveSerials, - 'expiredSerials' => $expiredSerials, + 'byStatus' => $byStatus, + 'expiredFlagCount' => $expired ]); } catch (\Exception $e) { return $this->json([ @@ -508,49 +935,148 @@ class PlugWarrantyController extends AbstractController } } - #[Route('/api/plugins/warranty/serials/update-expired', name: 'plugin_warranty_update_expired', methods: ['POST'])] - public function plugin_warranty_update_expired(EntityManagerInterface $entityManager, Access $access): JsonResponse + #[Route('/api/plugins/warranty/serials/bulk-delete', name: 'plugin_warranty_serial_bulk_delete', methods: ['POST'])] + public function plugin_warranty_serial_bulk_delete(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - $jdate = new Jdate(); - $today = $jdate->GetTodayDate(); - - $expiredSerials = $repository->createQueryBuilder('s') - ->where('s.bid = :businessId') - ->andWhere('s.status = :status') - ->andWhere('s.warrantyEndDate IS NOT NULL') - ->andWhere('s.warrantyEndDate < :today') - ->setParameter('businessId', $acc['bid']) - ->setParameter('status', 'active') - ->setParameter('today', $today) - ->getQuery() - ->getResult(); - - $updatedCount = 0; - - foreach ($expiredSerials as $serial) { - $serial->setStatus('expired'); - $updatedCount++; + $params = json_decode($request->getContent() ?: '{}', true); + $ids = $params['ids'] ?? []; + + if (!is_array($ids) || empty($ids)) { + return $this->json([ + 'success' => false, + 'message' => 'هیچ آیتمی برای حذف انتخاب نشده است' + ], 400); } - - if ($updatedCount > 0) { + + $deletedCount = 0; + $errors = []; + + foreach ($ids as $id) { + try { + $serial = $repository->findOneBy([ + 'id' => $id, + 'business' => $acc['bid'] + ]); + + if ($serial) { + $serialNumber = $serial->getSerialNumber(); + $entityManager->remove($serial); + $deletedCount++; + + $log->insert( + 'گارانتی', + 'حذف گروهی سریال: ' . $serialNumber, + $this->getUser(), + $acc['bid'] + ); + } + } catch (\Exception $e) { + $errors[] = "خطا در حذف سریال با ID {$id}: " . $e->getMessage(); + } + } + + if ($deletedCount > 0) { $entityManager->flush(); } return $this->json([ 'success' => true, - 'message' => 'به‌روزرسانی وضعیت سریال‌های منقضی شده تکمیل شد', - 'updatedCount' => $updatedCount + 'message' => "{$deletedCount} سریال با موفقیت حذف شد", + 'deletedCount' => $deletedCount, + 'errors' => $errors ]); } catch (\Exception $e) { return $this->json([ + 'success' => false, 'error' => $e->getMessage() ], 500); } } -} \ No newline at end of file + + #[Route('/api/plugins/warranty/settings/get', name: 'plugin_warranty_settings_get', methods: ['GET'])] + public function plugin_warranty_settings_get(Access $access, registryMGR $registryMGR, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('plugWarrantyManager'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $business = $entityManager->getRepository(Business::class)->find($acc['bid']); + + $require = filter_var($business->getRequireWarrantyOnDelivery(), FILTER_VALIDATE_BOOLEAN); + $grace = (int) ($business->getActivationGraceDays() ?? 7); + $match = filter_var($business->getMatchWarrantyToSerial(), FILTER_VALIDATE_BOOLEAN); + return $this->json([ + 'requireWarrantyOnDelivery' => (bool) $require, + 'activationGraceDays' => max(0, $grace), + 'matchWarrantyToSerial' => (bool) $match + ]); + } + + #[Route('/api/plugins/warranty/settings/save', name: 'plugin_warranty_settings_save', methods: ['POST'])] + public function plugin_warranty_settings_save(Request $request, Access $access, registryMGR $registryMGR, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('plugWarrantyManager'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $business = $entityManager->getRepository(Business::class)->find($acc['bid']); + + $params = json_decode($request->getContent() ?: '{}', true); + $require = isset($params['requireWarrantyOnDelivery']) && ($params['requireWarrantyOnDelivery'] === true || $params['requireWarrantyOnDelivery'] === '1' || $params['requireWarrantyOnDelivery'] === 1 || $params['requireWarrantyOnDelivery'] === 'true'); + $graceDays = isset($params['activationGraceDays']) ? (int) $params['activationGraceDays'] : 7; + if ($graceDays < 0) { $graceDays = 0; } + $match = isset($params['matchWarrantyToSerial']) && ($params['matchWarrantyToSerial'] === true || $params['matchWarrantyToSerial'] === '1' || $params['matchWarrantyToSerial'] === 1 || $params['matchWarrantyToSerial'] === 'true'); + + $business->setRequireWarrantyOnDelivery($require); + $business->setActivationGraceDays($graceDays); + $business->setMatchWarrantyToSerial($match); + + $entityManager->flush(); + + return $this->json(['success' => true]); + } + + private function jalaliToGregorian(?string $input): ?string + { + if (!$input) { + return null; + } + + $s = trim($input); + + // پشتیبانی از جلالی با جداکننده '/' + if (preg_match('/^(\d{4})\/(\d{2})\/(\d{2})$/', $s, $m)) { + $y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3]; + try { + $g = CalendarUtils::toGregorian($y, $mo, $d); + return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]); + } catch (\Throwable $e) { + return $s; + } + } + + // پشتیبانی از جلالی با جداکننده '-' + if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $s, $m)) { + $y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3]; + // اگر سال جلالی باشد (مثلاً 1400) آن را تبدیل کن، در غیر اینصورت همان مقدار را برگردان + if ($y < 1700) { + try { + $g = CalendarUtils::toGregorian($y, $mo, $d); + return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]); + } catch (\Throwable $e) { + return $s; + } + } + return $s; // احتمالاً میلادی است + } + + return $s; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/Plugins/TaxSettingsController.php b/hesabixCore/src/Controller/Plugins/TaxSettingsController.php index 54c6c6a..c000d99 100644 --- a/hesabixCore/src/Controller/Plugins/TaxSettingsController.php +++ b/hesabixCore/src/Controller/Plugins/TaxSettingsController.php @@ -3,7 +3,7 @@ /** * Developed by Mohammad Rezai * https://pirouz.xyz – 2025-07-28 -*/ + */ namespace App\Controller\Plugins; @@ -25,7 +25,7 @@ use DateTime; class TaxSettingsController extends AbstractController { - + private function getMoadianBaseUrl(registryMGR $registryMGR): string { $sandboxMode = filter_var($registryMGR->get('system_settings', 'tax_system_sandbox_mode'), FILTER_VALIDATE_BOOLEAN); @@ -154,14 +154,14 @@ class TaxSettingsController extends AbstractController $params = $request->getPayload()->all(); $personType = $params['personType'] ?? 'natural'; - + if (empty($params['nationalId'])) { return $this->json([ 'success' => false, 'message' => 'شناسه ملی الزامی است' ]); } - + if ($personType === 'legal') { if (empty($params['nameFa']) || empty($params['nameEn']) || empty($params['email'])) { return $this->json([ @@ -174,15 +174,15 @@ class TaxSettingsController extends AbstractController try { $privateKey = $this->generatePrivateKey(); $publicKey = $this->generatePublicKey($privateKey); - + $businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid']; - + $response = [ 'success' => true, 'privateKey' => $privateKey, 'publicKey' => $publicKey ]; - + if ($personType === 'legal') { $csr = $this->generateCSR($privateKey, $params); $response['csr'] = $csr; @@ -555,6 +555,7 @@ class TaxSettingsController extends AbstractController $itemDiscountType = $row->getDiscountType() ?? 'fixed'; $itemDiscountPercent = $row->getDiscountPercent() ?? 0; $itemTax = $row->getTax() ?? 0; + $count = $row->getCommdityCount() ?? 0; if ($itemDiscountType === 'percent' && $itemDiscountPercent > 0) { $originalPrice = $basePrice / (1 - ($itemDiscountPercent / 100)); @@ -562,8 +563,8 @@ class TaxSettingsController extends AbstractController } else { $originalPrice = $basePrice + $itemDiscount; } - - $unitPrice = $row->getCommdityCount() > 0 ? $originalPrice / $row->getCommdityCount() : 0; + $unitPrice = $count > 0 ? $originalPrice / $count : 0; + $es = $count > 0 ? ($unitPrice * $count) : $originalPrice; $netPrice = $basePrice; $totalInvoice += $netPrice; @@ -574,8 +575,9 @@ class TaxSettingsController extends AbstractController 'name' => $row->getCommodity()->getName(), 'code' => $row->getCommodity()->getCode() ], - 'count' => $row->getCommdityCount(), + 'count' => $count, 'price' => $unitPrice, + 'prdis' => $es, 'discountPercent' => $itemDiscountPercent, 'discountAmount' => $itemDiscount, 'total' => $netPrice, @@ -663,7 +665,7 @@ class TaxSettingsController extends AbstractController $rowNumber = 1; foreach ($data['items'] as $item) { $commodity = $item['name']; - + if (empty($commodity['code'])) { $errors[] = "ردیف {$rowNumber}: کد کالا/خدمت تعریف نشده است"; } @@ -832,22 +834,22 @@ class TaxSettingsController extends AbstractController $moadian->setToken($token); $invoice = $taxInvoice->getInvoice(); - + try { if (!$invoice) { throw new \Exception('فاکتور معتبر نیست'); } - + $validationResult = $this->validateInvoiceForTax($invoice); if (!$validationResult['valid']) { throw new \Exception($validationResult['message']); } - + $invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings->getEconomicCode()); - if (!$invoiceDto) { + if (!$invoiceDto) { throw new \Exception('خطا در آماده‌سازی فاکتور: خطا در ساخت DTO فاکتور'); } - + $response = $moadian->sendInvoices([$invoiceDto]); } catch (\Exception $e) { return $this->json([ @@ -1111,7 +1113,7 @@ class TaxSettingsController extends AbstractController $vra = round(($itemTax / $itemTotal) * 100, 2); $invoiceType = $invoice->getType() ?? 'sell'; - + switch ($invoiceType) { case 'return_sell': case 'return_buy': @@ -1165,7 +1167,7 @@ class TaxSettingsController extends AbstractController $buyerNationalId = null; $buyerEconomicCode = null; $buyerPostalCode = null; - + $buyerPerson = null; foreach ($invoice->getHesabdariRows() as $row) { if ($row->getPerson()) { @@ -1182,7 +1184,7 @@ class TaxSettingsController extends AbstractController if (empty($buyerNationalId) || trim($buyerNationalId) === '') { $buyerNationalId = null; } - + if (empty($buyerEconomicCode) || trim($buyerEconomicCode) === '') { $buyerEconomicCode = null; } @@ -1193,7 +1195,7 @@ class TaxSettingsController extends AbstractController } $personType = 1; - + if (strlen($buyerNationalId) == 11) { $personType = 2; } @@ -1229,7 +1231,7 @@ class TaxSettingsController extends AbstractController ->setScc(null) ->setCrn(null) ->setBillid(null) - ->setTprdis($data['totalInvoice']) + ->setTprdis(array_sum(array_column($data['items'], 'prdis'))) ->setTdis($data['totalDiscount']) ->setTadis($data['totalInvoice'] - $data['totalDiscount']) ->setTvam($totalTax) @@ -1241,13 +1243,23 @@ class TaxSettingsController extends AbstractController ->setTvop(null) ->setTax17(0); $bodyItems = []; - + foreach ($data['items'] as $item) { $itemTax = $item['tax']; - $itemTotal = $item['total'] + $itemTax; - + $vra = $this->calculateVra($item['total'], $itemTax, $invoice); - + + $prdis = $item['count'] * $item['price']; + + $adis = $prdis - $item['discountAmount']; + + $ks = ($adis * $vra) / 100; + + $ks2 = 0; + $ks3 = 0; + + $os = $adis + $ks + $ks2 + $ks3; + $bodyDto = (new \SnappMarketPro\Moadian\Dto\InvoiceBodyDto()) ->setSstid($this->getCommodityTaxCodeFromInvoice($invoice, $item['name']['id'])) ->setSstt($item['name']['name']) @@ -1257,11 +1269,11 @@ class TaxSettingsController extends AbstractController ->setCfee(null) ->setCut(null) ->setExr(null) - ->setPrdis($item['total']) + ->setPrdis($prdis) ->setDis($item['discountAmount']) - ->setAdis($item['total'] - $item['discountAmount']) + ->setAdis($adis) ->setVra($vra) - ->setVam($itemTax) + ->setVam($ks) ->setOdt(null) ->setOdr(null) ->setOdam(null) @@ -1275,7 +1287,7 @@ class TaxSettingsController extends AbstractController ->setCop(null) ->setVop(null) ->setBsrn(null) - ->setTsstam($itemTotal); + ->setTsstam($os); $bodyItems[] = $bodyDto; } @@ -1419,7 +1431,7 @@ class TaxSettingsController extends AbstractController } $invoice = $taxInvoice->getInvoice(); - + if (!$invoice) { $results[] = [ 'id' => $id, @@ -1430,7 +1442,7 @@ class TaxSettingsController extends AbstractController $errorCount++; continue; } - + $validationResult = $this->validateInvoiceForTax($invoice); if (!$validationResult['valid']) { $results[] = [ @@ -1442,9 +1454,9 @@ class TaxSettingsController extends AbstractController $errorCount++; continue; } - + $invoiceDto = $this->buildInvoiceDto($invoice, $moadian, $taxSettings->getEconomicCode()); - if (!$invoiceDto) { + if (!$invoiceDto) { $results[] = [ 'id' => $id, 'code' => $taxInvoice->getInvoiceCode(), @@ -1454,7 +1466,7 @@ class TaxSettingsController extends AbstractController $errorCount++; continue; } - + $response = $moadian->sendInvoices([$invoiceDto]); if (isset($response['result'][0]['referenceNumber']) && !empty($response['result'])) { @@ -1583,7 +1595,7 @@ class TaxSettingsController extends AbstractController } $buyerInfo = $this->validateBuyerEconomicInfo($invoice); - + if (!$buyerInfo['is_valid']) { return $this->json([ 'success' => false, @@ -1611,7 +1623,6 @@ class TaxSettingsController extends AbstractController $buyerEconomicCode = null; $missingFields = []; - // دریافت شخص خریدار از ردیف‌های فاکتور foreach ($invoice->getHesabdariRows() as $row) { if ($row->getPerson()) { $buyerPerson = $row->getPerson(); @@ -1661,7 +1672,7 @@ class TaxSettingsController extends AbstractController if (in_array('economic_code', $missingFields)) { $missingFieldsText[] = 'کد اقتصادی'; } - + $result['message'] = 'اطلاعات اقتصادی خریدار ناقص است. فیلدهای زیر تکمیل نشده‌اند: ' . implode('، ', $missingFieldsText); } else { $result['message'] = 'اطلاعات اقتصادی خریدار کامل است.'; diff --git a/hesabixCore/src/Controller/PublicController.php b/hesabixCore/src/Controller/PublicController.php new file mode 100644 index 0000000..04b87a8 --- /dev/null +++ b/hesabixCore/src/Controller/PublicController.php @@ -0,0 +1,400 @@ +isActive('warranty', $business)) { + return $this->json([ + 'success' => false, + 'message' => 'پلاگین گارانتی برای این کسب و کار فعال نیست', + 'error' => 'PLUGIN_NOT_ACTIVE' + ], 404); + } + return null; + } + #[Route('/api/public/{businessId}/warranty/check/{code}', name: 'api_public_warranty_check', methods: ['GET'])] + public function checkWarrantyCode(string $businessId, string $code, EntityManagerInterface $entityManager, PluginService $pluginService, registryMGR $registryMGR): JsonResponse + { + // Validate input + if (empty($code)) { + return $this->json([ + 'success' => false, + 'message' => 'کد گارانتی وارد نشده است' + ], 400); + } + + // Find business + $business = $entityManager->getRepository(Business::class)->find($businessId); + if (!$business) { + return $this->json([ + 'success' => false, + 'message' => 'کسب و کار مورد نظر یافت نشد' + ], 404); + } + + // Check if warranty plugin is active + $pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService); + if ($pluginCheck) { + return $pluginCheck; + } + + // Find warranty serial + $warrantySerial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ + 'business' => $business, + 'serialNumber' => $code + ]); + + if (!$warrantySerial) { + return $this->json([ + 'success' => false, + 'message' => 'کد گارانتی نامعتبر است یا متعلق به این کسب و کار نیست' + ], 404); + } + + // Check if already activated (used) + if (!$warrantySerial->isUsed()) { + return $this->json([ + 'success' => false, + 'message' => 'این گارانتی ثبت نشده است', + ], 400); + } + + // if ($warrantySerial->getActivation() === 'active') { + // return $this->json([ + // 'success' => false, + // 'message' => 'این گارانتی قبلاً فعال شده است', + // ], 400); + // } + + $activationTimeLimit = (int) ($registryMGR->get('warranty', 'activationGraceDays') ?? 7); + $allocatedAt = $warrantySerial->getAllocatedAt(); + $currentDate = new \DateTime(); + $daysDiff = $currentDate->diff($allocatedAt)->days; + + if ($daysDiff > $activationTimeLimit) { + return $this->json([ + 'success' => false, + 'message' => "مهلت فعال‌سازی این گارانتی ({$activationTimeLimit} روز) به پایان رسیده است" + ], 400); + } + + // Get commodity information + $commodity = $warrantySerial->getCommodity(); + + // Calculate warranty period end date + $warrantyEndDate = null; + if ($warrantySerial->getWarrantyEndDate()) { + $warrantyEndDate = $warrantySerial->getWarrantyEndDate(); + } elseif ($warrantySerial->getWarrantyStartDate()) { + // If only start date is set, assume 12 months warranty period + $startDate = new \DateTime($warrantySerial->getWarrantyStartDate()); + $endDate = $startDate->add(new \DateInterval('P12M')); + $warrantyEndDate = $endDate->format('Y-m-d'); + } + + // Prepare product information + $productInfo = [ + 'serialNumber' => $warrantySerial->getSerialNumber(), + 'commoditySerial' => $warrantySerial->getCommoditySerial(), + 'productName' => $commodity ? $commodity->getName() : 'نامشخص', + 'productCode' => $commodity ? $commodity->getCode() : null, + 'description' => $warrantySerial->getDescription(), + 'submitDate' => $warrantySerial->getDateSubmit(), + 'warrantyStartDate' => $warrantySerial->getWarrantyStartDate(), + 'warrantyEndDate' => $warrantyEndDate, + 'status' => $warrantySerial->getStatus(), + 'activation' => $warrantySerial->getActivation(), + 'notes' => $warrantySerial->getNotes(), + 'activationTimeLimit' => $activationTimeLimit, + 'daysRemaining' => max(0, $activationTimeLimit - $daysDiff), + 'submitter' => $warrantySerial->getSubmitter() ? $warrantySerial->getSubmitter()->getFullName() : null, + 'businessName' => $business->getName(), + 'activationTicketCode' => $warrantySerial->getActivationTicketCode(), + 'requireActivationSecret' => true + ]; + + return $this->json([ + 'success' => true, + 'data' => $productInfo + ]); + } + + #[Route('/api/public/{businessId}/warranty/activate/{code}', name: 'api_public_warranty_activate', methods: ['POST'])] + public function activateWarranty(string $businessId, string $code, Request $request, EntityManagerInterface $entityManager, PluginService $pluginService, registryMGR $registryMGR): JsonResponse + { + // Validate input + if (empty($code)) { + return $this->json([ + 'success' => false, + 'message' => 'کد گارانتی وارد نشده است' + ], 400); + } + + // Find business + $business = $entityManager->getRepository(Business::class)->find($businessId); + if (!$business) { + return $this->json([ + 'success' => false, + 'message' => 'کسب و کار مورد نظر یافت نشد' + ], 404); + } + + // Check if warranty plugin is active + $pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService); + if ($pluginCheck) { + return $pluginCheck; + } + + // Find warranty serial + $warrantySerial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ + 'business' => $business, + 'serialNumber' => $code + ]); + + if (!$warrantySerial) { + return $this->json([ + 'success' => false, + 'message' => 'کد گارانتی نامعتبر است یا متعلق به این کسب و کار نیست' + ], 404); + } + + // Check if already activated + if (!$warrantySerial->isUsed()) { + return $this->json([ + 'success' => false, + 'message' => 'این گارانتی ثبت نشده است', + ], 400); + } + + // Check status + if ($warrantySerial->getActivation() !== 'deactive') { + return $this->json([ + 'success' => false, + 'message' => 'وضعیت این گارانتی فعال است' + ], 400); + } + + $activationTimeLimit = (int) ($registryMGR->get('warranty', 'activationGraceDays') ?? 7); + $allocatedAt = $warrantySerial->getAllocatedAt(); + $currentDate = new \DateTime(); + $daysDiff = $currentDate->diff($allocatedAt)->days; + + if ($daysDiff > $activationTimeLimit) { + return $this->json([ + 'success' => false, + 'message' => "مهلت فعال‌سازی این گارانتی ({$activationTimeLimit} روز) به پایان رسیده است" + ], 400); + } + + $secret = json_decode($request->getContent() ?: '{}', true)['activationSecret'] ?? ''; + if ($warrantySerial->getActivationTicketSecret()) { + if (!$secret || $secret !== $warrantySerial->getActivationTicketSecret()) { + return $this->json([ + 'success' => false, + 'message' => 'کد فعال‌سازی حواله نامعتبر است' + ], 400); + } + } + + $warrantySerial->setActivation('active'); + $warrantySerial->setActivationAt(new \DateTimeImmutable()); + + // Set warranty start date to current date if not already set + // if (!$warrantySerial->getWarrantyStartDate()) { + // $warrantySerial->setWarrantyStartDate(date('Y-m-d')); + // } + + // Set warranty end date if not already set (default 12 months) + // if (!$warrantySerial->getWarrantyEndDate()) { + // $endDate = new \DateTime(); + // $endDate->add(new \DateInterval('P12M')); + // $warrantySerial->setWarrantyEndDate($endDate->format('Y-m-d')); + // } + + // Save changes + $entityManager->persist($warrantySerial); + $entityManager->flush(); + + // Get commodity information for response + $commodity = $warrantySerial->getCommodity(); + + // Prepare activation information + $activationInfo = [ + 'serialNumber' => $warrantySerial->getSerialNumber(), + 'productName' => $commodity ? $commodity->getName() : 'نامشخص', + 'productCode' => $commodity ? $commodity->getCode() : null, + 'activationDate' => $warrantySerial->getActivationAt()?->format('Y-m-d'), + 'warrantyStartDate' => $warrantySerial->getWarrantyStartDate() ? jalaliToGregorian($warrantySerial->getWarrantyStartDate()->format('Y/m/d')) : null, + 'warrantyEndDate' => $warrantySerial->getWarrantyEndDate() ? jalaliToGregorian($warrantySerial->getWarrantyEndDate()->format('Y/m/d')) : null, + 'businessName' => $business->getName(), + 'notes' => $warrantySerial->getNotes() + ]; + + return $this->json([ + 'success' => true, + 'message' => 'گارانتی با موفقیت فعال شد', + 'data' => $activationInfo + ]); + } + + #[Route('/api/public/{businessId}/warranty/help', name: 'api_public_warranty_help', methods: ['GET'])] + public function getWarrantyHelp(string $businessId, PluginService $pluginService, EntityManagerInterface $entityManager): JsonResponse + { + // Find business + $business = $entityManager->getRepository(Business::class)->find($businessId); + if (!$business) { + return $this->json([ + 'success' => false, + 'message' => 'کسب و کار مورد نظر یافت نشد' + ], 404); + } + + // Check if warranty plugin is active + $pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService); + if ($pluginCheck) { + return $pluginCheck; + } + $helpInfo = [ + 'codeFormat' => [ + 'prefix' => 'WR', + 'length' => 11, + 'example' => 'WR-123456789', + 'description' => 'کد گارانتی با حروف WR شروع می‌شود و شامل 9 رقم است' + ], + 'locations' => [ + 'محصول' => 'روی برچسب چسبیده شده به محصول', + 'فاکتور' => 'در فاکتور خرید یا رسید پرداخت', + 'بسته‌بندی' => 'در جعبه یا بسته‌بندی محصول', + 'ایمیل' => 'در ایمیل تأیید خرید' + ], + 'activationTimeLimit' => [ + 'default' => 7, + 'unit' => 'روز', + 'description' => 'گارانتی باید حداکثر 7 روز پس از خرید فعال شود' + ], + 'support' => [ + 'phone' => '021-88888888', + 'email' => 'support@example.com', + 'hours' => 'شنبه تا پنج‌شنبه، 8 صبح تا 8 شب' + ] + ]; + + return $this->json([ + 'success' => true, + 'data' => $helpInfo + ]); + } + + #[Route('/api/public/{businessId}/status', name: 'api_public_business_status', methods: ['GET'])] + public function getBusinessStatus(string $businessId, PluginService $pluginService, EntityManagerInterface $entityManager): JsonResponse + { + // Find business + $business = $entityManager->getRepository(Business::class)->find($businessId); + if (!$business) { + return $this->json([ + 'success' => false, + 'message' => 'کسب و کار مورد نظر یافت نشد', + 'error' => 'BUSINESS_NOT_FOUND' + ], 404); + } + + // Check if warranty plugin is active + if (!$pluginService->isActive('warranty', $business)) { + return $this->json([ + 'success' => false, + 'message' => 'پلاگین گارانتی برای این کسب و کار فعال نیست', + 'error' => 'PLUGIN_NOT_ACTIVE' + ], 404); + } + + return $this->json([ + 'success' => true, + 'message' => 'کسب و کار و پلاگین گارانتی فعال است', + 'data' => [ + 'businessName' => $business->getName(), + 'pluginActive' => true + ] + ]); + } + + #[Route('/api/public/{businessId}/warranty/qr-scan', name: 'api_public_warranty_qr_scan', methods: ['POST'])] + public function scanQrCode(string $businessId, Request $request, PluginService $pluginService, EntityManagerInterface $entityManager): JsonResponse + { + // Find business + $business = $entityManager->getRepository(Business::class)->find($businessId); + if (!$business) { + return $this->json([ + 'success' => false, + 'message' => 'کسب و کار مورد نظر یافت نشد' + ], 404); + } + + // Check if warranty plugin is active + $pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService); + if ($pluginCheck) { + return $pluginCheck; + } + + // TODO: Implement QR code scanning logic + + $params = json_decode($request->getContent(), true); + $qrData = $params['qrData'] ?? ''; + + if (empty($qrData)) { + return $this->json([ + 'success' => false, + 'message' => 'داده QR خالی است' + ], 400); + } + + // Extract warranty code from QR data + // Assuming QR contains warranty code directly or in a URL + $warrantyCode = $this->extractWarrantyCodeFromQr($qrData); + + if (!$warrantyCode) { + return $this->json([ + 'success' => false, + 'message' => 'کد گارانتی در QR یافت نشد' + ], 400); + } + + return $this->json([ + 'success' => true, + 'warrantyCode' => $warrantyCode + ]); + } + + private function extractWarrantyCodeFromQr(string $qrData): ?string + { + if (preg_match('/WR-\d{9}/', $qrData, $matches)) { + return $matches[0]; + } + + if (preg_match('/warranty.*code[=\/]([A-Za-z0-9-]+)/', $qrData, $matches)) { + return $matches[1]; + } + + return null; + } +} +function jalaliToGregorian($date) { + $p = explode('/', $date); + return implode('-', CalendarUtils::toGregorian($p[0], $p[1], $p[2])); +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/SellController.php b/hesabixCore/src/Controller/SellController.php index 6bf2a26..40dc27f 100644 --- a/hesabixCore/src/Controller/SellController.php +++ b/hesabixCore/src/Controller/SellController.php @@ -68,6 +68,41 @@ class SellController extends AbstractController ]); } + #[Route('/api/sell/approve/{code}', name: 'app_sell_approve', methods: ['POST'])] + public function approveSellDoc(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('sell'); + if (!$acc) throw $this->createAccessDeniedException(); + $doc = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $code, + 'money' => $acc['money'] + ]); + if (!$doc) throw $this->createNotFoundException('فاکتور یافت نشد'); + $doc->setStatus('approved'); + $entityManager->persist($doc); + $entityManager->flush(); + return $this->json(['result' => 0]); + } + + #[Route('/api/sell/payment/approve/{code}', name: 'app_sell_payment_approve', methods: ['POST'])] + public function approveSellPayment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('sell'); + if (!$acc) throw $this->createAccessDeniedException(); + $paymentDoc = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $code, + 'money' => $acc['money'], + 'type' => 'sell_receive' + ]); + if (!$paymentDoc) throw $this->createNotFoundException('سند دریافت یافت نشد'); + $paymentDoc->setStatus('approved'); + $entityManager->persist($paymentDoc); + $entityManager->flush(); + return $this->json(['result' => 0]); + } + #[Route('/api/sell/get/info/{code}', name: 'app_sell_get_info')] public function app_sell_get_info(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, string $code): JsonResponse { @@ -195,6 +230,16 @@ class SellController extends AbstractController $doc->setSubmitter($this->getUser()); $doc->setMoney($acc['money']); $doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting')); + + // Set approval fields based on business settings + $business = $acc['bid']; + if ($business->isRequireTwoStepApproval()) { + $doc->setIsPreview(true); + $doc->setIsApproved(false); + } else { + $doc->setIsPreview(false); + $doc->setIsApproved(true); + } } if ($params['transferCost'] != 0) { $hesabdariRow = new HesabdariRow(); @@ -287,6 +332,18 @@ class SellController extends AbstractController $hesabdariRow->setPerson($person); $entityManager->persist($hesabdariRow); + // Two-step approval: اگر کسب‌وکار تأیید دو مرحله‌ای را الزامی کرده باشد + $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + if ($businessRequire) { + $doc->setIsPreview(true); + $doc->setIsApproved(false); + $doc->setApprovedBy(null); + } else { + $doc->setIsPreview(false); + $doc->setIsApproved(true); + $doc->setApprovedBy($this->getUser()); + } $entityManager->persist($doc); $entityManager->flush(); if (!$doc->getShortlink()) { @@ -424,10 +481,13 @@ class SellController extends AbstractController $queryBuilder = $entityManager->createQueryBuilder() ->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount') + ->addSelect('d.isPreview, d.isApproved') ->addSelect('u.fullName as submitter') + ->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail') ->addSelect('l.code as labelCode, l.label as labelLabel') ->from(HesabdariDoc::class, 'd') ->leftJoin('d.submitter', 'u') + ->leftJoin('d.approvedBy', 'approver') ->leftJoin('d.InvoiceLabel', 'l') ->leftJoin('d.hesabdariRows', 'r') ->where('d.bid = :bid') @@ -489,7 +549,9 @@ class SellController extends AbstractController 'plugin' => 'd.plugin', 'refData' => 'd.refData', 'shortlink' => 'd.shortlink', - 'status' => 'd.status', + 'isPreview' => 'd.isPreview', + 'isApproved' => 'd.isApproved', + 'approvedBy' => 'd.approvedBy', 'submitter' => 'u.fullName', 'label' => 'l.label', // از InvoiceLabel ]; @@ -535,6 +597,13 @@ class SellController extends AbstractController 'code' => $doc['labelCode'], 'label' => $doc['labelLabel'] ] : null, + 'isPreview' => $doc['isPreview'], + 'isApproved' => $doc['isApproved'], + 'approvedBy' => $doc['approvedByName'] ? [ + 'fullName' => $doc['approvedByName'], + 'id' => $doc['approvedById'], + 'email' => $doc['approvedByEmail'] + ] : null, ]; $mainRow = $entityManager->getRepository(HesabdariRow::class) @@ -778,6 +847,10 @@ class SellController extends AbstractController $accountStatus['label'] = 'بدهکار'; $accountStatus['value'] = $bd - $bs; } + // فقط در صورت تایید نهایی مجاز به چاپ هستیم + if ($doc->getStatus() !== 'approved') { + return $this->json(['result' => -10, 'message' => 'فاکتور هنوز تایید نشده است'], 403); + } if ($params['pdf'] == true || $params['printers'] == true) { $note = ''; if ($printSettings) { @@ -1187,6 +1260,19 @@ class SellController extends AbstractController $hesabdariRow->setPerson($person); $entityManager->persist($hesabdariRow); + // Two-step approval: اگر کسب‌وکار تأیید دو مرحله‌ای را الزامی کرده باشد + $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + if ($businessRequire) { + $doc->setIsPreview(true); + $doc->setIsApproved(false); + $doc->setApprovedBy(null); + } else { + $doc->setIsPreview(false); + $doc->setIsApproved(true); + $doc->setApprovedBy($this->getUser()); + } + // ذخیره فاکتور $entityManager->persist($doc); $entityManager->flush(); @@ -1272,6 +1358,14 @@ class SellController extends AbstractController $receiveRow->setPerson($person); $entityManager->persist($receiveRow); + // Two-step approval برای دریافت/پرداخت + // $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + // $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + // if ($businessRequire) { + // $paymentDoc->setStatus('pending_approval'); + // } else { + // $paymentDoc->setStatus('approved'); + // } $entityManager->persist($paymentDoc); } $entityManager->flush(); diff --git a/hesabixCore/src/Controller/StoreroomController.php b/hesabixCore/src/Controller/StoreroomController.php index 931c30a..9d68632 100644 --- a/hesabixCore/src/Controller/StoreroomController.php +++ b/hesabixCore/src/Controller/StoreroomController.php @@ -19,6 +19,8 @@ use App\Service\Log; use App\Service\PluginService; use App\Service\registryMGR; use App\Service\SMS; +use App\Entity\PlugWarrantySerial; +use App\Entity\ArchiveFile; use Doctrine\ORM\EntityManagerInterface; use ReflectionException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -54,6 +56,85 @@ class StoreroomController extends AbstractController ); } + #[Route('/api/storeroom/ticket/attachments/upload/{code}', name: 'app_storeroom_ticket_upload_attachment', methods: ['POST'])] + public function uploadTicketAttachment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): JsonResponse + { + $acc = $access->hasRole('store'); + if (!$acc) throw $this->createAccessDeniedException(); + $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]); + if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد'); + + $file = $request->files->get('file'); + if (!$file) { + return $this->json(['result'=>-1,'message'=>'فایل ارسال نشده است'], 400); + } + // Store securely in var/storage + $stored = $storage->store($file, (string)$acc['bid']->getId(), 'storeroom_attachments'); + + $archive = new ArchiveFile(); + $archive->setBid($acc['bid']); + $archive->setSubmitter($acc['user']); + $archive->setDateSubmit(date('Y-m-d H:i:s')); + $archive->setFilename($stored['relativePath']); + $archive->setCat('storeroom_ticket'); + $archive->setFileType($stored['mime'] ?: 'application/octet-stream'); + $archive->setPublic(false); + $archive->setDes($request->request->get('des')); + $archive->setRelatedDocType('storeroom_ticket'); + $archive->setRelatedDocCode($ticket->getCode()); + $archive->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null); + $entityManager->persist($archive); + $entityManager->flush(); + + return $this->json(['result'=>0]); + } + + #[Route('/api/storeroom/ticket/attachments/{code}', name: 'app_storeroom_ticket_list_attachments', methods: ['GET'])] + public function listTicketAttachments(string $code, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('store'); + if (!$acc) throw $this->createAccessDeniedException(); + $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]); + if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد'); + $items = $entityManager->getRepository(ArchiveFile::class)->findBy([ + 'bid'=>$acc['bid'], + 'relatedDocType'=>'storeroom_ticket', + 'relatedDocCode'=>$ticket->getCode() + ], ['id'=>'DESC']); + return $this->json(array_map(function(ArchiveFile $a){ + return [ + 'id'=>$a->getId(), + 'filename'=>$a->getFilename(), + 'fileType'=>$a->getFileType(), + 'fileSize'=>$a->getFileSize(), + 'des'=>$a->getDes(), + 'dateSubmit'=>$a->getDateSubmit(), + ]; + }, $items)); + } + + #[Route('/api/storeroom/ticket/attachments/download/{id}', name: 'app_storeroom_ticket_download_attachment', methods: ['GET'])] + public function downloadTicketAttachment(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): Response + { + $acc = $access->hasRole('store'); + if (!$acc) throw $this->createAccessDeniedException(); + $a = $entityManager->getRepository(ArchiveFile::class)->find($id); + if (!$a || $a->getBid()->getId() !== $acc['bid']->getId()) { + throw $this->createNotFoundException('فایل یافت نشد'); + } + $abs = $storage->absolutePath((string)$a->getFilename()); + if (!is_file($abs) || !is_readable($abs)) { + throw $this->createNotFoundException('فایل موجود نیست'); + } + $response = new \Symfony\Component\HttpFoundation\BinaryFileResponse($abs); + $response->setContentDisposition( + \Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_ATTACHMENT, + basename($abs) + ); + $response->headers->set('Content-Type', $a->getFileType() ?: 'application/octet-stream'); + return $response; + } + #[Route('/api/storeroom/mod/{code}', name: 'app_storeroom_mod')] public function app_storeroom_mod(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $code = 0): JsonResponse { @@ -134,8 +215,12 @@ class StoreroomController extends AbstractController foreach ($buys as $buy) { $temp = $provider->Entity2Array($buy, 0); $person = $this->getPerson($buy); - $temp['person'] = Explore::ExplorePerson($person); - $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $temp['person'] = Explore::ExplorePerson($person); + $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $temp['person'] = null; + } $temp['commodities'] = $this->getCommodities($buy, $provider); //check storeroom exist $this->calcStoreRemaining($temp, $buy, $entityManager); @@ -155,8 +240,12 @@ class StoreroomController extends AbstractController foreach ($sells as $sell) { $temp = $provider->Entity2Array($sell, 0); $person = $this->getPerson($sell); - $temp['person'] = Explore::ExplorePerson($person); - $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $temp['person'] = Explore::ExplorePerson($person); + $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $temp['person'] = null; + } $temp['commodities'] = $this->getCommodities($sell, $provider); //check storeroom exist $this->calcStoreRemaining($temp, $sell, $entityManager); @@ -176,8 +265,12 @@ class StoreroomController extends AbstractController foreach ($rfsells as $sell) { $temp = $provider->Entity2Array($sell, 0); $person = $this->getPerson($sell); - $temp['person'] = Explore::ExplorePerson($person); - $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $temp['person'] = Explore::ExplorePerson($person); + $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $temp['person'] = null; + } $temp['commodities'] = $this->getCommodities($sell, $provider); //check storeroom exist $this->calcStoreRemaining($temp, $sell, $entityManager); @@ -197,8 +290,12 @@ class StoreroomController extends AbstractController foreach ($rfbuys as $buy) { $temp = $provider->Entity2Array($buy, 0); $person = $this->getPerson($buy); - $temp['person'] = Explore::ExplorePerson($person); - $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $temp['person'] = Explore::ExplorePerson($person); + $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $temp['person'] = null; + } $temp['commodities'] = $this->getCommodities($buy, $provider); //check storeroom exist $this->calcStoreRemaining($temp, $buy, $entityManager); @@ -281,8 +378,12 @@ class StoreroomController extends AbstractController } } $res = $provider->Entity2Array($doc, 0); - $res['person'] = $provider->Entity2Array($person, 0); - $res['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $res['person'] = $provider->Entity2Array($person, 0); + $res['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $res['person'] = null; + } $res['commodities'] = $provider->ArrayEntity2Array($commodities, 1, ['doc', 'bid', 'year']); //calculate rows data $this->calcStoreRemaining($res, $doc, $entityManager); @@ -403,6 +504,10 @@ class StoreroomController extends AbstractController $ticket->setTransfer($params['ticket']['transfer']); $ticket->setYear($acc['year']); $ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom')); + $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $rand = ''; + for ($i = 0; $i < 8; $i++) { $rand .= $alphabet[random_int(0, strlen($alphabet)-1)]; } + $ticket->setActivationCode($rand); $ticket->setReceiver($params['ticket']['receiver']); $ticket->setTransferType($transferType); $ticket->setReferral($params['ticket']['referral']); @@ -411,6 +516,9 @@ class StoreroomController extends AbstractController $ticket->setType($params['ticket']['type']); $ticket->setTypeString($params['ticket']['typeString']); $ticket->setDes($params['ticket']['des']); + if (array_key_exists('importWorkflowCode', $params['ticket'])) { + $ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']); + } $entityManager->persist($ticket); //$entityManager->flush(); @@ -418,6 +526,26 @@ class StoreroomController extends AbstractController $docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([ 'doc' => $doc ]); + + // Determine if warranty serials are required based on flag or provided lines + $hasSerialLines = false; + foreach (($params['items'] ?? []) as $it) { + if (!empty($it['serialLines']) && is_array($it['serialLines'])) { $hasSerialLines = true; break; } + } + $requireWarrantySerial = (isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true) || $hasSerialLines; + if ($requireWarrantySerial) { + if (!$pluginService->isActive('warranty', $acc['bid'])) { + return $this->json(['result' => -5, 'message' => 'افزونه گارانتی فعال نیست'], 403); + } + // Validate counts up-front + foreach ($params['items'] as $item) { + $lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : []; + if ((int)($item['ticketCount'] ?? 0) > 0 && count($lines) < (int)$item['ticketCount']) { + return $this->json(['result' => -3, 'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'], 400); + } + } + } + foreach ($params['items'] as $item) { $row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([ 'bid' => $acc['bid'], @@ -441,8 +569,58 @@ class StoreroomController extends AbstractController $ticketItem->setCommodity($row->getCommodity()); $ticketItem->setType($item['type']); $entityManager->persist($ticketItem); + + // Bind warranty serials per item if provided + if ($requireWarrantySerial) { + $lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : []; + if ((int)$item['ticketCount'] > 0) { + // Ensure we have an id to bind to + $entityManager->flush(); + $lines = array_slice($lines, 0, (int)$item['ticketCount']); + foreach ($lines as $ln) { + $warrantyCode = $ln['warranty'] ?? null; + $deviceSerial = $ln['serial'] ?? null; + if (!$warrantyCode) { + return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400); + } + /** @var PlugWarrantySerial|null $serial */ + $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ + 'business' => $acc['bid'], + 'serialNumber' => $warrantyCode, + 'commodity' => $row->getCommodity(), + ]); + if (!$serial || $serial->getStatus() !== PlugWarrantySerial::STATUS_AVAILABLE) { + return $this->json(['result' => -4, 'message' => 'گارانتی نامعتبر یا آزاد نیست: ' . $warrantyCode], 400); + } + $serial->setStatus(PlugWarrantySerial::STATUS_CONSUMED); + $serial->setCommoditySerial($deviceSerial); + $serial->setBuyer($person); + $serial->setAllocatedToDocumentId($doc->getId()); + $serial->setAllocatedAt(new \DateTimeImmutable()); + $serial->setBoundToItemId($ticketItem->getId()); + $serial->setBoundAt(new \DateTimeImmutable()); + $serial->setActivationTicketCode($ticket->getCode()); + $serial->setActivationTicketSecret($ticket->getActivationCode()); + $entityManager->persist($serial); + } + } + } + } + $entityManager->flush(); + + $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + if ($businessRequire) { + $ticket->setIsPreview(true); + $ticket->setIsApproved(false); + $ticket->setApprovedBy(null); // هنوز تأیید نشده + } else { + $ticket->setIsPreview(false); + $ticket->setIsApproved(true); + $ticket->setApprovedBy($this->getUser()); // تأیید شده توسط کاربر فعلی + } //save logs $log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']); if ($pluginService->isActive('accpro', $acc['bid'])) { @@ -503,6 +681,58 @@ class StoreroomController extends AbstractController ]); } + #[Route('/api/storeroom/ticket/status/{code}', name: 'app_storeroom_ticket_status_update', methods: ['POST'])] + public function app_storeroom_ticket_status_update(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('store'); + if (!$acc) + throw $this->createAccessDeniedException(); + $params = json_decode($request->getContent() ?: '{}', true); + $status = $params['status'] ?? null; // in_progress|done|rejected|approved|pending_approval + if (!in_array($status, ['in_progress','done','rejected','approved','pending_approval'])) { + return $this->json(['result' => -1, 'message' => 'وضعیت نامعتبر'], 400); + } + $ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $code + ]); + if (!$ticket) { + throw $this->createNotFoundException('حواله یافت نشد.'); + } + // $ticket->setStatus($status); + $entityManager->persist($ticket); + $entityManager->flush(); + return $this->json(['result' => 0]); + } + + #[Route('/api/storeroom/tickets', name: 'app_storeroom_tickets_by_status', methods: ['GET'])] + public function app_storeroom_tickets_by_status(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('store'); + if (!$acc) + throw $this->createAccessDeniedException(); + $status = $request->query->get('status'); + $criteria = [ + 'bid' => $acc['bid'], + 'year' => $acc['year'], + ]; + if ($status) { + $criteria['status'] = $status; + } + $tickets = $entityManager->getRepository(StoreroomTicket::class)->findBy($criteria, ['date' => 'DESC']); + return $this->json(array_map(function(StoreroomTicket $t){ + return [ + 'code' => $t->getCode(), + 'date' => $t->getDate(), + 'type' => $t->getType(), + 'typeString' => $t->getTypeString(), + 'importWorkflowCode' => $t->getImportWorkflowCode(), + 'person' => $t->getPerson() ? $t->getPerson()->getNikename() : null, + 'storeroom' => $t->getStoreroom() ? $t->getStoreroom()->getName() : null, + ]; + }, $tickets)); + } + #[Route('/api/storeroom/tickets/list/{type}', name: 'app_storeroom_tickets_list')] public function app_storeroom_tickets_list(string $type, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse { @@ -516,15 +746,33 @@ class StoreroomController extends AbstractController ], [ 'date' => 'DESC' ]); - return $this->json($provider->ArrayEntity2ArrayJustIncludes($tickets, [ + $result = $provider->ArrayEntity2ArrayJustIncludes($tickets, [ 'getDes', 'getCode', 'getDate', 'getPerson', 'getNikename', 'getDoc', - 'getTypeString' - ])); + 'getTypeString', + 'isPreview', + 'isApproved' + ], 2); + + foreach ($result as $key => &$ticket) { + $ticketEntity = $tickets[$key]; + if ($ticketEntity->getApprovedBy()) { + $approvedBy = $ticketEntity->getApprovedBy(); + $ticket['approvedBy'] = [ + 'id' => $approvedBy->getId(), + 'fullName' => $approvedBy->getFullName(), + 'email' => $approvedBy->getEmail() + ]; + } else { + $ticket['approvedBy'] = null; + } + } + + return $this->json($result); } #[Route('/api/storeroom/tickets/info/{code}', name: 'app_storeroom_ticket_view')] @@ -542,7 +790,7 @@ class StoreroomController extends AbstractController //get items $items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]); $res = []; - $res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString'], 2); + $res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString', 'isPreview', 'isApproved'], 2); $res['transferType'] = $provider->Entity2ArrayJustIncludes($ticket->getTransferType(), ['getName'], 0); $res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0); //get rows @@ -659,6 +907,15 @@ class StoreroomController extends AbstractController } else { $title = 'حواله خروج از انبار'; } + + $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + if ($businessRequire) { + // بررسی وضعیت تأیید از طریق StoreroomTicket + if ($doc->isPreview()) { + return $this->json(['result' => -10, 'message' => 'حواله هنوز تایید نشده است'], 403); + } + } $pdfPid = 0; $pdfPid = $provider->createPrint( $acc['bid'], diff --git a/hesabixCore/src/Entity/Business.php b/hesabixCore/src/Entity/Business.php index 3a78707..08b7aac 100644 --- a/hesabixCore/src/Entity/Business.php +++ b/hesabixCore/src/Entity/Business.php @@ -117,6 +117,9 @@ class Business #[ORM\Column(length: 255, nullable: true)] private ?string $cashdeskCode = '1000'; + #[ORM\Column(type: Types::BIGINT, nullable: true)] + private ?string $importWorkflowCode = null; + #[ORM\OneToMany(mappedBy: 'bid', targetEntity: Salary::class, orphanRemoval: true)] private Collection $salaries; @@ -307,6 +310,30 @@ class Business #[Ignore] private Collection $plugWarrantySerials; + #[ORM\OneToMany(mappedBy: 'business', targetEntity: ImportWorkflow::class, orphanRemoval: true)] + private Collection $importWorkflows; + + #[ORM\Column(nullable: true)] + private ?bool $requireTwoStepApproval = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $invoiceApprover = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $warehouseApprover = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $financialApprover = null; + + #[ORM\Column(nullable: true)] + private ?bool $requireWarrantyOnDelivery = null; + + #[ORM\Column(nullable: true)] + private ?int $activationGraceDays = null; + + #[ORM\Column(nullable: true)] + private ?bool $matchWarrantyToSerial = null; + public function __construct() { $this->logs = new ArrayCollection(); @@ -352,6 +379,7 @@ class Business $this->plugHrmDocs = new ArrayCollection(); $this->aiConversations = new ArrayCollection(); $this->plugWarrantySerials = new ArrayCollection(); + $this->importWorkflows = new ArrayCollection(); } public function getId(): ?int @@ -879,6 +907,18 @@ class Business return $this; } + public function getImportWorkflowCode(): ?string + { + return $this->importWorkflowCode; + } + + public function setImportWorkflowCode(?string $importWorkflowCode): self + { + $this->importWorkflowCode = $importWorkflowCode; + + return $this; + } + /** * @return Collection */ @@ -2155,4 +2195,105 @@ class Business return $this; } -} + + public function getImportWorkflows(): Collection + { + return $this->importWorkflows; + } + + public function addImportWorkflow(ImportWorkflow $importWorkflow): static + { + if (!$this->importWorkflows->contains($importWorkflow)) { + $this->importWorkflows->add($importWorkflow); + $importWorkflow->setBusiness($this); + } + return $this; + } + + public function removeImportWorkflow(ImportWorkflow $importWorkflow): static + { + if ($this->importWorkflows->removeElement($importWorkflow)) { + if ($importWorkflow->getBusiness() === $this) { + $importWorkflow->setBusiness(null); + } + } + return $this; + } + + public function isRequireTwoStepApproval(): ?bool + { + return $this->requireTwoStepApproval; + } + + public function setRequireTwoStepApproval(?bool $requireTwoStepApproval): static + { + $this->requireTwoStepApproval = $requireTwoStepApproval; + return $this; + } + + public function getInvoiceApprover(): ?string + { + return $this->invoiceApprover; + } + + public function setInvoiceApprover(?string $invoiceApprover): static + { + $this->invoiceApprover = $invoiceApprover; + return $this; + } + + public function getWarehouseApprover(): ?string + { + return $this->warehouseApprover; + } + + public function setWarehouseApprover(?string $warehouseApprover): static + { + $this->warehouseApprover = $warehouseApprover; + return $this; + } + + public function getFinancialApprover(): ?string + { + return $this->financialApprover; + } + + public function setFinancialApprover(?string $financialApprover): static + { + $this->financialApprover = $financialApprover; + return $this; + } + + public function getRequireWarrantyOnDelivery(): ?bool + { + return $this->requireWarrantyOnDelivery; + } + + public function setRequireWarrantyOnDelivery(?bool $requireWarrantyOnDelivery): static + { + $this->requireWarrantyOnDelivery = $requireWarrantyOnDelivery; + return $this; + } + + public function getActivationGraceDays(): ?int + { + return $this->activationGraceDays; + } + + public function setActivationGraceDays(?int $activationGraceDays): static + { + $this->activationGraceDays = $activationGraceDays; + return $this; + } + + public function getMatchWarrantyToSerial(): ?bool + { + return $this->matchWarrantyToSerial; + } + + public function setMatchWarrantyToSerial(?bool $matchWarrantyToSerial): static + { + $this->matchWarrantyToSerial = $matchWarrantyToSerial; + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/HesabdariDoc.php b/hesabixCore/src/Entity/HesabdariDoc.php index c7f33c9..b3b86ff 100644 --- a/hesabixCore/src/Entity/HesabdariDoc.php +++ b/hesabixCore/src/Entity/HesabdariDoc.php @@ -726,4 +726,48 @@ class HesabdariDoc return $this; } + + // Approval fields + #[ORM\Column(nullable: true)] + private ?bool $isPreview = null; + + #[ORM\Column(nullable: true)] + private ?bool $isApproved = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: true)] + private ?User $approvedBy = null; + + public function isPreview(): ?bool + { + return $this->isPreview; + } + + public function setIsPreview(?bool $isPreview): static + { + $this->isPreview = $isPreview; + return $this; + } + + public function isApproved(): ?bool + { + return $this->isApproved; + } + + public function setIsApproved(?bool $isApproved): static + { + $this->isApproved = $isApproved; + return $this; + } + + public function getApprovedBy(): ?User + { + return $this->approvedBy; + } + + public function setApprovedBy(?User $approvedBy): static + { + $this->approvedBy = $approvedBy; + return $this; + } } \ No newline at end of file diff --git a/hesabixCore/src/Entity/HesabdariRow.php b/hesabixCore/src/Entity/HesabdariRow.php index 6e91683..b138312 100644 --- a/hesabixCore/src/Entity/HesabdariRow.php +++ b/hesabixCore/src/Entity/HesabdariRow.php @@ -368,4 +368,6 @@ class HesabdariRow return $this; } + + } \ No newline at end of file diff --git a/hesabixCore/src/Entity/ImportWorkflow.php b/hesabixCore/src/Entity/ImportWorkflow.php new file mode 100644 index 0000000..eef7f9e --- /dev/null +++ b/hesabixCore/src/Entity/ImportWorkflow.php @@ -0,0 +1,441 @@ +items = new ArrayCollection(); + $this->payments = new ArrayCollection(); + $this->documents = new ArrayCollection(); + $this->stages = new ArrayCollection(); + $this->shipping = new ArrayCollection(); + $this->customs = new ArrayCollection(); + $this->dateSubmit = date('Y-m-d H:i:s'); + $this->status = 'draft'; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(string $code): static + { + $this->code = $code; + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + return $this; + } + + public function getBusiness(): ?Business + { + return $this->business; + } + + public function setBusiness(?Business $business): static + { + $this->business = $business; + return $this; + } + + public function getSubmitter(): ?User + { + return $this->submitter; + } + + public function setSubmitter(?User $submitter): static + { + $this->submitter = $submitter; + return $this; + } + + public function getDateSubmit(): ?string + { + return $this->dateSubmit; + } + + public function setDateSubmit(string $dateSubmit): static + { + $this->dateSubmit = $dateSubmit; + return $this; + } + + public function getDateMod(): ?string + { + return $this->dateMod; + } + + public function setDateMod(?string $dateMod): static + { + $this->dateMod = $dateMod; + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): static + { + $this->status = $status; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + return $this; + } + + public function getSupplierName(): ?string + { + return $this->supplierName; + } + + public function setSupplierName(?string $supplierName): static + { + $this->supplierName = $supplierName; + return $this; + } + + public function getSupplierCountry(): ?string + { + return $this->supplierCountry; + } + + public function setSupplierCountry(?string $supplierCountry): static + { + $this->supplierCountry = $supplierCountry; + return $this; + } + + public function getSupplierAddress(): ?string + { + return $this->supplierAddress; + } + + public function setSupplierAddress(?string $supplierAddress): static + { + $this->supplierAddress = $supplierAddress; + return $this; + } + + public function getSupplierPhone(): ?string + { + return $this->supplierPhone; + } + + public function setSupplierPhone(?string $supplierPhone): static + { + $this->supplierPhone = $supplierPhone; + return $this; + } + + public function getSupplierEmail(): ?string + { + return $this->supplierEmail; + } + + public function setSupplierEmail(?string $supplierEmail): static + { + $this->supplierEmail = $supplierEmail; + return $this; + } + + public function getTotalAmount(): ?string + { + return $this->totalAmount; + } + + public function setTotalAmount(?string $totalAmount): static + { + $this->totalAmount = $totalAmount; + return $this; + } + + public function getCurrency(): ?string + { + return $this->currency; + } + + public function setCurrency(?string $currency): static + { + $this->currency = $currency; + return $this; + } + + public function getExchangeRate(): ?string + { + return $this->exchangeRate; + } + + public function setExchangeRate(?string $exchangeRate): static + { + $this->exchangeRate = $exchangeRate; + return $this; + } + + public function getTotalAmountIRR(): ?string + { + return $this->totalAmountIRR; + } + + public function setTotalAmountIRR(?string $totalAmountIRR): static + { + $this->totalAmountIRR = $totalAmountIRR; + return $this; + } + + public function getItems(): Collection + { + return $this->items; + } + + public function addItem(ImportWorkflowItem $item): static + { + if (!$this->items->contains($item)) { + $this->items->add($item); + $item->setImportWorkflow($this); + } + return $this; + } + + public function removeItem(ImportWorkflowItem $item): static + { + if ($this->items->removeElement($item)) { + if ($item->getImportWorkflow() === $this) { + $item->setImportWorkflow(null); + } + } + return $this; + } + + public function getPayments(): Collection + { + return $this->payments; + } + + public function addPayment(ImportWorkflowPayment $payment): static + { + if (!$this->payments->contains($payment)) { + $this->payments->add($payment); + $payment->setImportWorkflow($this); + } + return $this; + } + + public function removePayment(ImportWorkflowPayment $payment): static + { + if ($this->payments->removeElement($payment)) { + if ($payment->getImportWorkflow() === $this) { + $payment->setImportWorkflow(null); + } + } + return $this; + } + + public function getDocuments(): Collection + { + return $this->documents; + } + + public function addDocument(ImportWorkflowDocument $document): static + { + if (!$this->documents->contains($document)) { + $this->documents->add($document); + $document->setImportWorkflow($this); + } + return $this; + } + + public function removeDocument(ImportWorkflowDocument $document): static + { + if ($this->documents->removeElement($document)) { + if ($document->getImportWorkflow() === $this) { + $document->setImportWorkflow(null); + } + } + return $this; + } + + public function getStages(): Collection + { + return $this->stages; + } + + public function addStage(ImportWorkflowStage $stage): static + { + if (!$this->stages->contains($stage)) { + $this->stages->add($stage); + $stage->setImportWorkflow($this); + } + return $this; + } + + public function removeStage(ImportWorkflowStage $stage): static + { + if ($this->stages->removeElement($stage)) { + if ($stage->getImportWorkflow() === $this) { + $stage->setImportWorkflow(null); + } + } + return $this; + } + + public function getShipping(): Collection + { + return $this->shipping; + } + + public function addShipping(ImportWorkflowShipping $shipping): static + { + if (!$this->shipping->contains($shipping)) { + $this->shipping->add($shipping); + $shipping->setImportWorkflow($this); + } + return $this; + } + + public function removeShipping(ImportWorkflowShipping $shipping): static + { + if ($this->shipping->removeElement($shipping)) { + if ($shipping->getImportWorkflow() === $this) { + $shipping->setImportWorkflow(null); + } + } + return $this; + } + + public function getCustoms(): Collection + { + return $this->customs; + } + + public function addCustom(ImportWorkflowCustoms $custom): static + { + if (!$this->customs->contains($custom)) { + $this->customs->add($custom); + $custom->setImportWorkflow($this); + } + return $this; + } + + public function removeCustom(ImportWorkflowCustoms $custom): static + { + if ($this->customs->removeElement($custom)) { + if ($custom->getImportWorkflow() === $this) { + $custom->setImportWorkflow(null); + } + } + return $this; + } +} + diff --git a/hesabixCore/src/Entity/ImportWorkflowCustoms.php b/hesabixCore/src/Entity/ImportWorkflowCustoms.php new file mode 100644 index 0000000..e8c2cd5 --- /dev/null +++ b/hesabixCore/src/Entity/ImportWorkflowCustoms.php @@ -0,0 +1,254 @@ +dateSubmit = date('Y-m-d H:i:s'); + $this->status = 'pending'; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getImportWorkflow(): ?ImportWorkflow + { + return $this->importWorkflow; + } + + public function setImportWorkflow(?ImportWorkflow $importWorkflow): static + { + $this->importWorkflow = $importWorkflow; + return $this; + } + + public function getDeclarationNumber(): ?string + { + return $this->declarationNumber; + } + + public function setDeclarationNumber(?string $declarationNumber): static + { + $this->declarationNumber = $declarationNumber; + return $this; + } + + public function getCustomsCode(): ?string + { + return $this->customsCode; + } + + public function setCustomsCode(?string $customsCode): static + { + $this->customsCode = $customsCode; + return $this; + } + + public function getClearanceDate(): ?string + { + return $this->clearanceDate; + } + + public function setClearanceDate(?string $clearanceDate): static + { + $this->clearanceDate = $clearanceDate; + return $this; + } + + public function getCustomsDuty(): ?string + { + return $this->customsDuty; + } + + public function setCustomsDuty(?string $customsDuty): static + { + $this->customsDuty = $customsDuty; + return $this; + } + + public function getValueAddedTax(): ?string + { + return $this->valueAddedTax; + } + + public function setValueAddedTax(?string $valueAddedTax): static + { + $this->valueAddedTax = $valueAddedTax; + return $this; + } + + public function getOtherCharges(): ?string + { + return $this->otherCharges; + } + + public function setOtherCharges(?string $otherCharges): static + { + $this->otherCharges = $otherCharges; + return $this; + } + + public function getTotalCustomsCharges(): ?string + { + return $this->totalCustomsCharges; + } + + public function setTotalCustomsCharges(?string $totalCustomsCharges): static + { + $this->totalCustomsCharges = $totalCustomsCharges; + return $this; + } + + public function getCustomsBroker(): ?string + { + return $this->customsBroker; + } + + public function setCustomsBroker(?string $customsBroker): static + { + $this->customsBroker = $customsBroker; + return $this; + } + + public function getCustomsBrokerPhone(): ?string + { + return $this->customsBrokerPhone; + } + + public function setCustomsBrokerPhone(?string $customsBrokerPhone): static + { + $this->customsBrokerPhone = $customsBrokerPhone; + return $this; + } + + public function getCustomsBrokerEmail(): ?string + { + return $this->customsBrokerEmail; + } + + public function setCustomsBrokerEmail(?string $customsBrokerEmail): static + { + $this->customsBrokerEmail = $customsBrokerEmail; + return $this; + } + + public function getWarehouseNumber(): ?string + { + return $this->warehouseNumber; + } + + public function setWarehouseNumber(?string $warehouseNumber): static + { + $this->warehouseNumber = $warehouseNumber; + return $this; + } + + public function getWarehouseLocation(): ?string + { + return $this->warehouseLocation; + } + + public function setWarehouseLocation(?string $warehouseLocation): static + { + $this->warehouseLocation = $warehouseLocation; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + return $this; + } + + public function getDateSubmit(): ?string + { + return $this->dateSubmit; + } + + public function setDateSubmit(string $dateSubmit): static + { + $this->dateSubmit = $dateSubmit; + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): static + { + $this->status = $status; + return $this; + } +} diff --git a/hesabixCore/src/Entity/ImportWorkflowDocument.php b/hesabixCore/src/Entity/ImportWorkflowDocument.php new file mode 100644 index 0000000..6cb779c --- /dev/null +++ b/hesabixCore/src/Entity/ImportWorkflowDocument.php @@ -0,0 +1,213 @@ +dateSubmit = date('Y-m-d H:i:s'); + $this->status = 'active'; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getImportWorkflow(): ?ImportWorkflow + { + return $this->importWorkflow; + } + + public function setImportWorkflow(?ImportWorkflow $importWorkflow): static + { + $this->importWorkflow = $importWorkflow; + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + return $this; + } + + public function getFilePath(): ?string + { + return $this->filePath; + } + + public function setFilePath(?string $filePath): static + { + $this->filePath = $filePath; + return $this; + } + + public function getFileName(): ?string + { + return $this->fileName; + } + + public function setFileName(?string $fileName): static + { + $this->fileName = $fileName; + return $this; + } + + public function getFileSize(): ?string + { + return $this->fileSize; + } + + public function setFileSize(?string $fileSize): static + { + $this->fileSize = $fileSize; + return $this; + } + + public function getFileType(): ?string + { + return $this->fileType; + } + + public function setFileType(?string $fileType): static + { + $this->fileType = $fileType; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + return $this; + } + + public function getDateSubmit(): ?string + { + return $this->dateSubmit; + } + + public function setDateSubmit(string $dateSubmit): static + { + $this->dateSubmit = $dateSubmit; + return $this; + } + + public function getDocumentNumber(): ?string + { + return $this->documentNumber; + } + + public function setDocumentNumber(?string $documentNumber): static + { + $this->documentNumber = $documentNumber; + return $this; + } + + public function getIssueDate(): ?string + { + return $this->issueDate; + } + + public function setIssueDate(?string $issueDate): static + { + $this->issueDate = $issueDate; + return $this; + } + + public function getExpiryDate(): ?string + { + return $this->expiryDate; + } + + public function setExpiryDate(?string $expiryDate): static + { + $this->expiryDate = $expiryDate; + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): static + { + $this->status = $status; + return $this; + } +} + diff --git a/hesabixCore/src/Entity/ImportWorkflowItem.php b/hesabixCore/src/Entity/ImportWorkflowItem.php new file mode 100644 index 0000000..aa3dd68 --- /dev/null +++ b/hesabixCore/src/Entity/ImportWorkflowItem.php @@ -0,0 +1,270 @@ +dateSubmit = date('Y-m-d H:i:s'); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getImportWorkflow(): ?ImportWorkflow + { + return $this->importWorkflow; + } + + public function setImportWorkflow(?ImportWorkflow $importWorkflow): static + { + $this->importWorkflow = $importWorkflow; + return $this; + } + + public function getCommodity(): ?Commodity + { + return $this->commodity; + } + + public function setCommodity(?Commodity $commodity): static + { + $this->commodity = $commodity; + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + return $this; + } + + public function getProductCode(): ?string + { + return $this->productCode; + } + + public function setProductCode(?string $productCode): static + { + $this->productCode = $productCode; + return $this; + } + + public function getBrand(): ?string + { + return $this->brand; + } + + public function setBrand(?string $brand): static + { + $this->brand = $brand; + return $this; + } + + public function getModel(): ?string + { + return $this->model; + } + + public function setModel(?string $model): static + { + $this->model = $model; + return $this; + } + + public function getOriginCountry(): ?string + { + return $this->originCountry; + } + + public function setOriginCountry(?string $originCountry): static + { + $this->originCountry = $originCountry; + return $this; + } + + public function getQuantity(): ?string + { + return $this->quantity; + } + + public function setQuantity(string $quantity): static + { + $this->quantity = $quantity; + return $this; + } + + public function getUnitPrice(): ?string + { + return $this->unitPrice; + } + + public function setUnitPrice(?string $unitPrice): static + { + $this->unitPrice = $unitPrice; + return $this; + } + + public function getUnitPriceIRR(): ?string + { + return $this->unitPriceIRR; + } + + public function setUnitPriceIRR(?string $unitPriceIRR): static + { + $this->unitPriceIRR = $unitPriceIRR; + return $this; + } + + public function getTotalPrice(): ?string + { + return $this->totalPrice; + } + + public function setTotalPrice(?string $totalPrice): static + { + $this->totalPrice = $totalPrice; + return $this; + } + + public function getTotalPriceIRR(): ?string + { + return $this->totalPriceIRR; + } + + public function setTotalPriceIRR(?string $totalPriceIRR): static + { + $this->totalPriceIRR = $totalPriceIRR; + return $this; + } + + public function getWeight(): ?string + { + return $this->weight; + } + + public function setWeight(?string $weight): static + { + $this->weight = $weight; + return $this; + } + + public function getVolume(): ?string + { + return $this->volume; + } + + public function setVolume(?string $volume): static + { + $this->volume = $volume; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + return $this; + } + + public function getSpecifications(): ?string + { + return $this->specifications; + } + + public function setSpecifications(?string $specifications): static + { + $this->specifications = $specifications; + return $this; + } + + public function getDateSubmit(): ?string + { + return $this->dateSubmit; + } + + public function setDateSubmit(string $dateSubmit): static + { + $this->dateSubmit = $dateSubmit; + return $this; + } +} + diff --git a/hesabixCore/src/Entity/ImportWorkflowPayment.php b/hesabixCore/src/Entity/ImportWorkflowPayment.php new file mode 100644 index 0000000..80eec3a --- /dev/null +++ b/hesabixCore/src/Entity/ImportWorkflowPayment.php @@ -0,0 +1,227 @@ +dateSubmit = date('Y-m-d H:i:s'); + $this->status = 'pending'; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getImportWorkflow(): ?ImportWorkflow + { + return $this->importWorkflow; + } + + public function setImportWorkflow(?ImportWorkflow $importWorkflow): static + { + $this->importWorkflow = $importWorkflow; + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + return $this; + } + + public function getAmount(): ?string + { + return $this->amount; + } + + public function setAmount(string $amount): static + { + $this->amount = $amount; + return $this; + } + + public function getCurrency(): ?string + { + return $this->currency; + } + + public function setCurrency(?string $currency): static + { + $this->currency = $currency; + return $this; + } + + public function getAmountIRR(): ?string + { + return $this->amountIRR; + } + + public function setAmountIRR(?string $amountIRR): static + { + $this->amountIRR = $amountIRR; + return $this; + } + + public function getPaymentDate(): ?string + { + return $this->paymentDate; + } + + public function setPaymentDate(string $paymentDate): static + { + $this->paymentDate = $paymentDate; + return $this; + } + + public function getReferenceNumber(): ?string + { + return $this->referenceNumber; + } + + public function setReferenceNumber(?string $referenceNumber): static + { + $this->referenceNumber = $referenceNumber; + return $this; + } + + public function getBankName(): ?string + { + return $this->bankName; + } + + public function setBankName(?string $bankName): static + { + $this->bankName = $bankName; + return $this; + } + + public function getAccountNumber(): ?string + { + return $this->accountNumber; + } + + public function setAccountNumber(?string $accountNumber): static + { + $this->accountNumber = $accountNumber; + return $this; + } + + public function getRecipientName(): ?string + { + return $this->recipientName; + } + + public function setRecipientName(?string $recipientName): static + { + $this->recipientName = $recipientName; + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): static + { + $this->status = $status; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + return $this; + } + + public function getReceiptNumber(): ?string + { + return $this->receiptNumber; + } + + public function setReceiptNumber(?string $receiptNumber): static + { + $this->receiptNumber = $receiptNumber; + return $this; + } + + public function getDateSubmit(): ?string + { + return $this->dateSubmit; + } + + public function setDateSubmit(string $dateSubmit): static + { + $this->dateSubmit = $dateSubmit; + return $this; + } +} + diff --git a/hesabixCore/src/Entity/ImportWorkflowShipping.php b/hesabixCore/src/Entity/ImportWorkflowShipping.php new file mode 100644 index 0000000..4d38032 --- /dev/null +++ b/hesabixCore/src/Entity/ImportWorkflowShipping.php @@ -0,0 +1,269 @@ +dateSubmit = date('Y-m-d H:i:s'); + $this->status = 'pending'; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getImportWorkflow(): ?ImportWorkflow + { + return $this->importWorkflow; + } + + public function setImportWorkflow(?ImportWorkflow $importWorkflow): static + { + $this->importWorkflow = $importWorkflow; + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + return $this; + } + + public function getContainerNumber(): ?string + { + return $this->containerNumber; + } + + public function setContainerNumber(?string $containerNumber): static + { + $this->containerNumber = $containerNumber; + return $this; + } + + public function getBillOfLading(): ?string + { + return $this->billOfLading; + } + + public function setBillOfLading(?string $billOfLading): static + { + $this->billOfLading = $billOfLading; + return $this; + } + + public function getShippingDate(): ?string + { + return $this->shippingDate; + } + + public function setShippingDate(?string $shippingDate): static + { + $this->shippingDate = $shippingDate; + return $this; + } + + public function getArrivalDate(): ?string + { + return $this->arrivalDate; + } + + public function setArrivalDate(?string $arrivalDate): static + { + $this->arrivalDate = $arrivalDate; + return $this; + } + + public function getUnloadingDate(): ?string + { + return $this->unloadingDate; + } + + public function setUnloadingDate(?string $unloadingDate): static + { + $this->unloadingDate = $unloadingDate; + return $this; + } + + public function getShippingCompany(): ?string + { + return $this->shippingCompany; + } + + public function setShippingCompany(?string $shippingCompany): static + { + $this->shippingCompany = $shippingCompany; + return $this; + } + + public function getShippingCompanyPhone(): ?string + { + return $this->shippingCompanyPhone; + } + + public function setShippingCompanyPhone(?string $shippingCompanyPhone): static + { + $this->shippingCompanyPhone = $shippingCompanyPhone; + return $this; + } + + public function getShippingCompanyEmail(): ?string + { + return $this->shippingCompanyEmail; + } + + public function setShippingCompanyEmail(?string $shippingCompanyEmail): static + { + $this->shippingCompanyEmail = $shippingCompanyEmail; + return $this; + } + + public function getOriginPort(): ?string + { + return $this->originPort; + } + + public function setOriginPort(?string $originPort): static + { + $this->originPort = $originPort; + return $this; + } + + public function getDestinationPort(): ?string + { + return $this->destinationPort; + } + + public function setDestinationPort(?string $destinationPort): static + { + $this->destinationPort = $destinationPort; + return $this; + } + + public function getVesselName(): ?string + { + return $this->vesselName; + } + + public function setVesselName(?string $vesselName): static + { + $this->vesselName = $vesselName; + return $this; + } + + public function getVoyageNumber(): ?string + { + return $this->voyageNumber; + } + + public function setVoyageNumber(?string $voyageNumber): static + { + $this->voyageNumber = $voyageNumber; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + return $this; + } + + public function getDateSubmit(): ?string + { + return $this->dateSubmit; + } + + public function setDateSubmit(string $dateSubmit): static + { + $this->dateSubmit = $dateSubmit; + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): static + { + $this->status = $status; + return $this; + } +} + diff --git a/hesabixCore/src/Entity/ImportWorkflowStage.php b/hesabixCore/src/Entity/ImportWorkflowStage.php new file mode 100644 index 0000000..8a73100 --- /dev/null +++ b/hesabixCore/src/Entity/ImportWorkflowStage.php @@ -0,0 +1,157 @@ +dateSubmit = date('Y-m-d H:i:s'); + $this->status = 'pending'; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getImportWorkflow(): ?ImportWorkflow + { + return $this->importWorkflow; + } + + public function setImportWorkflow(?ImportWorkflow $importWorkflow): static + { + $this->importWorkflow = $importWorkflow; + return $this; + } + + public function getStage(): ?string + { + return $this->stage; + } + + public function setStage(string $stage): static + { + $this->stage = $stage; + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(string $status): static + { + $this->status = $status; + return $this; + } + + public function getStartDate(): ?string + { + return $this->startDate; + } + + public function setStartDate(?string $startDate): static + { + $this->startDate = $startDate; + return $this; + } + + public function getEndDate(): ?string + { + return $this->endDate; + } + + public function setEndDate(?string $endDate): static + { + $this->endDate = $endDate; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + return $this; + } + + public function getAssignedTo(): ?string + { + return $this->assignedTo; + } + + public function setAssignedTo(?string $assignedTo): static + { + $this->assignedTo = $assignedTo; + return $this; + } + + public function getDateSubmit(): ?string + { + return $this->dateSubmit; + } + + public function setDateSubmit(string $dateSubmit): static + { + $this->dateSubmit = $dateSubmit; + return $this; + } + + public function getNotes(): ?string + { + return $this->notes; + } + + public function setNotes(?string $notes): static + { + $this->notes = $notes; + return $this; + } +} + diff --git a/hesabixCore/src/Entity/Permission.php b/hesabixCore/src/Entity/Permission.php index 233d9a7..48ac1a0 100644 --- a/hesabixCore/src/Entity/Permission.php +++ b/hesabixCore/src/Entity/Permission.php @@ -141,6 +141,12 @@ class Permission #[ORM\Column(nullable: true)] private ?bool $ai = null; + #[ORM\Column(nullable: true)] + private ?bool $warehouseManager = null; + + #[ORM\Column(nullable: true)] + private ?bool $importWorkflow = null; + public function getId(): ?int { return $this->id; @@ -649,4 +655,28 @@ class Permission return $this; } -} + + public function isWarehouseManager(): ?bool + { + return $this->warehouseManager; + } + + public function setWarehouseManager(?bool $warehouseManager): static + { + $this->warehouseManager = $warehouseManager; + + return $this; + } + + public function isImportWorkflow(): ?bool + { + return $this->importWorkflow; + } + + public function setImportWorkflow(?bool $importWorkflow): static + { + $this->importWorkflow = $importWorkflow; + + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/Person.php b/hesabixCore/src/Entity/Person.php index d2733ab..13e26f0 100644 --- a/hesabixCore/src/Entity/Person.php +++ b/hesabixCore/src/Entity/Person.php @@ -161,6 +161,8 @@ class Person #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $tags = null; + + public function __construct() { $this->hesabdariRows = new ArrayCollection(); @@ -913,4 +915,6 @@ class Person $this->tags = $tags; return $this; } + + } diff --git a/hesabixCore/src/Entity/PlugWarrantySerial.php b/hesabixCore/src/Entity/PlugWarrantySerial.php index 8555311..a557047 100644 --- a/hesabixCore/src/Entity/PlugWarrantySerial.php +++ b/hesabixCore/src/Entity/PlugWarrantySerial.php @@ -2,170 +2,177 @@ namespace App\Entity; -use App\Repository\PlugWarrantySerialRepository; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Ignore; -#[ORM\Entity(repositoryClass: PlugWarrantySerialRepository::class)] +#[ORM\Entity] +#[ORM\Table(name: 'plug_warranty_serial')] +#[ORM\UniqueConstraint(name: 'uniq_warranty_serial', columns: ['serial_number'])] +#[ORM\Index(name: 'idx_status_product', columns: ['status', 'commodity_id'])] +#[ORM\Index(name: 'idx_alloc_doc', columns: ['allocated_to_document_id'])] class PlugWarrantySerial { + public const STATUS_AVAILABLE = 'available'; + public const STATUS_ALLOCATED = 'allocated'; + public const STATUS_VERIFIED = 'verified'; + public const STATUS_BOUND = 'bound'; + public const STATUS_CONSUMED = 'consumed'; + public const STATUS_VOID = 'void'; + #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column] + #[ORM\Column(type: 'integer')] private ?int $id = null; #[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')] #[ORM\JoinColumn(nullable: false)] - #[Ignore] - private ?Business $bid = null; + private ?Business $business = null; #[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')] #[ORM\JoinColumn(nullable: false)] - #[Ignore] private ?Commodity $commodity = null; - #[ORM\Column(length: 255, unique: true)] + #[ORM\Column(name: 'serial_number', length: 255, unique: true)] private ?string $serialNumber = null; - #[ORM\Column(length: 25)] - private ?string $dateSubmit = null; + #[ORM\Column(type: 'datetime_immutable')] + private \DateTimeImmutable $dateSubmit; - #[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')] - #[Ignore] + #[ORM\ManyToOne] private ?User $submitter = null; - #[ORM\Column(length: 255, nullable: true)] + #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; - #[ORM\Column(length: 25, nullable: true)] - private ?string $warrantyStartDate = null; + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $warrantyStartDate = null; - #[ORM\Column(length: 25, nullable: true)] - private ?string $warrantyEndDate = null; + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $warrantyEndDate = null; - #[ORM\Column(length: 50, nullable: true)] - private ?string $status = 'active'; + #[ORM\Column(length: 20)] + private string $status = self::STATUS_AVAILABLE; - #[ORM\Column(length: 255, nullable: true)] + #[ORM\Column(type: 'string', length: 20)] + private string $activation = 'deactive'; + + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $activationAt = null; + + #[ORM\Column(type: 'text', nullable: true)] private ?string $notes = null; + #[ORM\Column(name: 'allocated_to_document_id', type: 'integer', nullable: true)] + private ?int $allocatedToDocumentId = null; + + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $allocatedAt = null; + + #[ORM\Column(name: 'bound_to_item_id', type: 'integer', nullable: true)] + private ?int $boundToItemId = null; + + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $boundAt = null; + + #[ORM\Column(type: 'string', length: 255, nullable: true)] + private ?string $voidReason = null; + + #[ORM\Column(type: 'string', length: 255, nullable: true)] + private ?string $commoditySerial = null; + + #[ORM\ManyToOne] + private ?Person $buyer = null; + + #[ORM\Column(type: 'string', length: 32, nullable: true)] + private ?string $activationTicketCode = null; + + #[ORM\Column(type: 'string', length: 32, nullable: true)] + private ?string $activationTicketSecret = null; + public function __construct() { - $this->dateSubmit = date('Y-m-d H:i:s'); + $this->dateSubmit = new \DateTimeImmutable(); } - public function getId(): ?int - { - return $this->id; + public function getId(): ?int { return $this->id; } + + public function getBusiness(): ?Business { return $this->business; } + public function setBusiness(?Business $business): self { $this->business = $business; return $this; } + + public function getCommodity(): ?Commodity { return $this->commodity; } + public function setCommodity(?Commodity $commodity): self { $this->commodity = $commodity; return $this; } + + public function getSerialNumber(): ?string { return $this->serialNumber; } + public function setSerialNumber(string $serialNumber): self { $this->serialNumber = $serialNumber; return $this; } + + public function getDateSubmit(): \DateTimeImmutable { return $this->dateSubmit; } + public function setDateSubmit(\DateTimeImmutable $d): self { $this->dateSubmit = $d; return $this; } + + public function getSubmitter(): ?User { return $this->submitter; } + public function setSubmitter(?User $submitter): self { $this->submitter = $submitter; return $this; } + + public function getDescription(): ?string { return $this->description; } + public function setDescription(?string $description): self { $this->description = $description; return $this; } + + public function getWarrantyStartDate(): ?\DateTimeImmutable { return $this->warrantyStartDate; } + public function setWarrantyStartDate(?\DateTimeImmutable $d): self { $this->warrantyStartDate = $d; return $this; } + + public function getWarrantyEndDate(): ?\DateTimeImmutable { return $this->warrantyEndDate; } + public function setWarrantyEndDate(?\DateTimeImmutable $d): self { $this->warrantyEndDate = $d; return $this; } + + public function getStatus(): string { return $this->status; } + public function setStatus(string $status): self { $this->status = $status; return $this; } + + public function getActivation(): string { return $this->activation; } + public function setActivation(string $activation): self { $this->activation = $activation; return $this; } + + public function getActivationAt(): ?\DateTimeImmutable { return $this->activationAt; } + public function setActivationAt(?\DateTimeImmutable $d): self { $this->activationAt = $d; return $this; } + + public function getNotes(): ?string { return $this->notes; } + public function setNotes(?string $notes): self { $this->notes = $notes; return $this; } + + public function getAllocatedToDocumentId(): ?int { return $this->allocatedToDocumentId; } + public function setAllocatedToDocumentId(?int $id): self { $this->allocatedToDocumentId = $id; return $this; } + + public function getAllocatedAt(): ?\DateTimeImmutable { return $this->allocatedAt; } + public function setAllocatedAt(?\DateTimeImmutable $d): self { $this->allocatedAt = $d; return $this; } + + public function getBoundToItemId(): ?int { return $this->boundToItemId; } + public function setBoundToItemId(?int $id): self { $this->boundToItemId = $id; return $this; } + + public function getBoundAt(): ?\DateTimeImmutable { return $this->boundAt; } + public function setBoundAt(?\DateTimeImmutable $d): self { $this->boundAt = $d; return $this; } + + public function getVoidReason(): ?string { return $this->voidReason; } + public function setVoidReason(?string $r): self { $this->voidReason = $r; return $this; } + + public function getCommoditySerial(): ?string { return $this->commoditySerial; } + public function setCommoditySerial(?string $commoditySerial): self { $this->commoditySerial = $commoditySerial; return $this; } + + public function getBuyer(): ?Person { return $this->buyer; } + public function setBuyer(?Person $buyer): self { $this->buyer = $buyer; return $this; } + + public function getActivationTicketCode(): ?string { return $this->activationTicketCode; } + public function setActivationTicketCode(?string $code): self { $this->activationTicketCode = $code; return $this; } + public function getActivationTicketSecret(): ?string { return $this->activationTicketSecret; } + public function setActivationTicketSecret(?string $secret): self { $this->activationTicketSecret = $secret; return $this; } + + public function isUsed(): bool { + return $this->status === self::STATUS_CONSUMED; } - public function getBid(): ?Business - { - return $this->bid; - } - - public function setBid(?Business $bid): static - { - $this->bid = $bid; + public function setUsed(bool $used): self { + $this->status = $used ? self::STATUS_CONSUMED : self::STATUS_AVAILABLE; return $this; } - public function getCommodity(): ?Commodity - { - return $this->commodity; - } - - public function setCommodity(?Commodity $commodity): static - { - $this->commodity = $commodity; + public function setUsedAt(string $usedAt): self { + $this->usedAt = new \DateTimeImmutable($usedAt); return $this; } - public function getSerialNumber(): ?string - { - return $this->serialNumber; - } - - public function setSerialNumber(string $serialNumber): static - { - $this->serialNumber = $serialNumber; + public function setUsedTicketCode(string $ticketCode): self { + $this->usedTicketCode = $ticketCode; return $this; } - - public function getDateSubmit(): ?string - { - return $this->dateSubmit; - } - - public function setDateSubmit(string $dateSubmit): static - { - $this->dateSubmit = $dateSubmit; - return $this; - } - - public function getSubmitter(): ?User - { - return $this->submitter; - } - - public function setSubmitter(?User $submitter): static - { - $this->submitter = $submitter; - return $this; - } - - public function getDescription(): ?string - { - return $this->description; - } - - public function setDescription(?string $description): static - { - $this->description = $description; - return $this; - } - - public function getWarrantyStartDate(): ?string - { - return $this->warrantyStartDate; - } - - public function setWarrantyStartDate(?string $warrantyStartDate): static - { - $this->warrantyStartDate = $warrantyStartDate; - return $this; - } - - public function getWarrantyEndDate(): ?string - { - return $this->warrantyEndDate; - } - - public function setWarrantyEndDate(?string $warrantyEndDate): static - { - $this->warrantyEndDate = $warrantyEndDate; - return $this; - } - - public function getStatus(): ?string - { - return $this->status; - } - - public function setStatus(?string $status): static - { - $this->status = $status; - return $this; - } - - public function getNotes(): ?string - { - return $this->notes; - } - - public function setNotes(?string $notes): static - { - $this->notes = $notes; - return $this; - } -} \ No newline at end of file +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/StoreroomTicket.php b/hesabixCore/src/Entity/StoreroomTicket.php index dab1c84..728e72e 100644 --- a/hesabixCore/src/Entity/StoreroomTicket.php +++ b/hesabixCore/src/Entity/StoreroomTicket.php @@ -77,6 +77,25 @@ class StoreroomTicket #[ORM\Column(nullable: true)] private ?bool $canShare = null; + + + #[ORM\Column(length: 255, nullable: true)] + private ?string $importWorkflowCode = null; + + #[ORM\Column(length: 32, nullable: true)] + private ?string $activationCode = null; + + // Approval fields + #[ORM\Column(nullable: true)] + private ?bool $isPreview = null; + + #[ORM\Column(nullable: true)] + private ?bool $isApproved = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: true)] + private ?User $approvedBy = null; + public function __construct() { $this->storeroomItems = new ArrayCollection(); @@ -332,4 +351,62 @@ class StoreroomTicket return $this; } + + + + public function getImportWorkflowCode(): ?string + { + return $this->importWorkflowCode; + } + + public function setImportWorkflowCode(?string $importWorkflowCode): static + { + $this->importWorkflowCode = $importWorkflowCode; + return $this; + } + + public function getActivationCode(): ?string + { + return $this->activationCode; + } + + public function setActivationCode(?string $activationCode): static + { + $this->activationCode = $activationCode; + return $this; + } + + // Approval methods + public function isPreview(): ?bool + { + return $this->isPreview; + } + + public function setIsPreview(?bool $isPreview): static + { + $this->isPreview = $isPreview; + return $this; + } + + public function isApproved(): ?bool + { + return $this->isApproved; + } + + public function setIsApproved(?bool $isApproved): static + { + $this->isApproved = $isApproved; + return $this; + } + + public function getApprovedBy(): ?User + { + return $this->approvedBy; + } + + public function setApprovedBy(?User $approvedBy): static + { + $this->approvedBy = $approvedBy; + return $this; + } } diff --git a/hesabixCore/src/Entity/User.php b/hesabixCore/src/Entity/User.php index 6c361e9..95b76cc 100644 --- a/hesabixCore/src/Entity/User.php +++ b/hesabixCore/src/Entity/User.php @@ -144,6 +144,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(mappedBy: 'user', targetEntity: AIConversation::class, orphanRemoval: true)] private Collection $aiConversations; + #[ORM\OneToMany(mappedBy: 'submitter', targetEntity: ImportWorkflow::class, orphanRemoval: true)] + private Collection $importWorkflows; + public function __construct() { $this->userTokens = new ArrayCollection(); @@ -171,6 +174,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface $this->backBuiltModules = new ArrayCollection(); $this->PlugGhestaDocs = new ArrayCollection(); $this->aiConversations = new ArrayCollection(); + $this->importWorkflows = new ArrayCollection(); } public function getId(): ?int @@ -1066,4 +1070,28 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + public function getImportWorkflows(): Collection + { + return $this->importWorkflows; + } + + public function addImportWorkflow(ImportWorkflow $importWorkflow): static + { + if (!$this->importWorkflows->contains($importWorkflow)) { + $this->importWorkflows->add($importWorkflow); + $importWorkflow->setSubmitter($this); + } + return $this; + } + + public function removeImportWorkflow(ImportWorkflow $importWorkflow): static + { + if ($this->importWorkflows->removeElement($importWorkflow)) { + if ($importWorkflow->getSubmitter() === $this) { + $importWorkflow->setSubmitter(null); + } + } + return $this; + } } diff --git a/hesabixCore/src/Repository/CommodityRepository.php b/hesabixCore/src/Repository/CommodityRepository.php index 8cbde62..1fdef24 100644 --- a/hesabixCore/src/Repository/CommodityRepository.php +++ b/hesabixCore/src/Repository/CommodityRepository.php @@ -142,8 +142,11 @@ class CommodityRepository extends ServiceEntityRepository } } - $query->setMaxResults($params['Take']) - ->orderBy('p.id', 'ASC'); + if (isset($params['Take']) && $params['Take'] !== -1) { + $query->setMaxResults($params['Take']); + } + + $query->orderBy('p.id', 'ASC'); return $query->getQuery()->getResult(); } diff --git a/hesabixCore/src/Repository/ImportWorkflowCustomsRepository.php b/hesabixCore/src/Repository/ImportWorkflowCustomsRepository.php new file mode 100644 index 0000000..65cbf26 --- /dev/null +++ b/hesabixCore/src/Repository/ImportWorkflowCustomsRepository.php @@ -0,0 +1,36 @@ +createQueryBuilder('iwc') + ->andWhere('iwc.importWorkflow = :importWorkflowId') + ->setParameter('importWorkflowId', $importWorkflowId) + ->orderBy('iwc.dateSubmit', 'ASC') + ->getQuery() + ->getResult(); + } + + public function findByDeclarationNumber($declarationNumber, $importWorkflowId) + { + return $this->createQueryBuilder('iwc') + ->andWhere('iwc.declarationNumber = :declarationNumber') + ->andWhere('iwc.importWorkflow = :importWorkflowId') + ->setParameter('declarationNumber', $declarationNumber) + ->setParameter('importWorkflowId', $importWorkflowId) + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/hesabixCore/src/Repository/ImportWorkflowDocumentRepository.php b/hesabixCore/src/Repository/ImportWorkflowDocumentRepository.php new file mode 100644 index 0000000..69de81c --- /dev/null +++ b/hesabixCore/src/Repository/ImportWorkflowDocumentRepository.php @@ -0,0 +1,37 @@ +createQueryBuilder('iwd') + ->andWhere('iwd.importWorkflow = :importWorkflowId') + ->setParameter('importWorkflowId', $importWorkflowId) + ->orderBy('iwd.dateSubmit', 'ASC') + ->getQuery() + ->getResult(); + } + + public function findByType($type, $importWorkflowId) + { + return $this->createQueryBuilder('iwd') + ->andWhere('iwd.type = :type') + ->andWhere('iwd.importWorkflow = :importWorkflowId') + ->setParameter('type', $type) + ->setParameter('importWorkflowId', $importWorkflowId) + ->orderBy('iwd.dateSubmit', 'ASC') + ->getQuery() + ->getResult(); + } +} diff --git a/hesabixCore/src/Repository/ImportWorkflowItemRepository.php b/hesabixCore/src/Repository/ImportWorkflowItemRepository.php new file mode 100644 index 0000000..408ce49 --- /dev/null +++ b/hesabixCore/src/Repository/ImportWorkflowItemRepository.php @@ -0,0 +1,25 @@ +createQueryBuilder('iwi') + ->andWhere('iwi.importWorkflow = :importWorkflowId') + ->setParameter('importWorkflowId', $importWorkflowId) + ->orderBy('iwi.dateSubmit', 'ASC') + ->getQuery() + ->getResult(); + } +} diff --git a/hesabixCore/src/Repository/ImportWorkflowPaymentRepository.php b/hesabixCore/src/Repository/ImportWorkflowPaymentRepository.php new file mode 100644 index 0000000..8def466 --- /dev/null +++ b/hesabixCore/src/Repository/ImportWorkflowPaymentRepository.php @@ -0,0 +1,37 @@ +createQueryBuilder('iwp') + ->andWhere('iwp.importWorkflow = :importWorkflowId') + ->setParameter('importWorkflowId', $importWorkflowId) + ->orderBy('iwp.paymentDate', 'ASC') + ->getQuery() + ->getResult(); + } + + public function findByType($type, $importWorkflowId) + { + return $this->createQueryBuilder('iwp') + ->andWhere('iwp.type = :type') + ->andWhere('iwp.importWorkflow = :importWorkflowId') + ->setParameter('type', $type) + ->setParameter('importWorkflowId', $importWorkflowId) + ->orderBy('iwp.paymentDate', 'ASC') + ->getQuery() + ->getResult(); + } +} diff --git a/hesabixCore/src/Repository/ImportWorkflowRepository.php b/hesabixCore/src/Repository/ImportWorkflowRepository.php new file mode 100644 index 0000000..d8b3be0 --- /dev/null +++ b/hesabixCore/src/Repository/ImportWorkflowRepository.php @@ -0,0 +1,66 @@ +createQueryBuilder('iw') + ->andWhere('iw.business = :businessId') + ->setParameter('businessId', $businessId) + ->orderBy('iw.dateSubmit', 'DESC') + ->getQuery() + ->getResult(); + } + + public function findByStatus($status, $businessId) + { + return $this->createQueryBuilder('iw') + ->andWhere('iw.status = :status') + ->andWhere('iw.business = :businessId') + ->setParameter('status', $status) + ->setParameter('businessId', $businessId) + ->orderBy('iw.dateSubmit', 'DESC') + ->getQuery() + ->getResult(); + } + + public function findByCode($code, $businessId) + { + return $this->createQueryBuilder('iw') + ->andWhere('iw.code = :code') + ->andWhere('iw.business = :businessId') + ->setParameter('code', $code) + ->setParameter('businessId', $businessId) + ->getQuery() + ->getOneOrNullResult(); + } + + public function findWithDetails($id, $businessId) + { + return $this->createQueryBuilder('iw') + ->leftJoin('iw.items', 'items') + ->leftJoin('iw.payments', 'payments') + ->leftJoin('iw.documents', 'documents') + ->leftJoin('iw.stages', 'stages') + ->leftJoin('iw.shipping', 'shipping') + ->leftJoin('iw.customs', 'customs') + ->addSelect('items', 'payments', 'documents', 'stages', 'shipping', 'customs') + ->andWhere('iw.id = :id') + ->andWhere('iw.business = :businessId') + ->setParameter('id', $id) + ->setParameter('businessId', $businessId) + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/hesabixCore/src/Repository/ImportWorkflowShippingRepository.php b/hesabixCore/src/Repository/ImportWorkflowShippingRepository.php new file mode 100644 index 0000000..58b737a --- /dev/null +++ b/hesabixCore/src/Repository/ImportWorkflowShippingRepository.php @@ -0,0 +1,36 @@ +createQueryBuilder('iws') + ->andWhere('iws.importWorkflow = :importWorkflowId') + ->setParameter('importWorkflowId', $importWorkflowId) + ->orderBy('iws.dateSubmit', 'ASC') + ->getQuery() + ->getResult(); + } + + public function findByType($type, $importWorkflowId) + { + return $this->createQueryBuilder('iws') + ->andWhere('iws.type = :type') + ->andWhere('iws.importWorkflow = :importWorkflowId') + ->setParameter('type', $type) + ->setParameter('importWorkflowId', $importWorkflowId) + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/hesabixCore/src/Repository/ImportWorkflowStageRepository.php b/hesabixCore/src/Repository/ImportWorkflowStageRepository.php new file mode 100644 index 0000000..422d187 --- /dev/null +++ b/hesabixCore/src/Repository/ImportWorkflowStageRepository.php @@ -0,0 +1,36 @@ +createQueryBuilder('iws') + ->andWhere('iws.importWorkflow = :importWorkflowId') + ->setParameter('importWorkflowId', $importWorkflowId) + ->orderBy('iws.dateSubmit', 'ASC') + ->getQuery() + ->getResult(); + } + + public function findByStage($stage, $importWorkflowId) + { + return $this->createQueryBuilder('iws') + ->andWhere('iws.stage = :stage') + ->andWhere('iws.importWorkflow = :importWorkflowId') + ->setParameter('stage', $stage) + ->setParameter('importWorkflowId', $importWorkflowId) + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/hesabixCore/src/Repository/PlugWarrantySerialRepository.php b/hesabixCore/src/Repository/PlugWarrantySerialRepository.php index 63fa855..746a565 100644 --- a/hesabixCore/src/Repository/PlugWarrantySerialRepository.php +++ b/hesabixCore/src/Repository/PlugWarrantySerialRepository.php @@ -22,7 +22,8 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository public function findByBusiness($bid): array { return $this->createQueryBuilder('p') - ->andWhere('p.bid = :val') + ->join('p.business', 'b') + ->andWhere('b.id = :val') ->setParameter('val', $bid) ->orderBy('p.dateSubmit', 'DESC') ->getQuery() @@ -36,7 +37,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository public function findByCommodity($bid, $commodityId): array { return $this->createQueryBuilder('p') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->andWhere('p.commodity = :commodityId') ->setParameter('bid', $bid) ->setParameter('commodityId', $commodityId) @@ -52,7 +53,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository public function findByStatus($bid, $status): array { return $this->createQueryBuilder('p') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->andWhere('p.status = :status') ->setParameter('bid', $bid) ->setParameter('status', $status) @@ -72,7 +73,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository ->setParameter('serialNumber', $serialNumber); if ($bid) { - $qb->andWhere('p.bid = :bid') + $qb->andWhere('p.business = :bid') ->setParameter('bid', $bid); } @@ -86,7 +87,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository { return $this->createQueryBuilder('p') ->leftJoin('p.commodity', 'c') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->andWhere('p.serialNumber LIKE :keyword OR c.name LIKE :keyword OR p.description LIKE :keyword') ->setParameter('bid', $bid) ->setParameter('keyword', '%' . $keyword . '%') diff --git a/hesabixCore/src/Service/Access.php b/hesabixCore/src/Service/Access.php index 971569c..c519049 100644 --- a/hesabixCore/src/Service/Access.php +++ b/hesabixCore/src/Service/Access.php @@ -50,9 +50,10 @@ class Access return false; } } - elseif($this->request->headers->get('api-key')){ + elseif($this->request->headers->get('api-key') || $this->request->headers->get('X-AUTH-TOKEN')){ + $rawToken = $this->request->headers->get('api-key') ?: $this->request->headers->get('X-AUTH-TOKEN'); $token = $this->em->getRepository(APIToken::class)->findOneBy([ - 'token'=>$this->request->headers->get('api-key') + 'token'=> $rawToken ]); if(!$token) { return false; } @@ -68,6 +69,10 @@ class Access $bid = $token->getBid(); } + else { + // بدون BID فعال یا توکن معتبر + return false; + } if ($this->request->headers->get('activeYear')) { $year = $this->em->getRepository(Year::class)->findOneBy([ 'id' => $this->request->headers->get('activeYear'), @@ -75,7 +80,7 @@ class Access ]); if (!$year) { return false; } } - elseif($this->request->headers->get('api-key')){ + elseif($this->request->headers->get('api-key') || $this->request->headers->get('X-AUTH-TOKEN')){ $year = $this->em->getRepository(Year::class)->findOneBy([ 'head' => true, 'bid'=>$bid @@ -126,6 +131,19 @@ class Access elseif ($this->user && $roll == 'join' && count($this->em->getRepository(Permission::class)->findBy(['user'=>$this->user,'bid'=>$bid]))){ return $accessArray; } + + $warehousePermission = $this->em->getRepository(Permission::class)->findOneBy([ + 'bid'=>$bid, + 'user'=>$this->user + ]); + + if($warehousePermission && $warehousePermission->isWarehouseManager()){ + $warehouseRoles = ['commodity', 'store', 'plugWarrantyManager']; + if(in_array($roll, $warehouseRoles)){ + return $accessArray; + } + } + $methodName = 'is' . ucfirst($roll); $permission = $this->em->getRepository(Permission::class)->findOneBy([ 'bid'=>$bid, diff --git a/hesabixCore/src/Service/Explore.php b/hesabixCore/src/Service/Explore.php index 751fa77..d1b014a 100644 --- a/hesabixCore/src/Service/Explore.php +++ b/hesabixCore/src/Service/Explore.php @@ -171,6 +171,10 @@ class Explore 'amount' => $doc->getAmount(), 'mdate' => '', 'plugin' => $doc->getPlugin(), + // Approval fields + 'isPreview' => $doc->isPreview(), + 'isApproved' => $doc->isApproved(), + 'approvedBy' => $doc->getApprovedBy() ? self::ExploreUser($doc->getApprovedBy()) : null, ]; } @@ -210,6 +214,9 @@ class Explore $row->setDiscount(0); $temp['tax'] = $row->getTax(); $temp['discount'] = $row->getDiscount(); + + // Approval fields - از سند اصلی گرفته می‌شود + return $temp; } @@ -347,6 +354,7 @@ class Explore 'address' => $person->getAddress(), 'prelabel' => null, 'tags' => $person->getTags(), + 'requireTwoStep' => $person->getBid() ? $person->getBid()->isRequireTwoStepApproval() : false, ]; if ($person->getPrelabel()) { $res['prelabel'] = $person->getPrelabel()->getLabel(); @@ -570,6 +578,10 @@ class Explore 'shortlinks' => $item->isShortlinks(), 'walletEnabled' => $item->isWalletEnable(), 'walletMatchBank' => $item->getWalletMatchBank() ? $item->getWalletMatchBank()->getId() : null, + 'requireTwoStepApproval' => $item->isRequireTwoStepApproval(), + 'invoiceApprover' => $item->getInvoiceApprover(), + 'warehouseApprover' => $item->getWarehouseApprover(), + 'financialApprover' => $item->getFinancialApprover(), 'updateSellPrice' => $item->isCommodityUpdateSellPriceAuto(), 'updateBuyPrice' => $item->isCommodityUpdateBuyPriceAuto(), ]; diff --git a/hesabixCore/src/Service/FileStorage.php b/hesabixCore/src/Service/FileStorage.php new file mode 100644 index 0000000..938a4cb --- /dev/null +++ b/hesabixCore/src/Service/FileStorage.php @@ -0,0 +1,42 @@ +getClientOriginalName()); + $relativeDir = 'storage/' . trim($businessId) . '/' . trim($context); + $absDir = rtrim($this->kernel->getProjectDir(), '/').'/var/' . $relativeDir; + if (!is_dir($absDir)) { + @mkdir($absDir, 0775, true); + } + $newName = uniqid('f_', true) . '_' . $safeOriginal; + $file->move($absDir, $newName); + $absPath = $absDir . '/' . $newName; + $size = @filesize($absPath) ?: null; + $mime = function_exists('mime_content_type') ? @mime_content_type($absPath) : null; + return [ + 'relativePath' => $relativeDir . '/' . $newName, + 'originalName' => $file->getClientOriginalName(), + 'size' => $size, + 'mime' => $mime ?: $file->getClientMimeType(), + ]; + } + + public function absolutePath(string $relativePath): string + { + $relativePath = ltrim($relativePath, '/'); + return rtrim($this->kernel->getProjectDir(), '/').'/var/' . $relativePath; + } +} + + diff --git a/public_html/uploads/storeroom/st_6899dc09b4eb05.86469834_4837DCA8-0915-4C7B-9BE1-70C87FF4385A.jpeg b/public_html/uploads/storeroom/st_6899dc09b4eb05.86469834_4837DCA8-0915-4C7B-9BE1-70C87FF4385A.jpeg new file mode 100644 index 0000000..ffe5a5e Binary files /dev/null and b/public_html/uploads/storeroom/st_6899dc09b4eb05.86469834_4837DCA8-0915-4C7B-9BE1-70C87FF4385A.jpeg differ diff --git a/public_html/uploads/storeroom/st_68a1ad4e1bb6c5.93976672_Screenshot_2025-03-30_233821.png b/public_html/uploads/storeroom/st_68a1ad4e1bb6c5.93976672_Screenshot_2025-03-30_233821.png new file mode 100644 index 0000000..795a746 Binary files /dev/null and b/public_html/uploads/storeroom/st_68a1ad4e1bb6c5.93976672_Screenshot_2025-03-30_233821.png differ diff --git a/public_html/uploads/storeroom/st_68a1af08b47979.56036064_Screenshot_2025-03-30_233821.png b/public_html/uploads/storeroom/st_68a1af08b47979.56036064_Screenshot_2025-03-30_233821.png new file mode 100644 index 0000000..795a746 Binary files /dev/null and b/public_html/uploads/storeroom/st_68a1af08b47979.56036064_Screenshot_2025-03-30_233821.png differ diff --git a/public_html/uploads/storeroom/st_68a1b350a9b8c3.34236142_NDr4WkNHDI.pdf b/public_html/uploads/storeroom/st_68a1b350a9b8c3.34236142_NDr4WkNHDI.pdf new file mode 100644 index 0000000..6717a5f Binary files /dev/null and b/public_html/uploads/storeroom/st_68a1b350a9b8c3.34236142_NDr4WkNHDI.pdf differ diff --git a/webUI/package.json b/webUI/package.json index b1999b0..e9b282f 100755 --- a/webUI/package.json +++ b/webUI/package.json @@ -41,6 +41,7 @@ "maz-ui": "^3.50.1", "monaco-editor": "^0.52.2", "pinia": "^3.0.2", + "qr-scanner": "^1.4.2", "sweetalert2": "^11.4.8", "v-money3": "^3.24.1", "vue": "^3.5.13", diff --git a/webUI/src/components/common/ApprovalManager.vue b/webUI/src/components/common/ApprovalManager.vue new file mode 100644 index 0000000..d180969 --- /dev/null +++ b/webUI/src/components/common/ApprovalManager.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/webUI/src/components/common/ApprovalStatus.vue b/webUI/src/components/common/ApprovalStatus.vue new file mode 100644 index 0000000..e608050 --- /dev/null +++ b/webUI/src/components/common/ApprovalStatus.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/webUI/src/components/plugins/import-workflow/ImportWorkflowCreateDialog.vue b/webUI/src/components/plugins/import-workflow/ImportWorkflowCreateDialog.vue new file mode 100644 index 0000000..e13d861 --- /dev/null +++ b/webUI/src/components/plugins/import-workflow/ImportWorkflowCreateDialog.vue @@ -0,0 +1,306 @@ + + + + diff --git a/webUI/src/components/plugins/import-workflow/ImportWorkflowCustoms.vue b/webUI/src/components/plugins/import-workflow/ImportWorkflowCustoms.vue new file mode 100644 index 0000000..f93ac61 --- /dev/null +++ b/webUI/src/components/plugins/import-workflow/ImportWorkflowCustoms.vue @@ -0,0 +1,423 @@ + + + + + \ No newline at end of file diff --git a/webUI/src/components/plugins/import-workflow/ImportWorkflowDocuments.vue b/webUI/src/components/plugins/import-workflow/ImportWorkflowDocuments.vue new file mode 100644 index 0000000..b654f55 --- /dev/null +++ b/webUI/src/components/plugins/import-workflow/ImportWorkflowDocuments.vue @@ -0,0 +1,460 @@ + + + + + + + + diff --git a/webUI/src/components/plugins/import-workflow/ImportWorkflowItems.vue b/webUI/src/components/plugins/import-workflow/ImportWorkflowItems.vue new file mode 100644 index 0000000..9e65d8e --- /dev/null +++ b/webUI/src/components/plugins/import-workflow/ImportWorkflowItems.vue @@ -0,0 +1,523 @@ + + + + + + + + diff --git a/webUI/src/components/plugins/import-workflow/ImportWorkflowPayments.vue b/webUI/src/components/plugins/import-workflow/ImportWorkflowPayments.vue new file mode 100644 index 0000000..f2cc8fc --- /dev/null +++ b/webUI/src/components/plugins/import-workflow/ImportWorkflowPayments.vue @@ -0,0 +1,547 @@ + + + + + + + + diff --git a/webUI/src/components/plugins/import-workflow/ImportWorkflowShipping.vue b/webUI/src/components/plugins/import-workflow/ImportWorkflowShipping.vue new file mode 100644 index 0000000..b7f1981 --- /dev/null +++ b/webUI/src/components/plugins/import-workflow/ImportWorkflowShipping.vue @@ -0,0 +1,394 @@ + + + + + + + + diff --git a/webUI/src/components/plugins/import-workflow/ImportWorkflowStages.vue b/webUI/src/components/plugins/import-workflow/ImportWorkflowStages.vue new file mode 100644 index 0000000..2f8b7a1 --- /dev/null +++ b/webUI/src/components/plugins/import-workflow/ImportWorkflowStages.vue @@ -0,0 +1,454 @@ + + + + + + + + diff --git a/webUI/src/components/plugins/warranty/BulkImportDialog.vue b/webUI/src/components/plugins/warranty/BulkImportDialog.vue index ab0cd28..a8853f4 100644 --- a/webUI/src/components/plugins/warranty/BulkImportDialog.vue +++ b/webUI/src/components/plugins/warranty/BulkImportDialog.vue @@ -137,16 +137,55 @@
-
آپلود فایل Excel
+
+
آپلود فایل Excel یا CSV
+
+ + دانلود CSV نمونه + + + دانلود Excel نمونه + +
+
+ + +
+ نکات مهم: +
    +
  • فایل باید شامل ستون‌های: شماره سریال، کد کالا، توضیحات، شروع گارانتی، پایان گارانتی، وضعیت باشد
  • +
  • شماره سریال و کد کالا فیلدهای الزامی هستند
  • +
  • کد کالا باید دقیقاً مطابق با کد موجود در سیستم باشد
  • +
  • تاریخ‌ها باید به فرمت YYYY/MM/DD یا YYYY-MM-DD باشند
  • +
  • وضعیت می‌تواند: active، inactive، یا expired باشد
  • +
+
+
(null) const filePreview = ref([]) const fileErrors = ref([]) @@ -285,7 +324,7 @@ const tableHeaders = [ const previewHeaders = [ { title: 'شماره سریال', key: 'serialNumber' }, - { title: 'محصول', key: 'commodity' }, + { title: 'کد کالا', key: 'commodity_code' }, { title: 'توضیحات', key: 'description' }, { title: 'شروع گارانتی', key: 'warrantyStartDate' }, { title: 'پایان گارانتی', key: 'warrantyEndDate' }, @@ -336,28 +375,61 @@ const removeManualRow = (index: number) => { manualSerials.value.splice(index, 1) } -const handleFileUpload = async (file: File) => { +const handleFileUpload = async (files: File | File[] | null) => { + if (!files) { + filePreview.value = [] + fileErrors.value = [] + return + } + + const file = Array.isArray(files) ? files[0] : files + if (!file) { filePreview.value = [] fileErrors.value = [] return } + if (!(file instanceof File)) { + fileErrors.value = ['فایل انتخاب شده معتبر نیست'] + return + } + try { const formData = new FormData() formData.append('file', file) - const response = await axios.post('/api/plugins/warranty/serials/preview-import', formData, { + const endpoint = '/api/plugins/warranty/serials/preview-import' + const fileName = file.name || '' + + const response = await axios.post(endpoint, formData, { headers: { 'Content-Type': 'multipart/form-data' } }) - filePreview.value = response.data.preview || [] + let previewData = response.data.preview || [] + if (previewData.length > 0) { + previewData = previewData.map((item: any) => { + if (item.commodity && !item.commodity_code) { + item.commodity_code = item.commodity + } + + if (item.commodity_id && !item.commodity_code) { + item.commodity_code = item.commodity_id + } + return item + }) + } + + filePreview.value = previewData fileErrors.value = response.data.errors || [] - } catch (error) { - fileErrors.value = ['خطا در خواندن فایل'] - console.error(error) + } catch (error: any) { + if (error.response?.data?.error) { + fileErrors.value = [error.response.data.error] + } else { + fileErrors.value = ['خطا در خواندن فایل'] + } } } @@ -393,7 +465,14 @@ const importData = async () => { return } - data = validSerials + data = validSerials.map(s => ({ + serialNumber: s.serialNumber, + commodity_code: s.commodity_id, + description: s.description, + warrantyStartDate: s.warrantyStartDate, + warrantyEndDate: s.warrantyEndDate, + status: s.status + })) } else { if (!uploadedFile.value) { await Swal.fire({ @@ -407,13 +486,29 @@ const importData = async () => { const formData = new FormData() formData.append('file', uploadedFile.value) - const response = await axios.post('/api/plugins/warranty/serials/import-excel', formData, { + const endpoint = '/api/plugins/warranty/serials/import-excel' + const fileName = uploadedFile.value?.name || '' + + const response = await axios.post(endpoint, formData, { headers: { 'Content-Type': 'multipart/form-data' } }) - data = response.data.serials || [] + let fileData = response.data.serials || [] + if (fileData.length > 0) { + fileData = fileData.map((item: any) => { + if (item.commodity && !item.commodity_code) { + return { + ...item, + commodity_code: item.commodity + } + } + return item + }) + } + + data = fileData } if (data.length === 0) { @@ -438,7 +533,6 @@ const importData = async () => { icon: 'error', confirmButtonText: 'قبول' }) - console.error(error) } finally { loading.value = false } @@ -462,6 +556,129 @@ const handleCommoditySelect = (selectedCommodity: any, index: number) => { } } +const downloadSampleFile = () => { + const sampleData = [ + { + serialNumber: 'SN001', + commodity_code: '1012', + description: 'لپ‌تاپ گیمینگ', + warrantyStartDate: '1403/01/01', + warrantyEndDate: '1406/01/01', + status: 'active' + }, + { + serialNumber: 'SN002', + commodity_code: '1012', + description: 'ماوس بی‌سیم', + warrantyStartDate: '1403/02/01', + warrantyEndDate: '1405/02/01', + status: 'active' + }, + { + serialNumber: 'SN003', + commodity_code: '1012', + description: 'کیبورد مکانیکال', + warrantyStartDate: '1402/12/01', + warrantyEndDate: '1404/12/01', + status: 'expired' + } + ] + + const headers = ['شماره سریال', 'کد کالا', 'توضیحات', 'شروع گارانتی', 'پایان گارانتی', 'وضعیت'] + const csvContent = [ + headers.join(','), + ...sampleData.map(row => [ + row.serialNumber, + row.commodity_code, + row.description, + row.warrantyStartDate, + row.warrantyEndDate, + row.status + ].join(',')) + ].join('\n') + + const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }) + const link = document.createElement('a') + const url = URL.createObjectURL(blob) + link.setAttribute('href', url) + link.setAttribute('download', 'نمونه_سریال_گارانتی.csv') + link.style.visibility = 'hidden' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} + +const downloadExcelSample = () => { + const sampleData = [ + { + serialNumber: 'SN001', + commodity_code: '1012', + description: 'لپ‌تاپ گیمینگ', + warrantyStartDate: '1403/01/01', + warrantyEndDate: '1406/01/01', + status: 'active' + }, + { + serialNumber: 'SN002', + commodity_code: '1012', + description: 'ماوس بی‌سیم', + warrantyStartDate: '1403/02/01', + warrantyEndDate: '1405/02/01', + status: 'active' + }, + { + serialNumber: 'SN003', + commodity_code: '1012', + description: 'کیبورد مکانیکال', + warrantyStartDate: '1402/12/01', + warrantyEndDate: '1404/12/01', + status: 'expired' + } + ] + + const headers = ['شماره سریال', 'کد کالا', 'توضیحات', 'شروع گارانتی', 'پایان گارانتی', 'وضعیت'] + + let htmlContent = ` + + + + + + + + + ${headers.map(h => ``).join('')} + + ${sampleData.map(row => ` + + + + + + + + + `).join('')} +
${h}
${row.serialNumber}${row.commodity_code}${row.description}${row.warrantyStartDate}${row.warrantyEndDate}${row.status}
+ + + ` + + const blob = new Blob([htmlContent], { type: 'application/vnd.ms-excel' }) + const link = document.createElement('a') + const url = URL.createObjectURL(blob) + link.setAttribute('href', url) + link.setAttribute('download', 'نمونه_سریال_گارانتی.xls') + link.style.visibility = 'hidden' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} + const resetData = () => { manualSerials.value = [{ serialNumber: '', diff --git a/webUI/src/components/plugins/warranty/SerialDialog.vue b/webUI/src/components/plugins/warranty/SerialDialog.vue index 1d5ef89..c36b178 100644 --- a/webUI/src/components/plugins/warranty/SerialDialog.vue +++ b/webUI/src/components/plugins/warranty/SerialDialog.vue @@ -1,5 +1,5 @@ - \ No newline at end of file + +video { + border-radius: 12px; + padding: 2px; +} + +.v-dialog { + direction: rtl +} + +.qr-card { + max-width: 95vw; + border-radius: 16px +} + +.qr-title { + text-align: center; + padding: 14px 16px +} + +.qr-wrap { + position: relative; + width: 100%; + max-width: 560px; + margin: 0 auto +} + +.qr-reader { + width: 100%; + min-height: 320px; + border-radius: 12px; + border: 1px solid #e5e7eb; + overflow: hidden; + background: #000; +} + +/* ویدئو تولیدی کتابخانه */ +:deep(#reader video) { + width: 100% !important; + height: auto !important; + display: block !important; + object-fit: cover; + min-height: 240px; +} + +/* کانتینر داخلی کتابخانه راست به چپ نشه */ +:deep(#reader div) { + direction: ltr +} + +/* وضعیت‌ها */ +.qr-status { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 12px +} + +.qr-actions { + padding: 12px 16px +} + +/* ریسپانسیو */ +@media (max-width:1024px) { + .qr-reader { + min-height: 300px + } + + :deep(#reader video) { + min-height: 220px + } +} + +@media (max-width:768px) { + .qr-card { + max-width: 95vw + } + + .qr-reader { + min-height: 260px + } + + :deep(#reader video) { + min-height: 200px + } +} + +@media (max-width:480px) { + .qr-reader { + min-height: 220px + } + + :deep(#reader video) { + min-height: 180px + } +} + +.v-input.v-input--horizontal.v-input--center-affix.v-input--density-compact.v-theme--light.v-locale--is-rtl.v-input--error.v-text-field.my-0 { + height: 3rem; +} + diff --git a/webUI/src/components/plugins/warranty/SerialViewDialog.vue b/webUI/src/components/plugins/warranty/SerialViewDialog.vue index 267575e..9477d02 100644 --- a/webUI/src/components/plugins/warranty/SerialViewDialog.vue +++ b/webUI/src/components/plugins/warranty/SerialViewDialog.vue @@ -1,43 +1,55 @@