Passed
Branch develop (356b3a)
by
unknown
98:06
created

Facture::getTooltipContentArray()   D

Complexity

Conditions 18

Size

Total Lines 56
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 18
eloc 35
nop 1
dl 0
loc 56
rs 4.8666
c 1
b 0
f 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/* Copyright (C) 2002-2007  Rodolphe Quiedeville    <[email protected]>
3
 * Copyright (C) 2004-2013  Laurent Destailleur     <[email protected]>
4
 * Copyright (C) 2004       Sebastien Di Cintio     <[email protected]>
5
 * Copyright (C) 2004       Benoit Mortier          <[email protected]>
6
 * Copyright (C) 2005       Marc Barilley / Ocebo   <[email protected]>
7
 * Copyright (C) 2005-2014  Regis Houssin           <[email protected]>
8
 * Copyright (C) 2006       Andre Cianfarani        <[email protected]>
9
 * Copyright (C) 2007       Franky Van Liedekerke   <[email protected]>
10
 * Copyright (C) 2010-2020  Juanjo Menent           <[email protected]>
11
 * Copyright (C) 2012-2014  Christophe Battarel     <[email protected]>
12
 * Copyright (C) 2012-2015  Marcos García           <[email protected]>
13
 * Copyright (C) 2012       Cédric Salvador         <[email protected]>
14
 * Copyright (C) 2012-2014  Raphaël Doursenaud      <[email protected]>
15
 * Copyright (C) 2013       Cedric Gross            <[email protected]>
16
 * Copyright (C) 2013       Florian Henry           <[email protected]>
17
 * Copyright (C) 2016-2022  Ferran Marcet           <[email protected]>
18
 * Copyright (C) 2018-2022  Alexandre Spangaro		<[email protected]>
19
 * Copyright (C) 2018       Nicolas ZABOURI         <[email protected]>
20
 * Copyright (C) 2022       Sylvain Legrand         <[email protected]>
21
 * Copyright (C) 2022      	Gauthier VERDOL       	<[email protected]>
22
 *
23
 * This program is free software; you can redistribute it and/or modify
24
 * it under the terms of the GNU General Public License as published by
25
 * the Free Software Foundation; either version 3 of the License, or
26
 * (at your option) any later version.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
31
 * GNU General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU General Public License
34
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
35
 */
36
37
/**
38
 *	\file       htdocs/compta/facture/class/facture.class.php
39
 *	\ingroup    facture
40
 *	\brief      File of class to manage invoices
41
 */
42
43
require_once DOL_DOCUMENT_ROOT.'/core/class/commoninvoice.class.php';
44
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobjectline.class.php';
45
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
46
require_once DOL_DOCUMENT_ROOT.'/societe/class/client.class.php';
47
require_once DOL_DOCUMENT_ROOT.'/margin/lib/margins.lib.php';
48
require_once DOL_DOCUMENT_ROOT.'/multicurrency/class/multicurrency.class.php';
49
50
if (isModEnabled('accounting')) {
51
	require_once DOL_DOCUMENT_ROOT.'/core/class/html.formaccounting.class.php';
52
}
53
if (isModEnabled('accounting')) {
54
	require_once DOL_DOCUMENT_ROOT.'/accountancy/class/accountingaccount.class.php';
55
}
56
57
/**
58
 *	Class to manage invoices
59
 */
60
class Facture extends CommonInvoice
61
{
62
	/**
63
	 * @var string ID to identify managed object
64
	 */
65
	public $element = 'facture';
66
67
	/**
68
	 * @var string Name of table without prefix where object is stored
69
	 */
70
	public $table_element = 'facture';
71
72
	/**
73
	 * @var string    Name of subtable line
74
	 */
75
	public $table_element_line = 'facturedet';
76
77
	/**
78
	 * @var string Fieldname with ID of parent key if this field has a parent
79
	 */
80
	public $fk_element = 'fk_facture';
81
82
	/**
83
	 * @var string String with name of icon for myobject.
84
	 */
85
	public $picto = 'bill';
86
87
	/**
88
	 * 0=No test on entity, 1=Test with field entity, 2=Test with link by societe
89
	 * @var int
90
	 */
91
	public $ismultientitymanaged = 1;
92
93
	/**
94
	 * 0=Default, 1=View may be restricted to sales representative only if no permission to see all or to company of external user if external user
95
	 * @var integer
96
	 */
97
	public $restrictiononfksoc = 1;
98
99
	/**
100
	 * {@inheritdoc}
101
	 */
102
	protected $table_ref_field = 'ref';
103
104
	/**
105
	 * @var int 1 if status is draft
106
	 * @deprecated
107
	 */
108
	public $brouillon;
109
110
	/**
111
	 * @var int thirdparty ID
112
	 */
113
	public $socid;
114
115
	public $author;
116
117
	/**
118
	 * @var int ID
119
	 */
120
	public $fk_user_author;
121
122
	/**
123
	 * @var int ID
124
	 */
125
	public $fk_user_valid;
126
127
	/**
128
	 * @var int ID
129
	 */
130
	public $fk_user_modif;
131
132
133
	public $date; // Date invoice
134
	public $datem;
135
136
	/**
137
	 * @var int	Date expected for delivery
138
	 * @deprecated
139
	 * @see $delivery_date
140
	 */
141
	public $date_livraison;
142
143
	/**
144
	 * @var int	Date expected for delivery
145
	 */
146
	public $delivery_date; // Date expected of shipment (date starting shipment, not the reception that occurs some days after)
147
148
	/**
149
	 * @var string customer ref
150
	 * @deprecated
151
	 * @see $ref_customer
152
	 */
153
	public $ref_client;
154
155
	/**
156
	 * @var string customer ref
157
	 */
158
	public $ref_customer;
159
160
	//Check constants for types
161
	public $type = self::TYPE_STANDARD;
162
163
	// Warning: Do not set default value into property defintion. it must stay null.
164
	// For example to avoid to have substition done when object is generic and not yet defined.
165
	public $remise_absolue;
166
	public $remise_percent;
167
	public $total_ht;
168
	public $total_tva;
169
	public $total_localtax1;
170
	public $total_localtax2;
171
	public $total_ttc;
172
	public $revenuestamp;
173
174
	public $resteapayer;
175
176
	/**
177
	 * ! Closing after partial payment: discount_vat, badcustomer or badsupplier, bankcharge, other
178
	 * ! Closing when no payment: replaced, abandoned
179
	 * @var string Close code
180
	 */
181
	public $close_code;
182
183
	/**
184
	 * ! Comment if paid without full payment
185
	 * @var string Close note
186
	 */
187
	public $close_note;
188
189
	/**
190
	 * 1 if invoice paid COMPLETELY, 0 otherwise (do not use it anymore, use statut and close_code)
191
	 */
192
	public $paye;
193
194
	//! key of module source when invoice generated from a dedicated module ('cashdesk', 'takepos', ...)
195
	public $module_source;
196
	//! key of pos source ('0', '1', ...)
197
	public $pos_source;
198
	//! id of template invoice when generated from a template invoice
199
	public $fk_fac_rec_source;
200
	//! id of source invoice if replacement invoice or credit note
201
	public $fk_facture_source;
202
	public $linked_objects = array();
203
204
	public $date_lim_reglement;
205
	public $cond_reglement_code; // Code in llx_c_paiement
206
	public $mode_reglement_code; // Code in llx_c_paiement
207
208
	/**
209
	 * @var int ID Field to store bank id to use when payment mode is withdraw
210
	 */
211
	public $fk_bank;
212
213
	/**
214
	 * @var FactureLigne[]
215
	 */
216
	public $lines = array();
217
218
	public $line;
219
	public $extraparams = array();
220
221
	public $fac_rec;
222
223
	public $date_pointoftax;
224
225
	// Multicurrency
226
	/**
227
	 * @var int ID
228
	 */
229
	public $fk_multicurrency;
230
231
	public $multicurrency_code;
232
	public $multicurrency_tx;
233
	public $multicurrency_total_ht;
234
	public $multicurrency_total_tva;
235
	public $multicurrency_total_ttc;
236
237
	/**
238
	 * @var int Situation cycle reference number
239
	 */
240
	public $situation_cycle_ref;
241
242
	/**
243
	 * @var int Situation counter inside the cycle
244
	 */
245
	public $situation_counter;
246
247
	/**
248
	 * @var int Final situation flag
249
	 */
250
	public $situation_final;
251
252
	/**
253
	 * @var array Table of previous situations
254
	 */
255
	public $tab_previous_situation_invoice = array();
256
257
	/**
258
	 * @var array Table of next situations
259
	 */
260
	public $tab_next_situation_invoice = array();
261
262
	public $oldcopy;
263
264
	/**
265
	 * @var double percentage of retainage
266
	 */
267
	public $retained_warranty;
268
269
	/**
270
	 * @var int timestamp of date limit of retainage
271
	 */
272
	public $retained_warranty_date_limit;
273
274
	/**
275
	 * @var int Code in llx_c_paiement
276
	 */
277
	public $retained_warranty_fk_cond_reglement;
278
279
280
	/**
281
	 *  'type' if the field format ('integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter]]', 'varchar(x)', 'double(24,8)', 'real', 'price', 'text', 'html', 'date', 'datetime', 'timestamp', 'duration', 'mail', 'phone', 'url', 'password')
282
	 *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)"
283
	 *  'label' the translation key.
284
	 *  'enabled' is a condition when the field must be managed.
285
	 *  'position' is the sort order of field.
286
	 *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
287
	 *  'visible' says if field is visible in list (Examples: 0=Not visible, 1=Visible on list and create/update/view forms, 2=Visible on list only, 3=Visible on create/update/view form only (not list), 4=Visible on list and update/view form only (not create). 5=Visible on list and view only (not create/not update). Using a negative value means field is not shown by default on list but can be selected for viewing)
288
	 *  'noteditable' says if field is not editable (1 or 0)
289
	 *  'default' is a default value for creation (can still be overwrote by the Setup of Default Values if field is editable in creation form). Note: If default is set to '(PROV)' and field is 'ref', the default value will be set to '(PROVid)' where id is rowid when a new record is created.
290
	 *  'index' if we want an index in database.
291
	 *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommanded to name the field fk_...).
292
	 *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
293
	 *  'isameasure' must be set to 1 if you want to have a total on list for this field. Field type must be summable like integer or double(24,8).
294
	 *  'css' is the CSS style to use on field. For example: 'maxwidth200'
295
	 *  'help' is a string visible as a tooltip on field
296
	 *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
297
	 *  'disabled' is 1 if we want to have the field locked by a 'disabled' attribute. In most cases, this is never set into the definition of $fields into class, but is set dynamically by some part of code.
298
	 *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
299
	 *  'comment' is not used. You can store here any text of your choice. It is not used by application.
300
	 *
301
	 *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
302
	 */
303
304
	// BEGIN MODULEBUILDER PROPERTIES
305
	/**
306
	 * @var array  Array with all fields and their property. Do not use it as a static var. It may be modified by constructor.
307
	 */
308
	public $fields = array(
309
		'rowid' =>array('type'=>'integer', 'label'=>'TechnicalID', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>1),
310
		'ref' =>array('type'=>'varchar(30)', 'label'=>'Ref', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'showoncombobox'=>1, 'position'=>5),
311
		'entity' =>array('type'=>'integer', 'label'=>'Entity', 'default'=>1, 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>20, 'index'=>1),
312
		'ref_client' =>array('type'=>'varchar(255)', 'label'=>'RefCustomer', 'enabled'=>1, 'visible'=>-1, 'position'=>10),
313
		'ref_ext' =>array('type'=>'varchar(255)', 'label'=>'Ref ext', 'enabled'=>1, 'visible'=>0, 'position'=>12),
314
		'type' =>array('type'=>'smallint(6)', 'label'=>'Type', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>15),
315
		//'increment' =>array('type'=>'varchar(10)', 'label'=>'Increment', 'enabled'=>1, 'visible'=>-1, 'position'=>45),
316
		'fk_soc' =>array('type'=>'integer:Societe:societe/class/societe.class.php', 'label'=>'ThirdParty', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>50),
317
		'datef' =>array('type'=>'date', 'label'=>'DateInvoice', 'enabled'=>1, 'visible'=>1, 'position'=>20),
318
		'date_valid' =>array('type'=>'date', 'label'=>'DateValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>22),
319
		'date_lim_reglement' =>array('type'=>'date', 'label'=>'DateDue', 'enabled'=>1, 'visible'=>1, 'position'=>25),
320
		'date_closing' =>array('type'=>'datetime', 'label'=>'Date closing', 'enabled'=>1, 'visible'=>-1, 'position'=>30),
321
		'paye' =>array('type'=>'smallint(6)', 'label'=>'InvoicePaidCompletely', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>80),
322
		//'amount' =>array('type'=>'double(24,8)', 'label'=>'Amount', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>85),
323
		'remise_percent' =>array('type'=>'double', 'label'=>'RelativeDiscount', 'enabled'=>1, 'visible'=>-1, 'position'=>90),
324
		'remise_absolue' =>array('type'=>'double', 'label'=>'CustomerRelativeDiscount', 'enabled'=>1, 'visible'=>-1, 'position'=>91),
325
		//'remise' =>array('type'=>'double', 'label'=>'Remise', 'enabled'=>1, 'visible'=>-1, 'position'=>100),
326
		'close_code' =>array('type'=>'varchar(16)', 'label'=>'EarlyClosingReason', 'enabled'=>1, 'visible'=>-1, 'position'=>92),
327
		'close_note' =>array('type'=>'varchar(128)', 'label'=>'EarlyClosingComment', 'enabled'=>1, 'visible'=>-1, 'position'=>93),
328
		'total_ht' =>array('type'=>'double(24,8)', 'label'=>'AmountHT', 'enabled'=>1, 'visible'=>1, 'position'=>95, 'isameasure'=>1),
329
		'total_tva' =>array('type'=>'double(24,8)', 'label'=>'AmountVAT', 'enabled'=>1, 'visible'=>-1, 'position'=>100, 'isameasure'=>1),
330
		'localtax1' =>array('type'=>'double(24,8)', 'label'=>'LT1', 'enabled'=>1, 'visible'=>-1, 'position'=>110, 'isameasure'=>1),
331
		'localtax2' =>array('type'=>'double(24,8)', 'label'=>'LT2', 'enabled'=>1, 'visible'=>-1, 'position'=>120, 'isameasure'=>1),
332
		'revenuestamp' =>array('type'=>'double(24,8)', 'label'=>'RevenueStamp', 'enabled'=>1, 'visible'=>-1, 'position'=>115, 'isameasure'=>1),
333
		'total_ttc' =>array('type'=>'double(24,8)', 'label'=>'AmountTTC', 'enabled'=>1, 'visible'=>1, 'position'=>130, 'isameasure'=>1),
334
		'fk_user_author' =>array('type'=>'integer:User:user/class/user.class.php', 'label'=>'UserAuthor', 'enabled'=>1, 'visible'=>-1, 'position'=>165),
335
		'fk_user_modif' =>array('type'=>'integer:User:user/class/user.class.php', 'label'=>'UserModif', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'position'=>166),
336
		'fk_user_valid' =>array('type'=>'integer:User:user/class/user.class.php', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>167),
337
		'fk_user_closing' =>array('type'=>'integer:User:user/class/user.class.php', 'label'=>'UserClosing', 'enabled'=>1, 'visible'=>-1, 'position'=>168),
338
		'fk_facture_source' =>array('type'=>'integer', 'label'=>'SourceInvoice', 'enabled'=>1, 'visible'=>-1, 'position'=>170),
339
		'fk_projet' =>array('type'=>'integer:Project:projet/class/project.class.php:1:fk_statut=1', 'label'=>'Project', 'enabled'=>1, 'visible'=>-1, 'position'=>175),
340
		'fk_account' =>array('type'=>'integer', 'label'=>'Fk account', 'enabled'=>1, 'visible'=>-1, 'position'=>180),
341
		'fk_currency' =>array('type'=>'varchar(3)', 'label'=>'CurrencyCode', 'enabled'=>1, 'visible'=>-1, 'position'=>185),
342
		'fk_cond_reglement' =>array('type'=>'integer', 'label'=>'PaymentTerm', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>190),
343
		'fk_mode_reglement' =>array('type'=>'integer', 'label'=>'PaymentMode', 'enabled'=>1, 'visible'=>-1, 'position'=>195),
344
		'note_private' =>array('type'=>'html', 'label'=>'NotePrivate', 'enabled'=>1, 'visible'=>0, 'position'=>205),
345
		'note_public' =>array('type'=>'html', 'label'=>'NotePublic', 'enabled'=>1, 'visible'=>0, 'position'=>210),
346
		'model_pdf' =>array('type'=>'varchar(255)', 'label'=>'Model pdf', 'enabled'=>1, 'visible'=>0, 'position'=>215),
347
		'extraparams' =>array('type'=>'varchar(255)', 'label'=>'Extraparams', 'enabled'=>1, 'visible'=>-1, 'position'=>225),
348
		'situation_cycle_ref' =>array('type'=>'smallint(6)', 'label'=>'Situation cycle ref', 'enabled'=>'$conf->global->INVOICE_USE_SITUATION', 'visible'=>-1, 'position'=>230),
349
		'situation_counter' =>array('type'=>'smallint(6)', 'label'=>'Situation counter', 'enabled'=>'$conf->global->INVOICE_USE_SITUATION', 'visible'=>-1, 'position'=>235),
350
		'situation_final' =>array('type'=>'smallint(6)', 'label'=>'Situation final', 'enabled'=>'empty($conf->global->INVOICE_USE_SITUATION) ? 0 : 1', 'visible'=>-1, 'position'=>240),
351
		'retained_warranty' =>array('type'=>'double', 'label'=>'Retained warranty', 'enabled'=>'$conf->global->INVOICE_USE_RETAINED_WARRANTY', 'visible'=>-1, 'position'=>245),
352
		'retained_warranty_date_limit' =>array('type'=>'date', 'label'=>'Retained warranty date limit', 'enabled'=>'$conf->global->INVOICE_USE_RETAINED_WARRANTY', 'visible'=>-1, 'position'=>250),
353
		'retained_warranty_fk_cond_reglement' =>array('type'=>'integer', 'label'=>'Retained warranty fk cond reglement', 'enabled'=>'$conf->global->INVOICE_USE_RETAINED_WARRANTY', 'visible'=>-1, 'position'=>255),
354
		'fk_incoterms' =>array('type'=>'integer', 'label'=>'IncotermCode', 'enabled'=>'$conf->incoterm->enabled', 'visible'=>-1, 'position'=>260),
355
		'location_incoterms' =>array('type'=>'varchar(255)', 'label'=>'IncotermLabel', 'enabled'=>'$conf->incoterm->enabled', 'visible'=>-1, 'position'=>265),
356
		'date_pointoftax' =>array('type'=>'date', 'label'=>'DatePointOfTax', 'enabled'=>'$conf->global->INVOICE_POINTOFTAX_DATE', 'visible'=>-1, 'position'=>270),
357
		'fk_multicurrency' =>array('type'=>'integer', 'label'=>'MulticurrencyID', 'enabled'=>'isModEnabled("multicurrency")', 'visible'=>-1, 'position'=>275),
358
		'multicurrency_code' =>array('type'=>'varchar(255)', 'label'=>'Currency', 'enabled'=>'isModEnabled("multicurrency")', 'visible'=>-1, 'position'=>280),
359
		'multicurrency_tx' =>array('type'=>'double(24,8)', 'label'=>'CurrencyRate', 'enabled'=>'isModEnabled("multicurrency")', 'visible'=>-1, 'position'=>285, 'isameasure'=>1),
360
		'multicurrency_total_ht' =>array('type'=>'double(24,8)', 'label'=>'MulticurrencyAmountHT', 'enabled'=>'isModEnabled("multicurrency")', 'visible'=>-1, 'position'=>290, 'isameasure'=>1),
361
		'multicurrency_total_tva' =>array('type'=>'double(24,8)', 'label'=>'MulticurrencyAmountVAT', 'enabled'=>'isModEnabled("multicurrency")', 'visible'=>-1, 'position'=>291, 'isameasure'=>1),
362
		'multicurrency_total_ttc' =>array('type'=>'double(24,8)', 'label'=>'MulticurrencyAmountTTC', 'enabled'=>'isModEnabled("multicurrency")', 'visible'=>-1, 'position'=>292, 'isameasure'=>1),
363
		'fk_fac_rec_source' =>array('type'=>'integer', 'label'=>'RecurringInvoiceSource', 'enabled'=>1, 'visible'=>-1, 'position'=>305),
364
		'last_main_doc' =>array('type'=>'varchar(255)', 'label'=>'LastMainDoc', 'enabled'=>1, 'visible'=>-1, 'position'=>310),
365
		'module_source' =>array('type'=>'varchar(32)', 'label'=>'POSModule', 'enabled'=>1, 'visible'=>-1, 'position'=>315),
366
		'pos_source' =>array('type'=>'varchar(32)', 'label'=>'POSTerminal', 'enabled'=>1, 'visible'=>-1, 'position'=>320),
367
		'datec' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-1, 'position'=>500),
368
		'tms' =>array('type'=>'timestamp', 'label'=>'DateModificationShort', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>500),
369
		'import_key' =>array('type'=>'varchar(14)', 'label'=>'ImportId', 'enabled'=>1, 'visible'=>-2, 'position'=>900),
370
		'fk_statut' =>array('type'=>'smallint(6)', 'label'=>'Status', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'position'=>1000, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Validated', 2=>'Paid', 3=>'Abandonned')),
371
	);
372
	// END MODULEBUILDER PROPERTIES
373
374
	/**
375
	 * Standard invoice
376
	 */
377
	const TYPE_STANDARD = 0;
378
379
	/**
380
	 * Replacement invoice
381
	 */
382
	const TYPE_REPLACEMENT = 1;
383
384
	/**
385
	 * Credit note invoice
386
	 */
387
	const TYPE_CREDIT_NOTE = 2;
388
389
	/**
390
	 * Deposit invoice
391
	 */
392
	const TYPE_DEPOSIT = 3;
393
394
	/**
395
	 * Proforma invoice (should not be used. a proforma is an order)
396
	 */
397
	const TYPE_PROFORMA = 4;
398
399
	/**
400
	 * Situation invoice
401
	 */
402
	const TYPE_SITUATION = 5;
403
404
	/**
405
	 * Draft status
406
	 */
407
	const STATUS_DRAFT = 0;
408
409
	/**
410
	 * Validated (need to be paid)
411
	 */
412
	const STATUS_VALIDATED = 1;
413
414
	/**
415
	 * Classified paid.
416
	 * If paid partially, $this->close_code can be:
417
	 * - CLOSECODE_DISCOUNTVAT
418
	 * - CLOSECODE_BADDEBT
419
	 * If paid completely, this->close_code will be null
420
	 */
421
	const STATUS_CLOSED = 2;
422
423
	/**
424
	 * Classified abandoned and no payment done.
425
	 * $this->close_code can be:
426
	 * - CLOSECODE_BADDEBT
427
	 * - CLOSECODE_ABANDONED
428
	 * - CLOSECODE_REPLACED
429
	 */
430
	const STATUS_ABANDONED = 3;
431
432
	const CLOSECODE_DISCOUNTVAT = 'discount_vat'; // Abandonned remain - escompte
433
	const CLOSECODE_BADDEBT = 'badcustomer'; // Abandonned remain - bad customer
434
	const CLOSECODE_BANKCHARGE = 'bankcharge'; // Abandonned remain - bank charge
435
	const CLOSECODE_OTHER = 'other'; // Abandonned remain - other
436
437
	const CLOSECODE_ABANDONED = 'abandon'; // Abandonned - other
438
	const CLOSECODE_REPLACED = 'replaced'; // Closed after doing a replacement invoice
439
440
441
	/**
442
	 * 	Constructor
443
	 *
444
	 * 	@param	DoliDB		$db			Database handler
445
	 */
446
	public function __construct(DoliDB $db)
447
	{
448
		$this->db = $db;
449
	}
450
451
	/**
452
	 *	Create invoice in database.
453
	 *  Note: this->ref can be set or empty. If empty, we will use "(PROV999)"
454
	 *  Note: this->fac_rec must be set to create invoice from a recurring invoice
455
	 *
456
	 *	@param	User	$user      		Object user that create
457
	 *	@param  int		$notrigger		1=Does not execute triggers, 0 otherwise
458
	 * 	@param	int		$forceduedate	If set, do not recalculate due date from payment condition but force it with value
459
	 *	@return	int						<0 if KO, >0 if OK
460
	 */
461
	public function create(User $user, $notrigger = 0, $forceduedate = 0)
462
	{
463
		global $langs, $conf, $mysoc, $hookmanager;
464
		$error = 0;
465
466
		// Clean parameters
467
		if (empty($this->type)) {
468
			$this->type = self::TYPE_STANDARD;
469
		}
470
		$this->ref_client = trim($this->ref_client);
471
		$this->note = (isset($this->note) ? trim($this->note) : trim($this->note_private)); // deprecated
472
		$this->note_private = (isset($this->note_private) ? trim($this->note_private) : trim($this->note_private));
473
		$this->note_public = trim($this->note_public);
474
		if (!$this->cond_reglement_id) {
475
			$this->cond_reglement_id = 0;
476
		}
477
		if (!$this->mode_reglement_id) {
478
			$this->mode_reglement_id = 0;
479
		}
480
		$this->brouillon = 1;
481
		$this->status = self::STATUS_DRAFT;
482
		$this->statut = self::STATUS_DRAFT;
483
484
		if (!empty($this->multicurrency_code)) {
485
			// Multicurrency (test on $this->multicurrency_tx because we should take the default rate of multicurrency_code only if not using original rate)
486
			if (empty($this->multicurrency_tx)) {
487
				// If original rate is not set, we take a default value from date
488
				list($this->fk_multicurrency, $this->multicurrency_tx) = MultiCurrency::getIdAndTxFromCode($this->db, $this->multicurrency_code, $this->date);
489
			} else {
490
				// original rate multicurrency_tx and multicurrency_code are set, we use them
491
				$this->fk_multicurrency = MultiCurrency::getIdFromCode($this->db, $this->multicurrency_code);
492
			}
493
		} else {
494
			$this->fk_multicurrency = 0;
495
		}
496
		if (empty($this->fk_multicurrency)) {
497
			$this->multicurrency_code = $conf->currency;
498
			$this->fk_multicurrency = 0;
499
			$this->multicurrency_tx = 1;
500
		}
501
502
		dol_syslog(get_class($this)."::create user=".$user->id." date=".$this->date);
503
504
		// Check parameters
505
		if (empty($this->date)) {
506
			$this->error = "Try to create an invoice with an empty parameter (date)";
507
			dol_syslog(get_class($this)."::create ".$this->error, LOG_ERR);
508
			return -3;
509
		}
510
		$soc = new Societe($this->db);
511
		$result = $soc->fetch($this->socid);
512
		if ($result < 0) {
513
			$this->error = "Failed to fetch company: ".$soc->error;
514
			dol_syslog(get_class($this)."::create ".$this->error, LOG_ERR);
515
			return -2;
516
		}
517
518
		$now = dol_now();
519
520
		$this->db->begin();
521
522
		$originaldatewhen = null;
523
		$nextdatewhen = null;
524
		$previousdaynextdatewhen = null;
525
526
		// Create invoice from a template recurring invoice
527
		if ($this->fac_rec > 0) {
528
			$this->fk_fac_rec_source = $this->fac_rec;
529
530
			require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture-rec.class.php';
531
			$_facrec = new FactureRec($this->db);
532
			$result = $_facrec->fetch($this->fac_rec);
533
			$result = $_facrec->fetchObjectLinked(null, '', null, '', 'OR', 1, 'sourcetype', 0); // This load $_facrec->linkedObjectsIds
534
535
			// Define some dates
536
			$originaldatewhen = $_facrec->date_when;
537
			$nextdatewhen = null; $previousdaynextdatewhen = null;
538
			if ($originaldatewhen) {
539
				$nextdatewhen = dol_time_plus_duree($originaldatewhen, $_facrec->frequency, $_facrec->unit_frequency);
540
				$previousdaynextdatewhen = dol_time_plus_duree($nextdatewhen, -1, 'd');
541
			}
542
543
			if (!empty($_facrec->frequency)) {  // Invoice are created on same thirdparty than template when there is a recurrence, but not necessarly when there is no recurrence.
544
				$this->socid = $_facrec->socid;
545
			}
546
			$this->entity            = $_facrec->entity; // Invoice created in same entity than template
547
548
			// Fields coming from GUI (priority on template). TODO Value of template should be used as default value on GUI so we can use here always value from GUI
549
			$this->fk_project        = GETPOST('projectid', 'int') > 0 ? ((int) GETPOST('projectid', 'int')) : $_facrec->fk_project;
550
			$this->note_public       = GETPOSTISSET('note_public') ? GETPOST('note_public', 'restricthtml') : $_facrec->note_public;
551
			$this->note_private      = GETPOSTISSET('note_private') ? GETPOST('note_private', 'restricthtml') : $_facrec->note_private;
552
			$this->model_pdf = GETPOSTISSET('model') ? GETPOST('model', 'alpha') : $_facrec->model_pdf;
553
			$this->cond_reglement_id = GETPOST('cond_reglement_id', 'int') > 0 ? ((int) GETPOST('cond_reglement_id', 'int')) : $_facrec->cond_reglement_id;
554
			$this->mode_reglement_id = GETPOST('mode_reglement_id', 'int') > 0 ? ((int) GETPOST('mode_reglement_id', 'int')) : $_facrec->mode_reglement_id;
555
			$this->fk_account        = GETPOST('fk_account') > 0 ? ((int) GETPOST('fk_account')) : $_facrec->fk_account;
556
557
			// Set here to have this defined for substitution into notes, should be recalculated after adding lines to get same result
558
			$this->total_ht          = $_facrec->total_ht;
559
			$this->total_ttc         = $_facrec->total_ttc;
560
561
			// Fields always coming from template
562
			$this->remise_absolue    = $_facrec->remise_absolue;
563
			$this->remise_percent    = $_facrec->remise_percent;
564
			$this->fk_incoterms = $_facrec->fk_incoterms;
565
			$this->location_incoterms = $_facrec->location_incoterms;
566
567
			// Clean parameters
568
			if (!$this->type) {
569
				$this->type = self::TYPE_STANDARD;
570
			}
571
			$this->ref_client = trim($this->ref_client);
572
			$this->note_public = trim($this->note_public);
573
			$this->note_private = trim($this->note_private);
574
			$this->note_private = dol_concatdesc($this->note_private, $langs->trans("GeneratedFromRecurringInvoice", $_facrec->ref));
575
576
			$this->array_options = $_facrec->array_options;
577
578
			if (!$this->mode_reglement_id) {
579
				$this->mode_reglement_id = 0;
580
			}
581
			$this->brouillon = 1;
582
			$this->status = self::STATUS_DRAFT;
583
			$this->statut = self::STATUS_DRAFT;
584
585
			$this->linked_objects = $_facrec->linkedObjectsIds;
586
			// We do not add link to template invoice or next invoice will be linked to all generated invoices
587
			//$this->linked_objects['facturerec'][0] = $this->fac_rec;
588
589
			$forceduedate = $this->calculate_date_lim_reglement();
590
591
			// For recurring invoices, update date and number of last generation of recurring template invoice, before inserting new invoice
592
			if ($_facrec->frequency > 0) {
593
				dol_syslog("This is a recurring invoice so we set date_last_gen and next date_when");
594
				if (empty($_facrec->date_when)) {
595
					$_facrec->date_when = $now;
596
				}
597
				$next_date = $_facrec->getNextDate(); // Calculate next date
598
				$result = $_facrec->setValueFrom('date_last_gen', $now, '', null, 'date', '', $user, '');
599
				//$_facrec->setValueFrom('nb_gen_done', $_facrec->nb_gen_done + 1);		// Not required, +1 already included into setNextDate when second param is 1.
600
				$result = $_facrec->setNextDate($next_date, 1);
601
			}
602
603
			// Define lang of customer
604
			$outputlangs = $langs;
605
			$newlang = '';
606
607
			if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && isset($this->thirdparty->default_lang)) {
608
				$newlang = $this->thirdparty->default_lang; // for proposal, order, invoice, ...
609
			}
610
			if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && isset($this->default_lang)) {
611
				$newlang = $this->default_lang; // for thirdparty
612
			}
613
			if (!empty($newlang)) {
614
				$outputlangs = new Translate("", $conf);
615
				$outputlangs->setDefaultLang($newlang);
616
			}
617
618
			// Array of possible substitutions (See also file mailing-send.php that should manage same substitutions)
619
			$substitutionarray = getCommonSubstitutionArray($outputlangs, 0, null, $this);
620
			$substitutionarray['__INVOICE_PREVIOUS_MONTH__'] = dol_print_date(dol_time_plus_duree($this->date, -1, 'm'), '%m');
621
			$substitutionarray['__INVOICE_MONTH__'] = dol_print_date($this->date, '%m');
622
			$substitutionarray['__INVOICE_NEXT_MONTH__'] = dol_print_date(dol_time_plus_duree($this->date, 1, 'm'), '%m');
623
			$substitutionarray['__INVOICE_PREVIOUS_MONTH_TEXT__'] = dol_print_date(dol_time_plus_duree($this->date, -1, 'm'), '%B');
624
			$substitutionarray['__INVOICE_MONTH_TEXT__'] = dol_print_date($this->date, '%B');
625
			$substitutionarray['__INVOICE_NEXT_MONTH_TEXT__'] = dol_print_date(dol_time_plus_duree($this->date, 1, 'm'), '%B');
626
			$substitutionarray['__INVOICE_PREVIOUS_YEAR__'] = dol_print_date(dol_time_plus_duree($this->date, -1, 'y'), '%Y');
627
			$substitutionarray['__INVOICE_YEAR__'] = dol_print_date($this->date, '%Y');
628
			$substitutionarray['__INVOICE_NEXT_YEAR__'] = dol_print_date(dol_time_plus_duree($this->date, 1, 'y'), '%Y');
629
			// Only for template invoice
630
			$substitutionarray['__INVOICE_DATE_NEXT_INVOICE_BEFORE_GEN__'] = (isset($originaldatewhen) ? dol_print_date($originaldatewhen, 'dayhour') : '');
631
			$substitutionarray['__INVOICE_DATE_NEXT_INVOICE_AFTER_GEN__'] = (isset($nextdatewhen) ? dol_print_date($nextdatewhen, 'dayhour') : '');
632
			$substitutionarray['__INVOICE_PREVIOUS_DATE_NEXT_INVOICE_AFTER_GEN__'] = (isset($previousdaynextdatewhen) ? dol_print_date($previousdaynextdatewhen, 'dayhour') : '');
633
			$substitutionarray['__INVOICE_COUNTER_CURRENT__'] = $_facrec->nb_gen_done;
634
			$substitutionarray['__INVOICE_COUNTER_MAX__'] = $_facrec->nb_gen_max;
635
636
			//var_dump($substitutionarray);exit;
637
638
			complete_substitutions_array($substitutionarray, $outputlangs);
639
640
			$this->note_public = make_substitutions($this->note_public, $substitutionarray);
641
			$this->note_private = make_substitutions($this->note_private, $substitutionarray);
642
		}
643
644
		// Define due date if not already defined
645
		if (empty($forceduedate)) {
646
			$duedate = $this->calculate_date_lim_reglement();
647
			/*if ($duedate < 0) {	Regression, a date can be negative if before 1970.
648
				dol_syslog(__METHOD__ . ' Error in calculate_date_lim_reglement. We got ' . $duedate, LOG_ERR);
649
				return -1;
650
			}*/
651
			$this->date_lim_reglement = $duedate;
652
		} else {
653
			$this->date_lim_reglement = $forceduedate;
654
		}
655
656
		// Insert into database
657
		$socid = $this->socid;
658
659
		$sql = "INSERT INTO ".MAIN_DB_PREFIX."facture (";
660
		$sql .= " ref";
661
		$sql .= ", entity";
662
		$sql .= ", ref_ext";
663
		$sql .= ", type";
664
		$sql .= ", fk_soc";
665
		$sql .= ", datec";
666
		$sql .= ", remise_absolue";
667
		$sql .= ", remise_percent";
668
		$sql .= ", datef";
669
		$sql .= ", date_pointoftax";
670
		$sql .= ", note_private";
671
		$sql .= ", note_public";
672
		$sql .= ", ref_client";
673
		$sql .= ", fk_account";
674
		$sql .= ", module_source, pos_source, fk_fac_rec_source, fk_facture_source, fk_user_author, fk_projet";
675
		$sql .= ", fk_cond_reglement, fk_mode_reglement, date_lim_reglement, model_pdf";
676
		$sql .= ", situation_cycle_ref, situation_counter, situation_final";
677
		$sql .= ", fk_incoterms, location_incoterms";
678
		$sql .= ", fk_multicurrency";
679
		$sql .= ", multicurrency_code";
680
		$sql .= ", multicurrency_tx";
681
		$sql .= ", retained_warranty";
682
		$sql .= ", retained_warranty_date_limit";
683
		$sql .= ", retained_warranty_fk_cond_reglement";
684
		$sql .= ")";
685
		$sql .= " VALUES (";
686
		$sql .= "'(PROV)'";
687
		$sql .= ", ".setEntity($this);
688
		$sql .= ", ".($this->ref_ext ? "'".$this->db->escape($this->ref_ext)."'" : "null");
689
		$sql .= ", '".$this->db->escape($this->type)."'";
690
		$sql .= ", ".((int) $socid);
691
		$sql .= ", '".$this->db->idate($now)."'";
692
		$sql .= ", ".($this->remise_absolue > 0 ? $this->remise_absolue : 'NULL');
693
		$sql .= ", ".($this->remise_percent > 0 ? $this->remise_percent : 'NULL');
694
		$sql .= ", '".$this->db->idate($this->date)."'";
695
		$sql .= ", ".(empty($this->date_pointoftax) ? "null" : "'".$this->db->idate($this->date_pointoftax)."'");
696
		$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "null");
