first commit
This commit is contained in:
447
checkprestabox.php
Normal file
447
checkprestabox.php
Normal file
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class CheckPrestaBox extends Module
|
||||
{
|
||||
|
||||
|
||||
public const API_URL = 'https://api.checkbox.in.ua';
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'checkprestabox';
|
||||
$this->tab = 'advertising_marketing';
|
||||
$this->version = '1.0.0';
|
||||
$this->author = 'Panariga';
|
||||
$this->need_instance = 0;
|
||||
parent::__construct();
|
||||
|
||||
$this->displayName = $this->trans('Checkbox');
|
||||
$this->description = $this->trans('Accept payments for your products via Checkbox service.');
|
||||
$this->confirmUninstall = $this->trans('Are you sure about removing these details?');
|
||||
$this->ps_versions_compliancy = array(
|
||||
'min' => '9.0',
|
||||
'max' => _PS_VERSION_,
|
||||
);
|
||||
}
|
||||
|
||||
public function install()
|
||||
{
|
||||
return parent::install() &&
|
||||
$this->registerHook('displayAdminOrderTabContent');
|
||||
|
||||
if (!parent::install() || !$this->registerHook('displayAdminOrderTabContent')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function uninstall()
|
||||
{
|
||||
if (!parent::uninstall()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display tracking tab link.
|
||||
*/
|
||||
public function hookDisplayAdminOrderTabLink(array $params)
|
||||
{
|
||||
return $this->render($this->getModuleTemplatePath() . 'DisplayAdminOrderTabLink.html.twig');
|
||||
}
|
||||
private function getModuleTemplatePath(): string
|
||||
{
|
||||
return sprintf('@Modules/%s/views/templates/admin/', $this->name);
|
||||
}
|
||||
public function hookDisplayAdminOrderTabContent($params)
|
||||
{
|
||||
$router = $this->get('router');
|
||||
|
||||
if (Tools::isSubmit('checkprestaboxNewFiscal')) {
|
||||
|
||||
$this->processNewFiscalForm((int) $params['id_order']);
|
||||
$orderURL = $router->generate('admin_orders_view', [
|
||||
'orderId' => (int) $params['id_order'],
|
||||
|
||||
|
||||
]).'#checkprestaboxTabContent';
|
||||
|
||||
Tools::redirectAdmin($orderURL);
|
||||
}
|
||||
|
||||
$order = new Order((int) $params['id_order']);
|
||||
|
||||
return $this->render($this->getModuleTemplatePath() . 'DisplayAdminOrderTabContent.html.twig', [
|
||||
'checkprestaboxFiscals' => $this->getOrderFiscals($order),
|
||||
'checkprestaboxFiscalForm' => $this->getFiscalDefaults($order),
|
||||
'id_order' => (int)$params['id_order'], // Pass order ID for the AJAX call
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Render a twig template.
|
||||
*/
|
||||
private function render(string $template, array $params = []): string
|
||||
{
|
||||
/** @var Twig_Environment $twig */
|
||||
$twig = $this->get('twig');
|
||||
|
||||
return $twig->render($template, $params);
|
||||
}
|
||||
|
||||
private function processNewFiscalForm(int $id_order)
|
||||
{
|
||||
$order = new Order($id_order);
|
||||
if (!Validate::isLoadedObject($order)) {
|
||||
$this->context->controller->errors[] = $this->l('Invalid Order ID.');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 1. Retrieve and Sanitize Form Data ---
|
||||
$submittedProducts = Tools::getValue('products', []);
|
||||
$submittedPayments = Tools::getValue('payments', []);
|
||||
|
||||
if (empty($submittedProducts) || empty($submittedPayments)) {
|
||||
$this->context->controller->errors[] = $this->l('Fiscalization failed: No products or payments were submitted.');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 2. Transform Data and Calculate Totals for Validation ---
|
||||
$goodsForFiscalize = [];
|
||||
$productsTotal = 0;
|
||||
|
||||
foreach ($submittedProducts as $product) {
|
||||
|
||||
$price = round((float)($product['price'] ?? 0.0) * 100, 2);
|
||||
$quantity = (int)($product['quantity'] ?? 0) * 1000;
|
||||
if ($quantity == 0) {
|
||||
continue;
|
||||
}
|
||||
$productsTotal += $price * $quantity;
|
||||
|
||||
$goodsForFiscalize[] = [
|
||||
'good' => [
|
||||
'code' => (string)($product['code'] ?? ''),
|
||||
'name' => (string)($product['name'] ?? 'Unknown Product'),
|
||||
'price' => $price,
|
||||
],
|
||||
'quantity' => $quantity,
|
||||
];
|
||||
}
|
||||
|
||||
$paymentsForFiscalize = [];
|
||||
$paymentsTotal = 0;
|
||||
foreach ($submittedPayments as $payment) {
|
||||
$value = round((float)($payment['value'] ?? 0.0), 2) * 100;
|
||||
$paymentsTotal += $value;
|
||||
|
||||
$paymentsForFiscalize[] = [
|
||||
'type' => $payment['type'] == 'Готівка' ? 'CASH' : 'CASHLESS',
|
||||
'label' => $payment['type'],
|
||||
'value' => round($value, 2),
|
||||
];
|
||||
}
|
||||
|
||||
// IMPORTANT: Re-fetch discounts from the order for security. Never trust client-side data for this.
|
||||
$discountsForFiscalize = [];
|
||||
$discountsTotal = 0;
|
||||
$cart_rules = $order->getCartRules();
|
||||
foreach ($cart_rules as $cart_rule) {
|
||||
$value = round((float) $cart_rule['value'], 2) * 100;
|
||||
$discountsTotal += $value;
|
||||
$discountsForFiscalize[] = [
|
||||
'type' => 'DISCOUNT',
|
||||
'mode' => 'VALUE',
|
||||
'value' => $value,
|
||||
'name' => $cart_rule['name'],
|
||||
];
|
||||
}
|
||||
|
||||
// --- 3. Server-Side Validation ---
|
||||
/* $grandTotal = $productsTotal - $discountsTotal;
|
||||
if (abs($grandTotal - $paymentsTotal) > 0.01) {
|
||||
$this->context->controller->errors[] = sprintf(
|
||||
$this->l('Fiscalization failed: Totals do not match. Amount to pay was %s, but payment amount was %s.'),
|
||||
number_format($grandTotal, 2),
|
||||
number_format($paymentsTotal, 2)
|
||||
);
|
||||
return;
|
||||
} */
|
||||
|
||||
// --- 4. Execute Fiscalization ---
|
||||
try {
|
||||
$header = Tools::getValue('header', '');
|
||||
$footer = Tools::getValue('footer', '');
|
||||
|
||||
$this->fiscalize(
|
||||
$order,
|
||||
$goodsForFiscalize,
|
||||
$paymentsForFiscalize,
|
||||
$discountsForFiscalize,
|
||||
$header,
|
||||
$footer
|
||||
);
|
||||
|
||||
$this->context->controller->confirmations[] = $this->l('Fiscal check was successfully created.');
|
||||
} catch (PrestaShopException $e) {
|
||||
// The fiscalize() function throws an exception on failure
|
||||
$this->context->controller->errors[] = $this->l('Fiscalization API Error:') . ' ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public function getOrderFiscals(Order $order): PrestaShopCollection
|
||||
{
|
||||
$fiscal = new PrestaShopCollection('OrderPayment');
|
||||
$fiscal->where('order_reference', '=', $order->reference); // Filter by order reference
|
||||
$fiscal->where('payment_method', '=', $this->name); // Filter by this module's payment method name
|
||||
return $fiscal->getAll();
|
||||
}
|
||||
public function getFiscalDefaults(Order $order): array
|
||||
{
|
||||
$details = $order->getOrderDetailList();
|
||||
|
||||
foreach ($details as $detail) {
|
||||
|
||||
$products[$detail['product_id']] = [
|
||||
'good' => [
|
||||
'code' => $detail['product_reference'],
|
||||
'name' => $detail['product_name'],
|
||||
'price' => round(((float) $detail['total_price_tax_incl'] / (int) $detail['product_quantity']), 2),
|
||||
],
|
||||
'quantity' => (int) $detail['product_quantity'],
|
||||
];
|
||||
}
|
||||
|
||||
$shipping = $order->getShipping();
|
||||
if ($shipping['0']['shipping_cost_tax_incl'] > 0) {
|
||||
|
||||
$products[$detail['shipping_item']] = [
|
||||
'good' => [
|
||||
'code' => 'NP52.29',
|
||||
'name' => 'Пакувальний матеріал',
|
||||
'price' => round((float) ($shipping['0']['shipping_cost_tax_incl']), 2),
|
||||
],
|
||||
// 'good_id' => $detail['product_id'],
|
||||
'quantity' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
$cart_rules = $order->getCartRules();
|
||||
|
||||
foreach ($cart_rules as $cart_rule) {
|
||||
$discounts[] = [
|
||||
'type' => 'DISCOUNT',
|
||||
'mode' => 'VALUE',
|
||||
'value' => round((float) $cart_rule['value'], 2),
|
||||
'name' => $cart_rule['name'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
$payments = [];
|
||||
$defaults = [
|
||||
'payments' => $payments,
|
||||
'products' => $products,
|
||||
'discounts' => $discounts,
|
||||
'header' => 'Дякуємо за покупку!',
|
||||
'footer' => 'Магазин ' . Configuration::get('PS_SHOP_NAME') . '. Замовлення ' . $order->reference . '.',
|
||||
];
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
public function addOrderPayment(Order $order, string $receipt_id, string $date_add): OrderPayment
|
||||
{
|
||||
|
||||
$order_payment = new OrderPayment();
|
||||
$order_payment->order_reference = $order->reference;
|
||||
$order_payment->id_currency = $order->id_currency;
|
||||
$order_payment->conversion_rate = 1;
|
||||
$order_payment->payment_method = $this->name;
|
||||
$order_payment->transaction_id = mb_substr($receipt_id, 0, 254);
|
||||
$order_payment->amount = 0;
|
||||
$order_payment->date_add = $date_add;
|
||||
if (!$order_payment->save()) {
|
||||
throw new Exception('failed to save OrderPayment');
|
||||
}
|
||||
|
||||
return $order_payment;
|
||||
}
|
||||
|
||||
public function getReceiptsByFiscalCode(string $fiscal_code): array
|
||||
{
|
||||
$result = $this->apiCall('/api/v1/receipts/search?fiscal_code=' . $fiscal_code, [], 'GET', null);
|
||||
if (isset($result['results'])) {
|
||||
return $result['results'];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getReceipt(string $receipt_id): array
|
||||
{
|
||||
return $this->apiCall('/api/v1/receipts/' . $receipt_id, [], 'GET', null);
|
||||
}
|
||||
|
||||
public function getShifts(string $statuses = 'OPENED'): array
|
||||
{
|
||||
|
||||
$resp = $this->apiCall('/api/v1/shifts?statuses=' . $statuses, [], 'GET', null);
|
||||
if ($resp['status'] == 'ok' && isset($resp['results'])) {
|
||||
return $resp['results'];
|
||||
}
|
||||
throw new PrestaShopException('getShifts failed');
|
||||
}
|
||||
|
||||
public function apiCall(string $endpoint, array $payload = [], string $method = 'POST', ?string $responseKey = ''): array
|
||||
{
|
||||
$this->log(['method' => $method, 'endpoint' => $endpoint, 'payload' => $payload]);
|
||||
$client = HttpClient::create([
|
||||
'base_uri' => self::API_URL,
|
||||
'auth_bearer' => $this->getAuthToken(),
|
||||
'timeout' => 150
|
||||
]);
|
||||
|
||||
if (count($payload)) {
|
||||
$response = $client->request($method, $endpoint, [
|
||||
'json' => $payload,
|
||||
]);
|
||||
} else {
|
||||
$response = $client->request($method, $endpoint);
|
||||
}
|
||||
|
||||
|
||||
$this->log([$response->getStatusCode()]);
|
||||
|
||||
// $this->log([$response->getContent(false)]);
|
||||
|
||||
$r = $response->toArray(false);
|
||||
|
||||
$this->log(['API response' => $r]);
|
||||
if ($response->getStatusCode(false) != 200) {
|
||||
// PrestaShopLogger::addLog(json_encode($r), 4);
|
||||
}
|
||||
if ($responseKey) {
|
||||
return $r[$responseKey];
|
||||
}
|
||||
return $r;
|
||||
}
|
||||
|
||||
public function getCashierData(): array
|
||||
{
|
||||
$response = $this->apiCall('/api/v1/cashier/me', [], 'GET', null);
|
||||
if (isset($response['id'])) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
throw new Exception('getCashierData failed');
|
||||
}
|
||||
|
||||
public function fiscalize(Order $order, array $goods, array $payments, array $discounts = [], string $header = '', string $footer = '')
|
||||
{
|
||||
|
||||
$cashier_data = $this->getCashierData();
|
||||
|
||||
$data = [
|
||||
'cashier_name' => $cashier_data['full_name'],
|
||||
'departament' => $departament ?? Tools::getShopDomainSsl(),
|
||||
'header' => $header,
|
||||
'footer' => $footer,
|
||||
'goods' => $goods,
|
||||
'payments' => $payments,
|
||||
'discounts' => $discounts,
|
||||
'callback_url' => $this->context->link->getModuleLink($this->name, 'callbackapi', []),
|
||||
'barcode' => $order->reference . '#' . $order->id,
|
||||
'context' => [
|
||||
'id_order' => $order->id
|
||||
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
$this->log(['fiscalize' => $data]);
|
||||
|
||||
|
||||
$resp = $this->apiCall('/api/v1/receipts/sell', $data, 'POST', null);
|
||||
if (isset($resp['message'])) {
|
||||
if ($resp['message'] == "Зміну не відкрито") {
|
||||
$this->openShift();
|
||||
return $this->fiscalize($order, $goods, $payments, $discounts, $header, $footer);
|
||||
}
|
||||
}
|
||||
if (isset($resp['message'])) {
|
||||
if ($resp['message'] == "Зміну не відкрито") {
|
||||
$this->openShift();
|
||||
return $this->fiscalize($order, $goods, $payments, $discounts, $header, $footer);
|
||||
}
|
||||
$orderPayment = $this->addOrderPayment($order, $resp['message'], date("Y-m-d H:i:s"));
|
||||
} else {
|
||||
}
|
||||
$orderPayment = $this->addOrderPayment($order, 'pending', date("Y-m-d H:i:s"));
|
||||
|
||||
|
||||
if (isset($resp['id'])) {
|
||||
$orderPayment->transaction_id = $resp['id'];
|
||||
$orderPayment->save();
|
||||
return $resp;
|
||||
}
|
||||
throw new PrestaShopException('fiscalize failed: ' . json_encode($resp));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function log(array $data)
|
||||
{
|
||||
$logdirectory = _PS_ROOT_DIR_ . '/var/modules/' . $this->name . '/logs/' . date("Y") . '/' . date("m") . '/' . date("d") . '/';
|
||||
if (!is_dir($logdirectory)) {
|
||||
mkdir($logdirectory, 0750, true);
|
||||
}
|
||||
$logger = new \FileLogger(0); //0 == debug level, logDebug() won’t work without this.
|
||||
$logger->setFilename($logdirectory . 'dayly.log');
|
||||
$logger->logInfo(json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
public function getAuthToken(): string
|
||||
{
|
||||
return $this->apiAuth();
|
||||
}
|
||||
|
||||
public function apiAuth(): string
|
||||
{
|
||||
$resp = json_decode(file_get_contents('https://zoooptimum.com/module/ffcheckbox/endpoint?keycheckbox=TYVRNSGYJTYVRNSGYJ'));
|
||||
|
||||
if (isset($resp->PS_FFCHECKBOX_AUTH_TOKEN)) {
|
||||
return $resp->PS_FFCHECKBOX_AUTH_TOKEN->value;
|
||||
}
|
||||
throw new PrestaShopException("apiAuth failed");
|
||||
}
|
||||
|
||||
public function openShift(): string
|
||||
{
|
||||
$headers = [
|
||||
'X-License-Key: ' . $key,
|
||||
];
|
||||
|
||||
$resp = $this->apiCall('/api/v1/shifts', [], 'POST', null);
|
||||
if (isset($resp['id']) && isset($resp['status']) && $resp['status'] == 'CREATED') {
|
||||
return $resp['id'];
|
||||
}
|
||||
throw new Exception('failed to open shift');
|
||||
}
|
||||
}
|
||||
12
config_uk.xml
Normal file
12
config_uk.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<module>
|
||||
<name>checkprestabox</name>
|
||||
<displayName><![CDATA[Checkbox]]></displayName>
|
||||
<version><![CDATA[1.0.0]]></version>
|
||||
<description><![CDATA[Accept payments for your products via Checkbox service.]]></description>
|
||||
<author><![CDATA[Panariga]]></author>
|
||||
<tab><![CDATA[advertising_marketing]]></tab>
|
||||
<confirmUninstall><![CDATA[Are you sure about removing these details?]]></confirmUninstall>
|
||||
<is_configurable>0</is_configurable>
|
||||
<need_instance>0</need_instance>
|
||||
</module>
|
||||
72
controllers/front/callbackapi.php
Normal file
72
controllers/front/callbackapi.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/*
|
||||
* 2007-2015 PrestaShop
|
||||
*
|
||||
* NOTICE OF LICENSE
|
||||
*
|
||||
* This source file is subject to the Academic Free License (AFL 3.0)
|
||||
* that is bundled with this package in the file LICENSE.txt.
|
||||
* It is also available through the world-wide-web at this URL:
|
||||
* http://opensource.org/licenses/afl-3.0.php
|
||||
* If you did not receive a copy of the license and are unable to
|
||||
* obtain it through the world-wide-web, please send an email
|
||||
* to license@prestashop.com so we can send you a copy immediately.
|
||||
*
|
||||
* DISCLAIMER
|
||||
*
|
||||
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
|
||||
* versions in the future. If you wish to customize PrestaShop for your
|
||||
* needs please refer to http://www.prestashop.com for more information.
|
||||
*
|
||||
* @author PrestaShop SA <contact@prestashop.com>
|
||||
* @copyright 2007-2015 PrestaShop SA
|
||||
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
|
||||
* International Registered Trademark & Property of PrestaShop SA
|
||||
*/
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @property \MonoPayment $module An instance of the MonoPayment module.
|
||||
*/
|
||||
class CheckPrestaBoxCallbackApiModuleFrontController extends ModuleFrontController
|
||||
{
|
||||
|
||||
public function postProcess()
|
||||
{
|
||||
$response = new Response();
|
||||
$message = file_get_contents('php://input');
|
||||
$this->module->log(['CallbackApi' => $message, '$_SERVER' => $_SERVER]);
|
||||
|
||||
$response->setStatusCode(403);
|
||||
$response->send();
|
||||
exit;
|
||||
if ($_SERVER['REQUEST_METHOD'] != 'POST' || !$this->module->verifySignature($signature, $message)) {
|
||||
} else {
|
||||
$this->module->log(['CallbackApi' => $message]);
|
||||
|
||||
try {
|
||||
|
||||
|
||||
$callbackData = json_decode($message, true, 512, JSON_THROW_ON_ERROR);
|
||||
$objectPayload = ObjectPayload::getInstance($callbackData['invoiceId'], $this->module->invoce_tag, $this->module->name);
|
||||
$objectPayload->addRecord('callback', $callbackData);
|
||||
$objectPayload->save();
|
||||
|
||||
if (isset($callbackData['status'])) {
|
||||
|
||||
$this->module->processCallbackAPI($callbackData, $objectPayload);
|
||||
|
||||
$response->setStatusCode(200);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
PrestaShopLogger::addLog($e->getTraceAsString(), 4);
|
||||
|
||||
$response->setStatusCode(500);
|
||||
}
|
||||
}
|
||||
|
||||
$response->send();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
80
views/js/admin_order_fiscal.js
Normal file
80
views/js/admin_order_fiscal.js
Normal file
@@ -0,0 +1,80 @@
|
||||
$(document).ready(function() {
|
||||
const productsBody = $('#fiscal-products-body');
|
||||
const paymentsContainer = $('#fiscal-payments-container');
|
||||
const addPaymentBtn = $('#add-payment-btn');
|
||||
const paymentRowTemplate = $('#payment-row-template');
|
||||
let paymentIndex = 0; // A counter for unique payment input names
|
||||
|
||||
// --- Core Calculation Function ---
|
||||
function updateTotals() {
|
||||
let productsTotal = 0;
|
||||
productsBody.find('.product-row').each(function() {
|
||||
const quantity = parseFloat($(this).find('.fiscal-quantity').val()) || 0;
|
||||
const price = parseFloat($(this).find('.fiscal-price').val()) || 0;
|
||||
const rowTotal = quantity * price;
|
||||
$(this).find('.fiscal-row-total').text(rowTotal.toFixed(2));
|
||||
productsTotal += rowTotal;
|
||||
});
|
||||
|
||||
let discountsTotal = 0;
|
||||
$('.discount-row').each(function() {
|
||||
const discountValue = parseFloat($(this).find('.fiscal-discount-value').text().replace(',', '.')) || 0;
|
||||
discountsTotal += Math.abs(discountValue);
|
||||
});
|
||||
|
||||
const grandTotal = productsTotal - discountsTotal;
|
||||
|
||||
let paymentsTotal = 0;
|
||||
paymentsContainer.find('.payment-row').each(function() {
|
||||
const paymentAmount = parseFloat($(this).find('.payment-amount').val()) || 0;
|
||||
paymentsTotal += paymentAmount;
|
||||
});
|
||||
|
||||
// Update summary display
|
||||
$('#fiscal-products-total').text(productsTotal.toFixed(2));
|
||||
$('#fiscal-discounts-total').text(discountsTotal.toFixed(2));
|
||||
$('#fiscal-grand-total').text(grandTotal.toFixed(2));
|
||||
$('#fiscal-payments-total').text(paymentsTotal.toFixed(2));
|
||||
|
||||
const mismatchError = $('#fiscal-total-mismatch-error');
|
||||
const createCheckBtn = $('#create-fiscal-check-btn');
|
||||
|
||||
if (Math.abs(grandTotal - paymentsTotal) > 0.001) {
|
||||
mismatchError.show();
|
||||
createCheckBtn.prop('disabled', true);
|
||||
} else {
|
||||
mismatchError.hide();
|
||||
createCheckBtn.prop('disabled', grandTotal <= 0);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Event Handlers ---
|
||||
$('form').on('input', '.fiscal-quantity, .fiscal-price', updateTotals);
|
||||
paymentsContainer.on('input', '.payment-amount', updateTotals);
|
||||
|
||||
addPaymentBtn.on('click', function() {
|
||||
const newRow = $(paymentRowTemplate.html());
|
||||
newRow.find('.payment-type').attr('name', `payments[${paymentIndex}][type]`);
|
||||
newRow.find('.payment-amount').attr('name', `payments[${paymentIndex}][value]`);
|
||||
paymentsContainer.append(newRow);
|
||||
|
||||
if (paymentsContainer.find('.payment-row').length === 1) {
|
||||
const grandTotal = parseFloat($('#fiscal-grand-total').text());
|
||||
if (grandTotal > 0) {
|
||||
newRow.find('.payment-amount').val(grandTotal.toFixed(2));
|
||||
}
|
||||
}
|
||||
|
||||
paymentIndex++;
|
||||
updateTotals();
|
||||
});
|
||||
|
||||
paymentsContainer.on('click', '.remove-payment-btn', function() {
|
||||
$(this).closest('.payment-row').remove();
|
||||
updateTotals();
|
||||
});
|
||||
|
||||
// --- Initial State ---
|
||||
updateTotals();
|
||||
addPaymentBtn.trigger('click');
|
||||
});
|
||||
199
views/templates/admin/DisplayAdminOrderTabContent.html.twig
Normal file
199
views/templates/admin/DisplayAdminOrderTabContent.html.twig
Normal file
@@ -0,0 +1,199 @@
|
||||
<script src="/modules/checkprestabox/views/js/admin_order_fiscal.js"></script>
|
||||
<div class="tab-pane fade " id="checkprestaboxTabContent" role="tabpanel" aria-labelledby="checkprestaboxTab">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header"></div>
|
||||
<div class="card-body">
|
||||
{% if checkprestaboxFiscals is not empty %}
|
||||
<h4>Існуючі фіскальні чеки:</h4>
|
||||
<ul>
|
||||
{% for fiscal in checkprestaboxFiscals %}
|
||||
<li>
|
||||
<a target="_blanc" href="https://check.checkbox.ua/{{ fiscal.transaction_id }}">
|
||||
Чек #
|
||||
{{ fiscal.transaction_id }}
|
||||
від
|
||||
{{ fiscal.date_add }}
|
||||
<i class="material-icons">open_in_new</i>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<hr>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#fiscalModal">
|
||||
Створити фіскальний чек
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Fiscalization Modal -->
|
||||
<div class="modal fade" id="fiscalModal" tabindex="-1" role="dialog" aria-labelledby="fiscalModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="fiscalModalLabel">Створення фіскального чеку</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="#checkprestaboxTabContent">
|
||||
<div class="card mt-2">
|
||||
<h3 class="card-header">
|
||||
<i class="material-icons">receipt</i>
|
||||
Створення фіскального чеку
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="card-body">
|
||||
{# Products Section #}
|
||||
<h6>
|
||||
<textarea rows="1" class="form-control " name="header">{{ checkprestaboxFiscalForm.header }}</textarea>
|
||||
</h6>
|
||||
<h6>Товари в чеку</h6>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Назва</th>
|
||||
<th>Код (Артикул)</th>
|
||||
<th style="width: 100px;">Кількість</th>
|
||||
<th style="width: 120px;">Ціна за од.</th>
|
||||
<th class="text-right" style="width: 120px;">Сума</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fiscal-products-body">
|
||||
{% for product in checkprestaboxFiscalForm.products %}
|
||||
<tr class="product-row">
|
||||
<td>{{ product.good.name }}</td>
|
||||
<td>{{ product.good.code }}</td>
|
||||
<td>
|
||||
{# HIDDEN inputs to pass non-editable data #}
|
||||
<input type="hidden" name="products[{{ loop.index0 }}][name]" value="{{ product.good.name }}">
|
||||
<input
|
||||
type="hidden" name="products[{{ loop.index0 }}][code]" value="{{ product.good.code }}">
|
||||
|
||||
{# EDITABLE inputs #}
|
||||
<input type="number" class="form-control fiscal-quantity" name="products[{{ loop.index0 }}][quantity]" value="{{ product.quantity }}" min="0">
|
||||
</td>
|
||||
<td><input type="number" step="0.01" class="form-control fiscal-price" name="products[{{ loop.index0 }}][price]" value="{{ product.good.price|number_format(2, '.', '') }}" min="0"></td>
|
||||
<td class="text-right">
|
||||
<span class="fiscal-row-total">0.00</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{# Discounts Section #}
|
||||
{% if checkprestaboxFiscalForm.discounts is not empty %}
|
||||
<h6>Знижки</h6>
|
||||
<table class="table table-sm">
|
||||
<tbody>
|
||||
{% for discount in checkprestaboxFiscalForm.discounts %}
|
||||
<tr class="discount-row">
|
||||
<td>{{ discount.name }}</td>
|
||||
<td class="text-right fiscal-discount-value">-{{ discount.value|number_format(2, '.', '') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
{# Summary and Payments Section #}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Оплати</h6>
|
||||
<div
|
||||
id="fiscal-payments-container">{# Payment rows will be inserted here by JavaScript #}
|
||||
</div>
|
||||
<button type="button" id="add-payment-btn" class="btn btn-sm btn-outline-primary mt-2">
|
||||
<i class="material-icons">add_circle_outline</i>
|
||||
Додати оплату
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Всього за товари:</td>
|
||||
<td class="text-right font-weight-bold">
|
||||
<span id="fiscal-products-total">0.00</span>
|
||||
грн</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Знижки:</td>
|
||||
<td class="text-right font-weight-bold">-<span id="fiscal-discounts-total">0.00</span>
|
||||
грн</td>
|
||||
</tr>
|
||||
<tr class="bg-light">
|
||||
<td class="font-weight-bold">ДО СПЛАТИ (РАЗОМ):</td>
|
||||
<td class="text-right font-weight-bold">
|
||||
<span id="fiscal-grand-total">0.00</span>
|
||||
грн</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Сплачено:</td>
|
||||
<td class="text-right font-weight-bold">
|
||||
<span id="fiscal-payments-total">0.00</span>
|
||||
грн</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="fiscal-total-mismatch-error" class="alert alert-danger" style="display: none;">
|
||||
<strong>Помилка:</strong>
|
||||
Сума оплат не збігається з підсумковою сумою до сплати.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<h6>
|
||||
<textarea rows="2" class="form-control " name="footer">{{ checkprestaboxFiscalForm.footer }}</textarea>
|
||||
</h6>
|
||||
<div
|
||||
class="d-flex justify-content-end">
|
||||
{# The important submit button #}
|
||||
<button type="submit" name="checkprestaboxNewFiscal" id="create-fiscal-check-btn" class="btn btn-primary" disabled>
|
||||
Створити та фіскалізувати
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Закрити</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# This invisible template is used by JavaScript to create new payment rows #}
|
||||
<template id="payment-row-template">
|
||||
<div class="form-row align-items-center mb-2 payment-row">
|
||||
<div
|
||||
class="col-5">
|
||||
{# The name attribute will be set by JavaScript #}
|
||||
<select class="form-control payment-type">
|
||||
<option value="Платіж через інтегратора НоваПей" data-label="Платіж через інтегратора НоваПей">НоваПей</option>
|
||||
<option value="Платіж через інтегратора Liqpay" data-label="Платіж через інтегратора Liqpay">Liqpay</option>
|
||||
<option value="Платіж через інтегратора RozetkaPay" data-label="Платіж через інтегратора RozetkaPay">RozetkaPay</option>
|
||||
<option value="Платіж через інтегратора mono" data-label="Платіж через інтегратора mono">mono</option>
|
||||
<option value="Готівка" data-label="Готівка">Готівка</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
class="col-5">
|
||||
{# The name attribute will be set by JavaScript #}
|
||||
<input type="number" step="0.01" class="form-control payment-amount" placeholder="Сума" min="0">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-payment-btn">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
6
views/templates/admin/DisplayAdminOrderTabLink.html.twig
Normal file
6
views/templates/admin/DisplayAdminOrderTabLink.html.twig
Normal file
@@ -0,0 +1,6 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="checkprestaboxTab" data-toggle="tab" href="#checkprestaboxTabContent" role="tab" aria-controls="checkprestaboxTabContent" aria-expanded="true" aria-selected="false">
|
||||
<i class="material-icons">app_registration</i>
|
||||
Checkbox
|
||||
</a>
|
||||
</li>
|
||||
Reference in New Issue
Block a user