Test Failed
Branch main (fda838)
by Rafael
54:38 queued 11s
created

Invoice::list_replacable_invoices()   A

Complexity

Conditions 4

Size

Total Lines 41
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 29
nop 1
dl 0
loc 41
rs 9.456
c 1
b 0
f 0
1
<?php
2
3
/* Copyright (C) 2002-2003  Rodolphe Quiedeville    <[email protected]>
4
 * Copyright (C) 2002-2003	Jean-Louis Bergamo		<[email protected]>
5
 * Copyright (C) 2004-2012	Laurent Destailleur		<[email protected]>
6
 * Copyright (C) 2004		Sebastien Di Cintio		<[email protected]>
7
 * Copyright (C) 2004		Benoit Mortier			<[email protected]>
8
 * Copyright (C) 2009-2017	Regis Houssin			<[email protected]>
9
 * Copyright (C) 2014-2018	Alexandre Spangaro		<[email protected]>
10
 * Copyright (C) 2015		Marcos García			<[email protected]>
11
 * Copyright (C) 2015-2024	Frédéric France			<[email protected]>
12
 * Copyright (C) 2015		Raphaël Doursenaud		<[email protected]>
13
 * Copyright (C) 2016		Juanjo Menent			<[email protected]>
14
 * Copyright (C) 2018-2019	Thibault FOUCART		<[email protected]>
15
 * Copyright (C) 2019		Nicolas ZABOURI 		<[email protected]>
16
 * Copyright (C) 2020		Josep Lluís Amador 		<[email protected]>
17
 * Copyright (C) 2021		Waël Almoman            <[email protected]>
18
 * Copyright (C) 2021		Philippe Grand          <[email protected]>
19
 * Copyright (C) 2024		MDW						<[email protected]>
20
 * Copyright (C) 2024       Rafael San José         <[email protected]>
21
 *
22
 * This program is free software; you can redistribute it and/or modify
23
 * it under the terms of the GNU General Public License as published by
24
 * the Free Software Foundation; either version 3 of the License, or
25
 * (at your option) any later version.
26
 *
27
 * This program is distributed in the hope that it will be useful,
28
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
30
 * GNU General Public License for more details.
31
 *
32
 * You should have received a copy of the GNU General Public License
33
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
34
 */
35
36
/**
37
 *  \file       htdocs/compta/facture/class/facture.class.php
38
 *  \ingroup    facture
39
 *  \brief      File of class to manage invoices
40
 */
41
42
namespace DoliModules\Billing\Model;
43
44
use DoliDB;
45
use WorkboardResponse;
46
47
/**
48
 *  Class to manage invoices
49
 */
50
class Invoice extends CommonInvoice
51
{
52
    /**
53
     * @var string ID to identify managed object
54
     */
55
    public $element = 'facture';
56
57
    /**
58
     * @var string Name of table without prefix where object is stored
59
     */
60
    public $table_element = 'facture';
61
62
    /**
63
     * @var string    Name of subtable line
64
     */
65
    public $table_element_line = 'facturedet';
66
67
    /**
68
     * @var string Fieldname with ID of parent key if this field has a parent
69
     */
70
    public $fk_element = 'fk_facture';
71
72
    /**
73
     * @var string String with name of icon for myobject.
74
     */
75
    public $picto = 'bill';
76
77
    /**
78
     * 0=No test on entity, 1=Test with field entity, 2=Test with link by societe
79
     * @var int
80
     */
81
    public $ismultientitymanaged = 1;
82
83
    /**
84
     * @var int  Does object support extrafields ? 0=No, 1=Yes
85
     */
86
    public $isextrafieldmanaged = 1;
87
88
    /**
89
     * 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
90
     * @var integer
91
     */
92
    public $restrictiononfksoc = 1;
93
94
    /**
95
     * {@inheritdoc}
96
     */
97
    protected $table_ref_field = 'ref';
98
99
    /**
100
     * @var int ID
101
     * @deprecated      Use $user_creation_id
102
     */
103
    public $fk_user_author;
104
105
    /**
106
     * @var int|null ID
107
     * @deprecated      Use $user_validation_id
108
     */
109
    public $fk_user_valid;
110
111
    /**
112
     * @var int ID
113
     * @deprecated      Use $user_modification_id
114
     */
115
    public $fk_user_modif;
116
117
118
    public $datem;
119
120
    /**
121
     * @var int Date expected for delivery
122
     */
123
    public $delivery_date; // Date expected of shipment (date of start of shipment, not the reception that occurs some days after)
124
125
    /**
126
     * @var string customer ref
127
     * @deprecated
128
     * @see $ref_customer
129
     */
130
    public $ref_client;
131
132
    /**
133
     * @var string customer ref
134
     */
135
    public $ref_customer;
136
137
    public $total_ht;
138
    public $total_tva;
139
    public $total_localtax1;
140
    public $total_localtax2;
141
    public $total_ttc;
142
    public $revenuestamp;
143
144
    public $resteapayer;
145
146
    /**
147
     * 1 if invoice paid COMPLETELY, 0 otherwise (do not use it anymore, use statut and close_code)
148
     */
149
    public $paye;
150
151
    //! key of module source when invoice generated from a dedicated module ('cashdesk', 'takepos', ...)
152
    public $module_source;
153
    //! key of pos source ('0', '1', ...)
154
    public $pos_source;
155
    //! id of template invoice when generated from a template invoice
156
    public $fk_fac_rec_source;
157
    //! id of source invoice if replacement invoice or credit note
158
    public $fk_facture_source;
159
    public $linked_objects = array();
160
161
    /**
162
     * @var int ID Field to store bank id to use when payment mode is withdraw
163
     */
164
    public $fk_bank;
165
166
    /**
167
     * @var FactureLigne[]
168
     */
169
    public $lines = array();
170
171
    /**
172
     * @var FactureLigne
173
     */
174
    public $line;
175
    public $extraparams = array();
176
177
    /**
178
     * @var int ID facture rec
179
     */
180
    public $fac_rec;
181
182
    public $date_pointoftax;
183
184
185
    /**
186
     * @var int Situation cycle reference number
187
     */
188
    public $situation_cycle_ref;
189
190
    /**
191
     * @var int Situation counter inside the cycle
192
     */
193
    public $situation_counter;
194
195
    /**
196
     * @var int Final situation flag
197
     */
198
    public $situation_final;
199
200
    /**
201
     * @var array Table of previous situations
202
     */
203
    public $tab_previous_situation_invoice = array();
204
205
    /**
206
     * @var array Table of next situations
207
     */
208
    public $tab_next_situation_invoice = array();
209
210
    /**
211
     * @var Facture object oldcopy
212
     */
213
    public $oldcopy;
214
215
    /**
216
     * @var double percentage of retainage
217
     */
218
    public $retained_warranty;
219
220
    /**
221
     * @var int timestamp of date limit of retainage
222
     */
223
    public $retained_warranty_date_limit;
224
225
    /**
226
     * @var int Code in llx_c_paiement
227
     */
228
    public $retained_warranty_fk_cond_reglement;
229
230
    /**
231
     * @var int availability ID
232
     */
233
    public $availability_id;
234
235
    public $date_closing;
236
237
    /**
238
     * @var int
239
     */
240
    public $source;
241
242
    /**
243
     * @var float   Percent of discount ("remise" in French)
244
     * @deprecated The discount percent is on line level now
245
     */
246
    public $remise_percent;
247
248
    /**
249
     * @var string payment url
250
     */
251
    public $online_payment_url;
252
253
254
255
    /**
256
     *  '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')
257
     *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)"
258
     *  'label' the translation key.
259
     *  'enabled' is a condition when the field must be managed.
260
     *  'position' is the sort order of field.
261
     *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
262
     *  '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)
263
     *  'noteditable' says if field is not editable (1 or 0)
264
     *  '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.
265
     *  'index' if we want an index in database.
266
     *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommended to name the field fk_...).
267
     *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
268
     *  '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).
269
     *  'css' is the CSS style to use on field. For example: 'maxwidth200'
270
     *  'help' is a string visible as a tooltip on field
271
     *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
272
     *  '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.
273
     *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
274
     *  'comment' is not used. You can store here any text of your choice. It is not used by application.
275
     *
276
     *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
277
     */
278
279
    // BEGIN MODULEBUILDER PROPERTIES
280
    /**
281
     * @var array<string,array{type:string,label:string,enabled:int<0,2>|string,position:int,notnull:int,visible:int,noteditable?:int,default?:string,index?:int,foreignkey?:string,searchall?:int,isameasure?:int,css?:string,csslist?:string,help?:string,showoncombobox?:int,disabled?:int,arrayofkeyval?:array<int,string>,comment?:string}>  Array with all fields and their property. Do not use it as a static var. It may be modified by constructor.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string,array{type:...ring>,comment?:string}> at position 16 could not be parsed: Expected '}' at position 16, but found 'int'.
Loading history...
282
     */
283
    public $fields = array(
284
        'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 1),
285
        'ref' => array('type' => 'varchar(30)', 'label' => 'Ref', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 1, 'position' => 5),
286
        'entity' => array('type' => 'integer', 'label' => 'Entity', 'default' => '1', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 20, 'index' => 1),
287
        'ref_client' => array('type' => 'varchar(255)', 'label' => 'RefCustomer', 'enabled' => 1, 'visible' => -1, 'position' => 10),
288
        'ref_ext' => array('type' => 'varchar(255)', 'label' => 'Ref ext', 'enabled' => 1, 'visible' => 0, 'position' => 12),
289
        'type' => array('type' => 'smallint(6)', 'label' => 'Type', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 15),
290
        'subtype' => array('type' => 'smallint(6)', 'label' => 'InvoiceSubtype', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 15),
291
        //'increment' =>array('type'=>'varchar(10)', 'label'=>'Increment', 'enabled'=>1, 'visible'=>-1, 'position'=>45),
292
        'fk_soc' => array('type' => 'integer:Societe:societe/class/societe.class.php', 'label' => 'ThirdParty', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 50),
293
        'datef' => array('type' => 'date', 'label' => 'DateInvoice', 'enabled' => 1, 'visible' => 1, 'position' => 20),
294
        'date_valid' => array('type' => 'date', 'label' => 'DateValidation', 'enabled' => 1, 'visible' => -1, 'position' => 22),
295
        'date_lim_reglement' => array('type' => 'date', 'label' => 'DateDue', 'enabled' => 1, 'visible' => 1, 'position' => 25),
296
        'date_closing' => array('type' => 'datetime', 'label' => 'Date closing', 'enabled' => 1, 'visible' => -1, 'position' => 30),
297
        'paye' => array('type' => 'smallint(6)', 'label' => 'InvoicePaidCompletely', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 80),
298
        //'amount' =>array('type'=>'double(24,8)', 'label'=>'Amount', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>85),
299
        //'remise_percent' =>array('type'=>'double', 'label'=>'RelativeDiscount', 'enabled'=>1, 'visible'=>-1, 'position'=>90),
300
        //'remise_absolue' =>array('type'=>'double', 'label'=>'CustomerRelativeDiscount', 'enabled'=>1, 'visible'=>-1, 'position'=>91),
301
        //'remise' =>array('type'=>'double', 'label'=>'Remise', 'enabled'=>1, 'visible'=>-1, 'position'=>100),
302
        'close_code' => array('type' => 'varchar(16)', 'label' => 'EarlyClosingReason', 'enabled' => 1, 'visible' => -1, 'position' => 92),
303
        'close_note' => array('type' => 'varchar(128)', 'label' => 'EarlyClosingComment', 'enabled' => 1, 'visible' => -1, 'position' => 93),
304
        'total_ht' => array('type' => 'double(24,8)', 'label' => 'AmountHT', 'enabled' => 1, 'visible' => 1, 'position' => 95, 'isameasure' => 1),
305
        'total_tva' => array('type' => 'double(24,8)', 'label' => 'AmountVAT', 'enabled' => 1, 'visible' => -1, 'position' => 100, 'isameasure' => 1),
306
        'localtax1' => array('type' => 'double(24,8)', 'label' => 'LT1', 'enabled' => 1, 'visible' => -1, 'position' => 110, 'isameasure' => 1),
307
        'localtax2' => array('type' => 'double(24,8)', 'label' => 'LT2', 'enabled' => 1, 'visible' => -1, 'position' => 120, 'isameasure' => 1),
308
        'revenuestamp' => array('type' => 'double(24,8)', 'label' => 'RevenueStamp', 'enabled' => 1, 'visible' => -1, 'position' => 115, 'isameasure' => 1),
309
        'total_ttc' => array('type' => 'double(24,8)', 'label' => 'AmountTTC', 'enabled' => 1, 'visible' => 1, 'position' => 130, 'isameasure' => 1),
310
        'fk_facture_source' => array('type' => 'integer', 'label' => 'SourceInvoice', 'enabled' => 1, 'visible' => -1, 'position' => 170),
311
        'fk_projet' => array('type' => 'integer:Project:projet/class/project.class.php:1:(fk_statut:=:1)', 'label' => 'Project', 'enabled' => 1, 'visible' => -1, 'position' => 175),
312
        'fk_account' => array('type' => 'integer', 'label' => 'Fk account', 'enabled' => 1, 'visible' => -1, 'position' => 180),
313
        'fk_currency' => array('type' => 'varchar(3)', 'label' => 'CurrencyCode', 'enabled' => 1, 'visible' => -1, 'position' => 185),
314
        'fk_cond_reglement' => array('type' => 'integer', 'label' => 'PaymentTerm', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 190),
315
        'fk_mode_reglement' => array('type' => 'integer', 'label' => 'PaymentMode', 'enabled' => 1, 'visible' => -1, 'position' => 195),
316
        'note_private' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 205),
317
        'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 210),
318
        'model_pdf' => array('type' => 'varchar(255)', 'label' => 'Model pdf', 'enabled' => 1, 'visible' => 0, 'position' => 215),
319
        'extraparams' => array('type' => 'varchar(255)', 'label' => 'Extraparams', 'enabled' => 1, 'visible' => -1, 'position' => 225),
320
        'situation_cycle_ref' => array('type' => 'smallint(6)', 'label' => 'Situation cycle ref', 'enabled' => '$conf->global->INVOICE_USE_SITUATION', 'visible' => -1, 'position' => 230),
321
        'situation_counter' => array('type' => 'smallint(6)', 'label' => 'Situation counter', 'enabled' => '$conf->global->INVOICE_USE_SITUATION', 'visible' => -1, 'position' => 235),
322
        'situation_final' => array('type' => 'smallint(6)', 'label' => 'Situation final', 'enabled' => 'empty($conf->global->INVOICE_USE_SITUATION) ? 0 : 1', 'visible' => -1, 'position' => 240),
323
        'retained_warranty' => array('type' => 'double', 'label' => 'Retained warranty', 'enabled' => '$conf->global->INVOICE_USE_RETAINED_WARRANTY', 'visible' => -1, 'position' => 245),
324
        'retained_warranty_date_limit' => array('type' => 'date', 'label' => 'Retained warranty date limit', 'enabled' => '$conf->global->INVOICE_USE_RETAINED_WARRANTY', 'visible' => -1, 'position' => 250),
325
        '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),
326
        'fk_incoterms' => array('type' => 'integer', 'label' => 'IncotermCode', 'enabled' => '$conf->incoterm->enabled', 'visible' => -1, 'position' => 260),
327
        'location_incoterms' => array('type' => 'varchar(255)', 'label' => 'IncotermLabel', 'enabled' => '$conf->incoterm->enabled', 'visible' => -1, 'position' => 265),
328
        'date_pointoftax' => array('type' => 'date', 'label' => 'DatePointOfTax', 'enabled' => '$conf->global->INVOICE_POINTOFTAX_DATE', 'visible' => -1, 'position' => 270),
329
        'fk_multicurrency' => array('type' => 'integer', 'label' => 'MulticurrencyID', 'enabled' => 'isModEnabled("multicurrency")', 'visible' => -1, 'position' => 275),
330
        'multicurrency_code' => array('type' => 'varchar(255)', 'label' => 'Currency', 'enabled' => 'isModEnabled("multicurrency")', 'visible' => -1, 'position' => 280),
331
        'multicurrency_tx' => array('type' => 'double(24,8)', 'label' => 'CurrencyRate', 'enabled' => 'isModEnabled("multicurrency")', 'visible' => -1, 'position' => 285, 'isameasure' => 1),
332
        'multicurrency_total_ht' => array('type' => 'double(24,8)', 'label' => 'MulticurrencyAmountHT', 'enabled' => 'isModEnabled("multicurrency")', 'visible' => -1, 'position' => 290, 'isameasure' => 1),
333
        'multicurrency_total_tva' => array('type' => 'double(24,8)', 'label' => 'MulticurrencyAmountVAT', 'enabled' => 'isModEnabled("multicurrency")', 'visible' => -1, 'position' => 291, 'isameasure' => 1),
334
        'multicurrency_total_ttc' => array('type' => 'double(24,8)', 'label' => 'MulticurrencyAmountTTC', 'enabled' => 'isModEnabled("multicurrency")', 'visible' => -1, 'position' => 292, 'isameasure' => 1),
335
        'fk_fac_rec_source' => array('type' => 'integer', 'label' => 'RecurringInvoiceSource', 'enabled' => 1, 'visible' => -1, 'position' => 305),
336
        'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 310),
337
        'module_source' => array('type' => 'varchar(32)', 'label' => 'POSModule', 'enabled' => "(isModEnabled('cashdesk') || isModEnabled('takepos') || getDolGlobalInt('INVOICE_SHOW_POS'))", 'visible' => -1, 'position' => 315),
338
        'pos_source' => array('type' => 'varchar(32)', 'label' => 'POSTerminal', 'enabled' => "(isModEnabled('cashdesk') || isModEnabled('takepos') || getDolGlobalInt('INVOICE_SHOW_POS'))", 'visible' => -1, 'position' => 320),
339
        'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -1, 'position' => 500),
340
        'tms' => array('type' => 'timestamp', 'label' => 'DateModificationShort', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'position' => 502),
341
        'fk_user_author' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserAuthor', 'enabled' => 1, 'visible' => -1, 'position' => 506),
342
        'fk_user_modif' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -1, 'notnull' => -1, 'position' => 508),
343
        'fk_user_valid' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserValidation', 'enabled' => 1, 'visible' => -1, 'position' => 510),
344
        'fk_user_closing' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserClosing', 'enabled' => 1, 'visible' => -1, 'position' => 512),
345
        'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'position' => 900),
346
        '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')),
347
    );
348
    // END MODULEBUILDER PROPERTIES
349
350
    /**
351
     * Standard invoice
352
     */
353
    const TYPE_STANDARD = 0;
354
355
    /**
356
     * Replacement invoice
357
     */
358
    const TYPE_REPLACEMENT = 1;
359
360
    /**
361
     * Credit note invoice
362
     */
363
    const TYPE_CREDIT_NOTE = 2;
364
365
    /**
366
     * Deposit invoice
367
     */
368
    const TYPE_DEPOSIT = 3;
369
370
    /**
371
     * Proforma invoice (should not be used. a proforma is an order)
372
     */
373
    const TYPE_PROFORMA = 4;
374
375
    /**
376
     * Situation invoice
377
     */
378
    const TYPE_SITUATION = 5;
379
380
    /**
381
     * Draft status
382
     */
383
    const STATUS_DRAFT = 0;
384
385
    /**
386
     * Validated (need to be paid)
387
     */
388
    const STATUS_VALIDATED = 1;
389
390
    /**
391
     * Classified paid.
392
     * If paid partially, $this->close_code can be:
393
     * - CLOSECODE_DISCOUNTVAT
394
     * - CLOSECODE_BADDEBT
395
     * If paid completely, this->close_code will be null
396
     */
397
    const STATUS_CLOSED = 2;
398
399
    /**
400
     * Classified abandoned and no payment done.
401
     * $this->close_code can be:
402
     * - CLOSECODE_BADDEBT
403
     * - CLOSECODE_ABANDONED
404
     * - CLOSECODE_REPLACED
405
     */
406
    const STATUS_ABANDONED = 3;
407
408
    const CLOSECODE_DISCOUNTVAT = 'discount_vat'; // Abandoned remain - escompte
409
    const CLOSECODE_BADDEBT = 'badcustomer'; // Abandoned remain - bad customer
410
    const CLOSECODE_BANKCHARGE = 'bankcharge'; // Abandoned remain - bank charge
411
    const CLOSECODE_OTHER = 'other'; // Abandoned remain - other
412
413
    const CLOSECODE_ABANDONED = 'abandon'; // Abandoned - other
414
    const CLOSECODE_REPLACED = 'replaced'; // Closed after doing a replacement invoice
415
416
417
    /**
418
     *  Constructor
419
     *
420
     *  @param  DoliDB      $db         Database handler
421
     */
422
    public function __construct(DoliDB $db)
423
    {
424
        $this->db = $db;
425
    }
426
427
    /**
428
     *  Create invoice in database.
429
     *  Note: this->ref can be set or empty. If empty, we will use "(PROV999)"
430
     *  Note: this->fac_rec must be set to create invoice from a recurring invoice
431
     *
432
     *  @param  User    $user           Object user that create
433
     *  @param  int     $notrigger      1=Does not execute triggers, 0 otherwise
434
     *  @param  int     $forceduedate   If set, do not recalculate due date from payment condition but force it with value
435
     *  @return int                     Return integer <0 if KO, >0 if OK
436
     */
437
    public function create(User $user, $notrigger = 0, $forceduedate = 0)
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\User was not found. Did you mean User? If so, make sure to prefix the type with \.
Loading history...
438
    {
439
        global $langs, $conf, $mysoc, $hookmanager;
440
        $error = 0;
441
442
        // Clean parameters
443
        if (empty($this->type)) {
444
            $this->type = self::TYPE_STANDARD;
445
        }
446
447
        $this->ref_client = trim($this->ref_client);
448
449
        $this->note_private = (isset($this->note_private) ? trim($this->note_private) : '');
450
        $this->note = (isset($this->note) ? trim($this->note) : $this->note_private); // deprecated
451
        $this->note_public = trim($this->note_public);
452
        if (!$this->cond_reglement_id) {
453
            $this->cond_reglement_id = 0;
454
        }
455
        if (!$this->mode_reglement_id) {
456
            $this->mode_reglement_id = 0;
457
        }
458
        $this->status = self::STATUS_DRAFT;
459
        $this->statut = self::STATUS_DRAFT; // deprecated
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$statut has been deprecated: Use $status instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

459
        /** @scrutinizer ignore-deprecated */ $this->statut = self::STATUS_DRAFT; // deprecated

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
460
461
        if (!empty($this->multicurrency_code)) {
462
            // Multicurrency (test on $this->multicurrency_tx because we should take the default rate of multicurrency_code only if not using original rate)
463
            if (empty($this->multicurrency_tx)) {
464
                // If original rate is not set, we take a default value from date
465
                [$this->fk_multicurrency, $this->multicurrency_tx] = MultiCurrency::getIdAndTxFromCode($this->db, $this->multicurrency_code, $this->date);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\MultiCurrency was not found. Did you mean MultiCurrency? If so, make sure to prefix the type with \.
Loading history...
466
            } else {
467
                // original rate multicurrency_tx and multicurrency_code are set, we use them
468
                $this->fk_multicurrency = MultiCurrency::getIdFromCode($this->db, $this->multicurrency_code);
469
            }
470
        } else {
471
            $this->fk_multicurrency = 0;
472
        }
473
        if (empty($this->fk_multicurrency)) {
474
            $this->multicurrency_code = $conf->currency;
475
            $this->fk_multicurrency = 0;
476
            $this->multicurrency_tx = 1;
477
        }
478
479
        dol_syslog(get_class($this) . "::create user=" . $user->id . " date=" . $this->date);
480
481
        // Check parameters
482
        if (empty($this->date)) {
483
            $this->error = "Try to create an invoice with an empty parameter (date)";
484
            dol_syslog(get_class($this) . "::create " . $this->error, LOG_ERR);
485
            return -3;
486
        }
487
        $soc = new Societe($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\Societe was not found. Did you mean Societe? If so, make sure to prefix the type with \.
Loading history...
488
        $result = $soc->fetch($this->socid);
489
        if ($result < 0) {
490
            $this->error = "Failed to fetch company: " . $soc->error;
491
            dol_syslog(get_class($this) . "::create " . $this->error, LOG_ERR);
492
            return -2;
493
        }
494
495
        $now = dol_now();
496
        $this->date_creation = $now;
497
498
        $this->db->begin();
499
500
        $originaldatewhen = null;
501
        $nextdatewhen = null;
502
        $previousdaynextdatewhen = null;
503
504
        // Erase some properties of the invoice to create with the one of the recurring invoice
505
        if ($this->fac_rec > 0) {
506
            $this->fk_fac_rec_source = $this->fac_rec;
507
508
            require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture-rec.class.php';
509
            $_facrec = new FactureRec($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\FactureRec was not found. Did you mean FactureRec? If so, make sure to prefix the type with \.
Loading history...
510
            $result = $_facrec->fetch($this->fac_rec);
511
            $result = $_facrec->fetchObjectLinked(null, '', null, '', 'OR', 1, 'sourcetype', 0); // This load $_facrec->linkedObjectsIds
512
513
            // Define some dates
514
            $originaldatewhen = $_facrec->date_when;
515
            $nextdatewhen = null;  // @phan-suppress-current-line PhanPluginRedundantAssignment
516
            $previousdaynextdatewhen = null;  // @phan-suppress-current-line PhanPluginRedundantAssignment
517
            if ($originaldatewhen) {
518
                $nextdatewhen = dol_time_plus_duree($originaldatewhen, $_facrec->frequency, $_facrec->unit_frequency);
519
                $previousdaynextdatewhen = dol_time_plus_duree($nextdatewhen, -1, 'd');
520
            }
521
522
            if (!empty($_facrec->frequency)) {  // Invoice are created on same thirdparty than template when there is a recurrence, but not necessarily when there is no recurrence.
523
                $this->socid = $_facrec->socid;
524
            }
525
            $this->entity            = $_facrec->entity; // Invoice created in same entity than template
526
527
            // Fields coming from GUI.
528
            // @TODO Value of template should be used as default value on the form on the GUI, and we should here always use the value from GUI
529
            // set by posted page with $object->xxx = ... and this section should be removed.
530
            $this->fk_project        = GETPOSTINT('projectid') > 0 ? GETPOSTINT('projectid') : $_facrec->fk_project;
531
            $this->note_public       = GETPOSTISSET('note_public') ? GETPOST('note_public', 'restricthtml') : $_facrec->note_public;
0 ignored issues
show
Documentation Bug introduced by
It seems like GETPOSTISSET('note_publi...: $_facrec->note_public can also be of type array or array or array. However, the property $note_public is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
532
            $this->note_private      = GETPOSTISSET('note_private') ? GETPOST('note_private', 'restricthtml') : $_facrec->note_private;
0 ignored issues
show
Documentation Bug introduced by
It seems like GETPOSTISSET('note_priva... $_facrec->note_private can also be of type array or array or array. However, the property $note_private is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
533
            $this->model_pdf = GETPOSTISSET('model') ? GETPOST('model', 'alpha') : $_facrec->model_pdf;
0 ignored issues
show
Documentation Bug introduced by
It seems like GETPOSTISSET('model') ? ...) : $_facrec->model_pdf can also be of type array or array or array. However, the property $model_pdf is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
534
            $this->cond_reglement_id = GETPOSTINT('cond_reglement_id') > 0 ? GETPOSTINT('cond_reglement_id') : $_facrec->cond_reglement_id;
535
            $this->mode_reglement_id = GETPOSTINT('mode_reglement_id') > 0 ? GETPOSTINT('mode_reglement_id') : $_facrec->mode_reglement_id;
536
            $this->fk_account        = GETPOST('fk_account') > 0 ? GETPOSTINT('fk_account') : $_facrec->fk_account;
537
538
            // Set here to have this defined for substitution into notes, should be recalculated after adding lines to get same result
539
            $this->total_ht          = $_facrec->total_ht;
540
            $this->total_ttc         = $_facrec->total_ttc;
541
542
            // Fields always coming from template
543
            //$this->remise_absolue    = $_facrec->remise_absolue;
544
            //$this->remise_percent    = $_facrec->remise_percent;  // TODO deprecated
545
            $this->fk_incoterms = $_facrec->fk_incoterms;
546
            $this->location_incoterms = $_facrec->location_incoterms;
547
548
            // Clean parameters
549
            if (!$this->type) {
550
                $this->type = self::TYPE_STANDARD;
551
            }
552
            $this->ref_client = trim($this->ref_client);
553
            $this->ref_customer = trim($this->ref_customer);
554
            $this->note_public = trim($this->note_public);
555
            $this->note_private = trim($this->note_private);
556
            $this->note_private = dol_concatdesc($this->note_private, $langs->trans("GeneratedFromRecurringInvoice", $_facrec->ref));
557
558
            $this->array_options = $_facrec->array_options;
559
560
            if (!$this->mode_reglement_id) {
561
                $this->mode_reglement_id = 0;
562
            }
563
            $this->status = self::STATUS_DRAFT;
564
            $this->statut = self::STATUS_DRAFT; // deprecated
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$statut has been deprecated: Use $status instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

564
            /** @scrutinizer ignore-deprecated */ $this->statut = self::STATUS_DRAFT; // deprecated

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
565
566
            $this->linked_objects = $_facrec->linkedObjectsIds;
567
            // We do not add link to template invoice or next invoice will be linked to all generated invoices
568
            //$this->linked_objects['facturerec'][0] = $this->fac_rec;
569
570
            // For recurring invoices, update date and number of last generation of recurring template invoice, before inserting new invoice
571
            if ($_facrec->frequency > 0) {
572
                dol_syslog("This is a recurring invoice so we set date_last_gen and next date_when");
573
                if (empty($_facrec->date_when)) {
574
                    $_facrec->date_when = $now;
575
                }
576
                $next_date = $_facrec->getNextDate(); // Calculate next date
577
                $result = $_facrec->setValueFrom('date_last_gen', $now, '', null, 'date', '', $user, '');
578
                //$_facrec->setValueFrom('nb_gen_done', $_facrec->nb_gen_done + 1);     // Not required, +1 already included into setNextDate when second param is 1.
579
                $result = $_facrec->setNextDate($next_date, 1);
580
            }
581
582
            // Define lang of customer
583
            $outputlangs = $langs;
584
            $newlang = '';
585
586
            if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && isset($this->thirdparty->default_lang)) {
587
                $newlang = $this->thirdparty->default_lang; // for proposal, order, invoice, ...
588
            }
589
            if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && isset($this->default_lang)) {
590
                $newlang = $this->default_lang; // for thirdparty
591
            }
592
            if (!empty($newlang)) {
593
                $outputlangs = new Translate("", $conf);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\Translate was not found. Did you mean Translate? If so, make sure to prefix the type with \.
Loading history...
594
                $outputlangs->setDefaultLang($newlang);
595
            }
596
597
            // Array of possible substitutions (See also file mailing-send.php that should manage same substitutions)
598
            $substitutionarray = getCommonSubstitutionArray($outputlangs, 0, null, $this);
599
            $substitutionarray['__INVOICE_PREVIOUS_MONTH__'] = dol_print_date(dol_time_plus_duree($this->date, -1, 'm'), '%m');
600
            $substitutionarray['__INVOICE_MONTH__'] = dol_print_date($this->date, '%m');
601
            $substitutionarray['__INVOICE_NEXT_MONTH__'] = dol_print_date(dol_time_plus_duree($this->date, 1, 'm'), '%m');
602
            $substitutionarray['__INVOICE_PREVIOUS_MONTH_TEXT__'] = dol_print_date(dol_time_plus_duree($this->date, -1, 'm'), '%B');
603
            $substitutionarray['__INVOICE_MONTH_TEXT__'] = dol_print_date($this->date, '%B');
604
            $substitutionarray['__INVOICE_NEXT_MONTH_TEXT__'] = dol_print_date(dol_time_plus_duree($this->date, 1, 'm'), '%B');
605
            $substitutionarray['__INVOICE_PREVIOUS_YEAR__'] = dol_print_date(dol_time_plus_duree($this->date, -1, 'y'), '%Y');
606
            $substitutionarray['__INVOICE_YEAR__'] = dol_print_date($this->date, '%Y');
607
            $substitutionarray['__INVOICE_NEXT_YEAR__'] = dol_print_date(dol_time_plus_duree($this->date, 1, 'y'), '%Y');
608
            // Only for template invoice
609
            $substitutionarray['__INVOICE_DATE_NEXT_INVOICE_BEFORE_GEN__'] = (isset($originaldatewhen) ? dol_print_date($originaldatewhen, 'dayhour') : '');
610
            $substitutionarray['__INVOICE_DATE_NEXT_INVOICE_AFTER_GEN__'] = (isset($nextdatewhen) ? dol_print_date($nextdatewhen, 'dayhour') : '');
611
            $substitutionarray['__INVOICE_PREVIOUS_DATE_NEXT_INVOICE_AFTER_GEN__'] = (isset($previousdaynextdatewhen) ? dol_print_date($previousdaynextdatewhen, 'dayhour') : '');
612
            $substitutionarray['__INVOICE_COUNTER_CURRENT__'] = $_facrec->nb_gen_done;
613
            $substitutionarray['__INVOICE_COUNTER_MAX__'] = $_facrec->nb_gen_max;
614
615
            //var_dump($substitutionarray);exit;
616
617
            complete_substitutions_array($substitutionarray, $outputlangs);
618
619
            $this->note_public = make_substitutions($this->note_public, $substitutionarray);
620
            $this->note_private = make_substitutions($this->note_private, $substitutionarray);
621
        }
622
623
        // Define due date if not already defined
624
        if (empty($forceduedate)) {
625
            $duedate = $this->calculate_date_lim_reglement();
626
            /*if ($duedate < 0) {   Regression, a date can be negative if before 1970.
627
                dol_syslog(__METHOD__ . ' Error in calculate_date_lim_reglement. We got ' . $duedate, LOG_ERR);
628
                return -1;
629
            }*/
630
            $this->date_lim_reglement = $duedate;
0 ignored issues
show
Documentation Bug introduced by
It seems like $duedate can also be of type string. However, the property $date_lim_reglement is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
631
        } else {
632
            $this->date_lim_reglement = $forceduedate;
633
        }
634
635
        // Insert into database
636
        $socid = $this->socid;
637
638
        $sql = "INSERT INTO " . MAIN_DB_PREFIX . "facture (";
639
        $sql .= " ref";
640
        $sql .= ", entity";
641
        $sql .= ", ref_ext";
642
        $sql .= ", type";
643
        $sql .= ", subtype";
644
        $sql .= ", fk_soc";
645
        $sql .= ", datec";
646
        $sql .= ", datef";
647
        $sql .= ", date_pointoftax";
648
        $sql .= ", note_private";
649
        $sql .= ", note_public";
650
        $sql .= ", ref_client";
651
        $sql .= ", fk_account";
652
        $sql .= ", module_source, pos_source, fk_fac_rec_source, fk_facture_source, fk_user_author, fk_projet";
653
        $sql .= ", fk_cond_reglement, fk_mode_reglement, date_lim_reglement, model_pdf";
654
        $sql .= ", situation_cycle_ref, situation_counter, situation_final";
655
        $sql .= ", fk_incoterms, location_incoterms";
656
        $sql .= ", fk_multicurrency";
657
        $sql .= ", multicurrency_code";
658
        $sql .= ", multicurrency_tx";
659
        $sql .= ", retained_warranty";
660
        $sql .= ", retained_warranty_date_limit";
661
        $sql .= ", retained_warranty_fk_cond_reglement";
662
        $sql .= ")";
663
        $sql .= " VALUES (";
664
        $sql .= "'(PROV)'";
665
        $sql .= ", " . setEntity($this);
666
        $sql .= ", " . ($this->ref_ext ? "'" . $this->db->escape($this->ref_ext) . "'" : "null");
667
        $sql .= ", '" . $this->db->escape($this->type) . "'";
668
        $sql .= ", " . ($this->subtype ? "'" . $this->db->escape($this->subtype) . "'" : "null");
669
        $sql .= ", " . ((int) $socid);
670
        $sql .= ", '" . $this->db->idate($this->date_creation) . "'";
671
        $sql .= ", '" . $this->db->idate($this->date) . "'";
672
        $sql .= ", " . (empty($this->date_pointoftax) ? "null" : "'" . $this->db->idate($this->date_pointoftax) . "'");
673
        $sql .= ", " . ($this->note_private ? "'" . $this->db->escape($this->note_private) . "'" : "null");
674
        $sql .= ", " . ($this->note_public ? "'" . $this->db->escape($this->note_public) . "'" : "null");
675
        $sql .= ", " . ($this->ref_customer ? "'" . $this->db->escape($this->ref_customer) . "'" : ($this->ref_client ? "'" . $this->db->escape($this->ref_client) . "'" : "null"));
676
        $sql .= ", " . ($this->fk_account > 0 ? $this->fk_account : 'NULL');
677
        $sql .= ", " . ($this->module_source ? "'" . $this->db->escape($this->module_source) . "'" : "null");
678
        $sql .= ", " . ($this->pos_source != '' ? "'" . $this->db->escape($this->pos_source) . "'" : "null");
679
        $sql .= ", " . ($this->fk_fac_rec_source ? "'" . $this->db->escape($this->fk_fac_rec_source) . "'" : "null");
680
        $sql .= ", " . ($this->fk_facture_source ? "'" . $this->db->escape($this->fk_facture_source) . "'" : "null");
681
        $sql .= ", " . ($user->id > 0 ? (int) $user->id : "null");
682
        $sql .= ", " . ($this->fk_project ? $this->fk_project : "null");
683
        $sql .= ", " . ((int) $this->cond_reglement_id);
684
        $sql .= ", " . ((int) $this->mode_reglement_id);
685
        $sql .= ", '" . $this->db->idate($this->date_lim_reglement) . "'";
686
        $sql .= ", " . (isset($this->model_pdf) ? "'" . $this->db->escape($this->model_pdf) . "'" : "null");
687
        $sql .= ", " . ($this->situation_cycle_ref ? "'" . $this->db->escape($this->situation_cycle_ref) . "'" : "null");
688
        $sql .= ", " . ($this->situation_counter ? "'" . $this->db->escape($this->situation_counter) . "'" : "null");
689
        $sql .= ", " . ($this->situation_final ? $this->situation_final : 0);
690
        $sql .= ", " . (int) $this->fk_incoterms;
691
        $sql .= ", '" . $this->db->escape($this->location_incoterms) . "'";
692
        $sql .= ", " . (int) $this->fk_multicurrency;
693
        $sql .= ", '" . $this->db->escape($this->multicurrency_code) . "'";
694
        $sql .= ", " . (float) $this->multicurrency_tx;
695
        $sql .= ", " . (empty($this->retained_warranty) ? "0" : $this->db->escape($this->retained_warranty));
696
        $sql .= ", " . (!empty($this->retained_warranty_date_limit) ? "'" . $this->db->idate($this->retained_warranty_date_limit) . "'" : 'NULL');
697
        $sql .= ", " . (int) $this->retained_warranty_fk_cond_reglement;
698
        $sql .= ")";
699
700
        $resql = $this->db->query($sql);
701
        if ($resql) {
702
            $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX . 'facture');
703
704
            // Update ref with new one
705
            $this->ref = '(PROV' . $this->id . ')';
706
            $sql = 'UPDATE ' . MAIN_DB_PREFIX . "facture SET ref='" . $this->db->escape($this->ref) . "' WHERE rowid=" . ((int) $this->id);
707
708
            $resql = $this->db->query($sql);
709
            if (!$resql) {
710
                $error++;
711
            }
712
713
            if (!empty($this->linkedObjectsIds) && empty($this->linked_objects)) {  // To use new linkedObjectsIds instead of old linked_objects
714
                $this->linked_objects = $this->linkedObjectsIds; // TODO Replace linked_objects with linkedObjectsIds
715
            }
716
717
            // Add object linked
718
            if (!$error && $this->id && !empty($this->linked_objects) && is_array($this->linked_objects)) {
719
                foreach ($this->linked_objects as $origin => $tmp_origin_id) {
720
                    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, ...))
721
                        foreach ($tmp_origin_id as $origin_id) {
722
                            $ret = $this->add_object_linked($origin, $origin_id);
723
                            if (!$ret) {
724
                                $this->error = $this->db->lasterror();
725
                                $error++;
726
                            }
727
                        }
728
                    } else { // Old behaviour, if linked_object has only one link per type, so is something like array('contract'=>id1))
729
                        $origin_id = $tmp_origin_id;
730
                        $ret = $this->add_object_linked($origin, $origin_id);
731
                        if (!$ret) {
732
                            $this->error = $this->db->lasterror();
733
                            $error++;
734
                        }
735
                    }
736
                }