697
		$sql .= ", ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "null");
698
		$sql .= ", ".($this->ref_client ? "'".$this->db->escape($this->ref_client)."'" : "null");
699
		$sql .= ", ".($this->fk_account > 0 ? $this->fk_account : 'NULL');
700
		$sql .= ", ".($this->module_source ? "'".$this->db->escape($this->module_source)."'" : "null");
701
		$sql .= ", ".($this->pos_source != '' ? "'".$this->db->escape($this->pos_source)."'" : "null");
702
		$sql .= ", ".($this->fk_fac_rec_source ? "'".$this->db->escape($this->fk_fac_rec_source)."'" : "null");
703
		$sql .= ", ".($this->fk_facture_source ? "'".$this->db->escape($this->fk_facture_source)."'" : "null");
704
		$sql .= ", ".($user->id > 0 ? (int) $user->id : "null");
705
		$sql .= ", ".($this->fk_project ? $this->fk_project : "null");
706
		$sql .= ", ".((int) $this->cond_reglement_id);
707
		$sql .= ", ".((int) $this->mode_reglement_id);
708
		$sql .= ", '".$this->db->idate($this->date_lim_reglement)."'";
709
		$sql .= ", ".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null");
710
		$sql .= ", ".($this->situation_cycle_ref ? "'".$this->db->escape($this->situation_cycle_ref)."'" : "null");
711
		$sql .= ", ".($this->situation_counter ? "'".$this->db->escape($this->situation_counter)."'" : "null");
712
		$sql .= ", ".($this->situation_final ? $this->situation_final : 0);
713
		$sql .= ", ".(int) $this->fk_incoterms;
714
		$sql .= ", '".$this->db->escape($this->location_incoterms)."'";
715
		$sql .= ", ".(int) $this->fk_multicurrency;
716
		$sql .= ", '".$this->db->escape($this->multicurrency_code)."'";
717
		$sql .= ", ".(double) $this->multicurrency_tx;
718
		$sql .= ", ".(empty($this->retained_warranty) ? "0" : $this->db->escape($this->retained_warranty));
719
		$sql .= ", ".(!empty($this->retained_warranty_date_limit) ? "'".$this->db->idate($this->retained_warranty_date_limit)."'" : 'NULL');
720
		$sql .= ", ".(int) $this->retained_warranty_fk_cond_reglement;
721
		$sql .= ")";
722
723
		$resql = $this->db->query($sql);
724
		if ($resql) {
725
			$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'facture');
726
727
			// Update ref with new one
728
			$this->ref = '(PROV'.$this->id.')';
729
			$sql = 'UPDATE '.MAIN_DB_PREFIX."facture SET ref='".$this->db->escape($this->ref)."' WHERE rowid=".((int) $this->id);
730
731
			$resql = $this->db->query($sql);
732
			if (!$resql) {
733
				$error++;
734
			}
735
736
			if (!empty($this->linkedObjectsIds) && empty($this->linked_objects)) {	// To use new linkedObjectsIds instead of old linked_objects
737
				$this->linked_objects = $this->linkedObjectsIds; // TODO Replace linked_objects with linkedObjectsIds
738
			}
739
740
			// Add object linked
741
			if (!$error && $this->id && !empty($this->linked_objects) && is_array($this->linked_objects)) {
742
				foreach ($this->linked_objects as $origin => $tmp_origin_id) {
743
					if (is_array($tmp_origin_id)) {       // New behaviour, if linked_object can have several links per type, so is something like array('contract'=>array(id1, id2, ...))
744
						foreach ($tmp_origin_id as $origin_id) {
745
							$ret = $this->add_object_linked($origin, $origin_id);
746
							if (!$ret) {
747
								$this->error = $this->db->lasterror();
748
								$error++;
749
							}
750
						}
751
					} else // Old behaviour, if linked_object has only one link per type, so is something like array('contract'=>id1))
752
					{
753
						$origin_id = $tmp_origin_id;
754
						$ret = $this->add_object_linked($origin, $origin_id);
755
						if (!$ret) {
756
							$this->error = $this->db->lasterror();
757
							$error++;
758
						}
759
					}
760
				}
761
			}
762
763
			// Propagate contacts
764
			if (!$error && $this->id && !empty($conf->global->MAIN_PROPAGATE_CONTACTS_FROM_ORIGIN) && !empty($this->origin) && !empty($this->origin_id)) {   // Get contact from origin object
765
				$originforcontact = $this->origin;
766
				$originidforcontact = $this->origin_id;
767
				if ($originforcontact == 'shipping') {     // shipment and order share the same contacts. If creating from shipment we take data of order
768
					require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
769
					$exp = new Expedition($this->db);
770
					$exp->fetch($this->origin_id);
771
					$exp->fetchObjectLinked(null, '', null, '', 'OR', 1, 'sourcetype', 0);
772
					if (count($exp->linkedObjectsIds['commande']) > 0) {
773
						foreach ($exp->linkedObjectsIds['commande'] as $key => $value) {
774
							$originforcontact = 'commande';
775
							if (is_object($value)) {
776
								$originidforcontact = $value->id;
777
							} else {
778
								$originidforcontact = $value;
779
							}
780
							break; // We take first one
781
						}
782
					}
783
				}
784
785
				$sqlcontact = "SELECT ctc.code, ctc.source, ec.fk_socpeople FROM ".MAIN_DB_PREFIX."element_contact as ec, ".MAIN_DB_PREFIX."c_type_contact as ctc";
786
				$sqlcontact .= " WHERE element_id = ".((int) $originidforcontact)." AND ec.fk_c_type_contact = ctc.rowid AND ctc.element = '".$this->db->escape($originforcontact)."'";
787
788
				$resqlcontact = $this->db->query($sqlcontact);
789
				if ($resqlcontact) {
790
					while ($objcontact = $this->db->fetch_object($resqlcontact)) {
791
						//print $objcontact->code.'-'.$objcontact->source.'-'.$objcontact->fk_socpeople."\n";
792
						$this->add_contact($objcontact->fk_socpeople, $objcontact->code, $objcontact->source); // May failed because of duplicate key or because code of contact type does not exists for new object
793
					}
794
				} else {
795
					dol_print_error($resqlcontact);
796
				}
797
			}
798
799
			/*
800
			 *  Insert lines of invoices, if not from template invoice, into database
801
			 */
802
			if (!$error && empty($this->fac_rec) && count($this->lines) && is_object($this->lines[0])) {	// If this->lines is array of InvoiceLines (preferred mode)
803
				$fk_parent_line = 0;
804
805
				dol_syslog("There is ".count($this->lines)." lines that are invoice lines objects");
806
				foreach ($this->lines as $i => $val) {
807
					$newinvoiceline = $this->lines[$i];
808
809
					$newinvoiceline->context = $this->context;
810
811
					$newinvoiceline->fk_facture = $this->id;
812
813
					$newinvoiceline->origin = $this->lines[$i]->element;
814
					$newinvoiceline->origin_id = $this->lines[$i]->id;
815
816
					// Auto set date of service ?
817
					if ($this->lines[$i]->date_start_fill == 1 && $originaldatewhen) {		// $originaldatewhen is defined when generating from recurring invoice only
818
						$newinvoiceline->date_start = $originaldatewhen;
819
					}
820
					if ($this->lines[$i]->date_end_fill == 1 && $previousdaynextdatewhen) {	// $previousdaynextdatewhen is defined when generating from recurring invoice only
821
						$newinvoiceline->date_end = $previousdaynextdatewhen;
822
					}
823
824
					if ($result >= 0) {
825
						// Reset fk_parent_line for no child products and special product
826
						if (($newinvoiceline->product_type != 9 && empty($newinvoiceline->fk_parent_line)) || $newinvoiceline->product_type == 9) {
827
							$fk_parent_line = 0;
828
						}
829
830
						// Complete vat rate with code
831
						$vatrate = $newinvoiceline->tva_tx;
832
						if ($newinvoiceline->vat_src_code && ! preg_match('/\(.*\)/', $vatrate)) $vatrate.=' ('.$newinvoiceline->vat_src_code.')';
833
834
						$newinvoiceline->fk_parent_line = $fk_parent_line;
835
836
						if ($this->type === Facture::TYPE_REPLACEMENT && $newinvoiceline->fk_remise_except) {
837
							$discount = new DiscountAbsolute($this->db);
838
							$discount->fetch($newinvoiceline->fk_remise_except);
839
840
							$discountId = $soc->set_remise_except($discount->amount_ht, $user, $discount->description, $discount->tva_tx);
841
							$newinvoiceline->fk_remise_except = $discountId;
842
						}
843
844
						$result = $this->addline(
845
							$newinvoiceline->desc,
846
							$newinvoiceline->subprice,
847
							$newinvoiceline->qty,
848
							$vatrate,
849
							$newinvoiceline->localtax1_tx,
850
							$newinvoiceline->localtax2_tx,
851
							$newinvoiceline->fk_product,
852
							$newinvoiceline->remise_percent,
853
							$newinvoiceline->date_start,
854
							$newinvoiceline->date_end,
855
							$newinvoiceline->fk_code_ventilation,
856
							$newinvoiceline->info_bits,
857
							$newinvoiceline->fk_remise_except,
858
							'HT',
859
							0,
860
							$newinvoiceline->product_type,
861
							$newinvoiceline->rang,
862
							$newinvoiceline->special_code,
863
							$newinvoiceline->element,
864
							$newinvoiceline->id,
865
							$fk_parent_line,
866
							$newinvoiceline->fk_fournprice,
867
							$newinvoiceline->pa_ht,
868
							$newinvoiceline->label,
869
							$newinvoiceline->array_options,
870
							$newinvoiceline->situation_percent,
871
							$newinvoiceline->fk_prev_id,
872
							$newinvoiceline->fk_unit,
873
							$newinvoiceline->multicurrency_subprice,
874
							$newinvoiceline->ref_ext,
875
							1
876
						);
877
878
						// Defined the new fk_parent_line
879
						if ($result > 0 && $newinvoiceline->product_type == 9) {
880
							$fk_parent_line = $result;
881
						}
882
					}
883
					if ($result < 0) {
884
						$this->error = $newinvoiceline->error;
885
						$this->errors = $newinvoiceline->errors;
886
						$error++;
887
						break;
888
					}
889
				}
890
			} elseif (!$error && empty($this->fac_rec)) { 		// If this->lines is an array of invoice line arrays
891
				$fk_parent_line = 0;
892
893
				dol_syslog("There is ".count($this->lines)." lines that are array lines");
894
895
				foreach ($this->lines as $i => $val) {
896
					$line = $this->lines[$i];
897
898
					// Test and convert into object this->lines[$i]. When coming from REST API, we may still have an array
899
					//if (! is_object($line)) $line=json_decode(json_encode($line), false);  // convert recursively array into object.
900
					if (!is_object($line)) {
901
						$line = (object) $line;
902
					}
903
904
					if ($result >= 0) {
905
						// Reset fk_parent_line for no child products and special product
906
						if (($line->product_type != 9 && empty($line->fk_parent_line)) || $line->product_type == 9) {
907
							$fk_parent_line = 0;
908
						}
909
910
						// Complete vat rate with code
911
						$vatrate = $line->tva_tx;
912
						if ($line->vat_src_code && !preg_match('/\(.*\)/', $vatrate)) {
913
							$vatrate .= ' ('.$line->vat_src_code.')';
914
						}
915
916
						if (!empty($conf->global->MAIN_CREATEFROM_KEEP_LINE_ORIGIN_INFORMATION)) {
917
							$originid = $line->origin_id;
918
							$origintype = $line->origin;
919
						} else {
920
							$originid = $line->id;
921
							$origintype = $this->element;
922
						}
923
924
						// init ref_ext
925
						if (empty($line->ref_ext)) {
926
							$line->ref_ext = '';
927
						}
928
929
						$result = $this->addline(
930
							$line->desc,
931
							$line->subprice,
932
							$line->qty,
933
							$vatrate,
934
							$line->localtax1_tx,
935
							$line->localtax2_tx,
936
							$line->fk_product,
937
							$line->remise_percent,
938
							$line->date_start,
939
							$line->date_end,
940
							$line->fk_code_ventilation,
941
							$line->info_bits,
942
							$line->fk_remise_except,
943
							'HT',
944
							0,
945
							$line->product_type,
946
							$line->rang,
947
							$line->special_code,
948
							$origintype,
949
							$originid,
950
							$fk_parent_line,
951
							$line->fk_fournprice,
952
							$line->pa_ht,
953
							$line->label,
954
							$line->array_options,
955
							$line->situation_percent,
956
							$line->fk_prev_id,
957
							$line->fk_unit,
958
							$line->multicurrency_subprice,
959
							$line->ref_ext,
960
							1
961
						);
962
						if ($result < 0) {
963
							$this->error = $this->db->lasterror();
964
							dol_print_error($this->db);
965
							$this->db->rollback();
966
							return -1;
967
						}
968
969
						// Defined the new fk_parent_line
970
						if ($result > 0 && $line->product_type == 9) {
971
							$fk_parent_line = $result;
972
						}
973
					}
974
				}
975
			}
976
977
			/*
978
			 * Insert lines of template invoices
979
			 */
980
			if (!$error && $this->fac_rec > 0) {
981
				foreach ($_facrec->lines as $i => $val) {
982
					if ($_facrec->lines[$i]->fk_product) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $_facrec does not seem to be defined for all execution paths leading up to this point.
Loading history...
983
						$prod = new Product($this->db);
984
						$res = $prod->fetch($_facrec->lines[$i]->fk_product);
985
					}
986
987
					// For line from template invoice, we use data from template invoice
988
					/*
989
					$tva_tx = get_default_tva($mysoc,$soc,$prod->id);
990
					$tva_npr = get_default_npr($mysoc,$soc,$prod->id);
991
					if (empty($tva_tx)) $tva_npr=0;
992
					$localtax1_tx=get_localtax($tva_tx,1,$soc,$mysoc,$tva_npr);
993
					$localtax2_tx=get_localtax($tva_tx,2,$soc,$mysoc,$tva_npr);
994
					*/
995
					$tva_tx = $_facrec->lines[$i]->tva_tx.($_facrec->lines[$i]->vat_src_code ? '('.$_facrec->lines[$i]->vat_src_code.')' : '');
996
					$tva_npr = $_facrec->lines[$i]->info_bits;
997
					if (empty($tva_tx)) {
998
						$tva_npr = 0;
999
					}
1000
					$localtax1_tx = $_facrec->lines[$i]->localtax1_tx;
1001
					$localtax2_tx = $_facrec->lines[$i]->localtax2_tx;
1002
1003
					$fk_product_fournisseur_price = empty($_facrec->lines[$i]->fk_product_fournisseur_price) ? null : $_facrec->lines[$i]->fk_product_fournisseur_price;
1004
					$buyprice = empty($_facrec->lines[$i]->buyprice) ? 0 : $_facrec->lines[$i]->buyprice;
1005
1006
					// If buyprice not defined from template invoice, we try to guess the best value
1007
					if (!$buyprice && $_facrec->lines[$i]->fk_product > 0) {
1008
						require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
1009
						$producttmp = new ProductFournisseur($this->db);
1010
						$producttmp->fetch($_facrec->lines[$i]->fk_product);
1011
1012
						// If margin module defined on costprice, we try the costprice
1013
						// If not defined or if module margin defined and pmp and stock module enabled, we try pmp price
1014
						// else we get the best supplier price
1015
						if ($conf->global->MARGIN_TYPE == 'costprice' && !empty($producttmp->cost_price)) {
1016
							$buyprice = $producttmp->cost_price;
1017
						} elseif (isModEnabled('stock') && ($conf->global->MARGIN_TYPE == 'costprice' || $conf->global->MARGIN_TYPE == 'pmp') && !empty($producttmp->pmp)) {
1018
							$buyprice = $producttmp->pmp;
1019
						} else {
1020
							if ($producttmp->find_min_price_product_fournisseur($_facrec->lines[$i]->fk_product) > 0) {
1021
								if ($producttmp->product_fourn_price_id > 0) {
1022
									$buyprice = price2num($producttmp->fourn_unitprice * (1 - $producttmp->fourn_remise_percent / 100) + $producttmp->fourn_remise, 'MU');
1023
								}
1024
							}
1025
						}
1026
					}
1027
1028
					$result_insert = $this->addline(
1029
						$_facrec->lines[$i]->desc,
1030
						$_facrec->lines[$i]->subprice,
1031
						$_facrec->lines[$i]->qty,
1032
						$tva_tx,
1033
						$localtax1_tx,
1034
						$localtax2_tx,
1035
						$_facrec->lines[$i]->fk_product,
1036
						$_facrec->lines[$i]->remise_percent,
1037
						($_facrec->lines[$i]->date_start_fill == 1 && $originaldatewhen) ? $originaldatewhen : '',
1038
						($_facrec->lines[$i]->date_end_fill == 1 && $previousdaynextdatewhen) ? $previousdaynextdatewhen : '',
1039
						0,
1040
						$tva_npr,
1041
						'',
1042
						'HT',
1043
						0,
1044
						$_facrec->lines[$i]->product_type,
1045
						$_facrec->lines[$i]->rang,
1046
						$_facrec->lines[$i]->special_code,
1047
						'',
1048
						0,
1049
						0,
1050
						$fk_product_fournisseur_price,
1051
						$buyprice,
1052
						$_facrec->lines[$i]->label,
1053
						empty($_facrec->lines[$i]->array_options) ?null:$_facrec->lines[$i]->array_options,
1054
						$_facrec->lines[$i]->situation_percent,
1055
						'',
1056
						$_facrec->lines[$i]->fk_unit,
1057
						$_facrec->lines[$i]->multicurrency_subprice,
1058
						$_facrec->lines[$i]->ref_ext,
1059
						1
1060
					);
1061
1062
					if ($result_insert < 0) {
1063
						$error++;
1064
						$this->error = $this->db->error();
1065
						break;
1066
					}
1067
				}
1068
			}
1069
1070
			if (!$error) {
1071
				$result = $this->update_price(1, 'auto', 0, $mysoc);
1072
				if ($result > 0) {
1073
					$action = 'create';
1074
1075
					// Actions on extra fields
1076
					if (!$error) {
1077
						$result = $this->insertExtraFields();
1078
						if ($result < 0) {
1079
							$error++;
1080
						}
1081
					}
1082
1083
					if (!$error && !$notrigger) {
1084
						// Call trigger
1085
						$result = $this->call_trigger('BILL_CREATE', $user);
1086
						if ($result < 0) {
1087
							$error++;
1088
						}
1089
						// End call triggers
1090
					}
1091
1092
					if (!$error) {
1093
						$this->db->commit();
1094
						return $this->id;
1095
					} else {
1096
						$this->db->rollback();
1097
						return -4;
1098
					}
1099
				} else {
1100
					$this->error = $langs->trans('FailedToUpdatePrice');
1101
					$this->db->rollback();
1102
					return -3;
1103
				}
1104
			} else {
1105
				dol_syslog(get_class($this)."::create error ".$this->error, LOG_ERR);
1106
				$this->db->rollback();
1107
				return -2;
1108
			}
1109
		} else {
1110
			$this->error = $this->db->error();
1111
			$this->db->rollback();
1112
			return -1;
1113
		}
1114
	}
1115
1116
1117
	/**
1118
	 *	Create a new invoice in database from current invoice
1119
	 *
1120
	 *	@param      User	$user    		Object user that ask creation
1121
	 *	@param		int		$invertdetail	Reverse sign of amounts for lines
1122
	 *	@return		int						<0 if KO, >0 if OK
1123
	 */
1124
	public function createFromCurrent(User $user, $invertdetail = 0)
1125
	{
1126
		global $conf;
1127
1128
		// Source invoice load
1129
		$facture = new Facture($this->db);
1130
1131
		// Retrieve all extrafield
1132
		// fetch optionals attributes and labels
1133
		$this->fetch_optionals();
1134
1135
		if (!empty($this->array_options)) {
1136
					$facture->array_options = $this->array_options;
1137
		}
1138
1139
		foreach ($this->lines as &$line) {
1140
					$line->fetch_optionals(); //fetch extrafields
1141
		}
1142
1143
		$facture->fk_facture_source = $this->fk_facture_source;
1144
		$facture->type 			    = $this->type;
1145
		$facture->socid 		    = $this->socid;
1146
		$facture->date              = $this->date;
1147
		$facture->date_pointoftax   = $this->date_pointoftax;
1148
		$facture->note_public       = $this->note_public;
1149
		$facture->note_private      = $this->note_private;
1150
		$facture->ref_client        = $this->ref_client;
1151
		$facture->modelpdf          = $this->model_pdf; // deprecated
1152
		$facture->model_pdf         = $this->model_pdf;
1153
		$facture->fk_project        = $this->fk_project;
1154
		$facture->cond_reglement_id = $this->cond_reglement_id;
1155
		$facture->mode_reglement_id = $this->mode_reglement_id;
1156
		$facture->remise_absolue    = $this->remise_absolue;
1157
		$facture->remise_percent    = $this->remise_percent;
1158
1159
		$facture->origin            = $this->origin;
1160
		$facture->origin_id         = $this->origin_id;
1161
1162
		$facture->lines = $this->lines; // Array of lines of invoice
1163
		$facture->situation_counter = $this->situation_counter;
1164
		$facture->situation_cycle_ref = $this->situation_cycle_ref;
1165
		$facture->situation_final = $this->situation_final;
1166
1167
		$facture->retained_warranty = $this->retained_warranty;
1168
		$facture->retained_warranty_fk_cond_reglement = $this->retained_warranty_fk_cond_reglement;
1169
		$facture->retained_warranty_date_limit = $this->retained_warranty_date_limit;
1170
1171
		$facture->fk_user_author = $user->id;
1172
1173
1174
		// Loop on each line of new invoice
1175
		foreach ($facture->lines as $i => $tmpline) {
1176
			$facture->lines[$i]->fk_prev_id = $this->lines[$i]->rowid;
1177
			if ($invertdetail) {
1178
				$facture->lines[$i]->subprice  = -$facture->lines[$i]->subprice;
1179
				$facture->lines[$i]->total_ht  = -$facture->lines[$i]->total_ht;
1180
				$facture->lines[$i]->total_tva = -$facture->lines[$i]->total_tva;
1181
				$facture->lines[$i]->total_localtax1 = -$facture->lines[$i]->total_localtax1;
1182
				$facture->lines[$i]->total_localtax2 = -$facture->lines[$i]->total_localtax2;
1183
				$facture->lines[$i]->total_ttc = -$facture->lines[$i]->total_ttc;
1184
				$facture->lines[$i]->ref_ext = '';
1185
			}
1186
		}
1187
1188
		dol_syslog(get_class($this)."::createFromCurrent invertdetail=".$invertdetail." socid=".$this->socid." nboflines=".count($facture->lines));
1189
1190
		$facid = $facture->create($user);
1191
		if ($facid <= 0) {
1192
			$this->error = $facture->error;
1193
			$this->errors = $facture->errors;
1194
		} elseif ($this->type == self::TYPE_SITUATION && !empty($conf->global->INVOICE_USE_SITUATION)) {
1195
			$this->fetchObjectLinked('', '', $this->id, 'facture');
1196
1197
			foreach ($this->linkedObjectsIds as $typeObject => $Tfk_object) {
1198
				foreach ($Tfk_object as $fk_object) {
1199
					$facture->add_object_linked($typeObject, $fk_object);
1200
				}
1201
			}
1202
1203
			$facture->add_object_linked('facture', $this->fk_facture_source);
1204
		}
1205
1206
		return $facid;
1207
	}
1208
1209
1210
	/**
1211
	 *	Load an object from its id and create a new one in database
1212
	 *
1213
	 *	@param      User	$user        	User that clone
1214
	 *  @param  	int 	$fromid         Id of object to clone
1215
	 * 	@return		int					    New id of clone
1216
	 */
1217
	public function createFromClone(User $user, $fromid = 0)
1218
	{
1219
		global $conf, $hookmanager;
1220
1221
		$error = 0;
1222
1223
		$object = new Facture($this->db);
1224
1225
		$this->db->begin();
1226
1227
		$object->fetch($fromid);
1228
1229
		// Load source object
1230
		$objFrom = clone $object;
1231
1232
		// Change socid if needed
1233
		if (!empty($this->socid) && $this->socid != $object->socid) {
1234
			$objsoc = new Societe($this->db);
1235
1236
			if ($objsoc->fetch($this->socid) > 0) {
1237
				$object->socid = $objsoc->id;
1238
				$object->cond_reglement_id	= (!empty($objsoc->cond_reglement_id) ? $objsoc->cond_reglement_id : 0);
1239
				$object->mode_reglement_id	= (!empty($objsoc->mode_reglement_id) ? $objsoc->mode_reglement_id : 0);
1240
				$object->fk_project = '';
1241
				$object->fk_delivery_address = '';
1242
			}
1243
1244
			// TODO Change product price if multi-prices
1245
		}
1246
1247
		$object->id = 0;
1248
		$object->statut = self::STATUS_DRAFT;
1249
		$object->status = self::STATUS_DRAFT;
1250
1251
		// Clear fields
1252
		$object->date               = (empty($this->date) ? dol_now() : $this->date);
1253
		$object->user_author        = $user->id; // deprecated
1254
		$object->user_valid         = null; // deprecated
1255
		$object->fk_user_author     = $user->id;
1256
		$object->fk_user_valid      = null;
1257
		$object->fk_facture_source  = 0;
1258
		$object->date_creation      = '';
1259
		$object->date_modification = '';
1260
		$object->date_validation    = '';
1261
		$object->ref_client         = '';
1262
		$object->close_code         = '';
1263
		$object->close_note         = '';
1264
		if ($conf->global->MAIN_DONT_KEEP_NOTE_ON_CLONING == 1) {
1265
			$object->note_private = '';
1266
			$object->note_public = '';
1267
		}
1268
1269
		// Loop on each line of new invoice
1270
		foreach ($object->lines as $i => $line) {
1271
			if (($object->lines[$i]->info_bits & 0x02) == 0x02) {	// We do not clone line of discounts
1272
				unset($object->lines[$i]);
1273
				continue;
1274
			}
1275
1276
			// Bloc to update dates of service (month by month only if previously filled and similare to start and end of month)
1277
			// If it's a service with start and end dates
1278
			if (!empty($conf->global->INVOICE_AUTO_NEXT_MONTH_ON_LINES) && !empty($line->date_start) && !empty($line->date_end)) {
1279
				// Get the dates
1280
				$start = dol_getdate($line->date_start);
1281
				$end = dol_getdate($line->date_end);
1282
1283
				// Get the first and last day of the month
1284
				$first = dol_get_first_day($start['year'], $start['mon']);
1285
				$last = dol_get_last_day($end['year'], $end['mon']);
1286
1287
				//print dol_print_date(dol_mktime(0, 0, 0, $start['mon'], $start['mday'], $start['year'], 'gmt'), 'dayhour').' '.dol_print_date($first, 'dayhour').'<br>';
1288
				//print dol_mktime(23, 59, 59, $end['mon'], $end['mday'], $end['year'], 'gmt').' '.$last.'<br>';exit;
1289
				// If start date is first date of month and end date is last date of month
1290
				if (dol_mktime(0, 0, 0, $start['mon'], $start['mday'], $start['year'], 'gmt') == $first
1291
					&& dol_mktime(23, 59, 59, $end['mon'], $end['mday'], $end['year'], 'gmt') == $last) {
1292
					$nextMonth = dol_get_next_month($end['mon'], $end['year']);
1293
					$newFirst = dol_get_first_day($nextMonth['year'], $nextMonth['month']);
1294
					$newLast = dol_get_last_day($nextMonth['year'], $nextMonth['month']);
1295
					$object->lines[$i]->date_start = $newFirst;
1296
					$object->lines[$i]->date_end = $newLast;
1297
				}
1298
			}
1299
1300
			$object->lines[$i]->ref_ext = ''; // Do not clone ref_ext
1301
		}
1302
1303
		// Create clone
1304
		$object->context['createfromclone'] = 'createfromclone';
1305
		$result = $object->create($user);
1306
		if ($result < 0) {
1307
			$error++;
1308
			$this->error = $object->error;
1309
			$this->errors = $object->errors;
1310
		} else {
1311
			// copy internal contacts
1312
			if ($object->copy_linked_contact($objFrom, 'internal') < 0) {
1313
				$error++;
1314
				$this->error = $object->error;
1315
				$this->errors = $object->errors;
1316
			} elseif ($object->socid == $objFrom->socid) {
1317
				// copy external contacts if same company
1318
				if ($object->copy_linked_contact($objFrom, 'external') < 0) {
1319
					$error++;
1320
					$this->error = $object->error;
1321
					$this->errors = $object->errors;
1322
				}
1323
			}
1324
		}
1325
1326
		if (!$error) {
1327
			// Hook of thirdparty module
1328
			if (is_object($hookmanager)) {
1329
				$parameters = array('objFrom'=>$objFrom);
1330
				$action = '';
1331
				$reshook = $hookmanager->executeHooks('createFrom', $parameters, $object, $action); // Note that $action and $object may have been modified by some hooks
1332
				if ($reshook < 0) {
1333
					$this->errors += $hookmanager->errors;
1334
					$this->error = $hookmanager->error;
1335
					$error++;
1336
				}
1337
			}
1338
		}
1339
1340
		unset($object->context['createfromclone']);
1341
1342
		// End
1343
		if (!$error) {
1344
			$this->db->commit();
1345
			return $object->id;
1346
		} else {
1347
			$this->db->rollback();
1348
			return -1;
1349
		}
1350
	}
1351
1352
	/**
1353
	 *  Load an object from an order and create a new invoice into database
1354
	 *
1355
	 *  @param      Object			$object         	Object source
1356
	 *  @param		User			$user				Object user
1357
	 *  @return     int             					<0 if KO, 0 if nothing done, 1 if OK
1358
	 */
1359
	public function createFromOrder($object, User $user)
