Initial commit: Vendor Sales Report Magento 2 module

This commit is contained in:
shopkeeperdev
2025-11-03 17:20:07 -05:00
commit fc2838d68a
25 changed files with 1839 additions and 0 deletions

141
.github/copilot-instructions.md vendored Normal file
View 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

View 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'));
}
}

View 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');
}
}
}

View 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()
]);
}
}
}

View 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;
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,9 @@
<?php
/**
* Shopkeeper VendorSalesReport Module Registration
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Shopkeeper_VendorSalesReport',
__DIR__
);

View 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"}}

View 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>

View 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>

View 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;
}