737
            }
738
739
            // Propagate contacts
740
            if (!$error && $this->id && getDolGlobalString('MAIN_PROPAGATE_CONTACTS_FROM_ORIGIN') && !empty($this->origin) && !empty($this->origin_id)) {   // Get contact from origin object
741
                $originforcontact = $this->origin;
742
                $originidforcontact = $this->origin_id;
743
                if ($originforcontact == 'shipping') {     // shipment and order share the same contacts. If creating from shipment we take data of order
744
                    require_once DOL_DOCUMENT_ROOT . '/expedition/class/expedition.class.php';
745
                    $exp = new Expedition($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\Expedition was not found. Did you mean Expedition? If so, make sure to prefix the type with \.
Loading history...
746
                    $exp->fetch($this->origin_id);
747
                    $exp->fetchObjectLinked(null, '', null, '', 'OR', 1, 'sourcetype', 0);
748
                    if (count($exp->linkedObjectsIds['commande']) > 0) {
749
                        foreach ($exp->linkedObjectsIds['commande'] as $key => $value) {
750
                            $originforcontact = 'commande';
751
                            if (is_object($value)) {
752
                                $originidforcontact = $value->id;
753
                            } else {
754
                                $originidforcontact = $value;
755
                            }
756
                            break; // We take first one
757
                        }
758
                    }
759
                }
760
761
                $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";
762
                $sqlcontact .= " WHERE element_id = " . ((int) $originidforcontact) . " AND ec.fk_c_type_contact = ctc.rowid AND ctc.element = '" . $this->db->escape($originforcontact) . "'";
763
764
                $resqlcontact = $this->db->query($sqlcontact);
765
                if ($resqlcontact) {
766
                    while ($objcontact = $this->db->fetch_object($resqlcontact)) {
767
                        //print $objcontact->code.'-'.$objcontact->source.'-'.$objcontact->fk_socpeople."\n";
768
                        $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
769
                    }
770
                } else {
771
                    dol_print_error($resqlcontact);
772
                }
773
            }
774
775
            /*
776
             *  Insert lines of invoices, if not from template invoice, into database
777
             */
778
            if (!$error && empty($this->fac_rec) && count($this->lines) && is_object($this->lines[0])) {    // If this->lines is array of InvoiceLines (preferred mode)
779
                $fk_parent_line = 0;
780
781
                dol_syslog("There is " . count($this->lines) . " lines into ->lines that are InvoiceLines");
782
                foreach ($this->lines as $i => $val) {
783
                    $newinvoiceline = $this->lines[$i];
784
785
                    $newinvoiceline->context = $this->context;
786
787
                    $newinvoiceline->fk_facture = $this->id;
788
789
                    $newinvoiceline->origin = $this->lines[$i]->element;
790
                    $newinvoiceline->origin_id = $this->lines[$i]->id;
791
792
                    // Auto set date of service ?
793
                    if ($this->lines[$i]->date_start_fill == 1 && $originaldatewhen) {      // $originaldatewhen is defined when generating from recurring invoice only
794
                        $newinvoiceline->date_start = $originaldatewhen;
795
                    }
796
                    if ($this->lines[$i]->date_end_fill == 1 && $previousdaynextdatewhen) { // $previousdaynextdatewhen is defined when generating from recurring invoice only
0 ignored issues
show
Bug Best Practice introduced by
The expression $previousdaynextdatewhen of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
797
                        $newinvoiceline->date_end = $previousdaynextdatewhen;
798
                    }
799
800
                    if ($result >= 0) {
801
                        // Reset fk_parent_line for no child products and special product
802
                        if (($newinvoiceline->product_type != 9 && empty($newinvoiceline->fk_parent_line)) || $newinvoiceline->product_type == 9) {
803
                            $fk_parent_line = 0;
804
                        }
805
806
                        // Complete vat rate with code
807
                        $vatrate = $newinvoiceline->tva_tx;
808
                        if ($newinvoiceline->vat_src_code && ! preg_match('/\(.*\)/', $vatrate)) {
809
                            $vatrate .= ' (' . $newinvoiceline->vat_src_code . ')';
810
                        }
811
812
                        $newinvoiceline->fk_parent_line = $fk_parent_line;
813
814
                        if ($this->type === Facture::TYPE_REPLACEMENT && $newinvoiceline->fk_remise_except) {
815
                            $discount = new DiscountAbsolute($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\DiscountAbsolute was not found. Did you mean DiscountAbsolute? If so, make sure to prefix the type with \.
Loading history...
816
                            $discount->fetch($newinvoiceline->fk_remise_except);
817
818
                            $discountId = $soc->set_remise_except($discount->amount_ht, $user, $discount->description, $discount->tva_tx);
819
                            $newinvoiceline->fk_remise_except = $discountId;
820
                        }
821
822
                        $result = $this->addline(
823
                            $newinvoiceline->desc,
824
                            $newinvoiceline->subprice,
825
                            $newinvoiceline->qty,
826
                            $vatrate,
827
                            $newinvoiceline->localtax1_tx,
828
                            $newinvoiceline->localtax2_tx,
829
                            $newinvoiceline->fk_product,
830
                            $newinvoiceline->remise_percent,
831
                            $newinvoiceline->date_start,
832
                            $newinvoiceline->date_end,
833
                            $newinvoiceline->fk_code_ventilation,
834
                            $newinvoiceline->info_bits,
835
                            $newinvoiceline->fk_remise_except,
836
                            'HT',
837
                            0,
838
                            $newinvoiceline->product_type,
839
                            $newinvoiceline->rang,
840
                            $newinvoiceline->special_code,
841
                            $newinvoiceline->element,
842
                            $newinvoiceline->id,
843
                            $fk_parent_line,
844
                            $newinvoiceline->fk_fournprice,
845
                            $newinvoiceline->pa_ht,
846
                            $newinvoiceline->label,
847
                            $newinvoiceline->array_options,
848
                            $newinvoiceline->situation_percent,
849
                            $newinvoiceline->fk_prev_id,
850
                            $newinvoiceline->fk_unit,
851
                            $newinvoiceline->multicurrency_subprice,
852
                            $newinvoiceline->ref_ext,
853
                            1
854
                        );
855
856
                        // Defined the new fk_parent_line
857
                        if ($result > 0 && $newinvoiceline->product_type == 9) {
858
                            $fk_parent_line = $result;
859
                        }
860
                    }
861
                    if ($result < 0) {
862
                        $this->error = $newinvoiceline->error;
863
                        $this->errors = $newinvoiceline->errors;
864
                        $error++;
865
                        break;
866
                    }
867
                }
868
            } elseif (!$error && empty($this->fac_rec)) {       // If this->lines is an array of invoice line arrays
869
                $fk_parent_line = 0;
870
871
                dol_syslog("There is " . count($this->lines) . " lines into ->lines as a simple array");
872
873
                foreach ($this->lines as $i => $val) {
874
                    $line = $this->lines[$i];
875
876
                    // Test and convert into object this->lines[$i]. When coming from REST API, we may still have an array
877
                    //if (! is_object($line)) $line=json_decode(json_encode($line), false);  // convert recursively array into object.
878
                    if (!is_object($line)) {
879
                        $line = (object) $line;
880
                    }
881
882
                    if ($result >= 0) {
883
                        // Reset fk_parent_line for no child products and special product
884
                        if (($line->product_type != 9 && empty($line->fk_parent_line)) || $line->product_type == 9) {
885
                            $fk_parent_line = 0;
886
                        }
887
888
                        // Complete vat rate with code
889
                        $vatrate = $line->tva_tx;
890
                        if ($line->vat_src_code && !preg_match('/\(.*\)/', $vatrate)) {
891
                            $vatrate .= ' (' . $line->vat_src_code . ')';
892
                        }
893
894
                        if (getDolGlobalString('MAIN_CREATEFROM_KEEP_LINE_ORIGIN_INFORMATION')) {
895
                            $originid = $line->origin_id;
896
                            $origintype = $line->origin;
897
                        } else {
898
                            $originid = $line->id;
899
                            $origintype = $this->element;
900
                        }
901
902
                        // init ref_ext
903
                        if (empty($line->ref_ext)) {
904
                            $line->ref_ext = '';
905
                        }
906
907
                        $result = $this->addline(
908
                            $line->desc,
909
                            $line->subprice,
910
                            $line->qty,
911
                            $vatrate,
912
                            $line->localtax1_tx,
913
                            $line->localtax2_tx,
914
                            $line->fk_product,
915
                            $line->remise_percent,
916
                            $line->date_start,
917
                            $line->date_end,
918
                            $line->fk_code_ventilation,
919
                            $line->info_bits,
920
                            $line->fk_remise_except,
921
                            'HT',
922
                            0,
923
                            $line->product_type,
924
                            $line->rang,
925
                            $line->special_code,
926
                            $origintype,
927
                            $originid,
928
                            $fk_parent_line,
929
                            $line->fk_fournprice,
930
                            $line->pa_ht,
931
                            $line->label,
932
                            $line->array_options,
933
                            $line->situation_percent,
934
                            $line->fk_prev_id,
935
                            $line->fk_unit,
936
                            $line->multicurrency_subprice,
937
                            $line->ref_ext,
938
                            1
939
                        );
940
                        if ($result < 0) {
941
                            $this->error = $this->db->lasterror();
942
                            dol_print_error($this->db);
943
                            $this->db->rollback();
944
                            return -1;
945
                        }
946
947
                        // Defined the new fk_parent_line
948
                        if ($result > 0 && $line->product_type == 9) {
949
                            $fk_parent_line = $result;
950
                        }
951
                    }
952
                }
953
            }
954
955
            /*
956
             * Insert lines coming from the template invoice
957
             */
958
            if (!$error && $this->fac_rec > 0) {
959
                dol_syslog("There is " . count($_facrec->lines) . " lines from recurring invoice");
960
                $fk_parent_line = 0;
961
962
                foreach ($_facrec->lines as $i => $val) {
963
                    if ($_facrec->lines[$i]->fk_product) {
964
                        $prod = new Product($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\Product was not found. Did you mean Product? If so, make sure to prefix the type with \.
Loading history...
965
                        $res = $prod->fetch($_facrec->lines[$i]->fk_product);
966
                    }
967
968
                    // Reset fk_parent_line for no child products and special product
969
                    if (($_facrec->lines[$i]->product_type != 9 && empty($_facrec->lines[$i]->fk_parent_line)) || $_facrec->lines[$i]->product_type == 9) {
970
                        $fk_parent_line = 0;
971
                    }
972
973
                    // For line from template invoice, we use data from template invoice
974
                    /*
975
                    $tva_tx = get_default_tva($mysoc,$soc,$prod->id);
976
                    $tva_npr = get_default_npr($mysoc,$soc,$prod->id);
977
                    if (empty($tva_tx)) $tva_npr=0;
978
                    $localtax1_tx=get_localtax($tva_tx,1,$soc,$mysoc,$tva_npr);
979
                    $localtax2_tx=get_localtax($tva_tx,2,$soc,$mysoc,$tva_npr);
980
                    */
981
                    $tva_tx = $_facrec->lines[$i]->tva_tx . ($_facrec->lines[$i]->vat_src_code ? '(' . $_facrec->lines[$i]->vat_src_code . ')' : '');
982
                    $tva_npr = $_facrec->lines[$i]->info_bits;
983
                    if (empty($tva_tx)) {
984
                        $tva_npr = 0;
985
                    }
986
                    $localtax1_tx = $_facrec->lines[$i]->localtax1_tx;
987
                    $localtax2_tx = $_facrec->lines[$i]->localtax2_tx;
988
989
                    $fk_product_fournisseur_price = empty($_facrec->lines[$i]->fk_product_fournisseur_price) ? null : $_facrec->lines[$i]->fk_product_fournisseur_price;
990
                    $buyprice = empty($_facrec->lines[$i]->buyprice) ? 0 : $_facrec->lines[$i]->buyprice;
991
992
                    // If buyprice not defined from template invoice, we try to guess the best value
993
                    if (!$buyprice && $_facrec->lines[$i]->fk_product > 0) {
994
                        require_once DOL_DOCUMENT_ROOT . '/fourn/class/fournisseur.product.class.php';
995
                        $producttmp = new ProductFournisseur($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\ProductFournisseur was not found. Did you mean ProductFournisseur? If so, make sure to prefix the type with \.
Loading history...
996
                        $producttmp->fetch($_facrec->lines[$i]->fk_product);
997
998
                        // If margin module defined on costprice, we try the costprice
999
                        // If not defined or if module margin defined and pmp and stock module enabled, we try pmp price
1000
                        // else we get the best supplier price
1001
                        if (getDolGlobalString('MARGIN_TYPE') == 'costprice' && !empty($producttmp->cost_price)) {
1002
                            $buyprice = $producttmp->cost_price;
1003
                        } elseif (isModEnabled('stock') && (getDolGlobalString('MARGIN_TYPE') == 'costprice' || getDolGlobalString('MARGIN_TYPE') == 'pmp') && !empty($producttmp->pmp)) {
1004
                            $buyprice = $producttmp->pmp;
1005
                        } else {
1006
                            if ($producttmp->find_min_price_product_fournisseur($_facrec->lines[$i]->fk_product) > 0) {
1007
                                if ($producttmp->product_fourn_price_id > 0) {
1008
                                    $buyprice = price2num($producttmp->fourn_unitprice * (1 - $producttmp->fourn_remise_percent / 100) + $producttmp->fourn_remise, 'MU');
1009
                                }
1010
                            }
1011
                        }
1012
                    }
1013
1014
                    $result_insert = $this->addline(
1015
                        $_facrec->lines[$i]->desc,
1016
                        $_facrec->lines[$i]->subprice,
1017
                        $_facrec->lines[$i]->qty,
1018
                        $tva_tx,
1019
                        $localtax1_tx,
1020
                        $localtax2_tx,
1021
                        $_facrec->lines[$i]->fk_product,
1022
                        $_facrec->lines[$i]->remise_percent,
1023
                        ($_facrec->lines[$i]->date_start_fill == 1 && $originaldatewhen) ? $originaldatewhen : '',
1024
                        ($_facrec->lines[$i]->date_end_fill == 1 && $previousdaynextdatewhen) ? $previousdaynextdatewhen : '',
0 ignored issues
show
Bug Best Practice introduced by
The expression $previousdaynextdatewhen of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1025
                        0,
1026
                        $tva_npr,
1027
                        '',
1028
                        'HT',
1029
                        0,
1030
                        $_facrec->lines[$i]->product_type,
1031
                        $_facrec->lines[$i]->rang,
1032
                        $_facrec->lines[$i]->special_code,
1033
                        '',
1034
                        0,
1035
                        $fk_parent_line,
1036
                        $fk_product_fournisseur_price,
1037
                        $buyprice,
1038
                        $_facrec->lines[$i]->label,
1039
                        empty($_facrec->lines[$i]->array_options) ? null : $_facrec->lines[$i]->array_options,
1040
                        100,    // situation percent is undefined on recurring invoice lines
1041
                        '',
1042
                        $_facrec->lines[$i]->fk_unit,
1043
                        $_facrec->lines[$i]->multicurrency_subprice,
1044
                        $_facrec->lines[$i]->ref_ext,
1045
                        1
1046
                    );
1047
1048
                    // Defined the new fk_parent_line
1049
                    if ($result_insert > 0 && $_facrec->lines[$i]->product_type == 9) {
1050
                        $fk_parent_line = $result_insert;
1051
                    }
1052
1053
                    if ($result_insert < 0) {
1054
                        $error++;
1055
                        $this->error = $this->db->error();
1056
                        break;
1057
                    }
1058
                }
1059
            }
1060
1061
            if (!$error) {
1062
                $result = $this->update_price(1, 'auto', 0, $mysoc);
1063
                if ($result > 0) {
1064
                    $action = 'create';
1065
1066
                    // Actions on extra fields
1067
                    if (!$error) {
1068
                        $result = $this->insertExtraFields();
1069
                        if ($result < 0) {
1070
                            $error++;
1071
                        }
1072
                    }
1073
1074
                    if (!$error && !$notrigger) {
1075
                        // Call trigger
1076
                        $result = $this->call_trigger('BILL_CREATE', $user);
1077
                        if ($result < 0) {
1078
                            $error++;
1079
                        }
1080
                        // End call triggers
1081
                    }
1082
1083
                    if (!$error) {
1084
                        $this->db->commit();
1085
                        return $this->id;
1086
                    } else {
1087
                        $this->db->rollback();
1088
                        return -4;
1089
                    }
1090
                } else {
1091
                    $this->error = $langs->trans('FailedToUpdatePrice');
1092
                    $this->db->rollback();
1093
                    return -3;
1094
                }
1095
            } else {
1096
                dol_syslog(get_class($this) . "::create error " . $this->error, LOG_ERR);
1097
                $this->db->rollback();
1098
                return -2;
1099
            }
1100
        } else {
1101
            $this->error = $this->db->error();
1102
            $this->db->rollback();
1103
            return -1;
1104
        }
1105
    }
1106
1107
1108
    /**
1109
     *  Create a new invoice in database from current invoice
1110
     *
1111
     *  @param      User    $user           Object user that ask creation
1112
     *  @param      int     $invertdetail   Reverse sign of amounts for lines
1113
     *  @return     int                     Return integer <0 if KO, >0 if OK
1114
     */
1115
    public function createFromCurrent(User $user, $invertdetail = 0)
1116
    {
1117
        global $conf;
1118
1119
        // Source invoice load
1120
        $facture = new Facture($this->db);
1121
1122
        // Retrieve all extrafield
1123
        // fetch optionals attributes and labels
1124
        $this->fetch_optionals();
1125
1126
        if (!empty($this->array_options)) {
1127
            $facture->array_options = $this->array_options;
1128
        }
1129
1130
        foreach ($this->lines as &$line) {
1131
            $line->fetch_optionals(); //fetch extrafields
1132
        }
1133
1134
        $facture->fk_facture_source = $this->fk_facture_source;
1135
        $facture->type              = $this->type;
1136
        $facture->subtype           = $this->subtype;
1137
        $facture->socid             = $this->socid;
1138
        $facture->date              = $this->date;
1139
        $facture->date_pointoftax   = $this->date_pointoftax;
1140
        $facture->note_public       = $this->note_public;
1141
        $facture->note_private      = $this->note_private;
1142
        $facture->ref_client        = $this->ref_client;
1143
        $facture->model_pdf         = $this->model_pdf;
1144
        $facture->fk_project        = $this->fk_project;
1145
        $facture->cond_reglement_id = $this->cond_reglement_id;
1146
        $facture->mode_reglement_id = $this->mode_reglement_id;
1147
        //$facture->remise_absolue    = $this->remise_absolue;
1148
        //$facture->remise_percent    = $this->remise_percent;  // TODO deprecated
1149
1150
        $facture->origin            = $this->origin;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$origin has been deprecated: Use now $origin_type and $origin_id; ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1150
        /** @scrutinizer ignore-deprecated */ $facture->origin            = $this->origin;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1151
        $facture->origin_id         = $this->origin_id;
1152
        $facture->fk_account         = $this->fk_account;
1153
1154
        $facture->lines = $this->lines; // Array of lines of invoice
1155
        $facture->situation_counter = $this->situation_counter;
1156
        $facture->situation_cycle_ref = $this->situation_cycle_ref;
1157
        $facture->situation_final = $this->situation_final;
1158
1159
        $facture->retained_warranty = $this->retained_warranty;
1160
        $facture->retained_warranty_fk_cond_reglement = $this->retained_warranty_fk_cond_reglement;
1161
        $facture->retained_warranty_date_limit = $this->retained_warranty_date_limit;
1162
1163
        $facture->fk_user_author = $user->id;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$fk_user_author has been deprecated: Use $user_creation_id ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1163
        /** @scrutinizer ignore-deprecated */ $facture->fk_user_author = $user->id;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1164
1165
1166
        // Loop on each line of new invoice
1167
        foreach ($facture->lines as $i => $tmpline) {
1168
            $facture->lines[$i]->fk_prev_id = $this->lines[$i]->rowid;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocumentLine::$rowid has been deprecated: Try to use id property as possible (even if field into database is still rowid) ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1168
            $facture->lines[$i]->fk_prev_id = /** @scrutinizer ignore-deprecated */ $this->lines[$i]->rowid;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1169
            if ($invertdetail) {
1170
                $facture->lines[$i]->subprice  = -$facture->lines[$i]->subprice;
1171
                $facture->lines[$i]->total_ht  = -$facture->lines[$i]->total_ht;
1172
                $facture->lines[$i]->total_tva = -$facture->lines[$i]->total_tva;
1173
                $facture->lines[$i]->total_localtax1 = -$facture->lines[$i]->total_localtax1;
1174
                $facture->lines[$i]->total_localtax2 = -$facture->lines[$i]->total_localtax2;
1175
                $facture->lines[$i]->total_ttc = -$facture->lines[$i]->total_ttc;
1176
                $facture->lines[$i]->ref_ext = '';
1177
            }
1178
        }
1179
1180
        dol_syslog(get_class($this) . "::createFromCurrent invertdetail=" . $invertdetail . " socid=" . $this->socid . " nboflines=" . count($facture->lines));
1181
1182
        $facid = $facture->create($user);
1183
        if ($facid <= 0) {
1184
            $this->error = $facture->error;
1185
            $this->errors = $facture->errors;
1186
        } elseif ($this->type == self::TYPE_SITUATION && getDolGlobalString('INVOICE_USE_SITUATION')) {
1187
            $this->fetchObjectLinked('', '', $this->id, 'facture');
1188
1189
            foreach ($this->linkedObjectsIds as $typeObject => $Tfk_object) {
1190
                foreach ($Tfk_object as $fk_object) {
1191
                    $facture->add_object_linked($typeObject, $fk_object);
1192
                }
1193
            }
1194
1195
            $facture->add_object_linked('facture', $this->fk_facture_source);
1196
        }
1197
1198
        return $facid;
1199
    }
1200
1201
1202
    /**
1203
     *  Load an object from its id and create a new one in database
1204
     *
1205
     *  @param      User    $user           User that clone
1206
     *  @param      int     $fromid         Id of object to clone
1207
     *  @return     int                     New id of clone
1208
     */
1209
    public function createFromClone(User $user, $fromid = 0)
1210
    {
1211
        global $conf, $hookmanager;
1212
1213
        $error = 0;
1214
1215
        $object = new Facture($this->db);
1216
1217
        $this->db->begin();
1218
1219
        $object->fetch($fromid);
1220
1221
        // Load source object
1222
        $objFrom = clone $object;
1223
1224
        // Change socid if needed
1225
        if (!empty($this->socid) && $this->socid != $object->socid) {
1226
            $objsoc = new Societe($this->db);
1227
1228
            if ($objsoc->fetch($this->socid) > 0) {
1229
                $object->socid = $objsoc->id;
1230
                $object->cond_reglement_id  = (!empty($objsoc->cond_reglement_id) ? $objsoc->cond_reglement_id : 0);
1231
                $object->mode_reglement_id  = (!empty($objsoc->mode_reglement_id) ? $objsoc->mode_reglement_id : 0);
1232
                $object->fk_project = 0;
1233
                $object->fk_delivery_address = 0;
1234
            }
1235
1236
            // TODO Change product price if multi-prices
1237
        }
1238
1239
        $object->id = 0;
1240
        $object->statut = self::STATUS_DRAFT;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$statut has been deprecated: Use $status instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1240
        /** @scrutinizer ignore-deprecated */ $object->statut = self::STATUS_DRAFT;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1241
        $object->status = self::STATUS_DRAFT;
1242
1243
        // Clear fields
1244
        $object->date               = (empty($this->date) ? dol_now() : $this->date);
1245
        $object->user_creation_id   = $user->id;
1246
        $object->user_validation_id = null;
1247
        $object->fk_user_author     = $user->id;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$fk_user_author has been deprecated: Use $user_creation_id ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1247
        /** @scrutinizer ignore-deprecated */ $object->fk_user_author     = $user->id;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1248
        $object->fk_user_valid      = null;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$fk_user_valid has been deprecated: Use $user_validation_id ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1248
        /** @scrutinizer ignore-deprecated */ $object->fk_user_valid      = null;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1249
        $object->fk_facture_source  = 0;
1250
        $object->date_creation      = '';
1251
        $object->date_modification = '';
1252
        $object->date_validation    = '';
1253
        $object->ref_client         = '';
1254
        $object->close_code         = '';
1255
        $object->close_note         = '';
1256
        if (getDolGlobalInt('MAIN_DONT_KEEP_NOTE_ON_CLONING') == 1) {
1257
            $object->note_private = '';
1258
            $object->note_public = '';
1259
        }
1260
1261
        // Loop on each line of new invoice
1262
        foreach ($object->lines as $i => $line) {
1263
            if (($object->lines[$i]->info_bits & 0x02) == 0x02) {   // We do not clone line of discounts
1264
                unset($object->lines[$i]);
1265
                continue;
1266
            }
1267
1268
            // Block to update dates of service (month by month only if previously filled and similar to start and end of month)
1269
            // If it's a service with start and end dates
1270
            if (getDolGlobalString('INVOICE_AUTO_NEXT_MONTH_ON_LINES') && !empty($line->date_start) && !empty($line->date_end)) {
1271
                // Get the dates
1272
                $start = dol_getdate($line->date_start);
1273
                $end = dol_getdate($line->date_end);
1274
1275
                // Get the first and last day of the month
1276
                $first = dol_get_first_day($start['year'], $start['mon']);
1277
                $last = dol_get_last_day($end['year'], $end['mon']);
1278
1279
                //print dol_print_date(dol_mktime(0, 0, 0, $start['mon'], $start['mday'], $start['year'], 'gmt'), 'dayhour').' '.dol_print_date($first, 'dayhour').'<br>';
1280
                //print dol_mktime(23, 59, 59, $end['mon'], $end['mday'], $end['year'], 'gmt').' '.$last.'<br>';exit;
1281
                // If start date is first date of month and end date is last date of month
1282
                if (
1283
                    dol_mktime(0, 0, 0, $start['mon'], $start['mday'], $start['year'], 'gmt') == $first
1284
                    && dol_mktime(23, 59, 59, $end['mon'], $end['mday'], $end['year'], 'gmt') == $last
1285
                ) {
1286
                    $nextMonth = dol_get_next_month($end['mon'], $end['year']);
1287
                    $newFirst = dol_get_first_day($nextMonth['year'], $nextMonth['month']);
1288
                    $newLast = dol_get_last_day($nextMonth['year'], $nextMonth['month']);
1289
                    $object->lines[$i]->date_start = $newFirst;
1290
                    $object->lines[$i]->date_end = $newLast;
1291
                }
1292
            }
1293
1294
            $object->lines[$i]->ref_ext = ''; // Do not clone ref_ext
1295
        }
1296
1297
        // Create clone
1298
        $object->context['createfromclone'] = 'createfromclone';
1299
        $result = $object->create($user);
1300
        if ($result < 0) {
1301
            $error++;
1302
            $this->error = $object->error;
1303
            $this->errors = $object->errors;
1304
        } else {
1305
            // copy internal contacts
1306
            if ($object->copy_linked_contact($objFrom, 'internal') < 0) {
1307
                $error++;
1308
                $this->error = $object->error;
1309
                $this->errors = $object->errors;
1310
            } elseif ($object->socid == $objFrom->socid) {
1311
                // copy external contacts if same company
1312
                if ($object->copy_linked_contact($objFrom, 'external') < 0) {
1313
                    $error++;
1314
                    $this->error = $object->error;
1315
                    $this->errors = $object->errors;
1316
                }
1317
            }
1318
        }
1319
1320
        if (!$error) {
1321
            // Hook of thirdparty module
1322
            if (is_object($hookmanager)) {
1323
                $parameters = array('objFrom' => $objFrom);
1324
                $action = '';
1325
                $reshook = $hookmanager->executeHooks('createFrom', $parameters, $object, $action); // Note that $action and $object may have been modified by some hooks
1326
                if ($reshook < 0) {
1327
                    $this->setErrorsFromObject($hookmanager);
1328
                    $error++;
1329
                }
1330
            }
1331
        }
1332
1333
        unset($object->context['createfromclone']);
1334
1335
        // End
1336
        if (!$error) {
1337
            $this->db->commit();
1338
            return $object->id;
1339
        } else {
1340
            $this->db->rollback();
1341
            return -1;
1342
        }
1343
    }
1344
1345
    /**
1346
     *  Load an object from an order and create a new invoice into database
1347
     *
1348
     *  @param      Object          $object             Object source
1349
     *  @param      User            $user               Object user
1350
     *  @return     int                                 Return integer <0 if KO, 0 if nothing done, 1 if OK
1351
     */
1352
    public function createFromOrder($object, User $user)
1353
    {
1354
        global $conf, $hookmanager;
1355
1356
        $error = 0;
1357
1358
        // Closed order
1359
        $this->date = dol_now();
1360
        $this->source = 0;
1361
1362
        $num = count($object->lines);
1363
        for ($i = 0; $i < $num; $i++) {
1364
            $line = new FactureLigne($this->db);
0 ignored issues
show
Deprecated Code introduced by
The class DoliModules\Billing\Model\FactureLigne has been deprecated: Use Invoice instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1364
            $line = /** @scrutinizer ignore-deprecated */ new FactureLigne($this->db);
Loading history...
1365
1366
            $line->libelle          = $object->lines[$i]->libelle; // deprecated
1367
            $line->label            = $object->lines[$i]->label;
1368
            $line->desc             = $object->lines[$i]->desc;
1369
            $line->subprice         = $object->lines[$i]->subprice;
1370
            $line->total_ht         = $object->lines[$i]->total_ht;
1371
            $line->total_tva        = $object->lines[$i]->total_tva;
1372
            $line->total_localtax1  = $object->lines[$i]->total_localtax1;
1373
            $line->total_localtax2  = $object->lines[$i]->total_localtax2;
1374
            $line->total_ttc        = $object->lines[$i]->total_ttc;
1375
            $line->vat_src_code = $object->lines[$i]->vat_src_code;
1376
            $line->tva_tx = $object->lines[$i]->tva_tx;
1377
            $line->localtax1_tx     = $object->lines[$i]->localtax1_tx;
1378
            $line->localtax2_tx     = $object->lines[$i]->localtax2_tx;
1379
            $line->qty = $object->lines[$i]->qty;
1380
            $line->fk_remise_except = $object->lines[$i]->fk_remise_except;
1381
            $line->remise_percent = $object->lines[$i]->remise_percent;
1382
            $line->fk_product = $object->lines[$i]->fk_product;
1383
            $line->info_bits = $object->lines[$i]->info_bits;
1384
            $line->product_type     = $object->lines[$i]->product_type;
1385
            $line->rang = $object->lines[$i]->rang;
1386
            $line->special_code     = $object->lines[$i]->special_code;
1387
            $line->fk_parent_line = $object->lines[$i]->fk_parent_line;
1388
            $line->fk_unit = $object->lines[$i]->fk_unit;
1389
            $line->date_start = $object->lines[$i]->date_start;
1390
            $line->date_end = $object->lines[$i]->date_end;
1391
1392
            // Multicurrency
1393
            $line->fk_multicurrency = $object->lines[$i]->fk_multicurrency;
1394
            $line->multicurrency_code = $object->lines[$i]->multicurrency_code;
1395
            $line->multicurrency_subprice = $object->lines[$i]->multicurrency_subprice;
1396
            $line->multicurrency_total_ht = $object->lines[$i]->multicurrency_total_ht;
1397
            $line->multicurrency_total_tva = $object->lines[$i]->multicurrency_total_tva;
1398
            $line->multicurrency_total_ttc = $object->lines[$i]->multicurrency_total_ttc;
1399
1400
            $line->fk_fournprice = $object->lines[$i]->fk_fournprice;
1401
            $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);
1402
            $line->pa_ht            = $marginInfos[0];
1403
1404
            // get extrafields from original line
1405
            $object->lines[$i]->fetch_optionals();
1406
            foreach ($object->lines[$i]->array_options as $options_key => $value) {
1407
                $line->array_options[$options_key] = $value;
1408
            }
1409
1410
            $this->lines[$i] = $line;
1411
        }
1412
1413
        $this->socid                = $object->socid;
1414
        $this->fk_project           = $object->fk_project;
1415
        $this->fk_account = $object->fk_account;
1416
        $this->cond_reglement_id    = $object->cond_reglement_id;
1417
        $this->mode_reglement_id    = $object->mode_reglement_id;
1418
        $this->availability_id      = $object->availability_id;
1419
        $this->demand_reason_id     = $object->demand_reason_id;
1420
        $this->delivery_date        = $object->delivery_date;
1421
        $this->fk_delivery_address  = $object->fk_delivery_address; // deprecated
1422
        $this->contact_id           = $object->contact_id;
1423
        $this->ref_client           = $object->ref_client;
1424
1425
        if (!getDolGlobalString('MAIN_DISABLE_PROPAGATE_NOTES_FROM_ORIGIN')) {
1426
            $this->note_private = $object->note_private;
1427
            $this->note_public = $object->note_public;
1428
        }
1429
1430
        $this->module_source = $object->module_source;
1431
        $this->pos_source = $object->pos_source;
1432
1433
        $this->origin = $object->element;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$origin has been deprecated: Use now $origin_type and $origin_id; ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1433
        /** @scrutinizer ignore-deprecated */ $this->origin = $object->element;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1434
        $this->origin_id = $object->id;
1435
1436
        $this->fk_user_author = $user->id;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$fk_user_author has been deprecated: Use $user_creation_id ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1436
        /** @scrutinizer ignore-deprecated */ $this->fk_user_author = $user->id;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1437
1438
        // get extrafields from original line
1439
        $object->fetch_optionals();
1440
        foreach ($object->array_options as $options_key => $value) {
1441
            $this->array_options[$options_key] = $value;
1442
        }
1443
1444
        // Possibility to add external linked objects with hooks
1445
        $this->linked_objects[$this->origin] = $this->origin_id;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$origin has been deprecated: Use now $origin_type and $origin_id; ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1445
        $this->linked_objects[/** @scrutinizer ignore-deprecated */ $this->origin] = $this->origin_id;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1446
        if (!empty($object->other_linked_objects) && is_array($object->other_linked_objects)) {
1447
            $this->linked_objects = array_merge($this->linked_objects, $object->other_linked_objects);
1448
        }
1449
1450
        $ret = $this->create($user);
1451
1452
        if ($ret > 0) {
1453
            // Actions hooked (by external module)
1454
            $hookmanager->initHooks(array('invoicedao'));
1455
1456
            $parameters = array('objFrom' => $object);
1457
            $action = '';
1458
            $reshook = $hookmanager->executeHooks('createFrom', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1459
            if ($reshook < 0) {
1460
                $this->setErrorsFromObject($hookmanager);
1461
                $error++;
1462
            }
1463
1464
            if (!$error) {
1465
                return 1;
1466
            } else {
1467
                return -1;
1468
            }
1469
        } else {
1470
            return -1;
1471
        }
1472
    }
1473
1474
    /**
1475
     *  Load an object from an order and create a new invoice into database
1476
     *
1477
     *  @param      Object          $object             Object source
1478
     *  @param      User            $user               Object user
1479
     *  @param      array           $lines              Ids of lines to use for invoice. If empty, all lines will be used.
1480
     *  @return     int                                 Return integer <0 if KO, 0 if nothing done, 1 if OK
1481
     */
1482
    public function createFromContract($object, User $user, $lines = array())
1483
    {
1484
        global $conf, $hookmanager;
1485
1486
        $error = 0;
1487
1488
        // Closed order
1489
        $this->date = dol_now();
1490
        $this->source = 0;
1491
1492
        $use_all_lines = empty($lines);
1493
        $num = count($object->lines);
1494
        for ($i = 0; $i < $num; $i++) {
1495
            if (!$use_all_lines && !in_array($object->lines[$i]->id, $lines)) {
1496
                continue;
1497
            }
1498
1499
            $line = new FactureLigne($this->db);
0 ignored issues
show
Deprecated Code introduced by
The class DoliModules\Billing\Model\FactureLigne has been deprecated: Use Invoice instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1499
            $line = /** @scrutinizer ignore-deprecated */ new FactureLigne($this->db);
Loading history...
1500
1501
            $line->libelle = $object->lines[$i]->libelle; // deprecated
1502
            $line->label            = $object->lines[$i]->label;
1503
            $line->desc             = $object->lines[$i]->desc;
1504
            $line->subprice         = $object->lines[$i]->subprice;
1505
            $line->total_ht         = $object->lines[$i]->total_ht;
1506
            $line->total_tva        = $object->lines[$i]->total_tva;
1507
            $line->total_localtax1  = $object->lines[$i]->total_localtax1;
1508
            $line->total_localtax2  = $object->lines[$i]->total_localtax2;
1509
            $line->total_ttc        = $object->lines[$i]->total_ttc;
1510
            $line->vat_src_code = $object->lines[$i]->vat_src_code;
1511
            $line->tva_tx = $object->lines[$i]->tva_tx;
1512
            $line->localtax1_tx     = $object->lines[$i]->localtax1_tx;
1513
            $line->localtax2_tx     = $object->lines[$i]->localtax2_tx;
1514
            $line->qty = $object->lines[$i]->qty;
1515
            $line->fk_remise_except = $object->lines[$i]->fk_remise_except;
1516
            $line->remise_percent = $object->lines[$i]->remise_percent;
1517
            $line->fk_product = $object->lines[$i]->fk_product;
1518
            $line->info_bits = $object->lines[$i]->info_bits;
1519
            $line->product_type     = $object->lines[$i]->product_type;
1520
            $line->rang = $object->lines[$i]->rang;
1521
            $line->special_code     = $object->lines[$i]->special_code;
1522
            $line->fk_parent_line = $object->lines[$i]->fk_parent_line;
1523
            $line->fk_unit = $object->lines[$i]->fk_unit;
1524
            $line->date_start = $object->lines[$i]->date_start;
1525
            $line->date_end = $object->lines[$i]->date_end;
1526
1527
            // Multicurrency
1528
            $line->fk_multicurrency = $object->lines[$i]->fk_multicurrency;
1529
            $line->multicurrency_code = $object->lines[$i]->multicurrency_code;
1530
            $line->multicurrency_subprice = $object->lines[$i]->multicurrency_subprice;
1531
            $line->multicurrency_total_ht = $object->lines[$i]->multicurrency_total_ht;
1532
            $line->multicurrency_total_tva = $object->lines[$i]->multicurrency_total_tva;
1533
            $line->multicurrency_total_ttc = $object->lines[$i]->multicurrency_total_ttc;
1534
1535
            $line->fk_fournprice = $object->lines[$i]->fk_fournprice;
1536
            $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);
1537
            $line->pa_ht            = $marginInfos[0];
1538
1539
            // get extrafields from original line
1540
            $object->lines[$i]->fetch_optionals();
1541
            foreach ($object->lines[$i]->array_options as $options_key => $value) {
1542
                $line->array_options[$options_key] = $value;
1543
            }
1544
1545
            $this->lines[$i] = $line;
1546
        }
1547
1548
        $this->socid                = $object->socid;
1549
        $this->fk_project           = $object->fk_project;
1550
        $this->fk_account = $object->fk_account;
1551
        $this->cond_reglement_id    = $object->cond_reglement_id;
1552
        $this->mode_reglement_id    = $object->mode_reglement_id;
1553
        $this->availability_id      = $object->availability_id;
1554
        $this->demand_reason_id     = $object->demand_reason_id;
1555
        $this->delivery_date        = $object->delivery_date;
1556
        $this->fk_delivery_address  = $object->fk_delivery_address; // deprecated
1557
        $this->contact_id           = $object->contact_id;
1558
        $this->ref_client           = $object->ref_client;
1559
1560
        if (!getDolGlobalString('MAIN_DISABLE_PROPAGATE_NOTES_FROM_ORIGIN')) {
1561
            $this->note_private = $object->note_private;
1562
            $this->note_public = $object->note_public;
1563
        }
1564
1565
        $this->module_source = $object->module_source;
1566
        $this->pos_source = $object->pos_source;
1567
1568
        $this->origin = $object->element;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$origin has been deprecated: Use now $origin_type and $origin_id; ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1568
        /** @scrutinizer ignore-deprecated */ $this->origin = $object->element;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1569
        $this->origin_id = $object->id;
1570
1571
        $this->fk_user_author = $user->id;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$fk_user_author has been deprecated: Use $user_creation_id ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1571
        /** @scrutinizer ignore-deprecated */ $this->fk_user_author = $user->id;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1572
1573
        // get extrafields from original line
1574
        $object->fetch_optionals();
1575
        foreach ($object->array_options as $options_key => $value) {
1576
            $this->array_options[$options_key] = $value;
1577
        }
1578
1579
        // Possibility to add external linked objects with hooks
1580
        $this->linked_objects[$this->origin] = $this->origin_id;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$origin has been deprecated: Use now $origin_type and $origin_id; ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1580
        $this->linked_objects[/** @scrutinizer ignore-deprecated */ $this->origin] = $this->origin_id;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1581
        if (!empty($object->other_linked_objects) && is_array($object->other_linked_objects)) {
1582
            $this->linked_objects = array_merge($this->linked_objects, $object->other_linked_objects);
1583
        }
1584
1585
        $ret = $this->create($user);
1586
1587
        if ($ret > 0) {
1588
            // Actions hooked (by external module)
1589
            $hookmanager->initHooks(array('invoicedao'));
1590
1591
            $parameters = array('objFrom' => $object);
1592
            $action = '';
1593
            $reshook = $hookmanager->executeHooks('createFrom', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1594
            if ($reshook < 0) {
1595
                $this->setErrorsFromObject($hookmanager);
1596
                $error++;
1597
            }
1598
1599
            if (!$error) {
1600
                return 1;
1601
            } else {
1602
                return -1;
1603
            }
1604
        } else {
1605
            return -1;
1606
        }
1607
    }
1608
1609
    /**
1610
     * Creates a deposit from a proposal or an order by grouping lines by VAT rates
1611
     *
1612
     * @param   Propal|Commande     $origin                 The original proposal or order
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\Commande was not found. Did you mean Commande? If so, make sure to prefix the type with \.
Loading history...
Bug introduced by
The type DoliModules\Billing\Model\Propal was not found. Did you mean Propal? If so, make sure to prefix the type with \.
Loading history...
1613
     * @param   int                 $date                   Invoice date
1614
     * @param   int                 $payment_terms_id       Invoice payment terms
1615
     * @param   User                $user                   Object user
1616
     * @param   int                 $notrigger              1=Does not execute triggers, 0= execute triggers
1617
     * @param   bool                $autoValidateDeposit    Whether to aumatically validate the deposit created
1618
     * @param   array               $overrideFields         Array of fields to force values
1619
     * @return  Facture|null                                The deposit created, or null if error (populates $origin->error in this case)
1620
     */
1621
    public static function createDepositFromOrigin(CommonObject $origin, $date, $payment_terms_id, User $user, $notrigger = 0, $autoValidateDeposit = false, $overrideFields = array())
1622
    {
1623
        global $conf, $langs, $hookmanager, $action;
1624
1625
        if (! in_array($origin->element, array('propal', 'commande'))) {
1626
            $origin->error = 'ErrorCanOnlyAutomaticallyGenerateADepositFromProposalOrOrder';
1627
            return null;
1628
        }
1629
1630
        if (empty($date)) {
1631
            $origin->error = $langs->trans('ErrorFieldRequired', $langs->transnoentities('DateInvoice'));
1632
            return null;
1633
        }
1634
1635
        require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php';
1636
1637
        if ($date > (dol_get_last_hour(dol_now('tzuserrel')) + (!getDolGlobalString('INVOICE_MAX_FUTURE_DELAY') ? 0 : $conf->global->INVOICE_MAX_FUTURE_DELAY))) {
1638
            $origin->error = 'ErrorDateIsInFuture';
1639
            return null;
1640
        }
1641
1642
        if ($payment_terms_id <= 0) {
1643
            $origin->error = $langs->trans('ErrorFieldRequired', $langs->transnoentities('PaymentConditionsShort'));
1644
            return null;
1645
        }
1646
1647
        $payment_conditions_deposit_percent = getDictionaryValue('c_payment_term', 'deposit_percent', $origin->cond_reglement_id);
1648
1649
        if (empty($payment_conditions_deposit_percent)) {
1650
            $origin->error = 'ErrorPaymentConditionsNotEligibleToDepositCreation';
1651
            return null;
1652
        }
1653
1654
        if (empty($origin->deposit_percent)) {
1655
            $origin->error = $langs->trans('ErrorFieldRequired', $langs->transnoentities('DepositPercent'));
1656
            return null;
1657
        }
1658
1659
        $deposit = new self($origin->db);
1660
        $deposit->socid = $origin->socid;
1661
        $deposit->type = self::TYPE_DEPOSIT;
1662
        $deposit->fk_project = $origin->fk_project;
1663
        $deposit->ref_client = $origin->ref_client;
1664
        $deposit->date = $date;
1665
        $deposit->mode_reglement_id = $origin->mode_reglement_id;
1666
        $deposit->cond_reglement_id = $payment_terms_id;
1667
        $deposit->availability_id = $origin->availability_id;
1668
        $deposit->demand_reason_id = $origin->demand_reason_id;
1669
        $deposit->fk_account = $origin->fk_account;
1670
        $deposit->fk_incoterms = $origin->fk_incoterms;
1671
        $deposit->location_incoterms = $origin->location_incoterms;
1672
        $deposit->fk_multicurrency = $origin->fk_multicurrency;
1673
        $deposit->multicurrency_code = $origin->multicurrency_code;
1674
        $deposit->multicurrency_tx = $origin->multicurrency_tx;
1675
        $deposit->module_source = $origin->module_source;
1676
        $deposit->pos_source = $origin->pos_source;
1677
        $deposit->model_pdf = 'crabe';
1678
1679
        $modelByTypeConfName = 'FACTURE_ADDON_PDF_' . $deposit->type;
1680
1681
        if (getDolGlobalString($modelByTypeConfName)) {
1682
            $deposit->model_pdf = getDolGlobalString($modelByTypeConfName);
1683
        } elseif (getDolGlobalString('FACTURE_ADDON_PDF')) {
1684
            $deposit->model_pdf = getDolGlobalString('FACTURE_ADDON_PDF');
1685
        }
1686
1687
        if (!getDolGlobalString('MAIN_DISABLE_PROPAGATE_NOTES_FROM_ORIGIN')) {
1688
            $deposit->note_private = $origin->note_private;
1689
            $deposit->note_public = $origin->note_public;
1690
        }
1691
1692
        $deposit->origin = $origin->element;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$origin has been deprecated: Use now $origin_type and $origin_id; ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1692
        /** @scrutinizer ignore-deprecated */ $deposit->origin = $origin->element;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1693
        $deposit->origin_id = $origin->id;
1694
1695
        $origin->fetch_optionals();
1696
1697
        foreach ($origin->array_options as $extrakey => $value) {
1698
            $deposit->array_options[$extrakey] = $value;
1699
        }
1700
1701
        $deposit->linked_objects[$deposit->origin] = $deposit->origin_id;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$origin has been deprecated: Use now $origin_type and $origin_id; ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1701
        $deposit->linked_objects[/** @scrutinizer ignore-deprecated */ $deposit->origin] = $deposit->origin_id;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1702
1703
        foreach ($overrideFields as $key => $value) {
1704
            $deposit->$key = $value;
1705
        }
1706
1707
        $deposit->context['createdepositfromorigin'] = 'createdepositfromorigin';
1708
1709
        $origin->db->begin();
1710
1711
        // Facture::create() also imports contact from origin
1712
        $createReturn = $deposit->create($user, $notrigger);
1713
1714
        if ($createReturn <= 0) {
1715
            $origin->db->rollback();
1716
            $origin->error = $deposit->error;
1717
            $origin->errors = $deposit->errors;
1718
            return null;
1719
        }
1720
1721
        $amount_ttc_diff = 0;
1722
        $amountdeposit = array();
1723
        $descriptions = array();
1724
1725
        if (getDolGlobalString('MAIN_DEPOSIT_MULTI_TVA')) {
1726
            $amount = $origin->total_ttc * ($origin->deposit_percent / 100);
1727
1728
            $TTotalByTva = array();
1729
            foreach ($origin->lines as &$line) {
1730
                if (!empty($line->special_code)) {
1731
                    continue;
1732
                }
1733
                $TTotalByTva[$line->tva_tx] += $line->total_ttc;
1734
                $descriptions[$line->tva_tx] .= '<li>' . (!empty($line->product_ref) ? $line->product_ref . ' - ' : '');
1735
                $descriptions[$line->tva_tx] .= (!empty($line->product_label) ? $line->product_label . ' - ' : '');
1736
                $descriptions[$line->tva_tx] .= $langs->trans('Qty') . ' : ' . $line->qty;
1737
                $descriptions[$line->tva_tx] .= ' - ' . $langs->trans('TotalHT') . ' : ' . price($line->total_ht) . '</li>';
1738
            }
1739
1740
            foreach ($TTotalByTva as $tva => &$total) {
1741
                $coef = $total / $origin->total_ttc; // Calc coef
1742
                $am = $amount * $coef;
1743
                $amount_ttc_diff += $am;
1744
                $amountdeposit[$tva] += $am / (1 + $tva / 100); // Convert into HT for the addline
1745
            }
1746
        } else {
1747
            $totalamount = 0;
1748
            $lines = $origin->lines;
1749
            $numlines = count($lines);
1750
            for ($i = 0; $i < $numlines; $i++) {
1751
                if (empty($lines[$i]->qty)) {
1752
                    continue; // We discard qty=0, it is an option
1753
                }
1754
                if (!empty($lines[$i]->special_code)) {
1755
                    continue; // We discard special_code (frais port, ecotaxe, option, ...)
1756
                }
1757
1758
                $totalamount += $lines[$i]->total_ht; // Fixme : is it not for the customer ? Shouldn't we take total_ttc ?
1759
                $tva_tx = $lines[$i]->tva_tx;
1760
                $amountdeposit[$tva_tx] += ($lines[$i]->total_ht * $origin->deposit_percent) / 100;
1761
                $descriptions[$tva_tx] .= '<li>' . (!empty($lines[$i]->product_ref) ? $lines[$i]->product_ref . ' - ' : '');
1762
                $descriptions[$tva_tx] .= (!empty($lines[$i]->product_label) ? $lines[$i]->product_label . ' - ' : '');
1763
                $descriptions[$tva_tx] .= $langs->trans('Qty') . ' : ' . $lines[$i]->qty;
1764
                $descriptions[$tva_tx] .= ' - ' . $langs->trans('TotalHT') . ' : ' . price($lines[$i]->total_ht) . '</li>';
1765
            }
1766
1767
            if ($totalamount == 0) {
1768
                $amountdeposit[0] = 0;
1769
            }
1770
1771
            $amount_ttc_diff = $amountdeposit[0];
1772
        }
1773
1774
        foreach ($amountdeposit as $tva => $amount) {
1775
            if (empty($amount)) {
1776
                continue;
1777
            }
1778
1779
            $descline = '(DEPOSIT) (' . $origin->deposit_percent . '%) - ' . $origin->ref;
1780
1781
            // Hidden conf
1782
            if (getDolGlobalString('INVOICE_DEPOSIT_VARIABLE_MODE_DETAIL_LINES_IN_DESCRIPTION') && !empty($descriptions[$tva])) {
1783
                $descline .= '<ul>' . $descriptions[$tva] . '</ul>';
1784
            }
1785
1786
            $addlineResult = $deposit->addline(
1787
                $descline,
1788
                $amount, // subprice
1789
                1, // quantity
1790
                $tva, // vat rate
1791
                0, // localtax1_tx
1792
                0, // localtax2_tx
1793
                (!getDolGlobalString('INVOICE_PRODUCTID_DEPOSIT') ? 0 : $conf->global->INVOICE_PRODUCTID_DEPOSIT), // fk_product
1794
                0, // remise_percent
1795
                0, // date_start
1796
                0, // date_end
1797
                0,
1798
                0, // info_bits
1799
                0,
1800
                'HT',
1801
                0,
1802
                0, // product_type
1803
                1,
1804
                0, // special_code
1805
                $deposit->origin,
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$origin has been deprecated: Use now $origin_type and $origin_id; ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1805
                /** @scrutinizer ignore-deprecated */ $deposit->origin,

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1806
                0,
1807
                0,
1808
                0,
1809
                0
1810
            //,$langs->trans('Deposit') //Deprecated
1811
            );
1812
1813
            if ($addlineResult < 0) {
1814
                $origin->db->rollback();
1815
                $origin->error = $deposit->error;
1816
                $origin->errors = $deposit->errors;
1817
                return null;
1818
            }
1819
        }
1820
1821
        $diff = $deposit->total_ttc - $amount_ttc_diff;
1822
1823
        if (getDolGlobalString('MAIN_DEPOSIT_MULTI_TVA') && $diff != 0) {
1824
            $deposit->fetch_lines();
1825
            $subprice_diff = $deposit->lines[0]->subprice - $diff / (1 + $deposit->lines[0]->tva_tx / 100);
1826
1827
            $updatelineResult = $deposit->updateline(
1828
                $deposit->lines[0]->id,
1829
                $deposit->lines[0]->desc,
1830
                $subprice_diff,
1831
                $deposit->lines[0]->qty,
1832
                $deposit->lines[0]->remise_percent,
1833
                $deposit->lines[0]->date_start,
1834
                $deposit->lines[0]->date_end,
1835
                $deposit->lines[0]->tva_tx,
1836
                0,
1837
                0,
1838
                'HT',
1839
                $deposit->lines[0]->info_bits,
1840
                $deposit->lines[0]->product_type,
1841
                0,
1842
                0,
1843
                0,
1844
                $deposit->lines[0]->pa_ht,
1845
                $deposit->lines[0]->label,
1846
                0,
1847
                array(),
1848
                100
1849
            );
1850
1851
            if ($updatelineResult < 0) {
1852
                $origin->db->rollback();
1853
                $origin->error = $deposit->error;
1854
                $origin->errors = $deposit->errors;
1855
                return null;
1856
            }
1857
        }
1858
1859
        $hookmanager->initHooks(array('invoicedao'));
1860
1861
        $parameters = array('objFrom' => $origin);
1862
        $reshook = $hookmanager->executeHooks('createFrom', $parameters, $deposit, $action); // Note that $action and $object may have been
1863
        // modified by hook
1864
        if ($reshook < 0) {
1865
            $origin->db->rollback();
1866
            $origin->error = $hookmanager->error;
1867
            $origin->errors = $hookmanager->errors;
1868
            return null;
1869
        }
1870
1871
        if (!empty($autoValidateDeposit)) {
1872
            $validateReturn = $deposit->validate($user, '', 0, $notrigger);
1873
1874
            if ($validateReturn < 0) {
1875
                $origin->db->rollback();
1876
                $origin->error = $deposit->error;
1877
                $origin->errors = $deposit->errors;
1878
                return null;
1879
            }
1880
        }
1881
1882
        unset($deposit->context['createdepositfromorigin']);
1883
1884
        $origin->db->commit();
1885
1886
        return $deposit;
1887
    }
1888
1889
    /**
1890
     * getTooltipContentArray
1891
     *
1892
     * @param array $params ex option, infologin
1893
     * @since v18
1894
     * @return array
1895
     */
1896
    public function getTooltipContentArray($params)
1897
    {
1898
        global $conf, $langs, $mysoc, $user;
1899
1900
        $langs->load('bills');
1901
1902
        $datas = [];
1903
        $moretitle = $params['moretitle'] ?? '';
1904
        $picto = $this->picto;
1905
        if ($this->type == self::TYPE_REPLACEMENT) {
1906
            $picto .= 'r'; // Replacement invoice
1907
        }
1908
        if ($this->type == self::TYPE_CREDIT_NOTE) {
1909
            $picto .= 'a'; // Credit note
1910
        }
1911
        if ($this->type == self::TYPE_DEPOSIT) {
1912
            $picto .= 'd'; // Deposit invoice
1913
        }
1914
1915
        if ($user->hasRight("facture", "read")) {
1916
            $datas['picto'] = img_picto('', $picto) . ' <u class="paddingrightonly">' . $langs->trans("Invoice") . '</u>';
1917
1918
            $datas['picto'] .= '&nbsp;' . $this->getLibType(1);
1919
1920
            // Complete datas
1921
            if (!empty($params['fromajaxtooltip']) && !isset($this->alreadypaid)) {
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$alreadypaid has been deprecated: Use $totalpaid instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1921
            if (!empty($params['fromajaxtooltip']) && !isset(/** @scrutinizer ignore-deprecated */ $this->alreadypaid)) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1922
                // Load the alreadypaid field
1923
                $this->alreadypaid = $this->getSommePaiement(0);
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$alreadypaid has been deprecated: Use $totalpaid instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1923
                /** @scrutinizer ignore-deprecated */ $this->alreadypaid = $this->getSommePaiement(0);

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
Documentation Bug introduced by
It seems like $this->getSommePaiement(0) can also be of type array<string,double>. However, the property $alreadypaid is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1924
            }
1925
            if (isset($this->status) && isset($this->alreadypaid)) {
1926
                $datas['picto'] .= ' ' . $this->getLibStatut(5, $this->alreadypaid);
1927
            }
1928
            if ($moretitle) {
1929
                $datas['picto'] .= ' - ' . $moretitle;
1930
            }
1931
            if (!empty($this->ref)) {
1932
                $datas['ref'] = '<br><b>' . $langs->trans('Ref') . ':</b> ' . $this->ref;
1933
            }
1934
            if (!empty($this->ref_customer)) {
1935
                $datas['refcustomer'] = '<br><b>' . $langs->trans('RefCustomer') . ':</b> ' . $this->ref_customer;
1936
            }
1937
            if (!empty($this->date)) {
1938
                $datas['date'] = '<br><b>' . $langs->trans('Date') . ':</b> ' . dol_print_date($this->date, 'day');
1939
            }
1940
            if (!empty($this->total_ht)) {
1941
                $datas['amountht'] = '<br><b>' . $langs->trans('AmountHT') . ':</b> ' . price($this->total_ht, 0, $langs, 0, -1, -1, $conf->currency);
1942
            }
1943
            if (!empty($this->total_tva)) {
1944
                $datas['amountvat'] = '<br><b>' . $langs->trans('AmountVAT') . ':</b> ' . price($this->total_tva, 0, $langs, 0, -1, -1, $conf->currency);
1945
            }
1946
            if (!empty($this->revenuestamp) && $this->revenuestamp != 0) {
1947
                $datas['amountrevenustamp'] = '<br><b>' . $langs->trans('RevenueStamp') . ':</b> ' . price($this->revenuestamp, 0, $langs, 0, -1, -1, $conf->currency);
1948
            }
1949
            if (!empty($this->total_localtax1) && $this->total_localtax1 != 0) {
1950
                // We keep test != 0 because $this->total_localtax1 can be '0.00000000'
1951
                $datas['amountlt1'] = '<br><b>' . $langs->transcountry('AmountLT1', $mysoc->country_code) . ':</b> ' . price($this->total_localtax1, 0, $langs, 0, -1, -1, $conf->currency);
1952
            }
1953
            if (!empty($this->total_localtax2) && $this->total_localtax2 != 0) {
1954
                $datas['amountlt2'] = '<br><b>' . $langs->transcountry('AmountLT2', $mysoc->country_code) . ':</b> ' . price($this->total_localtax2, 0, $langs, 0, -1, -1, $conf->currency);
1955
            }
1956
            if (!empty($this->total_ttc)) {
1957
                $datas['amountttc'] = '<br><b>' . $langs->trans('AmountTTC') . ':</b> ' . price($this->total_ttc, 0, $langs, 0, -1, -1, $conf->currency);
1958
            }
1959
        }
1960
1961
        return $datas;
1962
    }
1963
1964
    /**
1965
     *  Return clicable link of object (with eventually picto)
1966
     *
1967
     *  @param  int     $withpicto                  Add picto into link
1968
     *  @param  string  $option                     Where point the link
1969
     *  @param  int     $max                        Maxlength of ref
1970
     *  @param  int     $short                      1=Return just URL
1971
     *  @param  string  $moretitle                  Add more text to title tooltip
1972
     *  @param  int     $notooltip                  1=Disable tooltip
1973
     *  @param  int     $addlinktonotes             1=Add link to notes
1974
     *  @param  int     $save_lastsearch_value      -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
1975
     *  @param  string  $target                     Target of link ('', '_self', '_blank', '_parent', '_backoffice', ...)
1976
     *  @return string                              String with URL
1977
     */
1978
    public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $moretitle = '', $notooltip = 0, $addlinktonotes = 0, $save_lastsearch_value = -1, $target = '')
1979
    {
1980
        global $langs, $conf, $user, $mysoc;
1981
1982
        if (!empty($conf->dol_no_mouse_hover)) {
1983
            $notooltip = 1; // Force disable tooltips
1984
        }
1985
1986
        $result = '';
1987
1988
        if ($option == 'withdraw') {
1989
            $url = DOL_URL_ROOT . '/compta/facture/prelevement.php?facid=' . $this->id;
1990
        } else {
1991
            $url = DOL_URL_ROOT . '/compta/facture/card.php?id=' . $this->id;
1992
        }
1993
1994
        if (!$user->hasRight("facture", "read")) {
1995
            $option = 'nolink';
1996
        }
1997
1998
        if ($option !== 'nolink') {
1999
            // Add param to save lastsearch_values or not
2000
            $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
2001
            if ($save_lastsearch_value == -1 && isset($_SERVER['PHP_SELF']) && preg_match('/list\.php/', $_SERVER['PHP_SELF'])) {
2002
                $add_save_lastsearch_values = 1;
2003
            }
2004
            if ($add_save_lastsearch_values) {
2005
                $url .= '&save_lastsearch_values=1';
2006
            }
2007
        }
2008
2009
        if ($short) {
2010
            return $url;
2011
        }
2012
2013
        $picto = $this->picto;
2014
        if ($this->type == self::TYPE_REPLACEMENT) {
2015
            $picto .= 'r'; // Replacement invoice
2016
        }
2017
        if ($this->type == self::TYPE_CREDIT_NOTE) {
2018
            $picto .= 'a'; // Credit note
2019
        }
2020
        if ($this->type == self::TYPE_DEPOSIT) {
2021
            $picto .= 'd'; // Deposit invoice
2022
        }
2023
        $params = [
2024
            'id' => $this->id,
2025
            'objecttype' => $this->element,
2026
            'moretitle' => $moretitle,
2027
            'option' => $option,
2028
        ];
2029
        $classfortooltip = 'classfortooltip';
2030
        $dataparams = '';
2031
        if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
2032
            $classfortooltip = 'classforajaxtooltip';
2033
            $dataparams = ' data-params="' . dol_escape_htmltag(json_encode($params)) . '"';
2034
            $label = '';
2035
        } else {
2036
            $label = implode($this->getTooltipContentArray($params));
2037
        }
2038
2039
        $linkclose = ($target ? ' target="' . $target . '"' : '');
2040
        if (empty($notooltip) && $user->hasRight("facture", "read")) {
2041
            if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
2042
                $label = $langs->trans("Invoice");
2043
                $linkclose .= ' alt="' . dol_escape_htmltag($label, 1) . '"';
2044
            }
2045
            $linkclose .= ($label ? ' title="' . dol_escape_htmltag($label, 1) . '"' : ' title="tocomplete"');
2046
            $linkclose .= $dataparams . ' class="' . $classfortooltip . '"';
2047
        }
