dolibarr 25.0.0-alpha
factureligne.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2002-2007 Rodolphe Quiedeville <rodolphe@quiedeville.org>
3 * Copyright (C) 2004-2013 Laurent Destailleur <eldy@users.sourceforge.net>
4 * Copyright (C) 2004 Sebastien Di Cintio <sdicintio@ressource-toi.org>
5 * Copyright (C) 2004 Benoit Mortier <benoit.mortier@opensides.be>
6 * Copyright (C) 2005 Marc Barilley / Ocebo <marc@ocebo.com>
7 * Copyright (C) 2005-2014 Regis Houssin <regis.houssin@inodbox.com>
8 * Copyright (C) 2006 Andre Cianfarani <acianfa@free.fr>
9 * Copyright (C) 2007 Franky Van Liedekerke <franky.van.liedekerke@telenet.be>
10 * Copyright (C) 2010-2020 Juanjo Menent <jmenent@2byte.es>
11 * Copyright (C) 2012-2014 Christophe Battarel <christophe.battarel@altairis.fr>
12 * Copyright (C) 2012-2015 Marcos García <marcosgdf@gmail.com>
13 * Copyright (C) 2012 Cédric Salvador <csalvador@gpcsolutions.fr>
14 * Copyright (C) 2012-2014 Raphaël Doursenaud <rdoursenaud@gpcsolutions.fr>
15 * Copyright (C) 2013 Cedric Gross <c.gross@kreiz-it.fr>
16 * Copyright (C) 2013 Florian Henry <florian.henry@open-concept.pro>
17 * Copyright (C) 2016-2022 Ferran Marcet <fmarcet@2byte.es>
18 * Copyright (C) 2018-2024 Alexandre Spangaro <alexandre@inovea-conseil.com>
19 * Copyright (C) 2018 Nicolas ZABOURI <info@inovea-conseil.com>
20 * Copyright (C) 2022 Sylvain Legrand <contact@infras.fr>
21 * Copyright (C) 2023 Gauthier VERDOL <gauthier.verdol@atm-consulting.fr>
22 * Copyright (C) 2023 Nick Fragoulis
23 * Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
24 * Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
25 * Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
26 *
27 * This program is free software; you can redistribute it and/or modify
28 * it under the terms of the GNU General Public License as published by
29 * the Free Software Foundation; either version 3 of the License, or
30 * (at your option) any later version.
31 *
32 * This program is distributed in the hope that it will be useful,
33 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35 * GNU General Public License for more details.
36 *
37 * You should have received a copy of the GNU General Public License
38 * along with this program. If not, see <https://www.gnu.org/licenses/>.
39 */
40
47require_once DOL_DOCUMENT_ROOT.'/core/class/commoninvoice.class.php';
48require_once DOL_DOCUMENT_ROOT.'/core/class/commonobjectline.class.php';
49require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
50require_once DOL_DOCUMENT_ROOT.'/margin/lib/margins.lib.php';
51
57{
61 public $element = 'facturedet';
62
66 public $table_element = 'facturedet';
67
71 public $oldline;
72
74
77 public $fk_facture;
81 public $fk_parent_line;
82
86 public $desc;
90 public $ref_ext;
91
95 public $localtax1_type; // Local tax 1 type
99 public $localtax2_type; // Local tax 2 type
103 public $fk_remise_except; // Link to line into llx_remise_except
107 public $rang = 0;
111 public $fk_fournprice;
115 public $pa_ht;
119 public $marge_tx;
123 public $marque_tx;
124
128 public $tva_npr;
129
133 public $remise_percent;
134
138 public $batch;
142 public $fk_warehouse;
143
144
148 public $origin;
152 public $origin_id;
153
157 public $fk_code_ventilation = 0;
158
159
163 public $date_start;
167 public $date_end;
168
172 public $skip_update_total; // Skip update price total for special lines
173
177 public $fk_prev_id;
178
182 public $packaging;
183
189 public function __construct($db)
190 {
191 $this->db = $db;
192 }
193
200 public function fetch($rowid)
201 {
202 global $conf, $extrafields;
203
204 $extraFieldsCheck = false;
205 $doFetchInOneSqlRequest = getDolGlobalInt('MAIN_DO_FETCH_IN_ONE_SQL_REQUEST');
206
207 if ($doFetchInOneSqlRequest) {
208 // If $extrafields is not a known object, we initialize it
209 if (!isset($extrafields) || !is_object($extrafields)) {
210 require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
211 $extrafields = new ExtraFields($this->db);
212 }
213
214 // Load array of extrafields for elementype = $this->table_element
215 if (empty($extrafields->attributes[$this->table_element]['loaded'])) {
216 $extrafields->fetch_name_optionals_label($this->table_element);
217 }
218
219 $extraFieldsCheck = (
220 !empty($extrafields->attributes[$this->table_element]['label'])
221 && is_array($extrafields->attributes[$this->table_element]['label'])
222 && count($extrafields->attributes[$this->table_element]['label']) > 0
223 );
224 }
225
226 $sql = 'SELECT fd.rowid, fd.fk_facture, fd.fk_parent_line, fd.fk_product, fd.product_type, fd.label as custom_label, fd.description, fd.price, fd.qty, fd.vat_src_code, fd.tva_tx,';
227 $sql .= ' fd.localtax1_tx, fd. localtax2_tx, fd.remise, fd.remise_percent, fd.fk_remise_except, fd.subprice, fd.subprice_ttc, fd.ref_ext,';
228 $sql .= ' fd.date_start as date_start, fd.date_end as date_end, fd.fk_product_fournisseur_price as fk_fournprice, fd.buy_price_ht as pa_ht,';
229 $sql .= ' fd.info_bits, fd.special_code, fd.total_ht, fd.total_tva, fd.total_ttc, fd.total_localtax1, fd.total_localtax2, fd.rang,';
230 $sql .= ' fd.fk_code_ventilation,';
231 $sql .= ' fd.batch, fd.fk_warehouse,';
232 $sql .= ' fd.fk_unit, fd.fk_user_author, fd.fk_user_modif,';
233 $sql .= ' fd.situation_percent, fd.fk_prev_id, fd.extraparams,';
234 $sql .= ' fd.multicurrency_subprice,';
235 $sql .= ' fd.multicurrency_total_ht,';
236 $sql .= ' fd.multicurrency_total_tva,';
237 $sql .= ' fd.multicurrency_total_ttc,';
238 $sql .= ' p.ref as product_ref, p.label as product_label, p.description as product_desc,';
239 $sql .= ' p.packaging';
240
241 if ($doFetchInOneSqlRequest && $extraFieldsCheck) {
242 foreach ($extrafields->attributes[$this->table_element]['label'] as $key => $val) {
243 $type = !empty($extrafields->attributes[$this->table_element]['type'][$key])
244 ? $extrafields->attributes[$this->table_element]['type'][$key]
245 : '';
246
247 if ($type !== 'separate') {
248 if (in_array($type, array('point','multipts','linestrg','polygon'))) {
249 $sql .= ", ST_AsWKT(ef.".$this->db->sanitize($key).") as ".$this->db->sanitize($key);
250 } else {
251 $sql .= ", ef.".$this->db->sanitize($key);
252 }
253 }
254 }
255 }
256
257 $sql .= ' FROM '.$this->db->prefix().'facturedet as fd';
258
259 if ($doFetchInOneSqlRequest && $extraFieldsCheck) {
260 // Add LEFT JOIN for extrafields
261 $sql .= ' LEFT JOIN '.$this->db->prefix().$this->table_element.'_extrafields as ef ON fd.rowid = ef.fk_object';
262 }
263
264 $sql .= ' LEFT JOIN '.$this->db->prefix().'product as p ON fd.fk_product = p.rowid';
265 $sql .= ' WHERE fd.rowid = '.((int) $rowid);
266
267 $result = $this->db->query($sql);
268 if ($result) {
269 $objp = $this->db->fetch_object($result);
270
271 if (!$objp) {
272 $this->error = 'InvoiceLine with id '. $rowid .' not found sql='.$sql;
273 return 0;
274 }
275
276 $this->rowid = $objp->rowid;
277 $this->id = $objp->rowid;
278 $this->fk_facture = $objp->fk_facture;
279 $this->fk_parent_line = $objp->fk_parent_line;
280 $this->label = $objp->custom_label;
281 $this->desc = $objp->description;
282 $this->qty = $objp->qty;
283 $this->subprice = $objp->subprice;
284 $this->subprice_ttc = $objp->subprice_ttc;
285 $this->ref_ext = $objp->ref_ext;
286 $this->vat_src_code = $objp->vat_src_code;
287 $this->tva_tx = $objp->tva_tx;
288 $this->localtax1_tx = $objp->localtax1_tx;
289 $this->localtax2_tx = $objp->localtax2_tx;
290 $this->remise_percent = $objp->remise_percent;
291 $this->fk_remise_except = $objp->fk_remise_except;
292 $this->fk_product = $objp->fk_product;
293 $this->product_type = $objp->product_type;
294 $this->date_start = $this->db->jdate($objp->date_start);
295 $this->date_end = $this->db->jdate($objp->date_end);
296 $this->info_bits = $objp->info_bits;
297 $this->tva_npr = ((($objp->info_bits & 1) == 1) ? 1 : 0);
298 $this->special_code = $objp->special_code;
299 $this->total_ht = $objp->total_ht;
300 $this->total_tva = $objp->total_tva;
301 $this->total_localtax1 = $objp->total_localtax1;
302 $this->total_localtax2 = $objp->total_localtax2;
303 $this->total_ttc = $objp->total_ttc;
304 $this->fk_code_ventilation = $objp->fk_code_ventilation;
305 $this->batch = $objp->batch;
306 $this->fk_warehouse = $objp->fk_warehouse;
307 $this->rang = $objp->rang;
308 $this->fk_fournprice = $objp->fk_fournprice;
309 $marginInfos = getMarginInfos($objp->subprice, $objp->remise_percent, $objp->tva_tx, $objp->localtax1_tx, $objp->localtax2_tx, $this->fk_fournprice, $objp->pa_ht);
310 $this->pa_ht = $marginInfos[0];
311 $this->marge_tx = (string) $marginInfos[1];
312 $this->marque_tx = (string) $marginInfos[2];
313
314 $this->ref = $objp->product_ref; // deprecated
315
316 $this->product_ref = $objp->product_ref;
317 $this->product_label = $objp->product_label;
318 $this->product_desc = $objp->product_desc;
319
320 $this->fk_unit = $objp->fk_unit;
321 $this->fk_user_modif = $objp->fk_user_modif;
322 $this->fk_user_author = $objp->fk_user_author;
323
324 $this->situation_percent = $objp->situation_percent;
325 $this->fk_prev_id = $objp->fk_prev_id;
326
327 $this->extraparams = !empty($objp->extraparams) ? (array) json_decode($objp->extraparams, true) : array();
328
329 $this->multicurrency_subprice = $objp->multicurrency_subprice;
330 $this->multicurrency_total_ht = $objp->multicurrency_total_ht;
331 $this->multicurrency_total_tva = $objp->multicurrency_total_tva;
332 $this->multicurrency_total_ttc = $objp->multicurrency_total_ttc;
333
334 $this->packaging = $objp->packaging;
335
336 if ($doFetchInOneSqlRequest && $extraFieldsCheck) {
337 $this->array_options = array();
338
339 foreach ($extrafields->attributes[$this->table_element]['label'] as $key => $val) {
340 $type = !empty($extrafields->attributes[$this->table_element]['type'][$key])
341 ? $extrafields->attributes[$this->table_element]['type'][$key]
342 : '';
343
344 if ($type !== 'separate') {
345 $rawval = $objp->$key;
346
347 // date/datetime
348 if (in_array($type, array('date', 'datetime'))) {
349 $this->array_options['options_' . $key] = $this->db->jdate($rawval);
350 } elseif ($type == 'password') {
351 if (!empty($rawval) && preg_match('/^dolcrypt:/', $rawval)) {
352 $this->array_options['options_' . $key] = dolDecrypt($rawval);
353 } else {
354 $this->array_options['options_' . $key] = $rawval;
355 }
356 } else {
357 $this->array_options['options_' . $key] = $rawval;
358 }
359 }
360 }
361
362 // Champs "computed"
363 foreach ($extrafields->attributes[$this->table_element]['label'] as $key => $val) {
364 if (!empty($extrafields->attributes[$this->table_element]['computed'][$key])) {
365 if (empty($conf->disable_compute)) {
366 global $objectoffield;
367 $objectoffield = $this;
368 $this->array_options['options_' . $key] = dol_eval((string) $extrafields->attributes[$this->table_element]['computed'][$key], 1, 0, '2');
369 }
370 }
371 }
372 }
373
374 if (!$doFetchInOneSqlRequest) {
375 // Retrieve all extrafield
376 $this->fetch_optionals();
377 }
378
379 $this->db->free($result);
380
381 return 1;
382 } else {
383 $this->error = $this->db->lasterror();
384 return -1;
385 }
386 }
387
395 public function insert($notrigger = 0, $noerrorifdiscountalreadylinked = 0)
396 {
397 global $langs, $user;
398
399 $error = 0;
400
401 $pa_ht_isemptystring = (empty($this->pa_ht) && $this->pa_ht == ''); // If true, we can use a default value. If this->pa_ht = '0', we must use '0'.
402 $this->pa_ht = (float) $this->pa_ht; // convert to float but after checking if value is empty
403
404 dol_syslog(get_class($this)."::insert rang=".$this->rang, LOG_DEBUG);
405
406 // Clean parameters
407 $this->desc = trim($this->desc);
408 if (empty($this->tva_tx)) {
409 $this->tva_tx = 0;
410 }
411 if (empty($this->localtax1_tx)) {
412 $this->localtax1_tx = 0;
413 }
414 if (empty($this->localtax2_tx)) {
415 $this->localtax2_tx = 0;
416 }
417 if (empty($this->localtax1_type)) {
418 $this->localtax1_type = 0;
419 }
420 if (empty($this->localtax2_type)) {
421 $this->localtax2_type = 0;
422 }
423 if (empty($this->total_localtax1)) {
424 $this->total_localtax1 = 0;
425 }
426 if (empty($this->total_localtax2)) {
427 $this->total_localtax2 = 0;
428 }
429 if (empty($this->rang)) {
430 $this->rang = 0;
431 }
432 if (empty($this->remise_percent)) {
433 $this->remise_percent = 0;
434 }
435 if (empty($this->info_bits)) {
436 $this->info_bits = 0;
437 }
438 if (empty($this->subprice)) {
439 $this->subprice = 0;
440 }
441 if (empty($this->subprice_ttc)) {
442 $this->subprice_ttc = 0;
443 }
444 if (empty($this->ref_ext)) {
445 $this->ref_ext = '';
446 }
447 if (empty($this->special_code)) {
448 $this->special_code = 0;
449 }
450 if (empty($this->fk_parent_line)) {
451 $this->fk_parent_line = 0;
452 }
453 if (empty($this->fk_prev_id)) {
454 $this->fk_prev_id = 0;
455 }
456 if (!isset($this->situation_percent) || $this->situation_percent > 100 || (string) $this->situation_percent == '') {
457 $this->situation_percent = 100;
458 }
459
460 if (empty($this->multicurrency_subprice)) {
461 $this->multicurrency_subprice = 0;
462 }
463 if (empty($this->multicurrency_total_ht)) {
464 $this->multicurrency_total_ht = 0;
465 }
466 if (empty($this->multicurrency_total_tva)) {
467 $this->multicurrency_total_tva = 0;
468 }
469 if (empty($this->multicurrency_total_ttc)) {
470 $this->multicurrency_total_ttc = 0;
471 }
472
473 // if buy price not defined, define buyprice as configured in margin admin
474 if ($this->pa_ht == 0 && $pa_ht_isemptystring) {
475 $result = $this->defineBuyPrice($this->subprice, $this->remise_percent, $this->fk_product);
476 if ($result < 0) {
477 return $result;
478 } else {
479 $this->pa_ht = $result;
480 }
481 }
482
483 // Check parameters
484 if ($this->product_type < 0) {
485 $this->error = 'ErrorProductTypeMustBe0orMore';
486 return -1;
487 }
488 if (!empty($this->fk_product) && $this->fk_product > 0) {
489 // Check product exists
490 $result = Product::isExistingObject('product', $this->fk_product);
491 if ($result <= 0) {
492 $this->error = 'ErrorProductIdDoesNotExists';
493 dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
494 return -1;
495 }
496 }
497
498 $this->db->begin();
499
500 // Update line in database
501 $sql = 'INSERT INTO '.MAIN_DB_PREFIX.'facturedet';
502 $sql .= ' (fk_facture, fk_parent_line, label, description, qty,';
503 $sql .= ' vat_src_code, tva_tx, localtax1_tx, localtax2_tx, localtax1_type, localtax2_type,';
504 $sql .= ' fk_product, product_type, remise_percent, subprice, subprice_ttc, ref_ext, fk_remise_except,';
505 $sql .= ' date_start, date_end, fk_code_ventilation,';
506 $sql .= ' rang, special_code, fk_product_fournisseur_price, buy_price_ht,';
507 $sql .= ' info_bits, total_ht, total_tva, total_ttc, total_localtax1, total_localtax2,';
508 $sql .= ' situation_percent, fk_prev_id,';
509 $sql .= ' fk_unit, fk_user_author, fk_user_modif,';
510 $sql .= ' fk_multicurrency, multicurrency_code, multicurrency_subprice, multicurrency_total_ht, multicurrency_total_tva, multicurrency_total_ttc,';
511 $sql .= ' batch, fk_warehouse';
512 $sql .= ')';
513 $sql .= " VALUES (".((int) $this->fk_facture).",";
514 $sql .= " ".($this->fk_parent_line > 0 ? ((int) $this->fk_parent_line) : "null").",";
515 $sql .= " ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null").",";
516 $sql .= " '".$this->db->escape($this->desc)."',";
517 $sql .= " ".price2num($this->qty).",";
518 $sql .= " ".(empty($this->vat_src_code) ? "''" : "'".$this->db->escape($this->vat_src_code)."'").",";
519 $sql .= " ".price2num($this->tva_tx).",";
520 $sql .= " ".price2num($this->localtax1_tx).",";
521 $sql .= " ".price2num($this->localtax2_tx).",";
522 $sql .= " '".$this->db->escape((string) $this->localtax1_type)."',";
523 $sql .= " '".$this->db->escape((string) $this->localtax2_type)."',";
524 $sql .= ' '.((!empty($this->fk_product) && $this->fk_product > 0) ? ((int) $this->fk_product) : "null").',';
525 $sql .= " ".((int) $this->product_type).",";
526 $sql .= " ".price2num($this->remise_percent).",";
527 $sql .= " ".price2num($this->subprice).",";
528 $sql .= " ".price2num($this->subprice_ttc).",";
529 $sql .= " '".$this->db->escape($this->ref_ext)."',";
530 $sql .= ' '.(!empty($this->fk_remise_except) ? ((int) $this->fk_remise_except) : "null").',';
531 $sql .= " ".(!empty($this->date_start) ? "'".$this->db->idate($this->date_start)."'" : "null").",";
532 $sql .= " ".(!empty($this->date_end) ? "'".$this->db->idate($this->date_end)."'" : "null").",";
533 $sql .= ' '.((int) $this->fk_code_ventilation).',';
534 $sql .= ' '.((int) $this->rang).',';
535 $sql .= ' '.((int) $this->special_code).',';
536 $sql .= ' '.(!empty($this->fk_fournprice) ? ((int) $this->fk_fournprice) : "null").',';
537 $sql .= ' '.price2num($this->pa_ht).',';
538 $sql .= " '".$this->db->escape((string) $this->info_bits)."',";
539 $sql .= " ".price2num($this->total_ht).",";
540 $sql .= " ".price2num($this->total_tva).",";
541 $sql .= " ".price2num($this->total_ttc).",";
542 $sql .= " ".price2num($this->total_localtax1).",";
543 $sql .= " ".price2num($this->total_localtax2);
544 $sql .= ", ".((float) $this->situation_percent);
545 $sql .= ", ".(!empty($this->fk_prev_id) ? ((int) $this->fk_prev_id) : "null");
546 $sql .= ", ".(!$this->fk_unit ? 'NULL' : ((int) $this->fk_unit));
547 $sql .= ", ".((int) $user->id);
548 $sql .= ", ".((int) $user->id);
549 $sql .= ", ".(int) $this->fk_multicurrency;
550 $sql .= ", '".$this->db->escape($this->multicurrency_code)."'";
551 $sql .= ", ".price2num($this->multicurrency_subprice);
552 $sql .= ", ".price2num($this->multicurrency_total_ht);
553 $sql .= ", ".price2num($this->multicurrency_total_tva);
554 $sql .= ", ".price2num($this->multicurrency_total_ttc);
555 $sql .= ", '".$this->db->escape($this->batch)."'";
556 $sql .= ", ".((int) $this->fk_warehouse);
557 $sql .= ')';
558
559 dol_syslog(get_class($this)."::insert", LOG_DEBUG);
560 $resql = $this->db->query($sql);
561 if ($resql) {
562 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'facturedet');
563 $this->rowid = $this->id; // For backward compatibility
564
565 if (!$error) {
566 $result = $this->insertExtraFields();
567 if ($result < 0) {
568 $error++;
569 }
570 }
571
572 // If fk_remise_except is defined, the discount is linked to the invoice
573 // which flags it as "consumed".
574 if ($this->fk_remise_except && empty($error)) {
575 $discount = new DiscountAbsolute($this->db);
576 $result = $discount->fetch($this->fk_remise_except);
577 if ($result >= 0) {
578 // Check if discount was found
579 if ($result > 0) {
580 // Check if discount not already affected to another invoice
581 if ($discount->fk_facture_line > 0) {
582 if (empty($noerrorifdiscountalreadylinked)) {
583 $this->error = $langs->trans("ErrorDiscountAlreadyUsed", $discount->id);
584 dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
585 $this->db->rollback();
586 return -3;
587 }
588 } else {
589 $result = $discount->link_to_invoice($this->rowid, 0);
590 if ($result < 0) {
591 $this->error = $discount->error;
592 dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
593 $this->db->rollback();
594 return -3;
595 }
596 }
597 } else {
598 $this->error = $langs->trans("ErrorADiscountThatHasBeenRemovedIsIncluded");
599 dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
600 $this->db->rollback();
601 return -3;
602 }
603 } else {
604 $this->error = $discount->error;
605 dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
606 $this->db->rollback();
607 return -3;
608 }
609 }
610
611 if (!$notrigger && empty($error)) {
612 // Call trigger
613 $result = $this->call_trigger('LINEBILL_INSERT', $user);
614 if ($result < 0) {
615 $this->db->rollback();
616 return -2;
617 }
618 // End call triggers
619 }
620
621 if (!$error) {
622 $this->db->commit();
623 return $this->id;
624 }
625
626 foreach ($this->errors as $errmsg) {
627 dol_syslog(get_class($this)."::insert ".$errmsg, LOG_ERR);
628 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
629 }
630 $this->db->rollback();
631 return -3;
632 } else {
633 $this->error = $this->db->lasterror();
634 $this->db->rollback();
635 return -2;
636 }
637 }
638
646 public function update($user = null, $notrigger = 0)
647 {
648 global $user;
649
650 $error = 0;
651
652 $pa_ht_isemptystring = (empty($this->pa_ht) && $this->pa_ht == ''); // If true, we can use a default value. If this->pa_ht = '0', we must use '0'.
653
654 // Clean parameters
655 $this->desc = trim($this->desc);
656 if (empty($this->ref_ext)) {
657 $this->ref_ext = '';
658 }
659 if (empty($this->subprice_ttc)) {
660 $this->subprice_ttc = 0;
661 }
662 if (empty($this->tva_tx)) {
663 $this->tva_tx = 0;
664 }
665 if (empty($this->localtax1_tx)) {
666 $this->localtax1_tx = 0;
667 }
668 if (empty($this->localtax2_tx)) {
669 $this->localtax2_tx = 0;
670 }
671 if (empty($this->localtax1_type)) {
672 $this->localtax1_type = 0;
673 }
674 if (empty($this->localtax2_type)) {
675 $this->localtax2_type = 0;
676 }
677 if (empty($this->total_localtax1)) {
678 $this->total_localtax1 = 0;
679 }
680 if (empty($this->total_localtax2)) {
681 $this->total_localtax2 = 0;
682 }
683 if (empty($this->remise_percent)) {
684 $this->remise_percent = 0;
685 }
686 if (empty($this->info_bits)) {
687 $this->info_bits = 0;
688 }
689 if (empty($this->special_code)) {
690 $this->special_code = 0;
691 }
692 if (empty($this->product_type)) {
693 $this->product_type = 0;
694 }
695 if (empty($this->fk_parent_line)) {
696 $this->fk_parent_line = 0;
697 }
698 if (!isset($this->situation_percent) || $this->situation_percent > 100 || (string) $this->situation_percent == '') {
699 $this->situation_percent = 100;
700 }
701 if (empty($this->pa_ht)) {
702 $this->pa_ht = 0;
703 }
704
705 if (empty($this->multicurrency_subprice)) {
706 $this->multicurrency_subprice = 0;
707 }
708 if (empty($this->multicurrency_total_ht)) {
709 $this->multicurrency_total_ht = 0;
710 }
711 if (empty($this->multicurrency_total_tva)) {
712 $this->multicurrency_total_tva = 0;
713 }
714 if (empty($this->multicurrency_total_ttc)) {
715 $this->multicurrency_total_ttc = 0;
716 }
717
718 // Check parameters
719 if ($this->product_type < 0) {
720 return -1;
721 }
722
723 // if buy price not provided, define buyprice as configured in margin admin
724 if ($this->pa_ht == 0 && $pa_ht_isemptystring) {
725 // We call defineBuyPrice only if data was not provided (if input was '0', we will not go here and value will remaine '0')
726 $result = $this->defineBuyPrice($this->subprice, $this->remise_percent, $this->fk_product);
727 if ($result < 0) {
728 return $result;
729 } else {
730 $this->pa_ht = $result;
731 }
732 }
733
734 $this->db->begin();
735
736 // Update line in database
737 $sql = "UPDATE ".MAIN_DB_PREFIX."facturedet SET";
738 $sql .= " description='".$this->db->escape($this->desc)."'";
739 $sql .= ", ref_ext='".$this->db->escape($this->ref_ext)."'";
740 $sql .= ", label=".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
741 $sql .= ", subprice=".price2num($this->subprice);
742 $sql .= ", subprice_ttc=".price2num($this->subprice_ttc);
743 $sql .= ", remise_percent=".price2num($this->remise_percent);
744 if ($this->fk_remise_except) {
745 $sql .= ", fk_remise_except = ".((int) $this->fk_remise_except);
746 } else {
747 $sql .= ", fk_remise_except = null";
748 }
749 $sql .= ", vat_src_code = '".(empty($this->vat_src_code) ? '' : $this->db->escape($this->vat_src_code))."'";
750 $sql .= ", tva_tx=".price2num($this->tva_tx);
751 $sql .= ", localtax1_tx=".price2num($this->localtax1_tx);
752 $sql .= ", localtax2_tx=".price2num($this->localtax2_tx);
753 $sql .= ", localtax1_type='".$this->db->escape((string) $this->localtax1_type)."'";
754 $sql .= ", localtax2_type='".$this->db->escape((string) $this->localtax2_type)."'";
755 $sql .= ", qty = ".((float) price2num($this->qty));
756 $sql .= ", date_start = ".(!empty($this->date_start) ? "'".$this->db->idate($this->date_start)."'" : "null");
757 $sql .= ", date_end = ".(!empty($this->date_end) ? "'".$this->db->idate($this->date_end)."'" : "null");
758 $sql .= ", product_type = ".((int) $this->product_type);
759 $sql .= ", info_bits = '".$this->db->escape((string) $this->info_bits)."'";
760 $sql .= ", special_code = " . (int) $this->special_code;
761 if (empty($this->skip_update_total)) {
762 $sql .= ", total_ht = ".price2num($this->total_ht);
763 $sql .= ", total_tva = ".price2num($this->total_tva);
764 $sql .= ", total_ttc = ".price2num($this->total_ttc);
765 $sql .= ", total_localtax1 = ".price2num($this->total_localtax1);
766 $sql .= ", total_localtax2 = ".price2num($this->total_localtax2);
767 }
768 $sql .= ", fk_product_fournisseur_price = ".(!empty($this->fk_fournprice) ? "'".$this->db->escape((string) $this->fk_fournprice)."'" : "null");
769 $sql .= ", buy_price_ht = ".(($this->pa_ht || (string) $this->pa_ht === '0') ? price2num($this->pa_ht) : "null"); // $this->pa_ht should always be defined (set to 0 or to sell price depending on option)
770 $sql .= ", fk_parent_line = ".($this->fk_parent_line > 0 ? ((int) $this->fk_parent_line) : "null");
771 if (!empty($this->rang)) {
772 $sql .= ", rang = ".((int) $this->rang);
773 }
774 $sql .= ", situation_percent = ".((float) $this->situation_percent);
775 $sql .= ", fk_unit = ".(!$this->fk_unit ? 'NULL' : ((int) $this->fk_unit));
776 $sql .= ", fk_user_modif = ".((int) $user->id);
777
778 // Multicurrency
779 $sql .= ", multicurrency_subprice=".price2num($this->multicurrency_subprice);
780 $sql .= ", multicurrency_total_ht=".price2num($this->multicurrency_total_ht);
781 $sql .= ", multicurrency_total_tva=".price2num($this->multicurrency_total_tva);
782 $sql .= ", multicurrency_total_ttc=".price2num($this->multicurrency_total_ttc);
783
784 $sql .= ", batch = '".$this->db->escape($this->batch)."'";
785 $sql .= ", fk_warehouse = ".((int) $this->fk_warehouse);
786
787 $sql .= " WHERE rowid = ".((int) $this->rowid);
788
789 dol_syslog(get_class($this)."::update", LOG_DEBUG);
790 $resql = $this->db->query($sql);
791 if ($resql) {
792 if (!$error) {
793 $this->id = $this->rowid;
794 $result = $this->insertExtraFields();
795 if ($result < 0) {
796 $error++;
797 }
798 }
799
800 if (!$error && !$notrigger) {
801 // Call trigger
802 $result = $this->call_trigger('LINEBILL_MODIFY', $user);
803 if ($result < 0) {
804 $this->db->rollback();
805 return -2;
806 }
807 // End call triggers
808 }
809
810 if (!$error) {
811 $this->db->commit();
812 return 1;
813 }
814
815 foreach ($this->errors as $errmsg) {
816 dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
817 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
818 }
819
820 $this->db->rollback();
821 return -3;
822 } else {
823 $this->error = $this->db->error();
824 $this->db->rollback();
825 return -2;
826 }
827 }
828
836 public function delete($tmpuser = null, $notrigger = 0)
837 {
838 global $user;
839
840 $this->db->begin();
841
842 // Call trigger
843 if (empty($notrigger)) {
844 $result = $this->call_trigger('LINEBILL_DELETE', $user);
845 if ($result < 0) {
846 $this->db->rollback();
847 return -1;
848 }
849 }
850 // End call triggers
851
852 // extrafields
853 $result = $this->deleteExtraFields();
854 if ($result < 0) {
855 $this->db->rollback();
856 return -1;
857 }
858
859 // Free discount linked to invoice line
860 $sql = 'UPDATE '.MAIN_DB_PREFIX.'societe_remise_except';
861 $sql .= ' SET fk_facture_line = NULL';
862 $sql .= ' WHERE fk_facture_line = '.((int) $this->id);
863
864 dol_syslog(get_class($this)."::deleteline", LOG_DEBUG);
865 $result = $this->db->query($sql);
866 if (!$result) {
867 $this->error = $this->db->error();
868 $this->errors[] = $this->error;
869 $this->db->rollback();
870 return -1;
871 }
872
873 $sql = 'UPDATE '.MAIN_DB_PREFIX.'element_time';
874 $sql .= ' SET invoice_id = NULL, invoice_line_id = NULL';
875 $sql .= ' WHERE invoice_line_id = '.((int) $this->id);
876 if (!$this->db->query($sql)) {
877 $this->error = $this->db->error()." sql=".$sql;
878 $this->errors[] = $this->error;
879 $this->db->rollback();
880 return -1;
881 }
882
883 $sql = "DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".((int) $this->id);
884
885 if ($this->db->query($sql)) {
886 $this->db->commit();
887 return 1;
888 } else {
889 $this->error = $this->db->error()." sql=".$sql;
890 $this->errors[] = $this->error;
891 $this->db->rollback();
892 return -1;
893 }
894 }
895
896 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
903 public function update_total()
904 {
905 // phpcs:enable
906 $this->db->begin();
907 dol_syslog(get_class($this)."::update_total", LOG_DEBUG);
908
909 // Clean parameters
910 if (empty($this->total_localtax1)) {
911 $this->total_localtax1 = 0;
912 }
913 if (empty($this->total_localtax2)) {
914 $this->total_localtax2 = 0;
915 }
916
917 // Update line in database
918 $sql = "UPDATE ".MAIN_DB_PREFIX."facturedet SET";
919 $sql .= " total_ht=".price2num($this->total_ht);
920 $sql .= ",total_tva=".price2num($this->total_tva);
921 $sql .= ",total_localtax1=".price2num($this->total_localtax1);
922 $sql .= ",total_localtax2=".price2num($this->total_localtax2);
923 $sql .= ",total_ttc=".price2num($this->total_ttc);
924 $sql .= " WHERE rowid = ".((int) $this->rowid);
925
926 dol_syslog(get_class($this)."::update_total", LOG_DEBUG);
927
928 $resql = $this->db->query($sql);
929 if ($resql) {
930 $this->db->commit();
931 return 1;
932 } else {
933 $this->error = $this->db->error();
934 $this->db->rollback();
935 return -2;
936 }
937 }
938
939 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
949 public function get_prev_progress($invoiceid, $include_credit_note = true)
950 {
951 // phpcs:enable
952 global $invoicecache;
953
954 if (is_null($this->fk_prev_id) || empty($this->fk_prev_id) || $this->fk_prev_id == "") {
955 return 0;
956 } else {
957 // If invoice is not a situation invoice, this->fk_prev_id is used for something else
958 if (!isset($invoicecache[$invoiceid])) {
959 $invoicecache[$invoiceid] = new Facture($this->db);
960 $invoicecache[$invoiceid]->fetch($invoiceid);
961 }
962 if (empty($invoicecache[$invoiceid]->situation_cycle_ref)) {
963 return 0;
964 }
965
966 $sql = "SELECT situation_percent FROM ".MAIN_DB_PREFIX."facturedet";
967 $sql .= " WHERE rowid = ".((int) $this->fk_prev_id);
968
969 $resql = $this->db->query($sql);
970
971 if ($resql && $this->db->num_rows($resql) > 0) {
972 $returnPercent = 0;
973
974 $obj = $this->db->fetch_object($resql);
975 if ($obj) {
976 $returnPercent = (float) $obj->situation_percent;
977 }
978
979 if ($include_credit_note) {
980 $sql = 'SELECT fd.situation_percent FROM '.MAIN_DB_PREFIX.'facturedet fd';
981 $sql .= ' JOIN '.MAIN_DB_PREFIX.'facture f ON (f.rowid = fd.fk_facture) ';
982 $sql .= " WHERE fd.fk_prev_id = ".((int) $this->fk_prev_id);
983 $sql .= " AND f.situation_cycle_ref = ".((int) $invoicecache[$invoiceid]->situation_cycle_ref); // Prevent cycle outed
984 $sql .= " AND f.type = ".((int) Facture::TYPE_CREDIT_NOTE);
985
986 $res = $this->db->query($sql);
987 if ($res) {
988 while ($obj = $this->db->fetch_object($res)) {
989 $returnPercent += (float) $obj->situation_percent;
990 }
991 } else {
992 dol_print_error($this->db);
993 }
994 }
995
996 return $returnPercent;
997 } else {
998 $this->error = $this->db->error();
999 dol_syslog(get_class($this)."::select Error ".$this->error, LOG_ERR);
1000 $this->db->rollback();
1001 return -1;
1002 }
1003 }
1004 }
1005
1006 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1016 public function getAllPrevProgress($invoiceid, $include_credit_note = true)
1017 {
1018 // phpcs:enable
1019 global $invoicecache;
1020
1021 if (is_null($this->fk_prev_id) || empty($this->fk_prev_id) || $this->fk_prev_id == "") {
1022 return 0;
1023 } else {
1024 // If invoice is not a situation invoice, this->fk_prev_id is used for something else
1025 if (!isset($invoicecache[$invoiceid])) {
1026 $invoicecache[$invoiceid] = new Facture($this->db);
1027 $invoicecache[$invoiceid]->fetch($invoiceid);
1028 }
1029 if (!$invoicecache[$invoiceid]->isSituationInvoice()) {
1030 return 0;
1031 }
1032
1033 $all_found = false;
1034 $lastprevid = $this->fk_prev_id;
1035 $cumulated_percent = 0.0;
1036
1037 while (!$all_found) {
1038 $sql = "SELECT situation_percent, fk_prev_id FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".((int) $lastprevid);
1039 $resql = $this->db->query($sql);
1040
1041 if ($resql && $this->db->num_rows($resql) > 0) {
1042 $obj = $this->db->fetch_object($resql);
1043 $cumulated_percent += (float) $obj->situation_percent;
1044
1045 if ($include_credit_note) {
1046 $sql_credit_note = 'SELECT fd.situation_percent FROM '.MAIN_DB_PREFIX.'facturedet fd';
1047 $sql_credit_note .= ' JOIN '.MAIN_DB_PREFIX.'facture f ON (f.rowid = fd.fk_facture) ';
1048 $sql_credit_note .= " WHERE fd.fk_prev_id = ".((int) $lastprevid);
1049 $sql_credit_note .= " AND f.situation_cycle_ref = ".((int) $invoicecache[$invoiceid]->situation_cycle_ref); // Prevent cycle outed
1050 $sql_credit_note .= " AND f.type = ".Facture::TYPE_CREDIT_NOTE;
1051
1052 $res_credit_note = $this->db->query($sql_credit_note);
1053 if ($res_credit_note) {
1054 while ($cn = $this->db->fetch_object($res_credit_note)) {
1055 $cumulated_percent += (float) $cn->situation_percent;
1056 }
1057 } else {
1058 dol_print_error($this->db);
1059 }
1060 }
1061
1062 // Si fk_prev_id, on continue
1063 if ($obj->fk_prev_id) {
1064 $lastprevid = $obj->fk_prev_id;
1065 } else { // else we stop the loop
1066 $all_found = true;
1067 }
1068 } else {
1069 $this->error = $this->db->error();
1070 dol_syslog(get_class($this)."::select Error ".$this->error, LOG_ERR);
1071 $this->db->rollback();
1072 return -1;
1073 }
1074 }
1075 return $cumulated_percent;
1076 }
1077 }
1078
1092 public function getSituationRatio()
1093 {
1094 if (getDolGlobalInt('INVOICE_USE_SITUATION') === 1) {
1095 // in legacy mode, the situation invoice line stores the (cumulative) state of the
1096 // cycle at the current situation. To get the delta, we need to subtract the
1097 // state at the previous situation (if applicable).
1098 $prevProgress = $this->get_prev_progress($this->fk_facture);
1099
1100 if ($this->situation_percent == 0) {
1101 // should not happen
1102 return 0;
1103 }
1104
1105 return ($this->situation_percent - $prevProgress) / $this->situation_percent;
1106 }
1107 // new mode (INVOICE_USE_SITUATION == 2):
1108 // no ratio needed (data stored on line is already a delta)
1109 // or not a situation invoice: no ratio needed either
1110 return 1;
1111 }
1112}
$object ref
Definition info.php:90
Parent class of all other business classes for details of elements (invoices, contracts,...
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
defineBuyPrice($unitPrice=0.0, $discountPercent=0.0, $fk_product=0)
Get buy price to use for margin calculation.
static isExistingObject($element, $id, $ref='', $ref_ext='')
Check if an object id or ref exists If you don't need or want to instantiate the object and just need...
deleteExtraFields()
Delete all extra fields values for the current object.
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
Class to manage absolute discounts.
Class to manage standard extra fields.
Class to manage invoices.
const TYPE_CREDIT_NOTE
Credit note invoice.
Class to manage invoice lines.
insert($notrigger=0, $noerrorifdiscountalreadylinked=0)
Insert line into database.
get_prev_progress($invoiceid, $include_credit_note=true)
Returns situation_percent of the previous line.
fetch($rowid)
Load invoice line from database.
update($user=null, $notrigger=0)
Update line into database.
__construct($db)
Constructor.
getSituationRatio()
Determines if we are using situation invoices.
getAllPrevProgress($invoiceid, $include_credit_note=true)
Returns situation_percent of all the previous line.
update_total()
Update DB line fields total_xxx Used by migration.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_eval($s, $returnvalue=1, $hideerrors=1, $onlysimplestring='1')
Replace eval function to add more security.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
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
getMarginInfos($pv_ht, $remise_percent, $tva_tx, $localtax1_tx, $localtax2_tx, $fk_pa, $pa_ht)
Return an array with margins information of a line.
dolDecrypt($chain, $key='', $patterntotest='')
Decode a string with a symmetric encryption.