1360
	{
1361
		global $conf, $hookmanager;
1362
1363
		$error = 0;
1364
1365
		// Closed order
1366
		$this->date = dol_now();
1367
		$this->source = 0;
1368
1369
		$num = count($object->lines);
1370
		for ($i = 0; $i < $num; $i++) {
1371
			$line = new FactureLigne($this->db);
1372
1373
			$line->libelle = $object->lines[$i]->libelle; // deprecated
1374
			$line->label			= $object->lines[$i]->label;
1375
			$line->desc				= $object->lines[$i]->desc;
1376
			$line->subprice			= $object->lines[$i]->subprice;
1377
			$line->total_ht			= $object->lines[$i]->total_ht;
1378
			$line->total_tva		= $object->lines[$i]->total_tva;
1379
			$line->total_localtax1	= $object->lines[$i]->total_localtax1;
1380
			$line->total_localtax2	= $object->lines[$i]->total_localtax2;
1381
			$line->total_ttc		= $object->lines[$i]->total_ttc;
1382
			$line->vat_src_code = $object->lines[$i]->vat_src_code;
1383
			$line->tva_tx = $object->lines[$i]->tva_tx;
1384
			$line->localtax1_tx		= $object->lines[$i]->localtax1_tx;
1385
			$line->localtax2_tx		= $object->lines[$i]->localtax2_tx;
1386
			$line->qty = $object->lines[$i]->qty;
1387
			$line->fk_remise_except = $object->lines[$i]->fk_remise_except;
1388
			$line->remise_percent = $object->lines[$i]->remise_percent;
1389
			$line->fk_product = $object->lines[$i]->fk_product;
1390
			$line->info_bits = $object->lines[$i]->info_bits;
1391
			$line->product_type		= $object->lines[$i]->product_type;
1392
			$line->rang = $object->lines[$i]->rang;
1393
			$line->special_code		= $object->lines[$i]->special_code;
1394
			$line->fk_parent_line = $object->lines[$i]->fk_parent_line;
1395
			$line->fk_unit = $object->lines[$i]->fk_unit;
1396
			$line->date_start = $object->lines[$i]->date_start;
1397
			$line->date_end = $object->lines[$i]->date_end;
1398
1399
			// Multicurrency
1400
			$line->fk_multicurrency = $object->lines[$i]->fk_multicurrency;
1401
			$line->multicurrency_code = $object->lines[$i]->multicurrency_code;
1402
			$line->multicurrency_subprice = $object->lines[$i]->multicurrency_subprice;
1403
			$line->multicurrency_total_ht = $object->lines[$i]->multicurrency_total_ht;
1404
			$line->multicurrency_total_tva = $object->lines[$i]->multicurrency_total_tva;
1405
			$line->multicurrency_total_ttc = $object->lines[$i]->multicurrency_total_ttc;
1406
1407
			$line->fk_fournprice = $object->lines[$i]->fk_fournprice;
1408
			$marginInfos			= getMarginInfos($object->lines[$i]->subprice, $object->lines[$i]->remise_percent, $object->lines[$i]->tva_tx, $object->lines[$i]->localtax1_tx, $object->lines[$i]->localtax2_tx, $object->lines[$i]->fk_fournprice, $object->lines[$i]->pa_ht);
1409
			$line->pa_ht			= $marginInfos[0];
1410
1411
			// get extrafields from original line
1412
			$object->lines[$i]->fetch_optionals();
1413
			foreach ($object->lines[$i]->array_options as $options_key => $value) {
1414
				$line->array_options[$options_key] = $value;
1415
			}
1416
1417
			$this->lines[$i] = $line;
1418
		}
1419
1420
		$this->socid                = $object->socid;
1421
		$this->fk_project           = $object->fk_project;
1422
		$this->fk_account = $object->fk_account;
1423
		$this->cond_reglement_id    = $object->cond_reglement_id;
1424
		$this->mode_reglement_id    = $object->mode_reglement_id;
1425
		$this->availability_id      = $object->availability_id;
1426
		$this->demand_reason_id     = $object->demand_reason_id;
1427
		$this->delivery_date        = (empty($object->delivery_date) ? $object->date_livraison : $object->delivery_date);
1428
		$this->date_livraison       = $object->delivery_date; // deprecated
1429
		$this->fk_delivery_address  = $object->fk_delivery_address; // deprecated
1430
		$this->contact_id           = $object->contact_id;
1431
		$this->ref_client           = $object->ref_client;
1432
1433
		if (empty($conf->global->MAIN_DISABLE_PROPAGATE_NOTES_FROM_ORIGIN)) {
1434
			$this->note_private = $object->note_private;
1435
			$this->note_public = $object->note_public;
1436
		}
1437
1438
		$this->module_source = $object->module_source;
1439
		$this->pos_source = $object->pos_source;
1440
1441
		$this->origin = $object->element;
1442
		$this->origin_id = $object->id;
1443
1444
		$this->fk_user_author = $user->id;
1445
1446
		// get extrafields from original line
1447
		$object->fetch_optionals();
1448
		foreach ($object->array_options as $options_key => $value) {
1449
			$this->array_options[$options_key] = $value;
1450
		}
1451
1452
		// Possibility to add external linked objects with hooks
1453
		$this->linked_objects[$this->origin] = $this->origin_id;
1454
		if (!empty($object->other_linked_objects) && is_array($object->other_linked_objects)) {
1455
			$this->linked_objects = array_merge($this->linked_objects, $object->other_linked_objects);
1456
		}
1457
1458
		$ret = $this->create($user);
1459
1460
		if ($ret > 0) {
1461
			// Actions hooked (by external module)
1462
			$hookmanager->initHooks(array('invoicedao'));
1463
1464
			$parameters = array('objFrom'=>$object);
1465
			$action = '';
1466
			$reshook = $hookmanager->executeHooks('createFrom', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1467
			if ($reshook < 0) {
1468
				$this->errors += $hookmanager->errors;
1469
				$this->error = $hookmanager->error;
1470
				$error++;
1471
			}
1472
1473
			if (!$error) {
1474
				return 1;
1475
			} else {
1476
				return -1;
1477
			}
1478
		} else {
1479
			return -1;
1480
		}
1481
	}
1482
1483
	/**
1484
	 * Creates a deposit from a proposal or an order by grouping lines by VAT rates
1485
	 *
1486
	 * @param	Propal|Commande		$origin					The original proposal or order
1487
	 * @param	int					$date					Invoice date
1488
	 * @param	int					$payment_terms_id		Invoice payment terms
1489
	 * @param	User				$user					Object user
1490
	 * @param	int					$notrigger				1=Does not execute triggers, 0= execute triggers
1491
	 * @param	bool				$autoValidateDeposit	Whether to aumatically validate the deposit created
1492
	 * @param	array				$overrideFields			Array of fields to force values
1493
	 * @return	Facture|null								The deposit created, or null if error (populates $origin->error in this case)
1494
	 */
1495
	static public function createDepositFromOrigin(CommonObject $origin, $date, $payment_terms_id, User $user, $notrigger = 0, $autoValidateDeposit = false, $overrideFields = array())
1496
	{
1497
		global $conf, $langs, $hookmanager, $action;
1498
1499
		if (! in_array($origin->element, array('propal', 'commande'))) {
1500
			$origin->error = 'ErrorCanOnlyAutomaticallyGenerateADepositFromProposalOrOrder';
1501
			return null;
1502
		}
1503
1504
		if (empty($date)) {
1505
			$origin->error = $langs->trans('ErrorFieldRequired', $langs->transnoentities('DateInvoice'));
1506
			return null;
1507
		}
1508
1509
		require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php';
1510
1511
		if ($date > (dol_get_last_hour(dol_now('tzuserrel')) + (empty($conf->global->INVOICE_MAX_FUTURE_DELAY) ? 0 : $conf->global->INVOICE_MAX_FUTURE_DELAY))) {
1512
			$origin->error = 'ErrorDateIsInFuture';
1513
			return null;
1514
		}
1515
1516
		if ($payment_terms_id <= 0) {
1517
			$origin->error = $langs->trans('ErrorFieldRequired', $langs->transnoentities('PaymentConditionsShort'));
1518
			return null;
1519
		}
1520
1521
		$payment_conditions_deposit_percent = getDictionaryValue('c_payment_term', 'deposit_percent', $origin->cond_reglement_id);
1522
1523
		if (empty($payment_conditions_deposit_percent)) {
1524
			$origin->error = 'ErrorPaymentConditionsNotEligibleToDepositCreation';
1525
			return null;
1526
		}
1527
1528
		if (empty($origin->deposit_percent)) {
1529
			$origin->error = $langs->trans('ErrorFieldRequired', $langs->transnoentities('DepositPercent'));
1530
			return null;
1531
		}
1532
1533
		$deposit = new self($origin->db);
1534
		$deposit->socid = $origin->socid;
1535
		$deposit->type = self::TYPE_DEPOSIT;
1536
		$deposit->fk_project = $origin->fk_project;
1537
		$deposit->ref_client = $origin->ref_client;
1538
		$deposit->date = $date;
1539
		$deposit->mode_reglement_id = $origin->mode_reglement_id;
1540
		$deposit->cond_reglement_id = $payment_terms_id;
1541
		$deposit->availability_id = $origin->availability_id;
1542
		$deposit->demand_reason_id = $origin->demand_reason_id;
1543
		$deposit->fk_account = $origin->fk_account;
1544
		$deposit->fk_incoterms = $origin->fk_incoterms;
1545
		$deposit->location_incoterms = $origin->location_incoterms;
1546
		$deposit->fk_multicurrency = $origin->fk_multicurrency;
1547
		$deposit->multicurrency_code = $origin->multicurrency_code;
1548
		$deposit->multicurrency_tx = $origin->multicurrency_tx;
1549
		$deposit->module_source = $origin->module_source;
1550
		$deposit->pos_source = $origin->pos_source;
1551
		$deposit->model_pdf = 'crabe';
1552
1553
		$modelByTypeConfName = 'FACTURE_ADDON_PDF_' . $deposit->type;
1554
1555
		if (!empty($conf->global->$modelByTypeConfName)) {
1556
			$deposit->model_pdf = $conf->global->$modelByTypeConfName;
1557
		} elseif (!empty($conf->global->FACTURE_ADDON_PDF)) {
1558
			$deposit->model_pdf = $conf->global->FACTURE_ADDON_PDF;
1559
		}
1560
1561
		if (empty($conf->global->MAIN_DISABLE_PROPAGATE_NOTES_FROM_ORIGIN)) {
1562
			$deposit->note_private = $origin->note_private;
1563
			$deposit->note_public = $origin->note_public;
1564
		}
1565
1566
		$deposit->origin = $origin->element;
1567
		$deposit->origin_id = $origin->id;
1568
1569
		$origin->fetch_optionals();
1570
1571
		foreach ($origin->array_options as $extrakey => $value) {
1572
			$deposit->array_options[$extrakey] = $value;
1573
		}
1574
1575
		$deposit->linked_objects[$deposit->origin] = $deposit->origin_id;
1576
1577
		foreach ($overrideFields as $key => $value) {
1578
			$deposit->$key = $value;
1579
		}
1580
1581
		$deposit->context['createdepositfromorigin'] = 'createdepositfromorigin';
1582
1583
		$origin->db->begin();
1584
1585
		// Facture::create() also imports contact from origin
1586
		$createReturn = $deposit->create($user, $notrigger);
1587
1588
		if ($createReturn <= 0) {
1589
			$origin->db->rollback();
1590
			$origin->error = $deposit->error;
1591
			$origin->errors = $deposit->errors;
1592
			return null;
1593
		}
1594
1595
		$amount_ttc_diff = 0;
1596
		$amountdeposit = array();
1597
		$descriptions = array();
1598
1599
		if (!empty($conf->global->MAIN_DEPOSIT_MULTI_TVA)) {
1600
			$amount = $origin->total_ttc * ($origin->deposit_percent / 100);
1601
1602
			$TTotalByTva = array();
1603
			foreach ($origin->lines as &$line) {
1604
				if (!empty($line->special_code)) {
1605
					continue;
1606
				}
1607
				$TTotalByTva[$line->tva_tx] += $line->total_ttc;
1608
				$descriptions[$line->tva_tx] .= '<li>' . (!empty($line->product_ref) ? $line->product_ref . ' - ' :  '');
1609
				$descriptions[$line->tva_tx] .= (!empty($line->product_label) ? $line->product_label . ' - ' : '');
1610
				$descriptions[$line->tva_tx] .= $langs->trans('Qty') . ' : ' . $line->qty;
1611
				$descriptions[$line->tva_tx] .= ' - ' . $langs->trans('TotalHT') . ' : ' . price($line->total_ht) . '</li>';
1612
			}
1613
1614
			foreach ($TTotalByTva as $tva => &$total) {
1615
				$coef = $total / $origin->total_ttc; // Calc coef
1616
				$am = $amount * $coef;
1617
				$amount_ttc_diff += $am;
1618
				$amountdeposit[$tva] += $am / (1 + $tva / 100); // Convert into HT for the addline
1619
			}
1620
		} else {
1621
			$totalamount = 0;
1622
			$lines = $origin->lines;
1623
			$numlines = count($lines);
1624
			for ($i = 0; $i < $numlines; $i++) {
1625
				if (empty($lines[$i]->qty)) {
1626
					continue; // We discard qty=0, it is an option
1627
				}
1628
				if (!empty($lines[$i]->special_code)) {
1629
					continue; // We discard special_code (frais port, ecotaxe, option, ...)
1630
				}
1631
1632
				$totalamount += $lines[$i]->total_ht; // Fixme : is it not for the customer ? Shouldn't we take total_ttc ?
1633
				$tva_tx = $lines[$i]->tva_tx;
1634
				$amountdeposit[$tva_tx] += ($lines[$i]->total_ht * $origin->deposit_percent) / 100;
1635
				$descriptions[$tva_tx] .= '<li>' . (!empty($lines[$i]->product_ref) ? $lines[$i]->product_ref . ' - ' :  '');
1636
				$descriptions[$tva_tx] .= (!empty($lines[$i]->product_label) ? $lines[$i]->product_label . ' - ' : '');
1637
				$descriptions[$tva_tx] .= $langs->trans('Qty') . ' : ' . $lines[$i]->qty;
1638
				$descriptions[$tva_tx] .= ' - ' . $langs->trans('TotalHT') . ' : ' . price($lines[$i]->total_ht) . '</li>';
1639
			}
1640
1641
			if ($totalamount == 0) {
1642
				$amountdeposit[0] = 0;
1643
			}
1644
1645
			$amount_ttc_diff = $amountdeposit[0];
1646
		}
1647
1648
		foreach ($amountdeposit as $tva => $amount) {
1649
			if (empty($amount)) {
1650
				continue;
1651
			}
1652
1653
			$descline = '(DEPOSIT) ('. $origin->deposit_percent .'%) - '.$origin->ref;
1654
1655
			// Hidden conf
1656
			if (!empty($conf->global->INVOICE_DEPOSIT_VARIABLE_MODE_DETAIL_LINES_IN_DESCRIPTION) && !empty($descriptions[$tva])) {
1657
				$descline .= '<ul>' . $descriptions[$tva] . '</ul>';
1658
			}
1659
1660
			$addlineResult = $deposit->addline(
1661
				$descline,
1662
				$amount, // subprice
1663
				1, // quantity
1664
				$tva, // vat rate
1665
				0, // localtax1_tx
1666
				0, // localtax2_tx
1667
				(empty($conf->global->INVOICE_PRODUCTID_DEPOSIT) ? 0 : $conf->global->INVOICE_PRODUCTID_DEPOSIT), // fk_product
1668
				0, // remise_percent
1669
				0, // date_start
1670
				0, // date_end
1671
				0,
1672
				0, // info_bits
1673
				0,
1674
				'HT',
1675
				0,
1676
				0, // product_type
1677
				1,
1678
				0, // special_code
1679
				$deposit->origin,
1680
				0,
1681
				0,
1682
				0,
1683
				0
1684
				//,$langs->trans('Deposit') //Deprecated
1685
			);
1686
1687
			if ($addlineResult < 0) {
1688
				$origin->db->rollback();
1689
				$origin->error = $deposit->error;
1690
				$origin->errors = $deposit->errors;
1691
				return null;
1692
			}
1693
		}
1694
1695
		$diff = $deposit->total_ttc - $amount_ttc_diff;
1696
1697
		if (!empty($conf->global->MAIN_DEPOSIT_MULTI_TVA) && $diff != 0) {
1698
			$deposit->fetch_lines();
1699
			$subprice_diff = $deposit->lines[0]->subprice - $diff / (1 + $deposit->lines[0]->tva_tx / 100);
1700
1701
			$updatelineResult = $deposit->updateline(
1702
				$deposit->lines[0]->id,
1703
				$deposit->lines[0]->desc,
1704
				$subprice_diff,
1705
				$deposit->lines[0]->qty,
1706
				$deposit->lines[0]->remise_percent,
1707
				$deposit->lines[0]->date_start,
1708
				$deposit->lines[0]->date_end,
1709
				$deposit->lines[0]->tva_tx,
1710
				0,
1711
				0,
1712
				'HT',
1713
				$deposit->lines[0]->info_bits,
1714
				$deposit->lines[0]->product_type,
1715
				0,
1716
				0,
1717
				0,
1718
				$deposit->lines[0]->pa_ht,
1719
				$deposit->lines[0]->label,
1720
				0,
1721
				array(),
1722
				100
1723
			);
1724
1725
			if ($updatelineResult < 0) {
1726
				$origin->db->rollback();
1727
				$origin->error = $deposit->error;
1728
				$origin->errors = $deposit->errors;
1729
				return null;
1730
			}
1731
		}
1732
1733
1734
		if (! is_object($hookmanager)) {
1735
			require_once DOL_DOCUMENT_ROOT . '/core/class/hookmanager.class.php';
1736
			$hookmanager = new HookManager($origin->db);
1737
		}
1738
1739
		$hookmanager->initHooks(array('invoicedao'));
1740
1741
		$parameters = array('objFrom' => $origin);
1742
		$reshook = $hookmanager->executeHooks('createFrom', $parameters, $deposit, $action); // Note that $action and $object may have been
1743
		// modified by hook
1744
		if ($reshook < 0) {
1745
			$origin->db->rollback();
1746
			$origin->error = $hookmanager->error;
1747
			$origin->errors = $hookmanager->errors;
1748
			return null;
1749
		}
1750
1751
		if (!empty($autoValidateDeposit)) {
1752
			$validateReturn = $deposit->validate($user, '', 0, $notrigger);
1753
1754
			if ($validateReturn < 0) {
1755
				$origin->db->rollback();
1756
				$origin->error = $deposit->error;
1757
				$origin->errors = $deposit->errors;
1758
				return null;
1759
			}
1760
		}
1761
1762
		unset($deposit->context['createdepositfromorigin']);
1763
1764
		$origin->db->commit();
1765
1766
		return $deposit;
1767
	}
1768
1769
	/**
1770
	 * getTooltipContentArray
1771
	 *
1772
	 * @param array $params ex option, infologin
1773
	 * @since v18
1774
	 * @return array
1775
	 */
1776
	public function getTooltipContentArray($params)
1777
	{
1778
		global $conf, $langs, $mysoc, $user;
1779
1780
		$langs->load('bills');
1781
1782
		$datas = [];
1783
		$moretitle = $params['moretitle'] ?? '';
1784
		$picto = $this->picto;
1785
		if ($this->type == self::TYPE_REPLACEMENT) {
1786
			$picto .= 'r'; // Replacement invoice
1787
		}
1788
		if ($this->type == self::TYPE_CREDIT_NOTE) {
1789
			$picto .= 'a'; // Credit note
1790
		}
1791
		if ($this->type == self::TYPE_DEPOSIT) {
1792
			$picto .= 'd'; // Deposit invoice
1793
		}
1794
1795
		if ($user->hasRight("facture", "read")) {
1796
			$datas['picto'] = img_picto('', $picto).' <u class="paddingrightonly">'.$langs->trans("Invoice").'</u>';
1797
			if (isset($this->statut) && isset($this->alreadypaid)) {
1798
				$datas['picto'] .= ' '.$this->getLibStatut(5, $this->alreadypaid);
1799
			}
1800
			$datas['picto'] .= '&nbsp;'.$this->getLibType(1);
1801
			if ($moretitle) {
1802
				$datas['picto'] = ' - '.$moretitle;
1803
			}
1804
			if (!empty($this->ref)) {
1805
				$datas['ref'] = '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
1806
			}
1807
			if (!empty($this->ref_customer)) {
1808
				$datas['refcustomer'] = '<br><b>'.$langs->trans('RefCustomer').':</b> '.$this->ref_customer;
1809
			}
1810
			if (!empty($this->date)) {
1811
				$datas['date'] = '<br><b>'.$langs->trans('Date').':</b> '.dol_print_date($this->date, 'day');
1812
			}
1813
			if (!empty($this->total_ht)) {
1814
				$datas['amountht'] = '<br><b>'.$langs->trans('AmountHT').':</b> '.price($this->total_ht, 0, $langs, 0, -1, -1, $conf->currency);
1815
			}
1816
			if (!empty($this->total_tva)) {
1817
				$datas['amountvat'] = '<br><b>'.$langs->trans('AmountVAT').':</b> '.price($this->total_tva, 0, $langs, 0, -1, -1, $conf->currency);
1818
			}
1819
			if (!empty($this->total_localtax1) && $this->total_localtax1 != 0) {
1820
				// We keep test != 0 because $this->total_localtax1 can be '0.00000000'
1821
				$datas['amountlt1'] = '<br><b>'.$langs->transcountry('AmountLT1', $mysoc->country_code).':</b> '.price($this->total_localtax1, 0, $langs, 0, -1, -1, $conf->currency);
1822
			}
1823
			if (!empty($this->total_localtax2) && $this->total_localtax2 != 0) {
1824
				$datas['amountlt2'] = '<br><b>'.$langs->transcountry('AmountLT2', $mysoc->country_code).':</b> '.price($this->total_localtax2, 0, $langs, 0, -1, -1, $conf->currency);
1825
			}
1826
			if (!empty($this->total_ttc)) {
1827
				$datas['amountttc'] = '<br><b>'.$langs->trans('AmountTTC').':</b> '.price($this->total_ttc, 0, $langs, 0, -1, -1, $conf->currency);
1828
			}
1829
		}
1830
1831
		return $datas;
1832
	}
1833
1834
	/**
1835
	 *  Return clicable link of object (with eventually picto)
1836
	 *
1837
	 *  @param	int		$withpicto       			Add picto into link
1838
	 *  @param  string	$option          			Where point the link
1839
	 *  @param  int		$max             			Maxlength of ref
1840
	 *  @param  int		$short           			1=Return just URL
1841
	 *  @param  string  $moretitle       			Add more text to title tooltip
1842
	 *  @param	int  	$notooltip		 			1=Disable tooltip
1843
	 *  @param  int     $addlinktonotes  			1=Add link to notes
1844
	 *  @param  int     $save_lastsearch_value		-1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
1845
	 *  @param  string  $target                     Target of link ('', '_self', '_blank', '_parent', '_backoffice', ...)
1846
	 *  @return string 			         			String with URL
1847
	 */
1848
	public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $moretitle = '', $notooltip = 0, $addlinktonotes = 0, $save_lastsearch_value = -1, $target = '')
1849
	{
1850
		global $langs, $conf, $user, $mysoc;
1851
1852
		if (!empty($conf->dol_no_mouse_hover)) {
1853
			$notooltip = 1; // Force disable tooltips
1854
		}
1855
1856
		$result = '';
1857
1858
		if ($option == 'withdraw') {
1859
			$url = DOL_URL_ROOT.'/compta/facture/prelevement.php?facid='.$this->id;
1860
		} else {
1861
			$url = DOL_URL_ROOT.'/compta/facture/card.php?facid='.$this->id;
1862
		}
1863
1864
		if (!$user->hasRight("facture", "read")) {
1865
			$option = 'nolink';
1866
		}
1867
1868
		if ($option !== 'nolink') {
1869
			// Add param to save lastsearch_values or not
1870
			$add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
1871
			if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
1872
				$add_save_lastsearch_values = 1;
1873
			}
1874
			if ($add_save_lastsearch_values) {
1875
				$url .= '&save_lastsearch_values=1';
1876
			}
1877
		}
1878
1879
		if ($short) {
1880
			return $url;
1881
		}
1882
1883
		$picto = $this->picto;
1884
		if ($this->type == self::TYPE_REPLACEMENT) {
1885
			$picto .= 'r'; // Replacement invoice
1886
		}
1887
		if ($this->type == self::TYPE_CREDIT_NOTE) {
1888
			$picto .= 'a'; // Credit note
1889
		}
1890
		if ($this->type == self::TYPE_DEPOSIT) {
1891
			$picto .= 'd'; // Deposit invoice
1892
		}
1893
		$params = [
1894
			'id' => $this->id,
1895
			'objecttype' => $this->element,
1896
			'moretitle' => $moretitle,
1897
			'option' => $option,
1898
		];
1899
		$classfortooltip = 'classfortooltip';
1900
		$dataparams = '';
1901
		if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
1902
			$classfortooltip = 'classforajaxtooltip';
1903
			$dataparams = ' data-params='.json_encode($params);
1904
			// $label = $langs->trans('Loading');
1905
		}
1906
		$label = implode($this->getTooltipContentArray($params));
1907
1908
		$linkclose = ($target ? ' target="'.$target.'"' : '');
1909
		if (empty($notooltip) && $user->hasRight("facture", "read")) {
1910
			if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
1911
				$label = $langs->trans("Invoice");
1912
				$linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"';
1913
			}
1914
			$linkclose .= $dataparams.' title="'.dol_escape_htmltag($label, 1).'"';
1915
			$linkclose .= ' class="'.$classfortooltip.'"';
1916
		}
1917
1918
		$linkstart = '<a href="'.$url.'"';
1919
		$linkstart .= $linkclose.'>';
1920
		$linkend = '</a>';
1921
1922
		if ($option == 'nolink') {
1923
			$linkstart = '';
1924
			$linkend = '';
1925
		}
1926
1927
		$result .= $linkstart;
1928
		if ($withpicto) {
1929
			$result .= img_object(($notooltip ? '' : $label), $picto, ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : $dataparams.' class="'.(($withpicto != 2) ? 'paddingright ' : '').$classfortooltip.'"'), 0, 0, $notooltip ? 0 : 1);
1930
		}
1931
		if ($withpicto != 2) {
1932
			$result .= ($max ?dol_trunc($this->ref, $max) : $this->ref);
1933
		}
1934
		$result .= $linkend;
1935
1936
		if ($addlinktonotes) {
1937
			$txttoshow = ($user->socid > 0 ? $this->note_public : $this->note_private);
1938
			if ($txttoshow) {
1939
				//$notetoshow = $langs->trans("ViewPrivateNote").':<br>'.dol_string_nohtmltag($txttoshow, 1);
1940
				$notetoshow = $langs->trans("ViewPrivateNote").':<br>'.$txttoshow;
1941
				$result .= ' <span class="note inline-block">';
1942
				$result .= '<a href="'.DOL_URL_ROOT.'/compta/facture/note.php?id='.$this->id.'" class="classfortooltip" title="'.dol_escape_htmltag($notetoshow, 1, 1).'">';
1943
				$result .= img_picto('', 'note');
1944
				$result .= '</a>';
1945
				//$result.=img_picto($langs->trans("ViewNote"),'object_generic');
1946
				//$result.='</a>';
1947
				$result .= '</span>';
1948
			}
1949
		}
1950
1951
		global $action, $hookmanager;
1952
		$hookmanager->initHooks(array('invoicedao'));
1953
		$parameters = array('id'=>$this->id, 'getnomurl' => &$result, 'notooltip' => $notooltip, 'addlinktonotes' => $addlinktonotes, 'save_lastsearch_value'=> $save_lastsearch_value, 'target' => $target);
1954
		$reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1955
		if ($reshook > 0) {
1956
			$result = $hookmanager->resPrint;
1957
		} else {
1958
			$result .= $hookmanager->resPrint;
1959
		}
1960
1961
		return $result;
1962
	}
1963
1964
	/**
1965
	 *	Get object from database. Get also lines.
1966
	 *
1967
	 *	@param      int		$rowid       		Id of object to load
1968
	 * 	@param		string	$ref				Reference of invoice
1969
	 * 	@param		string	$ref_ext			External reference of invoice
1970
	 * 	@param		int		$notused			Not used
1971
	 *  @param		bool	$fetch_situation	Load also the previous and next situation invoice into $tab_previous_situation_invoice and $tab_next_situation_invoice
1972
	 *	@return     int         				>0 if OK, <0 if KO, 0 if not found
1973
	 */
1974
	public function fetch($rowid, $ref = '', $ref_ext = '', $notused = '', $fetch_situation = false)
1975
	{
1976
		if (empty($rowid) && empty($ref) && empty($ref_ext)) {
1977
			return -1;
1978
		}
1979
1980
		$sql = 'SELECT f.rowid, f.entity, f.ref, f.ref_client, f.ref_ext, f.type, f.fk_soc';
1981
		$sql .= ', f.total_tva, f.localtax1, f.localtax2, f.total_ht, f.total_ttc, f.revenuestamp';
1982
		$sql .= ', f.remise_percent, f.remise_absolue, f.remise';
1983
		$sql .= ', f.datef as df, f.date_pointoftax';
1984
		$sql .= ', f.date_lim_reglement as dlr';
1985
		$sql .= ', f.datec as datec';
1986
		$sql .= ', f.date_valid as datev';
1987
		$sql .= ', f.tms as datem';
1988
		$sql .= ', f.note_private, f.note_public, f.fk_statut, f.paye, f.close_code, f.close_note, f.fk_user_author, f.fk_user_valid, f.fk_user_modif, f.model_pdf, f.last_main_doc';
1989
		$sql .= ', f.fk_facture_source, f.fk_fac_rec_source';
1990
		$sql .= ', f.fk_mode_reglement, f.fk_cond_reglement, f.fk_projet as fk_project, f.extraparams';
1991
		$sql .= ', f.situation_cycle_ref, f.situation_counter, f.situation_final';
1992
		$sql .= ', f.fk_account';
1993
		$sql .= ", f.fk_multicurrency, f.multicurrency_code, f.multicurrency_tx, f.multicurrency_total_ht, f.multicurrency_total_tva, f.multicurrency_total_ttc";
1994
		$sql .= ', p.code as mode_reglement_code, p.libelle as mode_reglement_libelle';
1995
		$sql .= ', c.code as cond_reglement_code, c.libelle as cond_reglement_libelle, c.libelle_facture as cond_reglement_libelle_doc';
1996
		$sql .= ', f.fk_incoterms, f.location_incoterms';
1997
		$sql .= ', f.module_source, f.pos_source';
1998
		$sql .= ", i.libelle as label_incoterms";
1999
		$sql .= ", f.retained_warranty as retained_warranty, f.retained_warranty_date_limit as retained_warranty_date_limit, f.retained_warranty_fk_cond_reglement as retained_warranty_fk_cond_reglement";
2000
		$sql .= ' FROM '.MAIN_DB_PREFIX.'facture as f';
2001
		$sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_payment_term as c ON f.fk_cond_reglement = c.rowid';
2002
		$sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_paiement as p ON f.fk_mode_reglement = p.id';
2003
		$sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_incoterms as i ON f.fk_incoterms = i.rowid';
2004
2005
		if ($rowid) {
2006
			$sql .= " WHERE f.rowid = ".((int) $rowid);
2007
		} else {
2008
			$sql .= ' WHERE f.entity IN ('.getEntity('invoice').')'; // Don't use entity if you use rowid
2009
			if ($ref) {
2010
				$sql .= " AND f.ref = '".$this->db->escape($ref)."'";
2011
			}
2012
			if ($ref_ext) {
2013
				$sql .= " AND f.ref_ext = '".$this->db->escape($ref_ext)."'";
2014
			}
2015
		}
2016
2017
		dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
2018
		$resql = $this->db->query($sql);
2019
		if ($resql) {
2020
			if ($this->db->num_rows($resql)) {
2021
				$obj = $this->db->fetch_object($resql);
2022
2023
				$this->id = $obj->rowid;
2024
				$this->entity = $obj->entity;
2025
2026
				$this->ref					= $obj->ref;
2027
				$this->ref_client			= $obj->ref_client;
2028
				$this->ref_customer			= $obj->ref_client;
2029
				$this->ref_ext				= $obj->ref_ext;
2030
				$this->type					= $obj->type;
2031
				$this->date					= $this->db->jdate($obj->df);
2032
				$this->date_pointoftax		= $this->db->jdate($obj->date_pointoftax);
2033
				$this->date_creation = $this->db->jdate($obj->datec);
2034
				$this->date_validation		= $this->db->jdate($obj->datev);
2035
				$this->date_modification = $this->db->jdate($obj->datem);
2036
				$this->datem = $this->db->jdate($obj->datem);
2037
				$this->remise_percent		= $obj->remise_percent;
2038
				$this->remise_absolue		= $obj->remise_absolue;
2039
				$this->total_ht				= $obj->total_ht;
2040
				$this->total_tva			= $obj->total_tva;
2041
				$this->total_localtax1		= $obj->localtax1;
2042
				$this->total_localtax2		= $obj->localtax2;
2043
				$this->total_ttc			= $obj->total_ttc;
2044
				$this->revenuestamp = $obj->revenuestamp;
2045
				$this->paye = $obj->paye;
2046
				$this->close_code			= $obj->close_code;
2047
				$this->close_note			= $obj->close_note;
2048
2049
				$this->socid = $obj->fk_soc;
2050
				$this->thirdparty = null; // Clear if another value was already set by fetch_thirdparty
2051
2052
				$this->fk_project = $obj->fk_project;
2053
				$this->project = null; // Clear if another value was already set by fetch_projet
2054
2055
				$this->statut = $obj->fk_statut;
2056
				$this->status = $obj->fk_statut;
2057
2058
				$this->date_lim_reglement = $this->db->jdate($obj->dlr);
2059
				$this->mode_reglement_id	= $obj->fk_mode_reglement;
2060
				$this->mode_reglement_code	= $obj->mode_reglement_code;
2061
				$this->mode_reglement		= $obj->mode_reglement_libelle;
2062
				$this->cond_reglement_id	= $obj->fk_cond_reglement;
2063
				$this->cond_reglement_code	= $obj->cond_reglement_code;
2064
				$this->cond_reglement		= $obj->cond_reglement_libelle;
2065
				$this->cond_reglement_doc = $obj->cond_reglement_libelle_doc;
2066
				$this->fk_account = ($obj->fk_account > 0) ? $obj->fk_account : null;
2067
				$this->fk_facture_source	= $obj->fk_facture_source;
2068
				$this->fk_fac_rec_source	= $obj->fk_fac_rec_source;
2069
				$this->note = $obj->note_private; // deprecated
2070
				$this->note_private = $obj->note_private;
2071
				$this->note_public			= $obj->note_public;
2072
				$this->user_author			= $obj->fk_user_author; // deprecated
2073
				$this->user_valid           = $obj->fk_user_valid; // deprecated
2074
				$this->user_modification    = $obj->fk_user_modif; // deprecated
2075
				$this->fk_user_author       = $obj->fk_user_author;
2076
				$this->fk_user_valid        = $obj->fk_user_valid;
2077
				$this->fk_user_modif        = $obj->fk_user_modif;
2078
				$this->model_pdf = $obj->model_pdf;
2079
				$this->modelpdf = $obj->model_pdf; // deprecated
2080
				$this->last_main_doc = $obj->last_main_doc;
2081
				$this->situation_cycle_ref  = $obj->situation_cycle_ref;
2082
				$this->situation_counter    = $obj->situation_counter;
2083
				$this->situation_final      = $obj->situation_final;
2084
				$this->retained_warranty    = $obj->retained_warranty;
2085
				$this->retained_warranty_date_limit         = $this->db->jdate($obj->retained_warranty_date_limit);
2086
				$this->retained_warranty_fk_cond_reglement  = $obj->retained_warranty_fk_cond_reglement;
2087
2088
				$this->extraparams = (array) json_decode($obj->extraparams, true);
2089
2090
				//Incoterms
2091
				$this->fk_incoterms         = $obj->fk_incoterms;
2092
				$this->location_incoterms   = $obj->location_incoterms;
2093
				$this->label_incoterms = $obj->label_incoterms;
2094
2095
				$this->module_source = $obj->module_source;
2096
				$this->pos_source = $obj->pos_source;
2097
2098
				// Multicurrency
2099
				$this->fk_multicurrency 		= $obj->fk_multicurrency;
2100
				$this->multicurrency_code = $obj->multicurrency_code;
2101
				$this->multicurrency_tx 		= $obj->multicurrency_tx;
2102
				$this->multicurrency_total_ht = $obj->multicurrency_total_ht;
2103
				$this->multicurrency_total_tva 	= $obj->multicurrency_total_tva;
2104
				$this->multicurrency_total_ttc 	= $obj->multicurrency_total_ttc;
2105
2106
				if (($this->type == self::TYPE_SITUATION || ($this->type == self::TYPE_CREDIT_NOTE && $this->situation_cycle_ref > 0)) && $fetch_situation) {
2107
					$this->fetchPreviousNextSituationInvoice();
2108
				}
2109
2110
				if ($this->status == self::STATUS_DRAFT) {
2111
					$this->brouillon = 1;
2112
				}
2113
2114
				// Retrieve all extrafield
2115
				// fetch optionals attributes and labels
2116
				$this->fetch_optionals();
2117
2118
				// Lines
2119
				$this->lines = array();
2120
2121
				$result = $this->fetch_lines();
2122
				if ($result < 0) {
2123
					$this->error = $this->db->error();
2124
					return -3;
2125
				}
2126
2127
				$this->db->free($resql);
2128
2129
				return 1;
2130
			} else {
2131
				$this->error = 'Invoice with id='.$rowid.' or ref='.$ref.' or ref_ext='.$ref_ext.' not found';
2132
2133
				dol_syslog(__METHOD__.$this->error, LOG_WARNING);
2134
				return 0;
2135
			}
2136
		} else {
2137
			$this->error = $this->db->lasterror();
2138
			return -1;
2139
		}
2140
	}
2141
2142
2143
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2144
	/**
2145
	 *	Load all detailed lines into this->lines
2146
	 *
2147
	 *	@param		int		$only_product	Return only physical products
2148
	 *	@param		int		$loadalsotranslation	Return translation for products
2149
	 *
2150
	 *	@return     int         1 if OK, < 0 if KO
2151
	 */
2152
	public function fetch_lines($only_product = 0, $loadalsotranslation = 0)
2153
	{
2154
		// phpcs:enable
2155
		global $langs, $conf;
2156
2157
		$this->lines = array();
2158
2159
		$sql = 'SELECT l.rowid, l.fk_facture, l.fk_product, l.fk_parent_line, l.label as custom_label, l.description, l.product_type, l.price, l.qty, l.vat_src_code, l.tva_tx,';
2160
		$sql .= ' l.localtax1_tx, l.localtax2_tx, l.localtax1_type, l.localtax2_type, l.remise_percent, l.fk_remise_except, l.subprice, l.ref_ext,';
2161
		$sql .= ' l.situation_percent, l.fk_prev_id,';
2162
		$sql .= ' l.rang, l.special_code,';
2163
		$sql .= ' l.date_start as date_start, l.date_end as date_end,';
2164
		$sql .= ' l.info_bits, l.total_ht, l.total_tva, l.total_localtax1, l.total_localtax2, l.total_ttc, l.fk_code_ventilation, l.fk_product_fournisseur_price as fk_fournprice, l.buy_price_ht as pa_ht,';
2165
		$sql .= ' l.fk_unit,';
2166
		$sql .= ' l.fk_multicurrency, l.multicurrency_code, l.multicurrency_subprice, l.multicurrency_total_ht, l.multicurrency_total_tva, l.multicurrency_total_ttc,';
2167
		$sql .= ' p.ref as product_ref, p.fk_product_type as fk_product_type, p.label as product_label, p.description as product_desc';
2168
		$sql .= ' FROM '.MAIN_DB_PREFIX.'facturedet as l';
2169
		$sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'product as p ON l.fk_product = p.rowid';
2170
		$sql .= ' WHERE l.fk_facture = '.((int) $this->id);
2171
		$sql .= ' ORDER BY l.rang, l.rowid';
2172
2173
		dol_syslog(get_class($this).'::fetch_lines', LOG_DEBUG);
2174
		$result = $this->db->query($sql);
2175
		if ($result) {
2176
			$num = $this->db->num_rows($result);
2177
			$i = 0;
2178
			while ($i < $num) {
2179
				$objp = $this->db->fetch_object($result);
2180
				$line = new FactureLigne($this->db);
2181
2182
				$line->id               = $objp->rowid;
2183
				$line->rowid = $objp->rowid; // deprecated
2184
				$line->fk_facture       = $objp->fk_facture;
2185
				$line->label            = $objp->custom_label; // deprecated
2186
				$line->desc             = $objp->description; // Description line
2187
				$line->description      = $objp->description; // Description line
2188
				$line->product_type     = $objp->product_type; // Type of line
2189
				$line->ref              = $objp->product_ref; // Ref product
2190
				$line->product_ref      = $objp->product_ref; // Ref product
2191
				$line->libelle          = $objp->product_label; // deprecated
2192
				$line->product_label = $objp->product_label; // Label product
2193
				$line->product_desc     = $objp->product_desc; // Description product
2194
				$line->fk_product_type  = $objp->fk_product_type; // Type of product
2195
				$line->qty              = $objp->qty;
2196
				$line->subprice         = $objp->subprice;
2197
				$line->ref_ext          = $objp->ref_ext; // line external ref
2198
2199
				$line->vat_src_code = $objp->vat_src_code;
2200
				$line->tva_tx           = $objp->tva_tx;
2201
				$line->localtax1_tx     = $objp->localtax1_tx;
2202
				$line->localtax2_tx     = $objp->localtax2_tx;
2203
				$line->localtax1_type   = $objp->localtax1_type;
2204
				$line->localtax2_type   = $objp->localtax2_type;
2205
				$line->remise_percent   = $objp->remise_percent;
2206
				$line->fk_remise_except = $objp->fk_remise_except;
2207
				$line->fk_product       = $objp->fk_product;
2208
				$line->date_start       = $this->db->jdate($objp->date_start);
2209
				$line->date_end         = $this->db->jdate($objp->date_end);
2210
				$line->date_start       = $this->db->jdate($objp->date_start);
2211
				$line->date_end         = $this->db->jdate($objp->date_end);
2212
				$line->info_bits        = $objp->info_bits;
2213
				$line->total_ht         = $objp->total_ht;
2214
				$line->total_tva        = $objp->total_tva;
2215
				$line->total_localtax1  = $objp->total_localtax1;
2216
				$line->total_localtax2  = $objp->total_localtax2;
2217
				$line->total_ttc        = $objp->total_ttc;
2218
				$line->code_ventilation = $objp->fk_code_ventilation;
0 ignored issues
show
Bug introduced by
The property code_ventilation does not exist on FactureLigne. Did you mean fk_code_ventilation?
Loading history...
2219
				$line->fk_fournprice = $objp->fk_fournprice;
2220
				$marginInfos = getMarginInfos($objp->subprice, $objp->remise_percent, $objp->tva_tx, $objp->localtax1_tx, $objp->localtax2_tx, $line->fk_fournprice, $objp->pa_ht);
2221
				$line->pa_ht = $marginInfos[0];
2222
				$line->marge_tx			= $marginInfos[1];
2223
				$line->marque_tx		= $marginInfos[2];
2224
				$line->rang = $objp->rang;
2225
				$line->special_code = $objp->special_code;
2226
				$line->fk_parent_line = $objp->fk_parent_line;
2227
				$line->situation_percent = $objp->situation_percent;
2228
				$line->fk_prev_id = $objp->fk_prev_id;
2229
				$line->fk_unit = $objp->fk_unit;
2230
2231
				// Accountancy
2232
				$line->fk_accounting_account = $objp->fk_code_ventilation;
2233
2234
				// Multicurrency
2235
				$line->fk_multicurrency = $objp->fk_multicurrency;
2236
				$line->multicurrency_code = $objp->multicurrency_code;
2237
				$line->multicurrency_subprice 	= $objp->multicurrency_subprice;
2238
				$line->multicurrency_total_ht 	= $objp->multicurrency_total_ht;
2239
				$line->multicurrency_total_tva 	= $objp->multicurrency_total_tva;
2240
				$line->multicurrency_total_ttc 	= $objp->multicurrency_total_ttc;
2241
2242
				$line->fetch_optionals();
2243
2244
				// multilangs
2245
				if (getDolGlobalInt('MAIN_MULTILANGS') && !empty($objp->fk_product) && !empty($loadalsotranslation)) {
2246
					$tmpproduct = new Product($this->db);
2247
					$tmpproduct->fetch($objp->fk_product);
2248
					$tmpproduct->getMultiLangs();
2249
2250
					$line->multilangs = $tmpproduct->multilangs;
2251
				}
2252
2253
				$this->lines[$i] = $line;
2254
2255
				$i++;
2256
			}
2257
			$this->db->free($result);
2258
			return 1;
2259
		} else {
2260
			$this->error = $this->db->error();
2261
			return -3;
2262
		}
2263
	}
2264
2265
	/**
2266
	 * Fetch previous and next situations invoices.
2267
	 * Return all previous and next invoices (both standard and credit notes).
2268
	 *
2269
	 * @return	void
2270
	 */
2271
	public function fetchPreviousNextSituationInvoice()