2048
2049
        $linkstart = '<a href="' . $url . '"';
2050
        $linkstart .= $linkclose . '>';
2051
        $linkend = '</a>';
2052
2053
        if ($option == 'nolink') {
2054
            $linkstart = '';
2055
            $linkend = '';
2056
        }
2057
2058
        $result .= $linkstart;
2059
        if ($withpicto) {
2060
            $result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="' . (($withpicto != 2) ? 'paddingright ' : '') . '"'), 0, 0, $notooltip ? 0 : 1);
2061
        }
2062
        if ($withpicto != 2) {
2063
            $result .= ($max ? dol_trunc($this->ref, $max) : $this->ref);
2064
        }
2065
        $result .= $linkend;
2066
2067
        if ($addlinktonotes) {
2068
            $txttoshow = ($user->socid > 0 ? $this->note_public : $this->note_private);
2069
            if ($txttoshow) {
2070
                //$notetoshow = $langs->trans("ViewPrivateNote").':<br>'.dol_string_nohtmltag($txttoshow, 1);
2071
                $notetoshow = $langs->trans("ViewPrivateNote") . ':<br>' . $txttoshow;
2072
                $result .= ' <span class="note inline-block">';
2073
                $result .= '<a href="' . DOL_URL_ROOT . '/compta/facture/note.php?id=' . $this->id . '" class="classfortooltip" title="' . dol_escape_htmltag($notetoshow, 1, 1) . '">';
2074
                $result .= img_picto('', 'note');
2075
                $result .= '</a>';
2076
                //$result.=img_picto($langs->trans("ViewNote"),'object_generic');
2077
                //$result.='</a>';
2078
                $result .= '</span>';
2079
            }
2080
        }
2081
2082
        global $action, $hookmanager;
2083
        $hookmanager->initHooks(array('invoicedao'));
2084
        $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'notooltip' => $notooltip, 'addlinktonotes' => $addlinktonotes, 'save_lastsearch_value' => $save_lastsearch_value, 'target' => $target);
2085
        $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
2086
        if ($reshook > 0) {
2087
            $result = $hookmanager->resPrint;
2088
        } else {
2089
            $result .= $hookmanager->resPrint;
2090
        }
2091
2092
        return $result;
2093
    }
2094
2095
    /**
2096
     *  Get object from database. Get also lines.
2097
     *
2098
     *  @param      int     $rowid              Id of object to load
2099
     *  @param      string  $ref                Reference of invoice
2100
     *  @param      string  $ref_ext            External reference of invoice
2101
     *  @param      int     $notused            Not used
2102
     *  @param      bool    $fetch_situation    Load also the previous and next situation invoice into $tab_previous_situation_invoice and $tab_next_situation_invoice
2103
     *  @return     int                         >0 if OK, <0 if KO, 0 if not found
2104
     */
2105
    public function fetch($rowid, $ref = '', $ref_ext = '', $notused = 0, $fetch_situation = false)
