Files
kupshop/admin/products.variations.php
2025-08-02 16:30:27 +02:00

730 lines
27 KiB
PHP

<?php
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\CatalogBundle\Search\FulltextInterface;
use KupShop\I18nBundle\Translations\VariationsTranslation;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\EANValidator;
use Query\Operator;
class ProductsVariations extends Frame
{
use DatabaseCommunication;
protected $template = 'window/products.variations.tpl';
public function get_vars()
{
$vars = parent::get_vars();
$pageVars = getVal('body', $vars, []);
$ID = getVal('ID');
$productQb = sqlQueryBuilder()
->select('v.vat, p.discount, p.code, p.price')
->from('products', 'p')
->leftJoin('p', 'vats', 'v', 'p.vat = v.id')
->where(Operator::equals(['p.id' => $ID]));
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
$productQb->addSelect('pu.short_name_admin as unit')
->leftJoin('p', 'products_units', 'pu', 'p.unit = pu.id');
$productQb->addSelect('pu2.short_name_admin measure_quantity_name')
->leftJoin('p', 'products_units', 'pu2', 'p.measure_unit = pu2.id');
}
$PRODUCT = $productQb->execute()->fetch();
if (empty($PRODUCT['unit'])) {
$PRODUCT['unit'] = 'ks';
}
$acn = getVal('acn', null, 'show');
// Categorization
$CHOICES = [];
$VALUES = [];
$CHOICES_CATEGORIZED = [];
$query = 'SELECT id, label '
.'FROM '.getTableName('products_variations_choices_labels').' AS pvcl '
.'ORDER BY id ASC';
$SQL = sqlQuery($query);
foreach ($SQL as $row) {
$CHOICES[$row['id']] = $row['label'];
}
// ------------
// uz pouzite
$query = 'SELECT id_product, id_label, list_order '
.'FROM '.getTableName('products_variations_choices_categorization').' AS pvcc '
."WHERE id_product={$ID} "
.'ORDER BY list_order ASC';
$SQL = sqlQuery($query);
foreach ($SQL as $row) {
$CHOICES_CATEGORIZED[] = $row['id_label'];
}
// Values
$CHOICES_VALUES = [];
for ($x = 0; $x < count($CHOICES_CATEGORIZED); $x++) {
$SQL = sqlQuery('SELECT id, value, code
FROM '.getTableName('products_variations_choices_values').' AS pvcv
WHERE id_label='.intval($CHOICES_CATEGORIZED[$x]).'
ORDER BY sort ASC');
foreach ($SQL as $row) {
$value = $row['value'];
$id = $row['id'];
$VALUES[$id] = $value;
$CHOICES_VALUES[$CHOICES_CATEGORIZED[$x]][$id] = $value;
}
}
// ###############################################################
// nejdrive nacist vsechy kombinace k variantam
$COMBINATIONS = [];
$SQL = sqlQuery('SELECT pvc.id_variation, pvc.id_label, pvc.id_value
FROM '.getTableName('products_variations_combination').' AS pvc
JOIN '.getTableName('products_variations')." AS pv ON pv.id=pvc.id_variation
WHERE pv.id_product={$ID}");
foreach ($SQL as $row) {
$IDvariation = $row['id_variation'];
if (!isset($COMBINATIONS[$IDvariation])) {
$COMBINATIONS[$IDvariation] = [];
}
$COMBINATIONS[$IDvariation][] = $row;
}
$priceWithVat = $GLOBALS['dbcfg']['prod_prefer_price_vat'];
$vatPerc = 0;
$discount = 0;
if ($priceWithVat == 'Y') {
$vatPerc = $PRODUCT['vat'];
}
if ($priceWithVat == 'F') {
$vatPerc = $PRODUCT['vat'];
$discount = $PRODUCT['discount'];
}
$SQL = sqlQuery('SELECT pv.*, COALESCE(pv.price, p.price) as price_real
FROM '.getTableName('products_variations').' AS pv
LEFT JOIN '.getTableName('products').' p ON pv.id_product=p.id
LEFT JOIN '.getTableName('products_variations_combination').' pvc ON pvc.id_variation=pv.id
LEFT JOIN '.getTableName('products_variations_choices_values')." pvcv ON pvc.id_value=pvcv.id
WHERE id_product={$ID}
GROUP BY pv.id
ORDER BY pvcv.sort ASC");
$VARIATIONS = [];
foreach ($SQL as $key => $row) {
$row['price_orig'] = $row['price'];
if ($row['price']) {
$row['price'] = calcPrice($row['price'], $vatPerc, $discount);
// $row['price'] = round(setPrice($row['price'], $discount, $vatPerc, "format=false"), 2);
}
$row['price_real'] = calcPrice($row['price_real'], $vatPerc, $discount);
$row['in_store'] = floatval($row['in_store']);
$row['delivery_time_days'] = '';
if ($row['delivery_time'] > 0) {
$row['delivery_time_days'] = $row['delivery_time'];
$row['delivery_time'] = 1;
}
$row['piecesOrdered'] = returnSQLResult('SELECT SUM(oi.pieces)
FROM '.getTableName('order_items').' oi
LEFT JOIN '.getTableName('orders')." o ON oi.id_order=o.id
WHERE oi.id_product={$ID} AND oi.id_variation={$row['id']} AND
o.status_storno=0 AND o.status IN
(".join(',', getStatuses('notpacked')).')');
$row['class'] = 'row-green';
if ($row['in_store'] < 0) {
if ($row['id_product']) {
// Suppliers products
if (findModule('products_suppliers')) {
$query = 'SELECT SUM(in_store)
FROM '.getTableName('products_of_suppliers')." pos
WHERE pos.id_product={$row['id_product']} AND pos.id_variation={$row['id']}";
$row['piecesInSuppliers'] = returnSQLResult($query);
}
}
if ($row['in_store'] + $row['piecesOrdered'] < 0) {
$row['class'] = 'row-red';
if (findModule('products_suppliers')) {
if ($row['in_store'] + $row['piecesOrdered'] + $row['piecesInSuppliers'] > 0) {
$row['class'] = 'row-orange';
}
}
}
}
$VARIATIONS[] = $row;
}
$pageVars = array_merge($pageVars, [
'acn' => $acn,
's' => getVal('s'),
'ID' => $ID,
'choices' => $CHOICES,
'choices_categorized' => $CHOICES_CATEGORIZED,
'choices_values' => $CHOICES_VALUES,
'values' => $VALUES,
'variations' => $VARIATIONS,
'combinations' => $COMBINATIONS,
'product' => $PRODUCT,
]);
if (!empty($this->getErrors()) || !empty($this->getHTMLErrors())) {
$variationsData = getVal('variations', getVal('data'));
foreach ($pageVars['variations'] as $key => &$variation) {
if (!empty($variationsData[$key + 1])) {
$variation = array_merge($variation, $variationsData[$key + 1]);
}
}
}
$pageVars['variationTranslationsFigure'] = $this->getVariationTranslationsFigure(
array_map(fn ($x) => $x['id'], $pageVars['variations'] ?? [])
);
$this->deserializeVariationsCustomData($pageVars['variations']);
$vars['body'] = $pageVars;
return $vars;
}
public function handleGenerateEan()
{
if ($id_variation = intval(getVal('id_variation'))) {
$ean = EANValidator::generateEan(['id_variation' => $id_variation]);
$exists = Variations::eanExists($id_variation, $ean);
if ($exists) {
$message = Variations::eanExistsMessage($exists);
$this->addHTMLError('Vygenerovaný '.$message); // Vygenerovaný EAN {ean} není unikátní. {odkaz na duplicitní produkt}
} else {
sqlQueryBuilder()->update('products_variations')
->directValues(['ean' => $ean])
->where(Operator::equals(['id' => $id_variation]))
->execute();
$this->returnOK();
}
}
}
public function handleDeactivatePriceForDiscount()
{
if ($id_variation = intval(getVal('id_variation'))) {
sqlQueryBuilder()->update('products_variations')
->directValues(['price_for_discount' => -1])
->where(Operator::equals(['id' => $id_variation]))
->execute();
}
$this->returnOK();
}
public function handleActivatePriceForDiscount()
{
if ($id_variation = intval(getVal('id_variation'))) {
sqlQueryBuilder()->update('products_variations')
->directValues(['price_for_discount' => null])
->where(Operator::equals(['id' => $id_variation]))
->execute();
}
$this->returnOK();
}
public function handle()
{
parent::handle();
$ID = intval(getVal('ID'));
$SQL = sqlQuery('SELECT v.vat, p.discount FROM '.getTableName('products')." p
LEFT JOIN vats v ON p.vat=v.id
WHERE p.id={$ID}");
$PRODUCT = sqlFetchArray($SQL);
if (isset($_REQUEST['submitAddChoice']) && is_numeric($ID)) {
$id_label = getVal('id_label');
if ($id_label) {
sqlQuery('REPLACE INTO products_variations_choices_categorization SET id_product = :id_product, id_label = :id_label, list_order = :list_order', [
'id_product' => $ID,
'id_label' => $id_label,
'list_order' => intval(getVal('list_order')),
]);
$id_label_value = getVal('id_label_value');
if ($id_label_value) {
$variations = sqlQuery('SELECT id FROM products_variations WHERE id_product = :id_product', ['id_product' => $ID]);
foreach ($variations as $variation) {
sqlQueryBuilder()->insert('products_variations_combination')
->directValues([
'id_variation' => $variation['id'],
'id_label' => $id_label,
'id_value' => $id_label_value,
])->execute();
}
}
$ErrStr = getTextString('status', 'saved');
$url = 'launch.php?s='.$_REQUEST['s'].'&ID='.$_REQUEST['ID'].'&ErrStr='.urlencode($ErrStr);
redirect($url);
}
$this->returnError('Nebyla vybrána jmenovka.');
}
if (isset($_REQUEST['submitDeleteChoice']) && findRight('PROD_ERASE')) {
$id_label = getVal('id_label_del');
if ($id_label) {
sqlQuery('DELETE FROM products_variations_choices_categorization
WHERE id_product = :id_product AND id_label = :id_label', [
'id_product' => $ID,
'id_label' => $id_label,
]);
sqlQuery('DELETE pvc FROM products_variations_combination pvc
LEFT JOIN products_variations pv ON pvc.id_variation = pv.id
WHERE pv.id_product = :id_product AND id_label = :id_label', [
'id_product' => $ID,
'id_label' => $id_label,
]);
$ErrStr = getTextString('status', 'saved');
$url = 'launch.php?s='.$_REQUEST['s'].'&ID='.$_REQUEST['ID'].'&ErrStr='.urlencode($ErrStr);
redirect($url);
}
$this->returnError('Nebyla vybrána jmenovka.');
}
if (isset($_REQUEST['submitGenerateCombinations']) && is_numeric($_REQUEST['ID'])) {
$labels = [];
$qb = sqlQueryBuilder()->select('pvcv.id_label, pvcv.id')
->from('products_variations_choices_categorization', 'pvcc')
->join('pvcc', 'products_variations_choices_values', 'pvcv', 'pvcv.id_label = pvcc.id_label')
->andWhere(Operator::equals(['id_product' => $ID]))
->orderBy('list_order', 'ASC');
foreach ($qb->execute() as $row) {
$labels[$row['id_label']][$row['id']] = $row['id'];
}
$combinations = [[]];
foreach ($labels as $id_label => $values) {
$arr = [];
foreach ($combinations as $combination) {
foreach ($values as $value) {
$arr[] = array_replace($combination, [$id_label => $value]);
}
}
$combinations = $arr;
}
foreach ($combinations as $combination) {
sqlQueryBuilder()->insert('products_variations')->directValues(['id_product' => $ID])->execute();
$id = sqlInsertID();
foreach ($combination as $id_label => $id_value) {
sqlQueryBuilder()->insert('products_variations_combination')->directValues([
'id_variation' => $id,
'id_label' => $id_label,
'id_value' => $id_value,
])->execute();
}
Variations::updateTitle($id);
}
$count = count($combinations);
addActivityLog(ActivityLog::SEVERITY_SUCCESS, ActivityLog::TYPE_CHANGE,
sprintf(translate('activityEdited', 'products'), $this->getID(),
"Akce [Vygenerovat všechny možné varianty] byla provedena. Úspěšně vygenerovaných variant: {$count}.")
);
$this->returnOK();
}
$data = getVal('data');
$variations_data = getVal('variations', $data, []);
$OK = false;
foreach ($variations_data as $id => $var) {
$var['id_product'] = $ID;
if (!empty($var['delete']) || !$id) {
if ($id > 0) {
$this->deleteVariation($var);
}
continue;
}
$var['price'] = $this->preparePrice($var['price']);
if ($var['priceWithVat'] == 'Y') {
$var['price'] = $var['price'] / (1 + ($PRODUCT['vat'] / 100));
}
if ($var['priceWithVat'] == 'F') {
$var['price'] = $var['price'] / (1 + ($PRODUCT['vat'] / 100)) / (1 - ($PRODUCT['discount'] / 100));
}
if (isset($var['code']) && empty($var['code'])) {
$var['code'] = null;
}
if (!empty($var['weight'])) {
$var['weight'] = str_replace(',', '.', $var['weight']);
}
if (!empty($var['measure_quantity'])) {
$var['measure_quantity'] = str_replace(',', '.', $var['measure_quantity']);
}
foreach (['width', 'height', 'depth'] as $dimension) {
$value = mb_strlen($var[$dimension] ?? '') > 0 ? $var[$dimension] : null;
$var[$dimension] = isset($value) ? $this->preparePrice($value) : null;
}
if (findModule(Modules::BONUS_PROGRAM) && $var['bonus_points'] == '') {
$var['bonus_points'] = null;
}
$var['in_store'] = str_replace(',', '.', $var['in_store']);
if (!getVal('inStoreOverride', $var)) {
unset($var['in_store']);
}
if (isset($var['in_store_min'])) {
$this->prepareNull($var['in_store_min']);
}
$dbVariation = $this->fetchObject('products_variations', $var['id']);
$this->unserializeCustomData($dbVariation);
$var['data'] = array_merge($dbVariation['data'], $var['data'] ?? []);
$this->serializeCustomData($var);
try {
if ($id < 0) {
$this->setVariation($var);
} else {
$this->updateVariation($var);
// zalogovat, ze byla rucne zmenena skladovost
if (($var['in_store'] ?? false) !== false) {
addActivityLog(
ActivityLog::SEVERITY_NOTICE,
ActivityLog::TYPE_CHANGE,
sprintf(
translate('activityEdited', 'products'),
$this->getID(),
sprintf(translate('activityVariationInStoreEdited', 'products'), $dbVariation['title'], $dbVariation['in_store'] ?? 0, $var['in_store'])
)
);
}
}
// pokud na variante se nastavila cena (napr. kvuli zlevneni konkretni varianty)
// a CPS je prazdna (protoze byla prazdna cena = dedila se z produktu),
// nastavit CPS varianty = CPS produktu
if (findModule(Modules::PRICE_HISTORY)
&& empty($dbVariation['price_for_discount'])
&& !empty($var['price'])) {
sqlQueryBuilder()->update('products_variations', 'pv')
->join('pv', 'products', 'p', 'p.id = pv.id_product')
->set('pv.price_for_discount', 'IF(p.price_for_discount > 0, p.price_for_discount, null)')
->where(Operator::equals(['pv.id' => $var['id']]))
->execute();
}
} catch (Exception $e) {
switch (intval($e->getPrevious()->errorInfo[1])) {
case 1062:
$ErrStr = 'Chyba při ukládání: Duplicitní ';
$badFields = $this->getBadValues($var, ['id' => $this->ID]);
foreach ($badFields as $value) {
$ErrStr .= '{'.$value.'} ';
}
if (!$badFields) {
$ErrStr = "Duplicitní záznam: \n".$e->getMessage();
}
$this->addError($ErrStr);
break;
default:
throw $e;
}
}
$OK = true;
}
if (isset($_REQUEST['submitCopy']) && is_numeric($_REQUEST['ID']) && is_numeric($_REQUEST['productID'])) {
Variations::duplicateVariations(intval($_REQUEST['productID']), intval($_REQUEST['ID']));
}
// ############################################
// # Aktualizace fulltextového vyhledávání
if (findModule(\Modules::SEARCH) && getVal('submitUpdateVariation') && !findModule(Modules::KAFKA)) {
$fulltext = ServiceContainer::getService(FulltextInterface::class);
$fulltext->updateProduct($ID);
}
if ($OK && empty($this->getErrors()) && empty($this->getHTMLErrors())) {
$this->returnOK();
}
}
public function getBadValues($variation, $rowIdentifier = [])
{
$uniques = ['ean' => true];
if (findModule(Modules::PRODUCTS_VARIATIONS, Modules::SUB_CODE)) {
$uniques['code'] = true;
}
$where = '';
foreach ($rowIdentifier as $key => $value) {
$where .= " AND {$key}!=:{$key}";
}
$data = $variation;
$badFields = [];
foreach ($uniques as $key => $value) {
if ($value) {
if (isset($data[$key])) {
$SQL = returnSQLResult('SELECT COUNT(*) FROM '.getTableName('products_variations')." WHERE {$key}=:{$key} {$where}", array_merge($data, $rowIdentifier));
if ($SQL > 0) {
$badFields[] = $key;
}
}
}
}
return $badFields;
}
public function deleteVariation($variation)
{
$this->deleteSQL('products_variations', ['id' => $variation['id']]);
$prod = new Product(intval($_REQUEST['ID']));
$prod->updateInStore();
$prod->updateDeliveryTime();
// ############################################
// # Aktualizace fulltextového vyhledávání
if (findModule(\Modules::SEARCH) && !findModule(Modules::KAFKA)) {
$fulltext = ServiceContainer::getService(FulltextInterface::class);
$fulltext->updateProduct(getVal('ID'));
}
}
public function updateVariation($variation)
{
global $cfg;
$ID = $this->getID();
if ($variation['delivery_time'] == '1') {
$variation['delivery_time'] = intval($variation['delivery_time_days']);
}
if ($variation['price'] <= 0) {
$variation['price'] = null;
if (findModule(Modules::PRICE_HISTORY)) {
$variation['price_for_discount'] = null;
}
}
if ($variation['ean'] <= 0) {
$variation['ean'] = null;
}
if (isset($variation['weight'])) {
$this->prepareNull($variation['weight']);
}
if (isset($variation['measure_quantity'])) {
$this->prepareNull($variation['measure_quantity']);
}
if (isset($variation['price_buy'])) {
$variation['price_buy'] = (empty($variation['price_buy']['value']) ? null : $this->prepareVatPrice($variation['price_buy']));
}
if (isset($variation['price_common'])) {
// if empty string or negative number, set null
$variation['price_common'] = trim($variation['price_common']);
$variation['price_common'] = $this->preparePrice($variation['price_common']);
if ($variation['price_common'] < 0) {
$variation['price_common'] = null;
}
}
if (!empty($cfg['Modules']['products_variations']['variationCode'])) {
$this->prepareNull($variation['code']);
if ($variation['code']) {
$uniqueCode = Variations::makeCodeUnique($variation['id'], $variation['code']);
if ($uniqueCode != $variation['code']) {
$this->addError("Kód {$variation['code']} není unikátní a byl změněn na {$uniqueCode}!");
$variation['code'] = $uniqueCode;
}
}
}
if (!empty($variation['ean'])) {
if (Settings::getDefault()->admin_ean_check === 'Y'
&& !EANValidator::checkEAN($variation['ean'])
) {
$this->addHTMLError("EAN <strong>{$variation['ean']}</strong> není validní (kód varianty: <strong>{$variation['code']}</strong>).");
}
$exists = Variations::eanExists($variation['id'], $variation['ean']);
if ($exists) {
$message = Variations::eanExistsMessage($exists);
$this->addHTMLError($message); // EAN {ean} není unikátní. {odkaz na duplicitní produkt}
$variation['ean'] = null;
}
}
$this->updateSQL('products_variations', $variation, ['id' => $variation['id']], ['delete', 'changed', 'priceWithVat', 'delivery_time_days', 'inStoreOverride', 'combination', 'translation_figure', 'translation_figure_checkbox']);
$combinations = $variation['combination'];
if ($combinations) {
$this->deleteSQL('products_variations_combination', ['id_variation' => $variation['id']]);
foreach ($combinations as $id_label => $id_value) {
if (empty($id_value)) {
continue;
}
$this->insertSQL('products_variations_combination', [
'id_variation' => intval($variation['id']),
'id_label' => intval($id_label),
'id_value' => intval($id_value),
]);
}
}
$this->updateVariationTranslationsFigure((int) $variation['id'], $variation);
Variations::updateTitle($variation['id']);
$prod = new Product($ID);
$prod->updateInStore();
$prod->updateDeliveryTime();
}
public function setVariation(&$variation)
{
global $cfg;
$ID = $this->getID();
if ($variation['delivery_time'] == '1') {
$variation['delivery_time'] = intval($variation['delivery_time_days']);
}
if ($variation['price'] <= 0) {
$variation['price'] = null;
}
if ($variation['ean'] <= 0) {
$variation['ean'] = null;
}
// Prepare value
$variation['id_product'] = $ID;
if (!empty($cfg['Modules']['products_variations']['variationCode'])) {
$this->prepareNull($variation['code']);
if ($variation['code']) {
$uniqueCode = Variations::makeCodeUnique(0, $variation['code']);
if ($uniqueCode != $variation['code']) {
$this->addError("Kód {$variation['code']} není unikátní a byl změněn na {$uniqueCode}!");
$variation['code'] = $uniqueCode;
}
}
}
if (isset($variation['weight'])) {
$this->prepareNull($variation['weight']);
}
if (isset($variation['measure_quantity'])) {
$this->prepareNull($variation['measure_quantity']);
}
$this->insertSQL('products_variations', $variation, ['delete', 'priceWithVat', 'combination', 'changed', 'delivery_time_days', 'inStoreOverride', 'translation_figure', 'translation_figure_checkbox']);
$id = sqlInsertID();
$variation['id'] = $id;
$combinations = $variation['combination'];
foreach ($combinations as $id_label => $id_value) {
$this->insertSQL('products_variations_combination', [
'id_variation' => intval($id),
'id_label' => intval($id_label),
'id_value' => intval($id_value),
]);
}
$this->updateVariationTranslationsFigure((int) $id, $variation);
$prod = new Product($ID);
$prod->updateInStore();
$prod->updateDeliveryTime();
Variations::updateTitle($id);
}
public function deserializeVariationsCustomData(&$variations)
{
foreach ($variations as $key => $value) {
$this->unserializeCustomData($variations[$key]);
}
}
protected function updateVariationTranslationsFigure(int $variationId, array $variation): void
{
if (!findModule(Modules::TRANSLATIONS)) {
return;
}
$this->getTranslationUtil()->updateTranslationsFigure(
VariationsTranslation::class,
$variationId,
$variation['translation_figure'] ?? []
);
}
protected function getVariationTranslationsFigure(array $variationIds): array
{
if (!findModule(Modules::TRANSLATIONS)) {
return [];
}
return $this->getTranslationUtil()->getTranslationsFigure(
VariationsTranslation::class,
$variationIds
);
}
}
$main_class = 'ProductsVariations';