2272
	{
2273
		global $conf;
2274
2275
		$this->tab_previous_situation_invoice = array();
2276
		$this->tab_next_situation_invoice = array();
2277
2278
		$sql = 'SELECT rowid, type, situation_cycle_ref, situation_counter FROM '.MAIN_DB_PREFIX.'facture';
2279
		$sql .= " WHERE rowid <> ".((int) $this->id);
2280
		$sql .= ' AND entity = '.((int) $this->entity);
2281
		$sql .= ' AND situation_cycle_ref = '.(int) $this->situation_cycle_ref;
2282
		$sql .= ' ORDER BY situation_counter ASC';
2283
2284
		dol_syslog(get_class($this).'::fetchPreviousNextSituationInvoice ', LOG_DEBUG);
2285
		$result = $this->db->query($sql);
2286
		if ($result && $this->db->num_rows($result) > 0) {
2287
			while ($objp = $this->db->fetch_object($result)) {
2288
				$invoice = new Facture($this->db);
2289
				if ($invoice->fetch($objp->rowid) > 0) {
2290
					if ($objp->situation_counter < $this->situation_counter
2291
						|| ($objp->situation_counter == $this->situation_counter && $objp->rowid < $this->id) // This case appear when there are credit notes
2292
					   ) {
2293
						$this->tab_previous_situation_invoice[] = $invoice;
2294
					} else {
2295
						$this->tab_next_situation_invoice[] = $invoice;
2296
					}
2297
				}
2298
			}
2299
		}
2300
	}
2301
2302
	/**
2303
	 *      Update database
2304
	 *
2305
	 *      @param      User	$user        	User that modify
2306
	 *      @param      int		$notrigger	    0=launch triggers after, 1=disable triggers
2307
	 *      @return     int      			   	<0 if KO, >0 if OK
2308
	 */
2309
	public function update(User $user, $notrigger = 0)
2310
	{
2311
		global $conf;
2312
2313
		$error = 0;
2314
2315
		// Clean parameters
2316
		if (empty($this->type)) {
2317
			$this->type = self::TYPE_STANDARD;
2318
		}
2319
		if (isset($this->ref)) {
2320
			$this->ref = trim($this->ref);
2321
		}
2322
		if (isset($this->ref_ext)) {
2323
			$this->ref_ext = trim($this->ref_ext);
2324
		}
2325
		if (isset($this->ref_client)) {
2326
			$this->ref_client = trim($this->ref_client);
2327
		}
2328
		if (isset($this->increment)) {
2329
			$this->increment = trim($this->increment);
2330
		}
2331
		if (isset($this->close_code)) {
2332
			$this->close_code = trim($this->close_code);
2333
		}
2334
		if (isset($this->close_note)) {
2335
			$this->close_note = trim($this->close_note);
2336
		}
2337
		if (isset($this->note) || isset($this->note_private)) {
2338
			$this->note = (isset($this->note) ? trim($this->note) : trim($this->note_private)); // deprecated
2339
		}
2340
		if (isset($this->note) || isset($this->note_private)) {
2341
			$this->note_private = (isset($this->note_private) ? trim($this->note_private) : trim($this->note));
2342
		}
2343
		if (isset($this->note_public)) {
2344
			$this->note_public = trim($this->note_public);
2345
		}
2346
		if (isset($this->model_pdf)) {
2347
			$this->model_pdf = trim($this->model_pdf);
2348
		}
2349
		if (isset($this->import_key)) {
2350
			$this->import_key = trim($this->import_key);
2351
		}
2352
		if (isset($this->retained_warranty)) {
2353
			$this->retained_warranty = floatval($this->retained_warranty);
2354
		}
2355
2356
2357
		// Check parameters
2358
		// Put here code to add control on parameters values
2359
2360
		// Update request
2361
		$sql = "UPDATE ".MAIN_DB_PREFIX."facture SET";
2362
		$sql .= " ref=".(isset($this->ref) ? "'".$this->db->escape($this->ref)."'" : "null").",";
2363
		$sql .= " ref_ext=".(isset($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null").",";
2364
		$sql .= " type=".(isset($this->type) ? $this->db->escape($this->type) : "null").",";
2365
		$sql .= " ref_client=".(isset($this->ref_client) ? "'".$this->db->escape($this->ref_client)."'" : "null").",";
2366
		$sql .= " increment=".(isset($this->increment) ? "'".$this->db->escape($this->increment)."'" : "null").",";
2367
		$sql .= " fk_soc=".(isset($this->socid) ? $this->db->escape($this->socid) : "null").",";
2368
		$sql .= " datec=".(strval($this->date_creation) != '' ? "'".$this->db->idate($this->date_creation)."'" : 'null').",";
2369
		$sql .= " datef=".(strval($this->date) != '' ? "'".$this->db->idate($this->date)."'" : 'null').",";
2370
		$sql .= " date_pointoftax=".(strval($this->date_pointoftax) != '' ? "'".$this->db->idate($this->date_pointoftax)."'" : 'null').",";
2371
		$sql .= " date_valid=".(strval($this->date_validation) != '' ? "'".$this->db->idate($this->date_validation)."'" : 'null').",";
2372
		$sql .= " paye=".(isset($this->paye) ? $this->db->escape($this->paye) : 0).",";
2373
		$sql .= " remise_percent=".(isset($this->remise_percent) ? $this->db->escape($this->remise_percent) : "null").",";
2374
		$sql .= " remise_absolue=".(isset($this->remise_absolue) ? $this->db->escape($this->remise_absolue) : "null").",";
2375
		$sql .= " close_code=".(isset($this->close_code) ? "'".$this->db->escape($this->close_code)."'" : "null").",";
2376
		$sql .= " close_note=".(isset($this->close_note) ? "'".$this->db->escape($this->close_note)."'" : "null").",";
2377
		$sql .= " total_tva=".(isset($this->total_tva) ? $this->total_tva : "null").",";
2378
		$sql .= " localtax1=".(isset($this->total_localtax1) ? $this->total_localtax1 : "null").",";
2379
		$sql .= " localtax2=".(isset($this->total_localtax2) ? $this->total_localtax2 : "null").",";
2380
		$sql .= " total_ht=".(isset($this->total_ht) ? $this->total_ht : "null").",";
2381
		$sql .= " total_ttc=".(isset($this->total_ttc) ? $this->total_ttc : "null").",";
2382
		$sql .= " revenuestamp=".((isset($this->revenuestamp) && $this->revenuestamp != '') ? $this->db->escape($this->revenuestamp) : "null").",";
2383
		$sql .= " fk_statut=".(isset($this->statut) ? $this->db->escape($this->statut) : "null").",";
2384
		$sql .= " fk_user_author=".(isset($this->user_author) ? $this->db->escape($this->user_author) : "null").",";
2385
		$sql .= " fk_user_valid=".(isset($this->fk_user_valid) ? $this->db->escape($this->fk_user_valid) : "null").",";
2386
		$sql .= " fk_facture_source=".(isset($this->fk_facture_source) ? $this->db->escape($this->fk_facture_source) : "null").",";
2387
		$sql .= " fk_projet=".(isset($this->fk_project) ? $this->db->escape($this->fk_project) : "null").",";
2388
		$sql .= " fk_cond_reglement=".(isset($this->cond_reglement_id) ? $this->db->escape($this->cond_reglement_id) : "null").",";
2389
		$sql .= " fk_mode_reglement=".(isset($this->mode_reglement_id) ? $this->db->escape($this->mode_reglement_id) : "null").",";
2390
		$sql .= " date_lim_reglement=".(strval($this->date_lim_reglement) != '' ? "'".$this->db->idate($this->date_lim_reglement)."'" : 'null').",";
2391
		$sql .= " note_private=".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").",";
2392
		$sql .= " note_public=".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").",";
2393
		$sql .= " model_pdf=".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").",";
2394
		$sql .= " import_key=".(isset($this->import_key) ? "'".$this->db->escape($this->import_key)."'" : "null").",";
2395
		$sql .= " situation_cycle_ref=".(empty($this->situation_cycle_ref) ? "null" : $this->db->escape($this->situation_cycle_ref)).",";
2396
		$sql .= " situation_counter=".(empty($this->situation_counter) ? "null" : $this->db->escape($this->situation_counter)).",";
2397
		$sql .= " situation_final=".(empty($this->situation_final) ? "0" : $this->db->escape($this->situation_final)).",";
2398
		$sql .= " retained_warranty=".(empty($this->retained_warranty) ? "0" : $this->db->escape($this->retained_warranty)).",";
2399
		$sql .= " retained_warranty_date_limit=".(strval($this->retained_warranty_date_limit) != '' ? "'".$this->db->idate($this->retained_warranty_date_limit)."'" : 'null').",";
2400
		$sql .= " retained_warranty_fk_cond_reglement=".(isset($this->retained_warranty_fk_cond_reglement) ?intval($this->retained_warranty_fk_cond_reglement) : "null");
2401
		$sql .= " WHERE rowid=".((int) $this->id);
2402
2403
		$this->db->begin();
2404
2405
		dol_syslog(get_class($this)."::update", LOG_DEBUG);
2406
		$resql = $this->db->query($sql);
2407
		if (!$resql) {
2408
			$error++;
2409
			$this->errors[] = "Error ".$this->db->lasterror();
2410
		}
2411
2412
		if (!$error) {
2413
			$result = $this->insertExtraFields();
2414
			if ($result < 0) {
2415
				$error++;
2416
			}
2417
		}
2418
2419
		if (!$error && !$notrigger) {
2420
			// Call trigger
2421
			$result = $this->call_trigger('BILL_MODIFY', $user);
2422
			if ($result < 0) {
2423
				$error++;
2424
			}
2425
			// End call triggers
2426
		}
2427
2428
		// Commit or rollback
2429
		if ($error) {
2430
			foreach ($this->errors as $errmsg) {
2431
				dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
2432
				$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2433
			}
2434
			$this->db->rollback();
2435
			return -1 * $error;
2436
		} else {
2437
			$this->db->commit();
2438
			return 1;
2439
		}
2440
	}
2441
2442
2443
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2444
	/**
2445
	 *    Add a discount line into an invoice (as an invoice line) using an existing absolute discount (Consume the discount)
2446
	 *
2447
	 *    @param     int	$idremise	Id of absolute discount
2448
	 *    @return    int          		>0 if OK, <0 if KO
2449
	 */
2450
	public function insert_discount($idremise)
2451
	{
2452
		// phpcs:enable
2453
		global $conf, $langs;
2454
2455
		include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
2456
		include_once DOL_DOCUMENT_ROOT.'/core/class/discount.class.php';
2457
2458
		$this->db->begin();
2459
2460
		$remise = new DiscountAbsolute($this->db);
2461
		$result = $remise->fetch($idremise);
2462
2463
		if ($result > 0) {
2464
			if ($remise->fk_facture) {	// Protection against multiple submission
2465
				$this->error = $langs->trans("ErrorDiscountAlreadyUsed");
2466
				$this->db->rollback();
2467
				return -5;
2468
			}
2469
2470
			$facligne = new FactureLigne($this->db);
2471
			$facligne->fk_facture = $this->id;
2472
			$facligne->fk_remise_except = $remise->id;
2473
			$facligne->desc = $remise->description; // Description ligne
2474
			$facligne->vat_src_code = $remise->vat_src_code;
2475
			$facligne->tva_tx = $remise->tva_tx;
2476
			$facligne->subprice = -$remise->amount_ht;
2477
			$facligne->fk_product = 0; // Id produit predefini
2478
			$facligne->qty = 1;
2479
			$facligne->remise_percent = 0;
2480
			$facligne->rang = -1;
2481
			$facligne->info_bits = 2;
2482
2483
			if (!empty($conf->global->MAIN_ADD_LINE_AT_POSITION)) {
2484
				$facligne->rang = 1;
2485
				$linecount = count($this->lines);
2486
				for ($ii = 1; $ii <= $linecount; $ii++) {
2487
					$this->updateRangOfLine($this->lines[$ii - 1]->id, $ii+1);
2488
				}
2489
			}
2490
2491
			// Get buy/cost price of invoice that is source of discount
2492
			if ($remise->fk_facture_source > 0) {
2493
				$srcinvoice = new Facture($this->db);
2494
				$srcinvoice->fetch($remise->fk_facture_source);
2495
				include_once DOL_DOCUMENT_ROOT.'/core/class/html.formmargin.class.php'; // TODO Move this into commonobject
2496
				$formmargin = new FormMargin($this->db);
2497
				$arraytmp = $formmargin->getMarginInfosArray($srcinvoice, false);
2498
				$facligne->pa_ht = $arraytmp['pa_total'];
2499
			}
2500
2501
			$facligne->total_ht  = -$remise->amount_ht;
2502
			$facligne->total_tva = -$remise->amount_tva;
2503
			$facligne->total_ttc = -$remise->amount_ttc;
2504
2505
			$facligne->multicurrency_subprice = -$remise->multicurrency_subprice;
2506
			$facligne->multicurrency_total_ht = -$remise->multicurrency_amount_ht;
2507
			$facligne->multicurrency_total_tva = -$remise->multicurrency_amount_tva;
2508
			$facligne->multicurrency_total_ttc = -$remise->multicurrency_amount_ttc;
2509
2510
			$lineid = $facligne->insert();
2511
			if ($lineid > 0) {
2512
				$result = $this->update_price(1);
2513
				if ($result > 0) {
2514
					// Create link between discount and invoice line
2515
					$result = $remise->link_to_invoice($lineid, 0);
2516
					if ($result < 0) {
2517
						$this->error = $remise->error;
2518
						$this->db->rollback();
2519
						return -4;
2520
					}
2521
2522
					$this->db->commit();
2523
					return 1;
2524
				} else {
2525
					$this->error = $facligne->error;
2526
					$this->db->rollback();
2527
					return -1;
2528
				}
2529
			} else {
2530
				$this->error = $facligne->error;
2531
				$this->db->rollback();
2532
				return -2;
2533
			}
2534
		} else {
2535
			$this->db->rollback();
2536
			return -3;
2537
		}
2538
	}
2539
2540
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2541
	/**
2542
	 *	Set customer ref
2543
	 *
2544
	 *	@param     	string	$ref_client		Customer ref
2545
	 *  @param     	int		$notrigger		1=Does not execute triggers, 0= execute triggers
2546
	 *	@return		int						<0 if KO, >0 if OK
2547
	 */
2548
	public function set_ref_client($ref_client, $notrigger = 0)
2549
	{
2550
		// phpcs:enable
2551
		global $user;
2552
2553
		$error = 0;
2554
2555
		$this->db->begin();
2556
2557
		$sql = 'UPDATE '.MAIN_DB_PREFIX.'facture';
2558
		if (empty($ref_client)) {
2559
			$sql .= ' SET ref_client = NULL';
2560
		} else {
2561
			$sql .= ' SET ref_client = \''.$this->db->escape($ref_client).'\'';
2562
		}
2563
		$sql .= " WHERE rowid = ".((int) $this->id);
2564
2565
		dol_syslog(__METHOD__.' this->id='.$this->id.', ref_client='.$ref_client, LOG_DEBUG);
2566
		$resql = $this->db->query($sql);
2567
		if (!$resql) {
2568
			$this->errors[] = $this->db->error();
2569
			$error++;
2570
		}
2571
2572
		if (!$error) {
2573
			$this->ref_client = $ref_client;
2574
		}
2575
2576
		if (!$notrigger && empty($error)) {
2577
			// Call trigger
2578
			$result = $this->call_trigger('BILL_MODIFY', $user);
2579
			if ($result < 0) {
2580
				$error++;
2581
			}
2582
			// End call triggers
2583
		}
2584
2585
		if (!$error) {
2586
			$this->ref_client = $ref_client;
2587
2588
			$this->db->commit();
2589
			return 1;
2590
		} else {
2591
			foreach ($this->errors as $errmsg) {
2592
				dol_syslog(__METHOD__.' Error: '.$errmsg, LOG_ERR);
2593
				$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2594
			}
2595
			$this->db->rollback();
2596
			return -1 * $error;
2597
		}
2598
	}
2599
2600
	/**
2601
	 *	Delete invoice
2602
	 *
2603
	 *	@param     	User	$user      	    User making the deletion.
2604
	 *	@param		int		$notrigger		1=Does not execute triggers, 0= execute triggers
2605
	 *	@param		int		$idwarehouse	Id warehouse to use for stock change.
2606
	 *	@return		int						<0 if KO, 0=Refused, >0 if OK
2607
	 */
2608
	public function delete($user, $notrigger = 0, $idwarehouse = -1)
2609
	{
2610
		global $langs, $conf;
2611
		require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
2612
2613
		$rowid = $this->id;
2614
2615
		dol_syslog(get_class($this)."::delete rowid=".$rowid.", ref=".$this->ref.", thirdparty=".(empty($this->thirdparty) ? '' : $this->thirdparty->name), LOG_DEBUG);
2616
2617
		// Test to avoid invoice deletion (allowed if draft)
2618
		$result = $this->is_erasable();
2619
2620
		if ($result <= 0) {
2621
			return 0;
2622
		}
2623
2624
		$error = 0;
2625
2626
		$this->db->begin();
2627
2628
		if (!$error && !$notrigger) {
2629
			// Call trigger
2630
			$result = $this->call_trigger('BILL_DELETE', $user);
2631
			if ($result < 0) {
2632
				$error++;
2633
			}
2634
			// End call triggers
2635
		}
2636
2637
		// Removed extrafields
2638
		if (!$error) {
2639
			$result = $this->deleteExtraFields();
2640
			if ($result < 0) {
2641
				$error++;
2642
				dol_syslog(get_class($this)."::delete error deleteExtraFields ".$this->error, LOG_ERR);
2643
			}
2644
		}
2645
2646
		if (!$error) {
2647
			// Delete linked object
2648
			$res = $this->deleteObjectLinked();
2649
			if ($res < 0) {
2650
				$error++;
2651
			}
2652
		}
2653
2654
		if (!$error) {
2655
			// If invoice was converted into a discount not yet consumed, we remove discount
2656
			$sql = 'DELETE FROM '.MAIN_DB_PREFIX.'societe_remise_except';
2657
			$sql .= ' WHERE fk_facture_source = '.((int) $rowid);
2658
			$sql .= ' AND fk_facture_line IS NULL';
2659
			$resql = $this->db->query($sql);
2660
2661
			// If invoice has consumed discounts
2662
			$this->fetch_lines();
2663
			$list_rowid_det = array();
2664
			foreach ($this->lines as $key => $invoiceline) {
2665
				$list_rowid_det[] = $invoiceline->id;
2666
			}
2667
2668
			// Consumed discounts are freed
2669
			if (count($list_rowid_det)) {
2670
				$sql = 'UPDATE '.MAIN_DB_PREFIX.'societe_remise_except';
2671
				$sql .= ' SET fk_facture = NULL, fk_facture_line = NULL';
2672
				$sql .= ' WHERE fk_facture_line IN ('.$this->db->sanitize(join(',', $list_rowid_det)).')';
2673
2674
				if (!$this->db->query($sql)) {
2675
					$this->error = $this->db->error()." sql=".$sql;
2676
					$this->errors[] = $this->error;
2677
					$this->db->rollback();
2678
					return -5;
2679
				}
2680
			}
2681
2682
			// Remove other links to the deleted invoice
2683
2684
			$sql = 'UPDATE '.MAIN_DB_PREFIX.'eventorganization_conferenceorboothattendee';
2685
			$sql .= ' SET fk_invoice = NULL';
2686
			$sql .= ' WHERE fk_invoice = '.((int) $rowid);
2687
2688
			if (!$this->db->query($sql)) {
2689
				$this->error = $this->db->error()." sql=".$sql;
2690
				$this->errors[] = $this->error;
2691
				$this->db->rollback();
2692
				return -5;
2693
			}
2694
2695
			$sql = 'UPDATE '.MAIN_DB_PREFIX.'projet_task_time';
2696
			$sql .= ' SET invoice_id = NULL, invoice_line_id = NULL';
2697
			$sql .= ' WHERE invoice_id = '.((int) $rowid);
2698
2699
			if (!$this->db->query($sql)) {
2700
				$this->error = $this->db->error()." sql=".$sql;
2701
				$this->errors[] = $this->error;
2702
				$this->db->rollback();
2703
				return -5;
2704
			}
2705
2706
			// If we decrease stock on invoice validation, we increase back if a warehouse id was provided
2707
			if ($this->type != self::TYPE_DEPOSIT && $result >= 0 && isModEnabled('stock') && !empty($conf->global->STOCK_CALCULATE_ON_BILL) && $idwarehouse != -1) {
2708
				require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
2709
				$langs->load("agenda");
2710
2711
				$num = count($this->lines);
2712
				for ($i = 0; $i < $num; $i++) {
2713
					if ($this->lines[$i]->fk_product > 0) {
2714
						$mouvP = new MouvementStock($this->db);
2715
						$mouvP->origin = &$this;
2716
						$mouvP->setOrigin($this->element, $this->id);
2717
						// We decrease stock for product
2718
						if ($this->type == self::TYPE_CREDIT_NOTE) {
2719
							$result = $mouvP->livraison($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, $this->lines[$i]->subprice, $langs->trans("InvoiceDeleteDolibarr", $this->ref));
2720
						} else {
2721
							$result = $mouvP->reception($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, 0, $langs->trans("InvoiceDeleteDolibarr", $this->ref)); // we use 0 for price, to not change the weighted average value
2722
						}
2723
					}
2724
				}
2725
			}
2726
2727
			// Invoice line extrafileds
2728
			$main = MAIN_DB_PREFIX.'facturedet';
2729
			$ef = $main."_extrafields";
2730
			$sqlef = "DELETE FROM ".$ef." WHERE fk_object IN (SELECT rowid FROM ".$main." WHERE fk_facture = ".((int) $rowid).")";
2731
			// Delete invoice line
2732
			$sql = 'DELETE FROM '.MAIN_DB_PREFIX.'facturedet WHERE fk_facture = '.((int) $rowid);
2733
2734
			if ($this->db->query($sqlef) && $this->db->query($sql) && $this->delete_linked_contact()) {
2735
				$sql = 'DELETE FROM '.MAIN_DB_PREFIX.'facture WHERE rowid = '.((int) $rowid);
2736
2737
				$resql = $this->db->query($sql);
2738
				if ($resql) {
2739
					// Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive
2740
					$this->deleteEcmFiles();
2741
2742
					// On efface le repertoire de pdf provisoire
2743
					$ref = dol_sanitizeFileName($this->ref);
2744
					if ($conf->facture->dir_output && !empty($this->ref)) {
2745
						$dir = $conf->facture->dir_output."/".$ref;
2746
						$file = $conf->facture->dir_output."/".$ref."/".$ref.".pdf";
2747
						if (file_exists($file)) {	// We must delete all files before deleting directory
2748
							$ret = dol_delete_preview($this);
2749
2750
							if (!dol_delete_file($file, 0, 0, 0, $this)) { // For triggers
2751
								$langs->load("errors");
2752
								$this->error = $langs->trans("ErrorFailToDeleteFile", $file);
2753
								$this->errors[] = $this->error;
2754
								$this->db->rollback();
2755
								return 0;
2756
							}
2757
						}
2758
						if (file_exists($dir)) {
2759
							if (!dol_delete_dir_recursive($dir)) { // For remove dir and meta
2760
								$langs->load("errors");
2761
								$this->error = $langs->trans("ErrorFailToDeleteDir", $dir);
2762
								$this->errors[] = $this->error;
2763
								$this->db->rollback();
2764
								return 0;
2765
							}
2766
						}
2767
					}
2768
2769
					$this->db->commit();
2770
					return 1;
2771
				} else {
2772
					$this->error = $this->db->lasterror()." sql=".$sql;
2773
					$this->errors[] = $this->error;
2774
					$this->db->rollback();
2775
					return -6;
2776
				}
2777
			} else {
2778
				$this->error = $this->db->lasterror()." sql=".$sql;
2779
				$this->errors[] = $this->error;
2780
				$this->db->rollback();
2781
				return -4;
2782
			}
2783
		} else {
2784
			$this->db->rollback();
2785
			return -2;
2786
		}
2787
	}
2788
2789
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2790
	/**
2791
	 *  Tag the invoice as paid completely (if close_code is filled) => this->fk_statut=2, this->paye=1
2792
	 *  or partialy (if close_code filled) + appel trigger BILL_PAYED => this->fk_statut=2, this->paye stay 0
2793
	 *
2794
	 *	@deprecated
2795
	 *  @see setPaid()
2796
	 *  @param	User	$user      	Object user that modify
2797
	 *	@param  string	$close_code	Code renseigne si on classe a payee completement alors que paiement incomplet (cas escompte par exemple)
2798
	 *	@param  string	$close_note	Commentaire renseigne si on classe a payee alors que paiement incomplet (cas escompte par exemple)
2799
	 *  @return int         		<0 if KO, >0 if OK
2800
	 */
2801
	public function set_paid($user, $close_code = '', $close_note = '')
2802
	{
2803
		// phpcs:enable
2804
		dol_syslog(get_class($this)."::set_paid is deprecated, use setPaid instead", LOG_NOTICE);
2805
		return $this->setPaid($user, $close_code, $close_note);
2806
	}
2807
2808
	/**
2809
	 *  Tag the invoice as paid completely (if close_code is filled) => this->fk_statut=2, this->paye=1
2810
	 *  or partially (if close_code filled) + appel trigger BILL_PAYED => this->fk_statut=2, this->paye stay 0
2811
	 *
2812
	 *  @param	User	$user      	Object user that modify
2813
	 *	@param  string	$close_code	Code renseigne si on classe a payee completement alors que paiement incomplet (cas escompte par exemple)
2814
	 *	@param  string	$close_note	Commentaire renseigne si on classe a payee alors que paiement incomplet (cas escompte par exemple)
2815
	 *  @return int         		<0 if KO, >0 if OK
2816
	 */
2817
	public function setPaid($user, $close_code = '', $close_note = '')
2818
	{
2819
		$error = 0;
2820
2821
		if ($this->paye != 1) {
2822
			$this->db->begin();
2823
2824
			$now = dol_now();
2825
2826
			dol_syslog(get_class($this)."::setPaid rowid=".((int) $this->id), LOG_DEBUG);
2827
2828
			$sql = 'UPDATE '.MAIN_DB_PREFIX.'facture SET';
2829
			$sql .= ' fk_statut='.self::STATUS_CLOSED;
2830
			if (!$close_code) {
2831
				$sql .= ', paye=1';
2832
			}
2833
			if ($close_code) {
2834
				$sql .= ", close_code='".$this->db->escape($close_code)."'";
2835
			}
2836
			if ($close_note) {
2837
				$sql .= ", close_note='".$this->db->escape($close_note)."'";
2838
			}
2839
			$sql .= ', fk_user_closing = '.((int) $user->id);
2840
			$sql .= ", date_closing = '".$this->db->idate($now)."'";
2841
			$sql .= " WHERE rowid = ".((int) $this->id);
2842
2843
			$resql = $this->db->query($sql);
2844
			if ($resql) {
2845
				// Call trigger
2846
				$result = $this->call_trigger('BILL_PAYED', $user);
2847
				if ($result < 0) {
2848
					$error++;
2849
				}
2850
				// End call triggers
2851
			} else {
2852
				$error++;
2853
				$this->error = $this->db->lasterror();
2854
			}
2855
2856
			if (!$error) {
2857
				$this->db->commit();
2858
				return 1;
2859
			} else {
2860
				$this->db->rollback();
2861
				return -1;
2862
			}
2863
		} else {
2864
			return 0;
2865
		}
2866
	}
2867
2868
2869
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2870
	/**
2871
	 *  Tag la facture comme non payee completement + appel trigger BILL_UNPAYED
2872
	 *	Fonction utilisee quand un paiement prelevement est refuse,
2873
	 * 	ou quand une facture annulee et reouverte.
2874
	 *
2875
	 *	@deprecated
2876
	 *  @see setUnpaid()
2877
	 *  @param	User	$user       Object user that change status
2878
	 *  @return int         		<0 if KO, >0 if OK
2879
	 */
2880
	public function set_unpaid($user)
2881
	{
2882
		// phpcs:enable
2883
		dol_syslog(get_class($this)."::set_unpaid is deprecated, use setUnpaid instead", LOG_NOTICE);
2884
		return $this->setUnpaid($user);
2885
	}
2886
2887
	/**
2888
	 *  Tag la facture comme non payee completement + appel trigger BILL_UNPAYED
2889
	 *	Fonction utilisee quand un paiement prelevement est refuse,
2890
	 * 	ou quand une facture annulee et reouverte.
2891
	 *
2892
	 *  @param	User	$user       Object user that change status
2893
	 *  @return int         		<0 if KO, >0 if OK
2894
	 */
2895
	public function setUnpaid($user)
2896
	{
2897
		$error = 0;
2898
2899
		$this->db->begin();
2900
2901
		$sql = 'UPDATE '.MAIN_DB_PREFIX.'facture';
2902
		$sql .= ' SET paye=0, fk_statut='.self::STATUS_VALIDATED.', close_code=null, close_note=null,';
2903
		$sql .= ' date_closing=null,';
2904
		$sql .= ' fk_user_closing=null';
2905
		$sql .= " WHERE rowid = ".((int) $this->id);
2906
2907
		dol_syslog(get_class($this)."::setUnpaid", LOG_DEBUG);
2908
		$resql = $this->db->query($sql);
2909
		if ($resql) {
2910
			// Call trigger
2911
			$result = $this->call_trigger('BILL_UNPAYED', $user);
2912
			if ($result < 0) {
2913
				$error++;
2914
			}
2915
			// End call triggers
2916
		} else {
2917
			$error++;
2918
			$this->error = $this->db->error();
2919
			dol_print_error($this->db);
2920
		}
2921
2922
		if (!$error) {
2923
			$this->db->commit();
2924
			return 1;
2925
		} else {
2926
			$this->db->rollback();
2927
			return -1;
2928
		}
2929
	}
2930
2931
2932
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2933
	/**
2934
	 *	Tag invoice as canceled, with no payment on it (example for replacement invoice or payment never received) + call trigger BILL_CANCEL
2935
	 *	Warning, if option to decrease stock on invoice was set, this function does not change stock (it might be a cancel because
2936
	 *  of no payment even if merchandises were sent).
2937
	 *
2938
	 *	@deprecated
2939
	 *  @see setCanceled()
2940
	 *	@param	User	$user        	Object user making change
2941
	 *	@param	string	$close_code		Code of closing invoice (CLOSECODE_REPLACED, CLOSECODE_...)
2942
	 *	@param	string	$close_note		Comment
2943
	 *	@return int         			<0 if KO, >0 if OK
2944
	 */
2945
	public function set_canceled($user, $close_code = '', $close_note = '')
2946
	{
2947
		// phpcs:enable
2948
		dol_syslog(get_class($this)."::set_canceled is deprecated, use setCanceled instead", LOG_NOTICE);
2949
		return $this->setCanceled($user, $close_code, $close_note);
2950
	}
2951
2952
	/**
2953
	 *	Tag invoice as canceled, with no payment on it (example for replacement invoice or payment never received) + call trigger BILL_CANCEL
2954
	 *	Warning, if option to decrease stock on invoice was set, this function does not change stock (it might be a cancel because
2955
	 *  of no payment even if merchandises were sent).
2956
	 *
2957
	 *	@param	User	$user        	Object user making change
2958
	 *	@param	string	$close_code		Code of closing invoice (CLOSECODE_REPLACED, CLOSECODE_...)
2959
	 *	@param	string	$close_note		Comment
2960
	 *	@return int         			<0 if KO, >0 if OK
2961
	 */
2962
	public function setCanceled($user, $close_code = '', $close_note = '')
2963
	{
2964
		dol_syslog(get_class($this)."::setCanceled rowid=".((int) $this->id), LOG_DEBUG);
2965
2966
		$this->db->begin();
2967
2968
		$sql = 'UPDATE '.MAIN_DB_PREFIX.'facture SET';
2969
		$sql .= ' fk_statut='.self::STATUS_ABANDONED;
2970
		if ($close_code) {
2971
			$sql .= ", close_code='".$this->db->escape($close_code)."'";
2972
		}
2973
		if ($close_note) {
2974
			$sql .= ", close_note='".$this->db->escape($close_note)."'";
2975
		}
2976
		$sql .= " WHERE rowid = ".((int) $this->id);
2977
2978
		$resql = $this->db->query($sql);
2979
		if ($resql) {
2980
			// Bound discounts are deducted from the invoice
2981
			// as they have not been used since the invoice is abandoned.
2982
			$sql = 'UPDATE '.MAIN_DB_PREFIX.'societe_remise_except';
2983
			$sql .= ' SET fk_facture = NULL';
2984
			$sql .= ' WHERE fk_facture = '.((int) $this->id);
2985
2986
			$resql = $this->db->query($sql);
2987
			if ($resql) {
2988
				// Call trigger
2989
				$result = $this->call_trigger('BILL_CANCEL', $user);
2990
				if ($result < 0) {
2991
					$this->db->rollback();
2992
					return -1;
2993
				}
2994
				// End call triggers
2995
2996
				$this->db->commit();
2997
				return 1;
2998
			} else {
2999
				$this->error = $this->db->error()." sql=".$sql;
3000
				$this->db->rollback();
3001
				return -1;
3002
			}
3003
		} else {
3004
			$this->error = $this->db->error()." sql=".$sql;
3005
			$this->db->rollback();
3006
			return -2;
3007
		}
3008
	}
3009
3010
	/**
3011
	 * Tag invoice as validated + call trigger BILL_VALIDATE
3012
	 * Object must have lines loaded with fetch_lines
3013
	 *
3014
	 * @param	User	$user           Object user that validate
3015
	 * @param   string	$force_number	Reference to force on invoice
3016
	 * @param	int		$idwarehouse	Id of warehouse to use for stock decrease if option to decreasenon stock is on (0=no decrease)
3017
	 * @param	int		$notrigger		1=Does not execute triggers, 0= execute triggers
3018
	 * @param	int		$batch_rule		0=do not decrement batch, else batch rule to use, 1=take in batches ordered by sellby and eatby dates
3019
	 * @return	int						<0 if KO, 0=Nothing done because invoice is not a draft, >0 if OK
3020
	 */
3021
	public function validate($user, $force_number = '', $idwarehouse = 0, $notrigger = 0, $batch_rule = 0)
3022
	{
3023
		global $conf, $langs, $mysoc;
3024
		require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
3025
3026
		$productStatic = null;
3027
		$warehouseStatic = null;
3028
		if ($batch_rule > 0) {
3029
			require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
3030
			require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
3031
			require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
3032
			$productStatic = new Product($this->db);
3033
			$warehouseStatic = new Entrepot($this->db);
3034
			$productbatch = new ProductBatch($this->db);
3035
		}
3036
3037
		$now = dol_now();
3038
3039
		$error = 0;
3040
		dol_syslog(get_class($this).'::validate user='.$user->id.', force_number='.$force_number.', idwarehouse='.$idwarehouse);
3041
3042
		// Force to have object complete for checks
3043
		$this->fetch_thirdparty();
3044
		$this->fetch_lines();
3045
3046
		// Check parameters
3047
		if ($this->statut != self::STATUS_DRAFT) {
3048
			dol_syslog(get_class($this)."::validate status is not draft. operation canceled.", LOG_WARNING);
3049
			return 0;
3050
		}
3051
		if (count($this->lines) <= 0) {
3052
			$langs->load("errors");
3053
			$this->error = $langs->trans("ErrorObjectMustHaveLinesToBeValidated", $this->ref);
3054
			return -1;
3055
		}
3056
		if ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->facture->creer))
3057
		|| (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->facture->invoice_advance->validate))) {
3058
			$this->error = 'Permission denied';
3059
			dol_syslog(get_class($this)."::validate ".$this->error.' MAIN_USE_ADVANCED_PERMS='.$conf->global->MAIN_USE_ADVANCED_PERMS, LOG_ERR);
3060
			return -1;
3061
		}
3062
		if (!empty($conf->global-> INVOICE_CHECK_POSTERIOR_DATE)) {
3063
			$last_of_type = $this->willBeLastOfSameType(true);
3064
			if (!$last_of_type[0]) {
3065
				$this->error = $langs->transnoentities("ErrorInvoiceIsNotLastOfSameType", $this->ref, dol_print_date($this->date, 'day'), dol_print_date($last_of_type[1], 'day'));
3066
				return -1;
3067
			}
3068
		}
3069
3070
		// Check for mandatory fields in thirdparty (defined into setup)
3071
		if (!empty($this->thirdparty) && is_object($this->thirdparty)) {
3072
			$array_to_check = array('IDPROF1', 'IDPROF2', 'IDPROF3', 'IDPROF4', 'IDPROF5', 'IDPROF6', 'EMAIL', 'ACCOUNTANCY_CODE_CUSTOMER');
3073
			foreach ($array_to_check as $key) {
3074
				$keymin = strtolower($key);
3075
				if (!property_exists($this->thirdparty, $keymin)) {
3076
					continue;
3077
				}
3078
				$vallabel = $this->thirdparty->$keymin;
3079
3080
				$i = (int) preg_replace('/[^0-9]/', '', $key);
3081
				if ($i > 0) {
3082
					if ($this->thirdparty->isACompany()) {
3083
						// Check for mandatory prof id (but only if country is other than ours)
3084
						if ($mysoc->country_id > 0 && $this->thirdparty->country_id == $mysoc->country_id) {
3085
							$idprof_mandatory = 'SOCIETE_'.$key.'_INVOICE_MANDATORY';
3086
							if (!$vallabel && !empty($conf->global->$idprof_mandatory)) {
3087
								$langs->load("errors");
3088
								$this->error = $langs->trans('ErrorProdIdIsMandatory', $langs->transcountry('ProfId'.$i, $this->thirdparty->country_code)).' ('.$langs->trans("ForbiddenBySetupRules").') ['.$langs->trans('Company').' : '.$this->thirdparty->name.']';
3089
								dol_syslog(__METHOD__.' '.$this->error, LOG_ERR);
3090
								return -1;
3091
							}
3092
						}
3093
					}
3094
				} else {
3095
					if ($key == 'EMAIL') {
3096
						// Check for mandatory
3097
						if (!empty($conf->global->SOCIETE_EMAIL_INVOICE_MANDATORY) && !isValidEMail($this->thirdparty->email)) {
3098
							$langs->load("errors");
3099
							$this->error = $langs->trans("ErrorBadEMail", $this->thirdparty->email).' ('.$langs->trans("ForbiddenBySetupRules").') ['.$langs->trans('Company').' : '.$this->thirdparty->name.']';
3100
							dol_syslog(__METHOD__.' '.$this->error, LOG_ERR);
3101
							return -1;
3102
						}
3103
					}
3104
					if ($key == 'ACCOUNTANCY_CODE_CUSTOMER') {
3105
						// Check for mandatory
3106
						if (!empty($conf->global->SOCIETE_ACCOUNTANCY_CODE_CUSTOMER_INVOICE_MANDATORY) && empty($this->thirdparty->code_compta)) {
3107
							$langs->load("errors");
3108
							$this->error = $langs->trans("ErrorAccountancyCodeCustomerIsMandatory", $this->thirdparty->name).' ('.$langs->trans("ForbiddenBySetupRules").')';
3109
							dol_syslog(__METHOD__.' '.$this->error, LOG_ERR);
3110
							return -1;
3111
						}
3112
					}
3113
				}
3114
			}
3115
		}