2106
    {
2107
        if (empty($rowid) && empty($ref) && empty($ref_ext)) {
2108
            return -1;
2109
        }
2110
2111
        $sql = 'SELECT f.rowid, f.entity, f.ref, f.ref_client, f.ref_ext, f.type, f.subtype, f.fk_soc';
2112
        $sql .= ', f.total_tva, f.localtax1, f.localtax2, f.total_ht, f.total_ttc, f.revenuestamp';
2113
        $sql .= ', f.datef as df, f.date_pointoftax';
2114
        $sql .= ', f.date_lim_reglement as dlr';
2115
        $sql .= ', f.datec as datec';
2116
        $sql .= ', f.date_valid as datev';
2117
        $sql .= ', f.tms as datem';
2118
        $sql .= ', f.note_private, f.note_public, f.fk_statut as status, 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';
2119
        $sql .= ', f.fk_facture_source, f.fk_fac_rec_source';
2120
        $sql .= ', f.fk_mode_reglement, f.fk_cond_reglement, f.fk_projet as fk_project, f.extraparams';
2121
        $sql .= ', f.situation_cycle_ref, f.situation_counter, f.situation_final';
2122
        $sql .= ', f.fk_account';
2123
        $sql .= ", f.fk_multicurrency, f.multicurrency_code, f.multicurrency_tx, f.multicurrency_total_ht, f.multicurrency_total_tva, f.multicurrency_total_ttc";
2124
        $sql .= ', p.code as mode_reglement_code, p.libelle as mode_reglement_libelle';
2125
        $sql .= ', c.code as cond_reglement_code, c.libelle as cond_reglement_libelle, c.libelle_facture as cond_reglement_libelle_doc';
2126
        $sql .= ', f.fk_incoterms, f.location_incoterms';
2127
        $sql .= ', f.module_source, f.pos_source';
2128
        $sql .= ", i.libelle as label_incoterms";
2129
        $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";
2130
        $sql .= ' FROM ' . MAIN_DB_PREFIX . 'facture as f';
2131
        $sql .= ' LEFT JOIN ' . MAIN_DB_PREFIX . 'c_payment_term as c ON f.fk_cond_reglement = c.rowid';
2132
        $sql .= ' LEFT JOIN ' . MAIN_DB_PREFIX . 'c_paiement as p ON f.fk_mode_reglement = p.id';
2133
        $sql .= ' LEFT JOIN ' . MAIN_DB_PREFIX . 'c_incoterms as i ON f.fk_incoterms = i.rowid';
2134
2135
        if ($rowid) {
2136
            $sql .= " WHERE f.rowid = " . ((int) $rowid);
2137
        } else {
2138
            $sql .= ' WHERE f.entity IN (' . getEntity('invoice') . ')'; // Don't use entity if you use rowid
2139
            if ($ref) {
2140
                $sql .= " AND f.ref = '" . $this->db->escape($ref) . "'";
2141
            }
2142
            if ($ref_ext) {
2143
                $sql .= " AND f.ref_ext = '" . $this->db->escape($ref_ext) . "'";
2144
            }
2145
        }
2146
2147
        dol_syslog(get_class($this) . "::fetch", LOG_DEBUG);
2148
        $resql = $this->db->query($sql);
2149
        if ($resql) {
2150
            if ($this->db->num_rows($resql)) {
2151
                $obj = $this->db->fetch_object($resql);
2152
2153
                $this->id = $obj->rowid;
2154
                $this->entity = $obj->entity;
2155
2156
                $this->ref                  = $obj->ref;
2157
                $this->ref_client           = $obj->ref_client;
2158
                $this->ref_customer         = $obj->ref_client;
2159
                $this->ref_ext              = $obj->ref_ext;
2160
                $this->type                 = $obj->type;
2161
                $this->subtype              = $obj->subtype;
2162
                $this->date                 = $this->db->jdate($obj->df);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->jdate($obj->df) can also be of type string. However, the property $date is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2163
                $this->date_pointoftax      = $this->db->jdate($obj->date_pointoftax);
2164
                $this->date_creation        = $this->db->jdate($obj->datec);
2165
                $this->date_validation      = $this->db->jdate($obj->datev);
2166
                $this->date_modification    = $this->db->jdate($obj->datem);
2167
                $this->datem                = $this->db->jdate($obj->datem);
2168
                $this->total_ht             = $obj->total_ht;
2169
                $this->total_tva            = $obj->total_tva;
2170
                $this->total_localtax1      = $obj->localtax1;
2171
                $this->total_localtax2      = $obj->localtax2;
2172
                $this->total_ttc            = $obj->total_ttc;
2173
                $this->revenuestamp         = $obj->revenuestamp;
2174
                $this->paye                 = $obj->paye;
2175
                $this->close_code           = $obj->close_code;
2176
                $this->close_note           = $obj->close_note;
2177
2178
                $this->socid = $obj->fk_soc;
2179
                $this->thirdparty = null; // Clear if another value was already set by fetch_thirdparty
2180
2181
                $this->fk_project = $obj->fk_project;
2182
                $this->project = null; // Clear if another value was already set by fetch_projet
2183
2184
                $this->statut = $obj->status;   // deprecated
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$statut has been deprecated: Use $status instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2184
                /** @scrutinizer ignore-deprecated */ $this->statut = $obj->status;   // deprecated

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
2185
                $this->status = $obj->status;
2186
2187
                $this->date_lim_reglement = $this->db->jdate($obj->dlr);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->jdate($obj->dlr) can also be of type string. However, the property $date_lim_reglement is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2188
                $this->mode_reglement_id    = $obj->fk_mode_reglement;
2189
                $this->mode_reglement_code  = $obj->mode_reglement_code;
2190
                $this->mode_reglement       = $obj->mode_reglement_libelle;
2191
                $this->cond_reglement_id    = $obj->fk_cond_reglement;
2192
                $this->cond_reglement_code  = $obj->cond_reglement_code;
2193
                $this->cond_reglement       = $obj->cond_reglement_libelle;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$cond_reglement has been deprecated: Use $cond_reglement_id instead - Kept for compatibility ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2193
                /** @scrutinizer ignore-deprecated */ $this->cond_reglement       = $obj->cond_reglement_libelle;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
2194
                $this->cond_reglement_doc = $obj->cond_reglement_libelle_doc;
2195
                $this->fk_account = ($obj->fk_account > 0) ? $obj->fk_account : null;
2196
                $this->fk_facture_source    = $obj->fk_facture_source;
2197
                $this->fk_fac_rec_source    = $obj->fk_fac_rec_source;
2198
                $this->note = $obj->note_private; // deprecated
2199
                $this->note_private = $obj->note_private;
2200
                $this->note_public          = $obj->note_public;
2201
                $this->user_creation_id     = $obj->fk_user_author;
2202
                $this->user_validation_id   = $obj->fk_user_valid;
2203
                $this->user_modification_id = $obj->fk_user_modif;
2204
                $this->fk_user_author       = $obj->fk_user_author;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$fk_user_author has been deprecated: Use $user_creation_id ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2204
                /** @scrutinizer ignore-deprecated */ $this->fk_user_author       = $obj->fk_user_author;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
2205
                $this->fk_user_valid        = $obj->fk_user_valid;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$fk_user_valid has been deprecated: Use $user_validation_id ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2205
                /** @scrutinizer ignore-deprecated */ $this->fk_user_valid        = $obj->fk_user_valid;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
2206
                $this->fk_user_modif        = $obj->fk_user_modif;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$fk_user_modif has been deprecated: Use $user_modification_id ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2206
                /** @scrutinizer ignore-deprecated */ $this->fk_user_modif        = $obj->fk_user_modif;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
2207
                $this->model_pdf = $obj->model_pdf;
2208
                $this->last_main_doc = $obj->last_main_doc;
2209
                $this->situation_cycle_ref  = $obj->situation_cycle_ref;
2210
                $this->situation_counter    = $obj->situation_counter;
2211
                $this->situation_final      = $obj->situation_final;
2212
                $this->retained_warranty    = $obj->retained_warranty;
2213
                $this->retained_warranty_date_limit         = $this->db->jdate($obj->retained_warranty_date_limit);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->jdate($obj->r...ed_warranty_date_limit) can also be of type string. However, the property $retained_warranty_date_limit is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2214
                $this->retained_warranty_fk_cond_reglement  = $obj->retained_warranty_fk_cond_reglement;
2215
2216
                $this->extraparams = !empty($obj->extraparams) ? (array) json_decode($obj->extraparams, true) : array();
2217
2218
                //Incoterms
2219
                $this->fk_incoterms         = $obj->fk_incoterms;
2220
                $this->location_incoterms   = $obj->location_incoterms;
2221
                $this->label_incoterms = $obj->label_incoterms;
2222
2223
                $this->module_source = $obj->module_source;
2224
                $this->pos_source = $obj->pos_source;
2225
2226
                // Multicurrency
2227
                $this->fk_multicurrency         = $obj->fk_multicurrency;
2228
                $this->multicurrency_code = $obj->multicurrency_code;
2229
                $this->multicurrency_tx         = $obj->multicurrency_tx;
2230
                $this->multicurrency_total_ht = $obj->multicurrency_total_ht;
2231
                $this->multicurrency_total_tva  = $obj->multicurrency_total_tva;
2232
                $this->multicurrency_total_ttc  = $obj->multicurrency_total_ttc;
2233
2234
                if (($this->type == self::TYPE_SITUATION || ($this->type == self::TYPE_CREDIT_NOTE && $this->situation_cycle_ref > 0)) && $fetch_situation) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($this->type == self::TY... 0) && $fetch_situation, Probably Intended Meaning: $this->type == self::TYP... 0 && $fetch_situation)
Loading history...
2235
                    $this->fetchPreviousNextSituationInvoice();
2236
                }
2237
2238
                // Retrieve all extrafield
2239
                // fetch optionals attributes and labels
2240
                $this->fetch_optionals();
2241
2242
                // Lines
2243
                $this->lines = array();
2244
2245
                $result = $this->fetch_lines();
2246
                if ($result < 0) {
2247
                    $this->error = $this->db->error();
2248
                    return -3;
2249
                }
2250
2251
                $this->db->free($resql);
2252
2253
                return 1;
2254
            } else {
2255
                $this->error = 'Invoice with id=' . $rowid . ' or ref=' . $ref . ' or ref_ext=' . $ref_ext . ' not found';
2256
2257
                dol_syslog(__METHOD__ . $this->error, LOG_WARNING);
2258
                return 0;
2259
            }
2260
        } else {
2261
            $this->error = $this->db->lasterror();
2262
            return -1;
2263
        }
2264
    }
2265
2266
2267
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2268
    /**
2269
     *  Load all detailed lines into this->lines
2270
     *
2271
     *  @param      int     $only_product   Return only physical products
2272
     *  @param      int     $loadalsotranslation    Return translation for products
2273
     *
2274
     *  @return     int         1 if OK, < 0 if KO
2275
     */
2276
    public function fetch_lines($only_product = 0, $loadalsotranslation = 0)
2277
    {
2278
        // phpcs:enable
2279
        $this->lines = array();
2280
2281
        $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,';
2282
        $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,';
2283
        $sql .= ' l.situation_percent, l.fk_prev_id,';
2284
        $sql .= ' l.rang, l.special_code, l.batch, l.fk_warehouse,';
2285
        $sql .= ' l.date_start as date_start, l.date_end as date_end,';
2286
        $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,';
2287
        $sql .= ' l.fk_unit,';
2288
        $sql .= ' l.fk_multicurrency, l.multicurrency_code, l.multicurrency_subprice, l.multicurrency_total_ht, l.multicurrency_total_tva, l.multicurrency_total_ttc,';
2289
        $sql .= ' p.ref as product_ref, p.fk_product_type as fk_product_type, p.label as product_label, p.description as product_desc, p.barcode as product_barcode';
2290
        $sql .= ' FROM ' . MAIN_DB_PREFIX . 'facturedet as l';
2291
        $sql .= ' LEFT JOIN ' . MAIN_DB_PREFIX . 'product as p ON l.fk_product = p.rowid';
2292
        $sql .= ' WHERE l.fk_facture = ' . ((int) $this->id);
2293
        $sql .= ' ORDER BY l.rang, l.rowid';
2294
2295
        dol_syslog(get_class($this) . '::fetch_lines', LOG_DEBUG);
2296
        $result = $this->db->query($sql);
2297
        if ($result) {
2298
            $num = $this->db->num_rows($result);
2299
            $i = 0;
2300
            while ($i < $num) {
2301
                $objp = $this->db->fetch_object($result);
2302
                $line = new FactureLigne($this->db);
0 ignored issues
show
Deprecated Code introduced by
The class DoliModules\Billing\Model\FactureLigne has been deprecated: Use Invoice instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2302
                $line = /** @scrutinizer ignore-deprecated */ new FactureLigne($this->db);
Loading history...
2303
2304
                $line->id               = $objp->rowid;
2305
                $line->rowid = $objp->rowid; // deprecated
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocumentLine::$rowid has been deprecated: Try to use id property as possible (even if field into database is still rowid) ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2305
                /** @scrutinizer ignore-deprecated */ $line->rowid = $objp->rowid; // deprecated

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
2306
                $line->fk_facture       = $objp->fk_facture;
2307
                $line->label            = $objp->custom_label; // deprecated
2308
                $line->desc             = $objp->description; // Description line
2309
                $line->description      = $objp->description; // Description line
2310
                $line->product_type     = $objp->product_type; // Type of line
2311
                $line->ref              = $objp->product_ref; // Ref product
2312
                $line->product_ref      = $objp->product_ref; // Ref product
2313
                $line->libelle          = $objp->product_label; // deprecated
2314
                $line->product_label    = $objp->product_label; // Label product
2315
                $line->product_barcode  = $objp->product_barcode; // Barcode number product
2316
                $line->product_desc     = $objp->product_desc; // Description product
2317
                $line->fk_product_type  = $objp->fk_product_type; // Type of product
2318
                $line->qty              = $objp->qty;
2319
                $line->subprice         = $objp->subprice;
2320
                $line->ref_ext          = $objp->ref_ext; // line external ref
2321
2322
                $line->vat_src_code = $objp->vat_src_code;
2323
                $line->tva_tx           = $objp->tva_tx;
2324
                $line->localtax1_tx     = $objp->localtax1_tx;
2325
                $line->localtax2_tx     = $objp->localtax2_tx;
2326
                $line->localtax1_type   = $objp->localtax1_type;
2327
                $line->localtax2_type   = $objp->localtax2_type;
2328
                $line->remise_percent   = $objp->remise_percent;
2329
                $line->fk_remise_except = $objp->fk_remise_except;
2330
                $line->fk_product       = $objp->fk_product;
2331
                $line->date_start       = $this->db->jdate($objp->date_start);
2332
                $line->date_end         = $this->db->jdate($objp->date_end);
2333
                $line->info_bits        = $objp->info_bits;
2334
                $line->total_ht         = $objp->total_ht;
2335
                $line->total_tva        = $objp->total_tva;
2336
                $line->total_localtax1  = $objp->total_localtax1;
2337
                $line->total_localtax2  = $objp->total_localtax2;
2338
                $line->total_ttc        = $objp->total_ttc;
2339
2340
                $line->fk_fournprice = $objp->fk_fournprice;
2341
                $marginInfos = getMarginInfos($objp->subprice, $objp->remise_percent, $objp->tva_tx, $objp->localtax1_tx, $objp->localtax2_tx, $line->fk_fournprice, $objp->pa_ht);
2342
                $line->pa_ht = $marginInfos[0];
2343
                $line->marge_tx         = $marginInfos[1];
2344
                $line->marque_tx        = $marginInfos[2];
2345
                $line->rang = $objp->rang;
2346
                $line->special_code = $objp->special_code;
2347
                $line->fk_parent_line = $objp->fk_parent_line;
2348
                $line->situation_percent = $objp->situation_percent;
2349
                $line->fk_prev_id = $objp->fk_prev_id;
2350
                $line->fk_unit = $objp->fk_unit;
2351
2352
                $line->batch = $objp->batch;
2353
                $line->fk_warehouse = $objp->fk_warehouse;
2354
2355
                // Accountancy
2356
                $line->fk_accounting_account = $objp->fk_code_ventilation;
2357
2358
                // Multicurrency
2359
                $line->fk_multicurrency = $objp->fk_multicurrency;
2360
                $line->multicurrency_code = $objp->multicurrency_code;
2361
                $line->multicurrency_subprice   = $objp->multicurrency_subprice;
2362
                $line->multicurrency_total_ht   = $objp->multicurrency_total_ht;
2363
                $line->multicurrency_total_tva  = $objp->multicurrency_total_tva;
2364
                $line->multicurrency_total_ttc  = $objp->multicurrency_total_ttc;
2365
2366
                $line->fetch_optionals();
2367
2368
                // multilangs
2369
                if (getDolGlobalInt('MAIN_MULTILANGS') && !empty($objp->fk_product) && !empty($loadalsotranslation)) {
2370
                    $tmpproduct = new Product($this->db);
2371
                    $tmpproduct->fetch($objp->fk_product);
2372
                    $tmpproduct->getMultiLangs();
2373
2374
                    $line->multilangs = $tmpproduct->multilangs;
2375
                }
2376
2377
                $this->lines[$i] = $line;
2378
2379
                $i++;
2380
            }
2381
            $this->db->free($result);
2382
            return 1;
2383
        } else {
2384
            $this->error = $this->db->error();
2385
            return -3;
2386
        }
2387
    }
2388
2389
    /**
2390
     * Fetch previous and next situations invoices.
2391
     * Return all previous and next invoices (both standard and credit notes).
2392
     *
2393
     * @return  void
2394
     */
2395
    public function fetchPreviousNextSituationInvoice()
2396
    {
2397
        global $conf;
2398
2399
        $this->tab_previous_situation_invoice = array();
2400
        $this->tab_next_situation_invoice = array();
2401
2402
        $sql = 'SELECT rowid, type, situation_cycle_ref, situation_counter FROM ' . MAIN_DB_PREFIX . 'facture';
2403
        $sql .= " WHERE rowid <> " . ((int) $this->id);
2404
        $sql .= ' AND entity = ' . ((int) $this->entity);
2405
        $sql .= ' AND situation_cycle_ref = ' . (int) $this->situation_cycle_ref;
2406
        $sql .= ' ORDER BY situation_counter ASC';
2407
2408
        dol_syslog(get_class($this) . '::fetchPreviousNextSituationInvoice ', LOG_DEBUG);
2409
        $result = $this->db->query($sql);
2410
        if ($result && $this->db->num_rows($result) > 0) {
2411
            while ($objp = $this->db->fetch_object($result)) {
2412
                $invoice = new Facture($this->db);
2413
                if ($invoice->fetch($objp->rowid) > 0) {
2414
                    if (
2415
                        $objp->situation_counter < $this->situation_counter
2416
                        || ($objp->situation_counter == $this->situation_counter && $objp->rowid < $this->id) // This case appear when there are credit notes
2417
                    ) {
2418
                        $this->tab_previous_situation_invoice[] = $invoice;
2419
                    } else {
2420
                        $this->tab_next_situation_invoice[] = $invoice;
2421
                    }
2422
                }
2423
            }
2424
        }
2425
    }
2426
2427
    /**
2428
     *      Update database
2429
     *
2430
     *      @param      User    $user           User that modify
2431
     *      @param      int     $notrigger      0=launch triggers after, 1=disable triggers
2432
     *      @return     int                     Return integer <0 if KO, >0 if OK
2433
     */
2434
    public function update(User $user, $notrigger = 0)
2435
    {
2436
        $error = 0;
2437
2438
        // Clean parameters
2439
        if (empty($this->type)) {
2440
            $this->type = self::TYPE_STANDARD;
2441
        }
2442
        if (isset($this->subtype)) {
2443
            $this->subtype = (int) trim($this->subtype);
2444
        }
2445
        if (isset($this->ref)) {
2446
            $this->ref = trim($this->ref);
2447
        }
2448
        if (isset($this->ref_ext)) {
2449
            $this->ref_ext = trim($this->ref_ext);
2450
        }
2451
        if (isset($this->ref_client)) {
2452
            $this->ref_client = trim($this->ref_client);
2453
        }
2454
        if (isset($this->increment)) {
2455
            $this->increment = trim($this->increment);
2456
        }
2457
        if (isset($this->close_code)) {
2458
            $this->close_code = trim($this->close_code);
2459
        }
2460
        if (isset($this->close_note)) {
2461
            $this->close_note = trim($this->close_note);
2462
        }
2463
        if (isset($this->note) || isset($this->note_private)) {
2464
            $this->note = (isset($this->note) ? trim($this->note) : trim($this->note_private)); // deprecated
2465
        }
2466
        if (isset($this->note) || isset($this->note_private)) {
2467
            $this->note_private = (isset($this->note_private) ? trim($this->note_private) : trim($this->note));
2468
        }
2469
        if (isset($this->note_public)) {
2470
            $this->note_public = trim($this->note_public);
2471
        }
2472
        if (isset($this->model_pdf)) {
2473
            $this->model_pdf = trim($this->model_pdf);
2474
        }
2475
        if (isset($this->import_key)) {
2476
            $this->import_key = trim($this->import_key);
2477
        }
2478
        if (isset($this->retained_warranty)) {
2479
            $this->retained_warranty = (float) $this->retained_warranty;
2480
        }
2481
2482
2483
        // Check parameters
2484
        // Put here code to add control on parameters values
2485
2486
        // Update request
2487
        $sql = "UPDATE " . MAIN_DB_PREFIX . "facture SET";
2488
        $sql .= " ref=" . (isset($this->ref) ? "'" . $this->db->escape($this->ref) . "'" : "null") . ",";
2489
        $sql .= " ref_ext=" . (isset($this->ref_ext) ? "'" . $this->db->escape($this->ref_ext) . "'" : "null") . ",";
2490
        $sql .= " type=" . (isset($this->type) ? $this->db->escape($this->type) : "null") . ",";
2491
        $sql .= " subtype=" . (isset($this->subtype) ? $this->db->escape($this->subtype) : "null") . ",";
2492
        $sql .= " ref_client=" . (isset($this->ref_client) ? "'" . $this->db->escape($this->ref_client) . "'" : "null") . ",";
2493
        $sql .= " increment=" . (isset($this->increment) ? "'" . $this->db->escape($this->increment) . "'" : "null") . ",";
2494
        $sql .= " fk_soc=" . (isset($this->socid) ? $this->db->escape($this->socid) : "null") . ",";
2495
        $sql .= " datec=" . (strval($this->date_creation) != '' ? "'" . $this->db->idate($this->date_creation) . "'" : 'null') . ",";
2496
        $sql .= " datef=" . (strval($this->date) != '' ? "'" . $this->db->idate($this->date) . "'" : 'null') . ",";
2497
        $sql .= " date_pointoftax=" . (strval($this->date_pointoftax) != '' ? "'" . $this->db->idate($this->date_pointoftax) . "'" : 'null') . ",";
2498
        $sql .= " date_valid=" . (strval($this->date_validation) != '' ? "'" . $this->db->idate($this->date_validation) . "'" : 'null') . ",";
2499
        $sql .= " paye=" . (isset($this->paye) ? $this->db->escape($this->paye) : 0) . ",";
2500
        $sql .= " close_code=" . (isset($this->close_code) ? "'" . $this->db->escape($this->close_code) . "'" : "null") . ",";
2501
        $sql .= " close_note=" . (isset($this->close_note) ? "'" . $this->db->escape($this->close_note) . "'" : "null") . ",";
2502
        $sql .= " total_tva=" . (isset($this->total_tva) ? (float) $this->total_tva : "null") . ",";
2503
        $sql .= " localtax1=" . (isset($this->total_localtax1) ? (float) $this->total_localtax1 : "null") . ",";
2504
        $sql .= " localtax2=" . (isset($this->total_localtax2) ? (float) $this->total_localtax2 : "null") . ",";
2505
        $sql .= " total_ht=" . (isset($this->total_ht) ? (float) $this->total_ht : "null") . ",";
2506
        $sql .= " total_ttc=" . (isset($this->total_ttc) ? (float) $this->total_ttc : "null") . ",";
2507
        $sql .= " revenuestamp=" . ((isset($this->revenuestamp) && $this->revenuestamp != '') ? (int) $this->revenuestamp : "null") . ",";
2508
        $sql .= " fk_statut=" . (isset($this->status) ? (int) $this->status : "null") . ",";
2509
        $sql .= " fk_user_valid=" . (isset($this->fk_user_valid) ? (int) $this->fk_user_valid : "null") . ",";
2510
        $sql .= " fk_facture_source=" . (isset($this->fk_facture_source) ? (int) $this->fk_facture_source : "null") . ",";
2511
        $sql .= " fk_projet=" . (isset($this->fk_project) ? (int) $this->fk_project : "null") . ",";
2512
        $sql .= " fk_cond_reglement=" . (isset($this->cond_reglement_id) ? (int) $this->cond_reglement_id : "null") . ",";
2513
        $sql .= " fk_mode_reglement=" . (isset($this->mode_reglement_id) ? (int) $this->mode_reglement_id : "null") . ",";
2514
        $sql .= " date_lim_reglement=" . (strval($this->date_lim_reglement) != '' ? "'" . $this->db->idate($this->date_lim_reglement) . "'" : 'null') . ",";
2515
        $sql .= " note_private=" . (isset($this->note_private) ? "'" . $this->db->escape($this->note_private) . "'" : "null") . ",";
2516
        $sql .= " note_public=" . (isset($this->note_public) ? "'" . $this->db->escape($this->note_public) . "'" : "null") . ",";
2517
        $sql .= " model_pdf=" . (isset($this->model_pdf) ? "'" . $this->db->escape($this->model_pdf) . "'" : "null") . ",";
2518
        $sql .= " import_key=" . (isset($this->import_key) ? "'" . $this->db->escape($this->import_key) . "'" : "null") . ",";
2519
        $sql .= " situation_cycle_ref=" . (empty($this->situation_cycle_ref) ? "null" : (int) $this->situation_cycle_ref) . ",";
2520
        $sql .= " situation_counter=" . (empty($this->situation_counter) ? "null" : (int) $this->situation_counter) . ",";
2521
        $sql .= " situation_final=" . (empty($this->situation_final) ? "0" : (int) $this->situation_final) . ",";
2522
        $sql .= " retained_warranty=" . (empty($this->retained_warranty) ? "0" : (float) $this->retained_warranty) . ",";
2523
        $sql .= " retained_warranty_date_limit=" . (strval($this->retained_warranty_date_limit) != '' ? "'" . $this->db->idate($this->retained_warranty_date_limit) . "'" : 'null') . ",";
2524
        $sql .= " retained_warranty_fk_cond_reglement=" . (isset($this->retained_warranty_fk_cond_reglement) ? (int) $this->retained_warranty_fk_cond_reglement : "null");
2525
        $sql .= " WHERE rowid=" . ((int) $this->id);
2526
2527
        $this->db->begin();
2528
2529
        dol_syslog(get_class($this) . "::update", LOG_DEBUG);
2530
        $resql = $this->db->query($sql);
2531
        if (!$resql) {
2532
            $error++;
2533
            $this->errors[] = "Error " . $this->db->lasterror();
2534
        }
2535
2536
        if (!$error) {
2537
            $result = $this->insertExtraFields();
2538
            if ($result < 0) {
2539
                $error++;
2540
            }
2541
        }
2542
2543
        if (!$error && !$notrigger) {
2544
            // Call trigger
2545
            $result = $this->call_trigger('BILL_MODIFY', $user);
2546
            if ($result < 0) {
2547
                $error++;
2548
            }
2549
            // End call triggers
2550
        }
2551
2552
        // Commit or rollback
2553
        if ($error) {
2554
            foreach ($this->errors as $errmsg) {
2555
                dol_syslog(get_class($this) . "::update " . $errmsg, LOG_ERR);
2556
                $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
2557
            }
2558
            $this->db->rollback();
2559
            return -1 * $error;
2560
        } else {
2561
            $this->db->commit();
2562
            return 1;
2563
        }
2564
    }
2565
2566
2567
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2568
    /**
2569
     *    Add a discount line into an invoice (as an invoice line) using an existing absolute discount (Consume the discount)
2570
     *
2571
     *    @param     int    $idremise   Id of absolute discount
2572
     *    @return    int                >0 if OK, <0 if KO
2573
     */
2574
    public function insert_discount($idremise)
