Can you describe more detailed repro steps for this bug?
I'm unable to replicate it for now.
EDIT: Never mind, I replicated it. We should move the category on the same level and we should not have a category with the same position in the new place.
Can you try this if it's working for you so I can make a PR?
Category::update
public function update($nullValues = false)
{
if ($this->id_parent == $this->id) {
throw new PrestaShopException('a category cannot be its own parent');
}
if (PageCache::isEnabled()) {
PageCache::invalidateEntity('category', $this->id);
}
// Read current persisted parent (and level) before we write anything
$row = Db::readOnly()->getRow(
(new DbQuery())
->select('`id_parent`, `level_depth`')
->from('category')
->where('`id_category` = ' . (int) $this->id)
);
$oldParentId = $row ? (int) $row['id_parent'] : 0;
$oldLevelDepth = $row ? (int) $row['level_depth'] : null;
if ($this->is_root_category && $this->id_parent != (int) Configuration::get('PS_ROOT_CATEGORY')) {
$this->is_root_category = 0;
}
// Update group selection, if provided
if (is_array($this->groupBox)) {
$this->updateGroup($this->groupBox);
}
// Compute target depth from the new parent
$calculatedLevelDepth = $this->calcLevelDepth(); // throws if parent invalid
$parentChanged = ($oldParentId !== (int) $this->id_parent);
$levelChanged = ($oldLevelDepth === null) ? true : ($oldLevelDepth !== (int) $calculatedLevelDepth);
$this->level_depth = $calculatedLevelDepth;
// If parent changed we must reseat position; otherwise do it only if duplicate exists
$needReposition = $parentChanged || (bool) $this->getDuplicatePosition();
// === Multistore-safe shop list to touch ===
$shopIdsToTouch = [];
if ($needReposition) {
if (Tools::isSubmit('checkBoxShopAsso_category')) {
// Admin form posted: only the explicitly associated shops
$assoc = Tools::getArrayValue('checkBoxShopAsso_category');
$shopIdsToTouch = array_map('intval', array_keys((array) $assoc));
} elseif (Shop::getContext() == Shop::CONTEXT_SHOP) {
// Single shop context
$shopIdsToTouch = [ (int) Context::getContext()->shop->id ];
} else {
// Fallback: only shops already associated with the category
$rows = static::getShopsByCategory((int) $this->id);
foreach ($rows as $r) {
$shopIdsToTouch[] = (int) $r['id_shop'];
}
if (!$shopIdsToTouch) {
// If somehow none, at least touch default shop to keep data consistent
$shopIdsToTouch = [ (int) Configuration::get('PS_SHOP_DEFAULT') ];
}
}
// Reseat position per associated shop
foreach ($shopIdsToTouch as $idShop) {
$this->addPosition((int) static::getLastPosition((int) $this->id_parent, (int) $idShop), (int) $idShop);
}
}
$ret = parent::update($nullValues);
if ($ret) {
// Clean positions in both branches when moved; always clean in the new parent when we reseated
if ($needReposition) {
static::cleanPositions((int) $this->id_parent);
if ($parentChanged && $oldParentId) {
static::cleanPositions((int) $oldParentId);
}
}
// Any parent change or depth change requires a full ntree rebuild
if ((!isset($this->doNotRegenerateNTree) || !$this->doNotRegenerateNTree) && ($parentChanged || $levelChanged || $needReposition)) {
static::regenerateEntireNtree();
$this->recalculateLevelDepth($this->id); // fix depths of descendants
}
Hook::triggerEvent('actionCategoryUpdate', ['category' => $this]);
}
return $ret;
}