3116
3117
		// Check for mandatory fields in $this
3118
		$array_to_check = array('REF_CLIENT'=>'RefCustomer');
3119
		foreach ($array_to_check as $key => $val) {
3120
			$keymin = strtolower($key);
3121
			$vallabel = $this->$keymin;
3122
3123
			// Check for mandatory
3124
			$keymandatory = 'INVOICE_'.$key.'_MANDATORY_FOR_VALIDATION';
3125
			if (!$vallabel && !empty($conf->global->$keymandatory)) {
3126
				$langs->load("errors");
3127
				$error++;
3128
				setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv($val)), null, 'errors');
3129
			}
3130
		}
3131
3132
		$this->db->begin();
3133
3134
		// Check parameters
3135
		if ($this->type == self::TYPE_REPLACEMENT) {		// if this is a replacement invoice
3136
			// Check that source invoice is known
3137
			if ($this->fk_facture_source <= 0) {
3138
				$this->error = $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("InvoiceReplacement"));
3139
				$this->db->rollback();
3140
				return -10;
3141
			}
3142
3143
			// Load source invoice that has been replaced
3144
			$facreplaced = new Facture($this->db);
3145
			$result = $facreplaced->fetch($this->fk_facture_source);
3146
			if ($result <= 0) {
3147
				$this->error = $langs->trans("ErrorBadInvoice");
3148
				$this->db->rollback();
3149
				return -11;
3150
			}
3151
3152
			// Check that source invoice not already replaced by another one.
3153
			$idreplacement = $facreplaced->getIdReplacingInvoice('validated');
3154
			if ($idreplacement && $idreplacement != $this->id) {
3155
				$facreplacement = new Facture($this->db);
3156
				$facreplacement->fetch($idreplacement);
3157
				$this->error = $langs->trans("ErrorInvoiceAlreadyReplaced", $facreplaced->ref, $facreplacement->ref);
3158
				$this->db->rollback();
3159
				return -12;
3160
			}
3161
3162
			$result = $facreplaced->setCanceled($user, self::CLOSECODE_REPLACED, '');
3163
			if ($result < 0) {
3164
				$this->error = $facreplaced->error;
3165
				$this->db->rollback();
3166
				return -13;
3167
			}
3168
		}
3169
3170
		// Define new ref
3171
		if ($force_number) {
3172
			$num = $force_number;
3173
		} elseif (preg_match('/^[\(]?PROV/i', $this->ref) || empty($this->ref)) { // empty should not happened, but when it occurs, the test save life
3174
			if (!empty($conf->global->FAC_FORCE_DATE_VALIDATION)) {	// If option enabled, we force invoice date
3175
				$this->date = dol_now();
3176
				$this->date_lim_reglement = $this->calculate_date_lim_reglement();
3177
			}
3178
			$num = $this->getNextNumRef($this->thirdparty);
3179
		} else {
3180
			$num = $this->ref;
3181
		}
3182
3183
		$this->newref = dol_sanitizeFileName($num);
3184
3185
		if ($num) {
3186
			$this->update_price(1);
3187
3188
			// Validate
3189
			$sql = 'UPDATE '.MAIN_DB_PREFIX.'facture';
3190
			$sql .= " SET ref = '".$this->db->escape($num)."', fk_statut = ".self::STATUS_VALIDATED.", fk_user_valid = ".($user->id > 0 ? $user->id : "null").", date_valid = '".$this->db->idate($now)."'";
3191
			if (!empty($conf->global->FAC_FORCE_DATE_VALIDATION)) {	// If option enabled, we force invoice date
3192
				$sql .= ", datef='".$this->db->idate($this->date)."'";
3193
				$sql .= ", date_lim_reglement='".$this->db->idate($this->date_lim_reglement)."'";
3194
			}
3195
			$sql .= " WHERE rowid = ".((int) $this->id);
3196
3197
			dol_syslog(get_class($this)."::validate", LOG_DEBUG);
3198
			$resql = $this->db->query($sql);
3199
			if (!$resql) {
3200
				dol_print_error($this->db);
3201
				$error++;
3202
			}
3203
3204
			// We check if the invoice was provisional
3205
			if (!$error && (preg_match('/^[\(]?PROV/i', $this->ref))) {
3206
				// La verif qu'une remise n'est pas utilisee 2 fois est faite au moment de l'insertion de ligne
3207
			}
3208
3209
			if (!$error) {
3210
				// Define third party as a customer
3211
				$result = $this->thirdparty->set_as_client();
3212
3213
				// If active we decrement the main product and its components at invoice validation
3214
				if ($this->type != self::TYPE_DEPOSIT && $result >= 0 && isModEnabled('stock') && !empty($conf->global->STOCK_CALCULATE_ON_BILL) && $idwarehouse > 0) {
3215
					require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
3216
					$langs->load("agenda");
3217
3218
					// Loop on each line
3219
					$cpt = count($this->lines);
3220
					for ($i = 0; $i < $cpt; $i++) {
3221
						if ($this->lines[$i]->fk_product > 0) {
3222
							$mouvP = new MouvementStock($this->db);
3223
							$mouvP->origin = &$this;
3224
							$mouvP->setOrigin($this->element, $this->id);
3225
							// We decrease stock for product
3226
							if ($this->type == self::TYPE_CREDIT_NOTE) {
3227
								$result = $mouvP->reception($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, 0, $langs->trans("InvoiceValidatedInDolibarr", $num));
3228
								if ($result < 0) {
3229
									$error++;
3230
									$this->error = $mouvP->error;
3231
								}
3232
							} else {
3233
								$is_batch_line = false;
3234
								if ($batch_rule > 0) {
3235
									$productStatic->fetch($this->lines[$i]->fk_product);
3236
									if ($productStatic->hasbatch()) {
3237
										$is_batch_line = true;
3238
										$product_qty_remain = $this->lines[$i]->qty;
3239
3240
										$sortfield = null;
3241
										$sortorder = null;
3242
										// find all batch order by sellby (DLC) and eatby dates (DLUO) first
3243
										if ($batch_rule == Productbatch::BATCH_RULE_SELLBY_EATBY_DATES_FIRST) {
3244
											$sortfield = 'pl.sellby,pl.eatby,pb.qty,pl.rowid';
3245
											$sortorder = 'ASC,ASC,ASC,ASC';
3246
										}
3247
3248
										$resBatchList = $productbatch->findAllForProduct($productStatic->id, $idwarehouse, (!empty($conf->global->STOCK_ALLOW_NEGATIVE_TRANSFER) ? null : 0), $sortfield, $sortorder);
3249
										if (!is_array($resBatchList)) {
3250
											$error++;
3251
											$this->error = $this->db->lasterror();
3252
										}
3253
3254
										if (!$error) {
3255
											$batchList = $resBatchList;
3256
											if (empty($batchList)) {
3257
												$error++;
3258
												$langs->load('errors');
3259
												$warehouseStatic->fetch($idwarehouse);
3260
												$this->error = $langs->trans('ErrorBatchNoFoundForProductInWarehouse', $productStatic->label, $warehouseStatic->ref);
3261
												dol_syslog(__METHOD__.' Error: '.$langs->transnoentitiesnoconv('ErrorBatchNoFoundForProductInWarehouse', $productStatic->label, $warehouseStatic->ref), LOG_ERR);
3262
											}
3263
3264
											foreach ($batchList as $batch) {
3265
												if ($batch->qty <= 0) {
3266
													continue; // try to decrement only batches have positive quantity first
3267
												}
3268
3269
												// enough quantity in this batch
3270
												if ($batch->qty >= $product_qty_remain) {
3271
													$product_batch_qty = $product_qty_remain;
3272
												} else {
3273
													// not enough (take all in batch)
3274
													$product_batch_qty = $batch->qty;
3275
												}
3276
												$result = $mouvP->livraison($user, $productStatic->id, $idwarehouse, $product_batch_qty, $this->lines[$i]->subprice, $langs->trans('InvoiceValidatedInDolibarr', $num), '', '', '', $batch->batch);
3277
												if ($result < 0) {
3278
													$error++;
3279
													$this->error = $mouvP->error;
3280
													break;
3281
												}
3282
3283
												$product_qty_remain -= $product_batch_qty;
3284
												// all product quantity was decremented
3285
												if ($product_qty_remain <= 0) {
3286
													break;
3287
												}
3288
											}
3289
3290
											if (!$error && $product_qty_remain > 0) {
3291
												if ($conf->global->STOCK_ALLOW_NEGATIVE_TRANSFER) {
3292
													// take in the first batch
3293
													$batch = $batchList[0];
3294
													$result = $mouvP->livraison($user, $productStatic->id, $idwarehouse, $product_qty_remain, $this->lines[$i]->subprice, $langs->trans('InvoiceValidatedInDolibarr', $num), '', '', '', $batch->batch);
3295
													if ($result < 0) {
3296
														$error++;
3297
														$this->error = $mouvP->error;
3298
													}
3299
												} else {
3300
													$error++;
3301
													$langs->load('errors');
3302
													$warehouseStatic->fetch($idwarehouse);
3303
													$this->error = $langs->trans('ErrorBatchNoFoundEnoughQuantityForProductInWarehouse', $productStatic->label, $warehouseStatic->ref);
3304
													dol_syslog(__METHOD__.' Error: '.$langs->transnoentitiesnoconv('ErrorBatchNoFoundEnoughQuantityForProductInWarehouse', $productStatic->label, $warehouseStatic->ref), LOG_ERR);
3305
												}
3306
											}
3307
										}
3308
									}
3309
								}
3310
3311
								if (!$is_batch_line) {
3312
									$result = $mouvP->livraison($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, $this->lines[$i]->subprice, $langs->trans("InvoiceValidatedInDolibarr", $num));
3313
									if ($result < 0) {
3314
										$error++;
3315
										$this->error = $mouvP->error;
3316
									}
3317
								}
3318
							}
3319
						}
3320
					}
3321
				}
3322
			}
3323
3324
			/*
3325
			 * Set situation_final to 0 if is a credit note and the invoice source is a invoice situation (case when invoice situation is at 100%)
3326
			 * So we can continue to create new invoice situation
3327
			 */
3328
			if (!$error && $this->type == self::TYPE_CREDIT_NOTE && $this->fk_facture_source > 0) {
3329
				$invoice_situation = new Facture($this->db);
3330
				$result = $invoice_situation->fetch($this->fk_facture_source);
3331
				if ($result > 0 && $invoice_situation->type == self::TYPE_SITUATION && $invoice_situation->situation_final == 1) {
3332
					$invoice_situation->situation_final = 0;
3333
					// Disable triggers because module can force situation_final to 1 by triggers (ex: SubTotal)
3334
					$result = $invoice_situation->setFinal($user, 1);
3335
				}
3336
				if ($result < 0) {
3337
					$this->error = $invoice_situation->error;
3338
					$this->errors = $invoice_situation->errors;
3339
					$error++;
3340
				}
3341
			}
3342
3343
			// Trigger calls
3344
			if (!$error && !$notrigger) {
3345
				// Call trigger
3346
				$result = $this->call_trigger('BILL_VALIDATE', $user);
3347
				if ($result < 0) {
3348
					$error++;
3349
				}
3350
				// End call triggers
3351
			}
3352
3353
			if (!$error) {
3354
				$this->oldref = $this->ref;
3355
3356
				// Rename directory if dir was a temporary ref
3357
				if (preg_match('/^[\(]?PROV/i', $this->ref)) {
3358
					// Now we rename also files into index
3359
					$sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filename = CONCAT('".$this->db->escape($this->newref)."', SUBSTR(filename, ".(strlen($this->ref) + 1).")), filepath = 'facture/".$this->db->escape($this->newref)."'";
3360
					$sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'facture/".$this->db->escape($this->ref)."' and entity = ".$conf->entity;
3361
					$resql = $this->db->query($sql);
3362
					if (!$resql) {
3363
						$error++;
3364
						$this->error = $this->db->lasterror();
3365
					}
3366
3367
					// We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
3368
					$oldref = dol_sanitizeFileName($this->ref);
3369
					$newref = dol_sanitizeFileName($num);
3370
					$dirsource = $conf->facture->dir_output.'/'.$oldref;
3371
					$dirdest = $conf->facture->dir_output.'/'.$newref;
3372
					if (!$error && file_exists($dirsource)) {
3373
						dol_syslog(get_class($this)."::validate rename dir ".$dirsource." into ".$dirdest);
3374
3375
						if (@rename($dirsource, $dirdest)) {
3376
							dol_syslog("Rename ok");
3377
							// Rename docs starting with $oldref with $newref
3378
							$listoffiles = dol_dir_list($conf->facture->dir_output.'/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
3379
							foreach ($listoffiles as $fileentry) {
3380
								$dirsource = $fileentry['name'];
3381
								$dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource);
3382
								$dirsource = $fileentry['path'].'/'.$dirsource;
3383
								$dirdest = $fileentry['path'].'/'.$dirdest;
3384
								@rename($dirsource, $dirdest);
3385
							}
3386
						}
3387
					}
3388
				}
3389
			}
3390
3391
			if (!$error && !$this->is_last_in_cycle()) {
3392
				if (!$this->updatePriceNextInvoice($langs)) {
3393
					$error++;
3394
				}
3395
			}
3396
3397
			// Set new ref and define current status
3398
			if (!$error) {
3399
				$this->ref = $num;
3400
				$this->ref = $num;
3401
				$this->statut = self::STATUS_VALIDATED;
3402
				$this->status = self::STATUS_VALIDATED;
3403
				$this->brouillon = 0;
3404
				$this->date_validation = $now;
3405
				$i = 0;
3406
3407
				if (!empty($conf->global->INVOICE_USE_SITUATION)) {
3408
					$final = true;
3409
					$nboflines = count($this->lines);
3410
					while (($i < $nboflines) && $final) {
3411
						$final = ($this->lines[$i]->situation_percent == 100);
3412
						$i++;
3413
					}
3414
3415
					if (empty($final)) {
3416
						$this->situation_final = 0;
3417
					} else {
3418
						$this->situation_final = 1;
3419
					}
3420
3421
					$this->setFinal($user);
3422
				}
3423
			}
3424
		} else {
3425
			$error++;
3426
		}
3427
3428
		if (!$error) {
3429
			$this->db->commit();
3430
			return 1;
3431
		} else {
3432
			$this->db->rollback();
3433
			return -1;
3434
		}
3435
	}
3436
3437
	/**
3438
	 * Update price of next invoice
3439
	 *
3440
	 * @param	Translate	$langs	Translate object
3441
	 * @return 	bool				false if KO, true if OK
3442
	 */
3443
	public function updatePriceNextInvoice(&$langs)
3444
	{
3445
		foreach ($this->tab_next_situation_invoice as $next_invoice) {
3446
			$is_last = $next_invoice->is_last_in_cycle();
3447
3448
			if ($next_invoice->statut == self::STATUS_DRAFT && $is_last != 1) {
3449
				$this->error = $langs->trans('updatePriceNextInvoiceErrorUpdateline', $next_invoice->ref);
3450
				return false;
3451
			}
3452
3453
			$next_invoice->brouillon = 1;
3454
3455
			foreach ($next_invoice->lines as $line) {
3456
				$result = $next_invoice->updateline(
3457
					$line->id,
3458
					$line->desc,
3459
					$line->subprice,
3460
					$line->qty,
3461
					$line->remise_percent,
3462
					$line->date_start,
3463
					$line->date_end,
3464
					$line->tva_tx,
3465
					$line->localtax1_tx,
3466
					$line->localtax2_tx,
3467
					'HT',
3468
					$line->info_bits,
3469
					$line->product_type,
3470
					$line->fk_parent_line,
3471
					0,
3472
					$line->fk_fournprice,
3473
					$line->pa_ht,
3474
					$line->label,
3475
					$line->special_code,
3476
					$line->array_options,
3477
					$line->situation_percent,
3478
					$line->fk_unit
3479
				);
3480
3481
				if ($result < 0) {
3482
					$this->error = $langs->trans('updatePriceNextInvoiceErrorUpdateline', $next_invoice->ref);
3483
					return false;
3484
				}
3485
			}
3486
3487
			break; // Only the next invoice and not each next invoice
3488
		}
3489
3490
		return true;
3491
	}
3492
3493
	/**
3494
	 *	Set draft status
3495
	 *
3496
	 *	@param	User	$user			Object user that modify
3497
	 *	@param	int		$idwarehouse	Id warehouse to use for stock change.
3498
	 *	@return	int						<0 if KO, >0 if OK
3499
	 */
3500
	public function setDraft($user, $idwarehouse = -1)
3501
	{
3502
		// phpcs:enable
3503
		global $conf, $langs;
3504
3505
		$error = 0;
3506
3507
		if ($this->statut == self::STATUS_DRAFT) {
3508
			dol_syslog(__METHOD__." already draft status", LOG_WARNING);
3509
			return 0;
3510
		}
3511
3512
		dol_syslog(__METHOD__, LOG_DEBUG);
3513
3514
		$this->db->begin();
3515
3516
		$sql = "UPDATE ".MAIN_DB_PREFIX."facture";
3517
		$sql .= " SET fk_statut = ".self::STATUS_DRAFT;
3518
		$sql .= " WHERE rowid = ".((int) $this->id);
3519
3520
		$result = $this->db->query($sql);
3521
		if ($result) {
3522
			if (!$error) {
3523
				$this->oldcopy = clone $this;
3524
			}
3525
3526
			// If we decrease stock on invoice validation, we increase back
3527
			if ($this->type != self::TYPE_DEPOSIT && $result >= 0 && isModEnabled('stock') && !empty($conf->global->STOCK_CALCULATE_ON_BILL)) {
3528
				require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
3529
				$langs->load("agenda");
3530
3531
				$num = count($this->lines);
3532
				for ($i = 0; $i < $num; $i++) {
3533
					if ($this->lines[$i]->fk_product > 0) {
3534
						$mouvP = new MouvementStock($this->db);
3535
						$mouvP->origin = &$this;
3536
						$mouvP->setOrigin($this->element, $this->id);
3537
						// We decrease stock for product
3538
						if ($this->type == self::TYPE_CREDIT_NOTE) {
3539
							$result = $mouvP->livraison($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, $this->lines[$i]->subprice, $langs->trans("InvoiceBackToDraftInDolibarr", $this->ref));
3540
						} else {
3541
							$result = $mouvP->reception($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, 0, $langs->trans("InvoiceBackToDraftInDolibarr", $this->ref)); // we use 0 for price, to not change the weighted average value
3542
						}
3543
					}
3544
				}
3545
			}
3546
3547
			if ($error == 0) {
3548
				$old_statut = $this->statut;
3549
				$this->brouillon = 1;
3550
				$this->statut = self::STATUS_DRAFT;
3551
				$this->status = self::STATUS_DRAFT;
3552
3553
				// Call trigger
3554
				$result = $this->call_trigger('BILL_UNVALIDATE', $user);
3555
				if ($result < 0) {
3556
					$error++;
3557
					$this->statut = $old_statut;
3558
					$this->status = $old_statut;
3559
					$this->brouillon = 0;
3560
				}
3561
				// End call triggers
3562
			} else {
3563
				$this->db->rollback();
3564
				return -1;
3565
			}
3566
3567
			if ($error == 0) {
3568
				$this->db->commit();
3569
				return 1;
3570
			} else {
3571
				$this->db->rollback();
3572
				return -1;
3573
			}
3574
		} else {
3575
			$this->error = $this->db->error();
3576
			$this->db->rollback();
3577
			return -1;
3578
		}
3579
	}
3580
3581
3582
	/**
3583
	 *  Add an invoice line into database (linked to product/service or not).
3584
	 *  Les parametres sont deja cense etre juste et avec valeurs finales a l'appel
3585
	 *  de cette methode. Aussi, pour le taux tva, il doit deja avoir ete defini
3586
	 *  par l'appelant par la methode get_default_tva(societe_vendeuse,societe_acheteuse,produit)
3587
	 *  et le desc doit deja avoir la bonne valeur (a l'appelant de gerer le multilangue)
3588
	 *
3589
	 *  @param    	string		$desc            	Description of line
3590
	 *  @param    	double		$pu_ht              Unit price without tax (> 0 even for credit note)
3591
	 *  @param    	double		$qty             	Quantity
3592
	 *  @param    	double		$txtva           	Force Vat rate, -1 for auto (Can contain the vat_src_code too with syntax '9.9 (CODE)')
3593
	 *  @param		double		$txlocaltax1		Local tax 1 rate (deprecated, use instead txtva with code inside)
3594
	 *  @param		double		$txlocaltax2		Local tax 2 rate (deprecated, use instead txtva with code inside)
3595
	 *  @param    	int			$fk_product      	Id of predefined product/service
3596
	 *  @param    	double		$remise_percent  	Percent of discount on line
3597
	 *  @param    	int			$date_start      	Date start of service
3598
	 *  @param    	int			$date_end        	Date end of service
3599
	 *  @param    	int			$ventil          	Code of dispatching into accountancy
3600
	 *  @param    	int			$info_bits			Bits of type of lines
3601
	 *  @param    	int			$fk_remise_except	Id discount used
3602
	 *  @param		string		$price_base_type	'HT' or 'TTC'
3603
	 *  @param    	double		$pu_ttc             Unit price with tax (> 0 even for credit note)
3604
	 *  @param		int			$type				Type of line (0=product, 1=service). Not used if fk_product is defined, the type of product is used.
3605
	 *  @param      int			$rang               Position of line (-1 means last value + 1)
3606
	 *  @param		int			$special_code		Special code (also used by externals modules!)
3607
	 *  @param		string		$origin				Depend on global conf MAIN_CREATEFROM_KEEP_LINE_ORIGIN_INFORMATION can be 'orderdet', 'propaldet'..., else 'order','propal,'....
3608
	 *  @param		int			$origin_id			Depend on global conf MAIN_CREATEFROM_KEEP_LINE_ORIGIN_INFORMATION can be Id of origin object (aka line id), else object id
3609
	 *  @param		int			$fk_parent_line		Id of parent line
3610
	 *  @param		int			$fk_fournprice		Supplier price id (to calculate margin) or ''
3611
	 *  @param		int			$pa_ht				Buying price of line (to calculate margin) or ''
3612
	 *  @param		string		$label				Label of the line (deprecated, do not use)
3613
	 *  @param		array		$array_options		extrafields array
3614
	 *  @param      int         $situation_percent  Situation advance percentage
3615
	 *  @param      int         $fk_prev_id         Previous situation line id reference
3616
	 *  @param 		string		$fk_unit 			Code of the unit to use. Null to use the default one
3617
	 *  @param		double		$pu_ht_devise		Unit price in foreign currency
3618
	 *  @param		string		$ref_ext		    External reference of the line
3619
	 *  @param		int			$noupdateafterinsertline	No update after insert of line
3620
	 *  @return    	int             				<0 if KO, Id of line if OK
3621
	 */
3622
	public function addline(
3623
		$desc,
3624
		$pu_ht,
3625
		$qty,
3626
		$txtva,
3627
		$txlocaltax1 = 0,
3628
		$txlocaltax2 = 0,
3629
		$fk_product = 0,
3630
		$remise_percent = 0,
3631
		$date_start = '',
3632
		$date_end = '',
3633
		$ventil = 0,
3634
		$info_bits = 0,
3635
		$fk_remise_except = '',
3636
		$price_base_type = 'HT',
3637
		$pu_ttc = 0,
3638
		$type = 0,
3639
		$rang = -1,
3640
		$special_code = 0,
3641
		$origin = '',
3642
		$origin_id = 0,
3643
		$fk_parent_line = 0,
3644
		$fk_fournprice = null,
3645
		$pa_ht = 0,
3646
		$label = '',
3647
		$array_options = 0,
3648
		$situation_percent = 100,
3649
		$fk_prev_id = 0,
3650
		$fk_unit = null,
3651
		$pu_ht_devise = 0,
3652
		$ref_ext = '',
3653
		$noupdateafterinsertline = 0
3654
	) {
3655
		// Deprecation warning
3656
		if ($label) {
3657
			dol_syslog(__METHOD__.": using line label is deprecated", LOG_WARNING);
3658
			//var_dump(debug_backtrace(false));exit;
3659
		}
3660
3661
		global $mysoc, $conf, $langs;
3662
3663
		dol_syslog(get_class($this)."::addline id=$this->id, pu_ht=$pu_ht, qty=$qty, txtva=$txtva, txlocaltax1=$txlocaltax1, txlocaltax2=$txlocaltax2, fk_product=$fk_product, remise_percent=$remise_percent, date_start=$date_start, date_end=$date_end, ventil=$ventil, info_bits=$info_bits, fk_remise_except=$fk_remise_except, price_base_type=$price_base_type, pu_ttc=$pu_ttc, type=$type, fk_unit=$fk_unit, desc=".dol_trunc($desc, 25), LOG_DEBUG);
3664
3665
		if ($this->statut == self::STATUS_DRAFT) {
3666
			include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
3667
3668
			// Clean parameters
3669
			if (empty($remise_percent)) {
3670
				$remise_percent = 0;
3671
			}
3672
			if (empty($qty)) {
3673
				$qty = 0;
3674
			}
3675
			if (empty($info_bits)) {
3676
				$info_bits = 0;
3677
			}
3678
			if (empty($rang)) {
3679
				$rang = 0;
3680
			}
3681
			if (empty($ventil)) {
3682
				$ventil = 0;
3683
			}
3684
			if (empty($txtva)) {
3685
				$txtva = 0;
3686
			}
3687
			if (empty($txlocaltax1)) {
3688
				$txlocaltax1 = 0;
3689
			}
3690
			if (empty($txlocaltax2)) {
3691
				$txlocaltax2 = 0;
3692
			}
3693
			if (empty($fk_parent_line) || $fk_parent_line < 0) {
3694
				$fk_parent_line = 0;
3695
			}
3696
			if (empty($fk_prev_id)) {
3697
				$fk_prev_id = 'null';
3698
			}
3699
			if (!isset($situation_percent) || $situation_percent > 100 || (string) $situation_percent == '') {
3700
				$situation_percent = 100;
3701
			}
3702
			if (empty($ref_ext)) {
3703
				$ref_ext = '';
3704
			}
3705
3706
			$remise_percent = price2num($remise_percent);
3707
			$qty = price2num($qty);
3708
			$pu_ht = price2num($pu_ht);
3709
			$pu_ht_devise = price2num($pu_ht_devise);
3710
			$pu_ttc = price2num($pu_ttc);
3711
			$pa_ht = price2num($pa_ht);
3712
			if (!preg_match('/\((.*)\)/', $txtva)) {
3713
				$txtva = price2num($txtva); // $txtva can have format '5.0(XXX)' or '5'
3714
			}
3715
			$txlocaltax1 = price2num($txlocaltax1);
3716
			$txlocaltax2 = price2num($txlocaltax2);
3717
3718
			if ($price_base_type == 'HT') {
3719
				$pu = $pu_ht;
3720
			} else {
3721
				$pu = $pu_ttc;
3722
			}
3723
3724
			// Check parameters
3725
			if ($type < 0) {
3726
				return -1;
3727
			}
3728
3729
			if ($date_start && $date_end && $date_start > $date_end) {
3730
				$langs->load("errors");
3731
				$this->error = $langs->trans('ErrorStartDateGreaterEnd');
3732
				return -1;
3733
			}
3734
3735
			$this->db->begin();
3736
3737
			$product_type = $type;
3738
			if (!empty($fk_product) && $fk_product > 0) {
3739
				$product = new Product($this->db);
3740
				$result = $product->fetch($fk_product);
3741
				$product_type = $product->type;
3742
3743
				if (!empty($conf->global->STOCK_MUST_BE_ENOUGH_FOR_INVOICE) && $product_type == 0 && $product->stock_reel < $qty) {
3744
					$langs->load("errors");
3745
					$this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnInvoice', $product->ref);
3746
					$this->db->rollback();
3747
					return -3;
3748
				}
3749
			}
3750
3751
			$localtaxes_type = getLocalTaxesFromRate($txtva, 0, $this->thirdparty, $mysoc);
3752
3753
			// Clean vat code
3754
			$reg = array();
3755
			$vat_src_code = '';
3756
			if (preg_match('/\((.*)\)/', $txtva, $reg)) {
3757
				$vat_src_code = $reg[1];
3758
				$txtva = preg_replace('/\s*\(.*\)/', '', $txtva); // Remove code into vatrate.
3759
			}
3760
3761
			// Calcul du total TTC et de la TVA pour la ligne a partir de
3762
			// qty, pu, remise_percent et txtva
3763
			// TRES IMPORTANT: C'est au moment de l'insertion ligne qu'on doit stocker
3764
			// la part ht, tva et ttc, et ce au niveau de la ligne qui a son propre taux tva.
3765
3766
			$tabprice = calcul_price_total($qty, $pu, $remise_percent, $txtva, $txlocaltax1, $txlocaltax2, 0, $price_base_type, $info_bits, $product_type, $mysoc, $localtaxes_type, $situation_percent, $this->multicurrency_tx, $pu_ht_devise);
3767
3768
			$total_ht  = $tabprice[0];
3769
			$total_tva = $tabprice[1];
3770
			$total_ttc = $tabprice[2];
3771
			$total_localtax1 = $tabprice[9];
3772
			$total_localtax2 = $tabprice[10];
3773
			$pu_ht = $tabprice[3];
3774
3775
			// MultiCurrency
3776
			$multicurrency_total_ht = $tabprice[16];
3777
			$multicurrency_total_tva = $tabprice[17];
3778
			$multicurrency_total_ttc = $tabprice[18];
3779
			$pu_ht_devise = $tabprice[19];
3780
3781
			// Rank to use
3782
			$ranktouse = $rang;
3783
			if ($ranktouse == -1) {
3784
				$rangmax = $this->line_max($fk_parent_line);
3785
				$ranktouse = $rangmax + 1;
3786
			}
3787
3788
			// Insert line
3789
			$this->line = new FactureLigne($this->db);
3790
3791
			$this->line->context = $this->context;
3792
3793
			$this->line->fk_facture = $this->id;
3794
			$this->line->label = $label; // deprecated
3795
			$this->line->desc = $desc;
3796
			$this->line->ref_ext = $ref_ext;
3797
3798
			$this->line->qty = ($this->type == self::TYPE_CREDIT_NOTE ? abs($qty) : $qty); // For credit note, quantity is always positive and unit price negative
3799
			$this->line->subprice = ($this->type == self::TYPE_CREDIT_NOTE ? -abs($pu_ht) : $pu_ht); // For credit note, unit price always negative, always positive otherwise
3800
3801
			$this->line->vat_src_code = $vat_src_code;
3802
			$this->line->tva_tx = $txtva;
3803
			$this->line->localtax1_tx = ($total_localtax1 ? $localtaxes_type[1] : 0);
3804
			$this->line->localtax2_tx = ($total_localtax2 ? $localtaxes_type[3] : 0);
3805
			$this->line->localtax1_type = empty($localtaxes_type[0]) ? '' : $localtaxes_type[0];
3806
			$this->line->localtax2_type = empty($localtaxes_type[2]) ? '' : $localtaxes_type[2];
3807
3808
			$this->line->total_ht = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($total_ht) : $total_ht); // For credit note and if qty is negative, total is negative
3809
			$this->line->total_ttc = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($total_ttc) : $total_ttc); // For credit note and if qty is negative, total is negative
3810
			$this->line->total_tva = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($total_tva) : $total_tva); // For credit note and if qty is negative, total is negative
3811
			$this->line->total_localtax1 = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($total_localtax1) : $total_localtax1); // For credit note and if qty is negative, total is negative
3812
			$this->line->total_localtax2 = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($total_localtax2) : $total_localtax2); // For credit note and if qty is negative, total is negative
3813
3814
			$this->line->fk_product = $fk_product;
3815
			$this->line->product_type = $product_type;
3816
			$this->line->remise_percent = $remise_percent;
3817
			$this->line->date_start = $date_start;
3818
			$this->line->date_end = $date_end;
3819
			$this->line->ventil = $ventil;
0 ignored issues
show
Bug introduced by
The property ventil does not exist on FactureLigne. Did you mean fk_code_ventilation?
Loading history...
3820
			$this->line->rang = $ranktouse;
3821
			$this->line->info_bits = $info_bits;
3822
			$this->line->fk_remise_except = $fk_remise_except;
3823
3824
			$this->line->special_code = $special_code;
3825
			$this->line->fk_parent_line = $fk_parent_line;
3826
			$this->line->origin = $origin;
3827
			$this->line->origin_id = $origin_id;
3828
			$this->line->situation_percent = $situation_percent;
3829
			$this->line->fk_prev_id = $fk_prev_id;
3830
			$this->line->fk_unit = $fk_unit;
3831
3832
			// infos marge
3833
			$this->line->fk_fournprice = $fk_fournprice;
3834
			$this->line->pa_ht = $pa_ht;
3835
3836
			// Multicurrency
3837
			$this->line->fk_multicurrency = $this->fk_multicurrency;
3838
			$this->line->multicurrency_code = $this->multicurrency_code;
3839
			$this->line->multicurrency_subprice	= ($this->type == self::TYPE_CREDIT_NOTE ? -abs($pu_ht_devise) : $pu_ht_devise); // For credit note, unit price always negative, always positive otherwise
3840
3841
			$this->line->multicurrency_total_ht = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($multicurrency_total_ht) : $multicurrency_total_ht); // For credit note and if qty is negative, total is negative
3842
			$this->line->multicurrency_total_tva = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($multicurrency_total_tva) : $multicurrency_total_tva); // For credit note and if qty is negative, total is negative
3843
			$this->line->multicurrency_total_ttc = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($multicurrency_total_ttc) : $multicurrency_total_ttc); // For credit note and if qty is negative, total is negative
3844
3845
			if (is_array($array_options) && count($array_options) > 0) {
3846
				$this->line->array_options = $array_options;
3847
			}
3848
3849
			$result = $this->line->insert();
3850
			if ($result > 0) {
3851
				// Reorder if child line
3852
				if (!empty($fk_parent_line)) {
3853
					$this->line_order(true, 'DESC');
3854
				} elseif ($ranktouse > 0 && $ranktouse <= count($this->lines)) { // Update all rank of all other lines
3855
					$linecount = count($this->lines);
3856
					for ($ii = $ranktouse; $ii <= $linecount; $ii++) {
3857
						$this->updateRangOfLine($this->lines[$ii - 1]->id, $ii + 1);
3858
					}
3859
				}
3860
3861
				// Mise a jour informations denormalisees au niveau de la facture meme
3862
				if (empty($noupdateafterinsertline)) {
3863
					$result = $this->update_price(1, 'auto', 0, $mysoc); // The addline method is designed to add line from user input so total calculation with update_price must be done using 'auto' mode.
3864
				}
3865
3866
				if ($result > 0) {
3867
					$this->db->commit();
3868
					return $this->line->id;
3869
				} else {
3870
					$this->error = $this->db->lasterror();
3871
					$this->db->rollback();
3872
					return -1;
3873
				}
3874
			} else {
3875
				$this->error = $this->line->error;
3876
				$this->errors = $this->line->errors;
3877
				$this->db->rollback();
3878
				return -2;
3879
			}
3880
		} else {
3881
			$this->errors[]='status of invoice must be Draft to allow use of ->addline()';
3882
			dol_syslog(get_class($this)."::addline status of invoice must be Draft to allow use of ->addline()", LOG_ERR);
3883
			return -3;
3884
		}
3885
	}
3886
3887
	/**
3888
	 *  Update a detail line
3889
	 *
3890
	 *  @param     	int			$rowid           	Id of line to update
3891
	 *  @param     	string		$desc            	Description of line
3892
	 *  @param     	double		$pu              	Prix unitaire (HT ou TTC selon price_base_type) (> 0 even for credit note lines)
3893
	 *  @param     	double		$qty             	Quantity
3894
	 *  @param     	double		$remise_percent  	Percentage discount of the line
3895
	 *  @param     	int		    $date_start      	Date de debut de validite du service
3896
	 *  @param     	int		    $date_end        	Date de fin de validite du service
3897
	 *  @param     	double		$txtva          	VAT Rate (Can be '8.5', '8.5 (ABC)')
3898
	 * 	@param		double		$txlocaltax1		Local tax 1 rate
3899
	 *  @param		double		$txlocaltax2		Local tax 2 rate
3900
	 * 	@param     	string		$price_base_type 	HT or TTC
3901
	 * 	@param     	int			$info_bits 		    Miscellaneous informations
3902
	 * 	@param		int			$type				Type of line (0=product, 1=service)
3903
	 * 	@param		int			$fk_parent_line		Id of parent line (0 in most cases, used by modules adding sublevels into lines).
3904
	 * 	@param		int			$skip_update_total	Keep fields total_xxx to 0 (used for special lines by some modules)
3905
	 * 	@param		int			$fk_fournprice		Id of origin supplier price
3906
	 * 	@param		int			$pa_ht				Price (without tax) of product when it was bought
3907
	 * 	@param		string		$label				Label of the line (deprecated, do not use)
3908
	 * 	@param		int			$special_code		Special code (also used by externals modules!)
3909
	 *  @param		array		$array_options		extrafields array
3910
	 * 	@param      int         $situation_percent  Situation advance percentage
3911
	 * 	@param 		string		$fk_unit 			Code of the unit to use. Null to use the default one
3912
	 * 	@param		double		$pu_ht_devise		Unit price in currency
3913
	 * 	@param		int			$notrigger			disable line update trigger
3914
	 *  @param		string		$ref_ext		    External reference of the line
3915
	 *  @param		integer		$rang		    	rank of line
3916
	 *  @return    	int             				< 0 if KO, > 0 if OK
3917
	 */