2575
    {
2576
        // phpcs:enable
2577
        global $conf, $langs;
2578
2579
        include_once DOL_DOCUMENT_ROOT . '/core/lib/price.lib.php';
2580
        include_once DOL_DOCUMENT_ROOT . '/core/class/discount.class.php';
2581
2582
        $this->db->begin();
2583
2584
        $remise = new DiscountAbsolute($this->db);
2585
        $result = $remise->fetch($idremise);
2586
2587
        if ($result > 0) {
2588
            if ($remise->fk_facture) {  // Protection against multiple submission
2589
                $this->error = $langs->trans("ErrorDiscountAlreadyUsed");
2590
                $this->db->rollback();
2591
                return -5;
2592
            }
2593
2594
            $facligne = new FactureLigne($this->db);
0 ignored issues
show
Deprecated Code introduced by
The class DoliModules\Billing\Model\FactureLigne has been deprecated: Use Invoice instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2594
            $facligne = /** @scrutinizer ignore-deprecated */ new FactureLigne($this->db);
Loading history...
2595
            $facligne->fk_facture = $this->id;
2596
            $facligne->fk_remise_except = $remise->id;
2597
            $facligne->desc = $remise->description; // Description ligne
2598
            $facligne->vat_src_code = $remise->vat_src_code;
2599
            $facligne->tva_tx = $remise->tva_tx;
2600
            $facligne->subprice = -$remise->amount_ht;
2601
            $facligne->fk_product = 0; // Id produit predefini
2602
            $facligne->qty = 1;
2603
            $facligne->remise_percent = 0;
2604
            $facligne->rang = -1;
2605
            $facligne->info_bits = 2;
2606
2607
            if (getDolGlobalString('MAIN_ADD_LINE_AT_POSITION')) {
2608
                $facligne->rang = 1;
2609
                $linecount = count($this->lines);
2610
                for ($ii = 1; $ii <= $linecount; $ii++) {
2611
                    $this->updateRangOfLine($this->lines[$ii - 1]->id, $ii + 1);
2612
                }
2613
            }
2614
2615
            // Get buy/cost price of invoice that is source of discount
2616
            if ($remise->fk_facture_source > 0) {
2617
                $srcinvoice = new Facture($this->db);
2618
                $srcinvoice->fetch($remise->fk_facture_source);
2619
                include_once DOL_DOCUMENT_ROOT . '/core/class/html.formmargin.class.php'; // TODO Move this into commonobject
2620
                $formmargin = new FormMargin($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\FormMargin was not found. Did you mean FormMargin? If so, make sure to prefix the type with \.
Loading history...
2621
                $arraytmp = $formmargin->getMarginInfosArray($srcinvoice, false);
2622
                $facligne->pa_ht = $arraytmp['pa_total'];
2623
            }
2624
2625
            $facligne->total_ht  = -$remise->amount_ht;
2626
            $facligne->total_tva = -$remise->amount_tva;
2627
            $facligne->total_ttc = -$remise->amount_ttc;
2628
2629
            $facligne->multicurrency_subprice = -$remise->multicurrency_subprice;
2630
            $facligne->multicurrency_total_ht = -$remise->multicurrency_amount_ht;
2631
            $facligne->multicurrency_total_tva = -$remise->multicurrency_amount_tva;
2632
            $facligne->multicurrency_total_ttc = -$remise->multicurrency_amount_ttc;
2633
2634
            $lineid = $facligne->insert();
2635
            if ($lineid > 0) {
2636
                $result = $this->update_price(1);
2637
                if ($result > 0) {
2638
                    // Create link between discount and invoice line
2639
                    $result = $remise->link_to_invoice($lineid, 0);
2640
                    if ($result < 0) {
2641
                        $this->error = $remise->error;
2642
                        $this->db->rollback();
2643
                        return -4;
2644
                    }
2645
2646
                    $this->db->commit();
2647
                    return 1;
2648
                } else {
2649
                    $this->error = $facligne->error;
2650
                    $this->db->rollback();
2651
                    return -1;
2652
                }
2653
            } else {
2654
                $this->error = $facligne->error;
2655
                $this->db->rollback();
2656
                return -2;
2657
            }
2658
        } else {
2659
            $this->db->rollback();
2660
            return -3;
2661
        }
2662
    }
2663
2664
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2665
    /**
2666
     *  Set customer ref
2667
     *
2668
     *  @param      string  $ref_client     Customer ref
2669
     *  @param      int     $notrigger      1=Does not execute triggers, 0= execute triggers
2670
     *  @return     int                     Return integer <0 if KO, >0 if OK
2671
     */
2672
    public function set_ref_client($ref_client, $notrigger = 0)
2673
    {
2674
        // phpcs:enable
2675
        global $user;
2676
2677
        $error = 0;
2678
2679
        $this->db->begin();
2680
2681
        $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'facture';
2682
        if (empty($ref_client)) {
2683
            $sql .= ' SET ref_client = NULL';
2684
        } else {
2685
            $sql .= ' SET ref_client = \'' . $this->db->escape($ref_client) . '\'';
2686
        }
2687
        $sql .= " WHERE rowid = " . ((int) $this->id);
2688
2689
        dol_syslog(__METHOD__ . ' this->id=' . $this->id . ', ref_client=' . $ref_client, LOG_DEBUG);
2690
        $resql = $this->db->query($sql);
2691
        if (!$resql) {
2692
            $this->errors[] = $this->db->error();
2693
            $error++;
2694
        }
2695
2696
        if (!$error) {
2697
            $this->ref_client = $ref_client;
2698
        }
2699
2700
        if (!$notrigger && empty($error)) {
2701
            // Call trigger
2702
            $result = $this->call_trigger('BILL_MODIFY', $user);
2703
            if ($result < 0) {
2704
                $error++;
2705
            }
2706
            // End call triggers
2707
        }
2708
2709
        if (!$error) {
2710
            $this->ref_client = $ref_client;
2711
2712
            $this->db->commit();
2713
            return 1;
2714
        } else {
2715
            foreach ($this->errors as $errmsg) {
2716
                dol_syslog(__METHOD__ . ' Error: ' . $errmsg, LOG_ERR);
2717
                $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
2718
            }
2719
            $this->db->rollback();
2720
            return -1 * $error;
2721
        }
2722
    }
2723
2724
    /**
2725
     *  Delete invoice
2726
     *
2727
     *  @param      User    $user           User making the deletion.
2728
     *  @param      int     $notrigger      1=Does not execute triggers, 0= execute triggers
2729
     *  @param      int     $idwarehouse    Id warehouse to use for stock change.
2730
     *  @return     int                     Return integer <0 if KO, 0=Refused, >0 if OK
2731
     */
2732
    public function delete($user, $notrigger = 0, $idwarehouse = -1)
2733
    {
2734
        global $langs, $conf;
2735
        require_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
2736
2737
        $rowid = $this->id;
2738
2739
        dol_syslog(get_class($this) . "::delete rowid=" . $rowid . ", ref=" . $this->ref . ", thirdparty=" . (empty($this->thirdparty) ? '' : $this->thirdparty->name), LOG_DEBUG);
2740
2741
        // Test to avoid invoice deletion (allowed if draft)
2742
        $result = $this->is_erasable();
2743
2744
        if ($result <= 0) {
2745
            return 0;
2746
        }
2747
2748
        $error = 0;
2749
2750
        $this->db->begin();
2751
2752
        if (!$error && !$notrigger) {
2753
            // Call trigger
2754
            $result = $this->call_trigger('BILL_DELETE', $user);
2755
            if ($result < 0) {
2756
                $error++;
2757
            }
2758
            // End call triggers
2759
        }
2760
2761
        // Removed extrafields
2762
        if (!$error) {
2763
            $result = $this->deleteExtraFields();
2764
            if ($result < 0) {
2765
                $error++;
2766
                dol_syslog(get_class($this) . "::delete error deleteExtraFields " . $this->error, LOG_ERR);
2767
            }
2768
        }
2769
2770
        if (!$error) {
2771
            // Delete linked object
2772
            $res = $this->deleteObjectLinked();
2773
            if ($res < 0) {
2774
                $error++;
2775
            }
2776
        }
2777
2778
        if (!$error) {
2779
            // If invoice was converted into a discount not yet consumed, we remove discount
2780
            $sql = 'DELETE FROM ' . MAIN_DB_PREFIX . 'societe_remise_except';
2781
            $sql .= ' WHERE fk_facture_source = ' . ((int) $rowid);
2782
            $sql .= ' AND fk_facture_line IS NULL';
2783
            $resql = $this->db->query($sql);
2784
2785
            // If invoice has consumed discounts
2786
            $this->fetch_lines();
2787
            $list_rowid_det = array();
2788
            foreach ($this->lines as $key => $invoiceline) {
2789
                $list_rowid_det[] = $invoiceline->id;
2790
            }
2791
2792
            // Consumed discounts are freed
2793
            if (count($list_rowid_det)) {
2794
                $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'societe_remise_except';
2795
                $sql .= ' SET fk_facture = NULL, fk_facture_line = NULL';
2796
                $sql .= ' WHERE fk_facture_line IN (' . $this->db->sanitize(implode(',', $list_rowid_det)) . ')';
2797
2798
                if (!$this->db->query($sql)) {
2799
                    $this->error = $this->db->error() . " sql=" . $sql;
2800
                    $this->errors[] = $this->error;
2801
                    $this->db->rollback();
2802
                    return -5;
2803
                }
2804
            }
2805
2806
            // Remove other links to the deleted invoice
2807
2808
            $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'eventorganization_conferenceorboothattendee';
2809
            $sql .= ' SET fk_invoice = NULL';
2810
            $sql .= ' WHERE fk_invoice = ' . ((int) $rowid);
2811
2812
            if (!$this->db->query($sql)) {
2813
                $this->error = $this->db->error() . " sql=" . $sql;
2814
                $this->errors[] = $this->error;
2815
                $this->db->rollback();
2816
                return -5;
2817
            }
2818
2819
            $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'element_time';
2820
            $sql .= ' SET invoice_id = NULL, invoice_line_id = NULL';
2821
            $sql .= ' WHERE invoice_id = ' . ((int) $rowid);
2822
2823
            if (!$this->db->query($sql)) {
2824
                $this->error = $this->db->error() . " sql=" . $sql;
2825
                $this->errors[] = $this->error;
2826
                $this->db->rollback();
2827
                return -5;
2828
            }
2829
2830
            // If we decrease stock on invoice validation, we increase back if a warehouse id was provided
2831
            if ($this->type != self::TYPE_DEPOSIT && $result >= 0 && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $idwarehouse != -1) {
2832
                require_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
2833
                $langs->load("agenda");
2834
2835
                $num = count($this->lines);
2836
                for ($i = 0; $i < $num; $i++) {
2837
                    if ($this->lines[$i]->fk_product > 0) {
2838
                        $mouvP = new MouvementStock($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\MouvementStock was not found. Did you mean MouvementStock? If so, make sure to prefix the type with \.
Loading history...
2839
                        $mouvP->origin = &$this;
2840
                        $mouvP->setOrigin($this->element, $this->id);
2841
                        // We decrease stock for product
2842
                        if ($this->type == self::TYPE_CREDIT_NOTE) {
2843
                            $result = $mouvP->livraison($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, $this->lines[$i]->subprice, $langs->trans("InvoiceDeleteDolibarr", $this->ref));
2844
                        } else {
2845
                            $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
2846
                        }
2847
                    }
2848
                }
2849
            }
2850
2851
            // Invoice line extrafileds
2852
            $main = MAIN_DB_PREFIX . 'facturedet';
2853
            $ef = $main . "_extrafields";
2854
            $sqlef = "DELETE FROM " . $ef . " WHERE fk_object IN (SELECT rowid FROM " . $main . " WHERE fk_facture = " . ((int) $rowid) . ")";
2855
            // Delete invoice line
2856
            $sql = 'DELETE FROM ' . MAIN_DB_PREFIX . 'facturedet WHERE fk_facture = ' . ((int) $rowid);
2857
2858
            if ($this->db->query($sqlef) && $this->db->query($sql) && $this->delete_linked_contact()) {
2859
                $sql = 'DELETE FROM ' . MAIN_DB_PREFIX . 'facture WHERE rowid = ' . ((int) $rowid);
2860
2861
                $resql = $this->db->query($sql);
2862
                if ($resql) {
2863
                    // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive
2864
                    $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
2865
                    $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
2866
2867
                    // On efface le repertoire de pdf provisoire
2868
                    $ref = dol_sanitizeFileName($this->ref);
2869
                    if ($conf->facture->dir_output && !empty($this->ref)) {
2870
                        $dir = $conf->facture->dir_output . "/" . $ref;
2871
                        $file = $conf->facture->dir_output . "/" . $ref . "/" . $ref . ".pdf";
2872
                        if (file_exists($file)) {   // We must delete all files before deleting directory
2873
                            $ret = dol_delete_preview($this);
2874
2875
                            if (!dol_delete_file($file, 0, 0, 0, $this)) { // For triggers
2876
                                $langs->load("errors");
2877
                                $this->error = $langs->trans("ErrorFailToDeleteFile", $file);
2878
                                $this->errors[] = $this->error;
2879
                                $this->db->rollback();
2880
                                return 0;
2881
                            }
2882
                        }
2883
                        if (file_exists($dir)) {
2884
                            if (!dol_delete_dir_recursive($dir)) { // For remove dir and meta
2885
                                $langs->load("errors");
2886
                                $this->error = $langs->trans("ErrorFailToDeleteDir", $dir);
2887
                                $this->errors[] = $this->error;
2888
                                $this->db->rollback();
2889
                                return 0;
2890
                            }
2891
                        }
2892
                    }
2893
2894
                    $this->db->commit();
2895
                    return 1;
2896
                } else {
2897
                    $this->error = $this->db->lasterror() . " sql=" . $sql;
2898
                    $this->errors[] = $this->error;
2899
                    $this->db->rollback();
2900
                    return -6;
2901
                }
2902
            } else {
2903
                $this->error = $this->db->lasterror() . " sql=" . $sql;
2904
                $this->errors[] = $this->error;
2905
                $this->db->rollback();
2906
                return -4;
2907
            }
2908
        } else {
2909
            $this->db->rollback();
2910
            return -2;
2911
        }
2912
    }
2913
2914
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2915
    /**
2916
     *  Tag the invoice as paid completely (if close_code is filled) => this->fk_statut=2, this->paye=1
2917
     *  or partially (if close_code filled) + appel trigger BILL_PAYED => this->fk_statut=2, this->paye stay 0
2918
     *
2919
     *  @deprecated
2920
     *  @see setPaid()
2921
     *  @param  User    $user       Object user that modify
2922
     *  @param  string  $close_code Code set when forcing to set the invoice as fully paid while in practice it is incomplete (because of a discount (fr:escompte) for instance)
2923
     *  @param  string  $close_note Comment set when forcing to set the invoice as fully paid while in practice it is incomplete (because of a discount (fr:escompte) for instance)
2924
     *  @return int                 Return integer <0 if KO, >0 if OK
2925
     */
2926
    public function set_paid($user, $close_code = '', $close_note = '')
2927
    {
2928
        // phpcs:enable
2929
        dol_syslog(get_class($this) . "::set_paid is deprecated, use setPaid instead", LOG_NOTICE);
2930
        return $this->setPaid($user, $close_code, $close_note);
2931
    }
2932
2933
    /**
2934
     *  Tag the invoice as :
2935
     *  - paid completely (if close_code is not filled) => this->fk_statut=2, this->paye=1
2936
     *  - or partially (if close_code filled) + appel trigger BILL_PAYED => this->fk_statut=2, this->paye stay 0
2937
     *
2938
     *  @param  User    $user       Object user that modify
2939
     *  @param  string  $close_code Code set when forcing to set the invoice as fully paid while in practice it is incomplete (because of a discount (fr:escompte) for instance)
2940
     *  @param  string  $close_note Comment set when forcing to set the invoice as fully paid while in practice it is incomplete (because of a discount (fr:escompte) for instance)
2941
     *  @return int                 Return integer <0 if KO, >0 if OK
2942
     */
2943
    public function setPaid($user, $close_code = '', $close_note = '')
2944
    {
2945
        $error = 0;
2946
2947
        if ($this->paye != 1) {
2948
            $this->db->begin();
2949
2950
            $now = dol_now();
2951
2952
            dol_syslog(get_class($this) . "::setPaid rowid=" . ((int) $this->id), LOG_DEBUG);
2953
2954
            $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'facture SET';
2955
            $sql .= ' fk_statut=' . self::STATUS_CLOSED;
2956
            if (!$close_code) {
2957
                $sql .= ', paye=1';
2958
            }
2959
            if ($close_code) {
2960
                $sql .= ", close_code='" . $this->db->escape($close_code) . "'";
2961
            }
2962
            if ($close_note) {
2963
                $sql .= ", close_note='" . $this->db->escape($close_note) . "'";
2964
            }
2965
            $sql .= ', fk_user_closing = ' . ((int) $user->id);
2966
            $sql .= ", date_closing = '" . $this->db->idate($now) . "'";
2967
            $sql .= " WHERE rowid = " . ((int) $this->id);
2968
2969
            $resql = $this->db->query($sql);
2970
            if ($resql) {
2971
                // Call trigger
2972
                $result = $this->call_trigger('BILL_PAYED', $user);
2973
                if ($result < 0) {
2974
                    $error++;
2975
                }
2976
                // End call triggers
2977
            } else {
2978
                $error++;
2979
                $this->error = $this->db->lasterror();
2980
            }
2981
2982
            if (!$error) {
2983
                $this->db->commit();
2984
                return 1;
2985
            } else {
2986
                $this->db->rollback();
2987
                return -1;
2988
            }
2989
        } else {
2990
            return 0;
2991
        }
2992
    }
2993
2994
2995
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2996
    /**
2997
     *  Tags the invoice as incompletely paid and call the trigger BILL_UNPAYED
2998
     *  This method is used when a direct debit (fr:prelevement) is refused
2999
     *  or when a canceled invoice is reopened.
3000
     *
3001
     *  @deprecated
3002
     *  @see setUnpaid()
3003
     *  @param  User    $user       Object user that change status
3004
     *  @return int                 Return integer <0 if KO, >0 if OK
3005
     */
3006
    public function set_unpaid($user)
3007
    {
3008
        // phpcs:enable
3009
        dol_syslog(get_class($this) . "::set_unpaid is deprecated, use setUnpaid instead", LOG_NOTICE);
3010
        return $this->setUnpaid($user);
3011
    }
3012
3013
    /**
3014
     *  Tag the invoice as incompletely paid and call the trigger BILL_UNPAYED
3015
     *  This method is used when a direct debit (fr:prelevement) is refused
3016
     *  or when a canceled invoice is reopened.
3017
     *
3018
     *  @param  User    $user       Object user that change status
3019
     *  @return int                 Return integer <0 if KO, >0 if OK
3020
     */
3021
    public function setUnpaid($user)
3022
    {
3023
        $error = 0;
3024
3025
        $this->db->begin();
3026
3027
        $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'facture';
3028
        $sql .= ' SET paye=0, fk_statut=' . self::STATUS_VALIDATED . ', close_code=null, close_note=null,';
3029
        $sql .= ' date_closing=null,';
3030
        $sql .= ' fk_user_closing=null';
3031
        $sql .= " WHERE rowid = " . ((int) $this->id);
3032
3033
        dol_syslog(get_class($this) . "::setUnpaid", LOG_DEBUG);
3034
        $resql = $this->db->query($sql);
3035
        if ($resql) {
3036
            // Call trigger
3037
            $result = $this->call_trigger('BILL_UNPAYED', $user);
3038
            if ($result < 0) {
3039
                $error++;
3040
            }
3041
            // End call triggers
3042
        } else {
3043
            $error++;
3044
            $this->error = $this->db->error();
3045
            dol_print_error($this->db);
3046
        }
3047
3048
        if (!$error) {
3049
            $this->db->commit();
3050
            return 1;
3051
        } else {
3052
            $this->db->rollback();
3053
            return -1;
3054
        }
3055
    }
3056
3057
3058
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3059
    /**
3060
     *  Tag invoice as canceled, with no payment on it (example for replacement invoice or payment never received) + call trigger BILL_CANCEL
3061
     *  Warning, if option to decrease stock on invoice was set, this function does not change stock (it might be a cancel because
3062
     *  of no payment even if merchandises were sent).
3063
     *
3064
     *  @deprecated
3065
     *  @see setCanceled()
3066
     *  @param  User    $user           Object user making change
3067
     *  @param  string  $close_code     Code of closing invoice (CLOSECODE_REPLACED, CLOSECODE_...)
3068
     *  @param  string  $close_note     Comment
3069
     *  @return int                     Return integer <0 if KO, >0 if OK
3070
     */
3071
    public function set_canceled($user, $close_code = '', $close_note = '')
3072
    {
3073
        // phpcs:enable
3074
        dol_syslog(get_class($this) . "::set_canceled is deprecated, use setCanceled instead", LOG_NOTICE);
3075
        return $this->setCanceled($user, $close_code, $close_note);
3076
    }
3077
3078
    /**
3079
     *  Tag invoice as canceled, with no payment on it (example for replacement invoice or payment never received) + call trigger BILL_CANCEL
3080
     *  Warning, if option to decrease stock on invoice was set, this function does not change stock (it might be a cancel because
3081
     *  of no payment even if merchandises were sent).
3082
     *
3083
     *  @param  User    $user           Object user making change
3084
     *  @param  string  $close_code     Code of closing invoice (CLOSECODE_REPLACED, CLOSECODE_...)
3085
     *  @param  string  $close_note     Comment
3086
     *  @return int                     Return integer <0 if KO, >0 if OK
3087
     */
3088
    public function setCanceled($user, $close_code = '', $close_note = '')
3089
    {
3090
        dol_syslog(get_class($this) . "::setCanceled rowid=" . ((int) $this->id), LOG_DEBUG);
3091
3092
        $this->db->begin();
3093
        $now = dol_now();
3094
3095
        $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'facture SET';
3096
        $sql .= ' fk_statut=' . self::STATUS_ABANDONED;
3097
        if ($close_code) {
3098
            $sql .= ", close_code='" . $this->db->escape($close_code) . "'";
3099
        }
3100
        if ($close_note) {
3101
            $sql .= ", close_note='" . $this->db->escape($close_note) . "'";
3102
        }
3103
        $sql .= ', fk_user_closing = ' . ((int) $user->id);
3104
        $sql .= ", date_closing = '" . $this->db->idate($now) . "'";
3105
        $sql .= " WHERE rowid = " . ((int) $this->id);
3106
3107
        $resql = $this->db->query($sql);
3108
        if ($resql) {
3109
            // Bound discounts are deducted from the invoice
3110
            // as they have not been used since the invoice is abandoned.
3111
            $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'societe_remise_except';
3112
            $sql .= ' SET fk_facture = NULL';
3113
            $sql .= ' WHERE fk_facture = ' . ((int) $this->id);
3114
3115
            $resql = $this->db->query($sql);
3116
            if ($resql) {
3117
                // Call trigger
3118
                $result = $this->call_trigger('BILL_CANCEL', $user);
3119
                if ($result < 0) {
3120
                    $this->db->rollback();
3121
                    return -1;
3122
                }
3123
                // End call triggers
3124
3125
                $this->db->commit();
3126
                return 1;
3127
            } else {
3128
                $this->error = $this->db->error() . " sql=" . $sql;
3129
                $this->db->rollback();
3130
                return -1;
3131
            }
3132
        } else {
3133
            $this->error = $this->db->error() . " sql=" . $sql;
3134
            $this->db->rollback();
3135
            return -2;
3136
        }
3137
    }
3138
3139
    /**
3140
     * Tag invoice as validated + call trigger BILL_VALIDATE
3141
     * Object must have lines loaded with fetch_lines
3142
     *
3143
     * @param   User    $user           Object user that validate
3144
     * @param   string  $force_number   Reference to force on invoice
3145
     * @param   int     $idwarehouse    Id of warehouse to use for stock decrease if option to decreasenon stock is on (0=no decrease)
3146
     * @param   int     $notrigger      1=Does not execute triggers, 0= execute triggers
3147
     * @param   int     $batch_rule     0=do not decrement batch, else batch rule to use, 1=take in batches ordered by sellby and eatby dates
3148
     * @return  int                     Return integer <0 if KO, 0=Nothing done because invoice is not a draft, >0 if OK
3149
     */
3150
    public function validate($user, $force_number = '', $idwarehouse = 0, $notrigger = 0, $batch_rule = 0)
3151
    {
3152
        global $conf, $langs, $mysoc;
3153
        require_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
3154
3155
        $productStatic = null;
3156
        $warehouseStatic = null;
3157
        if ($batch_rule > 0) {
3158
            require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
3159
            require_once DOL_DOCUMENT_ROOT . '/product/class/productbatch.class.php';
3160
            require_once DOL_DOCUMENT_ROOT . '/product/stock/class/entrepot.class.php';
3161
            $productStatic = new Product($this->db);
3162
            $warehouseStatic = new Entrepot($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\Entrepot was not found. Did you mean Entrepot? If so, make sure to prefix the type with \.
Loading history...
3163
            $productbatch = new Productbatch($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\Productbatch was not found. Did you mean Productbatch? If so, make sure to prefix the type with \.
Loading history...
3164
        }
3165
3166
        $now = dol_now();
3167
3168
        $error = 0;
3169
        dol_syslog(get_class($this) . '::validate user=' . $user->id . ', force_number=' . $force_number . ', idwarehouse=' . $idwarehouse);
3170
3171
        // Force to have object complete for checks
3172
        $this->fetch_thirdparty();
3173
        $this->fetch_lines();
3174
3175
        // Check parameters
3176
        if ($this->status != self::STATUS_DRAFT) {
3177
            dol_syslog(get_class($this) . "::validate Current status is not draft. operation canceled.", LOG_WARNING);
3178
            return 0;
3179
        }
3180
        if (count($this->lines) <= 0) {
3181
            $langs->load("errors");
3182
            $this->error = $langs->trans("ErrorObjectMustHaveLinesToBeValidated", $this->ref);
3183
            return -1;
3184
        }
3185
        if (
3186
            (!getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && !$user->hasRight('facture', 'creer'))
3187
            || (getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && !$user->hasRight('facture', 'invoice_advance', 'validate'))
3188
        ) {
3189
            $this->error = 'Permission denied';
3190
            dol_syslog(get_class($this) . "::validate " . $this->error . ' MAIN_USE_ADVANCED_PERMS=' . getDolGlobalString('MAIN_USE_ADVANCED_PERMS'), LOG_ERR);
3191
            return -1;
3192
        }
3193
        if (
3194
            (preg_match('/^[\(]?PROV/i', $this->ref) || empty($this->ref)) &&   // empty should not happened, but when it occurs, the test save life
3195
            getDolGlobalString('FAC_FORCE_DATE_VALIDATION')                     // If option enabled, we force invoice date
3196
        ) {
3197
            $this->date = dol_now();
3198
            $this->date_lim_reglement = $this->calculate_date_lim_reglement();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->calculate_date_lim_reglement() can also be of type string. However, the property $date_lim_reglement is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
3199
        }
3200
        if (getDolGlobalString('INVOICE_CHECK_POSTERIOR_DATE')) {
3201
            $last_of_type = $this->willBeLastOfSameType(true);
3202
            if (!$last_of_type[0]) {
3203
                $this->error = $langs->transnoentities("ErrorInvoiceIsNotLastOfSameType", $this->ref, dol_print_date($this->date, 'day'), dol_print_date($last_of_type[1], 'day'));
3204
                return -1;
3205
            }
3206
        }
3207
3208
        // Check for mandatory fields in thirdparty (defined into setup)
3209
        if (!empty($this->thirdparty) && is_object($this->thirdparty)) {
3210
            $array_to_check = array('IDPROF1', 'IDPROF2', 'IDPROF3', 'IDPROF4', 'IDPROF5', 'IDPROF6', 'EMAIL', 'ACCOUNTANCY_CODE_CUSTOMER');
3211
            foreach ($array_to_check as $key) {
3212
                $keymin = strtolower($key);
3213
                if (!property_exists($this->thirdparty, $keymin)) {
3214
                    continue;
3215
                }
3216
                $vallabel = $this->thirdparty->$keymin;
3217
3218
                $i = (int) preg_replace('/[^0-9]/', '', $key);
3219
                if ($i > 0) {
3220
                    if ($this->thirdparty->isACompany()) {
3221
                        // Check for mandatory prof id (but only if country is other than ours)
3222
                        if ($mysoc->country_id > 0 && $this->thirdparty->country_id == $mysoc->country_id) {
3223
                            $idprof_mandatory = 'SOCIETE_' . $key . '_INVOICE_MANDATORY';
3224
                            if (!$vallabel && !empty($conf->global->$idprof_mandatory)) {
3225
                                $langs->load("errors");
3226
                                $this->error = $langs->trans('ErrorProdIdIsMandatory', $langs->transcountry('ProfId' . $i, $this->thirdparty->country_code)) . ' (' . $langs->trans("ForbiddenBySetupRules") . ') [' . $langs->trans('Company') . ' : ' . $this->thirdparty->name . ']';
3227
                                dol_syslog(__METHOD__ . ' ' . $this->error, LOG_ERR);
3228
                                return -1;
3229
                            }
3230
                        }
3231
                    }
3232
                } else {
3233
                    if ($key == 'EMAIL') {
3234
                        // Check for mandatory
3235
                        if (getDolGlobalString('SOCIETE_EMAIL_INVOICE_MANDATORY') && !isValidEmail($this->thirdparty->email)) {
3236
                            $langs->load("errors");
3237
                            $this->error = $langs->trans("ErrorBadEMail", $this->thirdparty->email) . ' (' . $langs->trans("ForbiddenBySetupRules") . ') [' . $langs->trans('Company') . ' : ' . $this->thirdparty->name . ']';
3238
                            dol_syslog(__METHOD__ . ' ' . $this->error, LOG_ERR);
3239
                            return -1;
3240
                        }
3241
                    }
3242
                    if ($key == 'ACCOUNTANCY_CODE_CUSTOMER') {
3243
                        // Check for mandatory
3244
                        if (getDolGlobalString('SOCIETE_ACCOUNTANCY_CODE_CUSTOMER_INVOICE_MANDATORY') && empty($this->thirdparty->code_compta)) {
0 ignored issues
show
Deprecated Code introduced by
The property Societe::$code_compta has been deprecated: Use $code_compta_client ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

3244
                        if (getDolGlobalString('SOCIETE_ACCOUNTANCY_CODE_CUSTOMER_INVOICE_MANDATORY') && empty(/** @scrutinizer ignore-deprecated */ $this->thirdparty->code_compta)) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
3245
                            $langs->load("errors");
3246
                            $this->error = $langs->trans("ErrorAccountancyCodeCustomerIsMandatory", $this->thirdparty->name) . ' (' . $langs->trans("ForbiddenBySetupRules") . ')';
3247
                            dol_syslog(__METHOD__ . ' ' . $this->error, LOG_ERR);
3248
                            return -1;
3249
                        }
3250
                    }
3251
                }
3252
            }
3253
        }
3254
3255
        // Check for mandatory fields in $this
3256
        $array_to_check = array('REF_CLIENT' => 'RefCustomer');
3257
        foreach ($array_to_check as $key => $val) {
3258
            $keymin = strtolower($key);
3259
            $vallabel = $this->$keymin;
3260
3261
            // Check for mandatory
3262
            $keymandatory = 'INVOICE_' . $key . '_MANDATORY_FOR_VALIDATION';
3263
            if (!$vallabel && getDolGlobalString($keymandatory)) {
3264
                $langs->load("errors");
3265
                $error++;
3266
                setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv($val)), null, 'errors');
3267
            }
3268
        }
3269
3270
        $this->db->begin();
3271
3272
        // Check parameters
3273
        if ($this->type == self::TYPE_REPLACEMENT) {        // if this is a replacement invoice
3274
            // Check that source invoice is known
3275
            if ($this->fk_facture_source <= 0) {
3276
                $this->error = $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("InvoiceReplacement"));
3277
                $this->db->rollback();
3278
                return -10;
3279
            }
3280
3281
            // Load source invoice that has been replaced
3282
            $facreplaced = new Facture($this->db);
3283
            $result = $facreplaced->fetch($this->fk_facture_source);
3284
            if ($result <= 0) {
3285
                $this->error = $langs->trans("ErrorBadInvoice");
3286
                $this->db->rollback();
3287
                return -11;
3288
            }
3289
3290
            // Check that source invoice not already replaced by another one.
3291
            $idreplacement = $facreplaced->getIdReplacingInvoice('validated');
3292
            if ($idreplacement && $idreplacement != $this->id) {
3293
                $facreplacement = new Facture($this->db);
3294
                $facreplacement->fetch($idreplacement);
3295
                $this->error = $langs->trans("ErrorInvoiceAlreadyReplaced", $facreplaced->ref, $facreplacement->ref);
3296
                $this->db->rollback();
3297
                return -12;
3298
            }
3299
3300
            $result = $facreplaced->setCanceled($user, self::CLOSECODE_REPLACED, '');
3301
            if ($result < 0) {
3302
                $this->error = $facreplaced->error;
3303
                $this->db->rollback();
3304
                return -13;
3305
            }
3306
        }
3307
3308
        // Define new ref
3309
        if ($force_number) {
3310
            $num = $force_number;
3311
        } elseif (preg_match('/^[\(]?PROV/i', $this->ref) || empty($this->ref)) { // empty should not happened, but when it occurs, the test save life
3312
            if (getDolGlobalString('FAC_FORCE_DATE_VALIDATION')) {  // If option enabled, we force invoice date
3313
                $this->date = dol_now();
3314
                $this->date_lim_reglement = $this->calculate_date_lim_reglement();
3315
            }
3316
            $num = $this->getNextNumRef($this->thirdparty);
3317
        } else {
3318
            $num = $this->ref;
3319
        }
3320
3321
        $this->newref = dol_sanitizeFileName($num);
3322
3323
        if ($num) {
3324
            $this->update_price(1);
3325
3326
            // Validate
3327
            $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'facture';
3328
            $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) . "'";
3329
            if (getDolGlobalString('FAC_FORCE_DATE_VALIDATION')) {  // If option enabled, we force invoice date
3330
                $sql .= ", datef='" . $this->db->idate($this->date) . "'";
3331
                $sql .= ", date_lim_reglement='" . $this->db->idate($this->date_lim_reglement) . "'";
3332
            }
3333
            $sql .= " WHERE rowid = " . ((int) $this->id);
3334
3335
            dol_syslog(get_class($this) . "::validate", LOG_DEBUG);
3336
            $resql = $this->db->query($sql);
3337
            if (!$resql) {
3338
                dol_print_error($this->db);
3339
                $error++;
3340
            }
3341
3342
            // We check if the invoice was provisional
3343
            if (!$error && (preg_match('/^[\(]?PROV/i', $this->ref))) {
3344
                // La verif qu'une remise n'est pas utilisee 2 fois est faite au moment de l'insertion de ligne
3345
            }
3346
3347
            if (!$error) {
3348
                // Define third party as a customer
3349
                $result = $this->thirdparty->setAsCustomer();
3350
3351
                // If active we decrement the main product and its components at invoice validation
3352
                if ($this->type != self::TYPE_DEPOSIT && $result >= 0 && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $idwarehouse > 0) {
3353
                    require_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
3354
                    $langs->load("agenda");
3355
3356
                    // Loop on each line
3357
                    $cpt = count($this->lines);
3358
                    for ($i = 0; $i < $cpt; $i++) {
3359
                        if ($this->lines[$i]->fk_product > 0) {
3360
                            $mouvP = new MouvementStock($this->db);
3361
                            $mouvP->origin = &$this;
3362
                            $mouvP->setOrigin($this->element, $this->id);
3363
                            // We decrease stock for product
3364
                            if ($this->type == self::TYPE_CREDIT_NOTE) {
3365
                                $result = $mouvP->reception($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, 0, $langs->trans("InvoiceValidatedInDolibarr", $num));
3366
                                if ($result < 0) {
3367
                                    $error++;
3368
                                    $this->error = $mouvP->error;
3369
                                }
3370
                            } else {
3371
                                $is_batch_line = false;
3372
                                if ($batch_rule > 0) {
3373
                                    $productStatic->fetch($this->lines[$i]->fk_product);
3374
                                    if ($productStatic->hasbatch()) {
3375
                                        $is_batch_line = true;
3376
                                        $product_qty_remain = $this->lines[$i]->qty;
3377
3378
                                        $sortfield = null;
3379
                                        $sortorder = null;
3380
                                        // find all batch order by sellby (DLC) and eatby dates (DLUO) first
3381
                                        if ($batch_rule == Productbatch::BATCH_RULE_SELLBY_EATBY_DATES_FIRST) {
3382
                                            $sortfield = 'pl.sellby,pl.eatby,pb.qty,pl.rowid';
3383
                                            $sortorder = 'ASC,ASC,ASC,ASC';
3384
                                        }
3385
3386
                                        $resBatchList = $productbatch->findAllForProduct($productStatic->id, $idwarehouse, (getDolGlobalInt('STOCK_ALLOW_NEGATIVE_TRANSFER') ? null : 0), $sortfield, $sortorder);
3387
                                        if (!is_array($resBatchList)) {
3388
                                            $error++;
3389
                                            $this->error = $this->db->lasterror();
3390
                                        }
3391
3392
                                        if (!$error) {
3393
                                            $batchList = $resBatchList;
3394
                                            if (empty($batchList)) {
3395
                                                $error++;
3396
                                                $langs->load('errors');
3397
                                                $warehouseStatic->fetch($idwarehouse);
3398
                                                $this->error = $langs->trans('ErrorBatchNoFoundForProductInWarehouse', $productStatic->label, $warehouseStatic->ref);
3399
                                                dol_syslog(__METHOD__ . ' Error: ' . $langs->transnoentitiesnoconv('ErrorBatchNoFoundForProductInWarehouse', $productStatic->label, $warehouseStatic->ref), LOG_ERR);
3400
                                            }
3401
3402
                                            foreach ($batchList as $batch) {
3403
                                                if ($batch->qty <= 0) {
3404
                                                    continue; // try to decrement only batches have positive quantity first
3405
                                                }
3406
3407
                                                // enough quantity in this batch
3408
                                                if ($batch->qty >= $product_qty_remain) {
3409
                                                    $product_batch_qty = $product_qty_remain;
3410
                                                } else {
3411
                                                    // not enough (take all in batch)
3412
                                                    $product_batch_qty = $batch->qty;
3413
                                                }
3414
                                                $result = $mouvP->livraison($user, $productStatic->id, $idwarehouse, $product_batch_qty, $this->lines[$i]->subprice, $langs->trans('InvoiceValidatedInDolibarr', $num), '', '', '', $batch->batch);
3415
                                                if ($result < 0) {
3416
                                                    $error++;
3417
                                                    $this->error = $mouvP->error;
3418
                                                    $this->errors = $mouvP->errors;
3419
                                                    break;
3420
                                                }
3421
3422
                                                $product_qty_remain -= $product_batch_qty;
3423
                                                // all product quantity was decremented
3424
                                                if ($product_qty_remain <= 0) {
3425
                                                    break;
3426
                                                }
3427
                                            }
3428
3429
                                            if (!$error && $product_qty_remain > 0) {
3430
                                                if (getDolGlobalInt('STOCK_ALLOW_NEGATIVE_TRANSFER')) {
3431
                                                    // take in the first batch
3432
                                                    $batch = $batchList[0];
3433
                                                    $result = $mouvP->livraison($user, $productStatic->id, $idwarehouse, $product_qty_remain, $this->lines[$i]->subprice, $langs->trans('InvoiceValidatedInDolibarr', $num), '', '', '', $batch->batch);
3434
                                                    if ($result < 0) {
3435
                                                        $error++;
3436
                                                        $this->error = $mouvP->error;
3437
                                                        $this->errors = $mouvP->errors;
3438
                                                    }
3439
                                                } else {
3440
                                                    $error++;
3441
                                                    $langs->load('errors');
3442
                                                    $warehouseStatic->fetch($idwarehouse);
3443
                                                    $this->error = $langs->trans('ErrorBatchNoFoundEnoughQuantityForProductInWarehouse', $productStatic->label, $warehouseStatic->ref);
3444
                                                    dol_syslog(__METHOD__ . ' Error: ' . $langs->transnoentitiesnoconv('ErrorBatchNoFoundEnoughQuantityForProductInWarehouse', $productStatic->label, $warehouseStatic->ref), LOG_ERR);
3445
                                                }
3446
                                            }
3447
                                        }
3448
                                    }
3449
                                }
3450
3451
                                if (!$is_batch_line) {
3452
                                    $result = $mouvP->livraison($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, $this->lines[$i]->subprice, $langs->trans("InvoiceValidatedInDolibarr", $num));
3453
                                    if ($result < 0) {
3454
                                        $error++;
3455
                                        $this->error = $mouvP->error;
3456
                                        $this->errors = $mouvP->errors;
3457
                                    }
3458
                                }
3459
                            }
3460
                        }
3461
                    }
3462
                }
3463
            }
3464
3465
            /*
3466
             * 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%)
3467
             * So we can continue to create new invoice situation
3468
             */
3469
            if (!$error && $this->type == self::TYPE_CREDIT_NOTE && $this->fk_facture_source > 0) {
3470
                $invoice_situation = new Facture($this->db);
3471
                $result = $invoice_situation->fetch($this->fk_facture_source);
3472
                if ($result > 0 && $invoice_situation->type == self::TYPE_SITUATION && $invoice_situation->situation_final == 1) {
3473
                    $invoice_situation->situation_final = 0;
3474
                    // Disable triggers because module can force situation_final to 1 by triggers (ex: SubTotal)
3475
                    $result = $invoice_situation->setFinal($user, 1);
3476
                }
3477
                if ($result < 0) {
3478
                    $this->error = $invoice_situation->error;
3479
                    $this->errors = $invoice_situation->errors;
3480
                    $error++;
3481
                }
3482
            }
3483
3484
            // Trigger calls
3485
            if (!$error && !$notrigger) {
3486
                // Call trigger
3487
                $result = $this->call_trigger('BILL_VALIDATE', $user);
3488
                if ($result < 0) {
3489
                    $error++;
3490
                }
3491
                // End call triggers
3492
            }
3493
3494
            if (!$error) {
3495
                $this->oldref = $this->ref;
3496
3497
                // Rename directory if dir was a temporary ref
3498
                if (preg_match('/^[\(]?PROV/i', $this->ref)) {
3499
                    // Now we rename also files into index
3500
                    $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) . "'";
3501
                    $sql .= " WHERE filename LIKE '" . $this->db->escape($this->ref) . "%' AND filepath = 'facture/" . $this->db->escape($this->ref) . "' and entity = " . $conf->entity;
3502
                    $resql = $this->db->query($sql);
3503
                    if (!$resql) {
3504
                        $error++;
3505
                        $this->error = $this->db->lasterror();
3506
                    }
3507
                    $sql = 'UPDATE ' . MAIN_DB_PREFIX . "ecm_files set filepath = 'facture/" . $this->db->escape($this->newref) . "'";
3508
                    $sql .= " WHERE filepath = 'facture/" . $this->db->escape($this->ref) . "' and entity = " . $conf->entity;
3509
                    $resql = $this->db->query($sql);
3510
                    if (!$resql) {
3511
                        $error++;
3512
                        $this->error = $this->db->lasterror();
3513
                    }
3514
3515
                    // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
3516
                    $oldref = dol_sanitizeFileName($this->ref);
3517
                    $newref = dol_sanitizeFileName($num);
3518
                    $dirsource = $conf->facture->dir_output . '/' . $oldref;
3519
                    $dirdest = $conf->facture->dir_output . '/' . $newref;
3520
                    if (!$error && file_exists($dirsource)) {
3521
                        dol_syslog(get_class($this) . "::validate rename dir " . $dirsource . " into " . $dirdest);
3522
3523
                        if (@rename($dirsource, $dirdest)) {
3524
                            dol_syslog("Rename ok");
3525
                            // Rename docs starting with $oldref with $newref
3526
                            $listoffiles = dol_dir_list($conf->facture->dir_output . '/' . $newref, 'files', 1, '^' . preg_quote($oldref, '/'));
3527
                            foreach ($listoffiles as $fileentry) {
3528
                                $dirsource = $fileentry['name'];
3529
                                $dirdest = preg_replace('/^' . preg_quote($oldref, '/') . '/', $newref, $dirsource);
3530
                                $dirsource = $fileentry['path'] . '/' . $dirsource;
3531
                                $dirdest = $fileentry['path'] . '/' . $dirdest;
3532
                                @rename($dirsource, $dirdest);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for rename(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

3532
                                /** @scrutinizer ignore-unhandled */ @rename($dirsource, $dirdest);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3533
                            }
3534
                        }
3535
                    }
3536
                }
3537
            }
3538
3539
            if (!$error && !$this->is_last_in_cycle()) {
3540
                if (!$this->updatePriceNextInvoice($langs)) {
3541
                    $error++;
3542
                }
3543
            }
3544
3545
            // Set new ref and define current status
3546
            if (!$error) {
3547
                $this->ref = $num;
3548
                $this->statut = self::STATUS_VALIDATED; // deprecated
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$statut has been deprecated: Use $status instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

3548
                /** @scrutinizer ignore-deprecated */ $this->statut = self::STATUS_VALIDATED; // deprecated

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
3549
                $this->status = self::STATUS_VALIDATED;
3550
                $this->date_validation = $now;
3551
                $i = 0;
3552
3553
                if (getDolGlobalString('INVOICE_USE_SITUATION')) {
3554
                    $final = true;
3555
                    $nboflines = count($this->lines);
3556
                    while (($i < $nboflines) && $final) {
3557
                        $final = ($this->lines[$i]->situation_percent == 100);
3558
                        $i++;
3559
                    }
3560
3561
                    if (empty($final)) {
3562
                        $this->situation_final = 0;
3563
                    } else {
3564
                        $this->situation_final = 1;
3565
                    }
3566
3567
                    $this->setFinal($user);
3568
                }
3569
            }
3570
        } else {
3571
            $error++;
3572
        }
3573
3574
        if (!$error) {
3575
            $this->db->commit();
3576
            return 1;
3577
        } else {
3578
            $this->db->rollback();
3579
            return -1;
3580
        }
3581
    }
