@@ -49,6 +49,7 @@ class ProductFamilyController extends AdminController | |||
'label' => 'TVA', | |||
'choices' => $choicesTaxRate, | |||
'mapped' => false, | |||
'data'=> 0, | |||
'choice_attr' => function ($choice, $key, $value) { | |||
return ['data-tax-rate-value' => $this->choicesTaxRateParam[$choice]]; | |||
}, | |||
@@ -90,9 +91,10 @@ class ProductFamilyController extends AdminController | |||
$formBuilder->add('behaviorCountStock', ChoiceType::class, array( | |||
'label' => 'Stock', | |||
'empty_data' => 'by-product-family', | |||
'choices' => array( | |||
'Gèrer le stock par déclinaison' => 'by-product', | |||
'Gèrer le stock par produit' => 'by-product-family' | |||
'Gèrer le stock par produit' => 'by-product-family', | |||
'Gèrer le stock par déclinaison' => 'by-product' | |||
), | |||
'multiple' => false, | |||
'expanded' => true | |||
@@ -143,9 +145,9 @@ class ProductFamilyController extends AdminController | |||
protected function processProducts($entity) | |||
{ | |||
//si il existe un et un seul produit pour ce product family n'ajoute rien supprime rien | |||
if(count($entity->getProducts()) == 0) { | |||
$product = new Product(); | |||
$product->setTitle($entity->getTitle()) ; | |||
$product->setCreatedBy($this->getUser()) ; | |||
$product->setUpdatedBy($this->getUser()) ; | |||
$product->setProductFamily($entity) ; | |||
@@ -154,10 +156,15 @@ class ProductFamilyController extends AdminController | |||
$entity->addProduct($product) ; | |||
} | |||
else { | |||
foreach($entity->getProducts() as $product) { | |||
$product->setProductFamily($entity) ; | |||
$product->setCreatedBy($this->getUser()) ; | |||
$product->setUpdatedBy($this->getUser()) ; | |||
$this->em->persist($product); | |||
$entity->addProduct($product) ; | |||
// die('ncici'); | |||
} | |||
} | |||
} | |||
@@ -211,7 +218,11 @@ class ProductFamilyController extends AdminController | |||
$editForm = $this->executeDynamicMethod('create<EntityName>EditForm', [$entity, $fields]); | |||
$deleteForm = $this->createDeleteForm($this->entity['name'], $id); | |||
$sortableProductsField = array(); | |||
foreach($editForm->get('products')->getData() as $k=>$product){ | |||
$sortableProductsField[$product->getPosition()] = $k; | |||
} | |||
ksort($sortableProductsField); | |||
$editForm->handleRequest($this->request); | |||
if ($editForm->isSubmitted() && $editForm->isValid()) { | |||
@@ -234,7 +245,8 @@ class ProductFamilyController extends AdminController | |||
'entity_fields' => $fields, | |||
'entity' => $entity, | |||
'delete_form' => $deleteForm->createView(), | |||
'categories' => $categories | |||
'categories' => $categories, | |||
'sortableProductsField' => $sortableProductsField | |||
]; | |||
return $this->executeDynamicMethod('render<EntityName>Template', ['edit', $this->entity['templates']['edit'], $parameters]); | |||
@@ -269,7 +281,7 @@ class ProductFamilyController extends AdminController | |||
$this->dispatch(EasyAdminEvents::POST_NEW, [ | |||
'entity_fields' => $fields, | |||
'form' => $newForm, | |||
'entity' => $entity, | |||
'entity' => $entity | |||
]); | |||
@@ -281,6 +293,7 @@ class ProductFamilyController extends AdminController | |||
'entity_fields' => $fields, | |||
'entity' => $entity, | |||
'categories' => $categories, | |||
'sortableProductsField' => array() | |||
]; | |||
return $this->executeDynamicMethod('render<EntityName>Template', ['new', $this->entity['templates']['new'], $parameters]); |
@@ -8,11 +8,13 @@ use FOS\CKEditorBundle\Form\Type\CKEditorType; | |||
use Lc\ShopBundle\Context\ProductFamilyInterface; | |||
use Lc\ShopBundle\Context\ProductInterface; | |||
use Lc\ShopBundle\Context\TaxRateInterface; | |||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | |||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | |||
use Symfony\Component\Form\Extension\Core\Type\HiddenType; | |||
use Symfony\Component\Form\Extension\Core\Type\NumberType; | |||
use function PHPSTORM_META\type; | |||
use Symfony\Component\Form\AbstractType; | |||
use Symfony\Component\Form\Extension\Core\Type\TextType; | |||
use Symfony\Component\Form\FormBuilderInterface; | |||
use Symfony\Component\OptionsResolver\OptionsResolver; | |||
@@ -27,10 +29,22 @@ class ProductType extends AbstractType | |||
public function buildForm(FormBuilderInterface $builder, array $options) | |||
{ | |||
$builder->add('title') ; | |||
$builder->add('titleInherited', CheckboxType::class, array( | |||
'mapped'=>false, | |||
'required'=>false | |||
)); | |||
$builder->add('title', TextType::class, array( | |||
"required" => false | |||
)) ; | |||
$builder->add('unitInherited', CheckboxType::class, array( | |||
'mapped'=>false, | |||
'required'=>false | |||
)); | |||
$builder->add('unit', ChoiceType::class, array( | |||
'label' => 'Unité', | |||
"required" => false, | |||
'choices' => array( | |||
'pièce' => 'piece', | |||
'g' => 'g', | |||
@@ -40,20 +54,35 @@ class ProductType extends AbstractType | |||
), | |||
)); | |||
$builder->add('weightInherited', CheckboxType::class, array( | |||
'mapped'=>false, | |||
'required'=>false | |||
)); | |||
$builder->add('weight', NumberType::class, array( | |||
'label' => 'Poids', | |||
"required" => false, | |||
'attr' => [ | |||
'append_html' => 'g' | |||
] | |||
)); | |||
$builder->add('stepInherited', CheckboxType::class, array( | |||
'mapped'=>false, | |||
'required'=>false | |||
)); | |||
$builder->add('step', NumberType::class, array( | |||
'label' => 'Pas', | |||
"required" => false, | |||
'help' => 'Quantité à incrémenter / décrémenter lors des mouvements de quantité', | |||
)); | |||
$builder->add('priceInherited', CheckboxType::class, array( | |||
'mapped'=>false, | |||
'required'=>false | |||
)); | |||
$builder->add('price', NumberType::class, array( | |||
'label' => 'Prix', | |||
"required" => false | |||
)); | |||
$builder->add('priceWithTax', NumberType::class, array( | |||
@@ -67,6 +96,8 @@ class ProductType extends AbstractType | |||
'required' => false, | |||
)); | |||
$builder->add('position', HiddenType::class); | |||
} | |||
public function configureOptions(OptionsResolver $resolver) |
@@ -3,12 +3,14 @@ | |||
namespace Lc\ShopBundle\Model; | |||
use Doctrine\ORM\Mapping as ORM; | |||
use Lc\ShopBundle\Context\SortableInterface; | |||
/** | |||
* @ORM\MappedSuperclass() | |||
*/ | |||
abstract class Product extends AbstractEntity | |||
abstract class Product extends AbstractEntity implements SortableInterface | |||
{ | |||
use SortableTrait; | |||
/** | |||
* @ORM\ManyToOne(targetEntity="Lc\ShopBundle\Context\ProductFamilyInterface", inversedBy="products") | |||
* @ORM\JoinColumn(nullable=false) | |||
@@ -16,7 +18,7 @@ abstract class Product extends AbstractEntity | |||
protected $productFamily; | |||
/** | |||
* @ORM\Column(type="string", length=255) | |||
* @ORM\Column(type="string", length=255, nullable=true) | |||
*/ | |||
protected $title; | |||
@@ -74,6 +74,12 @@ abstract class ProductFamily extends AbstractDocumentEntity | |||
*/ | |||
protected $behaviorCountStock; | |||
/** | |||
* @ORM\Column(type="boolean") | |||
*/ | |||
protected $activeProducts; | |||
public function __construct() | |||
{ | |||
$this->productCategories = new ArrayCollection(); | |||
@@ -261,4 +267,16 @@ abstract class ProductFamily extends AbstractDocumentEntity | |||
return $this; | |||
} | |||
public function getActiveProducts(): ?bool | |||
{ | |||
return $this->activeProducts; | |||
} | |||
public function setActiveProducts(bool $activeProducts): self | |||
{ | |||
$this->activeProducts = $activeProducts; | |||
return $this; | |||
} | |||
} |
@@ -14,7 +14,8 @@ easy_admin: | |||
- '/bundles/lcshop/js/backend/script/setup-ckfinder.js' | |||
- '/bundles/lcshop/js/backend/script/utils.js' | |||
- '/bundles/lcshop/js/backend/script/custom.js' | |||
- '/bundles/lcshop/js/backend/script/vueapp.js' | |||
- '/bundles/lcshop/js/backend/script/vuejs-mixins.js' | |||
- '/bundles/lcshop/js/backend/script/vuejs-product-family.js' | |||
css: | |||
- '/bundles/lcshop/css/backend/custom.css' | |||
@@ -77,10 +77,8 @@ | |||
text-align: right ; | |||
} | |||
.new-productfamily .btn-remove-product, | |||
.edit-productfamily .btn-remove-product { | |||
position: absolute ; | |||
top: 30px ; | |||
right: 20px ; | |||
} | |||
/* Product */ | |||
.product-form-modal{display: none;} | |||
.product-form.modal .form-check-label{font-style: italic; color: #666; text-align: left;} | |||
.products-collection-table .inherited{color: #888; font-style: italic; font-weight: initial;} | |||
.products-collection-table td{} |
@@ -2,6 +2,8 @@ jQuery(document).ready(function () { | |||
custom_switch_merchants(); | |||
initLcSortableList(); | |||
initLcCkEditor(); | |||
initLcSortableProductsList(); | |||
initLcSortableList(); | |||
}); | |||
function custom_switch_merchants() { | |||
@@ -10,6 +12,20 @@ function custom_switch_merchants() { | |||
}); | |||
} | |||
function initLcSortableProductsList(){ | |||
$('.lc-sortable-products tbody').sortable({ | |||
placeholder: "ui-state-highlight" | |||
}); | |||
$('.lc-sortable-products tbody').on("sortupdate", function (event, ui) { | |||
updateSortableProducts(); | |||
}); | |||
} | |||
function updateSortableProducts(){ | |||
$('.lc-sortable-products tr.lc-draggable').each(function (index, li) { | |||
$(li).find('.field-position').val(index); | |||
}); | |||
} | |||
function initLcSortableList() { | |||
$('.lc-sortable tbody').sortable({ |
@@ -1,147 +0,0 @@ | |||
// Reference array sent to dynamic staticRenderFns | |||
var staticRenderFns = []; | |||
Vue.component('component-unit', { | |||
props: ['template', 'keyForm', 'taxRateValue'], | |||
data() { | |||
return { | |||
templateRender: null, | |||
unit: null, | |||
price: null, | |||
priceWithTax: null | |||
}; | |||
}, | |||
mounted: function() { | |||
this.unit = $('#value-unit-'+this.keyForm).val() ; | |||
this.price = parseFloat($('#value-price-'+this.keyForm).val()).toFixed(3) ; | |||
this.priceUpdate('priceWithTax') ; | |||
}, | |||
methods: { | |||
getUnitReference: function () { | |||
if (this.unit == 'g') { | |||
return 'kg'; | |||
} else if (this.unit == 'ml') { | |||
return 'L'; | |||
} else { | |||
return this.unit; | |||
} | |||
}, | |||
changeTaxRate: function() { | |||
this.$emit('tax-rate-change') ; | |||
this.changePriceWithTax() ; | |||
}, | |||
changePrice: function() { | |||
this.priceUpdate('price') ; | |||
}, | |||
changePriceWithTax: function() { | |||
this.priceUpdate('priceWithTax') ; | |||
}, | |||
priceUpdate: function(priceType) { | |||
var taxRate = this.getTaxRate() ; | |||
if(priceType == 'priceWithTax') { | |||
this.price = parseFloat(this.price.replace(',','.')).toFixed(3) ; | |||
this.priceWithTax = getPriceWithTax(this.price, taxRate); | |||
} | |||
else { | |||
this.priceWithTax = parseFloat(this.priceWithTax.replace(',','.')).toFixed(2) ; | |||
this.price = getPrice(this.priceWithTax, taxRate) ; | |||
} | |||
}, | |||
getTaxRate: function() { | |||
var taxRate = this.taxRateValue ; | |||
if(this.taxRateValue == -1) { | |||
var taxRate = $('#productfamily_taxRate').find('option:selected').data('tax-rate-value'); | |||
if(typeof taxRate == 'undefined') { | |||
taxRate = 0 ; | |||
} | |||
} | |||
return taxRate ; | |||
} | |||
}, | |||
render(h) { | |||
if (!this.templateRender) { | |||
return h('div', 'loading...'); | |||
} else { // If there is a template, I'll show it | |||
return this.templateRender(); | |||
} | |||
}, | |||
watch: { | |||
taxRateValue: function(newVal, oldVal) { | |||
this.changePriceWithTax() ; | |||
}, | |||
// Every time the template prop changes, I recompile it to update the DOM | |||
template: { | |||
immediate: true, // makes the watcher fire on first render, too. | |||
handler() { | |||
if (this.template) { | |||
var res = Vue.compile(this.template); | |||
this.templateRender = res.render; | |||
// staticRenderFns belong into $options, | |||
// appearantly | |||
this.$options.staticRenderFns = [] | |||
// clean the cache of static elements | |||
// this is a cache with the results from the staticRenderFns | |||
this._staticTrees = [] | |||
// Fill it with the new staticRenderFns | |||
for (var i in res.staticRenderFns) { | |||
//staticRenderFns.push(res.staticRenderFns[i]); | |||
this.$options.staticRenderFns.push(res.staticRenderFns[i]) | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}); | |||
appProductFamily = new Vue({ | |||
el: '#lc-product-family-edit', | |||
delimiters: ['${', '}'], | |||
data: { | |||
indexFormProduct: 0, | |||
taxRateValue: -1, | |||
formProductArray: [], | |||
currentSection: 'general', | |||
sectionsArray: [ | |||
{ | |||
name: 'general', | |||
nameDisplay: 'Général' | |||
}, | |||
{ | |||
name: 'price', | |||
nameDisplay: 'Prix / stock' | |||
}, | |||
{ | |||
name: 'products', | |||
nameDisplay: 'Déclinaisons' | |||
} | |||
] | |||
}, | |||
methods: { | |||
changeSection: function (section) { | |||
this.currentSection = section.name; | |||
}, | |||
setTaxRateValue: function() { | |||
this.taxRateValue = $('#productfamily_taxRate').find('option:selected').data('tax-rate-value') ; | |||
}, | |||
addFormProduct: function() { | |||
var collectionHolder = $('ul.products'); | |||
var prototype = collectionHolder.data('prototype'); | |||
var newForm = prototype; | |||
newForm = newForm.replace(/__name__/g, this.indexFormProduct); | |||
this.formProductArray.push(newForm) ; | |||
this.indexFormProduct ++ ; | |||
}, | |||
deleteFormProduct: function(key) { | |||
this.formProductArray.splice(key, 1) ; | |||
} | |||
}, | |||
mounted() { | |||
} | |||
}); |
@@ -0,0 +1,117 @@ | |||
let mixinPriceProduct = { | |||
data() { | |||
return { | |||
price: null, | |||
priceWithTax: null | |||
}; | |||
}, | |||
methods: { | |||
changePrice: function () { | |||
this.priceUpdate('price'); | |||
}, | |||
changePriceWithTax: function () { | |||
this.priceUpdate('priceWithTax'); | |||
}, | |||
priceUpdate: function (priceType) { | |||
if (priceType == 'priceWithTax' && this.price) { | |||
this.price = parseFloat(this.price.replace(',', '.')).toFixed(3); | |||
this.priceWithTax = getPriceWithTax(this.price, this.productFamily.taxRateValue ); | |||
} else if (this.priceWithTax){ | |||
this.priceWithTax = parseFloat(this.priceWithTax.replace(',', '.')).toFixed(2); | |||
this.price = getPrice(this.priceWithTax, this.productFamily.taxRateValue ); | |||
} | |||
} | |||
} | |||
}; | |||
let mixinPriceWithTaxField = { | |||
data() { | |||
return { | |||
price: null, | |||
priceWithTax: null, | |||
taxRate:null, | |||
taxRateValue:null | |||
}; | |||
}, | |||
mounted: function () { | |||
this.setTaxRateValue(); | |||
}, | |||
methods: { | |||
/*changeTaxRate: function () { | |||
this.$emit('tax-rate-change'); | |||
this.changePriceWithTax(); | |||
},*/ | |||
changePrice: function () { | |||
this.priceUpdate('price'); | |||
}, | |||
changePriceWithTax: function () { | |||
this.priceUpdate('priceWithTax'); | |||
}, | |||
priceUpdate: function (priceType) { | |||
if (priceType == 'priceWithTax' && this.price) { | |||
this.price = parseFloat(this.price.replace(',', '.')).toFixed(3); | |||
this.priceWithTax = getPriceWithTax(this.price, this.taxRateValue ); | |||
} else if (this.priceWithTax){ | |||
this.priceWithTax = parseFloat(this.priceWithTax.replace(',', '.')).toFixed(2); | |||
this.price = getPrice(this.priceWithTax, this.taxRateValue ); | |||
} | |||
}, | |||
setTaxRateValue: function () { | |||
this.taxRateValue = $('#productfamily_taxRate').find('option:selected').data('tax-rate-value'); | |||
} | |||
}, | |||
watch:{ | |||
taxRate: function (){ | |||
this.setTaxRateValue(); | |||
this.changePriceWithTax(); | |||
} | |||
} | |||
}; | |||
let mixinTemplate = { | |||
data() { | |||
return { | |||
templateRender: null, | |||
} | |||
}, | |||
render(h) { | |||
if (!this.templateRender) { | |||
return h('div', 'loading...'); | |||
} else { // If there is a template, I'll show it | |||
return this.templateRender(); | |||
} | |||
}, | |||
watch: { | |||
// Every time the template prop changes, I recompile it to update the DOM | |||
template: { | |||
immediate: true, // makes the watcher fire on first render, too. | |||
handler() { | |||
if (this.template) { | |||
var res = Vue.compile(this.template); | |||
this.templateRender = res.render; | |||
// staticRenderFns belong into $options, | |||
// appearantly | |||
this.$options.staticRenderFns = [] | |||
// clean the cache of static elements | |||
// this is a cache with the results from the staticRenderFns | |||
this._staticTrees = [] | |||
// Fill it with the new staticRenderFns | |||
for (var i in res.staticRenderFns) { | |||
//staticRenderFns.push(res.staticRenderFns[i]); | |||
this.$options.staticRenderFns.push(res.staticRenderFns[i]) | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
@@ -0,0 +1,284 @@ | |||
// Reference array sent to dynamic staticRenderFns | |||
var staticRenderFns = []; | |||
Vue.component('product-unit-price', { | |||
mixins: [mixinPriceWithTaxField, mixinTemplate], | |||
props: ['template', 'keyForm'], | |||
data() { | |||
return Object.assign( | |||
{ | |||
step: null, | |||
unit: null, | |||
weight: null | |||
}, window.productUnitPriceValues); | |||
}, | |||
methods: { | |||
getUnitReference: function () { | |||
if (this.unit == 'g') { | |||
return 'kg'; | |||
} else if (this.unit == 'ml') { | |||
return 'L'; | |||
} else { | |||
return this.unit; | |||
} | |||
} | |||
}, | |||
mounted: function () { | |||
this.priceUpdate('priceWithTax'); | |||
}, | |||
watch: { | |||
/*log($('.product-form.modal .select-unit')); | |||
$('.product-form.modal .select-unit').find('option:selected').each(function(i,elm){ | |||
$(this).prop("selected", false); | |||
}); | |||
log($('.product-form.modal .select-unit'));*/ | |||
} | |||
}); | |||
Vue.component('product-form', { | |||
mixins: [mixinPriceProduct, mixinTemplate], | |||
props: ['template', 'keyForm', 'productFamily'], | |||
data() { | |||
return Object.assign( | |||
{ | |||
title: null, | |||
titleInherited: null, | |||
fieldToUpdate: ['title', 'unit', 'weight', 'step', 'price'], | |||
unit: null, | |||
unitInherited: null, | |||
step: null, | |||
stepInherited: null, | |||
weight: null, | |||
weightInherited: null, | |||
price: null, | |||
priceInherited: null, | |||
priceWithTax: null, | |||
stock: null, | |||
availableQuantity: null | |||
}, window.productForm[this.keyForm]) | |||
}, | |||
mounted: function () { | |||
//INIT VAR | |||
updateSortableProducts(); | |||
//METHOD | |||
this.updateProductForm(); | |||
this.updateProductView(); | |||
}, | |||
methods: { | |||
getUnitReference: function () { | |||
if (this.productFamily.unit == 'g') { | |||
return 'kg'; | |||
} else if (this.productFamily.unit == 'ml') { | |||
return 'L'; | |||
} else { | |||
return this.productFamily.unit; | |||
} | |||
}, | |||
updateProductForm: function () { | |||
this.changePriceWithTax(); | |||
$('#productfamily_products_' + this.keyForm + '_unit').find('option').hide(); | |||
log('fefe'); | |||
switch (this.productFamily.unit) { | |||
case 'kg' : | |||
case 'g' : | |||
$('.products-collection-table').find('th.step, td.step').show(); | |||
$('.products-collection-table').find('th.weight, td.weight').hide(); | |||
$('#productfamily_products_' + this.keyForm + '_unit').find('option[value="g"]').show(); | |||
$('#productfamily_products_' + this.keyForm + '_unit').find('option[value="kg"]').show(); | |||
break; | |||
case 'ml': | |||
case 'L' : | |||
$('.products-collection-table').find('th.step, td.step').show(); | |||
$('.products-collection-table').find('th.weight, td.weight').hide(); | |||
$('#productfamily_products_' + this.keyForm + '_unit').find('option[value="ml"]').show(); | |||
$('#productfamily_products_' + this.keyForm + '_unit').find('option[value="L"]').show(); | |||
break; | |||
case 'piece': | |||
$('.products-collection-table').find('th.step, td.step').hide(); | |||
$('.products-collection-table').find('th.weight, td.weight').show(); | |||
$('#productfamily_products_' + this.keyForm + '_unit').find('option[value="piece"]').show(); | |||
break; | |||
} | |||
}, | |||
updateProductView: function () { | |||
//NE PAS SUPPRIMER | |||
/* for (i = 0; i < this.fieldToUpdate.length; i++) { | |||
value = this[this.fieldToUpdate[i]]; | |||
inherited = this[this.fieldToUpdate[i]+'Inherited']; | |||
inheritedValue = this['productFamily'][this.fieldToUpdate[i]]; | |||
if (value) { | |||
this[this.fieldToUpdate[i]+'Inherited'] = false; | |||
$('#product-item-' + this.keyForm).find('.'+this.fieldToUpdate[i]).html(value); | |||
} else { | |||
this[this.fieldToUpdate[i]+'Inherited'] = true; | |||
$('#product-item-' + this.keyForm).find('.'+this.fieldToUpdate[i]).html('<span class="inherited">' + inheritedValue + '</span>'); | |||
} | |||
}*/ | |||
if (this.title) { | |||
this.titleInherited = false; | |||
$('#product-item-' + this.keyForm).find('.title').html(this.title); | |||
} else { | |||
this.titleInherited = true; | |||
$('#product-item-' + this.keyForm).find('.title').html('<span class="inherited">' + this.productFamily.title + '</span>'); | |||
} | |||
if (this.unit) { | |||
this.unitInherited = false; | |||
$('#product-item-' + this.keyForm).find('.unit').html(this.unit); | |||
} else { | |||
this.unitInherited = true; | |||
$('#product-item-' + this.keyForm).find('.unit').html('<span class="inherited">' + this.productFamily.unit + '</span>'); | |||
} | |||
if (this.step) { | |||
this.stepInherited = false; | |||
$('#product-item-' + this.keyForm).find('.step').html(this.step + ' / '+this.unit); | |||
} else { | |||
this.stepInherited = true; | |||
$('#product-item-' + this.keyForm).find('.step').html('<span class="inherited">' + this.productFamily.step + ' / '+ this.productFamily.unit+'</span>'); | |||
} | |||
if (this.weight) { | |||
this.weightInherited = false; | |||
$('#product-item-' + this.keyForm).find('.weight').html(this.weight + ' / '+this.getUnitReference()); | |||
} else { | |||
this.weightInherited = true; | |||
$('#product-item-' + this.keyForm).find('.weight').html('<span class="inherited">' + this.productFamily.weight + ' / '+this.getUnitReference()+'</span>'); | |||
} | |||
if (this.price) { | |||
this.priceInherited = false; | |||
$('#product-item-' + this.keyForm).find('.price').html(this.price + ' € HT / '+this.getUnitReference()); | |||
} else { | |||
this.priceInherited = true; | |||
$('#product-item-' + this.keyForm).find('.price').html('<span class="inherited">' + this.productFamily.price + ' € HT/ '+this.getUnitReference()+'</span>'); | |||
} | |||
log(this.productFamily.behaviorCountStock); | |||
if (this.productFamily.behaviorCountStock == 'by-product-family') { | |||
$('.products-collection-table').find('.stock').hide(); | |||
} else { | |||
$('.products-collection-table').find('.stock').show(); | |||
$('#product-item-' + this.keyForm).find('.stock').html(this.availableQuantity + ' / '+this.getUnitReference()); | |||
} | |||
}, | |||
saveProductForm: function () { | |||
this.updateProductView(); | |||
$('#form-product-modal-' + this.keyForm).modal('hide'); | |||
}, | |||
}, | |||
watch: { | |||
titleInherited: function (val) { | |||
if(val)this.title = null; | |||
}, | |||
unitInherited: function (val) { | |||
if(val)this.unit = null; | |||
}, | |||
weightInherited: function (val) { | |||
if(val)this.weight = null; | |||
}, | |||
stepInherited: function (val) { | |||
if(val)this.step = null; | |||
}, | |||
priceInherited: function (val) { | |||
if(val)this.price = null; | |||
}, | |||
} | |||
}); | |||
appProductFamily = new Vue({ | |||
el: '#lc-product-family-edit', | |||
delimiters: ['${', '}'], | |||
computed: { | |||
productFamily: function () { | |||
return { | |||
'title': this.title, | |||
'behaviorCountStock': this.behaviorCountStock, | |||
'unit': this.$refs.productUnitPrice.unit, | |||
'step': this.$refs.productUnitPrice.step, | |||
'weight': this.$refs.productUnitPrice.weight, | |||
'price': this.$refs.productUnitPrice.price, | |||
'taxRate': this.$refs.productUnitPrice.taxRate, | |||
'taxRateValue': this.$refs.productUnitPrice.taxRateValue | |||
}; | |||
}, | |||
}, | |||
data() { | |||
return Object.assign( | |||
{ | |||
indexFormProduct: 0, | |||
title: null, | |||
activeProducts: false, | |||
behaviorCountStock: null, | |||
formProductArray: [], | |||
currentSection: 'general', | |||
sectionsArray: [ | |||
{ | |||
name: 'general', | |||
nameDisplay: 'Général' | |||
}, | |||
{ | |||
name: 'price', | |||
nameDisplay: 'Prix / stock' | |||
}, | |||
{ | |||
name: 'products', | |||
nameDisplay: 'Déclinaisons' | |||
} | |||
] | |||
}, window.appProductFamilyValues); | |||
}, | |||
methods: { | |||
changeSection: function (section) { | |||
this.updateChild(); | |||
this.currentSection = section.name; | |||
}, | |||
addProductForm: function () { | |||
var $collectionHolder = $('tbody.products-collection'); | |||
var prototype = $collectionHolder.data('prototype'); | |||
var newForm = prototype; | |||
newForm = newForm.replace(/__name__/g, this.indexFormProduct); | |||
this.formProductArray.push(newForm); | |||
this.indexFormProduct++; | |||
}, | |||
editProductForm: function (key) { | |||
$('#form-product-modal-' + key).modal(); | |||
this.updateChild(); | |||
}, | |||
deleteProductForm: function (key) { | |||
this.formProductArray.splice(key, 1); | |||
}, | |||
getUnitReference: function () { | |||
if (typeof this.$refs.productUnitPrice !== 'undefined') { | |||
return this.$refs.productUnitPrice.getUnitReference(); | |||
} | |||
}, | |||
updateChild: function () { | |||
if (typeof this.$refs.productForm !== 'undefined') { | |||
for (i = 0; i < this.$refs.productForm.length; i++) { | |||
this.$refs.productForm[i].updateProductForm(); | |||
this.$refs.productForm[i].updateProductView(); | |||
} | |||
} | |||
} | |||
}, | |||
watch: { | |||
title: function () { | |||
this.updateChild() | |||
}, | |||
} | |||
}); |
@@ -0,0 +1,198 @@ | |||
{% macro printProductRow(product) %} | |||
<div :id="'form-product-modal-'+ keyForm" class="product-form modal" tabindex="-1" role="dialog" | |||
data-backdrop="static"> | |||
<div class="modal-dialog" role="document"> | |||
<div class="modal-content"> | |||
<div class="modal-header"> | |||
<h5 class="modal-title">Déclinaison</h5> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="form-group row"> | |||
{{ form_label(product.title, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-10"> | |||
{{ form_widget(product.titleInherited, {'label': 'Utiliser la valeur par défaut : {{ productFamily.title }}','attr' : {'v-model' : 'titleInherited'}}) }} | |||
<div v-show="titleInherited == false"> | |||
{{ form_widget(product.title, {'attr' : {'v-model' : 'title'}}) }} | |||
{{ form_help(product.title) }} | |||
</div> | |||
</div> | |||
</div> | |||
<div class="form-group row"> | |||
{{ form_label(product.unit, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-10"> | |||
{{ form_widget(product.unitInherited, {'label': 'Utiliser la valeur par défaut : {{ productFamily.unit }}','attr' : {'v-model' : 'unitInherited'}}) }} | |||
<div v-show="unitInherited == false"> | |||
{{ form_widget(product.unit, {"attr":{'v-model': 'unit', 'class' : 'select-unit'}}) }} | |||
{{ form_help(product.unit) }} | |||
</div> | |||
</div> | |||
</div> | |||
<div v-if="unit == 'piece'" class="form-group row field-weight"> | |||
{{ form_label(product.weight, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-10"> | |||
{{ form_widget(product.weightInherited, {'label': 'Utiliser la valeur par défaut : {{ productFamily.weight }}','attr' : {'v-model' : 'weightInherited'}}) }} | |||
<div v-show="weightInherited == false" class="input-group"> | |||
{{ form_widget(product.weight, {'attr' : {'v-model' : 'weight'}}) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">g</span> | |||
</div> | |||
</div> | |||
{{ form_help(product.weight) }} | |||
</div> | |||
</div> | |||
<div v-else class="form-group row field-step"> | |||
{{ form_label(product.step, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-8"> | |||
{{ form_widget(product.stepInherited, {'label': 'Utiliser la valeur par défaut : {{ productFamily.step }}','attr' : {'v-model' : 'stepInherited'}}) }} | |||
<div v-show="stepInherited == false" class="input-group"> | |||
{{ form_widget(product.step, {'attr' : {'v-model' : 'step'}}) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">{% verbatim %}{{ unit ? unit : productFamily.unit }}{% endverbatim %}</span> | |||
</div> | |||
</div> | |||
{{ form_help(product.step) }} | |||
</div> | |||
</div> | |||
<div class="form-group row field-price"> | |||
{{ form_label(product.price, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-10"> | |||
{{ form_widget(product.priceInherited, {'label': 'Utiliser la valeur par défaut : {{ productFamily.price }}','attr' : {'v-model' : 'priceInherited'}}) }} | |||
<div v-show="priceInherited == false" class="input-group"> | |||
{{ form_widget(product.price, {'attr' : {'v-model': 'price', '@change' : 'changePriceWithTax'}}) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">€ HT / {% verbatim %}{{ getUnitReference() }}{% endverbatim %}</span> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="form-group row field-price"> | |||
<div class="col-2"> </div> | |||
<div class="form-widget col-10"> | |||
<div v-show="priceInherited == false" class="input-group"> | |||
{{ form_widget(product.priceWithTax, {'attr' : {'v-model': 'priceWithTax', '@change' : 'changePrice'}}) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">€ TTC / {% verbatim %}{{ getUnitReference() }}{% endverbatim %}</span> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div v-show="productFamily.behaviorCountStock == 'by-product'" | |||
class="form-group row field-available-quantity"> | |||
{{ form_label(product.availableQuantity, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget"> | |||
<div class="input-group"> | |||
{{ form_widget(product.availableQuantity, {'attr' : {'v-model': 'availableQuantity'}}) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">{% verbatim %}{{ getUnitReference() }}{% endverbatim %}</span> | |||
</div> | |||
</div> | |||
{{ form_help(product.availableQuantity) }} | |||
</div> | |||
</div> | |||
</div> | |||
{{ form_widget(product.position, {'attr' : {'class' : 'field-position'}}) }} | |||
<div class="modal-footer"> | |||
<button @click="saveProductForm()" type="button" class="btn btn-primary">Fermer | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
{% endmacro %} | |||
{% import _self as formMacros %} | |||
<table class="table datagrid sortable lc-sortable-products products-collection-table"> | |||
<thead> | |||
<tr> | |||
<th> | |||
<span></span> | |||
</th> | |||
<th class="string"> | |||
<span>Titre</span> | |||
</th> | |||
<th class="string "> | |||
<span>Unité</span> | |||
</th> | |||
<th class="weight"> | |||
<span>Poids</span> | |||
</th> | |||
<th class="step"> | |||
<span>Pas</span> | |||
</th> | |||
<th class="price"> | |||
<span>Prix HT</span> | |||
</th> | |||
<th class="stock"> | |||
<span>Stock</span> | |||
</th> | |||
<th class=""> | |||
<span>Action</span> | |||
</th> | |||
</tr> | |||
</thead> | |||
<tbody class="products-collection" :data-index="formProductArray.length" | |||
data-prototype="{{ formMacros.printProductRow(form.products.vars.prototype)|e('html_attr') }}"> | |||
<tr class="product-item lc-draggable" :id="'product-item-'+key" | |||
v-for="(formProduct, key) in formProductArray"> | |||
<td><i class="fa fa-fw fa-sort"></i></td> | |||
<td class="title"></td> | |||
<td class="unit"></td> | |||
<td class="weight"></td> | |||
<td class="step"></td> | |||
<td class="price"></td> | |||
<td class="stock"></td> | |||
<td> | |||
<button type="button" class="btn-edit-product btn btn-default" | |||
@click="editProductForm(key)"><i class="fa fa-pen"></i> Éditer cette déclinaison | |||
</button> | |||
<button type="button" class="btn-remove-product btn btn-default" | |||
@click="deleteProductForm(key)"><i class="fa fa-trash"></i> Supprimer cette | |||
déclinaison | |||
</button> | |||
<product-form ref="productForm" v-bind:product-family="productFamily" :template="formProduct" | |||
:key-form="key"></product-form> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
<button type="button" class="add_tag_link btn-add-product btn btn-default" @click="addProductForm"><span | |||
class="fa fa-plus"></span> Ajouter une déclinaison | |||
</button> | |||
<script> | |||
window.productForm = new Array(); | |||
</script> | |||
{% for keyForm,i in sortableProductsField %} | |||
{% set product = form.products[i] %} | |||
<script> | |||
window.productForm[{{ keyForm }}] = { | |||
{% if product.vars.value.title %}title: "{{ product.vars.value.title }}",{% endif %} | |||
{% if product.vars.value.unit %}unit: "{{ product.vars.value.unit }}",{% endif %} | |||
{% if product.vars.value.step %}step: parseInt({{ product.vars.value.step }}),{% endif %} | |||
{% if product.vars.value.weight %}weight: parseFloat({{ product.vars.value.weight }}),{% endif %} | |||
{% if product.vars.value.price %}price: parseFloat({{ product.vars.value.price }}).toFixed(3),{% endif %} | |||
{% if product.vars.value.availableQuantity %}availableQuantity: parseInt({{ product.vars.value.availableQuantity }}){% endif %} | |||
}; | |||
jQuery(document).ready(function () { | |||
var formProduct = '{{ formMacros.printProductRow(product)|replace({"\n":' ', "\r":' ', "'" : "\\'"})|raw }}'; | |||
appProductFamily.formProductArray.push(formProduct); | |||
appProductFamily.indexFormProduct++; | |||
}); | |||
</script> | |||
{% endfor %} | |||
{% do form.products.setRendered %} | |||
@@ -1,7 +1,9 @@ | |||
{{ form_start(form) }} | |||
{{ form_start(form, {"attr": {'@change' : 'formUpdated'}}) }} | |||
<div class="lc-vue-js-container" id="lc-product-family-edit"> | |||
<div id="nav-params"> | |||
<button type="button" v-for="section in sectionsArray" | |||
v-if="(section.name == 'products' && activeProducts == true) || (section.name != 'products')" | |||
:class="'btn '+((currentSection == section.name) ? 'btn-primary' : 'btn-default')" | |||
@click="changeSection(section)"> | |||
${ section.nameDisplay } | |||
@@ -10,6 +12,23 @@ | |||
</button> | |||
</div> | |||
<div class="form"> | |||
<script>// rendered by server | |||
window.appProductFamilyValues = { | |||
{% if form.vars.value.title %}title: "{{ form.vars.value.title }}",{% endif %} | |||
{% if form.behaviorCountStock.vars.value %}behaviorCountStock: "{{ form.behaviorCountStock.vars.value }}",{% endif %} | |||
{% if form.vars.value.availableQuantity %}availableQuantity: "{{ form.vars.value.availableQuantity }}",{% endif %} | |||
{% if form.vars.value.activeProducts %}activeProducts: "{{ form.vars.value.activeProducts }}"{% endif %} | |||
}; | |||
window.productUnitPriceValues = { | |||
{% if form.vars.value.unit %}unit: "{{ form.vars.value.unit }}",{% endif %} | |||
{% if form.vars.value.step %}step: parseInt({{ form.vars.value.step }}),{% endif %} | |||
{% if form.vars.value.weight %}weight: parseFloat({{ form.vars.value.weight }}),{% endif %} | |||
{% if form.taxRate.vars.value != "" %}taxRate: "{{ form.taxRate.vars.value }}",{% endif %} | |||
{% if form.vars.value.price %}price: parseFloat({{ form.vars.value.price }}).toFixed(3){% endif %} | |||
} | |||
</script> | |||
<div v-show="currentSection == 'general'" class="panel panel-default"> | |||
<div class="row"> | |||
<div class="field-group col-8"> | |||
@@ -20,7 +39,7 @@ | |||
{{ form_row(form.supplier) }} | |||
</div> | |||
<div class="col-12"> | |||
{{ form_row(form.title) }} | |||
{{ form_row(form.title, {"attr" : {"v-model" : "title"}}) }} | |||
</div> | |||
<div class="col-12"> | |||
{{ form_row(form.image) }} | |||
@@ -57,16 +76,13 @@ | |||
<div class="field-group col-6"> | |||
<fieldset> | |||
<legend>Prix</legend> | |||
<component-unit inline-template key-form="productfamily" tax-rate-value="-1" v-on:tax-rate-change="setTaxRateValue"> | |||
<product-unit-price ref="productUnitPrice" inline-template key-form="productfamily"> | |||
<div class="row"> | |||
<div class="col-10"> | |||
{{ form_row(form.taxRate, {'attr': {'@change':'changeTaxRate'}}) }} | |||
{{ form_row(form.taxRate, {'attr': {'v-model':'taxRate'}}) }} | |||
</div> | |||
<div class="col-10"> | |||
{{ form_row(form.unit, {"attr":{'v-model': 'unit'}}) }} | |||
{% if form.vars.value %} | |||
<input type="hidden" :id="'value-unit-'+keyForm" value="{{ form.vars.value.unit }}" /> | |||
{% endif %} | |||
</div> | |||
<div class="col-10"> | |||
@@ -74,7 +90,7 @@ | |||
{{ form_label(form.weight) }} | |||
<div class="form-widget"> | |||
<div class="input-group"> | |||
{{ form_widget(form.weight) }} | |||
{{ form_widget(form.weight, {'attr' : {'v-model': 'weight'}}) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">g</span> | |||
</div> | |||
@@ -86,7 +102,7 @@ | |||
{{ form_label(form.step) }} | |||
<div class="form-widget"> | |||
<div class="input-group"> | |||
{{ form_widget(form.step) }} | |||
{{ form_widget(form.step, {'attr' : {'v-model': 'step'}}) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">${ unit }</span> | |||
</div> | |||
@@ -105,9 +121,6 @@ | |||
<div class="input-group-append"> | |||
<span class="input-group-text">€ HT / ${ getUnitReference() }</span> | |||
</div> | |||
{% if form.vars.value %} | |||
<input type="hidden" :id="'value-price-'+keyForm" value="{{ form.vars.value.price }}" /> | |||
{% endif %} | |||
</div> | |||
</div> | |||
</div> | |||
@@ -126,16 +139,40 @@ | |||
</div> | |||
</div> | |||
</div> | |||
</component-unit> | |||
</product-unit-price> | |||
</fieldset> | |||
</div> | |||
<div class="field-group col-6"> | |||
<fieldset> | |||
<legend>Déclinaisons</legend> | |||
{{ form_row(form.activeProducts, {"attr": {"v-model": 'activeProducts'}}) }} | |||
</fieldset> | |||
<fieldset> | |||
<legend>Stock</legend> | |||
<div class="row"> | |||
<div class="col-12"> | |||
{{ form_row(form.behaviorCountStock) }} | |||
{{ form_row(form.availableQuantity) }} | |||
{{ form_label(form.behaviorCountStock) }} | |||
{% for field in form.behaviorCountStock %} | |||
{% if field.vars.value == "by-product" %} | |||
<div v-if="activeProducts == true"> | |||
{{ form_widget(field, {"attr" : {"v-model" : 'behaviorCountStock'}}) }} | |||
</div> | |||
{% else %} | |||
{{ form_widget(field, {"attr" : {"v-model" : 'behaviorCountStock'}}) }} | |||
{% endif %} | |||
{% endfor %} | |||
<div v-show="behaviorCountStock == 'by-product-family'" class="form-group"> | |||
{{ form_label(form.availableQuantity) }} | |||
<div class="form-widget"> | |||
<div class="input-group"> | |||
{{ form_widget(form.availableQuantity) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">${ getUnitReference() }</span> | |||
</div> | |||
</div> | |||
{{ form_help(form.availableQuantity) }} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</fieldset> | |||
@@ -143,110 +180,7 @@ | |||
</div> | |||
</div> | |||
<div v-show="currentSection == 'products'" class="panel panel-default"> | |||
{% macro printProductRow(product) %} | |||
<div :id="'form-product-'+ keyForm"> | |||
<div class="field-group"> | |||
<fieldset> | |||
<legend>Déclinaison</legend> | |||
<div class="form-group row"> | |||
{{ form_label(product.title, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-10"> | |||
{{ form_widget(product.title) }} | |||
{{ form_help(product.title) }} | |||
{% if product.vars.value %} | |||
<input type="hidden" :id="'value-unit-'+keyForm" value="{{ product.vars.value.unit }}" /> | |||
{% endif %} | |||
</div> | |||
</div> | |||
<div class="form-group row"> | |||
{{ form_label(product.unit, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-10"> | |||
{{ form_widget(product.unit, {"attr":{'v-model': 'unit', 'class' : 'select-unit'}}) }} | |||
{{ form_help(product.unit) }} | |||
</div> | |||
</div> | |||
<div v-if="unit == 'piece'" class="form-group row field-weight"> | |||
{{ form_label(product.weight, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-10"> | |||
<div class="input-group"> | |||
{{ form_widget(product.weight) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">g</span> | |||
</div> | |||
</div> | |||
{{ form_help(product.weight) }} | |||
</div> | |||
</div> | |||
<div v-else class="form-group row field-step"> | |||
{{ form_label(product.step, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-8"> | |||
<div class="input-group"> | |||
{{ form_widget(product.step) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">{% verbatim %}{{ unit }}{% endverbatim %}</span> | |||
</div> | |||
</div> | |||
{{ form_help(product.step) }} | |||
</div> | |||
</div> | |||
<div class="form-group row field-price"> | |||
{{ form_label(product.price, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-10"> | |||
<div class="input-group"> | |||
{{ form_widget(product.price, {'attr' : {'v-model': 'price', '@change' : 'changePriceWithTax'}}) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">€ HT / {% verbatim %}{{ getUnitReference() }}{% endverbatim %}</span> | |||
</div> | |||
{% if product.vars.value %} | |||
<input type="hidden" :id="'value-price-'+keyForm" value="{{ product.vars.value.price }}" /> | |||
{% endif %} | |||
</div> | |||
</div> | |||
</div> | |||
<div class="form-group row field-price"> | |||
<div class="col-2"> </div> | |||
<div class="form-widget col-10"> | |||
<div class="input-group"> | |||
{{ form_widget(product.priceWithTax, {'attr' : {'v-model': 'priceWithTax','@change' : 'changePrice'}}) }} | |||
<div class="input-group-append"> | |||
<span class="input-group-text">€ TTC / {% verbatim %}{{ getUnitReference() }}{% endverbatim %}</span> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="form-group row field-available-quantity"> | |||
{{ form_label(product.availableQuantity, null, {'label_attr': {'class': 'col-2 col-form-label text-right'}}) }} | |||
<div class="form-widget col-10"> | |||
{{ form_widget(product.availableQuantity) }} | |||
</div> | |||
</div> | |||
</fieldset> | |||
</div> | |||
</div> | |||
{% endmacro %} | |||
{% import _self as formMacros %} | |||
<ul class="products" :data-index="formProductArray.length" data-prototype="{{ formMacros.printProductRow(form.products.vars.prototype)|e('html_attr') }}"> | |||
<li class="product" v-for="(formProduct, key) in formProductArray"> | |||
<button type="button" class="btn-remove-product btn btn-default" @click="deleteFormProduct(key)"><i class="fa fa-trash"></i> Supprimer cette déclinaison</button> | |||
<component-unit :template="formProduct" :key-form="key" :tax-rate-value="taxRateValue"></component-unit> | |||
</li> | |||
<li class="add"> | |||
<button type="button" class="add_tag_link btn-add-product btn btn-default" @click="addFormProduct"><span class="fa fa-plus"></span> Ajouter une déclinaison</button> | |||
</li> | |||
{% for product in form.products %} | |||
<script> | |||
jQuery(document).ready(function() { | |||
var formProduct = '{{ formMacros.printProductRow(product)|replace({"\n":' ', "\r":' ', "'" : "\\'"})|raw }}' ; | |||
appProductFamily.formProductArray.push(formProduct) ; | |||
appProductFamily.indexFormProduct ++ ; | |||
}) ; | |||
</script> | |||
{% endfor %} | |||
</ul> | |||
{% include '@LcShop/backend/default/product_macro.html.twig' %} | |||
</div> | |||
</div> | |||
</div> |