3918
	public function updateline($rowid, $desc, $pu, $qty, $remise_percent, $date_start, $date_end, $txtva, $txlocaltax1 = 0, $txlocaltax2 = 0, $price_base_type = 'HT', $info_bits = 0, $type = self::TYPE_STANDARD, $fk_parent_line = 0, $skip_update_total = 0, $fk_fournprice = null, $pa_ht = 0, $label = '', $special_code = 0, $array_options = 0, $situation_percent = 100, $fk_unit = null, $pu_ht_devise = 0, $notrigger = 0, $ref_ext = '', $rang = 0)
3919
	{
3920
		global $conf, $user;
3921
		// Deprecation warning
3922
		if ($label) {
3923
			dol_syslog(__METHOD__.": using line label is deprecated", LOG_WARNING);
3924
		}
3925
3926
		include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
3927
3928
		global $mysoc, $langs;
3929
3930
		dol_syslog(get_class($this)."::updateline rowid=$rowid, desc=$desc, pu=$pu, qty=$qty, remise_percent=$remise_percent, date_start=$date_start, date_end=$date_end, txtva=$txtva, txlocaltax1=$txlocaltax1, txlocaltax2=$txlocaltax2, price_base_type=$price_base_type, info_bits=$info_bits, type=$type, fk_parent_line=$fk_parent_line pa_ht=$pa_ht, special_code=$special_code, fk_unit=$fk_unit, pu_ht_devise=$pu_ht_devise", LOG_DEBUG);
3931
3932
		if ($this->statut == self::STATUS_DRAFT) {
3933
			if (!$this->is_last_in_cycle() && empty($this->error)) {
3934
				if (!$this->checkProgressLine($rowid, $situation_percent)) {
3935
					if (!$this->error) {
3936
						$this->error = $langs->trans('invoiceLineProgressError');
3937
					}
3938
					return -3;
3939
				}
3940
			}
3941
3942
			if ($date_start && $date_end && $date_start > $date_end) {
3943
				$langs->load("errors");
3944
				$this->error = $langs->trans('ErrorStartDateGreaterEnd');
3945
				return -1;
3946
			}
3947
3948
			$this->db->begin();
3949
3950
			// Clean parameters
3951
			if (empty($qty)) {
3952
				$qty = 0;
3953
			}
3954
			if (empty($fk_parent_line) || $fk_parent_line < 0) {
3955
				$fk_parent_line = 0;
3956
			}
3957
			if (empty($special_code) || $special_code == 3) {
3958
				$special_code = 0;
3959
			}
3960
			if (!isset($situation_percent) || $situation_percent > 100 || (string) $situation_percent == '') {
3961
				$situation_percent = 100;
3962
			}
3963
			if (empty($ref_ext)) {
3964
				$ref_ext = '';
3965
			}
3966
3967
			$remise_percent = price2num($remise_percent);
3968
			$qty			= price2num($qty);
3969
			$pu 			= price2num($pu);
3970
			$pu_ht_devise = price2num($pu_ht_devise);
3971
			$pa_ht = price2num($pa_ht);
3972
			if (!preg_match('/\((.*)\)/', $txtva)) {
3973
				$txtva = price2num($txtva); // $txtva can have format '5.0(XXX)' or '5'
3974
			}
3975
			$txlocaltax1	= price2num($txlocaltax1);
3976
			$txlocaltax2	= price2num($txlocaltax2);
3977
3978
			// Check parameters
3979
			if ($type < 0) {
3980
				return -1;
3981
			}
3982
3983
			// Calculate total with, without tax and tax from qty, pu, remise_percent and txtva
3984
			// TRES IMPORTANT: C'est au moment de l'insertion ligne qu'on doit stocker
3985
			// la part ht, tva et ttc, et ce au niveau de la ligne qui a son propre taux tva.
3986
3987
			$localtaxes_type = getLocalTaxesFromRate($txtva, 0, $this->thirdparty, $mysoc);
3988
3989
			// Clean vat code
3990
			$reg = array();
3991
			$vat_src_code = '';
3992
			if (preg_match('/\((.*)\)/', $txtva, $reg)) {
3993
				$vat_src_code = $reg[1];
3994
				$txtva = preg_replace('/\s*\(.*\)/', '', $txtva); // Remove code into vatrate.
3995
			}
3996
3997
			$tabprice = calcul_price_total($qty, $pu, $remise_percent, $txtva, $txlocaltax1, $txlocaltax2, 0, $price_base_type, $info_bits, $type, $mysoc, $localtaxes_type, $situation_percent, $this->multicurrency_tx, $pu_ht_devise);
3998
3999
			$total_ht  = $tabprice[0];
4000
			$total_tva = $tabprice[1];
4001
			$total_ttc = $tabprice[2];
4002
			$total_localtax1 = $tabprice[9];
4003
			$total_localtax2 = $tabprice[10];
4004
			$pu_ht  = $tabprice[3];
4005
			$pu_tva = $tabprice[4];
4006
			$pu_ttc = $tabprice[5];
4007
4008
			// MultiCurrency
4009
			$multicurrency_total_ht = $tabprice[16];
4010
			$multicurrency_total_tva = $tabprice[17];
4011
			$multicurrency_total_ttc = $tabprice[18];
4012
			$pu_ht_devise = $tabprice[19];
4013
4014
			// Old properties: $price, $remise (deprecated)
4015
			$price = $pu;
4016
			$remise = 0;
4017
			if ($remise_percent > 0) {
4018
				$remise = round(($pu * $remise_percent / 100), 2);
4019
				$price = ($pu - $remise);
4020
			}
4021
			$price = price2num($price);
4022
4023
			//Fetch current line from the database and then clone the object and set it in $oldline property
4024
			$line = new FactureLigne($this->db);
4025
			$line->fetch($rowid);
4026
			$line->fetch_optionals();
4027
4028
			if (!empty($line->fk_product)) {
4029
				$product = new Product($this->db);
4030
				$result = $product->fetch($line->fk_product);
4031
				$product_type = $product->type;
4032
4033
				if (!empty($conf->global->STOCK_MUST_BE_ENOUGH_FOR_INVOICE) && $product_type == 0 && $product->stock_reel < $qty) {
4034
					$langs->load("errors");
4035
					$this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnInvoice', $product->ref);
4036
					$this->db->rollback();
4037
					return -3;
4038
				}
4039
			}
4040
4041
			$staticline = clone $line;
4042
4043
			$line->oldline = $staticline;
4044
			$this->line = $line;
4045
			$this->line->context = $this->context;
4046
			$this->line->rang = $rang;
4047
4048
			// Reorder if fk_parent_line change
4049
			if (!empty($fk_parent_line) && !empty($staticline->fk_parent_line) && $fk_parent_line != $staticline->fk_parent_line) {
4050
				$rangmax = $this->line_max($fk_parent_line);
4051
				$this->line->rang = $rangmax + 1;
4052
			}
4053
4054
			$this->line->id = $rowid;
4055
			$this->line->rowid = $rowid;
4056
			$this->line->label = $label;
4057
			$this->line->desc = $desc;
4058
			$this->line->ref_ext = $ref_ext;
4059
			$this->line->qty = ($this->type == self::TYPE_CREDIT_NOTE ?abs($qty) : $qty); // For credit note, quantity is always positive and unit price negative
4060
4061
			$this->line->vat_src_code = $vat_src_code;
4062
			$this->line->tva_tx = $txtva;
4063
			$this->line->localtax1_tx		= $txlocaltax1;
4064
			$this->line->localtax2_tx		= $txlocaltax2;
4065
			$this->line->localtax1_type		= empty($localtaxes_type[0]) ? '' : $localtaxes_type[0];
4066
			$this->line->localtax2_type		= empty($localtaxes_type[2]) ? '' : $localtaxes_type[2];
4067
4068
			$this->line->remise_percent		= $remise_percent;
4069
			$this->line->subprice			= ($this->type == self::TYPE_CREDIT_NOTE ?-abs($pu_ht) : $pu_ht); // For credit note, unit price always negative, always positive otherwise
4070
			$this->line->date_start = $date_start;
4071
			$this->line->date_end			= $date_end;
4072
			$this->line->total_ht			= (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ?-abs($total_ht) : $total_ht); // For credit note and if qty is negative, total is negative
4073
			$this->line->total_tva			= (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ?-abs($total_tva) : $total_tva);
4074
			$this->line->total_localtax1	= $total_localtax1;
4075
			$this->line->total_localtax2	= $total_localtax2;
4076
			$this->line->total_ttc			= (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ?-abs($total_ttc) : $total_ttc);
4077
			$this->line->info_bits			= $info_bits;
4078
			$this->line->special_code		= $special_code;
4079
			$this->line->product_type		= $type;
4080
			$this->line->fk_parent_line = $fk_parent_line;
4081
			$this->line->skip_update_total = $skip_update_total;
4082
			$this->line->situation_percent = $situation_percent;
4083
			$this->line->fk_unit = $fk_unit;
4084
4085
			$this->line->fk_fournprice = $fk_fournprice;
4086
			$this->line->pa_ht = $pa_ht;
4087
4088
			// Multicurrency
4089
			$this->line->multicurrency_subprice		= ($this->type == self::TYPE_CREDIT_NOTE ?-abs($pu_ht_devise) : $pu_ht_devise); // For credit note, unit price always negative, always positive otherwise
4090
			$this->line->multicurrency_total_ht 	= (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ?-abs($multicurrency_total_ht) : $multicurrency_total_ht); // For credit note and if qty is negative, total is negative
4091
			$this->line->multicurrency_total_tva 	= (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ?-abs($multicurrency_total_tva) : $multicurrency_total_tva);
4092
			$this->line->multicurrency_total_ttc 	= (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ?-abs($multicurrency_total_ttc) : $multicurrency_total_ttc);
4093
4094
			if (is_array($array_options) && count($array_options) > 0) {
4095
				// We replace values in this->line->array_options only for entries defined into $array_options
4096
				foreach ($array_options as $key => $value) {
4097
					$this->line->array_options[$key] = $array_options[$key];
4098
				}
4099
			}
4100
4101
			$result = $this->line->update($user, $notrigger);
4102
			if ($result > 0) {
4103
				// Reorder if child line
4104
				if (!empty($fk_parent_line)) {
4105
					$this->line_order(true, 'DESC');
4106
				}
4107
4108
				// Mise a jour info denormalisees au niveau facture
4109
				$this->update_price(1);
4110
				$this->db->commit();
4111
				return $result;
4112
			} else {
4113
				$this->error = $this->line->error;
4114
				$this->db->rollback();
4115
				return -1;
4116
			}
4117
		} else {
4118
			$this->error = "Invoice statut makes operation forbidden";
4119
			return -2;
4120
		}
4121
	}
4122
4123
	/**
4124
	 * Check if the percent edited is lower of next invoice line
4125
	 *
4126
	 * @param	int		$idline				id of line to check
4127
	 * @param	float	$situation_percent	progress percentage need to be test
4128
	 * @return 	bool						false if KO, true if OK
4129
	 */
4130
	public function checkProgressLine($idline, $situation_percent)
4131
	{
4132
		$sql = 'SELECT fd.situation_percent FROM '.MAIN_DB_PREFIX.'facturedet fd
4133
				INNER JOIN '.MAIN_DB_PREFIX.'facture f ON (fd.fk_facture = f.rowid)
4134
				WHERE fd.fk_prev_id = '.((int) $idline).' AND f.fk_statut <> 0';
4135
4136
		$result = $this->db->query($sql);
4137
		if (!$result) {
4138
			$this->error = $this->db->error();
4139
			return false;
4140
		}
4141
4142
		$obj = $this->db->fetch_object($result);
4143
4144
		if ($obj === null) {
4145
			return true;
4146
		} else {
4147
			return ($situation_percent < $obj->situation_percent);
4148
		}
4149
	}
4150
4151
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4152
	/**
4153
	 * Update invoice line with percentage
4154
	 *
4155
	 * @param  FactureLigne $line       	Invoice line
4156
	 * @param  int          $percent    	Percentage
4157
	 * @param  boolean      $update_price   Update object price
4158
	 * @return void
4159
	 */
4160
	public function update_percent($line, $percent, $update_price = true)
4161
	{
4162
		// phpcs:enable
4163
		global $mysoc, $user;
4164
4165
		// Progress should never be changed for discount lines
4166
		if (($line->info_bits & 2) == 2) {
4167
			return;
4168
		}
4169
4170
		include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
4171
4172
		// Cap percentages to 100
4173
		if ($percent > 100) {
4174
			$percent = 100;
4175
		}
4176
		$line->situation_percent = $percent;
4177
		$tabprice = calcul_price_total($line->qty, $line->subprice, $line->remise_percent, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, 0, 'HT', 0, $line->product_type, $mysoc, '', $percent);
4178
		$line->total_ht = $tabprice[0];
4179
		$line->total_tva = $tabprice[1];
4180
		$line->total_ttc = $tabprice[2];
4181
		$line->total_localtax1 = $tabprice[9];
4182
		$line->total_localtax2 = $tabprice[10];
4183
		$line->multicurrency_total_ht  = $tabprice[16];
4184
		$line->multicurrency_total_tva = $tabprice[17];
4185
		$line->multicurrency_total_ttc = $tabprice[18];
4186
		$line->update($user);
4187
4188
		// sometimes it is better to not update price for each line, ie when updating situation on all lines
4189
		if ($update_price) {
4190
			$this->update_price(1);
4191
		}
4192
	}
4193
4194
	/**
4195
	 *	Delete line in database
4196
	 *
4197
	 *	@param		int		$rowid		Id of line to delete
4198
	 *  @param		int		$id			Id of object (for a check)
4199
	 *	@return		int					<0 if KO, >0 if OK
4200
	 */
4201
	public function deleteline($rowid, $id = 0)
4202
	{
4203
		global $user;
4204
4205
		dol_syslog(get_class($this)."::deleteline rowid=".((int) $rowid), LOG_DEBUG);
4206
4207
		if ($this->statut != self::STATUS_DRAFT) {
4208
			$this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
4209
			return -1;
4210
		}
4211
4212
		$line = new FactureLigne($this->db);
4213
4214
		$line->context = $this->context;
4215
4216
		// Load line
4217
		$result = $line->fetch($rowid);
4218
		if (!($result > 0)) {
4219
			dol_print_error($this->db, $line->error, $line->errors);
4220
			return -1;
4221
		}
4222
4223
		if ($id > 0 && $line->fk_facture != $id) {
4224
			$this->error = 'ErrorLineIDDoesNotMatchWithObjectID';
4225
			return -1;
4226
		}
4227
4228
		$this->db->begin();
4229
4230
		// Free discount linked to invoice line
4231
		$sql = 'UPDATE '.MAIN_DB_PREFIX.'societe_remise_except';
4232
		$sql .= ' SET fk_facture_line = NULL';
4233
		$sql .= ' WHERE fk_facture_line = '.((int) $rowid);
4234
4235
		dol_syslog(get_class($this)."::deleteline", LOG_DEBUG);
4236
		$result = $this->db->query($sql);
4237
		if (!$result) {
4238
			$this->error = $this->db->error();
4239
			$this->db->rollback();
4240
			return -1;
4241
		}
4242
4243
		// Memorize previous line for triggers
4244
		$staticline = clone $line;
4245
		$line->oldline = $staticline;
4246
4247
		if ($line->delete($user) > 0) {
4248
			$result = $this->update_price(1);
4249
4250
			if ($result > 0) {
4251
				$this->db->commit();
4252
				return 1;
4253
			} else {
4254
				$this->db->rollback();
4255
				$this->error = $this->db->lasterror();
4256
				return -1;
4257
			}
4258
		} else {
4259
			$this->db->rollback();
4260
			$this->error = $line->error;
4261
			return -1;
4262
		}
4263
	}
4264
4265
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4266
	/**
4267
	 *	Set percent discount
4268
	 *
4269
	 *  @deprecated
4270
	 *  @see setDiscount()
4271
	 *	@param     	User	$user		User that set discount
4272
	 *	@param     	double	$remise		Discount
4273
	 *  @param     	int		$notrigger	1=Does not execute triggers, 0= execute triggers
4274
	 *	@return		int 				<0 if KO, >0 if OK
4275
	 */
4276
	public function set_remise($user, $remise, $notrigger = 0)
4277
	{
4278
		// phpcs:enable
4279
		dol_syslog(get_class($this)."::set_remise is deprecated, use setDiscount instead", LOG_NOTICE);
4280
		return $this->setDiscount($user, $remise, $notrigger);
4281
	}
4282
4283
	/**
4284
	 *	Set percent discount
4285
	 *
4286
	 *	@param     	User	$user		User that set discount
4287
	 *	@param     	double	$remise		Discount
4288
	 *  @param     	int		$notrigger	1=Does not execute triggers, 0= execute triggers
4289
	 *	@return		int 				<0 if KO, >0 if OK
4290
	 */
4291
	public function setDiscount($user, $remise, $notrigger = 0)
4292
	{
4293
		// Clean parameters
4294
		if (empty($remise)) {
4295
			$remise = 0;
4296
		}
4297
4298
		if ($user->rights->facture->creer) {
4299
			$remise = price2num($remise, 2);
4300
4301
			$error = 0;
4302
4303
			$this->db->begin();
4304
4305
			$sql = 'UPDATE '.MAIN_DB_PREFIX.'facture';
4306
			$sql .= ' SET remise_percent = '.((float) $remise);
4307
			$sql .= " WHERE rowid = ".((int) $this->id);
4308
			$sql .= ' AND fk_statut = '.self::STATUS_DRAFT;
4309
4310
			dol_syslog(__METHOD__, LOG_DEBUG);
4311
			$resql = $this->db->query($sql);
4312
			if (!$resql) {
4313
				$this->errors[] = $this->db->error();
4314
				$error++;
4315
			}
4316
4317
			if (!$notrigger && empty($error)) {
4318
				// Call trigger
4319
				$result = $this->call_trigger('BILL_MODIFY', $user);
4320
				if ($result < 0) {
4321
					$error++;
4322
				}
4323
				// End call triggers
4324
			}
4325
4326
			if (!$error) {
4327
				$this->remise_percent = $remise;
4328
				$this->update_price(1);
4329
4330
				$this->db->commit();
4331
				return 1;
4332
			} else {
4333
				foreach ($this->errors as $errmsg) {
4334
					dol_syslog(__METHOD__.' Error: '.$errmsg, LOG_ERR);
4335
					$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
4336
				}
4337
				$this->db->rollback();
4338
				return -1 * $error;
4339
			}
4340
		}
4341
4342
		return 0;
4343
	}
4344
4345
4346
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4347
	/**
4348
	 *	Set absolute discount
4349
	 *
4350
	 *	@param     	User	$user 		User that set discount
4351
	 *	@param     	double	$remise		Discount
4352
	 *  @param     	int		$notrigger	1=Does not execute triggers, 0= execute triggers
4353
	 *	@return		int 				<0 if KO, >0 if OK
4354
	 */
4355
	public function set_remise_absolue($user, $remise, $notrigger = 0)
4356
	{
4357
		// phpcs:enable
4358
		if (empty($remise)) {
4359
			$remise = 0;
4360
		}
4361
4362
		if ($user->rights->facture->creer) {
4363
			$error = 0;
4364
4365
			$this->db->begin();
4366
4367
			$remise = price2num($remise);
4368
4369
			$sql = 'UPDATE '.MAIN_DB_PREFIX.'facture';
4370
			$sql .= ' SET remise_absolue = '.((float) $remise);
4371
			$sql .= " WHERE rowid = ".((int) $this->id);
4372
			$sql .= ' AND fk_statut = '.self::STATUS_DRAFT;
4373
4374
			dol_syslog(__METHOD__, LOG_DEBUG);
4375
			$resql = $this->db->query($sql);
4376
			if (!$resql) {
4377
				$this->errors[] = $this->db->error();
4378
				$error++;
4379
			}
4380
4381
			if (!$error) {
4382
				$this->oldcopy = clone $this;
4383
				$this->remise_absolue = $remise;
4384
				$this->update_price(1);
4385
			}
4386
4387
			if (!$notrigger && empty($error)) {
4388
				// Call trigger
4389
				$result = $this->call_trigger('BILL_MODIFY', $user);
4390
				if ($result < 0) {
4391
					$error++;
4392
				}
4393
				// End call triggers
4394
			}
4395
4396
			if (!$error) {
4397
				$this->db->commit();
4398
				return 1;
4399
			} else {
4400
				foreach ($this->errors as $errmsg) {
4401
					dol_syslog(__METHOD__.' Error: '.$errmsg, LOG_ERR);
4402
					$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
4403
				}
4404
				$this->db->rollback();
4405
				return -1 * $error;
4406
			}
4407
		}
4408
4409
		return 0;
4410
	}
4411
4412
	/**
4413
	 *      Return next reference of customer invoice not already used (or last reference)
4414
	 *      according to numbering module defined into constant FACTURE_ADDON
4415
	 *
4416
	 *      @param	   Societe		$soc		object company
4417
	 *      @param     string		$mode		'next' for next value or 'last' for last value
4418
	 *      @return    string					free ref or last ref
4419
	 */
4420
	public function getNextNumRef($soc, $mode = 'next')
4421
	{
4422
		global $conf, $langs;
4423
4424
		if ($this->module_source == 'takepos') {
4425
			$langs->load('cashdesk');
4426
4427
			$moduleName = 'takepos';
4428
			$moduleSourceName = 'Takepos';
4429
			$addonConstName = 'TAKEPOS_REF_ADDON';
4430
4431
			// Clean parameters (if not defined or using deprecated value)
4432
			if (empty($conf->global->TAKEPOS_REF_ADDON)) {
4433
				$conf->global->TAKEPOS_REF_ADDON = 'mod_takepos_ref_simple';
4434
			}
4435
4436
			$addon = $conf->global->TAKEPOS_REF_ADDON;
4437
		} else {
4438
			$langs->load('bills');
4439
4440
			$moduleName = 'facture';
4441
			$moduleSourceName = 'Invoice';
4442
			$addonConstName = 'FACTURE_ADDON';
4443
4444
			// Clean parameters (if not defined or using deprecated value)
4445
			if (empty($conf->global->FACTURE_ADDON)) {
4446
				$conf->global->FACTURE_ADDON = 'mod_facture_terre';
4447
			} elseif ($conf->global->FACTURE_ADDON == 'terre') {
4448
				$conf->global->FACTURE_ADDON = 'mod_facture_terre';
4449
			} elseif ($conf->global->FACTURE_ADDON == 'mercure') {
4450
				$conf->global->FACTURE_ADDON = 'mod_facture_mercure';
4451
			}
4452
4453
			$addon = $conf->global->FACTURE_ADDON;
4454
		}
4455
4456
		if (!empty($addon)) {
4457
			dol_syslog("Call getNextNumRef with ".$addonConstName." = ".$conf->global->FACTURE_ADDON.", thirdparty=".$soc->name.", type=".$soc->typent_code.", mode=".$mode, LOG_DEBUG);
4458
4459
			$mybool = false;
4460
4461
			$file = $addon.'.php';
4462
			$classname = $addon;
4463
4464
4465
			// Include file with class
4466
			$dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
4467
			foreach ($dirmodels as $reldir) {
4468
				$dir = dol_buildpath($reldir.'core/modules/'.$moduleName.'/');
4469
4470
				// Load file with numbering class (if found)
4471
				if (is_file($dir.$file) && is_readable($dir.$file)) {
4472
					$mybool |= include_once $dir.$file;
4473
				}
4474
			}
4475
4476
			// For compatibility
4477
			if (!$mybool) {
4478
				$file = $addon.'/'.$addon.'.modules.php';
4479
				$classname = 'mod_'.$moduleName.'_'.$addon;
4480
				$classname = preg_replace('/\-.*$/', '', $classname);
4481
				// Include file with class
4482
				foreach ($conf->file->dol_document_root as $dirroot) {
4483
					$dir = $dirroot.'/core/modules/'.$moduleName.'/';
4484
4485
					// Load file with numbering class (if found)
4486
					if (is_file($dir.$file) && is_readable($dir.$file)) {
4487
						$mybool |= include_once $dir.$file;
4488
					}
4489
				}
4490
			}
4491
4492
			if (!$mybool) {
4493
				dol_print_error('', 'Failed to include file '.$file);
4494
				return '';
4495
			}
4496
4497
			$obj = new $classname();
4498
4499
			$numref = $obj->getNextValue($soc, $this, $mode);
4500
4501
4502
			/**
4503
			 * $numref can be empty in case we ask for the last value because if there is no invoice created with the
4504
			 * set up mask.
4505
			 */
4506
			if ($mode != 'last' && !$numref) {
4507
				$this->error = $obj->error;
4508
				return '';
4509
			}
4510
4511
			return $numref;
4512
		} else {
4513
			$langs->load('errors');
4514
			print $langs->trans('Error').' '.$langs->trans('ErrorModuleSetupNotComplete', $langs->transnoentitiesnoconv($moduleSourceName));
4515
			return '';
4516
		}
4517
	}
4518
4519
	/**
4520
	 *	Load miscellaneous information for tab "Info"
4521
	 *
4522
	 *	@param  int		$id		Id of object to load
4523
	 *	@return	void
4524
	 */
4525
	public function info($id)
4526
	{
4527
		$sql = 'SELECT c.rowid, datec, date_valid as datev, tms as datem,';
4528
		$sql .= ' date_closing as dateclosing,';
4529
		$sql .= ' fk_user_author, fk_user_valid, fk_user_closing';
4530
		$sql .= ' FROM '.MAIN_DB_PREFIX.'facture as c';
4531
		$sql .= ' WHERE c.rowid = '.((int) $id);
4532
4533
		$result = $this->db->query($sql);
4534
		if ($result) {
4535
			if ($this->db->num_rows($result)) {
4536
				$obj = $this->db->fetch_object($result);
4537
				$this->id = $obj->rowid;
4538
				if ($obj->fk_user_author) {
4539
					$cuser = new User($this->db);
4540
					$cuser->fetch($obj->fk_user_author);
4541
					$this->user_creation = $cuser;
4542
				}
4543
				if ($obj->fk_user_valid) {
4544
					$vuser = new User($this->db);
4545
					$vuser->fetch($obj->fk_user_valid);
4546
					$this->user_validation = $vuser;
4547
				}
4548
				if ($obj->fk_user_closing) {
4549
					$cluser = new User($this->db);
4550
					$cluser->fetch($obj->fk_user_closing);
4551
					$this->user_closing = $cluser;
4552
				}
4553
4554
				$this->date_creation     = $this->db->jdate($obj->datec);
4555
				$this->date_modification = $this->db->jdate($obj->datem);
4556
				$this->date_validation   = $this->db->jdate($obj->datev);
4557
				$this->date_closing      = $this->db->jdate($obj->dateclosing);
4558
			}
4559
			$this->db->free($result);
4560
		} else {
4561
			dol_print_error($this->db);
4562
		}
4563
	}
4564
4565
4566
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4567
	/**
4568
	 *  Return list of invoices (eventually filtered on a user) into an array
4569
	 *
4570
	 *  @param		int		$shortlist		0=Return array[id]=ref, 1=Return array[](id=>id,ref=>ref,name=>name)
4571
	 *  @param      int		$draft      	0=not draft, 1=draft
4572
	 *  @param      User	$excluser      	Objet user to exclude
4573
	 *  @param    	int		$socid			Id third pary
4574
	 *  @param    	int		$limit			For pagination
4575
	 *  @param    	int		$offset			For pagination
4576
	 *  @param    	string	$sortfield		Sort criteria
4577
	 *  @param    	string	$sortorder		Sort order
4578
	 *  @return     array|int             	-1 if KO, array with result if OK
4579
	 */
4580
	public function liste_array($shortlist = 0, $draft = 0, $excluser = '', $socid = 0, $limit = 0, $offset = 0, $sortfield = 'f.datef,f.rowid', $sortorder = 'DESC')
4581
	{
4582
		// phpcs:enable
4583
		global $conf, $user;
4584
4585
		$ga = array();
4586
4587
		$sql = "SELECT s.rowid, s.nom as name, s.client,";
4588
		$sql .= " f.rowid as fid, f.ref as ref, f.datef as df";
4589
		if (empty($user->rights->societe->client->voir) && !$socid) {
4590
			$sql .= ", sc.fk_soc, sc.fk_user";
4591
		}
4592
		$sql .= " FROM ".MAIN_DB_PREFIX."societe as s, ".MAIN_DB_PREFIX."facture as f";
4593
		if (empty($user->rights->societe->client->voir) && !$socid) {
4594
			$sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
4595
		}
4596
		$sql .= " WHERE f.entity IN (".getEntity('invoice').")";
4597
		$sql .= " AND f.fk_soc = s.rowid";
4598
		if (empty($user->rights->societe->client->voir) && !$socid) { //restriction
4599
			$sql .= " AND s.rowid = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4600
		}
4601
		if ($socid) {
4602
			$sql .= " AND s.rowid = ".((int) $socid);
4603
		}
4604
		if ($draft) {
4605
			$sql .= " AND f.fk_statut = ".self::STATUS_DRAFT;
4606
		}
4607
		if (is_object($excluser)) {
4608
			$sql .= " AND f.fk_user_author <> ".((int) $excluser->id);
4609
		}
4610
		$sql .= $this->db->order($sortfield, $sortorder);
4611
		$sql .= $this->db->plimit($limit, $offset);
4612
4613
		$result = $this->db->query($sql);
4614
		if ($result) {
4615
			$numc = $this->db->num_rows($result);
4616
			if ($numc) {
4617
				$i = 0;
4618
				while ($i < $numc) {
4619
					$obj = $this->db->fetch_object($result);
4620
4621
					if ($shortlist == 1) {
4622
						$ga[$obj->fid] = $obj->ref;
4623
					} elseif ($shortlist == 2) {
4624
						$ga[$obj->fid] = $obj->ref.' ('.$obj->name.')';
4625
					} else {
4626
						$ga[$i]['id'] = $obj->fid;
4627
						$ga[$i]['ref'] 	= $obj->ref;
4628
						$ga[$i]['name'] = $obj->name;
4629
					}
4630
					$i++;
4631
				}
4632
			}
4633
			return $ga;
4634
		} else {
4635
			dol_print_error($this->db);
4636
			return -1;
4637
		}
4638
	}
4639
4640
4641
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4642
	/**
4643
	 *	Return list of invoices qualified to be replaced by another invoice.
4644
	 *	Invoices matching the following rules are returned:
4645
	 *	(Status validated or abandonned for a reason 'other') + not payed + no payment at all + not already replaced
4646
	 *
4647
	 *	@param		int			$socid		Id thirdparty
4648
	 *	@return    	array|int				Array of invoices ('id'=>id, 'ref'=>ref, 'status'=>status, 'paymentornot'=>0/1)
4649
	 */
4650
	public function list_replacable_invoices($socid = 0)
4651
	{
4652
		// phpcs:enable
4653
		global $conf;
4654
4655
		$return = array();
4656
4657
		$sql = "SELECT f.rowid as rowid, f.ref, f.fk_statut as status, f.paye as paid,";
4658
		$sql .= " ff.rowid as rowidnext";
4659
		//$sql .= ", SUM(pf.amount) as alreadypaid";
4660
		$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
4661
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."paiement_facture as pf ON f.rowid = pf.fk_facture";
4662
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."facture as ff ON f.rowid = ff.fk_facture_source";
4663
		$sql .= " WHERE (f.fk_statut = ".self::STATUS_VALIDATED." OR (f.fk_statut = ".self::STATUS_ABANDONED." AND f.close_code = '".self::CLOSECODE_ABANDONED."'))";
4664
		$sql .= " AND f.entity IN (".getEntity('invoice').")";
4665
		$sql .= " AND f.paye = 0"; // Not paid completely
4666
		$sql .= " AND pf.fk_paiement IS NULL"; // No payment already done
4667
		$sql .= " AND ff.fk_statut IS NULL"; // Return true if it is not a replacement invoice
4668
		if ($socid > 0) {
4669
			$sql .= " AND f.fk_soc = ".((int) $socid);
4670
		}
4671
		//$sql .= " GROUP BY f.rowid, f.ref, f.fk_statut, f.paye, ff.rowid";
4672
		$sql .= " ORDER BY f.ref";
4673
4674
		dol_syslog(get_class($this)."::list_replacable_invoices", LOG_DEBUG);
4675
		$resql = $this->db->query($sql);
4676
		if ($resql) {
4677
			while ($obj = $this->db->fetch_object($resql)) {
4678
				$return[$obj->rowid] = array(
4679
					'id' => $obj->rowid,
4680
					'ref' => $obj->ref,
4681
					'status' => $obj->status,
4682
					'paid' => $obj->paid,
4683
					'alreadypaid' => 0
4684
				);
4685
			}
4686
			//print_r($return);
4687
			return $return;
4688
		} else {
4689
			$this->error = $this->db->error();
4690
			return -1;
4691
		}
4692
	}
4693
4694
4695
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4696
	/**
4697
	 *	Return list of invoices qualified to be corrected by a credit note.
4698
	 *	Invoices matching the following rules are returned:
4699
	 *	(validated + payment on process) or classified (payed completely or payed partiely) + not already replaced + not already a credit note
4700
	 *
4701
	 *	@param		int			$socid		Id thirdparty
4702
	 *	@return    	array|int				Array of invoices ($id => array('ref'=>,'paymentornot'=>,'status'=>,'paye'=>)
4703
	 */
4704
	public function list_qualified_avoir_invoices($socid = 0)
4705
	{
4706
		// phpcs:enable
4707
		global $conf;
4708
4709
		$return = array();
4710
4711
4712
		$sql = "SELECT f.rowid as rowid, f.ref, f.fk_statut, f.type, f.paye, pf.fk_paiement";
4713
		$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
4714
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."paiement_facture as pf ON f.rowid = pf.fk_facture";
4715
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."facture as ff ON (f.rowid = ff.fk_facture_source AND ff.type=".self::TYPE_REPLACEMENT.")";
4716
		$sql .= " WHERE f.entity IN (".getEntity('invoice').")";
4717
		$sql .= " AND f.fk_statut in (".self::STATUS_VALIDATED.",".self::STATUS_CLOSED.")";
4718
		//  $sql.= " WHERE f.fk_statut >= 1";
4719
		//	$sql.= " AND (f.paye = 1";				// Classee payee completement
4720
		//	$sql.= " OR f.close_code IS NOT NULL)";	// Classee payee partiellement
4721
		$sql .= " AND ff.type IS NULL"; // Renvoi vrai si pas facture de remplacement
4722
		$sql .= " AND f.type <> ".self::TYPE_CREDIT_NOTE; // Exclude credit note invoices from selection
4723
4724
		if (!empty($conf->global->INVOICE_USE_SITUATION_CREDIT_NOTE)) {
4725
			// Keep invoices that are not situation invoices or that are the last in serie if it is a situation invoice
4726
			$sql .= " AND (f.type <> ".self::TYPE_SITUATION." OR f.rowid IN ";
4727
			$sql .= '(SELECT MAX(fs.rowid)'; // This select returns several ID becasue of the group by later
4728
			$sql .= " FROM ".MAIN_DB_PREFIX."facture as fs";
4729
			$sql .= " WHERE fs.entity IN (".getEntity('invoice').")";
4730
			$sql .= " AND fs.type = ".self::TYPE_SITUATION;
4731
			$sql .= " AND fs.fk_statut IN (".self::STATUS_VALIDATED.",".self::STATUS_CLOSED.")";
4732
			if ($socid > 0) {
4733
				$sql .= " AND fs.fk_soc = ".((int) $socid);
4734
			}
4735
			$sql .= " GROUP BY fs.situation_cycle_ref)"; // For each situation_cycle_ref, we take the higher rowid
4736
			$sql .= ")";
4737
		} else {
4738
			$sql .= " AND f.type <> ".self::TYPE_SITUATION; // Keep invoices that are not situation invoices
4739
		}
4740
4741
		if ($socid > 0) {
4742
			$sql .= " AND f.fk_soc = ".((int) $socid);
4743
		}
4744
		$sql .= " ORDER BY f.ref";
4745
4746
		dol_syslog(get_class($this)."::list_qualified_avoir_invoices", LOG_DEBUG);
4747
		$resql = $this->db->query($sql);
4748
		if ($resql) {
4749
			while ($obj = $this->db->fetch_object($resql)) {
4750
				$qualified = 0;
4751
				if ($obj->fk_statut == self::STATUS_VALIDATED) {
4752
					$qualified = 1;
4753
				}
4754
				if ($obj->fk_statut == self::STATUS_CLOSED) {
4755
					$qualified = 1;
4756
				}
4757
				if ($qualified) {
4758
					//$ref=$obj->ref;
4759
					$paymentornot = ($obj->fk_paiement ? 1 : 0);
4760
					$return[$obj->rowid] = array('ref'=>$obj->ref, 'status'=>$obj->fk_statut, 'type'=>$obj->type, 'paye'=>$obj->paye, 'paymentornot'=>$paymentornot);
4761
				}
4762
			}
4763
4764
			return $return;
4765
		} else {
4766
			$this->error = $this->db->error();
4767
			return -1;
4768
		}
4769
	}
4770
4771
4772
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4773
	/**
4774
	 *	Load indicators for dashboard (this->nbtodo and this->nbtodolate)
4775
	 *
4776
	 *	@param  User					$user    	Object user
4777
	 *	@return WorkboardResponse|int 				<0 if KO, WorkboardResponse if OK
4778
	 */
4779
	public function load_board($user)