3582
3583
    /**
3584
     * Update price of next invoice
3585
     *
3586
     * @param   Translate   $langs  Translate object
3587
     * @return  bool                false if KO, true if OK
3588
     */
3589
    public function updatePriceNextInvoice(&$langs)
3590
    {
3591
        foreach ($this->tab_next_situation_invoice as $next_invoice) {
3592
            $is_last = $next_invoice->is_last_in_cycle();
3593
3594
            if ($next_invoice->status == self::STATUS_DRAFT && $is_last != 1) {
3595
                $this->error = $langs->trans('updatePriceNextInvoiceErrorUpdateline', $next_invoice->ref);
3596
                return false;
3597
            }
3598
3599
            foreach ($next_invoice->lines as $line) {
3600
                $result = $next_invoice->updateline(
3601
                    $line->id,
3602
                    $line->desc,
3603
                    $line->subprice,
3604
                    $line->qty,
3605
                    $line->remise_percent,
3606
                    $line->date_start,
3607
                    $line->date_end,
3608
                    $line->tva_tx,
3609
                    $line->localtax1_tx,
3610
                    $line->localtax2_tx,
3611
                    'HT',
3612
                    $line->info_bits,
3613
                    $line->product_type,
3614
                    $line->fk_parent_line,
3615
                    0,
3616
                    $line->fk_fournprice,
3617
                    $line->pa_ht,
3618
                    $line->label,
3619
                    $line->special_code,
3620
                    $line->array_options,
3621
                    $line->situation_percent,
3622
                    $line->fk_unit
3623
                );
3624
3625
                if ($result < 0) {
3626
                    $this->error = $langs->trans('updatePriceNextInvoiceErrorUpdateline', $next_invoice->ref);
3627
                    return false;
3628
                }
3629
            }
3630
3631
            break; // Only the next invoice and not each next invoice
3632
        }
3633
3634
        return true;
3635
    }
3636
3637
    /**
3638
     *  Set draft status
3639
     *
3640
     *  @param  User    $user           Object user that modify
3641
     *  @param  int     $idwarehouse    Id warehouse to use for stock change.
3642
     *  @return int                     Return integer <0 if KO, >0 if OK
3643
     */
3644
    public function setDraft($user, $idwarehouse = -1)
3645
    {
3646
        // phpcs:enable
3647
        global $conf, $langs;
3648
3649
        $error = 0;
3650
3651
        if ($this->status == self::STATUS_DRAFT) {
3652
            dol_syslog(__METHOD__ . " already draft status", LOG_WARNING);
3653
            return 0;
3654
        }
3655
3656
        dol_syslog(__METHOD__, LOG_DEBUG);
3657
3658
        $this->db->begin();
3659
3660
        $sql = "UPDATE " . MAIN_DB_PREFIX . "facture";
3661
        $sql .= " SET fk_statut = " . self::STATUS_DRAFT;
3662
        $sql .= " WHERE rowid = " . ((int) $this->id);
3663
3664
        $result = $this->db->query($sql);
3665
        if ($result) {
3666
            if (!$error) {
3667
                $this->oldcopy = clone $this;
0 ignored issues
show
Documentation Bug introduced by
clone $this is of type DoliModules\Billing\Model\Invoice, but the property $oldcopy was declared to be of type DoliModules\Billing\Model\Facture. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
3668
            }
3669
3670
            // If we decrease stock on invoice validation, we increase back
3671
            if ($this->type != self::TYPE_DEPOSIT && $result >= 0 && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3672
                require_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
3673
                $langs->load("agenda");
3674
3675
                $num = count($this->lines);
3676
                for ($i = 0; $i < $num; $i++) {
3677
                    if ($this->lines[$i]->fk_product > 0) {
3678
                        $mouvP = new MouvementStock($this->db);
3679
                        $mouvP->origin = &$this;
3680
                        $mouvP->setOrigin($this->element, $this->id);
3681
                        // We decrease stock for product
3682
                        if ($this->type == self::TYPE_CREDIT_NOTE) {
3683
                            $result = $mouvP->livraison($user, $this->lines[$i]->fk_product, $idwarehouse, $this->lines[$i]->qty, $this->lines[$i]->subprice, $langs->trans("InvoiceBackToDraftInDolibarr", $this->ref));
3684
                        } else {
3685
                            $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
3686
                        }
3687
                    }
3688
                }
3689
            }
3690
3691
            if ($error == 0) {
3692
                $old_statut = $this->status;
3693
                $this->statut = self::STATUS_DRAFT; // deprecated
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$statut has been deprecated: Use $status instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

3693
                /** @scrutinizer ignore-deprecated */ $this->statut = self::STATUS_DRAFT; // deprecated

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
3694
                $this->status = self::STATUS_DRAFT;
3695
3696
                // Call trigger
3697
                $result = $this->call_trigger('BILL_UNVALIDATE', $user);
3698
                if ($result < 0) {
3699
                    $error++;
3700
                    $this->statut = $old_statut; // deprecated
3701
                    $this->status = $old_statut;
3702
                }
3703
                // End call triggers
3704
            } else {
3705
                $this->db->rollback();
3706
                return -1;
3707
            }
3708
3709
            if ($error == 0) {
3710
                $this->db->commit();
3711
                return 1;
3712
            } else {
3713
                $this->db->rollback();
3714
                return -1;
3715
            }
3716
        } else {
3717
            $this->error = $this->db->error();
3718
            $this->db->rollback();
3719
            return -1;
3720
        }
3721
    }
3722
3723
3724
    /**
3725
     *  Add an invoice line into database (linked to product/service or not).
3726
     *  Note: ->thirdparty must be defined.
3727
     *  Les parameters sont deja cense etre juste et avec valeurs finales a l'appel
3728
     *  de cette method. Aussi, pour le taux tva, il doit deja avoir ete defini
3729
     *  par l'appelant par la method get_default_tva(societe_vendeuse,societe_acheteuse,produit)
3730
     *  et le desc doit deja avoir la bonne valeur (a l'appelant de gerer le multilangue)
3731
     *
3732
     *  @param      string      $desc               Description of line
3733
     *  @param      double      $pu_ht              Unit price without tax (> 0 even for credit note)
3734
     *  @param      double      $qty                Quantity
3735
     *  @param      double      $txtva              Force Vat rate, -1 for auto (Can contain the vat_src_code too with syntax '9.9 (CODE)')
3736
     *  @param      double      $txlocaltax1        Local tax 1 rate (deprecated, use instead txtva with code inside)
3737
     *  @param      double      $txlocaltax2        Local tax 2 rate (deprecated, use instead txtva with code inside)
3738
     *  @param      int         $fk_product         Id of predefined product/service
3739
     *  @param      double      $remise_percent     Percent of discount on line
3740
     *  @param      int|string  $date_start         Date start of service
3741
     *  @param      int|string  $date_end           Date end of service
3742
     *  @param      int         $fk_code_ventilation    Code of dispatching into accountancy
3743
     *  @param      int         $info_bits          Bits of type of lines
3744
     *  @param      int         $fk_remise_except   Id discount used
3745
     *  @param      string      $price_base_type    'HT' or 'TTC'
3746
     *  @param      double      $pu_ttc             Unit price with tax (> 0 even for credit note)
3747
     *  @param      int         $type               Type of line (0=product, 1=service). Not used if fk_product is defined, the type of product is used.
3748
     *  @param      int         $rang               Position of line (-1 means last value + 1)
3749
     *  @param      int         $special_code       Special code (also used by externals modules!)
3750
     *  @param      string      $origin             Depend on global conf MAIN_CREATEFROM_KEEP_LINE_ORIGIN_INFORMATION can be 'orderdet', 'propaldet'..., else 'order','propal,'....
3751
     *  @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
3752
     *  @param      int         $fk_parent_line     Id of parent line
3753
     *  @param      int         $fk_fournprice      Supplier price id (to calculate margin) or ''
3754
     *  @param      int         $pa_ht              Buying price of line (to calculate margin) or ''
3755
     *  @param      string      $label              Label of the line (deprecated, do not use)
3756
     *  @param      array       $array_options      extrafields array
3757
     *  @param      int         $situation_percent  Situation advance percentage
3758
     *  @param      int         $fk_prev_id         Previous situation line id reference
3759
     *  @param      int|null    $fk_unit            Code of the unit to use. Null to use the default one
3760
     *  @param      double      $pu_ht_devise       Unit price in foreign currency
3761
     *  @param      string      $ref_ext            External reference of the line
3762
     *  @param      int         $noupdateafterinsertline    No update after insert of line
3763
     *  @return     int                             Return integer <0 if KO, Id of line if OK
3764
     */
3765
    public function addline(
3766
        $desc,
3767
        $pu_ht,
3768
        $qty,
3769
        $txtva,
3770
        $txlocaltax1 = 0,
3771
        $txlocaltax2 = 0,
3772
        $fk_product = 0,
3773
        $remise_percent = 0,
3774
        $date_start = '',
3775
        $date_end = '',
3776
        $fk_code_ventilation = 0,
3777
        $info_bits = 0,
3778
        $fk_remise_except = 0,
3779
        $price_base_type = 'HT',
3780
        $pu_ttc = 0,
3781
        $type = 0,
3782
        $rang = -1,
3783
        $special_code = 0,
3784
        $origin = '',
3785
        $origin_id = 0,
3786
        $fk_parent_line = 0,
3787
        $fk_fournprice = null,
3788
        $pa_ht = 0,
3789
        $label = '',
3790
        $array_options = array(),
3791
        $situation_percent = 100,
3792
        $fk_prev_id = 0,
3793
        $fk_unit = null,
3794
        $pu_ht_devise = 0,
3795
        $ref_ext = '',
3796
        $noupdateafterinsertline = 0
3797
    ) {
3798
        // Deprecation warning
3799
        if ($label) {
3800
            dol_syslog(__METHOD__ . ": using line label is deprecated", LOG_WARNING);
3801
            //var_dump(debug_backtrace(false));exit;
3802
        }
3803
3804
        global $mysoc, $conf, $langs;
3805
3806
        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, fk_code_ventilation=$fk_code_ventilation, 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);
3807
3808
        if ($this->status == self::STATUS_DRAFT) {
3809
            include_once DOL_DOCUMENT_ROOT . '/core/lib/price.lib.php';
3810
3811
            // Clean parameters
3812
            if (empty($remise_percent)) {
3813
                $remise_percent = 0;
3814
            }
3815
            if (empty($qty)) {
3816
                $qty = 0;
3817
            }
3818
            if (empty($info_bits)) {
3819
                $info_bits = 0;
3820
            }
3821
            if (empty($rang)) {
3822
                $rang = 0;
3823
            }
3824
            if (empty($fk_code_ventilation)) {
3825
                $fk_code_ventilation = 0;
3826
            }
3827
            if (empty($txtva)) {
3828
                $txtva = 0;
3829
            }
3830
            if (empty($txlocaltax1)) {
3831
                $txlocaltax1 = 0;
3832
            }
3833
            if (empty($txlocaltax2)) {
3834
                $txlocaltax2 = 0;
3835
            }
3836
            if (empty($fk_parent_line) || $fk_parent_line < 0) {
3837
                $fk_parent_line = 0;
3838
            }
3839
            if (empty($fk_prev_id)) {
3840
                $fk_prev_id = 'null';
3841
            }
3842
            if (!isset($situation_percent) || $situation_percent > 100 || (string) $situation_percent == '') {
3843
                $situation_percent = 100;
3844
            }
3845
            if (empty($ref_ext)) {
3846
                $ref_ext = '';
3847
            }
3848
3849
            $remise_percent = (float) price2num($remise_percent);
3850
            $qty = price2num($qty);
3851
            $pu_ht = price2num($pu_ht);
3852
            $pu_ht_devise = price2num($pu_ht_devise);
3853
            $pu_ttc = price2num($pu_ttc);
3854
            $pa_ht = price2num($pa_ht);
3855
            if (!preg_match('/\((.*)\)/', $txtva)) {
3856
                $txtva = price2num($txtva); // $txtva can have format '5.0(XXX)' or '5'
3857
            }
3858
            $txlocaltax1 = price2num($txlocaltax1);
3859
            $txlocaltax2 = price2num($txlocaltax2);
3860
3861
            if ($price_base_type == 'HT') {
3862
                $pu = $pu_ht;
3863
            } else {
3864
                $pu = $pu_ttc;
3865
            }
3866
3867
            // Check parameters
3868
            if ($type < 0) {
3869
                return -1;
3870
            }
3871
3872
            if ($date_start && $date_end && $date_start > $date_end) {
3873
                $langs->load("errors");
3874
                $this->error = $langs->trans('ErrorStartDateGreaterEnd');
3875
                return -1;
3876
            }
3877
3878
            $this->db->begin();
3879
3880
            $product_type = $type;
3881
            if (!empty($fk_product) && $fk_product > 0) {
3882
                $product = new Product($this->db);
3883
                $result = $product->fetch($fk_product);
3884
                $product_type = $product->type;
3885
3886
                if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_INVOICE') && $product_type == 0 && $product->stock_reel < $qty) {
3887
                    $langs->load("errors");
3888
                    $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnInvoice', $product->ref);
3889
                    $this->db->rollback();
3890
                    return -3;
3891
                }
3892
            }
3893
3894
            $localtaxes_type = getLocalTaxesFromRate($txtva, 0, $this->thirdparty, $mysoc);
3895
3896
            // Clean vat code
3897
            $reg = array();
3898
            $vat_src_code = '';
3899
            if (preg_match('/\((.*)\)/', $txtva, $reg)) {
3900
                $vat_src_code = $reg[1];
3901
                $txtva = preg_replace('/\s*\(.*\)/', '', $txtva); // Remove code into vatrate.
3902
            }
3903
3904
            // Calcul du total TTC et de la TVA pour la ligne a partir de
3905
            // qty, pu, remise_percent et txtva
3906
            // TRES IMPORTANT: C'est au moment de l'insertion ligne qu'on doit stocker
3907
            // la part ht, tva et ttc, et ce au niveau de la ligne qui a son propre taux tva.
3908
3909
            $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);
3910
3911
            $total_ht  = $tabprice[0];
3912
            $total_tva = $tabprice[1];
3913
            $total_ttc = $tabprice[2];
3914
            $total_localtax1 = $tabprice[9];
3915
            $total_localtax2 = $tabprice[10];
3916
            $pu_ht = $tabprice[3];
3917
3918
            // MultiCurrency
3919
            $multicurrency_total_ht = $tabprice[16];
3920
            $multicurrency_total_tva = $tabprice[17];
3921
            $multicurrency_total_ttc = $tabprice[18];
3922
            $pu_ht_devise = $tabprice[19];
3923
3924
            // Rank to use
3925
            $ranktouse = $rang;
3926
            if ($ranktouse == -1) {
3927
                $rangmax = $this->line_max($fk_parent_line);
3928
                $ranktouse = $rangmax + 1;
3929
            }
3930
3931
            // Insert line
3932
            $this->line = new FactureLigne($this->db);
0 ignored issues
show
Deprecated Code introduced by
The class DoliModules\Billing\Model\FactureLigne has been deprecated: Use Invoice instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

3932
            $this->line = /** @scrutinizer ignore-deprecated */ new FactureLigne($this->db);
Loading history...
3933
3934
            $this->line->context = $this->context;
3935
3936
            $this->line->fk_facture = $this->id;
3937
            $this->line->label = $label; // deprecated
3938
            $this->line->desc = $desc;
3939
            $this->line->ref_ext = $ref_ext;
3940
3941
            $this->line->qty = ($this->type == self::TYPE_CREDIT_NOTE ? abs($qty) : $qty); // For credit note, quantity is always positive and unit price negative
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->type == self::TYP...NOTE ? abs($qty) : $qty can also be of type string. However, the property $qty is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
3942
            $this->line->subprice = ($this->type == self::TYPE_CREDIT_NOTE ? -abs($pu_ht) : $pu_ht); // For credit note, unit price always negative, always positive otherwise
3943
3944
            $this->line->vat_src_code = $vat_src_code;
3945
            $this->line->tva_tx = $txtva;
0 ignored issues
show
Documentation Bug introduced by
It seems like $txtva can also be of type string. However, the property $tva_tx is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
3946
            $this->line->localtax1_tx = ($total_localtax1 ? $localtaxes_type[1] : 0);
3947
            $this->line->localtax2_tx = ($total_localtax2 ? $localtaxes_type[3] : 0);
3948
            $this->line->localtax1_type = empty($localtaxes_type[0]) ? '' : $localtaxes_type[0];
3949
            $this->line->localtax2_type = empty($localtaxes_type[2]) ? '' : $localtaxes_type[2];
3950
3951
            $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
3952
            $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
3953
            $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
3954
            $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
3955
            $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
3956
3957
            $this->line->fk_product = $fk_product;
3958
            $this->line->product_type = $product_type;
3959
            $this->line->remise_percent = $remise_percent;
3960
            $this->line->date_start = $date_start;
3961
            $this->line->date_end = $date_end;
3962
            $this->line->fk_code_ventilation = $fk_code_ventilation;
3963
            $this->line->rang = $ranktouse;
3964
            $this->line->info_bits = $info_bits;
3965
            $this->line->fk_remise_except = $fk_remise_except;
3966
3967
            $this->line->special_code = $special_code;
3968
            $this->line->fk_parent_line = $fk_parent_line;
3969
            $this->line->origin = $origin;
3970
            $this->line->origin_id = $origin_id;
3971
            $this->line->situation_percent = $situation_percent;
3972
            $this->line->fk_prev_id = $fk_prev_id;
0 ignored issues
show
Documentation Bug introduced by
It seems like $fk_prev_id can also be of type string. However, the property $fk_prev_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
3973
            $this->line->fk_unit = $fk_unit;
3974
3975
            // infos margin
3976
            $this->line->fk_fournprice = $fk_fournprice;
3977
            $this->line->pa_ht = $pa_ht;
3978
3979
            // Multicurrency
3980
            $this->line->fk_multicurrency = $this->fk_multicurrency;
3981
            $this->line->multicurrency_code = $this->multicurrency_code;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->multicurrency_code can also be of type string[]. However, the property $multicurrency_code is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
3982
            $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
3983
3984
            $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
3985
            $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
3986
            $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
3987
3988
            if (is_array($array_options) && count($array_options) > 0) {
3989
                $this->line->array_options = $array_options;
3990
            }
3991
3992
            $result = $this->line->insert();
3993
            if ($result > 0) {
3994
                // Reorder if child line
3995
                if (!empty($fk_parent_line)) {
3996
                    $this->line_order(true, 'DESC');
3997
                } elseif ($ranktouse > 0 && $ranktouse <= count($this->lines)) { // Update all rank of all other lines
3998
                    $linecount = count($this->lines);
3999
                    for ($ii = $ranktouse; $ii <= $linecount; $ii++) {
4000
                        $this->updateRangOfLine($this->lines[$ii - 1]->id, $ii + 1);
4001
                    }
4002
                }
4003
4004
                // Mise a jour information denormalisees au niveau de la facture meme
4005
                if (empty($noupdateafterinsertline)) {
4006
                    $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.
4007
                }
4008
4009
                if ($result > 0) {
4010
                    $this->db->commit();
4011
                    return $this->line->id;
4012
                } else {
4013
                    $this->error = $this->db->lasterror();
4014
                    $this->db->rollback();
4015
                    return -1;
4016
                }
4017
            } else {
4018
                $this->error = $this->line->error;
4019
                $this->errors = $this->line->errors;
4020
                $this->db->rollback();
4021
                return -2;
4022
            }
4023
        } else {
4024
            $this->errors[] = 'status of invoice must be Draft to allow use of ->addline()';
4025
            dol_syslog(get_class($this) . "::addline status of invoice must be Draft to allow use of ->addline()", LOG_ERR);
4026
            return -3;
4027
        }
4028
    }
4029
4030
    /**
4031
     *  Update a detail line
4032
     *
4033
     *  @param      int         $rowid              Id of line to update
4034
     *  @param      string      $desc               Description of line
4035
     *  @param      double      $pu                 Prix unitaire (HT ou TTC selon price_base_type) (> 0 even for credit note lines)
4036
     *  @param      double      $qty                Quantity
4037
     *  @param      double      $remise_percent     Percentage discount of the line
4038
     *  @param      int         $date_start         Date de debut de validite du service
4039
     *  @param      int         $date_end           Date de fin de validite du service
4040
     *  @param      double      $txtva              VAT Rate (Can be '8.5', '8.5 (ABC)')
4041
     *  @param      double      $txlocaltax1        Local tax 1 rate
4042
     *  @param      double      $txlocaltax2        Local tax 2 rate
4043
     *  @param      string      $price_base_type    HT or TTC
4044
     *  @param      int         $info_bits          Miscellaneous information
4045
     *  @param      int         $type               Type of line (0=product, 1=service)
4046
     *  @param      int         $fk_parent_line     Id of parent line (0 in most cases, used by modules adding sublevels into lines).
4047
     *  @param      int         $skip_update_total  Keep fields total_xxx to 0 (used for special lines by some modules)
4048
     *  @param      int         $fk_fournprice      Id of origin supplier price
4049
     *  @param      int         $pa_ht              Price (without tax) of product when it was bought
4050
     *  @param      string      $label              Label of the line (deprecated, do not use)
4051
     *  @param      int         $special_code       Special code (also used by externals modules!)
4052
     *  @param      array       $array_options      extrafields array
4053
     *  @param      int         $situation_percent  Situation advance percentage
4054
     *  @param      int|null    $fk_unit            Code of the unit to use. Null to use the default one
4055
     *  @param      double      $pu_ht_devise       Unit price in currency
4056
     *  @param      int         $notrigger          disable line update trigger
4057
     *  @param      string      $ref_ext            External reference of the line
4058
     *  @param      integer     $rang               rank of line
4059
     *  @return     int                             Return integer < 0 if KO, > 0 if OK
4060
     */
4061
    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 = array(), $situation_percent = 100, $fk_unit = null, $pu_ht_devise = 0, $notrigger = 0, $ref_ext = '', $rang = 0)
4062
    {
4063
        global $conf, $user;
4064
        // Deprecation warning
4065
        if ($label) {
4066
            dol_syslog(__METHOD__ . ": using line label is deprecated", LOG_WARNING);
4067
        }
4068
4069
        include_once DOL_DOCUMENT_ROOT . '/core/lib/price.lib.php';
4070
4071
        global $mysoc, $langs;
4072
4073
        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);
4074
4075
        if ($this->status == self::STATUS_DRAFT) {
4076
            if (!$this->is_last_in_cycle() && empty($this->error)) {
4077
                if (!$this->checkProgressLine($rowid, $situation_percent)) {
4078
                    if (!$this->error) {
4079
                        $this->error = $langs->trans('invoiceLineProgressError');
4080
                    }
4081
                    return -3;
4082
                }
4083
            }
4084
4085
            if ($date_start && $date_end && $date_start > $date_end) {
4086
                $langs->load("errors");
4087
                $this->error = $langs->trans('ErrorStartDateGreaterEnd');
4088
                return -1;
4089
            }
4090
4091
            $this->db->begin();
4092
4093
            // Clean parameters
4094
            if (empty($qty)) {
4095
                $qty = 0;
4096
            }
4097
            if (empty($fk_parent_line) || $fk_parent_line < 0) {
4098
                $fk_parent_line = 0;
4099
            }
4100
            if (empty($special_code) || $special_code == 3) {
4101
                $special_code = 0;
4102
            }
4103
            if (!isset($situation_percent) || $situation_percent > 100 || (string) $situation_percent == '') {
4104
                $situation_percent = 100;
4105
            }
4106
            if (empty($ref_ext)) {
4107
                $ref_ext = '';
4108
            }
4109
4110
            $remise_percent = (float) price2num($remise_percent);
4111
            $qty            = price2num($qty);
4112
            $pu             = price2num($pu);
4113
            $pu_ht_devise = price2num($pu_ht_devise);
4114
            $pa_ht = price2num($pa_ht);
4115
            if (!preg_match('/\((.*)\)/', $txtva)) {
4116
                $txtva = price2num($txtva); // $txtva can have format '5.0(XXX)' or '5'
4117
            }
4118
            $txlocaltax1    = (float) price2num($txlocaltax1);
4119
            $txlocaltax2    = (float) price2num($txlocaltax2);
4120
4121
            // Check parameters
4122
            if ($type < 0) {
4123
                return -1;
4124
            }
4125
4126
            // Calculate total with, without tax and tax from qty, pu, remise_percent and txtva
4127
            // TRES IMPORTANT: C'est au moment de l'insertion ligne qu'on doit stocker
4128
            // la part ht, tva et ttc, et ce au niveau de la ligne qui a son propre taux tva.
4129
4130
            $localtaxes_type = getLocalTaxesFromRate($txtva, 0, $this->thirdparty, $mysoc);
4131
4132
            // Clean vat code
4133
            $reg = array();
4134
            $vat_src_code = '';
4135
            if (preg_match('/\((.*)\)/', $txtva, $reg)) {
4136
                $vat_src_code = $reg[1];
4137
                $txtva = preg_replace('/\s*\(.*\)/', '', $txtva); // Remove code into vatrate.
4138
            }
4139
4140
            $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);
4141
4142
            $total_ht  = $tabprice[0];
4143
            $total_tva = $tabprice[1];
4144
            $total_ttc = $tabprice[2];
4145
            $total_localtax1 = $tabprice[9];
4146
            $total_localtax2 = $tabprice[10];
4147
            $pu_ht  = $tabprice[3];
4148
            $pu_tva = $tabprice[4];
4149
            $pu_ttc = $tabprice[5];
4150
4151
            // MultiCurrency
4152
            $multicurrency_total_ht = $tabprice[16];
4153
            $multicurrency_total_tva = $tabprice[17];
4154
            $multicurrency_total_ttc = $tabprice[18];
4155
            $pu_ht_devise = $tabprice[19];
4156
4157
            // Old properties: $price, $remise (deprecated)
4158
            $price = $pu;
4159
            $remise = 0;
4160
            if ($remise_percent > 0) {
4161
                $remise = round(((float) $pu * (float) $remise_percent / 100), 2);
4162
                $price = ((float) $pu - $remise);
4163
            }
4164
            $price = price2num($price);
4165
4166
            //Fetch current line from the database and then clone the object and set it in $oldline property
4167
            $line = new FactureLigne($this->db);
0 ignored issues
show
Deprecated Code introduced by
The class DoliModules\Billing\Model\FactureLigne has been deprecated: Use Invoice instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

4167
            $line = /** @scrutinizer ignore-deprecated */ new FactureLigne($this->db);
Loading history...
4168
            $line->fetch($rowid);
4169
            $line->fetch_optionals();
4170
4171
            if (!empty($line->fk_product)) {
4172
                $product = new Product($this->db);
4173
                $result = $product->fetch($line->fk_product);
4174
                $product_type = $product->type;
4175
4176
                if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_INVOICE') && $product_type == 0 && $product->stock_reel < $qty) {
4177
                    $langs->load("errors");
4178
                    $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnInvoice', $product->ref);
4179
                    $this->db->rollback();
4180
                    return -3;
4181
                }
4182
            }
4183
4184
            $staticline = clone $line;
4185
4186
            $line->oldline = $staticline;
4187
            $this->line = $line;
4188
            $this->line->context = $this->context;
4189
            $this->line->rang = $rang;
4190
4191
            // Reorder if fk_parent_line change
4192
            if (!empty($fk_parent_line) && !empty($staticline->fk_parent_line) && $fk_parent_line != $staticline->fk_parent_line) {
4193
                $rangmax = $this->line_max($fk_parent_line);
4194
                $this->line->rang = $rangmax + 1;
4195
            }
4196
4197
            $this->line->id = $rowid;
4198
            $this->line->rowid = $rowid;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocumentLine::$rowid has been deprecated: Try to use id property as possible (even if field into database is still rowid) ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

4198
            /** @scrutinizer ignore-deprecated */ $this->line->rowid = $rowid;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
4199
            $this->line->label = $label;
4200
            $this->line->desc = $desc;
4201
            $this->line->ref_ext = $ref_ext;
4202
            $this->line->qty = ($this->type == self::TYPE_CREDIT_NOTE ? abs($qty) : $qty); // For credit note, quantity is always positive and unit price negative
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->type == self::TYP...NOTE ? abs($qty) : $qty can also be of type string. However, the property $qty is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
4203
4204
            $this->line->vat_src_code = $vat_src_code;
4205
            $this->line->tva_tx = $txtva;
0 ignored issues
show
Documentation Bug introduced by
It seems like $txtva can also be of type string. However, the property $tva_tx is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
4206
            $this->line->localtax1_tx       = $txlocaltax1;
4207
            $this->line->localtax2_tx       = $txlocaltax2;
4208
            $this->line->localtax1_type     = empty($localtaxes_type[0]) ? '' : $localtaxes_type[0];
4209
            $this->line->localtax2_type     = empty($localtaxes_type[2]) ? '' : $localtaxes_type[2];
4210
4211
            $this->line->remise_percent     = $remise_percent;
4212
            $this->line->subprice           = ($this->type == self::TYPE_CREDIT_NOTE ? -abs($pu_ht) : $pu_ht); // For credit note, unit price always negative, always positive otherwise
4213
            $this->line->date_start = $date_start;
4214
            $this->line->date_end           = $date_end;
4215
            $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
4216
            $this->line->total_tva          = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($total_tva) : $total_tva);
4217
            $this->line->total_localtax1    = $total_localtax1;
4218
            $this->line->total_localtax2    = $total_localtax2;
4219
            $this->line->total_ttc          = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($total_ttc) : $total_ttc);
4220
            $this->line->info_bits          = $info_bits;
4221
            $this->line->special_code       = $special_code;
4222
            $this->line->product_type       = $type;
4223
            $this->line->fk_parent_line = $fk_parent_line;
4224
            $this->line->skip_update_total = $skip_update_total;
4225
            $this->line->situation_percent = $situation_percent;
4226
            $this->line->fk_unit = $fk_unit;
4227
4228
            $this->line->fk_fournprice = $fk_fournprice;
4229
            $this->line->pa_ht = $pa_ht;
4230
4231
            // Multicurrency
4232
            $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
4233
            $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
4234
            $this->line->multicurrency_total_tva    = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($multicurrency_total_tva) : $multicurrency_total_tva);
4235
            $this->line->multicurrency_total_ttc    = (($this->type == self::TYPE_CREDIT_NOTE || $qty < 0) ? -abs($multicurrency_total_ttc) : $multicurrency_total_ttc);
4236
4237
            if (is_array($array_options) && count($array_options) > 0) {
4238
                // We replace values in this->line->array_options only for entries defined into $array_options
4239
                foreach ($array_options as $key => $value) {
4240
                    $this->line->array_options[$key] = $array_options[$key];
4241
                }
4242
            }
4243
4244
            $result = $this->line->update($user, $notrigger);
4245
            if ($result > 0) {
4246
                // Reorder if child line
4247
                if (!empty($fk_parent_line)) {
4248
                    $this->line_order(true, 'DESC');
4249
                }
4250
4251
                // Mise a jour info denormalisees au niveau facture
4252
                $this->update_price(1, 'auto');
4253
                $this->db->commit();
4254
                return $result;
4255
            } else {
4256
                $this->error = $this->line->error;
4257
                $this->db->rollback();
4258
                return -1;
4259
            }
4260
        } else {
4261
            $this->error = "Invoice statut makes operation forbidden";
4262
            return -2;
4263
        }
4264
    }
4265
4266
    /**
4267
     * Check if the percent edited is lower of next invoice line
4268
     *
4269
     * @param   int     $idline             id of line to check
4270
     * @param   float   $situation_percent  progress percentage need to be test
4271
     * @return  bool                        false if KO, true if OK
4272
     */
4273
    public function checkProgressLine($idline, $situation_percent)
4274
    {
4275
        $sql = 'SELECT fd.situation_percent FROM ' . MAIN_DB_PREFIX . 'facturedet fd
4276
				INNER JOIN ' . MAIN_DB_PREFIX . 'facture f ON (fd.fk_facture = f.rowid)
4277
				WHERE fd.fk_prev_id = ' . ((int) $idline) . ' AND f.fk_statut <> 0';
4278
4279
        $result = $this->db->query($sql);
4280
        if (!$result) {
4281
            $this->error = $this->db->error();
4282
            return false;
4283
        }
4284
4285
        $obj = $this->db->fetch_object($result);
4286
4287
        if ($obj === null) {
4288
            return true;
4289
        } else {
4290
            return ($situation_percent < $obj->situation_percent);
4291
        }
4292
    }
4293
4294
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4295
    /**
4296
     * Update invoice line with percentage
4297
     *
4298
     * @param  FactureLigne $line           Invoice line
4299
     * @param  int          $percent        Percentage
4300
     * @param  boolean      $update_price   Update object price
4301
     * @return void
4302
     */
4303
    public function update_percent($line, $percent, $update_price = true)
4304
    {
4305
        // phpcs:enable
4306
        global $mysoc, $user;
4307
4308
        // Progress should never be changed for discount lines
4309
        if (($line->info_bits & 2) == 2) {
4310
            return;
4311
        }
4312
4313
        include_once DOL_DOCUMENT_ROOT . '/core/lib/price.lib.php';
4314
4315
        // Cap percentages to 100
4316
        if ($percent > 100) {
4317
            $percent = 100;
4318
        }
4319
        $line->situation_percent = $percent;
4320
        $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);
4321
        $line->total_ht = $tabprice[0];
4322
        $line->total_tva = $tabprice[1];
4323
        $line->total_ttc = $tabprice[2];
4324
        $line->total_localtax1 = $tabprice[9];
4325
        $line->total_localtax2 = $tabprice[10];
4326
        $line->multicurrency_total_ht  = $tabprice[16];
4327
        $line->multicurrency_total_tva = $tabprice[17];
4328
        $line->multicurrency_total_ttc = $tabprice[18];
4329
        $line->update($user);
4330
4331
        // sometimes it is better to not update price for each line, ie when updating situation on all lines
4332
        if ($update_price) {
4333
            $this->update_price(1);
4334
        }
4335
    }
4336
4337
    /**
4338
     *  Delete line in database
4339
     *
4340
     *  @param      int     $rowid      Id of line to delete
4341
     *  @param      int     $id         Id of object (for a check)
4342
     *  @return     int                 Return integer <0 if KO, >0 if OK
4343
     */
4344
    public function deleteLine($rowid, $id = 0)
4345
    {
4346
        global $user;
4347
4348
        dol_syslog(get_class($this) . "::deleteline rowid=" . ((int) $rowid), LOG_DEBUG);
4349
4350
        if ($this->status != self::STATUS_DRAFT) {
4351
            $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
4352
            return -1;
4353
        }
4354
4355
        $line = new FactureLigne($this->db);
0 ignored issues
show
Deprecated Code introduced by
The class DoliModules\Billing\Model\FactureLigne has been deprecated: Use Invoice instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

4355
        $line = /** @scrutinizer ignore-deprecated */ new FactureLigne($this->db);
Loading history...
4356
4357
        $line->context = $this->context;
4358
4359
        // Load line
4360
        $result = $line->fetch($rowid);
4361
        if (!($result > 0)) {
4362
            dol_print_error($this->db, $line->error, $line->errors);
4363
            return -1;
4364
        }
4365
4366
        if ($id > 0 && $line->fk_facture != $id) {
4367
            $this->error = 'ErrorLineIDDoesNotMatchWithObjectID';
4368
            return -1;
4369
        }
4370
4371
        $this->db->begin();
4372
4373
        // Memorize previous line for triggers
4374
        $staticline = clone $line;
4375
        $line->oldline = $staticline;
4376
4377
        if ($line->delete($user) > 0) {
4378
            $result = $this->update_price(1);
4379
4380
            if ($result > 0) {
4381
                $this->db->commit();
4382
                return 1;
4383
            } else {
4384
                $this->db->rollback();
4385
                $this->error = $this->db->lasterror();
4386
                return -1;
4387
            }
4388
        } else {
4389
            $this->db->rollback();
4390
            $this->error = $line->error;
4391
            return -1;
4392
        }
4393
    }
