dolibarr 21.0.0-beta
productMargins.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2012-2013 Christophe Battarel <christophe.battarel@altairis.fr>
3 * Copyright (C) 2014 Ferran Marcet <fmarcet@2byte.es>
4 * Copyright (C) 2020 Alexandre Spangaro <aspangaro@open-dsi.fr>
5 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
27// Load Dolibarr environment
28require '../main.inc.php';
29require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
30require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
31require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
32require_once DOL_DOCUMENT_ROOT.'/margin/lib/margins.lib.php';
33
42// Load translation files required by the page
43$langs->loadLangs(array('companies', 'bills', 'products', 'margins'));
44
45$id = GETPOSTINT('id');
46$ref = GETPOST('ref', 'alpha');
47$action = GETPOST('action', 'aZ09');
48$confirm = GETPOST('confirm', 'alpha');
49$TSelectedCats = GETPOST('categories', 'array');
50$socid = 0;
51
52$mesg = '';
53
54// Load variable for pagination
55$limit = GETPOSTINT('limit') ? GETPOSTINT('limit') : $conf->liste_limit;
56$sortfield = GETPOST('sortfield', 'aZ09comma');
57$sortorder = GETPOST('sortorder', 'aZ09comma');
58$page = GETPOSTISSET('pageplusone') ? (GETPOSTINT('pageplusone') - 1) : GETPOSTINT("page");
59if (empty($page) || $page == -1) {
60 $page = 0;
61} // If $page is not defined, or '' or -1
62$offset = $limit * $page;
63$pageprev = $page - 1;
64$pagenext = $page + 1;
65if (!$sortfield) {
66 if ($id > 0) {
67 $sortfield = "f.datef";
68 $sortorder = "DESC";
69 } else {
70 $sortfield = "p.ref";
71 $sortorder = "ASC";
72 }
73}
74
75$startdate = $enddate = '';
76if (GETPOST('startdatemonth')) {
77 $startdate = dol_mktime(0, 0, 0, GETPOSTINT('startdatemonth'), GETPOSTINT('startdateday'), GETPOSTINT('startdateyear'));
78}
79if (GETPOST('enddatemonth')) {
80 $enddate = dol_mktime(23, 59, 59, GETPOSTINT('enddatemonth'), GETPOSTINT('enddateday'), GETPOST('enddateyear'));
81}
82
83// Initialize a technical object to manage hooks of page. Note that conf->hooks_modules contains an array of hook context
84$object = new Product($db);
85$hookmanager->initHooks(array('marginproductlist'));
86
87// Security check
88$fieldvalue = (!empty($id) ? $id : (!empty($ref) ? $ref : ''));
89$fieldtype = (!empty($ref) ? 'ref' : 'rowid');
90if (!empty($user->socid)) {
91 $socid = $user->socid;
92}
93$result = restrictedArea($user, 'produit|service', $fieldvalue, 'product&product', '', '', $fieldtype);
94if (!$user->hasRight('margins', 'liretous')) {
96}
97
98
99/*
100 * View
101 */
102
103$product_static = new Product($db);
104$invoicestatic = new Facture($db);
105
106$form = new Form($db);
107
108llxHeader('', $langs->trans("Margins").' - '.$langs->trans("Products"), '', '', 0, 0, '', '', '', 'mod-margin page-productmargins');
109
110$text = $langs->trans("Margins");
111//print load_fiche_titre($text);
112
113// Show tabs
114$head = marges_prepare_head();
115
116$titre = $langs->trans("Margins");
117$picto = 'margin';
118
119print '<form method="post" name="sel" action="'.$_SERVER['PHP_SELF'].'">';
120print '<input type="hidden" name="token" value="'.newToken().'">';
121
122print dol_get_fiche_head($head, 'productMargins', $titre, 0, $picto);
123
124print '<table class="border centpercent">';
125
126// Product
127print '<tr><td class="titlefield">'.$langs->trans('ProductOrService').'</td>';
128print '<td class="maxwidthonsmartphone" colspan="4">';
129print img_picto('', 'product').$form->select_produits(($id > 0 ? $id : ''), 'id', '', 20, 0, 1, 2, '', 1, array(), 0, 'All', 0, '', 0, '', null, 1);
130print '</td></tr>';
131
132// Categories
133$TCats = $form->select_all_categories('product', array(), '', 64, 0, 3);
134
135print '<tr>';
136print '<td class="titlefield">'.$langs->trans('Category').'</td>';
137print '<td class="maxwidthonsmartphone" colspan="4">';
138print img_picto('', 'category', 'class="pictofixedwidth"').$form->multiselectarray('categories', $TCats, $TSelectedCats, 0, 0, 'quatrevingtpercent widthcentpercentminusx');
139print '</td>';
140print '</tr>';
141
142// Start date
143print '<tr>';
144print '<td class="titlefield">'.$langs->trans('DateStart').' ('.$langs->trans("DateValidation").')</td>';
145print '<td>';
146print $form->selectDate($startdate, 'startdate', 0, 0, 1, "sel", 1, 1);
147print '</td>';
148print '<td>'.$langs->trans('DateEnd').' ('.$langs->trans("DateValidation").')</td>';
149print '<td>';
150print $form->selectDate($enddate, 'enddate', 0, 0, 1, "sel", 1, 1);
151print '</td>';
152print '<td style="text-align: center;">';
153print '<input type="submit" class="button" value="'.dol_escape_htmltag($langs->trans('Refresh')).'" />';
154print '</td></tr>';
155
156print "</table>";
157
158print '<br>';
159
160print '<table class="border centpercent">';
161
162// Total Margin
163print '<tr><td class="titlefield">'.$langs->trans("TotalMargin").'</td><td colspan="4">';
164print '<span id="totalMargin" class="amount"></span> <span class="amount">'.$langs->getCurrencySymbol($conf->currency).'</span>'; // set by jquery (see below)
165print '</td></tr>';
166
167// Margin Rate
168if (getDolGlobalString('DISPLAY_MARGIN_RATES')) {
169 print '<tr><td>'.$langs->trans("MarginRate").'</td><td colspan="4">';
170 print '<span id="marginRate"></span>'; // set by jquery (see below)
171 print '</td></tr>';
172}
173
174// Mark Rate
175if (getDolGlobalString('DISPLAY_MARK_RATES')) {
176 print '<tr><td>'.$langs->trans("MarkRate").'</td><td colspan="4">';
177 print '<span id="markRate"></span>'; // set by jquery (see below)
178 print '</td></tr>';
179}
180
181print "</table>";
182
183print dol_get_fiche_end();
184
185print '</form>';
186
187$invoice_status_except_list = array(Facture::STATUS_DRAFT, Facture::STATUS_ABANDONED);
188
189$sql = "SELECT p.label, p.rowid, p.fk_product_type, p.ref, p.entity as pentity,";
190if ($id > 0) {
191 $sql .= " d.fk_product,";
192}
193if ($id > 0) {
194 $sql .= " f.rowid as facid, f.ref, f.total_ht, f.datef, f.paye, f.fk_statut as statut,";
195}
196$sql .= " SUM(d.total_ht) as selling_price,";
197$sql .= " SUM(d.qty) as product_qty,";
198
199// Note: qty and buy_price_ht is always positive (if not your database may be corrupted, you can update this)
200$sql .= " SUM(".$db->ifsql('(d.total_ht < 0 OR (d.total_ht = 0 AND f.type = 2))', '-1 * d.qty * d.buy_price_ht * (d.situation_percent / 100)', 'd.qty * d.buy_price_ht * (d.situation_percent / 100)').") as buying_price,";
201$sql .= " SUM(".$db->ifsql('(d.total_ht < 0 OR (d.total_ht = 0 AND f.type = 2))', '-1 * (abs(d.total_ht) - (d.buy_price_ht * d.qty * (d.situation_percent / 100)))', 'd.total_ht - (d.buy_price_ht * d.qty * (d.situation_percent / 100))').") as marge";
202
203$sql .= " FROM ".MAIN_DB_PREFIX."societe as s";
204$sql .= ", ".MAIN_DB_PREFIX."facture as f";
205$sql .= ", ".MAIN_DB_PREFIX."facturedet as d";
206$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = d.fk_product";
207if (!empty($TSelectedCats)) {
208 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'categorie_product as cp ON cp.fk_product=p.rowid';
209}
210$sql .= " WHERE f.fk_soc = s.rowid";
211$sql .= ' AND f.entity IN ('.getEntity('invoice').')';
212$sql .= " AND f.fk_statut NOT IN (".$db->sanitize(implode(', ', $invoice_status_except_list)).")";
213$sql .= " AND d.fk_facture = f.rowid";
214if ($id > 0) {
215 $sql .= " AND d.fk_product =".((int) $id);
216}
217if (!empty($TSelectedCats)) {
218 $sql .= ' AND cp.fk_categorie IN ('.$db->sanitize(implode(',', $TSelectedCats)).')';
219}
220if (!empty($startdate)) {
221 $sql .= " AND f.datef >= '".$db->idate($startdate)."'";
222}
223if (!empty($enddate)) {
224 $sql .= " AND f.datef <= '".$db->idate($enddate)."'";
225}
226$sql .= " AND d.buy_price_ht IS NOT NULL";
227// We should not use this here. Option ForceBuyingPriceIfNull should have effect only when inserting data. Once data is recorded, it must be used as it is for report.
228// We keep it with value ForceBuyingPriceIfNull = 2 for retroactive effect but results are unpredictable.
229if (getDolGlobalInt('ForceBuyingPriceIfNull') == 2) {
230 $sql .= " AND d.buy_price_ht <> 0";
231}
232if ($id > 0) {
233 $sql .= " GROUP BY p.label, p.rowid, p.fk_product_type, p.ref, p.entity, d.fk_product, f.rowid, f.ref, f.total_ht, f.datef, f.paye, f.fk_statut";
234} else {
235 $sql .= " GROUP BY p.label, p.rowid, p.fk_product_type, p.ref, p.entity";
236}
237$sql .= $db->order($sortfield, $sortorder);
238// TODO: calculate total to display then restore pagination
239//$sql.= $db->plimit($conf->liste_limit +1, $offset);
240
241$param = '&id='.((int) $id);
242if (GETPOSTINT('startdatemonth')) {
243 $param .= '&startdateyear='.GETPOSTINT('startdateyear');
244 $param .= '&startdatemonth='.GETPOSTINT('startdatemonth');
245 $param .= '&startdateday='.GETPOSTINT('startdateday');
246}
247if (GETPOSTINT('enddatemonth')) {
248 $param .= '&enddateyear='.GETPOSTINT('enddateyear');
249 $param .= '&enddatemonth='.GETPOSTINT('enddatemonth');
250 $param .= '&enddateday='.GETPOSTINT('enddateday');
251}
252$listofcateg = GETPOST('categories', 'array:int');
253if (is_array($listofcateg)) {
254 foreach ($listofcateg as $val) {
255 $param .= '&categories[]='.$val;
256 }
257}
258
259$totalMargin = 0;
260$marginRate = '';
261$markRate = '';
262dol_syslog('margin::productMargins.php', LOG_DEBUG);
263$result = $db->query($sql);
264if ($result) {
265 $num = $db->num_rows($result);
266
267 print '<br>';
268 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition, PhanPluginSuspiciousParamOrder
269 print_barre_liste($langs->trans("MarginDetails"), $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $num, '', 0, '', '', 0, 1);
270
271 //var_dump($conf->global->MARGIN_TYPE);
272 if (getDolGlobalString('MARGIN_TYPE') == "1") {
273 $labelcostprice = 'BuyingPrice';
274 } else { // value is 'costprice' or 'pmp'
275 $labelcostprice = 'CostPrice';
276 }
277
278 $moreforfilter = '';
279
280 $i = 0;
281 print '<div class="div-table-responsive">';
282 print '<table class="tagtable liste'.($moreforfilter ? " listwithfilterbefore" : "").'">'."\n";
283
284 print '<tr class="liste_titre">';
285 if ($id > 0) {
286 print_liste_field_titre("Invoice", $_SERVER["PHP_SELF"], "f.ref", "", $param, '', $sortfield, $sortorder);
287 print_liste_field_titre("DateInvoice", $_SERVER["PHP_SELF"], "f.datef", "", $param, '', $sortfield, $sortorder, 'center ');
288 } else {
289 print_liste_field_titre("ProductService", $_SERVER["PHP_SELF"], "p.ref", "", $param, '', $sortfield, $sortorder);
290 }
291 print_liste_field_titre("Qty", $_SERVER["PHP_SELF"], "product_qty", "", $param, '', $sortfield, $sortorder, 'center ');
292 print_liste_field_titre("SellingPrice", $_SERVER["PHP_SELF"], "selling_price", "", $param, '', $sortfield, $sortorder, 'right ');
293 print_liste_field_titre($labelcostprice, $_SERVER["PHP_SELF"], "buying_price", "", $param, '', $sortfield, $sortorder, 'right ');
294 print_liste_field_titre("Margin", $_SERVER["PHP_SELF"], "marge", "", $param, '', $sortfield, $sortorder, 'right ');
295 if (getDolGlobalString('DISPLAY_MARGIN_RATES')) {
296 print_liste_field_titre("MarginRate", $_SERVER["PHP_SELF"], "", "", $param, '', $sortfield, $sortorder, 'right ');
297 }
298 if (getDolGlobalString('DISPLAY_MARK_RATES')) {
299 print_liste_field_titre("MarkRate", $_SERVER["PHP_SELF"], "", "", $param, '', $sortfield, $sortorder, 'right ');
300 }
301 print "</tr>\n";
302
303 $cumul_achat = 0;
304 $cumul_vente = 0;
305 $cumul_qty = 0;
306
307 if ($num > 0) {
308 while ($i < $num /*&& $i < $conf->liste_limit*/) {
309 $objp = $db->fetch_object($result);
310 $qty = $objp->product_qty;
311 $pa = $objp->buying_price;
312 $pv = $objp->selling_price;
313 $marge = $objp->marge;
314
315 if ($pa != 0) {
316 $marginRate = (100 * $marge / $pa);
317 // We invert the sign if the margin is negative, regardless of the sign of the purchase price
318 if ($marge < 0) {
319 $marginRate = -$marginRate;
320 }
321 } else {
322 $marginRate = '';
323 }
324
325 if ($pv != 0) {
326 $markRate = (100 * $marge / $pv);
327 // We invert the sign if the margin is negative, as in the original logic
328 if ($marge < 0) {
329 $markRate = -$markRate;
330 }
331 } else {
332 $markRate = '';
333 }
334
335 print '<tr class="oddeven">';
336 if ($id > 0) {
337 print '<td>';
338 $invoicestatic->id = $objp->facid;
339 $invoicestatic->ref = $objp->ref;
340 print $invoicestatic->getNomUrl(1);
341 print "</td>\n";
342 print "<td class=\"center\">";
343 print dol_print_date($db->jdate($objp->datef), 'day')."</td>";
344 } else {
345 print '<td>';
346 if ($objp->rowid > 0) {
347 $product_static->type = $objp->fk_product_type;
348 $product_static->id = $objp->rowid;
349 $product_static->ref = $objp->ref;
350 $product_static->label = $objp->label;
351 $product_static->entity = $objp->pentity;
352 $text = $product_static->getNomUrl(1);
353 print $text .= ' - '.$objp->label;
354 } else {
355 print img_object('', 'product').' '.$langs->trans("NotPredefinedProducts");
356 }
357 print "</td>\n";
358 //print "<td>".$product_static->getNomUrl(1)."</td>\n";
359 }
360 print '<td class="center">'.$qty.'</td>';
361 print '<td class="nowrap right"><span class="amount">'.price(price2num($pv, 'MT')).'</span></td>';
362 print '<td class="nowrap right"><span class="amount">'.price(price2num($pa, 'MT')).'</span></td>';
363 print '<td class="nowrap right"><span class="amount">'.price(price2num($marge, 'MT')).'</span></td>';
364 if (getDolGlobalString('DISPLAY_MARGIN_RATES')) {
365 print '<td class="nowrap right">'.(($marginRate === '') ? 'n/a' : price(price2num($marginRate, 'MT'))."%").'</td>';
366 }
367 if (getDolGlobalString('DISPLAY_MARK_RATES')) {
368 print '<td class="nowrap right">'.(($markRate === '') ? 'n/a' : price(price2num($markRate, 'MT'))."%").'</td>';
369 }
370 print "</tr>\n";
371
372 $i++;
373 $cumul_achat += $objp->buying_price;
374 $cumul_vente += $objp->selling_price;
375 $cumul_qty += $objp->product_qty;
376 }
377 }
378
379 // affichage totaux marges
380
381 $totalMargin = $cumul_vente - $cumul_achat;
382
383 $marginRate = ($cumul_achat != 0) ? (100 * $totalMargin / $cumul_achat) : '';
384 $markRate = ($cumul_vente != 0) ? (100 * $totalMargin / $cumul_vente) : '';
385
386 print '<tr class="liste_total">';
387 if ($id > 0) {
388 print '<td colspan=2>';
389 } else {
390 print '<td>';
391 }
392 print $langs->trans('TotalMargin').'</td>';
393 print '<td class="center">'.$cumul_qty.'</td>';
394 print '<td class="nowrap right">'.price(price2num($cumul_vente, 'MT')).'</td>';
395 print '<td class="nowrap right">'.price(price2num($cumul_achat, 'MT')).'</td>';
396 print '<td class="nowrap right">'.price(price2num($totalMargin, 'MT')).'</td>';
397 if (getDolGlobalString('DISPLAY_MARGIN_RATES')) {
398 print '<td class="nowrap right">'.(($marginRate === '') ? 'n/a' : price(price2num($marginRate, 'MT'))."%").'</td>';
399 }
400 if (getDolGlobalString('DISPLAY_MARK_RATES')) {
401 print '<td class="nowrap right">'.(($markRate === '') ? 'n/a' : price(price2num($markRate, 'MT'))."%").'</td>';
402 }
403 print "</tr>\n";
404
405 print "</table>";
406 print '</div>';
407} else {
408 dol_print_error($db);
409}
410$db->free($result);
411
412
413print '
414<script type="text/javascript">
415$(document).ready(function() {
416 console.log("Init some values");
417 $("#totalMargin").html("'.price(price2num($totalMargin, 'MT')).'");
418 $("#marginRate").html("'.(($marginRate === '') ? 'n/a' : price(price2num($marginRate, 'MT'))."%").'");
419 $("#markRate").html("'.(($markRate === '') ? 'n/a' : price(price2num($markRate, 'MT'))."%").'");
420});
421</script>
422';
423
424// End of page
425llxFooter();
426$db->close();
$id
Definition account.php:48
if( $user->socid > 0) if(! $user->hasRight('accounting', 'chartofaccount')) $object
Definition card.php:66
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:71
Class to manage invoices.
const STATUS_DRAFT
Draft status.
const STATUS_ABANDONED
Classified abandoned and no payment done.
Class to manage generation of HTML components Only common components must be here.
Class to manage products or services.
llxFooter()
Footer empty.
Definition document.php:107
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed information (by default a local PHP server timestamp) Rep...
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0)
Show a picto called object_picto (generic function)
print_barre_liste($title, $page, $file, $options='', $sortfield='', $sortorder='', $morehtmlcenter='', $num=-1, $totalnboflines='', $picto='generic', $pictoisfullpath=0, $morehtmlright='', $morecss='', $limit=-1, $selectlimitsuffix=0, $hidenavigation=0, $pagenavastextinput=0, $morehtmlrightbeforearrow='')
Print a title with navigation controls for pagination.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2)
Show picto whatever it's its name (generic function)
GETPOSTINT($paramname, $method=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
dol_get_fiche_head($links=array(), $active='', $title='', $notab=0, $picto='', $pictoisfullpath=0, $morehtmlright='', $morecss='', $limittoshow=0, $moretabssuffix='', $dragdropfile=0)
Show tabs of a record.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_get_fiche_end($notab=0)
Return tab footer of a card.
price($amount, $form=0, $outlangs='', $trunc=1, $rounding=-1, $forcerounding=-1, $currency_code='')
Function to format a value into an amount for visual output Function used into PDF and HTML pages.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false)
Output date in a string format according to outputlangs (or langs if not defined).
print_liste_field_titre($name, $file="", $field="", $begin="", $moreparam="", $moreattrib="", $sortfield="", $sortorder="", $prefix="", $tooltip="", $forcenowrapcolumntitle=0)
Show title line of an array.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0)
Return value of a param into GET or POST supervariable.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
marges_prepare_head()
Return array of tabs to used on pages for third parties cards.
global $conf
The following vars must be defined: $type2label $form $conf, $lang, The following vars may also be de...
Definition member.php:79
restrictedArea(User $user, $features, $object=0, $tableandshare='', $feature2='', $dbt_keyfield='fk_soc', $dbt_select='rowid', $isdraft=0, $mode=0)
Check permissions of a user to show a page and an object.
accessforbidden($message='', $printheader=1, $printfooter=1, $showonlymessage=0, $params=null)
Show a message to say access is forbidden and stop program.