4780
	{
4781
		// phpcs:enable
4782
		global $conf, $langs;
4783
4784
		$clause = " WHERE";
4785
4786
		$sql = "SELECT f.rowid, f.date_lim_reglement as datefin, f.fk_statut, f.total_ht";
4787
		$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
4788
		if (empty($user->rights->societe->client->voir) && !$user->socid) {
4789
			$sql .= " JOIN ".MAIN_DB_PREFIX."societe_commerciaux as sc ON f.fk_soc = sc.fk_soc";
4790
			$sql .= " WHERE sc.fk_user = ".((int) $user->id);
4791
			$clause = " AND";
4792
		}
4793
		$sql .= $clause." f.paye=0";
4794
		$sql .= " AND f.entity IN (".getEntity('invoice').")";
4795
		$sql .= " AND f.fk_statut = ".self::STATUS_VALIDATED;
4796
		if ($user->socid) {
4797
			$sql .= " AND f.fk_soc = ".((int) $user->socid);
4798
		}
4799
4800
		$resql = $this->db->query($sql);
4801
		if ($resql) {
4802
			$langs->load("bills");
4803
			$now = dol_now();
4804
4805
			$response = new WorkboardResponse();
4806
			$response->warning_delay = $conf->facture->client->warning_delay / 60 / 60 / 24;
4807
			$response->label = $langs->trans("CustomerBillsUnpaid");
4808
			$response->labelShort = $langs->trans("Unpaid");
4809
			$response->url = DOL_URL_ROOT.'/compta/facture/list.php?search_status=1&mainmenu=billing&leftmenu=customers_bills';
4810
			$response->img = img_object('', "bill");
4811
4812
			$generic_facture = new Facture($this->db);
4813
4814
			while ($obj = $this->db->fetch_object($resql)) {
4815
				$generic_facture->date_lim_reglement = $this->db->jdate($obj->datefin);
4816
				$generic_facture->statut = $obj->fk_statut;
4817
4818
				$response->nbtodo++;
4819
				$response->total += $obj->total_ht;
4820
4821
				if ($generic_facture->hasDelay()) {
4822
					$response->nbtodolate++;
4823
					$response->url_late = DOL_URL_ROOT.'/compta/facture/list.php?search_option=late&mainmenu=billing&leftmenu=customers_bills';
4824
				}
4825
			}
4826
4827
			$this->db->free($resql);
4828
			return $response;
4829
		} else {
4830
			dol_print_error($this->db);
4831
			$this->error = $this->db->error();
4832
			return -1;
4833
		}
4834
	}
4835
4836
4837
	/* gestion des contacts d'une facture */
4838
4839
	/**
4840
	 *	Retourne id des contacts clients de facturation
4841
	 *
4842
	 *	@return     array       Liste des id contacts facturation
4843
	 */
4844
	public function getIdBillingContact()
4845
	{
4846
		return $this->getIdContact('external', 'BILLING');
4847
	}
4848
4849
	/**
4850
	 *	Retourne id des contacts clients de livraison
4851
	 *
4852
	 *	@return     array       Liste des id contacts livraison
4853
	 */
4854
	public function getIdShippingContact()
4855
	{
4856
		return $this->getIdContact('external', 'SHIPPING');
4857
	}
4858
4859
4860
	/**
4861
	 *  Initialise an instance with random values.
4862
	 *  Used to build previews or test instances.
4863
	 *	id must be 0 if object instance is a specimen.
4864
	 *
4865
	 *	@param	string		$option		''=Create a specimen invoice with lines, 'nolines'=No lines
4866
	 *  @return	void
4867
	 */
4868
	public function initAsSpecimen($option = '')
4869
	{
4870
		global $conf, $langs, $user;
4871
4872
		$now = dol_now();
4873
		$arraynow = dol_getdate($now);
4874
		$nownotime = dol_mktime(0, 0, 0, $arraynow['mon'], $arraynow['mday'], $arraynow['year']);
4875
4876
		// Load array of products prodids
4877
		$num_prods = 0;
4878
		$prodids = array();
4879
		$sql = "SELECT rowid";
4880
		$sql .= " FROM ".MAIN_DB_PREFIX."product";
4881
		$sql .= " WHERE entity IN (".getEntity('product').")";
4882
		$sql .= $this->db->plimit(100);
4883
4884
		$resql = $this->db->query($sql);
4885
		if ($resql) {
4886
			$num_prods = $this->db->num_rows($resql);
4887
			$i = 0;
4888
			while ($i < $num_prods) {
4889
				$i++;
4890
				$row = $this->db->fetch_row($resql);
4891
				$prodids[$i] = $row[0];
4892
			}
4893
		}
4894
		//Avoid php warning Warning: mt_rand(): max(0) is smaller than min(1) when no product exists
4895
		if (empty($num_prods)) {
4896
			$num_prods = 1;
4897
		}
4898
4899
		// Initialize parameters
4900
		$this->id = 0;
4901
		$this->entity = 1;
4902
		$this->ref = 'SPECIMEN';
4903
		$this->specimen = 1;
4904
		$this->socid = 1;
4905
		$this->date = $nownotime;
4906
		$this->date_lim_reglement = $nownotime + 3600 * 24 * 30;
4907
		$this->cond_reglement_id   = 1;
4908
		$this->cond_reglement_code = 'RECEP';
4909
		$this->date_lim_reglement = $this->calculate_date_lim_reglement();
4910
		$this->mode_reglement_id   = 0; // Not forced to show payment mode CHQ + VIR
4911
		$this->mode_reglement_code = ''; // Not forced to show payment mode CHQ + VIR
4912
4913
		$this->note_public = 'This is a comment (public)';
4914
		$this->note_private = 'This is a comment (private)';
4915
		$this->note = 'This is a comment (private)';
4916
4917
		$this->fk_user_author = $user->id;
4918
4919
		$this->multicurrency_tx = 1;
4920
		$this->multicurrency_code = $conf->currency;
4921
4922
		$this->fk_incoterms = 0;
4923
		$this->location_incoterms = '';
4924
4925
		if (empty($option) || $option != 'nolines') {
4926
			// Lines
4927
			$nbp = 5;
4928
			$xnbp = 0;
4929
			while ($xnbp < $nbp) {
4930
				$line = new FactureLigne($this->db);
4931
				$line->desc = $langs->trans("Description")." ".$xnbp;
4932
				$line->qty = 1;
4933
				$line->subprice = 100;
4934
				$line->tva_tx = 19.6;
4935
				$line->localtax1_tx = 0;
4936
				$line->localtax2_tx = 0;
4937
				$line->remise_percent = 0;
4938
				if ($xnbp == 1) {        // Qty is negative (product line)
4939
					$prodid = mt_rand(1, $num_prods);
4940
					$line->fk_product = $prodids[$prodid];
4941
					$line->qty = -1;
4942
					$line->total_ht = -100;
4943
					$line->total_ttc = -119.6;
4944
					$line->total_tva = -19.6;
4945
					$line->multicurrency_total_ht = -200;
4946
					$line->multicurrency_total_ttc = -239.2;
4947
					$line->multicurrency_total_tva = -39.2;
4948
				} elseif ($xnbp == 2) {    // UP is negative (free line)
4949
					$line->subprice = -100;
4950
					$line->total_ht = -100;
4951
					$line->total_ttc = -119.6;
4952
					$line->total_tva = -19.6;
4953
					$line->remise_percent = 0;
4954
					$line->multicurrency_total_ht = -200;
4955
					$line->multicurrency_total_ttc = -239.2;
4956
					$line->multicurrency_total_tva = -39.2;
4957
				} elseif ($xnbp == 3) {    // Discount is 50% (product line)
4958
					$prodid = mt_rand(1, $num_prods);
4959
					$line->fk_product = $prodids[$prodid];
4960
					$line->total_ht = 50;
4961
					$line->total_ttc = 59.8;
4962
					$line->total_tva = 9.8;
4963
					$line->multicurrency_total_ht = 100;
4964
					$line->multicurrency_total_ttc = 119.6;
4965
					$line->multicurrency_total_tva = 19.6;
4966
					$line->remise_percent = 50;
4967
				} else // (product line)
4968
				{
4969
					$prodid = mt_rand(1, $num_prods);
4970
					$line->fk_product = $prodids[$prodid];
4971
					$line->total_ht = 100;
4972
					$line->total_ttc = 119.6;
4973
					$line->total_tva = 19.6;
4974
					$line->multicurrency_total_ht = 200;
4975
					$line->multicurrency_total_ttc = 239.2;
4976
					$line->multicurrency_total_tva = 39.2;
4977
					$line->remise_percent = 0;
4978
				}
4979
4980
				$this->lines[$xnbp] = $line;
4981
4982
4983
				$this->total_ht       += $line->total_ht;
4984
				$this->total_tva      += $line->total_tva;
4985
				$this->total_ttc      += $line->total_ttc;
4986
4987
				$this->multicurrency_total_ht       += $line->multicurrency_total_ht;
4988
				$this->multicurrency_total_tva      += $line->multicurrency_total_tva;
4989
				$this->multicurrency_total_ttc      += $line->multicurrency_total_ttc;
4990
4991
				$xnbp++;
4992
			}
4993
			$this->revenuestamp = 0;
4994
4995
			// Add a line "offered"
4996
			$line = new FactureLigne($this->db);
4997
			$line->desc = $langs->trans("Description")." (offered line)";
4998
			$line->qty = 1;
4999
			$line->subprice = 100;
5000
			$line->tva_tx = 19.6;
5001
			$line->localtax1_tx = 0;
5002
			$line->localtax2_tx = 0;
5003
			$line->remise_percent = 100;
5004
			$line->total_ht = 0;
5005
			$line->total_ttc = 0; // 90 * 1.196
5006
			$line->total_tva = 0;
5007
			$line->multicurrency_total_ht = 0;
5008
			$line->multicurrency_total_ttc = 0;
5009
			$line->multicurrency_total_tva = 0;
5010
			$prodid = mt_rand(1, $num_prods);
5011
			$line->fk_product = $prodids[$prodid];
5012
5013
			$this->lines[$xnbp] = $line;
5014
			$xnbp++;
5015
		}
5016
	}
5017
5018
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5019
	/**
5020
	 *      Load indicators for dashboard (this->nbtodo and this->nbtodolate)
5021
	 *
5022
	 *      @return         int     <0 if KO, >0 if OK
5023
	 */
5024
	public function load_state_board()
5025
	{
5026
		// phpcs:enable
5027
		global $conf, $user;
5028
5029
		$this->nb = array();
5030
5031
		$clause = "WHERE";
5032
5033
		$sql = "SELECT count(f.rowid) as nb";
5034
		$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
5035
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON f.fk_soc = s.rowid";
5036
		if (empty($user->rights->societe->client->voir) && !$user->socid) {
5037
			$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe_commerciaux as sc ON s.rowid = sc.fk_soc";
5038
			$sql .= " WHERE sc.fk_user = ".((int) $user->id);
5039
			$clause = "AND";
5040
		}
5041
		$sql .= " ".$clause." f.entity IN (".getEntity('invoice').")";
5042
5043
		$resql = $this->db->query($sql);
5044
		if ($resql) {
5045
			while ($obj = $this->db->fetch_object($resql)) {
5046
				$this->nb["invoices"] = $obj->nb;
5047
			}
5048
			$this->db->free($resql);
5049
			return 1;
5050
		} else {
5051
			dol_print_error($this->db);
5052
			$this->error = $this->db->error();
5053
			return -1;
5054
		}
5055
	}
5056
5057
	/**
5058
	 * 	Create an array of invoice lines
5059
	 *
5060
	 * 	@return int		>0 if OK, <0 if KO
5061
	 */
5062
	public function getLinesArray()
5063
	{
5064
		return $this->fetch_lines();
5065
	}
5066
5067
	/**
5068
	 *  Create a document onto disk according to template module.
5069
	 *
5070
	 *	@param	string		$modele			Generator to use. Caller must set it to obj->model_pdf or GETPOST('model','alpha') for example.
5071
	 *	@param	Translate	$outputlangs	Object lang to use for translation
5072
	 *  @param  int			$hidedetails    Hide details of lines
5073
	 *  @param  int			$hidedesc       Hide description
5074
	 *  @param  int			$hideref        Hide ref
5075
	 *  @param  null|array  $moreparams     Array to provide more information
5076
	 *	@return int        					<0 if KO, >0 if OK
5077
	 */
5078
	public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
5079
	{
5080
		global $conf, $langs;
5081
5082
		$outputlangs->loadLangs(array("bills", "products"));
5083
5084
		if (!dol_strlen($modele)) {
5085
			$modele = 'crabe';
5086
			$thisTypeConfName = 'FACTURE_ADDON_PDF_'.$this->type;
5087
5088
			if (!empty($this->model_pdf)) {
5089
				$modele = $this->model_pdf;
5090
			} elseif (!empty($this->modelpdf)) {	// deprecated
5091
				$modele = $this->modelpdf;
5092
			} elseif (!empty($conf->global->$thisTypeConfName)) {
5093
				$modele = $conf->global->$thisTypeConfName;
5094
			} elseif (!empty($conf->global->FACTURE_ADDON_PDF)) {
5095
				$modele = $conf->global->FACTURE_ADDON_PDF;
5096
			}
5097
		}
5098
5099
		$modelpath = "core/modules/facture/doc/";
5100
5101
		return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
5102
	}
5103
5104
	/**
5105
	 * Gets the smallest reference available for a new cycle
5106
	 *
5107
	 * @return int >= 1 if OK, -1 if error
5108
	 */
5109
	public function newCycle()
5110
	{
5111
		$sql = 'SELECT max(situation_cycle_ref) FROM '.MAIN_DB_PREFIX.'facture as f';
5112
		$sql .= " WHERE f.entity IN (".getEntity('invoice', 0).")";
5113
		$resql = $this->db->query($sql);
5114
		if ($resql) {
5115
			if ($this->db->num_rows($resql) > 0) {
5116
				$res = $this->db->fetch_array($resql);
5117
				$ref = $res['max(situation_cycle_ref)'];
5118
				$ref++;
5119
			} else {
5120
				$ref = 1;
5121
			}
5122
			$this->db->free($resql);
5123
			return $ref;
5124
		} else {
5125
			$this->error = $this->db->lasterror();
5126
			dol_syslog("Error sql=".$sql.", error=".$this->error, LOG_ERR);
5127
			return -1;
5128
		}
5129
	}
5130
5131
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5132
	/**
5133
	 * Checks if the invoice is the first of a cycle
5134
	 *
5135
	 * @return boolean
5136
	 */
5137
	public function is_first()
5138
	{
5139
		// phpcs:enable
5140
		return ($this->situation_counter == 1);
5141
	}
5142
5143
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5144
	/**
5145
	 * Returns an array containing the previous situations as Facture objects
5146
	 *
5147
	 * @return mixed -1 if error, array of previous situations
5148
	 */
5149
	public function get_prev_sits()
5150
	{
5151
		// phpcs:enable
5152
		global $conf;
5153
5154
		$sql = 'SELECT rowid FROM '.MAIN_DB_PREFIX.'facture';
5155
		$sql .= ' WHERE situation_cycle_ref = '.((int) $this->situation_cycle_ref);
5156
		$sql .= ' AND situation_counter < '.((int) $this->situation_counter);
5157
		$sql .= ' AND entity = '.($this->entity > 0 ? $this->entity : $conf->entity);
5158
		$resql = $this->db->query($sql);
5159
		$res = array();
5160
		if ($resql && $this->db->num_rows($resql) > 0) {
5161
			while ($row = $this->db->fetch_object($resql)) {
5162
				$id = $row->rowid;
5163
				$situation = new Facture($this->db);
5164
				$situation->fetch($id);
5165
				$res[] = $situation;
5166
			}
5167
		} else {
5168
			$this->error = $this->db->error();
5169
			dol_syslog("Error sql=".$sql.", error=".$this->error, LOG_ERR);
5170
			return -1;
5171
		}
5172
5173
		return $res;
5174
	}
5175
5176
	/**
5177
	 * Sets the invoice as a final situation
5178
	 *
5179
	 *  @param  	User	$user    	Object user
5180
	 *  @param     	int		$notrigger	1=Does not execute triggers, 0= execute triggers
5181
	 *	@return		int 				<0 if KO, >0 if OK
5182
	 */
5183
	public function setFinal(User $user, $notrigger = 0)
5184
	{
5185
		$error = 0;
5186
5187
		$this->db->begin();
5188
5189
		$sql = 'UPDATE '.MAIN_DB_PREFIX.'facture SET situation_final = '.((int) $this->situation_final).' WHERE rowid = '.((int) $this->id);
5190
5191
		dol_syslog(__METHOD__, LOG_DEBUG);
5192
		$resql = $this->db->query($sql);
5193
		if (!$resql) {
5194
			$this->errors[] = $this->db->error();
5195
			$error++;
5196
		}
5197
5198
		if (!$notrigger && empty($error)) {
5199
			// Call trigger
5200
			$result = $this->call_trigger('BILL_MODIFY', $user);
5201
			if ($result < 0) {
5202
				$error++;
5203
			}
5204
			// End call triggers
5205
		}
5206
5207
		if (!$error) {
5208
			$this->db->commit();
5209
			return 1;
5210
		} else {
5211
			foreach ($this->errors as $errmsg) {
5212
				dol_syslog(__METHOD__.' Error: '.$errmsg, LOG_ERR);
5213
				$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
5214
			}
5215
			$this->db->rollback();
5216
			return -1 * $error;
5217
		}
5218
	}
5219
5220
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5221
	/**
5222
	 * Checks if the invoice is the last in its cycle
5223
	 *
5224
	 * @return bool Last of the cycle status
5225
	 */
5226
	public function is_last_in_cycle()
5227
	{
5228
		// phpcs:enable
5229
		global $conf;
5230
5231
		if (!empty($this->situation_cycle_ref)) {
5232
			// No point in testing anything if we're not inside a cycle
5233
			$sql = 'SELECT max(situation_counter) FROM '.MAIN_DB_PREFIX.'facture';
5234
			$sql .= ' WHERE situation_cycle_ref = '.((int) $this->situation_cycle_ref);
5235
			$sql .= ' AND entity = '.($this->entity > 0 ? $this->entity : $conf->entity);
5236
			$resql = $this->db->query($sql);
5237
5238
			if ($resql && $this->db->num_rows($resql) > 0) {
5239
				$res = $this->db->fetch_array($resql);
5240
				$last = $res['max(situation_counter)'];
5241
				return ($last == $this->situation_counter);
5242
			} else {
5243
				$this->error = $this->db->lasterror();
5244
				dol_syslog(get_class($this)."::select Error ".$this->error, LOG_ERR);
5245
				return false;
5246
			}
5247
		} else {
5248
			return true;
5249
		}
5250
	}
5251
5252
	/**
5253
	 * Function used to replace a thirdparty id with another one.
5254
	 *
5255
	 * @param 	DoliDB 	$dbs 		Database handler, because function is static we name it $dbs not $db to avoid breaking coding test
5256
	 * @param 	int 	$origin_id 	Old thirdparty id
5257
	 * @param 	int 	$dest_id 	New thirdparty id
5258
	 * @return 	bool
5259
	 */
5260
	public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
5261
	{
5262
		$tables = array(
5263
			'facture'
5264
		);
5265
5266
		return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
5267
	}
5268
5269
	/**
5270
	 * Function used to replace a product id with another one.
5271
	 *
5272
	 * @param DoliDB $db Database handler
5273
	 * @param int $origin_id Old product id
5274
	 * @param int $dest_id New product id
5275
	 * @return bool
5276
	 */
5277
	public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
5278
	{
5279
		$tables = array(
5280
			'facturedet'
5281
		);
5282
5283
		return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
5284
	}
5285
5286
	/**
5287
	 * Is the customer invoice delayed?
5288
	 *
5289
	 * @return bool
5290
	 */
5291
	public function hasDelay()
5292
	{
5293
		global $conf;
5294
5295
		$now = dol_now();
5296
5297
		// Paid invoices have status STATUS_CLOSED
5298
		if ($this->statut != Facture::STATUS_VALIDATED) {
5299
			return false;
5300
		}
5301
5302
		$hasDelay = $this->date_lim_reglement < ($now - $conf->facture->client->warning_delay);
5303
		if ($hasDelay && !empty($this->retained_warranty) && !empty($this->retained_warranty_date_limit)) {
5304
			$totalpaid = $this->getSommePaiement();
5305
			$totalpaid = floatval($totalpaid);
5306
			$RetainedWarrantyAmount = $this->getRetainedWarrantyAmount();
5307
			if ($totalpaid >= 0 && $RetainedWarrantyAmount >= 0) {
5308
				if (($totalpaid < $this->total_ttc - $RetainedWarrantyAmount) && $this->date_lim_reglement < ($now - $conf->facture->client->warning_delay)) {
5309
					$hasDelay = 1;
5310
				} elseif ($totalpaid < $this->total_ttc && $this->retained_warranty_date_limit < ($now - $conf->facture->client->warning_delay)) {
5311
					$hasDelay = 1;
5312
				} else {
5313
					$hasDelay = 0;
5314
				}
5315
			}
5316
		}
5317
5318
		return $hasDelay;
5319
	}
5320
5321
	/**
5322
	 * Currently used for documents generation : to know if retained warranty need to be displayed
5323
	 * @return bool
5324
	 */
5325
	public function displayRetainedWarranty()
5326
	{
5327
		global $conf;
5328
5329
		// TODO : add a flag on invoices to store this conf : INVOICE_RETAINED_WARRANTY_LIMITED_TO_FINAL_SITUATION
5330
5331
		// note : we don't need to test INVOICE_USE_RETAINED_WARRANTY because if $this->retained_warranty is not empty it's because it was set when this conf was active
5332
5333
		$displayWarranty = false;
5334
		if (!empty($this->retained_warranty)) {
5335
			$displayWarranty = true;
5336
5337
			if ($this->type == Facture::TYPE_SITUATION && !empty($conf->global->INVOICE_RETAINED_WARRANTY_LIMITED_TO_FINAL_SITUATION)) {
5338
				// Check if this situation invoice is 100% for real
5339
				$displayWarranty = false;
5340
				if (!empty($this->situation_final)) {
5341
					$displayWarranty = true;
5342
				} elseif (!empty($this->lines) && $this->status == Facture::STATUS_DRAFT) {
5343
					// $object->situation_final need validation to be done so this test is need for draft
5344
					$displayWarranty = true;
5345
5346
					foreach ($this->lines as $i => $line) {
5347
						if ($line->product_type < 2 && $line->situation_percent < 100) {
5348
							$displayWarranty = false;
5349
							break;
5350
						}
5351
					}
5352
				}
5353
			}
5354
		}
5355
5356
		return $displayWarranty;
5357
	}
5358
5359
	/**
5360
	 * @param	int			$rounding		Minimum number of decimal to show. If 0, no change, if -1, we use min($conf->global->MAIN_MAX_DECIMALS_UNIT,$conf->global->MAIN_MAX_DECIMALS_TOT)
5361
	 * @return float or -1 if not available
5362
	 */
5363
	public function getRetainedWarrantyAmount($rounding = -1)
5364
	{
5365
		global $conf;
5366
		if (empty($this->retained_warranty)) {
5367
			return -1;
5368
		}
5369
5370
		$retainedWarrantyAmount = 0;
5371
5372
		// Billed - retained warranty
5373
		if ($this->type == Facture::TYPE_SITUATION && !empty($conf->global->INVOICE_RETAINED_WARRANTY_LIMITED_TO_FINAL_SITUATION)) {
5374
			$displayWarranty = true;
5375
			// Check if this situation invoice is 100% for real
5376
			if (!empty($this->lines)) {
5377
				foreach ($this->lines as $i => $line) {
5378
					if ($line->product_type < 2 && $line->situation_percent < 100) {
5379
						$displayWarranty = false;
5380
						break;
5381
					}
5382
				}
5383
			}
5384
5385
			if ($displayWarranty && !empty($this->situation_final)) {
5386
				$this->fetchPreviousNextSituationInvoice();
5387
				$TPreviousIncoice = $this->tab_previous_situation_invoice;
5388
5389
				$total2BillWT = 0;
5390
				foreach ($TPreviousIncoice as &$fac) {
5391
					$total2BillWT += $fac->total_ttc;
5392
				}
5393
				$total2BillWT += $this->total_ttc;
5394
5395
				$retainedWarrantyAmount = $total2BillWT * $this->retained_warranty / 100;
5396
			} else {
5397
				return -1;
5398
			}
5399
		} else {
5400
			// Because one day retained warranty could be used on standard invoices
5401
			$retainedWarrantyAmount = $this->total_ttc * $this->retained_warranty / 100;
5402
		}
5403
5404
		if ($rounding < 0) {
5405
			$rounding = min($conf->global->MAIN_MAX_DECIMALS_UNIT, $conf->global->MAIN_MAX_DECIMALS_TOT);
5406
		}
5407
5408
		if ($rounding > 0) {
5409
			return round($retainedWarrantyAmount, $rounding);
5410
		}
5411
5412
		return $retainedWarrantyAmount;
5413
	}
5414
5415
	/**
5416
	 *  Change the retained warranty
5417
	 *
5418
	 *  @param		float		$value		value of retained warranty
5419
	 *  @return		int				>0 if OK, <0 if KO
5420
	 */
5421
	public function setRetainedWarranty($value)
5422
	{
5423
		dol_syslog(get_class($this).'::setRetainedWarranty('.$value.')');
5424
5425
		if ($this->statut >= 0) {
5426
			$fieldname = 'retained_warranty';
5427
			$sql = 'UPDATE '.MAIN_DB_PREFIX.$this->table_element;
5428
			$sql .= " SET ".$fieldname." = ".((float) $value);
5429
			$sql .= ' WHERE rowid='.((int) $this->id);
5430
5431
			if ($this->db->query($sql)) {
5432
				$this->retained_warranty = floatval($value);
5433
				return 1;
5434
			} else {
5435
				dol_syslog(get_class($this).'::setRetainedWarranty Erreur '.$sql.' - '.$this->db->error());
5436
				$this->error = $this->db->error();
5437
				return -1;
5438
			}
5439
		} else {
5440
			dol_syslog(get_class($this).'::setRetainedWarranty, status of the object is incompatible');
5441
			$this->error = 'Status of the object is incompatible '.$this->statut;
5442
			return -2;
5443
		}
5444
	}
5445
5446
5447
	/**
5448
	 *  Change the retained_warranty_date_limit
5449
	 *
5450
	 *  @param		int		$timestamp		date limit of retained warranty in timestamp format
5451
	 *  @param		string	$dateYmd		date limit of retained warranty in Y m d format
5452
	 *  @return		int				>0 if OK, <0 if KO
5453
	 */
5454
	public function setRetainedWarrantyDateLimit($timestamp, $dateYmd = false)
5455
	{
5456
		if (!$timestamp && $dateYmd) {
5457
			$timestamp = $this->db->jdate($dateYmd);
5458
		}
5459
5460
5461
		dol_syslog(get_class($this).'::setRetainedWarrantyDateLimit('.$timestamp.')');
5462
		if ($this->statut >= 0) {
5463
			$fieldname = 'retained_warranty_date_limit';
5464
			$sql = 'UPDATE '.MAIN_DB_PREFIX.$this->table_element;
5465
			$sql .= " SET ".$fieldname." = ".(strval($timestamp) != '' ? "'".$this->db->idate($timestamp)."'" : 'null');
5466
			$sql .= ' WHERE rowid = '.((int) $this->id);
5467
5468
			if ($this->db->query($sql)) {
5469
				$this->retained_warranty_date_limit = $timestamp;
5470
				return 1;
5471
			} else {
5472
				dol_syslog(get_class($this).'::setRetainedWarrantyDateLimit Erreur '.$sql.' - '.$this->db->error());
5473
				$this->error = $this->db->error();
5474
				return -1;
5475
			}
5476
		} else {
5477
			dol_syslog(get_class($this).'::setRetainedWarrantyDateLimit, status of the object is incompatible');
5478
			$this->error = 'Status of the object is incompatible '.$this->statut;
5479
			return -2;
5480
		}
5481
	}
5482
5483
5484
	/**
5485
	 *  Send reminders by emails for ivoices that are due
5486
	 *  CAN BE A CRON TASK
5487
	 *
5488
	 *  @param	int			$nbdays				Delay after due date (or before if delay is negative)
5489
	 *  @param	string		$paymentmode		'' or 'all' by default (no filter), or 'LIQ', 'CHQ', CB', ...
5490
	 *  @param	int|string	$template			Name (or id) of email template (Must be a template of type 'facture_send')
5491
	 *  @param	string		$forcerecipient		Force email of recipient (for example to send the email to an accountant supervisor instead of the customer)
5492
	 *  @return int         					0 if OK, <>0 if KO (this function is used also by cron so only 0 is OK)
5493
	 */
5494
	public function sendEmailsRemindersOnInvoiceDueDate($nbdays = 0, $paymentmode = 'all', $template = '', $forcerecipient = '')
5495
	{
5496
		global $conf, $langs, $user;
5497
5498
		$error = 0;
5499
		$this->output = '';
5500
		$this->error = '';
5501
		$nbMailSend = 0;
5502
		$errorsMsg = array();
5503
5504
		$langs->load("bills");
5505
5506
		if (!isModEnabled('facture')) {	// Should not happen. If module disabled, cron job should not be visible.
5507
			$this->output .= $langs->trans('ModuleNotEnabled', $langs->transnoentitiesnoconv("Facture"));
5508
			return 0;
5509
		}
5510
		/*if (empty($conf->global->FACTURE_REMINDER_EMAIL)) {
5511
			$langs->load("bills");
5512
			$this->output .= $langs->trans('EventRemindersByEmailNotEnabled', $langs->transnoentitiesnoconv("Facture"));
5513
			return 0;
5514
		}
5515
		*/
5516
5517
		require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
5518
		require_once DOL_DOCUMENT_ROOT.'/core/class/html.formmail.class.php';
5519
		require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
5520
		$formmail = new FormMail($this->db);
5521
5522
		$now = dol_now();
5523
		$tmpidate = dol_get_first_hour(dol_time_plus_duree($now, $nbdays, 'd'), 'gmt');
5524
5525
		$tmpinvoice = new Facture($this->db);
5526
5527
		dol_syslog(__METHOD__, LOG_DEBUG);
5528
5529
		// Select all action comm reminder
5530
		$sql = "SELECT rowid as id FROM ".MAIN_DB_PREFIX."facture as f";
5531
		if (!empty($paymentmode) && $paymentmode != 'all') {
5532
			$sql .= ", ".MAIN_DB_PREFIX."c_paiement as cp";
5533
		}
5534
		$sql .= " WHERE f.paye = 0";
5535
		$sql .= " AND f.fk_statut = ".self::STATUS_VALIDATED;
5536
		$sql .= " AND f.date_lim_reglement = '".$this->db->idate($tmpidate, 'gmt')."'";
5537
		$sql .= " AND f.entity IN (".getEntity('facture', 0).")";	// One batch process only one company (no sharing)
5538
		if (!empty($paymentmode) && $paymentmode != 'all') {
5539
			$sql .= " AND f.fk_mode_reglement = cp.id AND cp.code = '".$this->db->escape($paymentmode)."'";
5540
		}
5541
		// TODO Add a filter to check there is no payment started yet
5542
		$sql .= $this->db->order("date_lim_reglement", "ASC");
5543
5544
		$resql = $this->db->query($sql);
5545
5546
		$stmpidate = dol_print_date($tmpidate, 'day', 'gmt');
5547
		$this->output .= $langs->transnoentitiesnoconv("SearchUnpaidInvoicesWithDueDate", $stmpidate);
5548
		if (!empty($paymentmode) && $paymentmode != 'all') {
5549
			$this->output .= ' ('.$langs->transnoentitiesnoconv("PaymentMode").' '.$paymentmode.')';
5550
		}
5551
		$this->output .= '<br>';
5552
5553
		if ($resql) {
5554
			while ($obj = $this->db->fetch_object($resql)) {
5555
				if (!$error) {
5556
					// Load event
5557
					$res = $tmpinvoice->fetch($obj->id);
5558
					if ($res > 0) {
5559
						$tmpinvoice->fetch_thirdparty();
5560
5561
						$outputlangs = new Translate('', $conf);
5562
						if ($tmpinvoice->thirdparty->default_lang) {
5563
							$outputlangs->setDefaultLang($tmpinvoice->thirdparty->default_lang);
5564
							$outputlangs->loadLangs(array("main", "bills"));
5565
						} else {
5566
							$outputlangs = $langs;
5567
						}
5568
5569
						// Select email template
5570
						$arraymessage = $formmail->getEMailTemplate($this->db, 'facture_send', $user, $outputlangs, (is_numeric($template) ? $template : 0), 1, (is_numeric($template) ? '' : $template));
5571
						if (is_numeric($arraymessage) && $arraymessage <= 0) {
5572
							$langs->load("errors");
5573
							$this->output .= $langs->trans('ErrorFailedToFindEmailTemplate', $template);
5574
							return 0;
5575
						}
5576
5577
						// PREPARE EMAIL
5578
						$errormesg = '';
5579
5580
						// Make substitution in email content
5581
						$substitutionarray = getCommonSubstitutionArray($outputlangs, 0, '', $tmpinvoice);
5582
5583
						complete_substitutions_array($substitutionarray, $outputlangs, $tmpinvoice);
5584
5585
						// Topic
5586
						$sendTopic = make_substitutions(empty($arraymessage->topic) ? $outputlangs->transnoentitiesnoconv('InformationMessage') : $arraymessage->topic, $substitutionarray, $outputlangs, 1);
5587
5588
						// Content
5589
						$content = $outputlangs->transnoentitiesnoconv($arraymessage->content);
5590
5591
						$sendContent = make_substitutions($content, $substitutionarray, $outputlangs, 1);
5592
5593
						// Recipient
5594
						$to = array();
5595
						if ($forcerecipient) {	// If a recipient was forced
5596
							$to = array($forcerecipient);
5597
						} else {
5598
							$res = $tmpinvoice->fetch_thirdparty();
5599
							$recipient = $tmpinvoice->thirdparty;
5600
							if ($res > 0) {
5601
								$tmparraycontact = $tmpinvoice->liste_contact(-1, 'external', 0, 'BILLING');
5602
								if (is_array($tmparraycontact) && count($tmparraycontact) > 0) {
5603
									foreach ($tmparraycontact as $data_email) {
5604
										if (!empty($data_email['email'])) {
5605
											$to[] = $tmpinvoice->thirdparty->contact_get_property($data_email['id'], 'email');
5606
										}
5607
									}
5608
								}
5609
								if (empty($to) && !empty($recipient->email)) {
5610
									$to[] = $recipient->email;
5611
								} else {
5612
									$errormesg = "Failed to send remind to thirdparty id=".$tmpinvoice->socid.". No email defined for user.";
5613
									$error++;
5614
								}
5615
							} else {
5616
								$errormesg = "Failed to load recipient with thirdparty id=".$tmpinvoice->socid;
5617
								$error++;
5618
							}
5619
						}
5620
5621
						// Sender
5622
						$from = getDolGlobalString('MAIN_MAIL_EMAIL_FROM');
5623
						if (!empty($arraymessage->email_from)) {	// If a sender is defined into template, we use it in priority
5624
							$from = $arraymessage->email_from;
5625
						}
5626
						if (empty($from)) {
5627
							$errormesg = "Failed to get sender into global setup MAIN_MAIL_EMAIL_FROM";
5628
							$error++;
5629
						}
5630
5631
						if (!$error && !empty($to)) {
5632
							$this->db->begin();
5633
5634
							$to = implode(',', $to);
5635
							if (!empty($arraymessage->email_to)) {	// If a recipient is defined into template, we add it
5636
								$to = $to.','.$arraymessage->email_to;
5637
							}
5638
5639
							// Errors Recipient
5640
							$errors_to = $conf->global->MAIN_MAIL_ERRORS_TO;
5641
5642
							$trackid = 'inv'.$tmpinvoice->id;
5643
							$sendcontext = 'standard';
5644
5645
							$email_tocc = '';
5646
							if (!empty($arraymessage->email_tocc)) {	// If a CC is defined into template, we use it
5647
								$email_tocc = $arraymessage->email_tocc;
5648
							}
5649
5650
							$email_tobcc = '';
5651
							if (!empty($arraymessage->email_tobcc)) {	// If a BCC is defined into template, we use it
5652
								$email_tobcc = $arraymessage->email_tobcc;
5653
							}
5654
5655
							// Mail Creation
5656
							$cMailFile = new CMailFile($sendTopic, $to, $from, $sendContent, array(), array(), array(), $email_tocc, $email_tobcc, 0, 1, $errors_to, '', $trackid, '', $sendcontext, '');
5657
5658
							// Sending Mail
5659
							if ($cMailFile->sendfile()) {
5660
								$nbMailSend++;
5661
5662
								// Add a line into event table
5663
								require_once DOL_DOCUMENT_ROOT.'/comm/action/class/actioncomm.class.php';
5664
5665
								// Insert record of emails sent
5666
								$actioncomm = new ActionComm($this->db);
5667
5668
								$actioncomm->type_code = 'AC_OTH_AUTO'; // Event insert into agenda automatically
5669
								$actioncomm->socid = $tmpinvoice->thirdparty->id; // To link to a company
5670
								$actioncomm->contact_id = 0;
5671
5672
								$actioncomm->code = 'AC_EMAIL';
5673
								$actioncomm->label = 'sendEmailsRemindersOnInvoiceDueDateOK';
5674
								$actioncomm->note_private = $sendContent;
5675
								$actioncomm->fk_project = $tmpinvoice->fk_project;
5676
								$actioncomm->datep = dol_now();
5677
								$actioncomm->datef = $actioncomm->datep;
5678
								$actioncomm->percentage = -1; // Not applicable
5679
								$actioncomm->authorid = $user->id; // User saving action
5680
								$actioncomm->userownerid = $user->id; // Owner of action
5681
								// Fields when action is an email (content should be added into note)
5682
								$actioncomm->email_msgid = $cMailFile->msgid;
5683
								$actioncomm->email_from = $from;
5684
								$actioncomm->email_sender = '';
5685
								$actioncomm->email_to = $to;
5686
								//$actioncomm->email_tocc = $sendtocc;
5687
								//$actioncomm->email_tobcc = $sendtobcc;
5688
								//$actioncomm->email_subject = $subject;
5689
								$actioncomm->errors_to = $errors_to;
5690
5691
								$actioncomm->elementtype = 'invoice';
5692
								$actioncomm->fk_element = $tmpinvoice->id;
5693
5694
								//$actioncomm->extraparams = $extraparams;
5695
5696
								$actioncomm->create($user);
5697
							} else {
5698
								$errormesg = $cMailFile->error.' : '.$to;
5699
								$error++;
5700
5701
								// Add a line into event table
5702
								require_once DOL_DOCUMENT_ROOT.'/comm/action/class/actioncomm.class.php';
5703
5704
								// Insert record of emails sent
5705
								$actioncomm = new ActionComm($this->db);
5706
5707
								$actioncomm->type_code = 'AC_OTH_AUTO'; // Event insert into agenda automatically
5708
								$actioncomm->socid = $tmpinvoice->thirdparty->id; // To link to a company
5709
								$actioncomm->contact_id = 0;
5710
5711
								$actioncomm->code = 'AC_EMAIL';
5712
								$actioncomm->label = 'sendEmailsRemindersOnInvoiceDueDateKO';
5713
								$actioncomm->note_private = $errormesg;
5714
								$actioncomm->fk_project = $tmpinvoice->fk_project;
5715
								$actioncomm->datep = dol_now();
5716
								$actioncomm->datef = $actioncomm->datep;
5717
								$actioncomm->percentage = -1; // Not applicable
5718
								$actioncomm->authorid = $user->id; // User saving action
5719
								$actioncomm->userownerid = $user->id; // Owner of action
5720
								// Fields when action is an email (content should be added into note)
5721
								$actioncomm->email_msgid = $cMailFile->msgid;
5722
								$actioncomm->email_from = $from;
5723
								$actioncomm->email_sender = '';
5724
								$actioncomm->email_to = $to;
5725
								//$actioncomm->email_tocc = $sendtocc;
5726
								//$actioncomm->email_tobcc = $sendtobcc;
5727
								//$actioncomm->email_subject = $subject;
5728
								$actioncomm->errors_to = $errors_to;
5729
5730
								//$actioncomm->extraparams = $extraparams;
5731
5732
								$actioncomm->create($user);
5733
							}
5734
5735
							$this->db->commit();	// We always commit
5736
						}
5737
5738
						if ($errormesg) {
5739
							$errorsMsg[] = $errormesg;
5740
						}
5741
					} else {
5742
						$errorsMsg[] = 'Failed to fetch record invoice with ID = '.$obj->id;
5743
						$error++;
5744
					}
5745
				}
5746
			}
5747
		} else {
5748
			$error++;
5749
		}
5750
5751
		if (!$error) {
5752
			$this->output .= 'Nb of emails sent : '.$nbMailSend;
5753
			return 0;
5754
		} else {
5755
			$this->error = 'Nb of emails sent : '.$nbMailSend.', '.(!empty($errorsMsg)) ? join(', ', $errorsMsg) : $error;
5756
			return $error;
5757
		}
5758
	}
