Initial commit: Vendor Sales Report Magento 2 module
This commit is contained in:
141
.github/copilot-instructions.md
vendored
Normal file
141
.github/copilot-instructions.md
vendored
Normal file
@@ -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
|
||||||
|
```</content>
|
||||||
|
<parameter name="filePath">t:\Files\Documents\BIZ\Clients\McQueen\Repos\VendorSalesReport\.github\copilot-instructions.md
|
||||||
78
Block/Adminhtml/Report.php
Normal file
78
Block/Adminhtml/Report.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shopkeeper\VendorSalesReport\Block\Adminhtml;
|
||||||
|
|
||||||
|
use Magento\Backend\Block\Template;
|
||||||
|
use Magento\Backend\Block\Template\Context;
|
||||||
|
use Shopkeeper\VendorSalesReport\Helper\Data as ConfigHelper;
|
||||||
|
|
||||||
|
class Report extends Template
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ConfigHelper
|
||||||
|
*/
|
||||||
|
protected $configHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Context $context
|
||||||
|
* @param ConfigHelper $configHelper
|
||||||
|
* @param array $data
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
Context $context,
|
||||||
|
ConfigHelper $configHelper,
|
||||||
|
array $data = []
|
||||||
|
) {
|
||||||
|
$this->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
91
Controller/Adminhtml/Report/Export.php
Normal file
91
Controller/Adminhtml/Report/Export.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shopkeeper\VendorSalesReport\Controller\Adminhtml\Report;
|
||||||
|
|
||||||
|
use Magento\Backend\App\Action;
|
||||||
|
use Magento\Backend\App\Action\Context;
|
||||||
|
use Magento\Framework\App\Response\Http\FileFactory;
|
||||||
|
use Magento\Framework\App\Filesystem\DirectoryList;
|
||||||
|
use Shopkeeper\VendorSalesReport\Model\ReportGenerator;
|
||||||
|
use Shopkeeper\VendorSalesReport\Helper\Data as ConfigHelper;
|
||||||
|
|
||||||
|
class Export extends Action
|
||||||
|
{
|
||||||
|
const ADMIN_RESOURCE = 'Shopkeeper_VendorSalesReport::report';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var FileFactory
|
||||||
|
*/
|
||||||
|
protected $fileFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ReportGenerator
|
||||||
|
*/
|
||||||
|
protected $reportGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ConfigHelper
|
||||||
|
*/
|
||||||
|
protected $configHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Context $context
|
||||||
|
* @param FileFactory $fileFactory
|
||||||
|
* @param ReportGenerator $reportGenerator
|
||||||
|
* @param ConfigHelper $configHelper
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
Context $context,
|
||||||
|
FileFactory $fileFactory,
|
||||||
|
ReportGenerator $reportGenerator,
|
||||||
|
ConfigHelper $configHelper
|
||||||
|
) {
|
||||||
|
parent::__construct($context);
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
Controller/Adminhtml/Report/Grid.php
Normal file
86
Controller/Adminhtml/Report/Grid.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shopkeeper\VendorSalesReport\Controller\Adminhtml\Report;
|
||||||
|
|
||||||
|
use Magento\Backend\App\Action;
|
||||||
|
use Magento\Backend\App\Action\Context;
|
||||||
|
use Magento\Framework\Controller\Result\JsonFactory;
|
||||||
|
use Shopkeeper\VendorSalesReport\Model\ReportGenerator;
|
||||||
|
use Shopkeeper\VendorSalesReport\Helper\Data as ConfigHelper;
|
||||||
|
|
||||||
|
class Grid extends Action
|
||||||
|
{
|
||||||
|
const ADMIN_RESOURCE = 'Shopkeeper_VendorSalesReport::report';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var JsonFactory
|
||||||
|
*/
|
||||||
|
protected $resultJsonFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ReportGenerator
|
||||||
|
*/
|
||||||
|
protected $reportGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ConfigHelper
|
||||||
|
*/
|
||||||
|
protected $configHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Context $context
|
||||||
|
* @param JsonFactory $resultJsonFactory
|
||||||
|
* @param ReportGenerator $reportGenerator
|
||||||
|
* @param ConfigHelper $configHelper
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
Context $context,
|
||||||
|
JsonFactory $resultJsonFactory,
|
||||||
|
ReportGenerator $reportGenerator,
|
||||||
|
ConfigHelper $configHelper
|
||||||
|
) {
|
||||||
|
parent::__construct($context);
|
||||||
|
$this->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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Controller/Adminhtml/Report/Index.php
Normal file
43
Controller/Adminhtml/Report/Index.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shopkeeper\VendorSalesReport\Controller\Adminhtml\Report;
|
||||||
|
|
||||||
|
use Magento\Backend\App\Action;
|
||||||
|
use Magento\Backend\App\Action\Context;
|
||||||
|
use Magento\Framework\View\Result\PageFactory;
|
||||||
|
|
||||||
|
class Index extends Action
|
||||||
|
{
|
||||||
|
const ADMIN_RESOURCE = 'Shopkeeper_VendorSalesReport::report';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var PageFactory
|
||||||
|
*/
|
||||||
|
protected $resultPageFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Context $context
|
||||||
|
* @param PageFactory $resultPageFactory
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
Context $context,
|
||||||
|
PageFactory $resultPageFactory
|
||||||
|
) {
|
||||||
|
parent::__construct($context);
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
Cron/GenerateMonthlyReport.php
Normal file
99
Cron/GenerateMonthlyReport.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shopkeeper\VendorSalesReport\Cron;
|
||||||
|
|
||||||
|
use Shopkeeper\VendorSalesReport\Helper\Data as ConfigHelper;
|
||||||
|
use Shopkeeper\VendorSalesReport\Model\ReportGenerator;
|
||||||
|
use Shopkeeper\VendorSalesReport\Model\EmailSender;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class GenerateMonthlyReport
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ConfigHelper
|
||||||
|
*/
|
||||||
|
protected $configHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ReportGenerator
|
||||||
|
*/
|
||||||
|
protected $reportGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var EmailSender
|
||||||
|
*/
|
||||||
|
protected $emailSender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var LoggerInterface
|
||||||
|
*/
|
||||||
|
protected $logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConfigHelper $configHelper
|
||||||
|
* @param ReportGenerator $reportGenerator
|
||||||
|
* @param EmailSender $emailSender
|
||||||
|
* @param LoggerInterface $logger
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
ConfigHelper $configHelper,
|
||||||
|
ReportGenerator $reportGenerator,
|
||||||
|
EmailSender $emailSender,
|
||||||
|
LoggerInterface $logger
|
||||||
|
) {
|
||||||
|
$this->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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
Helper/Data.php
Normal file
114
Helper/Data.php
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shopkeeper\VendorSalesReport\Helper;
|
||||||
|
|
||||||
|
use Magento\Framework\App\Helper\AbstractHelper;
|
||||||
|
use Magento\Framework\App\Helper\Context;
|
||||||
|
use Magento\Store\Model\ScopeInterface;
|
||||||
|
|
||||||
|
class Data extends AbstractHelper
|
||||||
|
{
|
||||||
|
const XML_PATH_ENABLED = 'vendorsalesreport/general/enabled';
|
||||||
|
const XML_PATH_ORDER_STATUS = 'vendorsalesreport/general/order_status';
|
||||||
|
const XML_PATH_EMAIL_ENABLED = 'vendorsalesreport/email/enabled';
|
||||||
|
const XML_PATH_EMAIL_RECIPIENTS = 'vendorsalesreport/email/recipients';
|
||||||
|
const XML_PATH_EMAIL_SUBJECT = 'vendorsalesreport/email/subject';
|
||||||
|
const XML_PATH_EMAIL_SENDER = 'vendorsalesreport/email/sender';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if automated report is enabled
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isEnabled()
|
||||||
|
{
|
||||||
|
return $this->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';
|
||||||
|
}
|
||||||
|
}
|
||||||
97
INSTALL.md
Normal file
97
INSTALL.md
Normal file
@@ -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.
|
||||||
116
Model/EmailSender.php
Normal file
116
Model/EmailSender.php
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shopkeeper\VendorSalesReport\Model;
|
||||||
|
|
||||||
|
use Magento\Framework\Mail\Template\TransportBuilder;
|
||||||
|
use Magento\Framework\Translate\Inline\StateInterface;
|
||||||
|
use Magento\Store\Model\StoreManagerInterface;
|
||||||
|
use Shopkeeper\VendorSalesReport\Helper\Data as ConfigHelper;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class EmailSender
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var TransportBuilder
|
||||||
|
*/
|
||||||
|
protected $transportBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var StateInterface
|
||||||
|
*/
|
||||||
|
protected $inlineTranslation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var StoreManagerInterface
|
||||||
|
*/
|
||||||
|
protected $storeManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ConfigHelper
|
||||||
|
*/
|
||||||
|
protected $configHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var LoggerInterface
|
||||||
|
*/
|
||||||
|
protected $logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TransportBuilder $transportBuilder
|
||||||
|
* @param StateInterface $inlineTranslation
|
||||||
|
* @param StoreManagerInterface $storeManager
|
||||||
|
* @param ConfigHelper $configHelper
|
||||||
|
* @param LoggerInterface $logger
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
TransportBuilder $transportBuilder,
|
||||||
|
StateInterface $inlineTranslation,
|
||||||
|
StoreManagerInterface $storeManager,
|
||||||
|
ConfigHelper $configHelper,
|
||||||
|
LoggerInterface $logger
|
||||||
|
) {
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
201
Model/ReportGenerator.php
Normal file
201
Model/ReportGenerator.php
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shopkeeper\VendorSalesReport\Model;
|
||||||
|
|
||||||
|
use Magento\Framework\App\ResourceConnection;
|
||||||
|
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
|
||||||
|
|
||||||
|
class ReportGenerator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ResourceConnection
|
||||||
|
*/
|
||||||
|
protected $resourceConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TimezoneInterface
|
||||||
|
*/
|
||||||
|
protected $timezone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State abbreviation mapping
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $stateAbbreviations = [
|
||||||
|
'Alabama' => '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';
|
||||||
|
}
|
||||||
|
}
|
||||||
254
README.md
Normal file
254
README.md
Normal file
@@ -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
|
||||||
28
composer.json
Normal file
28
composer.json
Normal file
@@ -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\\": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
etc/acl.xml
Normal file
21
etc/acl.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
|
||||||
|
<acl>
|
||||||
|
<resources>
|
||||||
|
<resource id="Magento_Backend::admin">
|
||||||
|
<resource id="Magento_Reports::report">
|
||||||
|
<resource id="Magento_Reports::report_salesroot">
|
||||||
|
<resource id="Shopkeeper_VendorSalesReport::report" title="Vendor Sales Report" sortOrder="999"/>
|
||||||
|
</resource>
|
||||||
|
</resource>
|
||||||
|
<resource id="Magento_Backend::stores">
|
||||||
|
<resource id="Magento_Backend::stores_settings">
|
||||||
|
<resource id="Magento_Config::config">
|
||||||
|
<resource id="Shopkeeper_VendorSalesReport::config" title="Vendor Sales Report"/>
|
||||||
|
</resource>
|
||||||
|
</resource>
|
||||||
|
</resource>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
</acl>
|
||||||
|
</config>
|
||||||
12
etc/adminhtml/menu.xml
Normal file
12
etc/adminhtml/menu.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
|
||||||
|
<menu>
|
||||||
|
<add id="Shopkeeper_VendorSalesReport::report"
|
||||||
|
title="Vendor Sales Report"
|
||||||
|
module="Shopkeeper_VendorSalesReport"
|
||||||
|
sortOrder="999"
|
||||||
|
parent="Magento_Reports::report_salesroot"
|
||||||
|
action="vendorsalesreport/report/index"
|
||||||
|
resource="Shopkeeper_VendorSalesReport::report"/>
|
||||||
|
</menu>
|
||||||
|
</config>
|
||||||
8
etc/adminhtml/routes.xml
Normal file
8
etc/adminhtml/routes.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
|
||||||
|
<router id="admin">
|
||||||
|
<route id="vendorsalesreport" frontName="vendorsalesreport">
|
||||||
|
<module name="Shopkeeper_VendorSalesReport"/>
|
||||||
|
</route>
|
||||||
|
</router>
|
||||||
|
</config>
|
||||||
55
etc/adminhtml/system.xml
Normal file
55
etc/adminhtml/system.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
|
||||||
|
<system>
|
||||||
|
<tab id="shopkeeper" translate="label" sortOrder="300">
|
||||||
|
<label>Shopkeeper</label>
|
||||||
|
</tab>
|
||||||
|
<section id="vendorsalesreport" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="0" showInStore="0">
|
||||||
|
<label>Vendor Sales Report</label>
|
||||||
|
<tab>shopkeeper</tab>
|
||||||
|
<resource>Shopkeeper_VendorSalesReport::config</resource>
|
||||||
|
<group id="general" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
|
||||||
|
<label>General Settings</label>
|
||||||
|
<field id="enabled" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
|
||||||
|
<label>Enable Automated Monthly Report</label>
|
||||||
|
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
|
||||||
|
<comment>When enabled, report will be generated automatically on the 1st of each month</comment>
|
||||||
|
</field>
|
||||||
|
<field id="order_status" translate="label comment" type="multiselect" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0">
|
||||||
|
<label>Order Status Filter</label>
|
||||||
|
<source_model>Magento\Sales\Model\Config\Source\Order\Status</source_model>
|
||||||
|
<comment>Select which order statuses to include in the report</comment>
|
||||||
|
</field>
|
||||||
|
</group>
|
||||||
|
<group id="email" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0">
|
||||||
|
<label>Email Settings</label>
|
||||||
|
<field id="enabled" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
|
||||||
|
<label>Enable Email</label>
|
||||||
|
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
|
||||||
|
<comment>Send automated report via email</comment>
|
||||||
|
</field>
|
||||||
|
<field id="recipients" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0">
|
||||||
|
<label>Email Recipients</label>
|
||||||
|
<comment>Comma-separated email addresses (e.g., vendor@example.com, admin@example.com)</comment>
|
||||||
|
<depends>
|
||||||
|
<field id="enabled">1</field>
|
||||||
|
</depends>
|
||||||
|
</field>
|
||||||
|
<field id="subject" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0">
|
||||||
|
<label>Email Subject</label>
|
||||||
|
<comment>Subject line for automated report emails. Use {{month}} and {{year}} for dynamic values.</comment>
|
||||||
|
<depends>
|
||||||
|
<field id="enabled">1</field>
|
||||||
|
</depends>
|
||||||
|
</field>
|
||||||
|
<field id="sender" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0">
|
||||||
|
<label>Email Sender</label>
|
||||||
|
<source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model>
|
||||||
|
<depends>
|
||||||
|
<field id="enabled">1</field>
|
||||||
|
</depends>
|
||||||
|
</field>
|
||||||
|
</group>
|
||||||
|
</section>
|
||||||
|
</system>
|
||||||
|
</config>
|
||||||
16
etc/config.xml
Normal file
16
etc/config.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
|
||||||
|
<default>
|
||||||
|
<vendorsalesreport>
|
||||||
|
<general>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<order_status>complete</order_status>
|
||||||
|
</general>
|
||||||
|
<email>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<subject>Monthly Thermo Report - {{month}} {{year}}</subject>
|
||||||
|
<sender>general</sender>
|
||||||
|
</email>
|
||||||
|
</vendorsalesreport>
|
||||||
|
</default>
|
||||||
|
</config>
|
||||||
8
etc/crontab.xml
Normal file
8
etc/crontab.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
|
||||||
|
<group id="default">
|
||||||
|
<job name="shopkeeper_vendorsalesreport_monthly" instance="Shopkeeper\VendorSalesReport\Cron\GenerateMonthlyReport" method="execute">
|
||||||
|
<schedule>0 2 1 * *</schedule>
|
||||||
|
</job>
|
||||||
|
</group>
|
||||||
|
</config>
|
||||||
4
etc/email_templates.xml
Normal file
4
etc/email_templates.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd">
|
||||||
|
<template id="vendorsalesreport_email_template" label="Vendor Sales Report Email" file="report.html" type="html" module="Shopkeeper_VendorSalesReport" area="adminhtml"/>
|
||||||
|
</config>
|
||||||
10
etc/module.xml
Normal file
10
etc/module.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
|
||||||
|
<module name="Shopkeeper_VendorSalesReport" setup_version="1.0.0">
|
||||||
|
<sequence>
|
||||||
|
<module name="Magento_Sales"/>
|
||||||
|
<module name="Magento_Backend"/>
|
||||||
|
<module name="Magento_Cron"/>
|
||||||
|
</sequence>
|
||||||
|
</module>
|
||||||
|
</config>
|
||||||
9
registration.php
Normal file
9
registration.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Shopkeeper VendorSalesReport Module Registration
|
||||||
|
*/
|
||||||
|
\Magento\Framework\Component\ComponentRegistrar::register(
|
||||||
|
\Magento\Framework\Component\ComponentRegistrar::MODULE,
|
||||||
|
'Shopkeeper_VendorSalesReport',
|
||||||
|
__DIR__
|
||||||
|
);
|
||||||
22
view/adminhtml/email/report.html
Normal file
22
view/adminhtml/email/report.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!--@subject {{var subject}} @-->
|
||||||
|
<!--@vars
|
||||||
|
{"var subject":"Email Subject"}
|
||||||
|
@-->
|
||||||
|
|
||||||
|
{{template config_path="design/email/header_template"}}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr class="email-intro">
|
||||||
|
<td>
|
||||||
|
<p class="greeting">Hello,</p>
|
||||||
|
<p>
|
||||||
|
Please find attached the monthly vendor sales report for {{var month}} {{var year}}.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This report contains all completed orders for the specified period.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{template config_path="design/email/footer_template"}}
|
||||||
12
view/adminhtml/layout/vendorsalesreport_report_index.xml
Normal file
12
view/adminhtml/layout/vendorsalesreport_report_index.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
|
||||||
|
<head>
|
||||||
|
<css src="Shopkeeper_VendorSalesReport::css/vendor-report.css"/>
|
||||||
|
</head>
|
||||||
|
<update handle="styles"/>
|
||||||
|
<body>
|
||||||
|
<referenceContainer name="content">
|
||||||
|
<block class="Shopkeeper\VendorSalesReport\Block\Adminhtml\Report" name="vendorsalesreport.report" template="Shopkeeper_VendorSalesReport::report/view.phtml"/>
|
||||||
|
</referenceContainer>
|
||||||
|
</body>
|
||||||
|
</page>
|
||||||
299
view/adminhtml/templates/report/view.phtml
Normal file
299
view/adminhtml/templates/report/view.phtml
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var $block \Shopkeeper\VendorSalesReport\Block\Adminhtml\Report
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
/* Fix icon display - more aggressive targeting */
|
||||||
|
.item-vendorsalesreport > a:before,
|
||||||
|
#menu-magento-reports-report .item-vendorsalesreport > a:before,
|
||||||
|
a[href*="vendorsalesreport"]:before {
|
||||||
|
content: '' !important;
|
||||||
|
display: none !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix date range layout - align fields properly */
|
||||||
|
.date-range-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.date-range-wrapper .admin__field {
|
||||||
|
flex: 0 0 250px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.date-range-wrapper .admin__field-label {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.date-range-wrapper .field-date {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Report grid styling */
|
||||||
|
#report_grid {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
#report_grid th,
|
||||||
|
#report_grid td {
|
||||||
|
padding: 10px 8px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
#report_grid th {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
#report_grid tbody tr:nth-child(even) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
#report_grid tbody tr:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.admin__page-section-title-note {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="admin__page-section">
|
||||||
|
<div class="admin__page-section-title">
|
||||||
|
<span class="title"><?= $block->escapeHtml(__('Vendor Sales Report Generator')) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="admin__page-section-content">
|
||||||
|
<div class="admin__fieldset">
|
||||||
|
<div class="admin__field">
|
||||||
|
<div class="admin__field-label">
|
||||||
|
<label><?= $block->escapeHtml(__('Date Range')) ?></label>
|
||||||
|
</div>
|
||||||
|
<div class="admin__field-control">
|
||||||
|
<div class="date-range-wrapper">
|
||||||
|
<div class="admin__field _required field-date">
|
||||||
|
<div class="admin__field-label">
|
||||||
|
<label>
|
||||||
|
<span><?= $block->escapeHtml(__('From')) ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="admin__field-control">
|
||||||
|
<input type="text"
|
||||||
|
id="start_date"
|
||||||
|
name="start_date"
|
||||||
|
class="admin__control-text"
|
||||||
|
value="<?= $block->escapeHtmlAttr($block->getDefaultStartDate()) ?>"
|
||||||
|
data-mage-init='{"calendar": {"dateFormat": "yy-mm-dd"}}'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="admin__field _required field-date">
|
||||||
|
<div class="admin__field-label">
|
||||||
|
<label>
|
||||||
|
<span><?= $block->escapeHtml(__('To')) ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="admin__field-control">
|
||||||
|
<input type="text"
|
||||||
|
id="end_date"
|
||||||
|
name="end_date"
|
||||||
|
class="admin__control-text"
|
||||||
|
value="<?= $block->escapeHtmlAttr($block->getDefaultEndDate()) ?>"
|
||||||
|
data-mage-init='{"calendar": {"dateFormat": "yy-mm-dd"}}'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin__field">
|
||||||
|
<div class="admin__field-label">
|
||||||
|
<label><?= $block->escapeHtml(__('Order Status Filter')) ?></label>
|
||||||
|
</div>
|
||||||
|
<div class="admin__field-control">
|
||||||
|
<div class="admin__field-note">
|
||||||
|
<span><?= $block->escapeHtml(__('Currently configured to include: %1', implode(', ', $block->getOrderStatuses()))) ?></span>
|
||||||
|
<br/>
|
||||||
|
<span><?= $block->escapeHtml(__('To change this, go to: Stores > Configuration > Shopkeeper > Vendor Sales Report')) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin__field">
|
||||||
|
<div class="admin__field-control">
|
||||||
|
<button id="preview_report"
|
||||||
|
type="button"
|
||||||
|
class="action-default scalable primary"
|
||||||
|
style="margin-right: 10px;">
|
||||||
|
<span><?= $block->escapeHtml(__('Preview Report')) ?></span>
|
||||||
|
</button>
|
||||||
|
<button id="export_report"
|
||||||
|
type="button"
|
||||||
|
class="action-default scalable">
|
||||||
|
<span><?= $block->escapeHtml(__('Export CSV')) ?></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="report_preview" class="admin__page-section" style="display: none; margin-top: 20px;">
|
||||||
|
<div class="admin__page-section-title">
|
||||||
|
<span class="title"><?= $block->escapeHtml(__('Report Preview')) ?></span>
|
||||||
|
<span id="record_count" class="admin__page-section-title-note"></span>
|
||||||
|
</div>
|
||||||
|
<div class="admin__page-section-content">
|
||||||
|
<div class="admin__data-grid-wrap">
|
||||||
|
<table class="data-grid" id="report_grid">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= $block->escapeHtml(__('Date')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('Vendor Product #')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('Description')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('Dealer Product #')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('Country')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('City')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('State')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('Zip Code')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('Quantity')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('Cost')) ?></th>
|
||||||
|
<th><?= $block->escapeHtml(__('Order Status')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="report_tbody">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/x-magento-init">
|
||||||
|
{
|
||||||
|
"*": {
|
||||||
|
"Magento_Ui/js/core/app": {
|
||||||
|
"components": {
|
||||||
|
"vendorReportApp": {
|
||||||
|
"component": "Shopkeeper_VendorSalesReport/js/report"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
require(['jquery', 'Magento_Ui/js/modal/alert', 'loader'], function($, alert) {
|
||||||
|
$(document).ready(function() {
|
||||||
|
var gridUrl = '<?= $block->escapeJs($block->escapeUrl($block->getGridUrl())) ?>';
|
||||||
|
var exportUrl = '<?= $block->escapeJs($block->escapeUrl($block->getExportUrl())) ?>';
|
||||||
|
|
||||||
|
$('#preview_report').on('click', function() {
|
||||||
|
var startDate = $('#start_date').val();
|
||||||
|
var endDate = $('#end_date').val();
|
||||||
|
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
alert({
|
||||||
|
content: '<?= $block->escapeJs(__('Please select a date range.')) ?>'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('body').trigger('processStart');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: gridUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
start_date: startDate,
|
||||||
|
end_date: endDate,
|
||||||
|
form_key: FORM_KEY
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
$('body').trigger('processStop');
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
var tbody = $('#report_tbody');
|
||||||
|
tbody.empty();
|
||||||
|
|
||||||
|
if (response.data.length === 0) {
|
||||||
|
tbody.append('<tr><td colspan="11" style="text-align: center;"><?= $block->escapeJs(__('No data found for the selected date range.')) ?></td></tr>');
|
||||||
|
$('#record_count').text('');
|
||||||
|
} else {
|
||||||
|
$.each(response.data, function(index, row) {
|
||||||
|
var tr = $('<tr>');
|
||||||
|
tr.append($('<td>').text(row.Date));
|
||||||
|
tr.append($('<td>').text(row['Vendor Product number']));
|
||||||
|
tr.append($('<td>').text(row.Description));
|
||||||
|
tr.append($('<td>').text(row['Dealer product number']));
|
||||||
|
tr.append($('<td>').text(row.Country));
|
||||||
|
tr.append($('<td>').text(row.City));
|
||||||
|
tr.append($('<td>').text(row.State));
|
||||||
|
tr.append($('<td>').text(row['Zip code']));
|
||||||
|
tr.append($('<td>').text(row.Quantity));
|
||||||
|
tr.append($('<td>').text('$' + parseFloat(row.Cost).toFixed(2)));
|
||||||
|
tr.append($('<td>').text(row['Order Status']));
|
||||||
|
tbody.append(tr);
|
||||||
|
});
|
||||||
|
$('#record_count').text('(' + response.total + ' records)');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#report_preview').show();
|
||||||
|
} else {
|
||||||
|
alert({
|
||||||
|
content: response.message || '<?= $block->escapeJs(__('An error occurred while loading the report.')) ?>'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$('body').trigger('processStop');
|
||||||
|
alert({
|
||||||
|
content: '<?= $block->escapeJs(__('An error occurred while loading the report.')) ?>'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#export_report').on('click', function() {
|
||||||
|
var startDate = $('#start_date').val();
|
||||||
|
var endDate = $('#end_date').val();
|
||||||
|
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
alert({
|
||||||
|
content: '<?= $block->escapeJs(__('Please select a date range.')) ?>'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var form = $('<form>', {
|
||||||
|
'method': 'POST',
|
||||||
|
'action': exportUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
form.append($('<input>', {
|
||||||
|
'type': 'hidden',
|
||||||
|
'name': 'start_date',
|
||||||
|
'value': startDate
|
||||||
|
}));
|
||||||
|
|
||||||
|
form.append($('<input>', {
|
||||||
|
'type': 'hidden',
|
||||||
|
'name': 'end_date',
|
||||||
|
'value': endDate
|
||||||
|
}));
|
||||||
|
|
||||||
|
form.append($('<input>', {
|
||||||
|
'type': 'hidden',
|
||||||
|
'name': 'form_key',
|
||||||
|
'value': FORM_KEY
|
||||||
|
}));
|
||||||
|
|
||||||
|
$('body').append(form);
|
||||||
|
form.submit();
|
||||||
|
form.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
15
view/adminhtml/web/css/vendor-report.css
Normal file
15
view/adminhtml/web/css/vendor-report.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* Vendor Sales Report Menu - Remove icon */
|
||||||
|
.item-vendorsalesreport > a:before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional targeting for stubborn icons */
|
||||||
|
#menu-magento-reports-report .item-vendorsalesreport > a:before,
|
||||||
|
.admin__menu .item-vendorsalesreport > a:before {
|
||||||
|
content: none !important;
|
||||||
|
display: none !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user