dolibarr 25.0.0-alpha
product_tools.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2012 Regis Houssin <regis.houssin@inodbox.com>
3 * Copyright (C) 2013-2015 Laurent Destailleur <eldy@users.sourceforge.net>
4 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
5 * Copyright (C) 2025 MDW <mdeweerd@users.noreply.github.com>
6 * Copyright (C) 2026 Jose Martinez <jose.martinez@pichinov.com>
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21
28// TODO We must add a confirmation on button because this will make a mass change
29// FIXME Should also change table product_price for price levels
30
31// Load Dolibarr environment
32require '../../main.inc.php';
33require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
34require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
35require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
36require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
37
47// Load translation files required by the page
48$langs->loadLangs(array('admin', 'products'));
49
50// Security check
51if (!$user->admin) {
53}
54
55$action = GETPOST('action', 'aZ09');
56$oldvatrate = GETPOST('oldvatrate', 'alpha');
57$newvatrate = GETPOST('newvatrate', 'alpha');
58//$price_base_type=GETPOST('price_base_type');
59
60$hookmanager->initHooks(array('productvatadmin'));
61
62/*
63 * Actions
64 */
65
66if ($action == 'convert') {
67 $error = 0;
68
69 if ($oldvatrate == $newvatrate) {
70 $langs->load("errors");
71 setEventMessages($langs->trans("ErrorNewValueCantMatchOldValue"), null, 'errors');
72 $error++;
73 }
74
75 if (!$error) {
76 $country_id = $mysoc->country_id; // TODO Allow to choose country into form
77
78 $nbrecordsmodified = 0;
79
80 if (!getDolGlobalInt('VATUPDATE_NO_TRANSACTION')) {
81 $db->begin();
82 }
83
84 // Clean vat code old
85 $vat_src_code_old = '';
86 if (preg_match('/\‍((.*)\‍)/', $oldvatrate, $reg)) {
87 $vat_src_code_old = $reg[1];
88 $oldvatrateclean = preg_replace('/\s*\‍(.*\‍)/', '', $oldvatrate); // Remove code into vatrate.
89 } else {
90 $oldvatrateclean = $oldvatrate;
91 }
92
93 // Clean vat code new
94 $vat_src_code_new = '';
95 if (preg_match('/\‍((.*)\‍)/', $newvatrate, $reg)) {
96 $vat_src_code_new = $reg[1];
97 $newvatrateclean = preg_replace('/\s*\‍(.*\‍)/', '', $newvatrate); // Remove code into vatrate.
98 } else {
99 $newvatrateclean = $newvatrate;
100 }
101
102 // If country to edit is my country, so we change customer prices
103 if ($country_id == $mysoc->country_id) {
104 $sql = 'SELECT rowid';
105 $sql .= ' FROM '.MAIN_DB_PREFIX.'product';
106 $sql .= ' WHERE entity IN ('.getEntity('product').')';
107 $sql .= " AND tva_tx = '".$db->escape($oldvatrateclean)."'";
108 if ($vat_src_code_old) {
109 $sql .= " AND default_vat_code = '".$db->escape($vat_src_code_old)."'";
110 } else {
111 $sql .= " AND (default_vat_code IS NULL OR default_vat_code = '')";
112 }
113
114 $resql = $db->query($sql);
115 if ($resql) {
116 $num = $db->num_rows($resql);
117
118 $i = 0;
119 while ($i < $num) {
120 $obj = $db->fetch_object($resql);
121
122 $objectstatic = new Product($db); // Object init must be into loop to avoid to get value of previous step
123 $ret = $objectstatic->fetch($obj->rowid);
124 if ($ret > 0) {
125 $ret = 0;
126 $retm = 0;
127 $updatelevel1 = false;
128
129 // Update multiprice
130 $listofmulti = array_reverse($objectstatic->multiprices, true); // To finish with level 1
131 foreach ($listofmulti as $level => $multiprices) {
132 $price_base_type = $objectstatic->multiprices_base_type[$level]; // Get price_base_type of product/service to keep the same for update
133 if (empty($price_base_type)) {
134 continue; // Discard not defined price levels
135 }
136
137 if ($price_base_type == 'TTC') {
138 $newprice = price2num($objectstatic->multiprices_ttc[$level], 'MU'); // Second param must be MU (we want a unit price so 'MU'. If unit price was on 4 decimal, we must keep 4 decimals)
139 $newminprice = $objectstatic->multiprices_min_ttc[$level];
140 } else {
141 $newprice = price2num($objectstatic->multiprices[$level], 'MU'); // Second param must be MU (we want a unit price so 'MU'. If unit price was on 4 decimal, we must keep 4 decimals)
142 $newminprice = $objectstatic->multiprices_min[$level];
143 }
144 if ($newminprice > $newprice) {
145 $newminprice = $newprice;
146 }
147
148 $newvat = str_replace('*', '', $newvatrate);
149 $localtaxes_type = getLocalTaxesFromRate($newvat, 0, $mysoc, $mysoc);
150 $newnpr = $objectstatic->multiprices_recuperableonly[$level];
151 $newdefaultvatcode = $vat_src_code_new;
152 $newlevel = $level;
153
154 //print "$objectstatic->id $newprice, $price_base_type, $newvat, $newminprice, $newlevel, $newnpr<br>\n";
155 $retm = $objectstatic->updatePrice((float) $newprice, $price_base_type, $user, (float) $newvatrateclean, $newminprice, $newlevel, $newnpr, 0, 0, $localtaxes_type, $newdefaultvatcode);
156 if ($retm < 0) {
157 $error++;
158 break;
159 }
160
161 if ($newlevel == 1) {
162 $updatelevel1 = true;
163 }
164 }
165
166 // Update single price
167 $price_base_type = $objectstatic->price_base_type; // Get price_base_type of product/service to keep the same for update
168 if ($price_base_type == 'TTC') {
169 $newprice = price2num($objectstatic->price_ttc, 'MU'); // Second param must be MU (we want a unit price so 'MU'. If unit price was on 4 decimal, we must keep 4 decimals)
170 $newminprice = $objectstatic->price_min_ttc;
171 } else {
172 $newprice = price2num($objectstatic->price, 'MU'); // Second param must be MU (we want a unit price so 'MU'. If unit price was on 4 decimal, we must keep 4 decimals)
173 $newminprice = $objectstatic->price_min;
174 }
175 if ($newminprice > $newprice) {
176 $newminprice = $newprice;
177 }
178 $newvat = str_replace('*', '', $newvatrate);
179 $localtaxes_type = getLocalTaxesFromRate($newvat, 0, $mysoc, $mysoc);
180 $newnpr = $objectstatic->tva_npr;
181 $newdefaultvatcode = $vat_src_code_new;
182 $newlevel = 0;
183 if (!empty($price_base_type) && !$updatelevel1) {
184 //print "$objectstatic->id $newprice, $price_base_type, $newvat, $newminprice, $newlevel, $newnpr<br>\n";
185 $ret = $objectstatic->updatePrice((float) $newprice, $price_base_type, $user, (float) $newvatrateclean, $newminprice, $newlevel, $newnpr, 0, 0, $localtaxes_type, $newdefaultvatcode);
186 }
187
188 if ($ret < 0 || $retm < 0) {
189 $error++;
190 } else {
191 $nbrecordsmodified++;
192 }
193 }
194 unset($objectstatic);
195
196 $i++;
197 }
198 } else {
200 }
201
202 // Sweep: updatePrice() above only reaches products that have a defined price. Set the target
203 // rate and VAT code directly on any product/price row still matching the old value (products
204 // without a price, and historical price rows), so the mass change is complete.
205 $sweepwhere = " AND tva_tx = '".$db->escape($oldvatrateclean)."'";
206 if ($vat_src_code_old) {
207 $sweepwhere .= " AND default_vat_code = '".$db->escape($vat_src_code_old)."'";
208 } else {
209 $sweepwhere .= " AND (default_vat_code IS NULL OR default_vat_code = '')";
210 }
211 $db->query("UPDATE ".MAIN_DB_PREFIX."product SET tva_tx = '".$db->escape($newvatrateclean)."', default_vat_code = '".$db->escape($vat_src_code_new)."' WHERE entity IN (".getEntity('product').")".$sweepwhere);
212 $db->query("UPDATE ".MAIN_DB_PREFIX."product_price SET tva_tx = '".$db->escape($newvatrateclean)."', default_vat_code = '".$db->escape($vat_src_code_new)."' WHERE 1 = 1".$sweepwhere);
213 }
214
215 $fourn = new Fournisseur($db);
216
217 // Change supplier prices
218 $sql = 'SELECT pfp.rowid, pfp.fk_soc, pfp.price as price, pfp.quantity as qty, pfp.fk_availability, pfp.ref_fourn';
219 $sql .= ' FROM '.MAIN_DB_PREFIX.'product_fournisseur_price as pfp, '.MAIN_DB_PREFIX.'societe as s';
220 $sql .= ' WHERE pfp.fk_soc = s.rowid AND pfp.entity IN ('.getEntity('product').')';
221 $sql .= " AND tva_tx = '".$db->escape($oldvatrate)."'";
222 if ($vat_src_code_old) {
223 $sql .= " AND default_vat_code = '".$db->escape($vat_src_code_old)."'";
224 } else {
225 $sql .= " AND (default_vat_code IS NULL OR default_vat_code = '')";
226 }
227 $sql .= " AND s.fk_pays = ".((int) $country_id);
228
229 $resql = $db->query($sql);
230 if ($resql) {
231 $num = $db->num_rows($resql);
232
233 $i = 0;
234 while ($i < $num) {
235 $obj = $db->fetch_object($resql);
236
237 $objectstatic2 = new ProductFournisseur($db); // Object init must be into loop to avoid to get value of previous step
238 $ret = $objectstatic2->fetch_product_fournisseur_price($obj->rowid);
239 if ($ret > 0) {
240 $ret = 0;
241 $retm = 0;
242 $updatelevel1 = false;
243
244 $price_base_type = 'HT';
245 //$price_base_type = $objectstatic2->price_base_type; // Get price_base_type of product/service to keep the same for update
246 //if ($price_base_type == 'TTC')
247 //{
248 // $newprice=price2num($objectstatic2->price_ttc,'MU'); // Second param must be MU (we want a unit price so 'MU'. If unit price was on 4 decimal, we must keep 4 decimals)
249 // $newminprice=$objectstatic2->price_min_ttc;
250 //}
251 //else
252 //{
253 $newprice = price2num($obj->price, 'MU'); // Second param must be MU (we want a unit price so 'MU'. If unit price was on 4 decimal, we must keep 4 decimals)
254 //$newminprice=$objectstatic2->fourn_price_min;
255 //}
256 //if ($newminprice > $newprice) $newminprice=$newprice;
257 $newvat = str_replace('*', '', $newvatrate);
258 $localtaxes_type = getLocalTaxesFromRate($newvat, 0, $mysoc, $mysoc);
259 //$newnpr=$objectstatic2->tva_npr;
260 $newnpr = 0;
261 $newdefaultvatcode = $vat_src_code_new;
262
263 $newpercent = $objectstatic2->fourn_remise_percent;
264 $newdeliverydelay = $objectstatic2->delivery_time_days;
265 $newsupplierreputation = $objectstatic2->supplier_reputation;
266
267 $newlevel = 0;
268 if (!empty($price_base_type) && !$updatelevel1) {
269 //print "$objectstatic2->id $newprice, $price_base_type, $newvat, $newminprice, $newlevel, $newnpr<br>\n";
270 $fourn->id = $obj->fk_soc;
271 $ret = $objectstatic2->update_buyprice($obj->qty, (float) $newprice, $user, $price_base_type, $fourn, $obj->fk_availability, $obj->ref_fourn, (float) $newvat, '', $newpercent, 0, $newnpr, $newdeliverydelay, $newsupplierreputation, $localtaxes_type, $newdefaultvatcode);
272 }
273
274 if ($ret < 0 || $retm < 0) {
275 $error++;
276 } else {
277 $nbrecordsmodified++;
278 }
279 }
280 unset($objectstatic2);
281
282 $i++;
283 }
284 } else {
286 }
287
288
289 // add hook for external modules
290 $parameters = array('oldvatrate' => $oldvatrate, 'newvatrate' => $newvatrate);
291 $reshook = $hookmanager->executeHooks('hookAfterVatUpdate', $parameters);
292 if ($reshook < 0) {
293 setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
294 $error++;
295 }
296
297 if (!getDolGlobalInt('VATUPDATE_NO_TRANSACTION')) {
298 if (!$error) {
299 $db->commit();
300 } else {
301 $db->rollback();
302 }
303 }
304
305 // Output result
306 if (!$error) {
307 if ($nbrecordsmodified > 0) {
308 setEventMessages($langs->trans("RecordsModified", $nbrecordsmodified), null, 'mesgs');
309 } else {
310 setEventMessages($langs->trans("NoRecordFound"), null, 'warnings');
311 }
312 } else {
313 setEventMessages($langs->trans("Error"), null, 'errors');
314 }
315 }
316}
317
318/*
319 * View
320 */
321
322$form = new Form($db);
323
324$title = $langs->trans('ProductVatMassChange');
325
326llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-product page-admin_product_tools');
327
328print load_fiche_titre($title, '', 'title_setup');
329
330print $langs->trans("ProductVatMassChangeDesc").'<br><br>';
331
332if (empty($mysoc->country_code)) {
333 $langs->load("errors");
334 $warnpicto = img_error($langs->trans("WarningMandatorySetupNotComplete"));
335 print '<br><a href="'.DOL_URL_ROOT.'/admin/company.php?mainmenu=home">'.$warnpicto.' '.$langs->trans("WarningMandatorySetupNotComplete").'</a>';
336} else {
337 print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
338 print '<input type="hidden" name="token" value="'.newToken().'" />';
339 print '<input type="hidden" name="action" value="convert" />';
340
341 print '<table class="noborder centpercent">';
342 print '<tr class="liste_titre">';
343 print '<td>'.$langs->trans("Parameters").'</td>'."\n";
344 print '<td class="right" width="60">'.$langs->trans("Value").'</td>'."\n";
345 print '</tr>'."\n";
346
347
348 print '<tr class="oddeven">'."\n";
349 print '<td>'.$langs->trans("OldVATRates").'</td>'."\n";
350 print '<td width="60" class="right">'."\n";
351 // Old VAT rate: distinct (rate, code) pairs present on products, with the count of products for
352 // each -- so the source to convert (including rates without a VAT code) is visible and quantified.
353 print '<select class="flat" name="oldvatrate" id="oldvatrate">';
354 $sqloldvat = "SELECT tva_tx, default_vat_code, COUNT(*) as nb FROM ".MAIN_DB_PREFIX."product";
355 $sqloldvat .= " WHERE entity IN (".getEntity('product').") AND tva_tx IS NOT NULL";
356 $sqloldvat .= " GROUP BY tva_tx, default_vat_code ORDER BY tva_tx DESC, default_vat_code";
357 $resqloldvat = $db->query($sqloldvat);
358 while ($resqloldvat && $objoldvat = $db->fetch_object($resqloldvat)) {
359 $rateclean = price2num($objoldvat->tva_tx);
360 $hascode = !empty($objoldvat->default_vat_code);
361 $optval = $rateclean.($hascode ? ' ('.$objoldvat->default_vat_code.')' : '');
362 $optlbl = vatrate($rateclean, true).($hascode ? ' ('.$objoldvat->default_vat_code.')' : ' ('.$langs->trans("WithoutVATCode").')').' ('.$objoldvat->nb.')';
363 print '<option value="'.dol_escape_htmltag($optval).'"'.((string) $oldvatrate === (string) $optval ? ' selected' : '').'>'.$optlbl.'</option>';
364 }
365 print '</select>';
366 print '</td>'."\n";
367 print '</tr>'."\n";
368
369
370 print '<tr class="oddeven">'."\n";
371 print '<td>'.$langs->trans("NewVATRates").'</td>'."\n";
372 print '<td width="60" class="right">'."\n";
373 print $form->load_tva('newvatrate', $newvatrate, $mysoc, null, 0, 0, '', false, 1);
374 print '</td>'."\n";
375 print '</tr>'."\n";
376
377 /*
378
379 print '<tr class="oddeven">'."\n";
380 print '<td>'.$langs->trans("PriceBaseTypeToChange").'</td>'."\n";
381 print '<td width="60" class="right">'."\n";
382 print $form->selectPriceBaseType($price_base_type);
383 print '</td>'."\n";
384 print '</tr>'."\n";
385 */
386
387 print '</table>';
388
389 print '<br>';
390
391 // Buttons for actions
392
393 print '<div class="center">';
394 print '<input type="submit" id="convert_vatrate" name="convert_vatrate" value="'.$langs->trans("MassConvert").'" class="button" />';
395 print '</div>';
396
397 print '</form>';
398
399 // The conversion loops over every matching product with updatePrice(): on a large
400 // catalog the request runs for minutes with no feedback, which invites re-submits.
401 // Show the native blocking overlay (dolBlockUI + working.gif) while it runs.
402 print '<script>
403 jQuery(function() {
404 jQuery("#convert_vatrate").closest("form").on("submit", function() {
405 dolBlockUI("'.dol_escape_js($langs->transnoentities("MassConvertInProgress")).'");
406 });
407 });
408 </script>';
409}
410
411// End of page
412llxFooter();
413$db->close();
llxFooter($comment='', $zone='private', $disabledoutputofmessages=0)
Empty footer.
Definition wrapper.php:91
if(!defined('NOREQUIRESOC')) if(!defined( 'NOREQUIRETRAN')) if(!defined('NOTOKENRENEWAL')) if(!defined( 'NOREQUIREMENU')) if(!defined('NOREQUIREHTML')) if(!defined( 'NOREQUIREAJAX')) llxHeader($head='', $title='', $help_url='', $target='', $disablejs=0, $disablehead=0, $arrayofjs='', $arrayofcss='', $morequerystring='', $morecssonbody='', $replacemainareaby='', $disablenofollow=0, $disablenoindex=0)
Empty header.
Definition wrapper.php:73
Class to manage generation of HTML components Only common components must be here.
Class to manage suppliers.
Class to manage predefined suppliers products.
Class to manage products or services.
global $mysoc
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
vatrate($rate, $addpercent=false, $info_bits=0, $usestarfornpr=0, $html=0)
Return a string with VAT rate label formatted for view output Used into pdf and HTML pages.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
getLocalTaxesFromRate($vatrate, $local, $buyer, $seller, $firstparamisid=0)
Get type and rate of localtaxes for a particular vat rate/country of a thirdparty.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0, $nodefault=0)
Return value of a param into GET or POST supervariable.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
setEventMessages($mesg, $mesgs, $style='mesgs', $messagekey='', $noduplicate=0, $attop=0)
Set event messages in dol_events session object.
img_error($titlealt='default')
Show error logo.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
load_fiche_titre($title, $morehtmlright='', $picto='generic', $pictoisfullpath=0, $id='', $morecssontable='', $morehtmlcenter='', $morecssonpicto='widthpictotitle')
Load a title with picto.
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string
accessforbidden($message='', $printheader=1, $printfooter=1, $showonlymessage=0, $params=null)
Show a message to say access is forbidden and stop program.