5759
5760
	/**
5761
	 * See if current invoice date is posterior to the last invoice date among validated invoices of same type.
5762
	 *
5763
	 * @param 	boolean 	$allow_validated_drafts			return true if the invoice has been validated before returning to DRAFT state.
5764
	 * @return 	array										return array
5765
	 */
5766
	public function willBeLastOfSameType($allow_validated_drafts = false)
5767
	{
5768
		// get date of last validated invoices of same type
5769
		$sql  = "SELECT datef";
5770
		$sql .= " FROM ".MAIN_DB_PREFIX."facture";
5771
		$sql .= " WHERE type = " . (int) $this->type ;
5772
		$sql .= " AND date_valid IS NOT NULL";
5773
		$sql .= " ORDER BY datef DESC LIMIT 1";
5774
5775
		$result = $this->db->query($sql);
5776
		if ($result) {
5777
			// compare with current validation date
5778
			if ($this->db->num_rows($result)) {
5779
				$obj = $this->db->fetch_object($result);
5780
				$last_date = $this->db->jdate($obj->datef);
5781
				$invoice_date = $this->date;
5782
5783
				$is_last_of_same_type = $invoice_date >= $last_date;
5784
				if ($allow_validated_drafts) {
5785
					$is_last_of_same_type = $is_last_of_same_type || (!strpos($this->ref, 'PROV') && $this->status == self::STATUS_DRAFT);
5786
				}
5787
5788
				return array($is_last_of_same_type, $last_date);
5789
			} else {
5790
				// element is first of type to be validated
5791
				return array(true);
5792
			}
5793
		} else {
5794
			dol_print_error($this->db);
5795
		}
5796
5797
		return array();
5798
	}
5799
5800
	/**
5801
	 *	Return clicable link of object (with eventually picto)
5802
	 *
5803
	 *	@param      string	    $option                 Where point the link (0=> main card, 1,2 => shipment, 'nolink'=>No link)
5804
	 *  @param		array		$arraydata				Array of data
5805
	 *  @return		string								HTML Code for Kanban thumb.
5806
	 */
5807
	public function getKanbanView($option = '', $arraydata = null)
5808
	{
5809
		$return = '<div class="box-flex-item box-flex-grow-zero">';
5810
		$return .= '<div class="info-box info-box-sm">';
5811
		$return .= '<span class="info-box-icon bg-infobox-action">';
5812
		$return .= img_picto('', $this->picto);
5813
		//$return .= '<i class="fa fa-dol-action"></i>'; // Can be image
5814
		$return .= '</span>';
5815
		$return .= '<div class="info-box-content">';
5816
		$return .= '<span class="info-box-ref">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl(1) : $this->ref).'</span>';
5817
		if (property_exists($this, 'socid')) {
5818
			$return .= '<br><span class="info-box-label">'.$this->socid.'</span>';
5819
		}
5820
		if (property_exists($this, 'fk_user_author')) {
5821
			$return .= '<br><span class="info-box-label">'.$this->fk_user_author.'</span>';
5822
		}
5823
		if (method_exists($this, 'getLibStatut')) {
5824
			$return .= '<br><div class="info-box-status margintoponly">'.$this->getLibStatut(5).'</div>';
5825
		}
5826
		$return .= '</div>';
5827
		$return .= '</div>';
5828
		$return .= '</div>';
5829
		return $return;
5830
	}
5831
}
5832
5833
/**
5834
 *	Class to manage invoice lines.
5835
 *  Saved into database table llx_facturedet
5836
 */
5837
class FactureLigne extends CommonInvoiceLine
5838
{
5839
	/**
5840
	 * @var string ID to identify managed object
5841
	 */
5842
	public $element = 'facturedet';
5843
5844
	/**
5845
	 * @var string Name of table without prefix where object is stored
5846
	 */
5847
	public $table_element = 'facturedet';
5848
5849
	public $oldline;
5850
5851
	//! From llx_facturedet
5852
	//! Id facture
5853
	public $fk_facture;
5854
	//! Id parent line
5855
	public $fk_parent_line;
5856
5857
	//! Description ligne
5858
	public $desc;
5859
	public $ref_ext; // External reference of the line
5860
5861
	public $localtax1_type; // Local tax 1 type
5862
	public $localtax2_type; // Local tax 2 type
5863
	public $fk_remise_except; // Link to line into llx_remise_except
5864
	public $rang = 0;
5865
5866
	public $fk_fournprice;
5867
	public $pa_ht;
5868
	public $marge_tx;
5869
	public $marque_tx;
5870
5871
	public $remise_percent;
5872
5873
	public $special_code; // Liste d'options non cumulabels:
5874
	// 1: frais de port
5875
	// 2: ecotaxe
5876
	// 3: ??
5877
5878
	public $origin;
5879
	public $origin_id;
5880
5881
	public $fk_code_ventilation = 0;
5882
5883
	public $date_start;
5884
	public $date_end;
5885
5886
	public $skip_update_total; // Skip update price total for special lines
5887
5888
	/**
5889
	 * @var int Situation advance percentage
5890
	 */
5891
	public $situation_percent;
5892
5893
	/**
5894
	 * @var int Previous situation line id reference
5895
	 */
5896
	public $fk_prev_id;
5897
5898
	// Multicurrency
5899
	public $fk_multicurrency;
5900
	public $multicurrency_code;
5901
	public $multicurrency_subprice;
5902
	public $multicurrency_total_ht;
5903
	public $multicurrency_total_tva;
5904
	public $multicurrency_total_ttc;
5905
5906
	/**
5907
	 *	Load invoice line from database
5908
	 *
5909
	 *	@param	int		$rowid      id of invoice line to get
5910
	 *	@return	int					<0 if KO, >0 if OK
5911
	 */
5912
	public function fetch($rowid)
5913
	{
5914
		$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,';
5915
		$sql .= ' fd.localtax1_tx, fd. localtax2_tx, fd.remise, fd.remise_percent, fd.fk_remise_except, fd.subprice, fd.ref_ext,';
5916
		$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,';
5917
		$sql .= ' fd.info_bits, fd.special_code, fd.total_ht, fd.total_tva, fd.total_ttc, fd.total_localtax1, fd.total_localtax2, fd.rang,';
5918
		$sql .= ' fd.fk_code_ventilation,';
5919
		$sql .= ' fd.fk_unit, fd.fk_user_author, fd.fk_user_modif,';
5920
		$sql .= ' fd.situation_percent, fd.fk_prev_id,';
5921
		$sql .= ' fd.multicurrency_subprice,';
5922
		$sql .= ' fd.multicurrency_total_ht,';
5923
		$sql .= ' fd.multicurrency_total_tva,';
5924
		$sql .= ' fd.multicurrency_total_ttc,';
5925
		$sql .= ' p.ref as product_ref, p.label as product_label, p.description as product_desc';
5926
		$sql .= ' FROM '.MAIN_DB_PREFIX.'facturedet as fd';
5927
		$sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'product as p ON fd.fk_product = p.rowid';
5928
		$sql .= ' WHERE fd.rowid = '.((int) $rowid);
5929
5930
		$result = $this->db->query($sql);
5931
		if ($result) {
5932
			$objp = $this->db->fetch_object($result);
5933
5934
			if (!$objp) {
5935
				$this->error = 'InvoiceLine with id '. $rowid .' not found sql='.$sql;
5936
				return 0;
5937
			}
5938
5939
			$this->rowid = $objp->rowid;
5940
			$this->id = $objp->rowid;
5941
			$this->fk_facture = $objp->fk_facture;
5942
			$this->fk_parent_line = $objp->fk_parent_line;
5943
			$this->label				= $objp->custom_label;
5944
			$this->desc					= $objp->description;
5945
			$this->qty = $objp->qty;
5946
			$this->subprice = $objp->subprice;
5947
			$this->ref_ext = $objp->ref_ext;
5948
			$this->vat_src_code = $objp->vat_src_code;
5949
			$this->tva_tx = $objp->tva_tx;
5950
			$this->localtax1_tx			= $objp->localtax1_tx;
5951
			$this->localtax2_tx			= $objp->localtax2_tx;
5952
			$this->remise_percent = $objp->remise_percent;
5953
			$this->fk_remise_except = $objp->fk_remise_except;
5954
			$this->fk_product			= $objp->fk_product;
5955
			$this->product_type = $objp->product_type;
5956
			$this->date_start			= $this->db->jdate($objp->date_start);
5957
			$this->date_end				= $this->db->jdate($objp->date_end);
5958
			$this->info_bits			= $objp->info_bits;
5959
			$this->tva_npr = ($objp->info_bits & 1 == 1) ? 1 : 0;
5960
			$this->special_code = $objp->special_code;
5961
			$this->total_ht				= $objp->total_ht;
5962
			$this->total_tva			= $objp->total_tva;
5963
			$this->total_localtax1		= $objp->total_localtax1;
5964
			$this->total_localtax2		= $objp->total_localtax2;
5965
			$this->total_ttc			= $objp->total_ttc;
5966
			$this->fk_code_ventilation = $objp->fk_code_ventilation;
5967
			$this->rang					= $objp->rang;
5968
			$this->fk_fournprice = $objp->fk_fournprice;
5969
			$marginInfos				= getMarginInfos($objp->subprice, $objp->remise_percent, $objp->tva_tx, $objp->localtax1_tx, $objp->localtax2_tx, $this->fk_fournprice, $objp->pa_ht);
5970
			$this->pa_ht				= $marginInfos[0];
5971
			$this->marge_tx				= $marginInfos[1];
5972
			$this->marque_tx			= $marginInfos[2];
5973
5974
			$this->ref = $objp->product_ref; // deprecated
5975
5976
			$this->product_ref = $objp->product_ref;
5977
			$this->product_label		= $objp->product_label;
5978
			$this->product_desc			= $objp->product_desc;
5979
5980
			$this->fk_unit = $objp->fk_unit;
5981
			$this->fk_user_modif		= $objp->fk_user_modif;
5982
			$this->fk_user_author = $objp->fk_user_author;
5983
5984
			$this->situation_percent    = $objp->situation_percent;
5985
			$this->fk_prev_id           = $objp->fk_prev_id;
5986
5987
			$this->multicurrency_subprice = $objp->multicurrency_subprice;
5988
			$this->multicurrency_total_ht = $objp->multicurrency_total_ht;
5989
			$this->multicurrency_total_tva = $objp->multicurrency_total_tva;
5990
			$this->multicurrency_total_ttc = $objp->multicurrency_total_ttc;
5991
5992
			$this->db->free($result);
5993
5994
			return 1;
5995
		} else {
5996
			$this->error = $this->db->lasterror();
5997
			return -1;
5998
		}
5999
	}
6000
6001
	/**
6002
	 *	Insert line into database
6003
	 *
6004
	 *	@param      int		$notrigger		                 1 no triggers
6005
	 *  @param      int     $noerrorifdiscountalreadylinked  1=Do not make error if lines is linked to a discount and discount already linked to another
6006
	 *	@return		int						                 <0 if KO, >0 if OK
6007
	 */
6008
	public function insert($notrigger = 0, $noerrorifdiscountalreadylinked = 0)
6009
	{
6010
		global $langs, $user, $conf;
6011
6012
		$error = 0;
6013
6014
		$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'.
6015
6016
		dol_syslog(get_class($this)."::insert rang=".$this->rang, LOG_DEBUG);
6017
6018
		// Clean parameters
6019
		$this->desc = trim($this->desc);
6020
		if (empty($this->tva_tx)) {
6021
			$this->tva_tx = 0;
6022
		}
6023
		if (empty($this->localtax1_tx)) {
6024
			$this->localtax1_tx = 0;
6025
		}
6026
		if (empty($this->localtax2_tx)) {
6027
			$this->localtax2_tx = 0;
6028
		}
6029
		if (empty($this->localtax1_type)) {
6030
			$this->localtax1_type = 0;
6031
		}
6032
		if (empty($this->localtax2_type)) {
6033
			$this->localtax2_type = 0;
6034
		}
6035
		if (empty($this->total_localtax1)) {
6036
			$this->total_localtax1 = 0;
6037
		}
6038
		if (empty($this->total_localtax2)) {
6039
			$this->total_localtax2 = 0;
6040
		}
6041
		if (empty($this->rang)) {
6042
			$this->rang = 0;
6043
		}
6044
		if (empty($this->remise_percent)) {
6045
			$this->remise_percent = 0;
6046
		}
6047
		if (empty($this->info_bits)) {
6048
			$this->info_bits = 0;
6049
		}
6050
		if (empty($this->subprice)) {
6051
			$this->subprice = 0;
6052
		}
6053
		if (empty($this->ref_ext)) {
6054
			$this->ref_ext = '';
6055
		}
6056
		if (empty($this->special_code)) {
6057
			$this->special_code = 0;
6058
		}
6059
		if (empty($this->fk_parent_line)) {
6060
			$this->fk_parent_line = 0;
6061
		}
6062
		if (empty($this->fk_prev_id)) {
6063
			$this->fk_prev_id = 0;
6064
		}
6065
		if (!isset($this->situation_percent) || $this->situation_percent > 100 || (string) $this->situation_percent == '') {
6066
			$this->situation_percent = 100;
6067
		}
6068
6069
		if (empty($this->pa_ht)) {
6070
			$this->pa_ht = 0;
6071
		}
6072
		if (empty($this->multicurrency_subprice)) {
6073
			$this->multicurrency_subprice = 0;
6074
		}
6075
		if (empty($this->multicurrency_total_ht)) {
6076
			$this->multicurrency_total_ht = 0;
6077
		}
6078
		if (empty($this->multicurrency_total_tva)) {
6079
			$this->multicurrency_total_tva = 0;
6080
		}
6081
		if (empty($this->multicurrency_total_ttc)) {
6082
			$this->multicurrency_total_ttc = 0;
6083
		}
6084
6085
		// if buy price not defined, define buyprice as configured in margin admin
6086
		if ($this->pa_ht == 0 && $pa_ht_isemptystring) {
6087
			if (($result = $this->defineBuyPrice($this->subprice, $this->remise_percent, $this->fk_product)) < 0) {
6088
				return $result;
6089
			} else {
6090
				$this->pa_ht = $result;
6091
			}
6092
		}
6093
6094
		// Check parameters
6095
		if ($this->product_type < 0) {
6096
			$this->error = 'ErrorProductTypeMustBe0orMore';
6097
			return -1;
6098
		}
6099
		if (!empty($this->fk_product) && $this->fk_product > 0) {
6100
			// Check product exists
6101
			$result = Product::isExistingObject('product', $this->fk_product);
6102
			if ($result <= 0) {
6103
				$this->error = 'ErrorProductIdDoesNotExists';
6104
				dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
6105
				return -1;
6106
			}
6107
		}
6108
6109
		$this->db->begin();
6110
6111
		// Update line in database
6112
		$sql = 'INSERT INTO '.MAIN_DB_PREFIX.'facturedet';
6113
		$sql .= ' (fk_facture, fk_parent_line, label, description, qty,';
6114
		$sql .= ' vat_src_code, tva_tx, localtax1_tx, localtax2_tx, localtax1_type, localtax2_type,';
6115
		$sql .= ' fk_product, product_type, remise_percent, subprice, ref_ext, fk_remise_except,';
6116
		$sql .= ' date_start, date_end, fk_code_ventilation, ';
6117
		$sql .= ' rang, special_code, fk_product_fournisseur_price, buy_price_ht,';
6118
		$sql .= ' info_bits, total_ht, total_tva, total_ttc, total_localtax1, total_localtax2,';
6119
		$sql .= ' situation_percent, fk_prev_id,';
6120
		$sql .= ' fk_unit, fk_user_author, fk_user_modif,';
6121
		$sql .= ' fk_multicurrency, multicurrency_code, multicurrency_subprice, multicurrency_total_ht, multicurrency_total_tva, multicurrency_total_ttc';
6122
		$sql .= ')';
6123
		$sql .= " VALUES (".$this->fk_facture.",";
6124
		$sql .= " ".($this->fk_parent_line > 0 ? $this->fk_parent_line : "null").",";
6125
		$sql .= " ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null").",";
6126
		$sql .= " '".$this->db->escape($this->desc)."',";
6127
		$sql .= " ".price2num($this->qty).",";
6128
		$sql .= " ".(empty($this->vat_src_code) ? "''" : "'".$this->db->escape($this->vat_src_code)."'").",";
6129
		$sql .= " ".price2num($this->tva_tx).",";
6130
		$sql .= " ".price2num($this->localtax1_tx).",";
6131
		$sql .= " ".price2num($this->localtax2_tx).",";
6132
		$sql .= " '".$this->db->escape($this->localtax1_type)."',";
6133
		$sql .= " '".$this->db->escape($this->localtax2_type)."',";
6134
		$sql .= ' '.((!empty($this->fk_product) && $this->fk_product > 0) ? $this->fk_product : "null").',';
6135
		$sql .= " ".((int) $this->product_type).",";
6136
		$sql .= " ".price2num($this->remise_percent).",";
6137
		$sql .= " ".price2num($this->subprice).",";
6138
		$sql .= " '".$this->db->escape($this->ref_ext)."',";
6139
		$sql .= ' '.(!empty($this->fk_remise_except) ? $this->fk_remise_except : "null").',';
6140
		$sql .= " ".(!empty($this->date_start) ? "'".$this->db->idate($this->date_start)."'" : "null").",";
6141
		$sql .= " ".(!empty($this->date_end) ? "'".$this->db->idate($this->date_end)."'" : "null").",";
6142
		$sql .= ' '.((int) $this->fk_code_ventilation).',';
6143
		$sql .= ' '.((int) $this->rang).',';
6144
		$sql .= ' '.((int) $this->special_code).',';
6145
		$sql .= ' '.(!empty($this->fk_fournprice) ? $this->fk_fournprice : "null").',';
6146
		$sql .= ' '.price2num($this->pa_ht).',';
6147
		$sql .= " '".$this->db->escape($this->info_bits)."',";
6148
		$sql .= " ".price2num($this->total_ht).",";
6149
		$sql .= " ".price2num($this->total_tva).",";
6150
		$sql .= " ".price2num($this->total_ttc).",";
6151
		$sql .= " ".price2num($this->total_localtax1).",";
6152
		$sql .= " ".price2num($this->total_localtax2);
6153
		$sql .= ", ".((float) $this->situation_percent);
6154
		$sql .= ", ".(!empty($this->fk_prev_id) ? $this->fk_prev_id : "null");
6155
		$sql .= ", ".(!$this->fk_unit ? 'NULL' : $this->fk_unit);
6156
		$sql .= ", ".((int) $user->id);
6157
		$sql .= ", ".((int) $user->id);
6158
		$sql .= ", ".(int) $this->fk_multicurrency;
6159
		$sql .= ", '".$this->db->escape($this->multicurrency_code)."'";
6160
		$sql .= ", ".price2num($this->multicurrency_subprice);
6161
		$sql .= ", ".price2num($this->multicurrency_total_ht);
6162
		$sql .= ", ".price2num($this->multicurrency_total_tva);
6163
		$sql .= ", ".price2num($this->multicurrency_total_ttc);
6164
		$sql .= ')';
6165
6166
		dol_syslog(get_class($this)."::insert", LOG_DEBUG);
6167
		$resql = $this->db->query($sql);
6168
		if ($resql) {
6169
			$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'facturedet');
6170
			$this->rowid = $this->id; // For backward compatibility
6171
6172
			if (!$error) {
6173
				$result = $this->insertExtraFields();
6174
				if ($result < 0) {
6175
					$error++;
6176
				}
6177
			}
6178
6179
			// If fk_remise_except is defined, the discount is linked to the invoice
6180
			// which flags it as "consumed".
6181
			if ($this->fk_remise_except) {
6182
				$discount = new DiscountAbsolute($this->db);
6183
				$result = $discount->fetch($this->fk_remise_except);
6184
				if ($result >= 0) {
6185
					// Check if discount was found
6186
					if ($result > 0) {
6187
						// Check if discount not already affected to another invoice
6188
						if ($discount->fk_facture_line > 0) {
6189
							if (empty($noerrorifdiscountalreadylinked)) {
6190
								$this->error = $langs->trans("ErrorDiscountAlreadyUsed", $discount->id);
6191
								dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
6192
								$this->db->rollback();
6193
								return -3;
6194
							}
6195
						} else {
6196
							$result = $discount->link_to_invoice($this->rowid, 0);
6197
							if ($result < 0) {
6198
								$this->error = $discount->error;
6199
								dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
6200
								$this->db->rollback();
6201
								return -3;
6202
							}
6203
						}
6204
					} else {
6205
						$this->error = $langs->trans("ErrorADiscountThatHasBeenRemovedIsIncluded");
6206
						dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
6207
						$this->db->rollback();
6208
						return -3;
6209
					}
6210
				} else {
6211
					$this->error = $discount->error;
6212
					dol_syslog(get_class($this)."::insert Error ".$this->error, LOG_ERR);
6213
					$this->db->rollback();
6214
					return -3;
6215
				}
6216
			}
6217
6218
			if (!$notrigger) {
6219
				// Call trigger
6220
				$result = $this->call_trigger('LINEBILL_INSERT', $user);
6221
				if ($result < 0) {
6222
					$this->db->rollback();
6223
					return -2;
6224
				}
6225
				// End call triggers
6226
			}
6227
6228
			$this->db->commit();
6229
			return $this->id;
6230
		} else {
6231
			$this->error = $this->db->lasterror();
6232
			$this->db->rollback();
6233
			return -2;
6234
		}
6235
	}
6236
6237
	/**
6238
	 *	Update line into database
6239
	 *
6240
	 *	@param		User	$user		User object
6241
	 *	@param		int		$notrigger	Disable triggers
6242
	 *	@return		int					<0 if KO, >0 if OK
6243
	 */
6244
	public function update($user = '', $notrigger = 0)
6245
	{
6246
		global $user, $conf;
6247
6248
		$error = 0;
6249
6250
		$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'.
6251
6252
		// Clean parameters
6253
		$this->desc = trim($this->desc);
6254
		if (empty($this->ref_ext)) {
6255
			$this->ref_ext = '';
6256
		}
6257
		if (empty($this->tva_tx)) {
6258
			$this->tva_tx = 0;
6259
		}
6260
		if (empty($this->localtax1_tx)) {
6261
			$this->localtax1_tx = 0;
6262
		}
6263
		if (empty($this->localtax2_tx)) {
6264
			$this->localtax2_tx = 0;
6265
		}
6266
		if (empty($this->localtax1_type)) {
6267
			$this->localtax1_type = 0;
6268
		}
6269
		if (empty($this->localtax2_type)) {
6270
			$this->localtax2_type = 0;
6271
		}
6272
		if (empty($this->total_localtax1)) {
6273
			$this->total_localtax1 = 0;
6274
		}
6275
		if (empty($this->total_localtax2)) {
6276
			$this->total_localtax2 = 0;
6277
		}
6278
		if (empty($this->remise_percent)) {
6279
			$this->remise_percent = 0;
6280
		}
6281
		if (empty($this->info_bits)) {
6282
			$this->info_bits = 0;
6283
		}
6284
		if (empty($this->special_code)) {
6285
			$this->special_code = 0;
6286
		}
6287
		if (empty($this->product_type)) {
6288
			$this->product_type = 0;
6289
		}
6290
		if (empty($this->fk_parent_line)) {
6291
			$this->fk_parent_line = 0;
6292
		}
6293
		if (!isset($this->situation_percent) || $this->situation_percent > 100 || (string) $this->situation_percent == '') {
6294
			$this->situation_percent = 100;
6295
		}
6296
		if (empty($this->pa_ht)) {
6297
			$this->pa_ht = 0;
6298
		}
6299
6300
		if (empty($this->multicurrency_subprice)) {
6301
			$this->multicurrency_subprice = 0;
6302
		}
6303
		if (empty($this->multicurrency_total_ht)) {
6304
			$this->multicurrency_total_ht = 0;
6305
		}
6306
		if (empty($this->multicurrency_total_tva)) {
6307
			$this->multicurrency_total_tva = 0;
6308
		}
6309
		if (empty($this->multicurrency_total_ttc)) {
6310
			$this->multicurrency_total_ttc = 0;
6311
		}
6312
6313
		// Check parameters
6314
		if ($this->product_type < 0) {
6315
			return -1;
6316
		}
6317
6318
		// if buy price not provided, define buyprice as configured in margin admin
6319
		if ($this->pa_ht == 0 && $pa_ht_isemptystring) {
6320
			// We call defineBuyPrice only if data was not provided (if input was '0', we will not go here and value will remaine '0')
6321
			$result = $this->defineBuyPrice($this->subprice, $this->remise_percent, $this->fk_product);
6322
			if ($result < 0) {
6323
				return $result;
6324
			} else {
6325
				$this->pa_ht = $result;
6326
			}
6327
		}
6328
6329
		$this->db->begin();
6330
6331
		// Update line in database
6332
		$sql = "UPDATE ".MAIN_DB_PREFIX."facturedet SET";
6333
		$sql .= " description='".$this->db->escape($this->desc)."'";
6334
		$sql .= ", ref_ext='".$this->db->escape($this->ref_ext)."'";
6335
		$sql .= ", label=".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
6336
		$sql .= ", subprice=".price2num($this->subprice);
6337
		$sql .= ", remise_percent=".price2num($this->remise_percent);
6338
		if ($this->fk_remise_except) {
6339
			$sql .= ", fk_remise_except=".$this->fk_remise_except;
6340
		} else {
6341
			$sql .= ", fk_remise_except=null";
6342
		}
6343
		$sql .= ", vat_src_code = '".(empty($this->vat_src_code) ? '' : $this->db->escape($this->vat_src_code))."'";
6344
		$sql .= ", tva_tx=".price2num($this->tva_tx);
6345
		$sql .= ", localtax1_tx=".price2num($this->localtax1_tx);
6346
		$sql .= ", localtax2_tx=".price2num($this->localtax2_tx);
6347
		$sql .= ", localtax1_type='".$this->db->escape($this->localtax1_type)."'";
6348
		$sql .= ", localtax2_type='".$this->db->escape($this->localtax2_type)."'";
6349
		$sql .= ", qty=".price2num($this->qty);
6350
		$sql .= ", date_start=".(!empty($this->date_start) ? "'".$this->db->idate($this->date_start)."'" : "null");
6351
		$sql .= ", date_end=".(!empty($this->date_end) ? "'".$this->db->idate($this->date_end)."'" : "null");
6352
		$sql .= ", product_type=".$this->product_type;
6353
		$sql .= ", info_bits='".$this->db->escape($this->info_bits)."'";
6354
		$sql .= ", special_code='".$this->db->escape($this->special_code)."'";
6355
		if (empty($this->skip_update_total)) {
6356
			$sql .= ", total_ht=".price2num($this->total_ht);
6357
			$sql .= ", total_tva=".price2num($this->total_tva);
6358
			$sql .= ", total_ttc=".price2num($this->total_ttc);
6359
			$sql .= ", total_localtax1=".price2num($this->total_localtax1);
6360
			$sql .= ", total_localtax2=".price2num($this->total_localtax2);
6361
		}
6362
		$sql .= ", fk_product_fournisseur_price=".(!empty($this->fk_fournprice) ? "'".$this->db->escape($this->fk_fournprice)."'" : "null");
6363
		$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)
6364
		$sql .= ", fk_parent_line=".($this->fk_parent_line > 0 ? $this->fk_parent_line : "null");
6365
		if (!empty($this->rang)) {
6366
			$sql .= ", rang=".((int) $this->rang);
6367
		}
6368
		$sql .= ", situation_percent = ".((float) $this->situation_percent);
6369
		$sql .= ", fk_unit = ".(!$this->fk_unit ? 'NULL' : $this->fk_unit);
6370
		$sql .= ", fk_user_modif = ".((int) $user->id);
6371
6372
		// Multicurrency
6373
		$sql .= ", multicurrency_subprice=".price2num($this->multicurrency_subprice);
6374
		$sql .= ", multicurrency_total_ht=".price2num($this->multicurrency_total_ht);
6375
		$sql .= ", multicurrency_total_tva=".price2num($this->multicurrency_total_tva);
6376
		$sql .= ", multicurrency_total_ttc=".price2num($this->multicurrency_total_ttc);
6377
6378
		$sql .= " WHERE rowid = ".((int) $this->rowid);
6379
6380
		dol_syslog(get_class($this)."::update", LOG_DEBUG);
6381
		$resql = $this->db->query($sql);
6382
		if ($resql) {
6383
			if (!$error) {
6384
				$this->id = $this->rowid;
6385
				$result = $this->insertExtraFields();
6386
				if ($result < 0) {
6387
					$error++;
6388
				}
6389
			}
6390
6391
			if (!$error && !$notrigger) {
6392
				// Call trigger
6393
				$result = $this->call_trigger('LINEBILL_MODIFY', $user);
6394
				if ($result < 0) {
6395
					$this->db->rollback();
6396
					return -2;
6397
				}
6398
				// End call triggers
6399
			}
6400
			$this->db->commit();
6401
			return 1;
6402
		} else {
6403
			$this->error = $this->db->error();
6404
			$this->db->rollback();
6405
			return -2;
6406
		}
6407
	}
6408
6409
	/**
6410
	 * Delete line in database
6411
	 *
6412
	 * @param 	User 	$tmpuser    User that deletes
6413
	 * @param 	bool 	$notrigger  false=launch triggers after, true=disable triggers
6414
	 * @return 	int		           	<0 if KO, >0 if OK
6415
	 */
6416
	public function delete($tmpuser = null, $notrigger = false)
6417
	{
6418
		global $user;
6419
6420
		$this->db->begin();
6421
6422
		// Call trigger
6423
		if (empty($notrigger)) {
6424
			$result = $this->call_trigger('LINEBILL_DELETE', $user);
6425
			if ($result < 0) {
6426
				$this->db->rollback();
6427
				return -1;
6428
			}
6429
		}
6430
		// End call triggers
6431
6432
		// extrafields
6433
		$result = $this->deleteExtraFields();
6434
		if ($result < 0) {
6435
			$this->db->rollback();
6436
			return -1;
6437
		}
6438
6439
		$sql = "DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".((int) $this->rowid);
6440
6441
		if ($this->db->query($sql)) {
6442
			$this->db->commit();
6443
			return 1;
6444
		} else {
6445
			$this->error = $this->db->error()." sql=".$sql;
6446
			$this->db->rollback();
6447
			return -1;
6448
		}
6449
	}
6450
6451
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6452
	/**
6453
	 *	Update DB line fields total_xxx
6454
	 *	Used by migration
6455
	 *
6456
	 *	@return		int		<0 if KO, >0 if OK
6457
	 */
6458
	public function update_total()
6459
	{
6460
		// phpcs:enable
6461
		$this->db->begin();
6462
		dol_syslog(get_class($this)."::update_total", LOG_DEBUG);
6463
6464
		// Clean parameters
6465
		if (empty($this->total_localtax1)) {
6466
			$this->total_localtax1 = 0;
6467
		}
6468
		if (empty($this->total_localtax2)) {
6469
			$this->total_localtax2 = 0;
6470
		}
6471
6472
		// Update line in database
6473
		$sql = "UPDATE ".MAIN_DB_PREFIX."facturedet SET";
6474
		$sql .= " total_ht=".price2num($this->total_ht);
6475
		$sql .= ",total_tva=".price2num($this->total_tva);
6476
		$sql .= ",total_localtax1=".price2num($this->total_localtax1);
6477
		$sql .= ",total_localtax2=".price2num($this->total_localtax2);
6478
		$sql .= ",total_ttc=".price2num($this->total_ttc);
6479
		$sql .= " WHERE rowid = ".((int) $this->rowid);
6480
6481
		dol_syslog(get_class($this)."::update_total", LOG_DEBUG);
6482
6483
		$resql = $this->db->query($sql);
6484
		if ($resql) {
6485
			$this->db->commit();
6486
			return 1;
6487
		} else {
6488
			$this->error = $this->db->error();
6489
			$this->db->rollback();
6490
			return -2;
6491
		}
6492
	}
6493
6494
	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6495
	/**
6496
	 * Returns situation_percent of the previous line.
6497
	 * Warning: If invoice is a replacement invoice, this->fk_prev_id is id of the replaced line.
6498
	 *
6499
	 * @param  int     $invoiceid      Invoice id
6500
	 * @param  bool    $include_credit_note		Include credit note or not
6501
	 * @return int                     >= 0
6502
	 */
6503
	public function get_prev_progress($invoiceid, $include_credit_note = true)
6504
	{
6505
		// phpcs:enable
6506
		global $invoicecache;
6507
		if (is_null($this->fk_prev_id) || empty($this->fk_prev_id) || $this->fk_prev_id == "") {
6508
			return 0;
6509
		} else {
6510
			// If invoice is not a situation invoice, this->fk_prev_id is used for something else
6511
			if (!isset($invoicecache[$invoiceid])) {
6512
				$invoicecache[$invoiceid] = new Facture($this->db);
6513
				$invoicecache[$invoiceid]->fetch($invoiceid);
6514
			}
6515
			if ($invoicecache[$invoiceid]->type != Facture::TYPE_SITUATION) {
6516
				return 0;
6517
			}
6518
6519
			$sql = "SELECT situation_percent FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".((int) $this->fk_prev_id);
6520
			$resql = $this->db->query($sql);
6521
			if ($resql && $this->db->num_rows($resql) > 0) {
6522
				$res = $this->db->fetch_array($resql);
6523
6524
				$returnPercent = floatval($res['situation_percent']);
6525
6526
				if ($include_credit_note) {
6527
					$sql = 'SELECT fd.situation_percent FROM '.MAIN_DB_PREFIX.'facturedet fd';
6528
					$sql .= ' JOIN '.MAIN_DB_PREFIX.'facture f ON (f.rowid = fd.fk_facture) ';
6529
					$sql .= " WHERE fd.fk_prev_id = ".((int) $this->fk_prev_id);
6530
					$sql .= " AND f.situation_cycle_ref = ".((int) $invoicecache[$invoiceid]->situation_cycle_ref); // Prevent cycle outed
6531
					$sql .= " AND f.type = ".Facture::TYPE_CREDIT_NOTE;
6532
6533
					$res = $this->db->query($sql);
6534
					if ($res) {
6535
						while ($obj = $this->db->fetch_object($res)) {
6536
							$returnPercent = $returnPercent + floatval($obj->situation_percent);
6537
						}
6538
					} else {
6539
						dol_print_error($this->db);
6540
					}
6541
				}
6542
6543
				return $returnPercent;
6544
			} else {
6545
				$this->error = $this->db->error();
6546
				dol_syslog(get_class($this)."::select Error ".$this->error, LOG_ERR);
6547
				$this->db->rollback();
6548
				return -1;
6549
			}
6550
		}
6551
	}
6552
}
6553