From fc2838d68ae6df37879adebff0dc705e43e953a2 Mon Sep 17 00:00:00 2001 From: shopkeeperdev Date: Mon, 3 Nov 2025 17:20:07 -0500 Subject: [PATCH] Initial commit: Vendor Sales Report Magento 2 module --- .github/copilot-instructions.md | 141 +++++++++ Block/Adminhtml/Report.php | 78 +++++ Controller/Adminhtml/Report/Export.php | 91 ++++++ Controller/Adminhtml/Report/Grid.php | 86 +++++ Controller/Adminhtml/Report/Index.php | 43 +++ Cron/GenerateMonthlyReport.php | 99 ++++++ Helper/Data.php | 114 +++++++ INSTALL.md | 97 ++++++ Model/EmailSender.php | 116 +++++++ Model/ReportGenerator.php | 201 ++++++++++++ README.md | 254 +++++++++++++++ composer.json | 28 ++ etc/acl.xml | 21 ++ etc/adminhtml/menu.xml | 12 + etc/adminhtml/routes.xml | 8 + etc/adminhtml/system.xml | 55 ++++ etc/config.xml | 16 + etc/crontab.xml | 8 + etc/email_templates.xml | 4 + etc/module.xml | 10 + registration.php | 9 + view/adminhtml/email/report.html | 22 ++ .../layout/vendorsalesreport_report_index.xml | 12 + view/adminhtml/templates/report/view.phtml | 299 ++++++++++++++++++ view/adminhtml/web/css/vendor-report.css | 15 + 25 files changed, 1839 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 Block/Adminhtml/Report.php create mode 100644 Controller/Adminhtml/Report/Export.php create mode 100644 Controller/Adminhtml/Report/Grid.php create mode 100644 Controller/Adminhtml/Report/Index.php create mode 100644 Cron/GenerateMonthlyReport.php create mode 100644 Helper/Data.php create mode 100644 INSTALL.md create mode 100644 Model/EmailSender.php create mode 100644 Model/ReportGenerator.php create mode 100644 README.md create mode 100644 composer.json create mode 100644 etc/acl.xml create mode 100644 etc/adminhtml/menu.xml create mode 100644 etc/adminhtml/routes.xml create mode 100644 etc/adminhtml/system.xml create mode 100644 etc/config.xml create mode 100644 etc/crontab.xml create mode 100644 etc/email_templates.xml create mode 100644 etc/module.xml create mode 100644 registration.php create mode 100644 view/adminhtml/email/report.html create mode 100644 view/adminhtml/layout/vendorsalesreport_report_index.xml create mode 100644 view/adminhtml/templates/report/view.phtml create mode 100644 view/adminhtml/web/css/vendor-report.css diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..aa8c454 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,141 @@ +# AI Coding Instructions for Vendor Sales Report Module + +## Project Overview +This is a Magento 2 module (`Shopkeeper_VendorSalesReport`) that generates monthly vendor sales reports with automated email delivery. The module provides both manual report generation via admin interface and automated monthly reports via cron. + +## Architecture & Key Components + +### Core Models +- **`Model/ReportGenerator.php`**: Handles report data generation using raw SQL queries joining `sales_order`, `sales_order_item`, `sales_order_address`, and product tables. Includes state abbreviation logic and cost fallback from order item to current product cost. +- **`Model/EmailSender.php`**: Manages email delivery using Magento's TransportBuilder with CSV attachments. +- **`Helper/Data.php`**: Configuration helper using Magento's scope config for settings like order status filters, email recipients, and subject templates. + +### Admin Interface +- **Controller Structure**: `Controller/Adminhtml/Report/` with `Index.php` (main page), `Grid.php` (data grid), `Export.php` (CSV download). +- **Menu Integration**: Located under `Reports > Sales > Vendor Sales Report` in admin panel. +- **Configuration**: System config at `Stores > Configuration > Shopkeeper > Vendor Sales Report` with general and email settings. + +### Automation +- **`Cron/GenerateMonthlyReport.php`**: Executes monthly on the 1st at 2:00 AM, generates previous month's report, and emails it. +- **Cron Schedule**: `0 2 1 * *` (crontab.xml configuration). + +## Critical Patterns & Conventions + +### Report Generation Logic +```php +// Always exclude configurable products from reports +AND oi.product_type != 'configurable' + +// Cost fallback hierarchy: order item cost -> current product cost -> 0 +COALESCE(NULLIF(oi.base_cost, 0), current_cost.value, 0) + +// State abbreviation: only abbreviate if full name, leave 2-char codes unchanged +protected function abbreviateState($stateName) { + if (strlen($stateName) == 2) return $stateName; + return $this->stateAbbreviations[$stateName] ?? $stateName; +} +``` + +### CSV Export Format +- **Filename**: Always `FF_SAC_730_MCQUEEN.csv` (hardcoded in `ReportGenerator::getFilename()`) +- **Columns**: Date, Vendor Product number, Description, Vendor code, Vendor name, Dealer product number, Country, City, State, Zip code, Quantity, Cost +- **Date Format**: MM/DD/YYYY +- **Country**: "United States" for US orders, otherwise country code +- **State**: Abbreviated for US addresses only + +### Configuration Access +```php +// Use helper for all config access - never access scopeConfig directly in controllers/models +$this->configHelper->isEnabled() +$this->configHelper->getOrderStatus() // Returns array +$this->configHelper->getEmailRecipients() // Returns array +$this->configHelper->getEmailSubject($month, $year) // Handles {{month}}/{{year}} replacement +``` + +### Email Templates +- **Template ID**: `vendorsalesreport_email_template` +- **Variables**: `subject`, `month`, `year` +- **Attachment**: CSV file with `text/csv` MIME type + +## Development Workflow + +### Module Installation +```bash +# Enable module +php bin/magento module:enable Shopkeeper_VendorSalesReport +php bin/magento setup:upgrade +php bin/magento setup:di:compile +php bin/magento setup:static-content:deploy -f +php bin/magento cache:flush +``` + +### Testing Cron Jobs +```bash +# Manual cron execution +php bin/magento cron:run --group default + +# Check cron_schedule table +SELECT * FROM cron_schedule WHERE job_code = 'shopkeeper_vendorsalesreport_monthly' ORDER BY scheduled_at DESC; +``` + +### Debugging +- **Logs**: Check `var/log/system.log` for module-specific messages +- **SQL Queries**: Report generation uses complex joins - examine `ReportGenerator::generateReportData()` for table relationships +- **Email Issues**: Verify SMTP configuration and check Magento's email settings + +## Common Pitfalls + +### Data Issues +- **Empty Reports**: Check order status filter (default: 'complete') and date ranges +- **Missing Costs**: Ensure products have cost attribute set, module falls back gracefully +- **State Abbreviations**: Only US states are abbreviated; international addresses remain unchanged + +### Configuration +- **Email Recipients**: Must be comma-separated, validated before sending +- **Order Status**: Stored as comma-separated string, converted to array by helper +- **Subject Templates**: Support `{{month}}` and `{{year}}` placeholders + +### Performance +- **Raw SQL**: Used for performance with large datasets - avoid ORM for report queries +- **Memory Usage**: CSV generation uses `php://temp` stream to handle large reports +- **Cron Timing**: Runs early morning to avoid peak hours + +## Extension Points + +### Adding New Report Columns +1. Modify SQL query in `ReportGenerator::generateReportData()` +2. Update CSV headers in `ReportGenerator::generateCsv()` +3. Ensure column exclusion logic handles new fields appropriately + +### Custom Email Logic +1. Extend `EmailSender` class +2. Modify email template variables in `email_templates.xml` +3. Update template file `view/adminhtml/email/report.html` + +### Additional Filters +1. Add new config fields in `system.xml` +2. Update `Helper/Data.php` with new getter methods +3. Modify SQL WHERE clauses in `ReportGenerator::generateReportData()` + +## File Structure Reference +``` +etc/ +├── module.xml # Module dependencies +├── config.xml # Default configuration values +├── crontab.xml # Cron job definitions +├── email_templates.xml # Email template registration +└── adminhtml/ + ├── menu.xml # Admin menu placement + ├── routes.xml # URL routing + └── system.xml # Configuration UI + +Model/ # Business logic +├── ReportGenerator.php # Core report generation +└── EmailSender.php # Email delivery + +Controller/Adminhtml/Report/ # Admin interface +├── Index.php # Main report page +├── Grid.php # Data grid (AJAX) +└── Export.php # CSV download +``` +t:\Files\Documents\BIZ\Clients\McQueen\Repos\VendorSalesReport\.github\copilot-instructions.md \ No newline at end of file diff --git a/Block/Adminhtml/Report.php b/Block/Adminhtml/Report.php new file mode 100644 index 0000000..26438e2 --- /dev/null +++ b/Block/Adminhtml/Report.php @@ -0,0 +1,78 @@ +configHelper = $configHelper; + parent::__construct($context, $data); + } + + /** + * Get grid URL + * + * @return string + */ + public function getGridUrl() + { + return $this->getUrl('*/*/grid'); + } + + /** + * Get export URL + * + * @return string + */ + public function getExportUrl() + { + return $this->getUrl('*/*/export'); + } + + /** + * Get configured order statuses + * + * @return array + */ + public function getOrderStatuses() + { + return $this->configHelper->getOrderStatus(); + } + + /** + * Get default start date (first day of last month) + * + * @return string + */ + public function getDefaultStartDate() + { + return date('Y-m-01', strtotime('first day of last month')); + } + + /** + * Get default end date (last day of last month) + * + * @return string + */ + public function getDefaultEndDate() + { + return date('Y-m-t', strtotime('last day of last month')); + } +} diff --git a/Controller/Adminhtml/Report/Export.php b/Controller/Adminhtml/Report/Export.php new file mode 100644 index 0000000..e88d21c --- /dev/null +++ b/Controller/Adminhtml/Report/Export.php @@ -0,0 +1,91 @@ +fileFactory = $fileFactory; + $this->reportGenerator = $reportGenerator; + $this->configHelper = $configHelper; + } + + /** + * Export action + * + * @return \Magento\Framework\App\ResponseInterface + */ + public function execute() + { + try { + $startDate = $this->getRequest()->getParam('start_date'); + $endDate = $this->getRequest()->getParam('end_date'); + + if (!$startDate || !$endDate) { + $this->messageManager->addErrorMessage(__('Please select a date range.')); + return $this->_redirect('*/*/index'); + } + + // Get order statuses + $orderStatuses = $this->configHelper->getOrderStatus(); + + // Generate report data + $data = $this->reportGenerator->generateReportData($startDate, $endDate, $orderStatuses); + + if (empty($data)) { + $this->messageManager->addWarningMessage(__('No data found for the selected date range.')); + return $this->_redirect('*/*/index'); + } + + // Generate CSV + $csvContent = $this->reportGenerator->generateCsv($data); + $filename = $this->reportGenerator->getFilename(); + + return $this->fileFactory->create( + $filename, + $csvContent, + DirectoryList::VAR_DIR, + 'text/csv' + ); + + } catch (\Exception $e) { + $this->messageManager->addErrorMessage(__('An error occurred while generating the report: %1', $e->getMessage())); + return $this->_redirect('*/*/index'); + } + } +} diff --git a/Controller/Adminhtml/Report/Grid.php b/Controller/Adminhtml/Report/Grid.php new file mode 100644 index 0000000..45d6654 --- /dev/null +++ b/Controller/Adminhtml/Report/Grid.php @@ -0,0 +1,86 @@ +resultJsonFactory = $resultJsonFactory; + $this->reportGenerator = $reportGenerator; + $this->configHelper = $configHelper; + } + + /** + * Grid data action + * + * @return \Magento\Framework\Controller\Result\Json + */ + public function execute() + { + $result = $this->resultJsonFactory->create(); + + try { + $startDate = $this->getRequest()->getParam('start_date'); + $endDate = $this->getRequest()->getParam('end_date'); + + if (!$startDate || !$endDate) { + return $result->setData([ + 'success' => false, + 'message' => 'Please select a date range.' + ]); + } + + // Get order statuses + $orderStatuses = $this->configHelper->getOrderStatus(); + + // Generate report data + $data = $this->reportGenerator->generateReportData($startDate, $endDate, $orderStatuses); + + return $result->setData([ + 'success' => true, + 'data' => $data, + 'total' => count($data) + ]); + + } catch (\Exception $e) { + return $result->setData([ + 'success' => false, + 'message' => $e->getMessage() + ]); + } + } +} diff --git a/Controller/Adminhtml/Report/Index.php b/Controller/Adminhtml/Report/Index.php new file mode 100644 index 0000000..95dfcaa --- /dev/null +++ b/Controller/Adminhtml/Report/Index.php @@ -0,0 +1,43 @@ +resultPageFactory = $resultPageFactory; + } + + /** + * Index action + * + * @return \Magento\Backend\Model\View\Result\Page + */ + public function execute() + { + /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ + $resultPage = $this->resultPageFactory->create(); + $resultPage->setActiveMenu('Shopkeeper_VendorSalesReport::report'); + $resultPage->getConfig()->getTitle()->prepend(__('Vendor Sales Report')); + + return $resultPage; + } +} diff --git a/Cron/GenerateMonthlyReport.php b/Cron/GenerateMonthlyReport.php new file mode 100644 index 0000000..e2e3576 --- /dev/null +++ b/Cron/GenerateMonthlyReport.php @@ -0,0 +1,99 @@ +configHelper = $configHelper; + $this->reportGenerator = $reportGenerator; + $this->emailSender = $emailSender; + $this->logger = $logger; + } + + /** + * Execute cron job + * + * @return void + */ + public function execute() + { + if (!$this->configHelper->isEnabled()) { + $this->logger->info('Vendor Sales Report: Automated report is disabled'); + return; + } + + try { + // Get last month's date range + $startDate = date('Y-m-01', strtotime('first day of last month')); + $endDate = date('Y-m-t', strtotime('last day of last month')); + + $month = date('F', strtotime('first day of last month')); + $year = date('Y', strtotime('first day of last month')); + + $this->logger->info("Vendor Sales Report: Generating report for {$month} {$year}"); + + // Get order statuses + $orderStatuses = $this->configHelper->getOrderStatus(); + + // Generate report data + $data = $this->reportGenerator->generateReportData($startDate, $endDate, $orderStatuses); + + if (empty($data)) { + $this->logger->info("Vendor Sales Report: No data found for {$month} {$year}"); + return; + } + + // Generate CSV + $csvContent = $this->reportGenerator->generateCsv($data); + $filename = $this->reportGenerator->getFilename(); + + // Send email + $emailSent = $this->emailSender->sendReport($csvContent, $filename, $month, $year); + + if ($emailSent) { + $this->logger->info("Vendor Sales Report: Successfully sent report for {$month} {$year}"); + } else { + $this->logger->warning("Vendor Sales Report: Failed to send email for {$month} {$year}"); + } + + } catch (\Exception $e) { + $this->logger->error('Vendor Sales Report Cron Error: ' . $e->getMessage()); + } + } +} diff --git a/Helper/Data.php b/Helper/Data.php new file mode 100644 index 0000000..5cc4b9a --- /dev/null +++ b/Helper/Data.php @@ -0,0 +1,114 @@ +scopeConfig->isSetFlag( + self::XML_PATH_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Get order status filter + * + * @return array + */ + public function getOrderStatus() + { + $value = $this->scopeConfig->getValue( + self::XML_PATH_ORDER_STATUS, + ScopeInterface::SCOPE_STORE + ); + + if (is_string($value)) { + return explode(',', $value); + } + + return is_array($value) ? $value : ['complete']; + } + + /** + * Check if email is enabled + * + * @return bool + */ + public function isEmailEnabled() + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_EMAIL_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Get email recipients + * + * @return array + */ + public function getEmailRecipients() + { + $recipients = $this->scopeConfig->getValue( + self::XML_PATH_EMAIL_RECIPIENTS, + ScopeInterface::SCOPE_STORE + ); + + if (empty($recipients)) { + return []; + } + + return array_map('trim', explode(',', $recipients)); + } + + /** + * Get email subject + * + * @param string $month + * @param string $year + * @return string + */ + public function getEmailSubject($month = null, $year = null) + { + $subject = $this->scopeConfig->getValue( + self::XML_PATH_EMAIL_SUBJECT, + ScopeInterface::SCOPE_STORE + ); + + if ($month && $year) { + $subject = str_replace(['{{month}}', '{{year}}'], [$month, $year], $subject); + } + + return $subject ?: 'Monthly Thermo Report'; + } + + /** + * Get email sender + * + * @return string + */ + public function getEmailSender() + { + return $this->scopeConfig->getValue( + self::XML_PATH_EMAIL_SENDER, + ScopeInterface::SCOPE_STORE + ) ?: 'general'; + } +} diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..45e4f8f --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,97 @@ +# Quick Installation Guide + +## Step 1: Upload Module Files + +From your Magento root directory: + +```bash +# Create directory structure +mkdir -p app/code/Shopkeeper/VendorSalesReport + +# Upload all module files to this directory +# (You can use FTP, SCP, or extract from ZIP) +``` + +## Step 2: Enable Module + +```bash +php bin/magento module:enable Shopkeeper_VendorSalesReport +php bin/magento setup:upgrade +php bin/magento setup:di:compile +php bin/magento setup:static-content:deploy -f +php bin/magento cache:flush +``` + +## Step 3: Configure Module + +1. Log into Magento Admin +2. Go to **Stores > Configuration > Shopkeeper > Vendor Sales Report** +3. Configure settings: + +### Required Settings: +- **Enable Automated Monthly Report**: Yes +- **Order Status Filter**: complete (or your preferred status) +- **Enable Email**: Yes +- **Email Recipients**: `vendor@example.com` (replace with actual email) +- **Email Subject**: `Monthly Thermo Report - {{month}} {{year}}` +- **Email Sender**: General Contact + +4. Click **Save Config** +5. Run `php bin/magento cache:flush` + +## Step 4: Test Manual Report + +1. Go to **Reports > Sales > Vendor Sales Report** +2. Select a date range (defaults to last month) +3. Click **Preview Report** to see data +4. Click **Export CSV** to download + +## Step 5: Verify Cron Setup + +Test the automated report: + +```bash +php bin/magento cron:run --group default +``` + +Check logs: +```bash +tail -f var/log/system.log | grep "Vendor Sales Report" +``` + +## Common Issues + +### Module not appearing in admin menu +```bash +php bin/magento cache:flush +# Log out and log back into admin +``` + +### CSV downloads as blank +- Check that orders exist with the configured status in the date range +- Verify order status configuration + +### Emails not sending +- Test Magento email with: System > Tools > Email Templates +- Check SMTP settings +- Verify email recipients are configured + +## File Permissions (if needed) + +```bash +chmod -R 755 app/code/Shopkeeper/ +chown -R www-data:www-data app/code/Shopkeeper/ # adjust user as needed +``` + +## Uninstallation (if needed) + +```bash +php bin/magento module:disable Shopkeeper_VendorSalesReport +php bin/magento setup:upgrade +rm -rf app/code/Shopkeeper/VendorSalesReport +php bin/magento cache:flush +``` + +## Support + +Check README.md for detailed documentation and troubleshooting. diff --git a/Model/EmailSender.php b/Model/EmailSender.php new file mode 100644 index 0000000..2d43033 --- /dev/null +++ b/Model/EmailSender.php @@ -0,0 +1,116 @@ +transportBuilder = $transportBuilder; + $this->inlineTranslation = $inlineTranslation; + $this->storeManager = $storeManager; + $this->configHelper = $configHelper; + $this->logger = $logger; + } + + /** + * Send report email + * + * @param string $csvContent + * @param string $filename + * @param string $month + * @param string $year + * @return bool + */ + public function sendReport($csvContent, $filename, $month = null, $year = null) + { + if (!$this->configHelper->isEmailEnabled()) { + return false; + } + + $recipients = $this->configHelper->getEmailRecipients(); + if (empty($recipients)) { + $this->logger->warning('Vendor Sales Report: No email recipients configured'); + return false; + } + + try { + $this->inlineTranslation->suspend(); + + $sender = [ + 'name' => $this->storeManager->getStore()->getName(), + 'email' => $this->storeManager->getStore()->getConfig('trans_email/' . $this->configHelper->getEmailSender() . '/email') + ]; + + $subject = $this->configHelper->getEmailSubject($month, $year); + + $transport = $this->transportBuilder + ->setTemplateIdentifier('vendorsalesreport_email_template') + ->setTemplateOptions([ + 'area' => \Magento\Framework\App\Area::AREA_ADMINHTML, + 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, + ]) + ->setTemplateVars([ + 'subject' => $subject, + 'month' => $month, + 'year' => $year + ]) + ->setFromByScope($sender) + ->addTo($recipients) + ->addAttachment($csvContent, $filename, 'text/csv') + ->getTransport(); + + $transport->sendMessage(); + + $this->inlineTranslation->resume(); + + return true; + } catch (\Exception $e) { + $this->logger->error('Vendor Sales Report Email Error: ' . $e->getMessage()); + $this->inlineTranslation->resume(); + return false; + } + } +} diff --git a/Model/ReportGenerator.php b/Model/ReportGenerator.php new file mode 100644 index 0000000..866bd18 --- /dev/null +++ b/Model/ReportGenerator.php @@ -0,0 +1,201 @@ + 'AL', 'Alaska' => 'AK', 'Arizona' => 'AZ', 'Arkansas' => 'AR', + 'California' => 'CA', 'Colorado' => 'CO', 'Connecticut' => 'CT', 'Delaware' => 'DE', + 'Florida' => 'FL', 'Georgia' => 'GA', 'Hawaii' => 'HI', 'Idaho' => 'ID', + 'Illinois' => 'IL', 'Indiana' => 'IN', 'Iowa' => 'IA', 'Kansas' => 'KS', + 'Kentucky' => 'KY', 'Louisiana' => 'LA', 'Maine' => 'ME', 'Maryland' => 'MD', + 'Massachusetts' => 'MA', 'Michigan' => 'MI', 'Minnesota' => 'MN', 'Mississippi' => 'MS', + 'Missouri' => 'MO', 'Montana' => 'MT', 'Nebraska' => 'NE', 'Nevada' => 'NV', + 'New Hampshire' => 'NH', 'New Jersey' => 'NJ', 'New Mexico' => 'NM', 'New York' => 'NY', + 'North Carolina' => 'NC', 'North Dakota' => 'ND', 'Ohio' => 'OH', 'Oklahoma' => 'OK', + 'Oregon' => 'OR', 'Pennsylvania' => 'PA', 'Rhode Island' => 'RI', 'South Carolina' => 'SC', + 'South Dakota' => 'SD', 'Tennessee' => 'TN', 'Texas' => 'TX', 'Utah' => 'UT', + 'Vermont' => 'VT', 'Virginia' => 'VA', 'Washington' => 'WA', 'West Virginia' => 'WV', + 'Wisconsin' => 'WI', 'Wyoming' => 'WY', 'District of Columbia' => 'DC', + 'Puerto Rico' => 'PR', 'Guam' => 'GU', 'Virgin Islands' => 'VI' + ]; + + /** + * @param ResourceConnection $resourceConnection + * @param TimezoneInterface $timezone + */ + public function __construct( + ResourceConnection $resourceConnection, + TimezoneInterface $timezone + ) { + $this->resourceConnection = $resourceConnection; + $this->timezone = $timezone; + } + + /** + * Generate report data + * + * @param string $startDate + * @param string $endDate + * @param array $orderStatuses + * @return array + */ + public function generateReportData($startDate, $endDate, $orderStatuses = ['complete']) + { + $connection = $this->resourceConnection->getConnection(); + + $salesOrderItemTable = $this->resourceConnection->getTableName('sales_order_item'); + $salesOrderTable = $this->resourceConnection->getTableName('sales_order'); + $salesOrderAddressTable = $this->resourceConnection->getTableName('sales_order_address'); + $catalogProductEntityTable = $this->resourceConnection->getTableName('catalog_product_entity'); + $catalogProductEntityDecimalTable = $this->resourceConnection->getTableName('catalog_product_entity_decimal'); + $eavAttributeTable = $this->resourceConnection->getTableName('eav_attribute'); + + // Build status filter + $statusPlaceholders = implode(',', array_fill(0, count($orderStatuses), '?')); + + $sql = " + SELECT + DATE_FORMAT(o.created_at, '%m/%d/%Y') AS 'Date', + oi.sku AS 'Vendor Product number', + oi.name AS 'Description', + '' AS 'Vendor code', + '' AS 'Vendor name', + o.increment_id AS 'Dealer product number', + CASE + WHEN sa.country_id = 'US' THEN 'United States' + ELSE sa.country_id + END AS 'Country', + sa.city AS 'City', + sa.region AS 'State', + sa.postcode AS 'Zip code', + CAST(oi.qty_ordered AS UNSIGNED) AS 'Quantity', + COALESCE( + NULLIF(oi.base_cost, 0), + current_cost.value, + 0 + ) AS 'Cost', + o.status AS 'Order Status' + FROM + {$salesOrderItemTable} oi + INNER JOIN + {$salesOrderTable} o ON oi.order_id = o.entity_id + INNER JOIN + {$salesOrderAddressTable} sa ON o.entity_id = sa.parent_id + AND sa.address_type = 'shipping' + LEFT JOIN + {$catalogProductEntityTable} cpe ON oi.sku = cpe.sku + LEFT JOIN + {$eavAttributeTable} ea ON ea.attribute_code = 'cost' + AND ea.entity_type_id = 4 + LEFT JOIN + {$catalogProductEntityDecimalTable} current_cost ON current_cost.entity_id = cpe.entity_id + AND current_cost.attribute_id = ea.attribute_id + AND current_cost.store_id = 0 + WHERE + o.created_at BETWEEN ? AND ? + AND o.status IN ({$statusPlaceholders}) + AND oi.product_type != 'configurable' + ORDER BY + o.created_at DESC, o.entity_id, oi.item_id + "; + + $bind = array_merge( + [$startDate . ' 00:00:00', $endDate . ' 23:59:59'], + $orderStatuses + ); + + $results = $connection->fetchAll($sql, $bind); + + // Convert state names to abbreviations + foreach ($results as &$row) { + $row['State'] = $this->abbreviateState($row['State']); + } + + return $results; + } + + /** + * Abbreviate state name + * + * @param string $stateName + * @return string + */ + protected function abbreviateState($stateName) + { + // If already abbreviated (2 characters), return as-is + if (strlen($stateName) == 2) { + return $stateName; + } + + // Look up abbreviation + if (isset($this->stateAbbreviations[$stateName])) { + return $this->stateAbbreviations[$stateName]; + } + + // Return original if not found + return $stateName; + } + + /** + * Generate CSV content + * + * @param array $data + * @return string + */ + public function generateCsv($data) + { + if (empty($data)) { + return ''; + } + + $output = fopen('php://temp', 'r+'); + + // Headers - exclude Order Status + $headers = array_keys($data[0]); + $headers = array_filter($headers, function($header) { + return $header !== 'Order Status'; + }); + fputcsv($output, $headers); + + // Data rows - exclude Order Status + foreach ($data as $row) { + unset($row['Order Status']); + fputcsv($output, array_values($row)); + } + + rewind($output); + $csv = stream_get_contents($output); + fclose($output); + + return $csv; + } + + /** + * Get filename + * + * @return string + */ + public function getFilename() + { + return 'FF_SAC_730_MCQUEEN.csv'; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..dec808c --- /dev/null +++ b/README.md @@ -0,0 +1,254 @@ +# Shopkeeper Vendor Sales Report + +A Magento 2.4.x module that generates monthly vendor sales reports with automated email delivery. + +## Features + +- **Manual Report Generation**: Generate reports for any custom date range via admin interface +- **Preview Grid**: View report data before exporting +- **Automated Monthly Reports**: Automatic generation on the 1st of each month via cron +- **Email Delivery**: Automatically email reports with CSV attachment +- **Configurable**: + - Order status filtering + - Email recipients + - Email subject line + - Email sender +- **State Abbreviation**: Automatically converts full state names to 2-letter codes +- **Cost Fallback**: Uses current product cost if order item cost is missing + +## Installation + +### Method 1: Manual Installation + +1. **Upload the module files:** + ```bash + # From your Magento root directory + mkdir -p app/code/Shopkeeper/VendorSalesReport + # Upload all module files to app/code/Shopkeeper/VendorSalesReport/ + ``` + +2. **Enable the module:** + ```bash + php bin/magento module:enable Shopkeeper_VendorSalesReport + php bin/magento setup:upgrade + php bin/magento setup:di:compile + php bin/magento setup:static-content:deploy -f + php bin/magento cache:flush + ``` + +3. **Set permissions (if needed):** + ```bash + chmod -R 755 app/code/Shopkeeper/ + ``` + +### Method 2: Composer Installation (if you package it) + +```bash +composer require shopkeeper/module-vendor-sales-report +php bin/magento module:enable Shopkeeper_VendorSalesReport +php bin/magento setup:upgrade +php bin/magento setup:di:compile +php bin/magento setup:static-content:deploy -f +php bin/magento cache:flush +``` + +## Configuration + +### Admin Configuration Path +**Stores > Configuration > Shopkeeper > Vendor Sales Report** + +### Settings + +#### General Settings +- **Enable Automated Monthly Report**: Enable/disable automatic monthly report generation +- **Order Status Filter**: Select which order statuses to include (default: Complete) + +#### Email Settings +- **Enable Email**: Enable/disable email delivery +- **Email Recipients**: Comma-separated list of email addresses +- **Email Subject**: Subject line for automated emails (supports {{month}} and {{year}} placeholders) +- **Email Sender**: Choose which store email identity to use as sender + +### Example Configuration + +``` +Enable Automated Monthly Report: Yes +Order Status Filter: complete +Enable Email: Yes +Email Recipients: vendor@example.com, admin@example.com +Email Subject: Monthly Thermo Report - {{month}} {{year}} +Email Sender: General Contact +``` + +## Usage + +### Manual Report Generation + +1. Go to **Reports > Sales > Vendor Sales Report** +2. Select date range using the date pickers +3. Click **Preview Report** to view data in a grid +4. Click **Export CSV** to download the report + +The report will be downloaded as `FF_SAC_730_MCQUEEN.csv` + +### Automated Monthly Reports + +Once configured, the module will automatically: +1. Generate a report on the 1st of each month at 2:00 AM +2. Include all orders from the previous month +3. Email the report as a CSV attachment to configured recipients + +### Report Format + +The CSV file includes the following columns: + +| Column | Description | +|--------|-------------| +| Date | Order date (MM/DD/YYYY format) | +| Vendor Product number | Product SKU | +| Description | Product name | +| Vendor code | (Empty - reserved for vendor use) | +| Vendor name | (Empty - reserved for vendor use) | +| Dealer product number | Order increment ID | +| Country | Shipping address country | +| City | Shipping address city | +| State | Shipping address state (abbreviated) | +| Zip code | Shipping address postal code | +| Quantity | Quantity ordered | +| Cost | Product cost (uses order item cost, falls back to current product cost if missing) | + +## Cron Schedule + +The automated report runs according to this schedule: +``` +0 2 1 * * +``` +Translation: At 2:00 AM on the 1st day of every month + +To test the cron manually: +```bash +php bin/magento cron:run --group default +``` + +## Troubleshooting + +### Reports not generating automatically + +1. **Check if cron is enabled:** + ```bash + php bin/magento cron:run + ``` + +2. **Check cron_schedule table:** + ```sql + SELECT * FROM cron_schedule + WHERE job_code = 'shopkeeper_vendorsalesreport_monthly' + ORDER BY scheduled_at DESC + LIMIT 10; + ``` + +3. **Check module configuration:** + - Ensure "Enable Automated Monthly Report" is set to Yes + - Verify order status is configured + +### Emails not sending + +1. **Check email configuration:** + - Verify email recipients are configured + - Ensure "Enable Email" is set to Yes + - Check Magento's email sending is working (test with other emails) + +2. **Check logs:** + ```bash + tail -f var/log/system.log | grep "Vendor Sales Report" + ``` + +### No data in report + +1. **Check order status filter:** + - Verify orders have the configured status (default: "complete") + - Try including additional statuses like "processing" + +2. **Verify date range:** + - Ensure orders exist within the selected date range + +### State abbreviations not working + +- The module includes all US states, DC, and territories +- If international addresses appear, they'll show as-is (not abbreviated) +- Already abbreviated states (2 characters) are left unchanged + +## Module Structure + +``` +Shopkeeper/VendorSalesReport/ +├── Block/ +│ └── Adminhtml/ +│ └── Report.php +├── Controller/ +│ └── Adminhtml/ +│ └── Report/ +│ ├── Export.php +│ ├── Grid.php +│ └── Index.php +├── Cron/ +│ └── GenerateMonthlyReport.php +├── etc/ +│ ├── acl.xml +│ ├── config.xml +│ ├── crontab.xml +│ ├── email_templates.xml +│ ├── module.xml +│ └── adminhtml/ +│ ├── menu.xml +│ ├── routes.xml +│ └── system.xml +├── Helper/ +│ └── Data.php +├── Model/ +│ ├── EmailSender.php +│ └── ReportGenerator.php +├── view/ +│ └── adminhtml/ +│ ├── email/ +│ │ └── report.html +│ ├── layout/ +│ │ └── vendorsalesreport_report_index.xml +│ └── templates/ +│ └── report/ +│ └── view.phtml +└── registration.php +``` + +## Requirements + +- Magento 2.4.x +- PHP 7.4 or higher +- MySQL 5.7 or higher + +## Support + +For issues or questions: +1. Check the troubleshooting section above +2. Review Magento logs: `var/log/system.log` +3. Verify module is enabled: `php bin/magento module:status` + +## License + +Proprietary - Shopkeeper + +## Version + +1.0.0 + +## Changelog + +### Version 1.0.0 +- Initial release +- Manual report generation with date range selector +- Preview grid functionality +- Automated monthly cron job +- Email delivery with CSV attachment +- Configurable order status filter +- State abbreviation support +- Cost fallback to current product cost diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..95811fe --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "shopkeeper/module-vendor-sales-report", + "description": "Automated vendor sales report generator with email delivery for Magento 2", + "type": "magento2-module", + "version": "1.0.0", + "license": "proprietary", + "authors": [ + { + "name": "Shopkeeper", + "email": "taber@shopkeeper.dev" + } + ], + "require": { + "php": "^7.4|^8.0|^8.1|^8.2|^8.3", + "magento/framework": "^103.0|^104.0", + "magento/module-backend": "^102.0|^103.0", + "magento/module-sales": "^103.0|^104.0", + "magento/module-cron": "^100.4|^101.0" + }, + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Shopkeeper\\VendorSalesReport\\": "" + } + } +} diff --git a/etc/acl.xml b/etc/acl.xml new file mode 100644 index 0000000..eb7d910 --- /dev/null +++ b/etc/acl.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/etc/adminhtml/menu.xml b/etc/adminhtml/menu.xml new file mode 100644 index 0000000..0701251 --- /dev/null +++ b/etc/adminhtml/menu.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/etc/adminhtml/routes.xml b/etc/adminhtml/routes.xml new file mode 100644 index 0000000..01b469f --- /dev/null +++ b/etc/adminhtml/routes.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml new file mode 100644 index 0000000..06ec480 --- /dev/null +++ b/etc/adminhtml/system.xml @@ -0,0 +1,55 @@ + + + + + + +
+ + shopkeeper + Shopkeeper_VendorSalesReport::config + + + + + Magento\Config\Model\Config\Source\Yesno + When enabled, report will be generated automatically on the 1st of each month + + + + Magento\Sales\Model\Config\Source\Order\Status + Select which order statuses to include in the report + + + + + + + Magento\Config\Model\Config\Source\Yesno + Send automated report via email + + + + Comma-separated email addresses (e.g., vendor@example.com, admin@example.com) + + 1 + + + + + Subject line for automated report emails. Use {{month}} and {{year}} for dynamic values. + + 1 + + + + + Magento\Config\Model\Config\Source\Email\Identity + + 1 + + + +
+
+
diff --git a/etc/config.xml b/etc/config.xml new file mode 100644 index 0000000..b25fa3d --- /dev/null +++ b/etc/config.xml @@ -0,0 +1,16 @@ + + + + + + 1 + complete + + + 1 + Monthly Thermo Report - {{month}} {{year}} + general + + + + diff --git a/etc/crontab.xml b/etc/crontab.xml new file mode 100644 index 0000000..89912fb --- /dev/null +++ b/etc/crontab.xml @@ -0,0 +1,8 @@ + + + + + 0 2 1 * * + + + diff --git a/etc/email_templates.xml b/etc/email_templates.xml new file mode 100644 index 0000000..c3530bc --- /dev/null +++ b/etc/email_templates.xml @@ -0,0 +1,4 @@ + + +