4394
4395
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4396
    /**
4397
     *  Set percent discount
4398
     *
4399
     *  @deprecated
4400
     *  @see setDiscount()
4401
     *  @param      User    $user       User that set discount
4402
     *  @param      double  $remise     Discount
4403
     *  @param      int     $notrigger  1=Does not execute triggers, 0= execute triggers
4404
     *  @return     int                 Return integer <0 if KO, >0 if OK
4405
     */
4406
    public function set_remise($user, $remise, $notrigger = 0)
4407
    {
4408
        // phpcs:enable
4409
        dol_syslog(get_class($this) . "::set_remise is deprecated, use setDiscount instead", LOG_NOTICE);
4410
        // @phan-suppress-next-line PhanDeprecatedFunction
4411
        return $this->setDiscount($user, $remise, $notrigger);
4412
    }
4413
4414
    /**
4415
     *  Set percent discount
4416
     *
4417
     *  @param      User    $user       User that set discount
4418
     *  @param      float   $remise     Discount
4419
     *  @param      int     $notrigger  1=Does not execute triggers, 0= execute triggers
4420
     *  @return     int                 Return integer <0 if KO, >0 if OK
4421
     */
4422
    public function setDiscount($user, $remise, $notrigger = 0)
4423
    {
4424
        // Clean parameters
4425
        if (empty($remise)) {
4426
            $remise = 0;
4427
        }
4428
4429
        if ($user->hasRight('facture', 'creer')) {
4430
            $remise = (float) price2num($remise, 2);
4431
4432
            $error = 0;
4433
4434
            $this->db->begin();
4435
4436
            $sql = "UPDATE " . MAIN_DB_PREFIX . "facture";
4437
            $sql .= " SET remise_percent = " . ((float) $remise);
4438
            $sql .= " WHERE rowid = " . ((int) $this->id);
4439
            $sql .= " AND fk_statut = " . ((int) self::STATUS_DRAFT);
4440
4441
            dol_syslog(__METHOD__, LOG_DEBUG);
4442
            $resql = $this->db->query($sql);
4443
            if (!$resql) {
4444
                $this->errors[] = $this->db->error();
4445
                $error++;
4446
            }
4447
4448
            if (!$notrigger && empty($error)) {
4449
                // Call trigger
4450
                $result = $this->call_trigger('BILL_MODIFY', $user);
4451
                if ($result < 0) {
4452
                    $error++;
4453
                }
4454
                // End call triggers
4455
            }
4456
4457
            if (!$error) {
4458
                $this->remise_percent = $remise;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$remise_percent has been deprecated: The discount percent is on line level now ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

4458
                /** @scrutinizer ignore-deprecated */ $this->remise_percent = $remise;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
4459
                $this->update_price(1);
4460
4461
                $this->db->commit();
4462
                return 1;
4463
            } else {
4464
                foreach ($this->errors as $errmsg) {
4465
                    dol_syslog(__METHOD__ . ' Error: ' . $errmsg, LOG_ERR);
4466
                    $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
4467
                }
4468
                $this->db->rollback();
4469
                return -1 * $error;
4470
            }
4471
        }
4472
4473
        return 0;
4474
    }
4475
4476
4477
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4478
    /**
4479
     *  Set absolute discount
4480
     *
4481
     *  @param      User    $user       User that set discount
4482
     *  @param      double  $remise     Discount
4483
     *  @param      int     $notrigger  1=Does not execute triggers, 0= execute triggers
4484
     *  @return     int                 Return integer <0 if KO, >0 if OK
4485
     */
4486
    /*
4487
    public function set_remise_absolue($user, $remise, $notrigger = 0)
4488
    {
4489
		// phpcs:enable
4490
        if (empty($remise)) {
4491
            $remise = 0;
4492
        }
4493
4494
        if ($user->hasRight('facture', 'creer')) {
4495
            $error = 0;
4496
4497
            $this->db->begin();
4498
4499
            $remise = price2num($remise);
4500
4501
            $sql = 'UPDATE '.MAIN_DB_PREFIX.'facture';
4502
            $sql .= ' SET remise_absolue = '.((float) $remise);
4503
            $sql .= " WHERE rowid = ".((int) $this->id);
4504
            $sql .= ' AND fk_statut = '.self::STATUS_DRAFT;
4505
4506
            dol_syslog(__METHOD__, LOG_DEBUG);
4507
            $resql = $this->db->query($sql);
4508
            if (!$resql) {
4509
                $this->errors[] = $this->db->error();
4510
                $error++;
4511
            }
4512
4513
            if (!$error) {
4514
                $this->oldcopy = clone $this;
4515
                $this->remise_absolue = $remise;
4516
                $this->update_price(1);
4517
            }
4518
4519
            if (!$notrigger && empty($error)) {
4520
                // Call trigger
4521
                $result = $this->call_trigger('BILL_MODIFY', $user);
4522
                if ($result < 0) {
4523
                    $error++;
4524
                }
4525
                // End call triggers
4526
            }
4527
4528
            if (!$error) {
4529
                $this->db->commit();
4530
                return 1;
4531
            } else {
4532
                foreach ($this->errors as $errmsg) {
4533
                    dol_syslog(__METHOD__.' Error: '.$errmsg, LOG_ERR);
4534
                    $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
4535
                }
4536
                $this->db->rollback();
4537
                return -1 * $error;
4538
            }
4539
        }
4540
4541
        return 0;
4542
    }
4543
    */
4544
4545
    /**
4546
     *      Return next reference of customer invoice not already used (or last reference)
4547
     *      according to numbering module defined into constant FACTURE_ADDON
4548
     *
4549
     *      @param     Societe      $soc        object company
4550
     *      @param     string       $mode       'next' for next value or 'last' for last value
4551
     *      @return    string                   free ref or last ref
4552
     */
4553
    public function getNextNumRef($soc, $mode = 'next')
4554
    {
4555
        global $conf, $langs;
4556
4557
        if ($this->module_source == 'takepos') {
4558
            $langs->load('cashdesk');
4559
4560
            $moduleName = 'takepos';
4561
            $moduleSourceName = 'Takepos';
4562
            $addonConstName = 'TAKEPOS_REF_ADDON';
4563
4564
            // Clean parameters (if not defined or using deprecated value)
4565
            if (!getDolGlobalString('TAKEPOS_REF_ADDON')) {
4566
                $conf->global->TAKEPOS_REF_ADDON = 'mod_takepos_ref_simple';
4567
            }
4568
4569
            $addon = getDolGlobalString('TAKEPOS_REF_ADDON');
4570
        } else {
4571
            $langs->load('bills');
4572
4573
            $moduleName = 'facture';
4574
            $moduleSourceName = 'Invoice';
4575
            $addonConstName = 'FACTURE_ADDON';
4576
4577
            // Clean parameters (if not defined or using deprecated value)
4578
            if (!getDolGlobalString('FACTURE_ADDON')) {
4579
                $conf->global->FACTURE_ADDON = 'mod_facture_terre';
4580
            } elseif (getDolGlobalString('FACTURE_ADDON') == 'terre') {
4581
                $conf->global->FACTURE_ADDON = 'mod_facture_terre';
4582
            } elseif (getDolGlobalString('FACTURE_ADDON') == 'mercure') {
4583
                $conf->global->FACTURE_ADDON = 'mod_facture_mercure';
4584
            }
4585
4586
            $addon = getDolGlobalString('FACTURE_ADDON');
4587
        }
4588
4589
        if (!empty($addon)) {
4590
            dol_syslog("Call getNextNumRef with " . $addonConstName . " = " . getDolGlobalString('FACTURE_ADDON') . ", thirdparty=" . $soc->name . ", type=" . $soc->typent_code . ", mode=" . $mode, LOG_DEBUG);
4591
4592
            $mybool = false;
4593
4594
            $file = $addon . '.php';
4595
            $classname = $addon;
4596
4597
4598
            // Include file with class
4599
            $dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
4600
            foreach ($dirmodels as $reldir) {
4601
                $dir = dol_buildpath($reldir . 'core/modules/' . $moduleName . '/');
4602
4603
                // Load file with numbering class (if found)
4604
                if (is_file($dir . $file) && is_readable($dir . $file)) {
4605
                    $mybool |= include_once $dir . $file;
4606
                }
4607
            }
4608
4609
            // For compatibility
4610
            if (!$mybool) {
4611
                $file = $addon . '/' . $addon . '.modules.php';
4612
                $classname = 'mod_' . $moduleName . '_' . $addon;
4613
                $classname = preg_replace('/\-.*$/', '', $classname);
4614
                // Include file with class
4615
                foreach ($conf->file->dol_document_root as $dirroot) {
4616
                    $dir = $dirroot . '/core/modules/' . $moduleName . '/';
4617
4618
                    // Load file with numbering class (if found)
4619
                    if (is_file($dir . $file) && is_readable($dir . $file)) {
4620
                        $mybool |= include_once $dir . $file;
4621
                    }
4622
                }
4623
            }
4624
4625
            if (!$mybool) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mybool of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
4626
                dol_print_error(null, 'Failed to include file ' . $file);
4627
                return '';
4628
            }
4629
4630
            $obj = new $classname();
4631
4632
            $numref = $obj->getNextValue($soc, $this, $mode);
4633
4634
4635
            /**
4636
             * $numref can be empty in case we ask for the last value because if there is no invoice created with the
4637
             * set up mask.
4638
             */
4639
            if ($mode != 'last' && !$numref) {
4640
                $this->error = $obj->error;
4641
                return '';
4642
            }
4643
4644
            return $numref;
4645
        } else {
4646
            $langs->load('errors');
4647
            print $langs->trans('Error') . ' ' . $langs->trans('ErrorModuleSetupNotComplete', $langs->transnoentitiesnoconv($moduleSourceName));
4648
            return '';
4649
        }
4650
    }
4651
4652
    /**
4653
     *  Load miscellaneous information for tab "Info"
4654
     *
4655
     *  @param  int     $id     Id of object to load
4656
     *  @return void
4657
     */
4658
    public function info($id)
4659
    {
4660
        $sql = 'SELECT c.rowid, datec, date_valid as datev, tms as datem,';
4661
        $sql .= ' date_closing as dateclosing,';
4662
        $sql .= ' fk_user_author, fk_user_valid, fk_user_closing';
4663
        $sql .= ' FROM ' . MAIN_DB_PREFIX . 'facture as c';
4664
        $sql .= ' WHERE c.rowid = ' . ((int) $id);
4665
4666
        $result = $this->db->query($sql);
4667
        if ($result) {
4668
            if ($this->db->num_rows($result)) {
4669
                $obj = $this->db->fetch_object($result);
4670
4671
                $this->id = $obj->rowid;
4672
                $this->user_creation_id = $obj->fk_user_author;
4673
                $this->user_validation_id = $obj->fk_user_valid;
4674
                $this->user_closing_id = $obj->fk_user_closing;
4675
4676
                $this->date_creation     = $this->db->jdate($obj->datec);
4677
                $this->date_modification = $this->db->jdate($obj->datem);
4678
                $this->date_validation   = $this->db->jdate($obj->datev);
4679
                $this->date_closing      = $this->db->jdate($obj->dateclosing);
4680
            }
4681
            $this->db->free($result);
4682
        } else {
4683
            dol_print_error($this->db);
4684
        }
4685
    }
4686
4687
4688
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4689
    /**
4690
     *  Return list of invoices (eventually filtered on a user) into an array
4691
     *
4692
     *  @param      int     $shortlist      0=Return array[id]=ref, 1=Return array[](id=>id,ref=>ref,name=>name)
4693
     *  @param      int     $draft          0=not draft, 1=draft
4694
     *  @param      User    $excluser       Object user to exclude
4695
     *  @param      int     $socid          Id third party
4696
     *  @param      int     $limit          For pagination
4697
     *  @param      int     $offset         For pagination
4698
     *  @param      string  $sortfield      Sort criteria
4699
     *  @param      string  $sortorder      Sort order
4700
     *  @return     array|int               -1 if KO, array with result if OK
4701
     */
4702
    public function liste_array($shortlist = 0, $draft = 0, $excluser = null, $socid = 0, $limit = 0, $offset = 0, $sortfield = 'f.datef,f.rowid', $sortorder = 'DESC')
4703
    {
4704
        // phpcs:enable
4705
        global $user;
4706
4707
        $ga = array();
4708
4709
        $sql = "SELECT s.rowid, s.nom as name, s.client,";
4710
        $sql .= " f.rowid as fid, f.ref as ref, f.datef as df";
4711
        $sql .= " FROM " . MAIN_DB_PREFIX . "societe as s, " . MAIN_DB_PREFIX . "facture as f";
4712
        $sql .= " WHERE f.entity IN (" . getEntity('invoice') . ")";
4713
        $sql .= " AND f.fk_soc = s.rowid";
4714
        if ($draft) {
4715
            $sql .= " AND f.fk_statut = " . self::STATUS_DRAFT;
4716
        }
4717
        if (is_object($excluser)) {
4718
            $sql .= " AND f.fk_user_author <> " . ((int) $excluser->id);
4719
        }
4720
        // If the internal user must only see his customers, force searching by him
4721
        $search_sale = 0;
4722
        if (!$user->hasRight('societe', 'client', 'voir')) {
4723
            $search_sale = $user->id;
4724
        }
4725
        // Search on sale representative
4726
        if ($search_sale && $search_sale != '-1') {
4727
            if ($search_sale == -2) {
4728
                $sql .= " AND NOT EXISTS (SELECT sc.fk_soc FROM " . MAIN_DB_PREFIX . "societe_commerciaux as sc WHERE sc.fk_soc = f.fk_soc)";
4729
            } elseif ($search_sale > 0) {
4730
                $sql .= " AND EXISTS (SELECT sc.fk_soc FROM " . MAIN_DB_PREFIX . "societe_commerciaux as sc WHERE sc.fk_soc = f.fk_soc AND sc.fk_user = " . ((int) $search_sale) . ")";
4731
            }
4732
        }
4733
        // Search on socid
4734
        if ($socid) {
4735
            $sql .= " AND f.fk_soc = " . ((int) $socid);
4736
        }
4737
        $sql .= $this->db->order($sortfield, $sortorder);
4738
        $sql .= $this->db->plimit($limit, $offset);
4739
4740
        $result = $this->db->query($sql);
4741
        if ($result) {
4742
            $numc = $this->db->num_rows($result);
4743
            if ($numc) {
4744
                $i = 0;
4745
                while ($i < $numc) {
4746
                    $obj = $this->db->fetch_object($result);
4747
4748
                    if ($shortlist == 1) {
4749
                        $ga[$obj->fid] = $obj->ref;
4750
                    } elseif ($shortlist == 2) {
4751
                        $ga[$obj->fid] = $obj->ref . ' (' . $obj->name . ')';
4752
                    } else {
4753
                        $ga[$i]['id'] = $obj->fid;
4754
                        $ga[$i]['ref']  = $obj->ref;
4755
                        $ga[$i]['name'] = $obj->name;
4756
                    }
4757
                    $i++;
4758
                }
4759
            }
4760
            return $ga;
4761
        } else {
4762
            dol_print_error($this->db);
4763
            return -1;
4764
        }
4765
    }
4766
4767
4768
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4769
    /**
4770
     *  Return list of invoices qualified to be replaced by another invoice.
4771
     *  Invoices matching the following rules are returned:
4772
     *  (Status validated or abandoned for a reason 'other') + not paid + no payment at all + not already replaced
4773
     *
4774
     *  @param      int         $socid      Id thirdparty
4775
     *  @return     array|int               Array of invoices ('id'=>id, 'ref'=>ref, 'status'=>status, 'paymentornot'=>0/1)
4776
     */
4777
    public function list_replacable_invoices($socid = 0)
4778
    {
4779
        // phpcs:enable
4780
        global $conf;
4781
4782
        $return = array();
4783
4784
        $sql = "SELECT f.rowid as rowid, f.ref, f.fk_statut as status, f.paye as paid,";
4785
        $sql .= " ff.rowid as rowidnext";
4786
        //$sql .= ", SUM(pf.amount) as alreadypaid";
4787
        $sql .= " FROM " . MAIN_DB_PREFIX . "facture as f";
4788
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "paiement_facture as pf ON f.rowid = pf.fk_facture";
4789
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "facture as ff ON f.rowid = ff.fk_facture_source";
4790
        $sql .= " WHERE (f.fk_statut = " . self::STATUS_VALIDATED . " OR (f.fk_statut = " . self::STATUS_ABANDONED . " AND f.close_code = '" . self::CLOSECODE_ABANDONED . "'))";
4791
        $sql .= " AND f.entity IN (" . getEntity('invoice') . ")";
4792
        $sql .= " AND f.paye = 0"; // Not paid completely
4793
        $sql .= " AND pf.fk_paiement IS NULL"; // No payment already done
4794
        $sql .= " AND ff.fk_statut IS NULL"; // Return true if it is not a replacement invoice
4795
        if ($socid > 0) {
4796
            $sql .= " AND f.fk_soc = " . ((int) $socid);
4797
        }
4798
        //$sql .= " GROUP BY f.rowid, f.ref, f.fk_statut, f.paye, ff.rowid";
4799
        $sql .= " ORDER BY f.ref";
4800
4801
        dol_syslog(get_class($this) . "::list_replacable_invoices", LOG_DEBUG);
4802
        $resql = $this->db->query($sql);
4803
        if ($resql) {
4804
            while ($obj = $this->db->fetch_object($resql)) {
4805
                $return[$obj->rowid] = array(
4806
                    'id' => $obj->rowid,
4807
                    'ref' => $obj->ref,
4808
                    'status' => $obj->status,
4809
                    'paid' => $obj->paid,
4810
                    'alreadypaid' => 0
4811
                );
4812
            }
4813
            //print_r($return);
4814
            return $return;
4815
        } else {
4816
            $this->error = $this->db->error();
4817
            return -1;
4818
        }
4819
    }
4820
4821
4822
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4823
    /**
4824
     *  Return list of invoices qualified to be corrected by a credit note.
4825
     *  Invoices matching the following rules are returned:
4826
     *  (validated + payment on process) or classified (paid completely or paid partiely) + not already replaced + not already a credit note
4827
     *
4828
     *  @param      int         $socid      Id thirdparty
4829
     *  @return     array|int               Array of invoices ($id => array('ref'=>,'paymentornot'=>,'status'=>,'paye'=>)
4830
     */
4831
    public function list_qualified_avoir_invoices($socid = 0)
4832
    {
4833
        // phpcs:enable
4834
        global $conf;
4835
4836
        $return = array();
4837
4838
4839
        $sql = "SELECT f.rowid as rowid, f.ref, f.fk_statut, f.type, f.subtype, f.paye, pf.fk_paiement";
4840
        $sql .= " FROM " . MAIN_DB_PREFIX . "facture as f";
4841
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "paiement_facture as pf ON f.rowid = pf.fk_facture";
4842
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "facture as ff ON (f.rowid = ff.fk_facture_source AND ff.type=" . self::TYPE_REPLACEMENT . ")";
4843
        $sql .= " WHERE f.entity IN (" . getEntity('invoice') . ")";
4844
        $sql .= " AND f.fk_statut in (" . self::STATUS_VALIDATED . "," . self::STATUS_CLOSED . ")";
4845
        //  $sql.= " WHERE f.fk_statut >= 1";
4846
        //  $sql.= " AND (f.paye = 1";              // Classee payee completement
4847
        //  $sql.= " OR f.close_code IS NOT NULL)"; // Classee payee partiellement
4848
        $sql .= " AND ff.type IS NULL"; // Renvoi vrai si pas facture de replacement
4849
        $sql .= " AND f.type <> " . self::TYPE_CREDIT_NOTE; // Exclude credit note invoices from selection
4850
4851
        if (getDolGlobalString('INVOICE_USE_SITUATION_CREDIT_NOTE')) {
4852
            // Keep invoices that are not situation invoices or that are the last in series if it is a situation invoice
4853
            $sql .= " AND (f.type <> " . self::TYPE_SITUATION . " OR f.rowid IN ";
4854
            $sql .= '(SELECT MAX(fs.rowid)'; // This select returns several ID because of the group by later
4855
            $sql .= " FROM " . MAIN_DB_PREFIX . "facture as fs";
4856
            $sql .= " WHERE fs.entity IN (" . getEntity('invoice') . ")";
4857
            $sql .= " AND fs.type = " . self::TYPE_SITUATION;
4858
            $sql .= " AND fs.fk_statut IN (" . self::STATUS_VALIDATED . "," . self::STATUS_CLOSED . ")";
4859
            if ($socid > 0) {
4860
                $sql .= " AND fs.fk_soc = " . ((int) $socid);
4861
            }
4862
            $sql .= " GROUP BY fs.situation_cycle_ref)"; // For each situation_cycle_ref, we take the higher rowid
4863
            $sql .= ")";
4864
        } else {
4865
            $sql .= " AND f.type <> " . self::TYPE_SITUATION; // Keep invoices that are not situation invoices
4866
        }
4867
4868
        if ($socid > 0) {
4869
            $sql .= " AND f.fk_soc = " . ((int) $socid);
4870
        }
4871
        $sql .= " ORDER BY f.ref";
4872
4873
        dol_syslog(get_class($this) . "::list_qualified_avoir_invoices", LOG_DEBUG);
4874
        $resql = $this->db->query($sql);
4875
        if ($resql) {
4876
            while ($obj = $this->db->fetch_object($resql)) {
4877
                $qualified = 0;
4878
                if ($obj->fk_statut == self::STATUS_VALIDATED) {
4879
                    $qualified = 1;
4880
                }
4881
                if ($obj->fk_statut == self::STATUS_CLOSED) {
4882
                    $qualified = 1;
4883
                }
4884
                if ($qualified) {
4885
                    //$ref=$obj->ref;
4886
                    $paymentornot = ($obj->fk_paiement ? 1 : 0);
4887
                    $return[$obj->rowid] = array('ref' => $obj->ref, 'status' => $obj->fk_statut, 'type' => $obj->type, 'paye' => $obj->paye, 'paymentornot' => $paymentornot);
4888
                }
4889
            }
4890
4891
            return $return;
4892
        } else {
4893
            $this->error = $this->db->error();
4894
            return -1;
4895
        }
4896
    }
4897
4898
4899
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4900
    /**
4901
     *  Load indicators for dashboard (this->nbtodo and this->nbtodolate)
4902
     *
4903
     *  @param  User                    $user       Object user
4904
     *  @return WorkboardResponse|int               Return integer <0 if KO, WorkboardResponse if OK
4905
     */
4906
    public function load_board($user)
4907
    {
4908
        // phpcs:enable
4909
        global $conf, $langs;
4910
4911
        $clause = " WHERE";
4912
4913
        $sql = "SELECT f.rowid, f.date_lim_reglement as datefin, f.fk_statut as status, f.total_ht";
4914
        $sql .= " FROM " . MAIN_DB_PREFIX . "facture as f";
4915
        if (!$user->hasRight('societe', 'client', 'voir')) {
4916
            $sql .= " JOIN " . MAIN_DB_PREFIX . "societe_commerciaux as sc ON f.fk_soc = sc.fk_soc";
4917
            $sql .= " WHERE sc.fk_user = " . ((int) $user->id);
4918
            $clause = " AND";
4919
        }
4920
        $sql .= $clause . " f.paye=0";
4921
        $sql .= " AND f.entity IN (" . getEntity('invoice') . ")";
4922
        $sql .= " AND f.fk_statut = " . self::STATUS_VALIDATED;
4923
        if ($user->socid) {
4924
            $sql .= " AND f.fk_soc = " . ((int) $user->socid);
4925
        }
4926
4927
        $resql = $this->db->query($sql);
4928
        if ($resql) {
4929
            $langs->load("bills");
4930
            $now = dol_now();
4931
4932
            $response = new WorkboardResponse();
4933
            $response->warning_delay = $conf->facture->client->warning_delay / 60 / 60 / 24;
4934
            $response->label = $langs->trans("CustomerBillsUnpaid");
4935
            $response->labelShort = $langs->trans("Unpaid");
4936
            $response->url = DOL_URL_ROOT . '/compta/facture/list.php?search_status=1&mainmenu=billing&leftmenu=customers_bills';
4937
            $response->img = img_object('', "bill");
4938
4939
            $generic_facture = new Facture($this->db);
4940
4941
            while ($obj = $this->db->fetch_object($resql)) {
4942
                $generic_facture->date_lim_reglement = $this->db->jdate($obj->datefin);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->jdate($obj->datefin) can also be of type string. However, the property $date_lim_reglement is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
4943
                $generic_facture->statut = $obj->status;
0 ignored issues
show
Deprecated Code introduced by
The property DoliCore\Base\GenericDocument::$statut has been deprecated: Use $status instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

4943
                /** @scrutinizer ignore-deprecated */ $generic_facture->statut = $obj->status;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
4944
                $generic_facture->status = $obj->status;
4945
4946
                $response->nbtodo++;
4947
                $response->total += $obj->total_ht;
4948
4949
                if ($generic_facture->hasDelay()) {
4950
                    $response->nbtodolate++;
4951
                    $response->url_late = DOL_URL_ROOT . '/compta/facture/list.php?search_option=late&mainmenu=billing&leftmenu=customers_bills';
4952
                }
4953
            }
4954
4955
            $this->db->free($resql);
4956
            return $response;
4957
        } else {
4958
            dol_print_error($this->db);
4959
            $this->error = $this->db->error();
4960
            return -1;
4961
        }
4962
    }
4963
4964
4965
    /* gestion des contacts d'une facture */
4966
4967
    /**
4968
     *  Retourne id des contacts clients de facturation
4969
     *
4970
     *  @return     array       Liste des id contacts facturation
4971
     */
4972
    public function getIdBillingContact()
4973
    {
4974
        return $this->getIdContact('external', 'BILLING');
4975
    }
4976
4977
    /**
4978
     *  Retourne id des contacts clients de livraison
4979
     *
4980
     *  @return     array       Liste des id contacts livraison
4981
     */
4982
    public function getIdShippingContact()
4983
    {
4984
        return $this->getIdContact('external', 'SHIPPING');
4985
    }
4986
4987
4988
    /**
4989
     *  Initialise an instance with random values.
4990
     *  Used to build previews or test instances.
4991
     *  id must be 0 if object instance is a specimen.
4992
     *
4993
     *  @param  string      $option     ''=Create a specimen invoice with lines, 'nolines'=No lines
4994
     *  @return int
4995
     */
4996
    public function initAsSpecimen($option = '')
4997
    {
4998
        global $conf, $langs, $user;
4999
5000
        $now = dol_now();
5001
        $arraynow = dol_getdate($now);
5002
        $nownotime = dol_mktime(0, 0, 0, $arraynow['mon'], $arraynow['mday'], $arraynow['year']);
5003
5004
        // Load array of products prodids
5005
        $num_prods = 0;
5006
        $prodids = array();
5007
        $sql = "SELECT rowid";
5008
        $sql .= " FROM " . MAIN_DB_PREFIX . "product";
5009
        $sql .= " WHERE entity IN (" . getEntity('product') . ")";
5010
        $sql .= $this->db->plimit(100);
5011
5012
        $resql = $this->db->query($sql);
5013
        if ($resql) {
5014
            $num_prods = $this->db->num_rows($resql);
5015
            $i = 0;
5016
            while ($i < $num_prods) {
5017
                $i++;
5018
                $row = $this->db->fetch_row($resql);
5019
                $prodids[$i] = $row[0];
5020
            }
5021
        }
5022
        //Avoid php warning Warning: mt_rand(): max(0) is smaller than min(1) when no product exists
5023
        if (empty($num_prods)) {
5024
            $num_prods = 1;
5025
        }
5026
5027
        // Initialize parameters
5028
        $this->id = 0;
5029
        $this->entity = 1;
5030
        $this->ref = 'SPECIMEN';
5031
        $this->specimen = 1;
5032
        $this->socid = 1;
5033
        $this->date = $nownotime;
0 ignored issues
show
Documentation Bug introduced by
It seems like $nownotime can also be of type string. However, the property $date is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
5034
        $this->date_lim_reglement = $nownotime + 3600 * 24 * 30;
5035
        $this->cond_reglement_id   = 1;
5036
        $this->cond_reglement_code = 'RECEP';
5037
        $this->date_lim_reglement = $this->calculate_date_lim_reglement();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->calculate_date_lim_reglement() can also be of type string. However, the property $date_lim_reglement is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
5038
        $this->mode_reglement_id   = 0; // Not forced to show payment mode CHQ + VIR
5039
        $this->mode_reglement_code = ''; // Not forced to show payment mode CHQ + VIR
5040
5041
        $this->note_public = 'This is a comment (public)';
5042
        $this->note_private = 'This is a comment (private)';
5043
        $this->note = 'This is a comment (private)';
5044
5045
        $this->fk_user_author = $user->id;
0 ignored issues
show
Deprecated Code introduced by
The property DoliModules\Billing\Model\Invoice::$fk_user_author has been deprecated: Use $user_creation_id ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

5045
        /** @scrutinizer ignore-deprecated */ $this->fk_user_author = $user->id;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
5046
5047
        $this->multicurrency_tx = 1;
5048
        $this->multicurrency_code = $conf->currency;
5049
5050
        $this->fk_incoterms = 0;
5051
        $this->location_incoterms = '';
5052
5053
        if (empty($option) || $option != 'nolines') {
5054
            // Lines
5055
            $nbp = 5;
5056
            $xnbp = 0;
5057
            while ($xnbp < $nbp) {
5058
                $line = new FactureLigne($this->db);
0 ignored issues
show
Deprecated Code introduced by
The class DoliModules\Billing\Model\FactureLigne has been deprecated: Use Invoice instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

5058
                $line = /** @scrutinizer ignore-deprecated */ new FactureLigne($this->db);
Loading history...
5059
                $line->desc = $langs->trans("Description") . " " . $xnbp;
5060
                $line->qty = 1;
5061
                $line->subprice = 100;
5062
                $line->tva_tx = 19.6;
5063
                $line->localtax1_tx = 0;
5064
                $line->localtax2_tx = 0;
5065
                $line->remise_percent = 0;
5066
                if ($xnbp == 1) {        // Qty is negative (product line)
5067
                    $prodid = mt_rand(1, $num_prods);
5068
                    $line->fk_product = $prodids[$prodid];
5069
                    $line->qty = -1;
5070
                    $line->total_ht = -100;
5071
                    $line->total_ttc = -119.6;
5072
                    $line->total_tva = -19.6;
5073
                    $line->multicurrency_total_ht = -200;
5074
                    $line->multicurrency_total_ttc = -239.2;
5075
                    $line->multicurrency_total_tva = -39.2;
5076
                } elseif ($xnbp == 2) {    // UP is negative (free line)
5077
                    $line->subprice = -100;
5078
                    $line->total_ht = -100;
5079
                    $line->total_ttc = -119.6;
5080
                    $line->total_tva = -19.6;
5081
                    $line->remise_percent = 0;
5082
                    $line->multicurrency_total_ht = -200;
5083
                    $line->multicurrency_total_ttc = -239.2;
5084
                    $line->multicurrency_total_tva = -39.2;
5085
                } elseif ($xnbp == 3) {    // Discount is 50% (product line)
5086
                    $prodid = mt_rand(1, $num_prods);
5087
                    $line->fk_product = $prodids[$prodid];
5088
                    $line->total_ht = 50;
5089
                    $line->total_ttc = 59.8;
5090
                    $line->total_tva = 9.8;
5091
                    $line->multicurrency_total_ht = 100;
5092
                    $line->multicurrency_total_ttc = 119.6;
5093
                    $line->multicurrency_total_tva = 19.6;
5094
                    $line->remise_percent = 50;
5095
                } else { // (product line)
5096
                    $prodid = mt_rand(1, $num_prods);
5097
                    $line->fk_product = $prodids[$prodid];
5098
                    $line->total_ht = 100;
5099
                    $line->total_ttc = 119.6;
5100
                    $line->total_tva = 19.6;
5101
                    $line->multicurrency_total_ht = 200;
5102
                    $line->multicurrency_total_ttc = 239.2;
5103
                    $line->multicurrency_total_tva = 39.2;
5104
                    $line->remise_percent = 0;
5105
                }
5106
5107
                $this->lines[$xnbp] = $line;
5108
5109
5110
                $this->total_ht       += $line->total_ht;
5111
                $this->total_tva      += $line->total_tva;
5112
                $this->total_ttc      += $line->total_ttc;
5113
5114
                $this->multicurrency_total_ht       += $line->multicurrency_total_ht;
5115
                $this->multicurrency_total_tva      += $line->multicurrency_total_tva;
5116
                $this->multicurrency_total_ttc      += $line->multicurrency_total_ttc;
5117
5118
                $xnbp++;
5119
            }
5120
            $this->revenuestamp = 0;
5121
5122
            // Add a line "offered"
5123
            $line = new FactureLigne($this->db);
0 ignored issues
show
Deprecated Code introduced by
The class DoliModules\Billing\Model\FactureLigne has been deprecated: Use Invoice instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

5123
            $line = /** @scrutinizer ignore-deprecated */ new FactureLigne($this->db);
Loading history...
5124
            $line->desc = $langs->trans("Description") . " (offered line)";
5125
            $line->qty = 1;
5126
            $line->subprice = 100;
5127
            $line->tva_tx = 19.6;
5128
            $line->localtax1_tx = 0;
5129
            $line->localtax2_tx = 0;
5130
            $line->remise_percent = 100;
5131
            $line->total_ht = 0;
5132
            $line->total_ttc = 0; // 90 * 1.196
5133
            $line->total_tva = 0;
5134
            $line->multicurrency_total_ht = 0;
5135
            $line->multicurrency_total_ttc = 0;
5136
            $line->multicurrency_total_tva = 0;
5137
            $prodid = mt_rand(1, $num_prods);
5138
            $line->fk_product = $prodids[$prodid];
5139
5140
            $this->lines[$xnbp] = $line;
5141
            $xnbp++;
5142
        }
5143
5144
        return 1;
5145
    }
5146
5147
    /**
5148
     *      Load indicators for dashboard (this->nbtodo and this->nbtodolate)
5149
     *
5150
     *      @return         int     Return integer <0 if KO, >0 if OK
5151
     */
5152
    public function loadStateBoard()
5153
    {
5154
        global $conf, $user;
5155
5156
        $this->nb = array();
5157
5158
        $clause = "WHERE";
5159
5160
        $sql = "SELECT count(f.rowid) as nb";
5161
        $sql .= " FROM " . MAIN_DB_PREFIX . "facture as f";
5162
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid";
5163
        if (!$user->hasRight('societe', 'client', 'voir')) {
5164
            $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "societe_commerciaux as sc ON s.rowid = sc.fk_soc";
5165
            $sql .= " WHERE sc.fk_user = " . ((int) $user->id);
5166
            $clause = "AND";
5167
        }
5168
        $sql .= " " . $clause . " f.entity IN (" . getEntity('invoice') . ")";
5169
5170
        $resql = $this->db->query($sql);
5171
        if ($resql) {
5172
            while ($obj = $this->db->fetch_object($resql)) {
5173
                $this->nb["invoices"] = $obj->nb;
5174
            }
5175
            $this->db->free($resql);
5176
            return 1;
5177
        } else {
5178
            dol_print_error($this->db);
5179
            $this->error = $this->db->error();
5180
            return -1;
5181
        }
5182
    }
5183
5184
    /**
5185
     *  Create an array of invoice lines
5186
     *
5187
     *  @return int     >0 if OK, <0 if KO
5188
     */
5189
    public function getLinesArray()
5190
    {
5191
        return $this->fetch_lines();
5192
    }
5193
5194
    /**
5195
     *  Create a document onto disk according to template module.
5196
     *
5197
     *  @param  string      $modele         Generator to use. Caller must set it to obj->model_pdf or GETPOST('model','alpha') for example.
5198
     *  @param  Translate   $outputlangs    Object lang to use for translation
5199
     *  @param  int         $hidedetails    Hide details of lines
5200
     *  @param  int         $hidedesc       Hide description
5201
     *  @param  int         $hideref        Hide ref
5202
     *  @param  null|array  $moreparams     Array to provide more information
5203
     *  @return int                         Return integer <0 if KO, >0 if OK
5204
     */
