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

Facture::updateline()   F

Complexity

Conditions 52

Size

Total Lines 202
Code Lines 134

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 52
eloc 134
nop 26
dl 0
loc 202
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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