5205
    public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
5206
    {
5207
        global $conf, $langs;
5208
5209
        $outputlangs->loadLangs(array("bills", "products"));
5210
5211
        if (!dol_strlen($modele)) {
5212
            $modele = 'crabe';
5213
            $thisTypeConfName = 'FACTURE_ADDON_PDF_' . $this->type;
5214
5215
            if (!empty($this->model_pdf)) {
5216
                $modele = $this->model_pdf;
5217
            } elseif (getDolGlobalString($thisTypeConfName)) {
5218
                $modele = getDolGlobalString($thisTypeConfName);
5219
            } elseif (getDolGlobalString('FACTURE_ADDON_PDF')) {
5220
                $modele = getDolGlobalString('FACTURE_ADDON_PDF');
5221
            }
5222
        }
5223
5224
        $modelpath = "core/modules/facture/doc/";
5225
5226
        return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
5227
    }
5228
5229
    /**
5230
     * Gets the smallest reference available for a new cycle
5231
     *
5232
     * @return int >= 1 if OK, -1 if error
5233
     */
5234
    public function newCycle()
5235
    {
5236
        $sql = 'SELECT max(situation_cycle_ref) FROM ' . MAIN_DB_PREFIX . 'facture as f';
5237
        $sql .= " WHERE f.entity IN (" . getEntity('invoice', 0) . ")";
5238
        $resql = $this->db->query($sql);
5239
        if ($resql) {
5240
            if ($this->db->num_rows($resql) > 0) {
5241
                $res = $this->db->fetch_array($resql);
5242
                $ref = $res['max(situation_cycle_ref)'];
5243
                $ref++;
5244
            } else {
5245
                $ref = 1;
5246
            }
5247
            $this->db->free($resql);
5248
            return $ref;
5249
        } else {
5250
            $this->error = $this->db->lasterror();
5251
            dol_syslog("Error sql=" . $sql . ", error=" . $this->error, LOG_ERR);
5252
            return -1;
5253
        }
5254
    }
5255
5256
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5257
    /**
5258
     * Checks if the invoice is the first of a cycle
5259
     *
5260
     * @return boolean
5261
     */
5262
    public function is_first()
5263
    {
5264
        // phpcs:enable
5265
        return ($this->situation_counter == 1);
5266
    }
5267
5268
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5269
    /**
5270
     * Returns an array containing the previous situations as Facture objects
5271
     *
5272
     * @return mixed -1 if error, array of previous situations
5273
     */
5274
    public function get_prev_sits()
5275
    {
5276
        // phpcs:enable
5277
        global $conf;
5278
5279
        $sql = 'SELECT rowid FROM ' . MAIN_DB_PREFIX . 'facture';
5280
        $sql .= ' WHERE situation_cycle_ref = ' . ((int) $this->situation_cycle_ref);
5281
        $sql .= ' AND situation_counter < ' . ((int) $this->situation_counter);
5282
        $sql .= ' AND entity = ' . ($this->entity > 0 ? $this->entity : $conf->entity);
5283
        $resql = $this->db->query($sql);
5284
        $res = array();
5285
        if ($resql && $this->db->num_rows($resql) > 0) {
5286
            while ($row = $this->db->fetch_object($resql)) {
5287
                $id = $row->rowid;
5288
                $situation = new Facture($this->db);
5289
                $situation->fetch($id);
5290
                $res[] = $situation;
5291
            }
5292
        } else {
5293
            $this->error = $this->db->error();
5294
            dol_syslog("Error sql=" . $sql . ", error=" . $this->error, LOG_ERR);
5295
            return -1;
5296
        }
5297
5298
        return $res;
5299
    }
5300
5301
    /**
5302
     * Sets the invoice as a final situation
5303
     *
5304
     *  @param      User    $user       Object user
5305
     *  @param      int     $notrigger  1=Does not execute triggers, 0= execute triggers
5306
     *  @return     int                 Return integer <0 if KO, >0 if OK
5307
     */
5308
    public function setFinal(User $user, $notrigger = 0)
5309
    {
5310
        $error = 0;
5311
5312
        $this->db->begin();
5313
5314
        $sql = 'UPDATE ' . MAIN_DB_PREFIX . 'facture SET situation_final = ' . ((int) $this->situation_final) . ' WHERE rowid = ' . ((int) $this->id);
5315
5316
        dol_syslog(__METHOD__, LOG_DEBUG);
5317
        $resql = $this->db->query($sql);
5318
        if (!$resql) {
5319
            $this->errors[] = $this->db->error();
5320
            $error++;
5321
        }
5322
5323
        if (!$notrigger && empty($error)) {
5324
            // Call trigger
5325
            $result = $this->call_trigger('BILL_MODIFY', $user);
5326
            if ($result < 0) {
5327
                $error++;
5328
            }
5329
            // End call triggers
5330
        }
5331
5332
        if (!$error) {
5333
            $this->db->commit();
5334
            return 1;
5335
        } else {
5336
            foreach ($this->errors as $errmsg) {
5337
                dol_syslog(__METHOD__ . ' Error: ' . $errmsg, LOG_ERR);
5338
                $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
5339
            }
5340
            $this->db->rollback();
5341
            return -1 * $error;
5342
        }
5343
    }
5344
5345
    // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5346
    /**
5347
     * Checks if the invoice is the last in its cycle
5348
     *
5349
     * @return bool Last of the cycle status
5350
     */
5351
    public function is_last_in_cycle()
5352
    {
5353
        // phpcs:enable
5354
        global $conf;
5355
5356
        if (!empty($this->situation_cycle_ref)) {
5357
            // No point in testing anything if we're not inside a cycle
5358
            $sql = 'SELECT max(situation_counter) FROM ' . MAIN_DB_PREFIX . 'facture';
5359
            $sql .= ' WHERE situation_cycle_ref = ' . ((int) $this->situation_cycle_ref);
5360
            $sql .= ' AND entity = ' . ($this->entity > 0 ? $this->entity : $conf->entity);
5361
            $resql = $this->db->query($sql);
5362
5363
            if ($resql && $this->db->num_rows($resql) > 0) {
5364
                $res = $this->db->fetch_array($resql);
5365
                $last = $res['max(situation_counter)'];
5366
                return ($last == $this->situation_counter);
5367
            } else {
5368
                $this->error = $this->db->lasterror();
5369
                dol_syslog(get_class($this) . "::select Error " . $this->error, LOG_ERR);
5370
                return false;
5371
            }
5372
        } else {
5373
            return true;
5374
        }
5375
    }
5376
5377
    /**
5378
     * Replace a thirdparty id with another one.
5379
     *
5380
     * @param   DoliDB  $dbs        Database handler, because function is static we name it $dbs not $db to avoid breaking coding test
5381
     * @param   int     $origin_id  Old thirdparty id
5382
     * @param   int     $dest_id    New thirdparty id
5383
     * @return  bool
5384
     */
5385
    public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
5386
    {
5387
        $tables = array(
5388
            'facture'
5389
        );
5390
5391
        return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
5392
    }
5393
5394
    /**
5395
     * Replace a product id with another one.
5396
     *
5397
     * @param DoliDB $db Database handler
5398
     * @param int $origin_id Old product id
5399
     * @param int $dest_id New product id
5400
     * @return bool
5401
     */
5402
    public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
5403
    {
5404
        $tables = array(
5405
            'facturedet'
5406
        );
5407
5408
        return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
5409
    }
5410
5411
    /**
5412
     * Is the customer invoice delayed?
5413
     *
5414
     * @return bool
5415
     */
5416
    public function hasDelay()
5417
    {
5418
        global $conf;
5419
5420
        $now = dol_now();
5421
5422
        // Paid invoices have status STATUS_CLOSED
5423
        if ($this->status != Facture::STATUS_VALIDATED) {
5424
            return false;
5425
        }
5426
5427
        $hasDelay = $this->date_lim_reglement < ($now - $conf->facture->client->warning_delay);
5428
        if ($hasDelay && !empty($this->retained_warranty) && !empty($this->retained_warranty_date_limit)) {
5429
            $totalpaid = $this->getSommePaiement();
5430
            $totalpaid = (float) $totalpaid;
5431
            $RetainedWarrantyAmount = $this->getRetainedWarrantyAmount();
5432
            if ($totalpaid >= 0 && $RetainedWarrantyAmount >= 0) {
5433
                if (($totalpaid < $this->total_ttc - $RetainedWarrantyAmount) && $this->date_lim_reglement < ($now - $conf->facture->client->warning_delay)) {
5434
                    $hasDelay = 1;
5435
                } elseif ($totalpaid < $this->total_ttc && $this->retained_warranty_date_limit < ($now - $conf->facture->client->warning_delay)) {
5436
                    $hasDelay = 1;
5437
                } else {
5438
                    $hasDelay = 0;
5439
                }
5440
            }
5441
        }
5442
5443
        return $hasDelay;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $hasDelay also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
5444
    }
5445
5446
    /**
5447
     * Currently used for documents generation : to know if retained warranty need to be displayed
5448
     * @return bool
5449
     */
5450
    public function displayRetainedWarranty()
5451
    {
5452
        global $conf;
5453
5454
        // TODO : add a flag on invoices to store this conf : INVOICE_RETAINED_WARRANTY_LIMITED_TO_FINAL_SITUATION
5455
5456
        // 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
5457
5458
        $displayWarranty = false;
5459
        if (!empty($this->retained_warranty)) {
5460
            $displayWarranty = true;
5461
5462
            if ($this->type == Facture::TYPE_SITUATION && getDolGlobalString('INVOICE_RETAINED_WARRANTY_LIMITED_TO_FINAL_SITUATION')) {
5463
                // Check if this situation invoice is 100% for real
5464
                $displayWarranty = false;
5465
                if (!empty($this->situation_final)) {
5466
                    $displayWarranty = true;
5467
                } elseif (!empty($this->lines) && $this->status == Facture::STATUS_DRAFT) {
5468
                    // $object->situation_final need validation to be done so this test is need for draft
5469
                    $displayWarranty = true;
5470
5471
                    foreach ($this->lines as $i => $line) {
5472
                        if ($line->product_type < 2 && $line->situation_percent < 100) {
5473
                            $displayWarranty = false;
5474
                            break;
5475
                        }
5476
                    }
5477
                }
5478
            }
5479
        }
5480
5481
        return $displayWarranty;
5482
    }
5483
5484
    /**
5485
     * @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)
5486
     * @return float or -1 if not available
5487
     */
5488
    public function getRetainedWarrantyAmount($rounding = -1)
5489
    {
5490
        global $conf;
5491
        if (empty($this->retained_warranty)) {
5492
            return -1;
5493
        }
5494
5495
        $retainedWarrantyAmount = 0;
5496
5497
        // Billed - retained warranty
5498
        if ($this->type == Facture::TYPE_SITUATION && getDolGlobalString('INVOICE_RETAINED_WARRANTY_LIMITED_TO_FINAL_SITUATION')) {
5499
            $displayWarranty = true;
5500
            // Check if this situation invoice is 100% for real
5501
            if (!empty($this->lines)) {
5502
                foreach ($this->lines as $i => $line) {
5503
                    if ($line->product_type < 2 && $line->situation_percent < 100) {
5504
                        $displayWarranty = false;
5505
                        break;
5506
                    }
5507
                }
5508
            }
5509
5510
            if ($displayWarranty && !empty($this->situation_final)) {
5511
                $this->fetchPreviousNextSituationInvoice();
5512
                $TPreviousIncoice = $this->tab_previous_situation_invoice;
5513
5514
                $total2BillWT = 0;
5515
                foreach ($TPreviousIncoice as &$fac) {
5516
                    $total2BillWT += $fac->total_ttc;
5517
                }
5518
                $total2BillWT += $this->total_ttc;
5519
5520
                $retainedWarrantyAmount = $total2BillWT * $this->retained_warranty / 100;
5521
            } else {
5522
                return -1;
5523
            }
5524
        } else {
5525
            // Because one day retained warranty could be used on standard invoices
5526
            $retainedWarrantyAmount = $this->total_ttc * $this->retained_warranty / 100;
5527
        }
5528
5529
        if ($rounding < 0) {
5530
            $rounding = min(getDolGlobalString('MAIN_MAX_DECIMALS_UNIT'), getDolGlobalString('MAIN_MAX_DECIMALS_TOT'));
5531
        }
5532
5533
        if ($rounding > 0) {
5534
            return round($retainedWarrantyAmount, $rounding);
5535
        }
5536
5537
        return $retainedWarrantyAmount;
5538
    }
5539
5540
    /**
5541
     *  Change the retained warranty
5542
     *
5543
     *  @param      float       $value      value of retained warranty
5544
     *  @return     int             >0 if OK, <0 if KO
5545
     */
5546
    public function setRetainedWarranty($value)
5547
    {
5548
        dol_syslog(get_class($this) . '::setRetainedWarranty(' . $value . ')');
5549
5550
        if ($this->status >= 0) {
5551
            $fieldname = 'retained_warranty';
5552
            $sql = 'UPDATE ' . MAIN_DB_PREFIX . $this->table_element;
5553
            $sql .= " SET " . $fieldname . " = " . ((float) $value);
5554
            $sql .= ' WHERE rowid=' . ((int) $this->id);
5555
5556
            if ($this->db->query($sql)) {
5557
                $this->retained_warranty = (float) $value;
5558
                return 1;
5559
            } else {
5560
                dol_syslog(get_class($this) . '::setRetainedWarranty Erreur ' . $sql . ' - ' . $this->db->error());
5561
                $this->error = $this->db->error();
5562
                return -1;
5563
            }
5564
        } else {
5565
            dol_syslog(get_class($this) . '::setRetainedWarranty, status of the object is incompatible');
5566
            $this->error = 'Status of the object is incompatible ' . $this->status;
5567
            return -2;
5568
        }
5569
    }
5570
5571
5572
    /**
5573
     *  Change the retained_warranty_date_limit
5574
     *
5575
     *  @param      int     $timestamp      date limit of retained warranty in timestamp format
5576
     *  @param      string  $dateYmd        date limit of retained warranty in Y m d format
5577
     *  @return     int             >0 if OK, <0 if KO
5578
     */
5579
    public function setRetainedWarrantyDateLimit($timestamp, $dateYmd = '')
5580
    {
5581
        if (!$timestamp && $dateYmd) {
5582
            $timestamp = $this->db->jdate($dateYmd);
5583
        }
5584
5585
5586
        dol_syslog(get_class($this) . '::setRetainedWarrantyDateLimit(' . $timestamp . ')');
5587
        if ($this->status >= 0) {
5588
            $fieldname = 'retained_warranty_date_limit';
5589
            $sql = 'UPDATE ' . MAIN_DB_PREFIX . $this->table_element;
5590
            $sql .= " SET " . $fieldname . " = " . (strval($timestamp) != '' ? "'" . $this->db->idate($timestamp) . "'" : 'null');
5591
            $sql .= ' WHERE rowid = ' . ((int) $this->id);
5592
5593
            if ($this->db->query($sql)) {
5594
                $this->retained_warranty_date_limit = $timestamp;
0 ignored issues
show
Documentation Bug introduced by
It seems like $timestamp can also be of type string. However, the property $retained_warranty_date_limit is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
5595
                return 1;
5596
            } else {
5597
                dol_syslog(get_class($this) . '::setRetainedWarrantyDateLimit Erreur ' . $sql . ' - ' . $this->db->error());
5598
                $this->error = $this->db->error();
5599
                return -1;
5600
            }
5601
        } else {
5602
            dol_syslog(get_class($this) . '::setRetainedWarrantyDateLimit, status of the object is incompatible');
5603
            $this->error = 'Status of the object is incompatible ' . $this->status;
5604
            return -2;
5605
        }
5606
    }
5607
5608
5609
    /**
5610
     *  Send reminders by emails for invoices validated that are due.
5611
     *  CAN BE A CRON TASK
5612
     *
5613
     *  @param  int         $nbdays             Delay before due date (or after if delay is negative)
5614
     *  @param  string      $paymentmode        '' or 'all' by default (no filter), or 'LIQ', 'CHQ', CB', ...
5615
     *  @param  int|string  $template           Name (or id) of email template (Must be a template of type 'facture_send')
5616
     *  @param  string      $datetouse          'duedate' (default) or 'invoicedate'
5617
     *  @param  string      $forcerecipient     Force email of recipient (for example to send the email to an accountant supervisor instead of the customer)
5618
     *  @return int                             0 if OK, <>0 if KO (this function is used also by cron so only 0 is OK)
5619
     */
5620
    public function sendEmailsRemindersOnInvoiceDueDate($nbdays = 0, $paymentmode = 'all', $template = '', $datetouse = 'duedate', $forcerecipient = '')
5621
    {
5622
        global $conf, $langs, $user;
5623
5624
        $error = 0;
5625
        $this->output = '';
5626
        $this->error = '';
5627
        $nbMailSend = 0;
5628
        $errorsMsg = array();
5629
5630
        $langs->load("bills");
5631
5632
        if (!isModEnabled('invoice')) { // Should not happen. If module disabled, cron job should not be visible.
5633
            $this->output .= $langs->trans('ModuleNotEnabled', $langs->transnoentitiesnoconv("Facture"));
5634
            return 0;
5635
        }
5636
        if (!in_array($datetouse, array('duedate', 'invoicedate'))) {
5637
            $this->output .= 'Bad value for parameter datetouse. Must be "duedate" or "invoicedate"';
5638
            return 0;
5639
        }
5640
        /*if (empty($conf->global->FACTURE_REMINDER_EMAIL)) {
5641
            $langs->load("bills");
5642
            $this->output .= $langs->trans('EventRemindersByEmailNotEnabled', $langs->transnoentitiesnoconv("Facture"));
5643
            return 0;
5644
        }
5645
        */
5646
5647
        require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php';
5648
        require_once DOL_DOCUMENT_ROOT . '/core/class/html.formmail.class.php';
5649
        require_once DOL_DOCUMENT_ROOT . '/core/class/CMailFile.class.php';
5650
        $formmail = new FormMail($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\FormMail was not found. Did you mean FormMail? If so, make sure to prefix the type with \.
Loading history...
5651
5652
        $now = dol_now();
5653
        $tmpidate = dol_get_first_hour(dol_time_plus_duree($now, $nbdays, 'd'), 'gmt');
5654
5655
        $tmpinvoice = new Facture($this->db);
5656
5657
        dol_syslog(__METHOD__ . " start", LOG_INFO);
5658
5659
        // Select all action comm reminder
5660
        $sql = "SELECT rowid as id FROM " . MAIN_DB_PREFIX . "facture as f";
5661
        if (!empty($paymentmode) && $paymentmode != 'all') {
5662
            $sql .= ", " . MAIN_DB_PREFIX . "c_paiement as cp";
5663
        }
5664
        $sql .= " WHERE f.paye = 0";    // Only unpaid
5665
        $sql .= " AND f.fk_statut = " . self::STATUS_VALIDATED;   // Only validated status
5666
        if ($datetouse == 'invoicedate') {
5667
            $sql .= " AND f.datef = '" . $this->db->idate($tmpidate, 'gmt') . "'";
5668
        } else {
5669
            $sql .= " AND f.date_lim_reglement = '" . $this->db->idate($tmpidate, 'gmt') . "'";
5670
        }
5671
        $sql .= " AND f.entity IN (" . getEntity('facture', 0) . ")";   // One batch process only one company (no sharing)
5672
        if (!empty($paymentmode) && $paymentmode != 'all') {
5673
            $sql .= " AND f.fk_mode_reglement = cp.id AND cp.code = '" . $this->db->escape($paymentmode) . "'";
5674
        }
5675
        // TODO Add a filter to check there is no payment started yet
5676
        if ($datetouse == 'invoicedate') {
5677
            $sql .= $this->db->order("datef", "ASC");
5678
        } else {
5679
            $sql .= $this->db->order("date_lim_reglement", "ASC");
5680
        }
5681
5682
        $resql = $this->db->query($sql);
5683
5684
        $stmpidate = dol_print_date($tmpidate, 'day', 'gmt');
5685
        if ($datetouse == 'invoicedate') {
5686
            $this->output .= $langs->transnoentitiesnoconv("SearchValidatedInvoicesWithDate", $stmpidate);
5687
        } else {
5688
            $this->output .= $langs->transnoentitiesnoconv("SearchUnpaidInvoicesWithDueDate", $stmpidate);
5689
        }
5690
        if (!empty($paymentmode) && $paymentmode != 'all') {
5691
            $this->output .= ' (' . $langs->transnoentitiesnoconv("PaymentMode") . ' ' . $paymentmode . ')';
5692
        }
5693
        $this->output .= '<br>';
5694
5695
        if ($resql) {
5696
            while ($obj = $this->db->fetch_object($resql)) {
5697
                if (!$error) {
5698
                    // Load event
5699
                    $res = $tmpinvoice->fetch($obj->id);
5700
                    if ($res > 0) {
5701
                        $tmpinvoice->fetch_thirdparty();
5702
5703
                        $outputlangs = new Translate('', $conf);
5704
                        if ($tmpinvoice->thirdparty->default_lang) {
5705
                            $outputlangs->setDefaultLang($tmpinvoice->thirdparty->default_lang);
5706
                            $outputlangs->loadLangs(array("main", "bills"));
5707
                        } else {
5708
                            $outputlangs = $langs;
5709
                        }
5710
5711
                        // Select email template according to language of recipient
5712
                        $arraymessage = $formmail->getEMailTemplate($this->db, 'facture_send', $user, $outputlangs, (is_numeric($template) ? $template : 0), 1, (is_numeric($template) ? '' : $template));
5713
                        if (is_numeric($arraymessage) && $arraymessage <= 0) {
5714
                            $langs->load("errors");
5715
                            $this->output .= $langs->trans('ErrorFailedToFindEmailTemplate', $template);
5716
                            return 0;
5717
                        }
5718
5719
                        // PREPARE EMAIL
5720
                        $errormesg = '';
5721
5722
                        // Make substitution in email content
5723
                        $substitutionarray = getCommonSubstitutionArray($outputlangs, 0, '', $tmpinvoice);
5724
5725
                        complete_substitutions_array($substitutionarray, $outputlangs, $tmpinvoice);
5726
5727
                        // Topic
5728
                        $sendTopic = make_substitutions(empty($arraymessage->topic) ? $outputlangs->transnoentitiesnoconv('InformationMessage') : $arraymessage->topic, $substitutionarray, $outputlangs, 1);
5729
5730
                        // Content
5731
                        $content = $outputlangs->transnoentitiesnoconv($arraymessage->content);
5732
5733
                        $sendContent = make_substitutions($content, $substitutionarray, $outputlangs, 1);
5734
5735
                        // Recipient
5736
                        $to = array();
5737
                        if ($forcerecipient) {  // If a recipient was forced
5738
                            $to = array($forcerecipient);
5739
                        } else {
5740
                            $res = $tmpinvoice->fetch_thirdparty();
5741
                            $recipient = $tmpinvoice->thirdparty;
5742
                            if ($res > 0) {
5743
                                $tmparraycontact = $tmpinvoice->liste_contact(-1, 'external', 0, 'BILLING');
5744
                                if (is_array($tmparraycontact) && count($tmparraycontact) > 0) {
5745
                                    foreach ($tmparraycontact as $data_email) {
5746
                                        if (!empty($data_email['email'])) {
5747
                                            $to[] = $tmpinvoice->thirdparty->contact_get_property($data_email['id'], 'email');
0 ignored issues
show
Bug introduced by
The method contact_get_property() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

5747
                                            /** @scrutinizer ignore-call */ 
5748
                                            $to[] = $tmpinvoice->thirdparty->contact_get_property($data_email['id'], 'email');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
5748
                                        }
5749
                                    }
5750
                                }
5751
                                if (empty($to) && !empty($recipient->email)) {
5752
                                    $to[] = $recipient->email;
5753
                                }
5754
                                if (empty($to)) {
5755
                                    $errormesg = "Failed to send remind to thirdparty id=" . $tmpinvoice->socid . ". No email defined for invoice or customer.";
5756
                                    $error++;
5757
                                }
5758
                            } else {
5759
                                $errormesg = "Failed to load recipient with thirdparty id=" . $tmpinvoice->socid;
5760
                                $error++;
5761
                            }
5762
                        }
5763
5764
                        // Sender
5765
                        $from = getDolGlobalString('MAIN_MAIL_EMAIL_FROM');
5766
                        if (!empty($arraymessage->email_from)) {    // If a sender is defined into template, we use it in priority
5767
                            $from = $arraymessage->email_from;
5768
                        }
5769
                        if (empty($from)) {
5770
                            $errormesg = "Failed to get sender into global setup MAIN_MAIL_EMAIL_FROM";
5771
                            $error++;
5772
                        }
5773
5774
                        if (!$error && !empty($to)) {
5775
                            $this->db->begin();
5776
5777
                            $to = implode(',', $to);
5778
                            if (!empty($arraymessage->email_to)) {  // If a recipient is defined into template, we add it
5779
                                $to = $to . ',' . $arraymessage->email_to;
5780
                            }
5781
5782
                            // Errors Recipient
5783
                            $errors_to = getDolGlobalString('MAIN_MAIL_ERRORS_TO');
5784
5785
                            $trackid = 'inv' . $tmpinvoice->id;
5786
                            $sendcontext = 'standard';
5787
5788
                            $email_tocc = '';
5789
                            if (!empty($arraymessage->email_tocc)) {    // If a CC is defined into template, we use it
5790
                                $email_tocc = $arraymessage->email_tocc;
5791
                            }
5792
5793
                            $email_tobcc = '';
5794
                            if (!empty($arraymessage->email_tobcc)) {   // If a BCC is defined into template, we use it
5795
                                $email_tobcc = $arraymessage->email_tobcc;
5796
                            }
5797
5798
                            //join file is asked
5799
                            $joinFile = [];
5800
                            $joinFileName = [];
5801
                            $joinFileMime = [];
5802
                            if ($arraymessage->joinfiles == 1 && !empty($tmpinvoice->last_main_doc)) {
5803
                                $joinFile[] = DOL_DATA_ROOT . '/' . $tmpinvoice->last_main_doc;
5804
                                $joinFileName[] = basename($tmpinvoice->last_main_doc);
5805
                                $joinFileMime[] = dol_mimetype(DOL_DATA_ROOT . '/' . $tmpinvoice->last_main_doc);
5806
                            }
5807
5808
                            // Mail Creation
5809
                            $cMailFile = new CMailFile($sendTopic, $to, $from, $sendContent, $joinFile, $joinFileMime, $joinFileName, $email_tocc, $email_tobcc, 0, 1, $errors_to, '', $trackid, '', $sendcontext, '');
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\CMailFile was not found. Did you mean CMailFile? If so, make sure to prefix the type with \.
Loading history...
5810
5811
                            // Sending Mail
5812
                            if ($cMailFile->sendfile()) {
5813
                                $nbMailSend++;
5814
5815
                                // Add a line into event table
5816
                                require_once DOL_DOCUMENT_ROOT . '/comm/action/class/actioncomm.class.php';
5817
5818
                                // Insert record of emails sent
5819
                                $actioncomm = new ActionComm($this->db);
0 ignored issues
show
Bug introduced by
The type DoliModules\Billing\Model\ActionComm was not found. Did you mean ActionComm? If so, make sure to prefix the type with \.
Loading history...
5820
5821
                                $actioncomm->type_code = 'AC_OTH_AUTO'; // Event insert into agenda automatically
5822
                                $actioncomm->socid = $tmpinvoice->thirdparty->id; // To link to a company
5823
                                $actioncomm->contact_id = 0;
5824
5825
                                $actioncomm->code = 'AC_EMAIL';
5826
                                $actioncomm->label = 'sendEmailsRemindersOnInvoiceDueDateOK (nbdays=' . $nbdays . ' paymentmode=' . $paymentmode . ' template=' . $template . ' datetouse=' . $datetouse . ' forcerecipient=' . $forcerecipient . ')';
5827
                                $actioncomm->note_private = $sendContent;
5828
                                $actioncomm->fk_project = $tmpinvoice->fk_project;
5829
                                $actioncomm->datep = dol_now();
5830
                                $actioncomm->datef = $actioncomm->datep;
5831
                                $actioncomm->percentage = -1; // Not applicable
5832
                                $actioncomm->authorid = $user->id; // User saving action
5833
                                $actioncomm->userownerid = $user->id; // Owner of action
5834
                                // Fields when action is an email (content should be added into note)
5835
                                $actioncomm->email_msgid = $cMailFile->msgid;
5836
                                $actioncomm->email_subject = $sendTopic;
5837
                                $actioncomm->email_from = $from;
5838
                                $actioncomm->email_sender = '';
5839
                                $actioncomm->email_to = $to;
5840
                                //$actioncomm->email_tocc = $sendtocc;
5841
                                //$actioncomm->email_tobcc = $sendtobcc;
5842
                                //$actioncomm->email_subject = $subject;
5843
                                $actioncomm->errors_to = $errors_to;
5844
5845
                                $actioncomm->elementtype = 'invoice';
5846
                                $actioncomm->fk_element = $tmpinvoice->id;
5847
5848
                                //$actioncomm->extraparams = $extraparams;
5849
5850
                                $actioncomm->create($user);
5851
                            } else {
5852
                                $errormesg = $cMailFile->error . ' : ' . $to;
5853
                                $error++;
5854
5855
                                // Add a line into event table
5856
                                require_once DOL_DOCUMENT_ROOT . '/comm/action/class/actioncomm.class.php';
5857
5858
                                // Insert record of emails sent
5859
                                $actioncomm = new ActionComm($this->db);
5860
5861
                                $actioncomm->type_code = 'AC_OTH_AUTO'; // Event insert into agenda automatically
5862
                                $actioncomm->socid = $tmpinvoice->thirdparty->id; // To link to a company
5863
                                $actioncomm->contact_id = 0;
5864
5865
                                $actioncomm->code = 'AC_EMAIL';
5866
                                $actioncomm->label = 'sendEmailsRemindersOnInvoiceDueDateKO';
5867
                                $actioncomm->note_private = $errormesg;
5868
                                $actioncomm->fk_project = $tmpinvoice->fk_project;
5869
                                $actioncomm->datep = dol_now();
5870
                                $actioncomm->datef = $actioncomm->datep;
5871
                                $actioncomm->percentage = -1; // Not applicable
5872
                                $actioncomm->authorid = $user->id; // User saving action
5873
                                $actioncomm->userownerid = $user->id; // Owner of action
5874
                                // Fields when action is an email (content should be added into note)
5875
                                $actioncomm->email_msgid = $cMailFile->msgid;
5876
                                $actioncomm->email_from = $from;
5877
                                $actioncomm->email_sender = '';
5878
                                $actioncomm->email_to = $to;
5879
                                //$actioncomm->email_tocc = $sendtocc;
5880
                                //$actioncomm->email_tobcc = $sendtobcc;
5881
                                //$actioncomm->email_subject = $subject;
5882
                                $actioncomm->errors_to = $errors_to;
5883
5884
                                //$actioncomm->extraparams = $extraparams;
5885
5886
                                $actioncomm->create($user);
5887
                            }
5888
5889
                            $this->db->commit();    // We always commit
5890
                        }
5891
5892
                        if ($errormesg) {
5893
                            $errorsMsg[] = $errormesg;
5894
                        }
5895
                    } else {
5896
                        $errorsMsg[] = 'Failed to fetch record invoice with ID = ' . $obj->id;
5897
                        $error++;
5898
                    }
5899
                }
5900
            }
5901
        } else {
5902
            $error++;
5903
        }
5904
5905
        if (!$error) {
5906
            $this->output .= 'Nb of emails sent : ' . $nbMailSend;
5907
5908
            dol_syslog(__METHOD__ . " end - " . $this->output, LOG_INFO);
5909
5910
            return 0;
5911
        } else {
5912
            $this->error = 'Nb of emails sent : ' . $nbMailSend . ', ' . (!empty($errorsMsg) ? implode(', ', $errorsMsg) : $error);
5913
5914
            dol_syslog(__METHOD__ . " end - " . $this->error, LOG_INFO);
5915
5916
            return $error;
5917
        }
5918
    }
5919
5920
    /**
5921
     * See if current invoice date is posterior to the last invoice date among validated invoices of same type.
5922
     *
5923
     * @param   boolean     $allow_validated_drafts         return true if the invoice has been validated before returning to DRAFT state.
5924
     * @return  array                                       return array
5925
     */
5926
    public function willBeLastOfSameType($allow_validated_drafts = false)
5927
    {
5928
        // get date of last validated invoices of same type
5929
        $sql  = "SELECT datef";
5930
        $sql .= " FROM " . MAIN_DB_PREFIX . "facture";
5931
        $sql .= " WHERE type = " . (int) $this->type ;
5932
        $sql .= " AND date_valid IS NOT NULL";
5933
        $sql .= " AND entity IN (" . getEntity('invoice') . ")";
5934
        $sql .= " ORDER BY datef DESC LIMIT 1";
5935
5936
        $result = $this->db->query($sql);
5937
        if ($result) {
5938
            // compare with current validation date
5939
            if ($this->db->num_rows($result)) {
5940
                $obj = $this->db->fetch_object($result);
5941
                $last_date = $this->db->jdate($obj->datef);
5942
                $invoice_date = $this->date;
5943
5944
                $is_last_of_same_type = $invoice_date >= $last_date;
5945
                if ($allow_validated_drafts) {
5946
                    $is_last_of_same_type = $is_last_of_same_type || (!strpos($this->ref, 'PROV') && $this->status == self::STATUS_DRAFT);
5947
                }
5948
5949
                return array($is_last_of_same_type, $last_date);
5950
            } else {
5951
                // element is first of type to be validated
5952
                return array(true);
5953
            }
5954
        } else {
5955
            dol_print_error($this->db);
5956
        }
5957
5958
        return array();
5959
    }
5960
5961
    /**
5962
     *  Return clicable link of object (with eventually picto)
5963
     *
5964
     *  @param      string      $option                 Where point the link (0=> main card, 1,2 => shipment, 'nolink'=>No link)
5965
     *  @param      array       $arraydata              Array of data
5966
     *  @return     string                              HTML Code for Kanban thumb.
5967
     */
5968
    public function getKanbanView($option = '', $arraydata = null)
5969
    {
5970
        global $langs;
5971
5972
        $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
5973
5974
        $return = '<div class="box-flex-item box-flex-grow-zero">';
5975
        $return .= '<div class="info-box info-box-sm">';
5976
        $return .= '<span class="info-box-icon bg-infobox-action">';
5977
        $return .= img_picto('', $this->picto);
5978
        $return .= '</span>';
5979
        $return .= '<div class="info-box-content">';
5980
        $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . (method_exists($this, 'getNomUrl') ? $this->getNomUrl(1) : $this->ref) . '</span>';
5981
        if ($selected >= 0) {
5982
            $return .= '<input id="cb' . $this->id . '" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="' . $this->id . '"' . ($selected ? ' checked="checked"' : '') . '>';
5983
        }
5984
        if (!empty($arraydata['thirdparty'])) {
5985
            $return .= '<br><span class="info-box-label">' . $arraydata['thirdparty'] . '</span>';
5986
        }
5987
        if (property_exists($this, 'date')) {
5988
            $return .= '<br><span class="info-box-label">' . dol_print_date($this->date, 'day') . '</span>';
5989
        }
5990
        if (property_exists($this, 'total_ht')) {
5991
            $return .= ' &nbsp; <span class="info-box-label amount" title="' . dol_escape_htmltag($langs->trans("AmountHT")) . '">' . price($this->total_ht);
5992
            $return .= ' ' . $langs->trans("HT");
5993
            $return .= '</span>';
5994
        }
5995
        if (method_exists($this, 'getLibStatut')) {
5996
            $alreadypaid = (empty($arraydata['alreadypaid']) ? 0 : $arraydata['alreadypaid']);
5997
            $return .= '<br><div class="info-box-status">' . $this->getLibStatut(3, $alreadypaid) . '</div>';
5998
        }
5999
        $return .= '</div>';
6000
        $return .= '</div>';
6001
        $return .= '</div>';
6002
        return $return;
6003
    }
6004
}
6005