Files
dolibarr/htdocs/core/class/commoninvoice.class.php

2382 lines
82 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/* Copyright (C) 2012 Regis Houssin <regis.houssin@inodbox.com>
* Copyright (C) 2012 Cédric Salvador <csalvador@gpcsolutions.fr>
* Copyright (C) 2012-2014 Raphaël Doursenaud <rdoursenaud@gpcsolutions.fr>
* Copyright (C) 2023 Nick Fragoulis
* Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024 Alexandre Spangaro <alexandre@inovea-conseil.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* \file htdocs/core/class/commoninvoice.class.php
* \ingroup core
* \brief File of the superclass of invoice classes (customer and supplier)
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/commonincoterm.class.php';
/**
* Superclass for invoice classes
*/
abstract class CommonInvoice extends CommonObject
{
use CommonIncoterm;
/**
* @var string Label used as ref for template invoices
*/
public $title;
/**
* @var int Type of invoice (See TYPE_XXX constants)
*/
public $type = self::TYPE_STANDARD;
/**
* @var ?int Sub type of invoice (A subtype code coming from llx_invoice_subtype table, we store the rowid. May be used by some countries like Greece)
*/
public $subtype;
/**
* @var int Thirdparty ID
* @deprecated Use $socid
* @see $socid
*/
public $fk_soc;
/**
* @var int Thirdparty ID
*/
public $socid;
/**
* @var int<0,1>
*/
public $paye;
/**
* Invoice date (date)
*
* @var int
*/
public $date;
/**
* @var int Deadline for payment
*/
public $date_lim_reglement;
/**
* @var ?int Payment term ID
*/
public $cond_reglement_id; // Id in llx_c_paiement
/**
* @var string|int Code in llx_c_paiement
*/
public $cond_reglement_code; // Code in llx_c_paiement
/**
* @var string Label in llx_c_paiement
*/
public $cond_reglement_label;
/**
* @var string Label for doc in llx_c_paiement
*/
public $cond_reglement_doc;
/**
* @var ?int Payment method ID (cheque, cash, ...)
*/
public $mode_reglement_id;
/**
* @var string Code in llx_c_paiement
*/
public $mode_reglement_code;
/**
* @var string
*/
public $mode_reglement;
/**
* @var float
*/
public $total_ht;
/**
* @var float
*/
public $total_tva;
/**
* @var float
*/
public $total_localtax1;
/**
* @var float
*/
public $total_localtax2;
/**
* @var float
*/
public $total_ttc;
/**
* @var float|null
*/
public $revenuestamp;
/**
* @var float
*/
public $totalpaid;
/**
* @var int|float
*/
public $totaldeposits;
/**
* @var int|float
*/
public $totalcreditnotes;
/**
* @var int|float|string May be used for status
*/
public $remaintopay;
/**
* @var int May be used for status
*/
public $nbofopendirectdebitorcredittransfer;
/**
* @var int[] return of getListIdAvoirFromInvoice()
*/
public $creditnote_ids;
/**
* @var int
*/
public $stripechargedone;
/**
* @var int
*/
public $stripechargeerror;
/**
* @var string Can be used to store a payment reference that must be unique for the invoice (not commonly used)
*/
public $payment_reference;
/**
* @var int 1 if a dispute has been open on one payment of invoice, keep null or 0 if not
*/
public $dispute_status = 0;
/**
* Payment description
* @var string
*/
public $description;
/**
* @var string
* @deprecated
* @see $ref_customer
*/
public $ref_client;
/**
* @var int Situation cycle reference number
*/
public $situation_cycle_ref;
/**
* ! Closing after partial payment: CLOSECODE_DISCOUNTVAT, CLOSECODE_BADDEBT, CLOSECODE_BANKCHARGE, CLOSECODE_OTHER
* ! Closing when no payment: CLOSECODE_ABANDONED, CLOSECODE_REPLACED
* @var string Close code
*/
public $close_code;
/**
* ! Comment if paid without full payment
* @var string Close note
*/
public $close_note;
/**
* ! Populated by payment modules like Stripe
* @var string[] Messages returned by an online payment module
*/
public $postactionmessages;
/**
* Standard invoice
*/
const TYPE_STANDARD = 0;
/**
* Replacement invoice
*/
const TYPE_REPLACEMENT = 1;
/**
* Credit note invoice
*/
const TYPE_CREDIT_NOTE = 2;
/**
* Deposit invoice
*/
const TYPE_DEPOSIT = 3;
/**
* Proforma invoice.
* @deprecated Remove this. A "proforma invoice" is an order with a look of invoice, not an invoice !
*/
const TYPE_PROFORMA = 4;
/**
* Situation invoice
*/
const TYPE_SITUATION = 5;
/**
* Draft status
*/
const STATUS_DRAFT = 0;
/**
* Validated (need to be paid)
*/
const STATUS_VALIDATED = 1;
/**
* Classified paid.
* If paid partially, $this->close_code can be:
* - CLOSECODE_DISCOUNTVAT
* - CLOSECODE_BADDEBT
* If paid completely, this->close_code will be null
*/
const STATUS_CLOSED = 2;
/**
* Classified abandoned and no payment done.
* $this->close_code can be:
* - CLOSECODE_BADDEBT
* - CLOSECODE_ABANDONED
* - CLOSECODE_REPLACED
*/
const STATUS_ABANDONED = 3;
const CLOSECODE_DISCOUNTVAT = 'discount_vat'; // Abandoned remain - escompte
const CLOSECODE_BADDEBT = 'badcustomer'; // Abandoned remain - bad customer
const CLOSECODE_BANKCHARGE = 'bankcharge'; // Abandoned remain - bank charge
const CLOSECODE_WITHHOLDINGTAX = 'withholdingtax'; // Abandoned remain - source tax
const CLOSECODE_OTHER = 'other'; // Abandoned remain - other
const CLOSECODE_ABANDONED = 'abandon'; // Abandoned - other
const CLOSECODE_REPLACED = 'replaced'; // Closed after doing a replacement invoice
/**
* Return remain amount to pay. Property ->id and ->total_ttc must be set.
* This does not include open direct debit requests.
*
* @param int $multicurrency Return multicurrency_amount instead of amount
* @return float|string Remain of amount to pay
*/
public function getRemainToPay($multicurrency = 0)
{
$alreadypaid = 0.0;
$alreadypaid += $this->getSommePaiement($multicurrency);
$alreadypaid += $this->getSumDepositsUsed($multicurrency);
$alreadypaid += $this->getSumCreditNotesUsed($multicurrency);
if ((int) $multicurrency > 0) {
$totalamount = $this->multicurrency_total_ttc;
} else {
$totalamount = $this->total_ttc;
}
$remaintopay = price2num($totalamount - $alreadypaid, 'MT');
if ($this->status == self::STATUS_CLOSED && $this->close_code == 'discount_vat') { // If invoice closed with discount for anticipated payment
$remaintopay = 0.0;
}
return $remaintopay;
}
/**
* Return amount of payments already done. This must include ONLY the record into the payment table.
* Payments done using discounts, credit notes, etc are not included.
* This also set ->totalpaid and ->totalpaid_multicurrency
*
* @param int<-1,1> $multicurrency Return multicurrency_amount instead of amount. -1=Return both.
* @return float|int|array{alreadypaid:float,alreadypaid_multicurrency:float} Amount of payment already done, <0 and set ->error if KO
* @see getSumDepositsUsed(), getSumCreditNotesUsed()
*/
public function getSommePaiement($multicurrency = 0)
{
$table = 'paiement_facture';
$field = 'fk_facture';
if ($this->element == 'facture_fourn' || $this->element == 'invoice_supplier') {
$table = 'paiementfourn_facturefourn';
$field = 'fk_facturefourn';
}
$sql = "SELECT sum(amount) as amount, sum(multicurrency_amount) as multicurrency_amount";
$sql .= " FROM ".$this->db->prefix().$table;
$sql .= " WHERE ".$field." = ".((int) $this->id);
dol_syslog(get_class($this)."::getSommePaiement", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
$this->db->free($resql);
if ($obj) {
if ($multicurrency < 0) {
$this->totalpaid = $obj->amount;
$this->totalpaid_multicurrency = $obj->multicurrency_amount;
return array('alreadypaid' => (float) $obj->amount, 'alreadypaid_multicurrency' => (float) $obj->multicurrency_amount);
} elseif ($multicurrency) {
$this->totalpaid_multicurrency = $obj->multicurrency_amount;
return (float) $obj->multicurrency_amount;
} else {
$this->totalpaid = $obj->amount;
return (float) $obj->amount;
}
} else {
return 0;
}
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Return amount (with tax) of all deposits invoices used by invoice.
* Should always be empty, except if option FACTURE_DEPOSITS_ARE_JUST_PAYMENTS is on for sale invoices (not recommended),
* of FACTURE_SUPPLIER_DEPOSITS_ARE_JUST_PAYMENTS is on for purchase invoices (not recommended).
*
* @param int $multicurrency Return multicurrency_amount instead of amount
* @return float Return integer <0 and set ->error if KO, Sum of deposits amount otherwise
* @see getSommePaiement(), getSumCreditNotesUsed()
*/
public function getSumDepositsUsed($multicurrency = 0)
{
/*if ($this->element == 'facture_fourn' || $this->element == 'invoice_supplier') {
// FACTURE_SUPPLIER_DEPOSITS_ARE_JUST_PAYMENTS was never supported for purchase invoice, so we can return 0 with no need of SQL for this case.
return 0.0;
}*/
require_once DOL_DOCUMENT_ROOT.'/core/class/discount.class.php';
$discountstatic = new DiscountAbsolute($this->db);
$result = $discountstatic->getSumDepositsUsed($this, $multicurrency);
if ($result >= 0) {
if ($multicurrency) {
$this->totaldeposits_multicurrency = $result;
} else {
$this->totaldeposits = $result;
}
return $result;
} else {
$this->error = $discountstatic->error;
return -1;
}
}
/**
* Return amount (with tax) of all credit notes invoices + excess received used by invoice
*
* @param int $multicurrency Return multicurrency_amount instead of amount
* @return float|string Return string 'Error...' and set ->error if KO, Sum of credit notes and deposits amount otherwise
* @see getSommePaiement(), getSumDepositsUsed()
*/
public function getSumCreditNotesUsed($multicurrency = 0)
{
require_once DOL_DOCUMENT_ROOT.'/core/class/discount.class.php';
$discountstatic = new DiscountAbsolute($this->db);
$result = $discountstatic->getSumCreditNotesUsed($this, $multicurrency);
if (is_numeric($result)) {
if ($multicurrency) {
$this->totalcreditnotes_multicurrency = $result;
} else {
$this->totalcreditnotes = $result;
}
return $result;
} else {
$this->error = $discountstatic->error;
return $result;
}
}
/**
* Return amount (with tax) of all converted amount for this credit note
*
* @param int $multicurrency Return multicurrency_amount instead of amount
* @return float Return integer <0 if KO, Sum of credit notes and deposits amount otherwise
*/
public function getSumFromThisCreditNotesNotUsed($multicurrency = 0)
{
require_once DOL_DOCUMENT_ROOT.'/core/class/discount.class.php';
$discountstatic = new DiscountAbsolute($this->db);
$result = $discountstatic->getSumFromThisCreditNotesNotUsed($this, $multicurrency);
if ($result >= 0) {
return $result;
} else {
$this->error = $discountstatic->error;
return -1;
}
}
/**
* Returns array of credit note ids from the invoice
*
* @return int[] Array of credit note ids
*/
public function getListIdAvoirFromInvoice()
{
$idarray = array();
$sql = "SELECT rowid";
$sql .= " FROM ".$this->db->prefix().$this->table_element;
$sql .= " WHERE fk_facture_source = ".((int) $this->id);
$sql .= " AND type = ".self::TYPE_CREDIT_NOTE;
$resql = $this->db->query($sql);
if ($resql) {
$num = $this->db->num_rows($resql);
$i = 0;
while ($i < $num) {
$row = $this->db->fetch_row($resql);
$idarray[] = $row[0];
$i++;
}
} else {
dol_print_error($this->db);
}
$this->creditnote_ids = $idarray;
return $idarray;
}
/**
* Returns the id of the invoice that replaces it
*
* @param string $option status filter ('', 'validated', ...)
* @return int Return integer <0 si KO, 0 if no invoice replaces it, id of invoice otherwise
*/
public function getIdReplacingInvoice($option = '')
{
$sql = "SELECT rowid";
$sql .= " FROM ".$this->db->prefix().$this->table_element;
$sql .= " WHERE fk_facture_source = ".((int) $this->id);
$sql .= " AND type < 2";
if ($option == 'validated') {
$sql .= ' AND fk_statut = 1';
}
// PROTECTION BAD DATA
// In case the database is corrupted and there is a valid replectement invoice
// and another no, priority is given to the valid one.
// Should not happen (unless concurrent access and 2 people have created a
// replacement invoice for the same invoice at the same time)
$sql .= " ORDER BY fk_statut DESC";
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
if ($obj) {
// If there is any
return $obj->rowid;
} else {
// If no invoice replaces it
return 0;
}
} else {
return -1;
}
}
/**
* Return list of open direct debit or credit transfer
*
* @param 'bank-transfer'|'direct-debit' $type 'bank-transfer' or 'direct-debit'
* @return array<array{id:int,invoiceid:int,date:''|int,amount:float}> Array with list of payments
*/
public function getListOfOpenDirectDebitOrCreditTransfer($type)
{
$listofopendirectdebitorcredittransfer = array();
// TODO Add a cache to store array of open requests for each invoice ID
$sql = "SELECT pfd.rowid, pfd.traite, pfd.date_demande as date_demande, pfd.date_traite as date_traite, pfd.amount";
$sql .= " FROM ".MAIN_DB_PREFIX."prelevement_demande as pfd";
if ($type == 'bank-transfer') {
$sql .= " WHERE fk_facture_fourn = ".((int) $this->id);
} else {
$sql .= " WHERE fk_facture = ".((int) $this->id);
}
$sql .= " AND pfd.traite = 0";
$sql .= " AND pfd.type = 'ban'";
$sql .= " ORDER BY pfd.date_demande DESC";
$resql = $this->db->query($sql);
$num = 0;
if ($resql) {
$num = $this->db->num_rows($resql);
$i = 0;
while ($i < $num) {
$obj = $this->db->fetch_object($resql);
if ($obj) {
$listofopendirectdebitorcredittransfer[] = array(
'id' => (int) $obj->rowid,
'invoiceid' => (int) $this->id,
'date' => $this->db->jdate($obj->date_demande),
'amount' => (float) $obj->amount
);
}
$i++;
}
} else {
$this->error = $this->db->lasterror();
}
$this->nbofopendirectdebitorcredittransfer = $num;
return $listofopendirectdebitorcredittransfer;
}
/**
* Return list of payments
*
* @see $error Empty string '' if no error.
*
* @param string $filtertype 1 to filter on type of payment == 'PRE' for the payment lines
* @param int $multicurrency Return multicurrency_amount instead of amount
* @param int $mode 0=payments + discount, 1=payments only
* @return array<array{amount:int|float,date:int,num:string,ref:string,ref_ext?:string,fk_bank_line?:int,type:string}> Array with list of payments
*/
public function getListOfPayments($filtertype = '', $multicurrency = 0, $mode = 0)
{
$retarray = array();
$this->error = ''; // By default no error, list can be empty.
$table = 'paiement_facture';
$table2 = 'paiement';
$field = 'fk_facture';
$field2 = 'fk_paiement';
$field3 = ', p.ref_ext';
$field4 = ', p.fk_bank'; // Bank line id
$sharedentity = 'facture';
if ($this->element == 'facture_fourn' || $this->element == 'invoice_supplier') {
$table = 'paiementfourn_facturefourn';
$table2 = 'paiementfourn';
$field = 'fk_facturefourn';
$field2 = 'fk_paiementfourn';
$field3 = '';
$sharedentity = 'facture_fourn';
}
// List of payments
if (empty($mode) || $mode == 1) {
$sql = "SELECT p.ref, pf.amount, pf.multicurrency_amount, p.fk_paiement, p.datep, p.num_paiement as num, t.code".$field3 . $field4;
$sql .= " FROM ".$this->db->prefix().$table." as pf, ".$this->db->prefix().$table2." as p, ".$this->db->prefix()."c_paiement as t";
$sql .= " WHERE pf.".$field." = ".((int) $this->id);
$sql .= " AND pf.".$field2." = p.rowid";
$sql .= ' AND p.fk_paiement = t.id';
$sql .= ' AND p.entity IN ('.getEntity($sharedentity).')';
if ($filtertype) {
$sql .= " AND t.code='PRE'";
}
dol_syslog(get_class($this)."::getListOfPayments filtertype=".$filtertype." multicurrency=".$multicurrency." mode=".$mode, LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
$num = $this->db->num_rows($resql);
$i = 0;
while ($i < $num) {
$obj = $this->db->fetch_object($resql);
if ($multicurrency) {
$tmp = array('amount' => $obj->multicurrency_amount, 'type' => $obj->code, 'typeline' => 'payment', 'date' => $obj->datep, 'num' => $obj->num, 'ref' => $obj->ref);
} else {
$tmp = array('amount' => $obj->amount, 'type' => $obj->code, 'typeline' => 'payment', 'date' => $obj->datep, 'num' => $obj->num, 'ref' => $obj->ref);
}
if (!empty($field3)) {
$tmp['ref_ext'] = $obj->ref_ext;
}
if (!empty($field4)) {
$tmp['fk_bank_line'] = $obj->fk_bank;
}
$retarray[] = $tmp;
$i++;
}
$this->db->free($resql);
} else {
$this->error = $this->db->lasterror();
dol_print_error($this->db);
return array();
}
}
// Look for credit notes and discounts and deposits
if (empty($mode) || $mode == 2) {
$sql = '';
if ($this->element == 'facture' || $this->element == 'invoice') {
$sql = "SELECT rc.amount_ttc as amount, rc.multicurrency_amount_ttc as multicurrency_amount, rc.datec as date, f.ref as ref, rc.description as type";
$sql .= ' FROM '.$this->db->prefix().'societe_remise_except as rc, '.$this->db->prefix().'facture as f';
$sql .= ' WHERE rc.fk_facture_source=f.rowid AND rc.fk_facture = '.((int) $this->id);
$sql .= ' AND (f.type = 2 OR f.type = 0 OR f.type = 3)'; // Find discount coming from credit note or excess received or deposits (payments from deposits are always null except if FACTURE_DEPOSITS_ARE_JUST_PAYMENTS is set)
} elseif ($this->element == 'facture_fourn' || $this->element == 'invoice_supplier') {
$sql = "SELECT rc.amount_ttc as amount, rc.multicurrency_amount_ttc as multicurrency_amount, rc.datec as date, f.ref as ref, rc.description as type";
$sql .= ' FROM '.$this->db->prefix().'societe_remise_except as rc, '.$this->db->prefix().'facture_fourn as f';
$sql .= ' WHERE rc.fk_invoice_supplier_source=f.rowid AND rc.fk_invoice_supplier = '.((int) $this->id);
$sql .= ' AND (f.type = 2 OR f.type = 0 OR f.type = 3)'; // Find discount coming from credit note or excess received or deposits (payments from deposits are always null except if FACTURE_SUPPLIER_DEPOSITS_ARE_JUST_PAYMENTS is set)
}
if ($sql) {
$resql = $this->db->query($sql);
if ($resql) {
$num = $this->db->num_rows($resql);
$i = 0;
while ($i < $num) {
$obj = $this->db->fetch_object($resql);
if ($multicurrency) {
$retarray[] = array('amount' => $obj->multicurrency_amount, 'type' => $obj->type, 'typeline' => 'discount', 'date' => $obj->date, 'num' => '0', 'ref' => $obj->ref);
} else {
$retarray[] = array('amount' => $obj->amount, 'type' => $obj->type, 'typeline' => 'discount', 'date' => $obj->date, 'num' => '', 'ref' => $obj->ref);
}
$i++;
}
} else {
$this->error = $this->db->lasterror();
dol_print_error($this->db);
return array();
}
$this->db->free($resql);
} else {
$this->error = $this->db->lasterror();
dol_print_error($this->db);
return array();
}
}
return $retarray;
}
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Return if an invoice can be deleted
* Rule is:
* If invoice is draft and has a temporary ref -> yes (1)
* If hidden option INVOICE_CAN_NEVER_BE_REMOVED is 1 -> no (0)
* If invoice is transferred in bookkeeping -> no (-1)
* If invoice has a definitive ref, is not last in ref -> no (-2)
* If invoice has a definitive ref, is not last in a situation cycle -> no (-3)
* If there is one payment -> no (-4)
* If already sent by email -> no (-5)
* If already printed -> no (-6)
* Otherwise -> yes (2)
*
* @return int Return integer <=0 if no, >0 if yes
*/
public function is_erasable()
{
// phpcs:enable
// We check if invoice is a temporary number (PROVxxxx)
$tmppart = substr($this->ref, 1, 4);
if ($this->status == self::STATUS_DRAFT && $tmppart === 'PROV') { // If draft invoice and ref not yet defined
return 1;
}
if (getDolGlobalInt('INVOICE_CAN_NEVER_BE_REMOVED')) {
return 0;
}
// If not a draft invoice and not temporary invoice
if ($tmppart !== 'PROV') {
if ($this instanceOf Facture) {
/* @var Facture $this */
// If sent by email, we refuse
if ((int) $this->email_sent_counter > 0) {
return -5;
}
// If printed, we refuse
if ((int) $this->pos_print_counter > 0) {
return -6;
}
}
// If in accountancy, we refuse
$ventilExportCompta = $this->getVentilExportCompta();
if ($ventilExportCompta != 0) {
return -1;
}
// Get last number of validated invoice
if ($this->element != 'invoice_supplier') {
if (empty($this->thirdparty)) {
$this->fetch_thirdparty(); // We need to have this->thirdparty defined, in case of the numbering rule uses tags that depend on thirdparty (like {t} tag).
}
$maxref = $this->getNextNumRef($this->thirdparty, 'last');
// If invoice to delete is not the last one, we refuse
if ($maxref != '' && $maxref != $this->ref) {
return -2;
}
// TODO If there is payment in bookkeeping, check the payment is not dispatched in accounting and return -2.
// ...
// If invoice is situation type, we refuse it it is not the last in situation cycle
if (!getDolGlobalString('INVOICE_SITUATION_CAN_BE_REMOVED_EVEN_IF_NOT_LAST') && $this->situation_cycle_ref && method_exists($this, 'is_last_in_cycle')) {
$last = $this->is_last_in_cycle();
if (!$last) {
return -3;
}
}
}
// Test if there is at least one payment. If yes, we refuse to delete.
if ($this->getSommePaiement() > 0) {
return -4;
}
}
return 2;
}
/**
* Return if an invoice was transferred into accountnancy.
* This is true if at least on line was transferred into table accounting_bookkeeping
*
* @param int $mode 0=Return nb of record, 1=return the transaction ID (piece_num)
* @return int Return integer <0 if KO, 0=no, nb transaction=yes or ID transaction
*/
public function getVentilExportCompta($mode = 0)
{
$alreadydispatched = 0;
$type = 'customer_invoice';
if ($this->element == 'invoice_supplier') {
$type = 'supplier_invoice';
}
$sql = " SELECT ".($mode ? 'DISTINCT piece_num' : 'COUNT(ab.rowid)')." as nb";
$sql .= " FROM ".$this->db->prefix()."accounting_bookkeeping as ab";
$sql .= " WHERE ab.doc_type = '".$this->db->escape($type)."' AND ab.fk_doc = ".((int) $this->id);
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
if ($obj) {
$alreadydispatched = $obj->nb;
}
} else {
$this->error = $this->db->lasterror();
return -1;
}
if ($alreadydispatched) {
return $alreadydispatched;
}
return 0;
}
/**
* Return next reference of invoice not already used (or last reference)
*
* @param Societe $soc Thirdparty object
* @param string $mode 'next' for next value or 'last' for last value
* @return string free ref or last ref
*/
public function getNextNumRef($soc, $mode = 'next')
{
// TODO Must be implemented into main class
return '';
}
/**
* Return label of type of invoice
*
* @param int $withbadge 1=Add span for badge css, 2=Add span and show short label
* @return string Label of type of invoice
*/
public function getLibType($withbadge = 0)
{
global $langs;
$labellong = "Unknown";
$labelshort = "Unknown";
if ($this->type == CommonInvoice::TYPE_STANDARD) {
$labellong = "InvoiceStandard";
$labelshort = "InvoiceStandardShort";
} elseif ($this->type == CommonInvoice::TYPE_REPLACEMENT) {
$labellong = "InvoiceReplacement";
$labelshort = "InvoiceReplacementShort";
} elseif ($this->type == CommonInvoice::TYPE_CREDIT_NOTE) {
$labellong = "InvoiceAvoir";
$labelshort = "CreditNote";
} elseif ($this->type == CommonInvoice::TYPE_DEPOSIT) {
$labellong = "InvoiceDeposit";
$labelshort = "Deposit";
} elseif ($this->type == CommonInvoice::TYPE_PROFORMA) { // @phan-suppress-current-line PhanDeprecatedClassConstant
$labellong = "InvoiceProForma"; // Not used.
$labelshort = "ProForma";
} elseif ($this->type == CommonInvoice::TYPE_SITUATION) {
$labellong = "InvoiceSituation";
$labelshort = "Situation";
}
$out = '';
if ($withbadge) {
$out .= '<span class="badgeneutral" title="'.dol_escape_htmltag($langs->trans($labellong)).'">';
}
$out .= $langs->trans($withbadge == 2 ? $labelshort : $labellong);
if ($withbadge) {
$out .= '</span>';
}
return $out;
}
/**
* Return label of invoice subtype
*
* @param string $table table of invoice
* @return string|int Label of invoice subtype or -1 if error
*/
public function getSubtypeLabel($table = '')
{
$subtypeLabel = '';
if ($table === 'facture' || $table === 'facture_fourn') {
$sql = "SELECT s.label FROM " . $this->db->prefix() . $table . " AS f";
$sql .= " INNER JOIN " . $this->db->prefix() . "c_invoice_subtype AS s ON f.subtype = s.rowid";
$sql .= " WHERE f.ref = '".$this->db->escape($this->ref)."'";
} elseif ($table === 'facture_rec' || $table === 'facture_fourn_rec') {
$sql = "SELECT s.label FROM " . $this->db->prefix() . $table . " AS f";
$sql .= " INNER JOIN " . $this->db->prefix() . "c_invoice_subtype AS s ON f.subtype = s.rowid";
$sql .= " WHERE f.titre = '".$this->db->escape($this->title)."'";
} else {
return -1;
}
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$subtypeLabel = $obj->label;
}
} else {
dol_print_error($this->db);
return -1;
}
return $subtypeLabel;
}
/**
* Retrieve a list of invoice subtype labels or codes.
*
* @param int<0,1> $mode 0=Return id+label, 1=Return code+id
* @return array<int,string>|array<string,int> Array of subtypes
*/
public function getArrayOfInvoiceSubtypes($mode = 0)
{
global $mysoc;
$effs = array();
$sql = "SELECT rowid, code, label as label";
$sql .= " FROM " . MAIN_DB_PREFIX . 'c_invoice_subtype';
$sql .= " WHERE active = 1 AND fk_country = ".((int) $mysoc->country_id)." AND entity IN(".getEntity('c_invoice_subtype').")";
$sql .= " ORDER by rowid, code";
dol_syslog(get_class($this) . '::getArrayOfInvoiceSubtypes', LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
$num = $this->db->num_rows($resql);
$i = 0;
while ($i < $num) {
$objp = $this->db->fetch_object($resql);
if (!$mode) {
$key = $objp->rowid;
$effs[$key] = $objp->label;
} else {
$key = $objp->code;
$effs[$key] = $objp->rowid;
}
$i++;
}
$this->db->free($resql);
}
return $effs;
}
/**
* Return label of object status
*
* @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=short label + picto, 6=Long label + picto
* @param int|float $alreadypaid 0=No payment already done, >0=Some payments were already done (we recommend to put here float amount paid if you have it, 1 otherwise)
* @return string Label of status
*/
public function getLibStatut($mode = 0, $alreadypaid = -1)
{
$moreparams = array(
'nbofopendirectdebitorcredittransfer' => $this->nbofopendirectdebitorcredittransfer,
'close_code' => $this->close_code,
'close_note' => $this->close_note,
'dispute_status' => $this->dispute_status
);
return $this->LibStatut($this->paye, $this->status, $mode, $alreadypaid, $this->type, $moreparams);
}
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Return label of a status
*
* @param int $paye Field to 1 if invoice is closed (TODO Detect this using $status instead of using this param).
* @param int $status ID status
* @param int<0,6> $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=short label + picto, 6=long label + picto
* @param int|float $alreadypaid 0=No payment already done, >0=Some payments were already done (we recommend to put here float amount paid if you have it, -1 otherwise)
* @param int $type Type invoice. If -1, we use $this->type
* @param int|array<string,int|string> $moreparams More params. Example: array('nbofopendirectdebitorcredittransfer' => x) for nb of open direct debit or credit transfer
* @return string Label of status
*/
public function LibStatut($paye, $status, $mode = 0, $alreadypaid = -1, $type = -1, $moreparams = array())
{
// phpcs:enable
global $langs, $hookmanager;
$langs->load('bills');
if ($type == -1) {
$type = $this->type;
}
// For backward compatibility
if (is_int($moreparams)) {
$moreparams = array('nbofopendirectdebitorcredittransfer' => (int) $moreparams);
}
$nbofopendirectdebitorcredittransfer = 0;
foreach ($moreparams as $moreparamkey => $moreparamvalue) {
if ($moreparamkey == 'nbofopendirectdebitorcredittransfer') {
$nbofopendirectdebitorcredittransfer = $moreparamvalue;
}
}
$statusType = 'status0';
$prefix = 'Short';
if (!$paye) {
if ($status == 0) {
$labelStatus = $langs->transnoentitiesnoconv('BillStatusDraft');
$labelStatusShort = $langs->transnoentitiesnoconv('Bill'.$prefix.'StatusDraft');
} elseif (($status == 3 || $status == 2) && $alreadypaid <= 0) {
if ($status == 3) {
$labelStatus = $langs->transnoentitiesnoconv('BillStatusCanceled');
$labelStatusShort = $langs->transnoentitiesnoconv('Bill'.$prefix.'StatusCanceled');
} else {
$labelStatus = $langs->transnoentitiesnoconv('BillStatusClosedUnpaid');
$labelStatusShort = $langs->transnoentitiesnoconv('Bill'.$prefix.'StatusClosedUnpaid');
}
$statusType = 'status5';
} elseif (($status == 3 || $status == 2) && $alreadypaid > 0) {
$labelStatus = $langs->transnoentitiesnoconv('BillStatusClosedPaidPartially');
$labelStatusShort = $langs->transnoentitiesnoconv('Bill'.$prefix.'StatusClosedPaidPartially');
$statusType = 'status9';
} elseif ($alreadypaid == 0 && $nbofopendirectdebitorcredittransfer == 0) {
$labelStatus = $langs->transnoentitiesnoconv('BillStatusNotPaid');
$labelStatusShort = $langs->transnoentitiesnoconv('Bill'.$prefix.'StatusNotPaid');
$statusType = 'status1';
} else {
$labelStatus = $langs->transnoentitiesnoconv('BillStatusStarted');
$labelStatusShort = $langs->transnoentitiesnoconv('Bill'.$prefix.'StatusStarted');
$statusType = 'status3';
}
} else {
$statusType = 'status6';
if ($type == self::TYPE_CREDIT_NOTE) {
$labelStatus = $langs->transnoentitiesnoconv('BillStatusPaidBackOrConverted'); // credit note
$labelStatusShort = $langs->transnoentitiesnoconv('Bill'.$prefix.'StatusPaidBackOrConverted'); // credit note
} elseif ($type == self::TYPE_DEPOSIT) {
$labelStatus = $langs->transnoentitiesnoconv('BillStatusConverted'); // deposit invoice
$labelStatusShort = $langs->transnoentitiesnoconv('Bill'.$prefix.'StatusConverted'); // deposit invoice
} else {
$labelStatus = $langs->transnoentitiesnoconv('BillStatusPaid');
$labelStatusShort = $langs->transnoentitiesnoconv('Bill'.$prefix.'StatusPaid');
}
}
$paramsBadge = array('badgeParams' => array('attr' => array(
'data-status-element' => $this->element,
'data-already-paid' => $alreadypaid > 0 ? 1 : 0,
'data-status' => (int) $status
)));
$parameters = array(
'status' => $status,
'mode' => $mode,
'paye' => $paye,
'alreadypaid' => $alreadypaid,
'type' => $type,
'paramsBadge'=>& $paramsBadge
);
$reshook = $hookmanager->executeHooks('LibStatut', $parameters, $this); // Note that $action and $object may have been modified by hook
if ($reshook > 0) {
return $hookmanager->resPrint;
}
if (!empty($moreparams['close_code'])) {
$titlestringtoshow = '';
if ($moreparams['close_code'] == self::CLOSECODE_DISCOUNTVAT) {
$titlestringtoshow = $langs->trans("HelpEscompte");
} elseif ($moreparams['close_code'] == self::CLOSECODE_BADDEBT) {
$titlestringtoshow = $langs->trans("ConfirmClassifyPaidPartiallyReasonBadCustomer");
} elseif ($moreparams['close_code'] == self::CLOSECODE_BANKCHARGE) {
$titlestringtoshow = $langs->trans("ConfirmClassifyPaidPartiallyReasonBankCharge");
} elseif ($moreparams['close_code'] == self::CLOSECODE_WITHHOLDINGTAX) {
$titlestringtoshow = $langs->trans("ConfirmClassifyPaidPartiallyReasonWithholdingTax");
} elseif ($moreparams['close_code'] == self::CLOSECODE_OTHER) {
$titlestringtoshow = $langs->trans("Other");
}
//$paramsbutton = array('badgeParams' => array('attr' => array('title' => 'rrrr')));
$paramsBadge['badgeParams' ]['attr']['title'] = $titlestringtoshow;
}
/*
if (isset($moreparams['dispute_status'])) {
$statusdispute = $moreparams['dispute_status'] ? img_picto($langs->trans("DisputeOpen"), 'warning') : '';
}
*/
if (isset($moreparams['dispute_status']) && $moreparams['dispute_status']) {
$labelStatus .= ' - '.$langs->trans("DisputeOpen");
$statusType = 'status8';
}
$statusbadge = dolGetStatus($labelStatus, $labelStatusShort, '', $statusType, $mode, '', $paramsBadge);
return $statusbadge;
}
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Returns an invoice payment deadline based on the invoice settlement
* conditions and billing date.
*
* @param int $cond_reglement Condition of payment (code or id) to use. If 0, we use current condition.
* @return int|string Date limit of payment if OK, <0 or string if KO
*/
public function calculate_date_lim_reglement($cond_reglement = 0)
{
// phpcs:enable
if (!$cond_reglement) {
$cond_reglement = $this->cond_reglement_code;
}
if (!$cond_reglement) {
$cond_reglement = $this->cond_reglement_id;
}
if (!$cond_reglement) {
return $this->date;
}
$cdr_nbjour = 0;
$cdr_type = 0;
$cdr_decalage = 0;
$sqltemp = "SELECT c.type_cdr, c.nbjour, c.decalage";
$sqltemp .= " FROM ".$this->db->prefix()."c_payment_term as c";
if (is_numeric($cond_reglement)) {
$sqltemp .= " WHERE c.rowid=".((int) $cond_reglement);
} else {
$sqltemp .= " WHERE c.entity IN (".getEntity('c_payment_term').")";
$sqltemp .= " AND c.code = '".$this->db->escape($cond_reglement)."'";
}
dol_syslog(get_class($this).'::calculate_date_lim_reglement', LOG_DEBUG);
$resqltemp = $this->db->query($sqltemp);
if ($resqltemp) {
if ($this->db->num_rows($resqltemp)) {
$obj = $this->db->fetch_object($resqltemp);
$cdr_nbjour = $obj->nbjour;
$cdr_type = $obj->type_cdr;
$cdr_decalage = $obj->decalage;
}
} else {
$this->error = $this->db->error();
return -1;
}
$this->db->free($resqltemp);
/* Definition de la date limit */
// 0 : adding the number of days
if ($cdr_type == 0) {
$datelim = $this->date + ($cdr_nbjour * 3600 * 24);
$datelim += ($cdr_decalage * 3600 * 24);
} elseif ($cdr_type == 1) {
// 1 : application of the "end of the month" rule
$datelim = $this->date + ($cdr_nbjour * 3600 * 24);
$mois = date('m', $datelim);
$annee = date('Y', $datelim);
if ($mois == 12) {
$mois = 1;
$annee += 1;
} else {
$mois += 1;
}
// We move at the beginning of the next month, and we take a day off
$datelim = dol_mktime(12, 0, 0, $mois, 1, $annee);
$datelim -= (3600 * 24);
$datelim += ($cdr_decalage * 3600 * 24);
} elseif ($cdr_type == 2 && !empty($cdr_decalage)) {
// 2 : application of the rule, the N of the current or next month
include_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
$datelim = $this->date + ($cdr_nbjour * 3600 * 24);
$date_piece = dol_mktime(0, 0, 0, (int) date('m', $datelim), (int) date('d', $datelim), (int) date('Y', $datelim)); // Sans les heures minutes et secondes
$date_lim_current = dol_mktime(0, 0, 0, (int) date('m', $datelim), (int) $cdr_decalage, (int) date('Y', $datelim)); // Sans les heures minutes et secondes
$date_lim_next = dol_time_plus_duree((int) $date_lim_current, 1, 'm'); // Add 1 month
$diff = $date_piece - $date_lim_current;
if ($diff < 0) {
$datelim = $date_lim_current;
} else {
$datelim = $date_lim_next;
}
} else {
return 'Bad value for type_cdr in database for record cond_reglement = '.$cond_reglement;
}
return $datelim;
}
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Create a withdrawal request for a direct debit order or a credit transfer order.
* Use the remain to pay excluding all existing open direct debit requests.
*
* @param User $fuser User asking the direct debit transfer
* @param float $amount Amount we request direct debit for
* @param string $type 'direct-debit' or 'bank-transfer'
* @param string $sourcetype Source ('facture' or 'supplier_invoice')
* @param int $checkduplicateamongall 0=Default (check among open requests only to find if request already exists). 1=Check also among requests completely processed and cancel if at least 1 request exists whatever is its status.
* @param int $ribId If defined, will use this ID to get the RIB. Otherwise, the default RIB will be taken.
* @return int Return integer <0 if KO, 0 if a request already exists, >0 if OK
*/
public function demande_prelevement(User $fuser, float $amount = 0, string $type = 'direct-debit', string $sourcetype = 'facture', int $checkduplicateamongall = 0, int $ribId = 0)
{
// phpcs:enable
global $conf;
$error = 0;
dol_syslog(get_class($this)."::demande_prelevement", LOG_DEBUG);
if ($this->status > self::STATUS_DRAFT && $this->paye == 0) {
require_once DOL_DOCUMENT_ROOT.'/societe/class/companybankaccount.class.php';
$bac = new CompanyBankAccount($this->db);
$bac->fetch($ribId, '', $this->socid);
$sql = "SELECT count(rowid) as nb";
$sql .= " FROM ".$this->db->prefix()."prelevement_demande";
if ($type == 'bank-transfer') {
$sql .= " WHERE fk_facture_fourn = ".((int) $this->id);
} else {
$sql .= " WHERE fk_facture = ".((int) $this->id);
}
$sql .= " AND type = 'ban'"; // To exclude record done for some online payments
if (empty($checkduplicateamongall)) {
$sql .= " AND traite = 0";
}
dol_syslog(get_class($this)."::demande_prelevement", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
if ($obj && $obj->nb == 0) { // If no request found yet
$now = dol_now();
$totalpaid = $this->getSommePaiement();
$totalcreditnotes = $this->getSumCreditNotesUsed();
$totaldeposits = $this->getSumDepositsUsed();
//print "totalpaid=".$totalpaid." totalcreditnotes=".$totalcreditnotes." totaldeposts=".$totaldeposits;
// We can also use bcadd to avoid pb with floating points
// For example print 239.2 - 229.3 - 9.9; does not return 0.
//$resteapayer=bcadd($this->total_ttc,$totalpaid,$conf->global->MAIN_MAX_DECIMALS_TOT);
//$resteapayer=bcadd($resteapayer,$totalavoir,$conf->global->MAIN_MAX_DECIMALS_TOT);
if (empty($amount)) {
$amount = price2num($this->total_ttc - $totalpaid - $totalcreditnotes - $totaldeposits, 'MT');
}
if (is_numeric($amount) && $amount != 0) {
$sql = 'INSERT INTO '.$this->db->prefix().'prelevement_demande(';
if ($type == 'bank-transfer') {
$sql .= 'fk_facture_fourn, ';
} else {
$sql .= 'fk_facture, ';
}
$sql .= ' amount, date_demande, fk_user_demande, code_banque, code_guichet, number, cle_rib, sourcetype, type, entity';
if (empty($bac->id)) {
$sql .= ')';
} else {
$sql .= ', fk_societe_rib)';
}
$sql .= " VALUES (".((int) $this->id);
$sql .= ", ".((float) price2num($amount));
$sql .= ", '".$this->db->idate($now)."'";
$sql .= ", ".((int) $fuser->id);
$sql .= ", '".$this->db->escape($bac->code_banque)."'";
$sql .= ", '".$this->db->escape($bac->code_guichet)."'";
$sql .= ", '".$this->db->escape($bac->number)."'";
$sql .= ", '".$this->db->escape($bac->cle_rib)."'";
$sql .= ", '".$this->db->escape($sourcetype)."'";
$sql .= ", 'ban'";
$sql .= ", ".((int) $conf->entity);
if (!empty($bac->id)) {
$sql .= ", '".$this->db->escape((string) $bac->id)."'";
}
$sql .= ")";
dol_syslog(get_class($this)."::demande_prelevement", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$this->error = $this->db->lasterror();
dol_syslog(get_class($this).'::demandeprelevement Erreur');
$error++;
}
} else {
$this->error = 'WithdrawRequestErrorNilAmount';
dol_syslog(get_class($this).'::demandeprelevement WithdrawRequestErrorNilAmount');
$error++;
}
if (!$error) {
// Force payment mode of invoice to withdraw
$payment_mode_id = dol_getIdFromCode($this->db, ($type == 'bank-transfer' ? 'VIR' : 'PRE'), 'c_paiement', 'code', 'id', 1);
if ($payment_mode_id > 0) {
$result = $this->setPaymentMethods($payment_mode_id);
}
}
if ($error) {
return -1;
}
return 1;
} else {
$this->error = "A request already exists";
dol_syslog(get_class($this).'::demandeprelevement Can t create a request to generate a direct debit, a request already exists.');
return 0;
}
} else {
$this->error = $this->db->error();
dol_syslog(get_class($this).'::demandeprelevement Error -2');
return -2;
}
} else {
$this->error = "Status of invoice does not allow this";
dol_syslog(get_class($this)."::demandeprelevement ".$this->error." $this->status, $this->paye, $this->mode_reglement_id");
return -3;
}
}
/**
* Create a payment with Stripe card
* Must take amount using Stripe and record an event into llx_actioncomm
* Record bank payment
* Send email to customer ?
*
* @param User $fuser User asking the direct debit transfer
* @param int $id Invoice ID with remain to pay
* @param string $sourcetype Source ('facture' or 'supplier_invoice')
* @return int Return integer <0 if KO, >0 if OK
*/
public function makeStripeCardRequest($fuser, $id, $sourcetype = 'facture')
{
// TODO See in sellyoursaas
return 0;
}
/**
* Create a direct debit order into prelevement_bons for a given prelevement_request, then
* Send the payment order to the service (for a direct debit order or a credit transfer order) and record an event in llx_actioncomm.
*
* @param User $fuser User asking the direct debit transfer
* @param int $did ID of unitary payment request to pay
* @param string $type 'direct-debit' or 'bank-transfer'
* @param string $sourcetype Source ('facture' or 'supplier_invoice')
* @param string $service 'StripeTest', 'StripeLive', ...
* @param string $forcestripe To force another stripe env: 'cus_account@pk_...:sk_...'
* @return int Return integer <0 if KO, >0 if OK
*/
public function makeStripeSepaRequest($fuser, $did, $type = 'direct-debit', $sourcetype = 'facture', $service = '', $forcestripe = '')
{
global $conf, $user, $langs;
if ($type != 'bank-transfer' && $type != 'credit-transfer' && !getDolGlobalString('STRIPE_SEPA_DIRECT_DEBIT')) {
return 0;
}
if ($type != 'direct-debit' && !getDolGlobalString('STRIPE_SEPA_CREDIT_TRANSFER')) {
return 0;
}
// Set a default value for service if not provided
if (empty($service)) {
$service = 'StripeTest';
if (getDolGlobalString('STRIPE_LIVE')/* && !GETPOST('forcesandbox', 'alpha')*/) {
$service = 'StripeLive';
}
}
$error = 0;
dol_syslog(get_class($this)."::makeStripeSepaRequest start did=".$did." type=".$type." service=".$service." sourcetype=".$sourcetype." forcestripe=".$forcestripe, LOG_DEBUG);
if ($this->status > self::STATUS_DRAFT && $this->paye == 0) {
// Get the default payment mode for BAN payment of the third party
require_once DOL_DOCUMENT_ROOT.'/societe/class/companybankaccount.class.php';
$bac = new CompanyBankAccount($this->db); // Table societe_rib
$result = $bac->fetch(0, '', $this->socid, 1, 'ban');
if ($result <= 0 || empty($bac->id)) {
$this->error = $langs->trans("ThirdpartyHasNoDefaultBankAccount");
$this->errors[] = $this->error;
dol_syslog(get_class($this)."::makeStripeSepaRequest ".$this->error);
return -1;
}
// Load the pending payment request to process (with rowid=$did)
$sql = "SELECT rowid, date_demande, amount, fk_facture, fk_facture_fourn, fk_salary, fk_prelevement_bons";
$sql .= " FROM ".$this->db->prefix()."prelevement_demande";
$sql .= " WHERE rowid = ".((int) $did);
if ($type != 'bank-transfer' && $type != 'credit-transfer') {
$sql .= " AND fk_facture = ".((int) $this->id); // Add a protection to not pay another invoice than current one
}
if ($type != 'direct-debit') {
if ($sourcetype == 'salary') {
$sql .= " AND fk_salary = ".((int) $this->id); // Add a protection to not pay another salary than current one
} else {
$sql .= " AND fk_facture_fourn = ".((int) $this->id); // Add a protection to not pay another invoice than current one
}
}
$sql .= " AND traite = 0"; // To not process payment request that were already converted into a direct debit or credit transfer order (Note: fk_prelevement_bons is also empty when traite = 0)
dol_syslog(get_class($this)."::makeStripeSepaRequest load requests to process", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
if (!$obj) {
dol_print_error($this->db, 'CantFindRequestWithId');
return -2;
}
// amount to pay
$amount = $obj->amount;
if (is_numeric($amount) && $amount != 0) {
require_once DOL_DOCUMENT_ROOT.'/societe/class/companypaymentmode.class.php';
$companypaymentmode = new CompanyPaymentMode($this->db); // table societe_rib
$companypaymentmode->fetch($bac->id);
$this->stripechargedone = 0;
$this->stripechargeerror = 0;
$now = dol_now();
$currency = $conf->currency;
$errorforinvoice = 0; // We reset the $errorforinvoice at each invoice loop
$this->fetch_thirdparty();
dol_syslog("makeStripeSepaRequest Process payment request amount=".$amount." thirdparty_id=" . $this->thirdparty->id . ", thirdparty_name=" . $this->thirdparty->name . " ban id=" . $bac->id, LOG_DEBUG);
//$alreadypayed = $this->getSommePaiement();
//$amount_credit_notes_included = $this->getSumCreditNotesUsed();
//$amounttopay = $this->total_ttc - $alreadypayed - $amount_credit_notes_included;
$amounttopay = $amount;
// Correct the amount according to unit of currency
// See https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support
$arrayzerounitcurrency = ['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'];
$amountstripe = $amounttopay;
if (!in_array($currency, $arrayzerounitcurrency)) {
$amountstripe *= 100;
}
$fk_bank_account = getDolGlobalInt('STRIPE_BANK_ACCOUNT_FOR_PAYMENTS'); // Bank account used for SEPA direct debit or credit transfer. Must be the Stripe account in Dolibarr.
if (!($fk_bank_account > 0)) {
$error++;
$errorforinvoice++;
dol_syslog("makeStripeSepaRequest Error no bank account defined for Stripe payments", LOG_ERR);
$this->error = "Error bank account for Stripe payments not defined into Stripe module";
$this->errors[] = $this->error;
}
$this->db->begin();
// Create a prelevement_bon
require_once DOL_DOCUMENT_ROOT.'/compta/prelevement/class/bonprelevement.class.php';
$bon = new BonPrelevement($this->db);
if (!$error) {
if (empty($obj->fk_prelevement_bons)) {
// This creates a record into llx_prelevement_bons and updates link with llx_prelevement_demande
$nbinvoices = $bon->create('0', '0', 'real', 'ALL', 0, 0, $type, $did, $fk_bank_account);
if ($nbinvoices <= 0) {
$error++;
$errorforinvoice++;
dol_syslog("makeStripeSepaRequest Error on BonPrelevement creation", LOG_ERR);
$this->error = "Error on BonPrelevement creation";
$this->errors[] = $this->error;
}
/*
if (!$error) {
// Update the direct debit payment request of the processed request to save the id of the prelevement_bon
$sql = "UPDATE ".MAIN_DB_PREFIX."prelevement_demande SET";
$sql .= " fk_prelevement_bons = ".((int) $bon->id);
$sql .= " WHERE rowid = ".((int) $did);
$result = $this->db->query($sql);
if ($result < 0) {
$error++;
$this->error = "Error on updating fk_prelevement_bons to ".$bon->id;
$this->errors[] = $this->error;
}
}
*/
} else {
$error++;
$errorforinvoice++;
dol_syslog("makeStripeSepaRequest Error Line already part of a bank payment order", LOG_ERR);
$this->error = "The line is already included into a bank payment order. Delete the bank payment order first.";
$this->errors[] = $this->error;
}
}
$paymentintent = null;
if (!$error) {
if ($amountstripe > 0) {
try {
global $savstripearrayofkeysbyenv;
global $stripearrayofkeysbyenv;
$servicestatus = 0;
if ($service == 'StripeLive') {
$servicestatus = 1;
}
//var_dump($companypaymentmode);
dol_syslog("makeStripeSepaRequest We will try to pay with companypaymentmodeid=" . $companypaymentmode->id . " stripe_card_ref=" . $companypaymentmode->stripe_card_ref . " mode=" . $companypaymentmode->status, LOG_DEBUG);
$thirdparty = new Societe($this->db);
$resultthirdparty = $thirdparty->fetch($this->socid);
include_once DOL_DOCUMENT_ROOT . '/stripe/class/stripe.class.php'; // This include the include of htdocs/stripe/config.php
// So it inits or erases the $stripearrayofkeysbyenv
$stripe = new Stripe($this->db);
if (empty($savstripearrayofkeysbyenv)) {
$savstripearrayofkeysbyenv = $stripearrayofkeysbyenv;
}
dol_syslog("makeStripeSepaRequest Current Stripe environment is " . $stripearrayofkeysbyenv[$servicestatus]['publishable_key']);
dol_syslog("makeStripeSepaRequest Current Saved Stripe environment is ".$savstripearrayofkeysbyenv[$servicestatus]['publishable_key']);
$foundalternativestripeaccount = '';
// Force stripe to another value (by default this value is empty)
if (! empty($forcestripe)) {
dol_syslog("makeStripeSepaRequest A dedicated stripe account was forced, so we switch to it.");
$tmparray = explode('@', $forcestripe);
if (! empty($tmparray[1])) {
$tmparray2 = explode(':', $tmparray[1]);
if (! empty($tmparray2[1])) {
$stripearrayofkeysbyenv[$servicestatus]["publishable_key"] = $tmparray2[0];
$stripearrayofkeysbyenv[$servicestatus]["secret_key"] = $tmparray2[1];
$stripearrayofkeys = $stripearrayofkeysbyenv[$servicestatus];
\Stripe\Stripe::setApiKey($stripearrayofkeys['secret_key']);
$foundalternativestripeaccount = $tmparray[0]; // Store the customer id
dol_syslog("makeStripeSepaRequest We use now customer=".$foundalternativestripeaccount." publishable_key=".$stripearrayofkeys['publishable_key'], LOG_DEBUG);
}
}
if (! $foundalternativestripeaccount) {
$stripearrayofkeysbyenv = $savstripearrayofkeysbyenv;
$stripearrayofkeys = $savstripearrayofkeysbyenv[$servicestatus];
\Stripe\Stripe::setApiKey($stripearrayofkeys['secret_key']);
dol_syslog("makeStripeSepaRequest We found a bad value for Stripe Account for thirdparty id=".$thirdparty->id.", so we ignore it and keep using the global one, so ".$stripearrayofkeys['publishable_key'], LOG_WARNING);
}
} else {
$stripearrayofkeysbyenv = $savstripearrayofkeysbyenv;
$stripearrayofkeys = $savstripearrayofkeysbyenv[$servicestatus];
\Stripe\Stripe::setApiKey($stripearrayofkeys['secret_key']);
dol_syslog("makeStripeSepaRequest No dedicated Stripe Account requested, so we use global one, so ".$stripearrayofkeys['publishable_key'], LOG_DEBUG);
}
$stripeacc = $stripe->getStripeAccount($service, $this->socid); // Get Stripe OAuth connect account if it exists (no network access here)
if ($foundalternativestripeaccount) {
if (empty($stripeacc)) { // If the Stripe connect account not set, we use common API usage
$customer = \Stripe\Customer::retrieve(array('id' => "$foundalternativestripeaccount", 'expand[]' => 'sources'));
} else {
$customer = \Stripe\Customer::retrieve(array('id' => "$foundalternativestripeaccount", 'expand[]' => 'sources'), array("stripe_account" => $stripeacc));
}
} else {
$customer = $stripe->customerStripe($thirdparty, $stripeacc, $servicestatus, 0);
if (empty($customer) && ! empty($stripe->error)) {
$this->error = $stripe->error;
$this->errors[] = $this->error;
}
/*if (!empty($customer) && empty($customer->sources)) {
$customer = null;
$this->error = '\Stripe\Customer::retrieve did not returned the sources';
$this->errors[] = $this->error;
}*/
}
// $nbhoursbetweentries = (empty($conf->global->SELLYOURSAAS_NBHOURSBETWEENTRIES) ? 49 : $conf->global->SELLYOURSAAS_NBHOURSBETWEENTRIES); // Must have more that 48 hours + 1 between each try (so 1 try every 3 daily batch)
// $nbdaysbeforeendoftries = (empty($conf->global->SELLYOURSAAS_NBDAYSBEFOREENDOFTRIES) ? 35 : $conf->global->SELLYOURSAAS_NBDAYSBEFOREENDOFTRIES);
$postactionmessages = [];
if ($resultthirdparty > 0 && !empty($customer)) {
if (!$error) { // Payment was not canceled
$stripecard = null;
if ($companypaymentmode->type == 'ban') {
// Check into societe_rib if a payment mode for Stripe and ban payment exists
// To make a Stripe SEPA payment request, we must have the payment mode source already saved into societe_rib and retrieved with ->sepaStripe
// The payment mode source is created when we create the bank account on Stripe with paymentmodes.php?action=create
$stripecard = $stripe->sepaStripe($customer, $companypaymentmode, $stripeacc, $servicestatus, 0);
} else {
$error++;
$this->error = 'The payment mode type is not "ban"';
}
if ($stripecard) { // Can be src_... (for sepa) or pm_... (new card mode). Note that card_... (old card mode) should not happen here.
$FULLTAG = 'DID='.$did.'-INV=' . $this->id . '-CUS=' . $thirdparty->id;
$description = 'Stripe payment from makeStripeSepaRequest: ' . $FULLTAG . ' did='.$did.' ref=' . $this->ref;
$stripefailurecode = '';
$stripefailuremessage = '';
$stripefailuredeclinecode = '';
// Using new SCA method
dol_syslog("* Create payment on SEPA " . $stripecard->id . ", amounttopay=" . $amounttopay . ", amountstripe=" . $amountstripe . ", FULLTAG=" . $FULLTAG, LOG_DEBUG);
// Create payment intent and charge payment (confirmnow = true)
$paymentintent = $stripe->getPaymentIntent($amounttopay, $currency, $FULLTAG, $description, $this, $customer->id, $stripeacc, $servicestatus, 0, 'automatic', true, $stripecard->id, 1, 1, $did);
$charge = new stdClass();
if ($paymentintent->status === 'succeeded' || $paymentintent->status === 'processing') {
$charge->status = 'ok';
$charge->id = $paymentintent->id;
$charge->customer = $customer->id;
} elseif ($paymentintent->status === 'requires_action') {
//paymentintent->status may be => 'requires_action' (no error in such a case)
dol_syslog(var_export($paymentintent, true), LOG_DEBUG);
$charge->status = 'failed';
$charge->customer = $customer->id;
$charge->failure_code = $stripe->code;
$charge->failure_message = $stripe->error;
$charge->failure_declinecode = $stripe->declinecode;
$stripefailurecode = $stripe->code;
$stripefailuremessage = 'Action required. Contact the support at ';// . $conf->global->SELLYOURSAAS_MAIN_EMAIL;
$stripefailuredeclinecode = $stripe->declinecode;
} else {
dol_syslog(var_export($paymentintent, true), LOG_DEBUG);
$charge->status = 'failed';
$charge->customer = $customer->id;
$charge->failure_code = $stripe->code;
$charge->failure_message = $stripe->error;
$charge->failure_declinecode = $stripe->declinecode;
$stripefailurecode = $stripe->code;
$stripefailuremessage = $stripe->error;
$stripefailuredeclinecode = $stripe->declinecode;
}
//var_dump("stripefailurecode=".$stripefailurecode." stripefailuremessage=".$stripefailuremessage." stripefailuredeclinecode=".$stripefailuredeclinecode);
//exit;
// Return $charge = array('id'=>'ch_XXXX', 'status'=>'succeeded|pending|failed', 'failure_code'=>, 'failure_message'=>...)
if (empty($charge) || $charge->status == 'failed') {
dol_syslog('Failed to charge payment mode ' . $stripecard->id . ' stripefailurecode=' . $stripefailurecode . ' stripefailuremessage=' . $stripefailuremessage . ' stripefailuredeclinecode=' . $stripefailuredeclinecode, LOG_WARNING);
// Save a stripe payment was in error
$this->stripechargeerror++;
$error++;
$errorforinvoice++;
$errmsg = $langs->trans("FailedToChargeSEPA");
if (!empty($charge)) {
if ($stripefailuredeclinecode == 'authentication_required') {
$errauthenticationmessage = $langs->trans("ErrSCAAuthentication");
$errmsg = $errauthenticationmessage;
} elseif (in_array($stripefailuredeclinecode, ['insufficient_funds', 'generic_decline'])) {
$errmsg .= ': ' . $charge->failure_code;
$errmsg .= ($charge->failure_message ? ' - ' : '') . ' ' . $charge->failure_message;
if (empty($stripefailurecode)) {
$stripefailurecode = $charge->failure_code;
}
if (empty($stripefailuremessage)) {
$stripefailuremessage = $charge->failure_message;
}
} else {
$errmsg .= ': failure_code=' . $charge->failure_code;
$errmsg .= ($charge->failure_message ? ' - ' : '') . ' failure_message=' . $charge->failure_message;
if (empty($stripefailurecode)) {
$stripefailurecode = $charge->failure_code;
}
if (empty($stripefailuremessage)) {
$stripefailuremessage = $charge->failure_message;
}
}
} else {
$errmsg .= ': ' . $stripefailurecode . ' - ' . $stripefailuremessage;
$errmsg .= ($stripefailuredeclinecode ? ' - ' . $stripefailuredeclinecode : '');
}
$description = 'Stripe payment ERROR from makeStripeSepaRequest: ' . $FULLTAG;
$postactionmessages[] = $errmsg . ' (' . $stripearrayofkeys['publishable_key'] . ')';
$this->error = $errmsg;
$this->errors[] = $this->error;
} else {
dol_syslog('Successfuly request '.$type.' '.$stripecard->id);
$postactionmessages[] = 'Success to request '.$type.' (' . $charge->id . ' with ' . $stripearrayofkeys['publishable_key'] . ')';
// Save a stripe payment was done in real life so later we will be able to force a commit on recorded payments
// even if in batch mode (method doTakePaymentStripe), we will always make all action in one transaction with a forced commit.
$this->stripechargedone++;
// Default description used for label of event. Will be overwrite by another value later.
$description = 'Stripe payment request OK (' . $charge->id . ') from makeStripeSepaRequest: ' . $FULLTAG;
}
$object = $this;
// Track an event
if (empty($charge) || $charge->status == 'failed') {
$actioncode = 'PAYMENT_STRIPE_KO';
$extraparams = $stripefailurecode;
$extraparams .= (($extraparams && $stripefailuremessage) ? ' - ' : '') . $stripefailuremessage;
$extraparams .= (($extraparams && $stripefailuredeclinecode) ? ' - ' : '') . $stripefailuredeclinecode;
} else {
$actioncode = 'PAYMENT_STRIPE_OK';
$extraparams = array();
}
} else {
$error++;
$errorforinvoice++;
dol_syslog("No ban payment method found for this stripe customer " . $customer->id, LOG_WARNING);
$this->error = 'Failed to get direct debit payment method for stripe customer = ' . $customer->id;
$this->errors[] = $this->error;
$description = 'Failed to find or use the payment mode - no ban defined for the thirdparty account';
$stripefailurecode = 'BADPAYMENTMODE';
$stripefailuremessage = 'Failed to find or use the payment mode - no ban defined for the thirdparty account';
$postactionmessages[] = $description . ' (' . $stripearrayofkeys['publishable_key'] . ')';
$object = $this;
$actioncode = 'PAYMENT_STRIPE_KO';
$extraparams = array();
}
} else {
// If error because payment was canceled for a logical reason, we do nothing (no event added)
$description = '';
$stripefailurecode = '';
$stripefailuremessage = '';
$object = $this;
$actioncode = '';
$extraparams = array();
}
} else { // Else of the if ($resultthirdparty > 0 && ! empty($customer)) {
if ($resultthirdparty <= 0) {
dol_syslog('SellYourSaasUtils Failed to load customer for thirdparty_id = ' . $thirdparty->id, LOG_WARNING);
$this->error = 'Failed to load Stripe account for thirdparty_id = ' . $thirdparty->id;
$this->errors[] = $this->error;
} else { // $customer stripe not found
dol_syslog('SellYourSaasUtils Failed to get Stripe customer id for thirdparty_id = ' . $thirdparty->id . " in mode " . $servicestatus . " in Stripe env " . $stripearrayofkeysbyenv[$servicestatus]['publishable_key'], LOG_WARNING);
$this->error = 'Failed to get Stripe account id for thirdparty_id = ' . $thirdparty->id . " in mode " . $servicestatus . " in Stripe env " . $stripearrayofkeysbyenv[$servicestatus]['publishable_key'];
$this->errors[] = $this->error;
}
$error++;
$errorforinvoice++;
$description = 'Failed to find or use your payment mode (no payment mode for this customer id)';
$stripefailurecode = 'BADPAYMENTMODE';
$stripefailuremessage = 'Failed to find or use your payment mode (no payment mode for this customer id)';
$postactionmessages = [];
$object = $this;
$actioncode = 'PAYMENT_STRIPE_KO';
$extraparams = array();
}
if ($description) {
dol_syslog("* Record event for credit transfer or direct debit request result - " . $description);
require_once DOL_DOCUMENT_ROOT.'/comm/action/class/actioncomm.class.php';
// Insert record of payment (success or error)
$actioncomm = new ActionComm($this->db);
$actioncomm->type_code = 'AC_OTH_AUTO'; // Type of event ('AC_OTH', 'AC_OTH_AUTO', 'AC_XXX'...)
$actioncomm->code = 'AC_' . $actioncode;
$actioncomm->label = $description;
$actioncomm->note_private = implode(",\n", $postactionmessages);
$actioncomm->fk_project = $this->fk_project;
$actioncomm->datep = $now;
$actioncomm->datef = $now;
$actioncomm->percentage = -1; // Not applicable
$actioncomm->socid = $thirdparty->id;
$actioncomm->contactid = 0;
$actioncomm->authorid = $user->id; // User saving action
$actioncomm->userownerid = $user->id; // Owner of action
// Fields when action is a real email (content is already into note)
/*$actioncomm->email_msgid = $object->email_msgid;
$actioncomm->email_from = $object->email_from;
$actioncomm->email_sender= $object->email_sender;
$actioncomm->email_to = $object->email_to;
$actioncomm->email_tocc = $object->email_tocc;
$actioncomm->email_tobcc = $object->email_tobcc;
$actioncomm->email_subject = $object->email_subject;
$actioncomm->errors_to = $object->errors_to;*/
$actioncomm->fk_element = $this->id;
$actioncomm->elementid = $this->id;
$actioncomm->elementtype = $this->element;
$actioncomm->extraparams = $extraparams; // Can be null, empty string or array()
$actioncomm->create($user);
}
$this->description = $description;
$this->postactionmessages = $postactionmessages;
} catch (Exception $e) {
$error++;
$errorforinvoice++;
dol_syslog('Error ' . $e->getMessage(), LOG_ERR);
$this->error = 'Error ' . $e->getMessage();
$this->errors[] = $this->error;
}
} else { // If remain to pay is null
$error++;
$errorforinvoice++;
dol_syslog("Remain to pay is null for the invoice " . $this->id . " " . $this->ref . ". Why is the invoice not classified 'Paid' ?", LOG_WARNING);
$this->error = "Remain to pay is null for the invoice " . $this->id . " " . $this->ref . ". Why is the invoice not classified 'Paid' ?";
$this->errors[] = $this->error;
}
}
// Set status of the order to "Transferred" with method 'api'
if (!$error && !$errorforinvoice) {
$result = $bon->set_infotrans($user, $now, 3);
if ($result < 0) {
$error++;
$errorforinvoice++;
dol_syslog("Error on BonPrelevement creation", LOG_ERR);
$this->error = "Error on BonPrelevement creation";
$this->errors[] = $this->error;
}
}
if (!$error && !$errorforinvoice && $paymentintent !== null) {
// Update the direct debit payment request of the processed invoice to save the id of the prelevement_bon
$sql = "UPDATE ".MAIN_DB_PREFIX."prelevement_demande SET";
$sql .= " ext_payment_id = '".$this->db->escape($paymentintent->id)."',";
$sql .= " ext_payment_site = '".$this->db->escape($service)."'";
$sql .= " WHERE rowid = ".((int) $did);
dol_syslog(get_class($this)."::makeStripeSepaRequest update to save stripe paymentintent ids", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$this->error = $this->db->lasterror();
dol_syslog(get_class($this).'::makeStripeSepaRequest Erreur');
$error++;
}
}
if (!$error && !$errorforinvoice) {
$this->db->commit();
} else {
$this->db->rollback();
}
} else {
$this->error = 'WithdrawRequestErrorNilAmount';
dol_syslog(get_class($this).'::makeStripeSepaRequest WithdrawRequestErrorNilAmount');
$error++;
}
/*
if (!$error) {
// Force payment mode of the invoice to withdraw
$payment_mode_id = dol_getIdFromCode($this->db, ($type == 'bank-transfer' ? 'VIR' : 'PRE'), 'c_paiement', 'code', 'id', 1);
if ($payment_mode_id > 0) {
$result = $this->setPaymentMethods($payment_mode_id);
}
}*/
if ($error) {
return -1;
}
return 1;
} else {
$this->error = $this->db->error();
dol_syslog(get_class($this).'::makeStripeSepaRequest Erreur -2');
return -2;
}
} else {
$this->error = "Status of invoice does not allow this";
dol_syslog(get_class($this)."::makeStripeSepaRequest ".$this->error." ".$this->status." ,".$this->paye.", ".$this->mode_reglement_id, LOG_WARNING);
return -3;
}
}
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Remove a direct debit request or a credit transfer request
*
* @param User $fuser User making delete
* @param int $did ID of request to delete
* @return int Return integer <0 if OK, >0 if KO
*/
public function demande_prelevement_delete($fuser, $did)
{
// phpcs:enable
$sql = 'DELETE FROM '.$this->db->prefix().'prelevement_demande';
$sql .= ' WHERE rowid = '.((int) $did);
$sql .= ' AND traite = 0';
if ($this->db->query($sql)) {
return 0;
} else {
$this->error = $this->db->lasterror();
dol_syslog(get_class($this).'::demande_prelevement_delete Error '.$this->error);
return -1;
}
}
/**
* Build string for EPC QR Code
*
* @return string String for EPC QR Code
*/
public function buildEPCQrCodeString()
{
global $mysoc;
// Get the amount to pay
$amount_to_pay = $this->getRemainToPay();
// Prevent negative values (e.g. overpayments)
$amount_to_pay = max(0, $amount_to_pay);
// Ensure numeric formatting for EPC QR code
$amount_to_pay = price2num($amount_to_pay, 'MT');
// Initialize an array to hold the lines of the QR code
$lines = array();
// Add the standard elements to the QR code
$lines = [
'BCD', // Service Tag (optional)
'002', // Version (optional)
'1', // Character set (optional)
'SCT', // Identification (optional)
];
// Add the bank account information
include_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
$idofbankaccountouse = $this->fk_account;
if (empty($idofbankaccountouse)) {
$idofbankaccountouse = $this->fk_bank; // for backward compatibility
}
if (empty($idofbankaccountouse)) {
$idofbankaccountouse = getDolGlobalInt('FACTURE_RIB_NUMBER');
}
if ($idofbankaccountouse > 0) {
$bankAccount = new Account($this->db);
$bankAccount->fetch($idofbankaccountouse);
$lines[] = $bankAccount->bic; //BIC (required)
if (!empty($bankAccount->owner_name)) {
$lines[] = $bankAccount->owner_name; //Owner of the bank account, if present (required)
} else {
$lines[] = $mysoc->name; //Name (required)
}
$lines[] = $bankAccount->iban; //IBAN (required)
} else {
$lines[] = ""; //BIC (required)
$lines[] = $mysoc->name; //Name (required)
$lines[] = ""; //IBAN (required)
}
// Add the amount and reference
$lines[] = 'EUR' . $amount_to_pay; // Amount (optional)
$lines[] = ''; // Purpose (optional)
$lines[] = ''; // Payment reference (optional)
$lines[] = $this->ref; // Remittance Information (optional)
// Join the lines with newline characters and return the result
return implode("\n", $lines);
}
/**
* Build string for ZATCA QR Code (Arabi Saudia)
*
* @return string String for ZATCA QR Code
*/
public function buildZATCAQRString()
{
global $conf, $mysoc;
$tmplang = new Translate('', $conf);
$tmplang->setDefaultLang('en_US');
$tmplang->load("main");
$datestring = dol_print_date($this->date, 'dayhourrfc');
//$pricewithtaxstring = price($this->total_ttc, 0, $tmplang, 0, -1, 2);
//$pricetaxstring = price($this->total_tva, 0, $tmplang, 0, -1, 2);
$pricewithtaxstring = price2num($this->total_ttc, 2, 1);
$pricetaxstring = price2num($this->total_tva, 2, 1);
/*
$name = implode(unpack("H*", $this->thirdparty->name));
$vatnumber = implode(unpack("H*", $this->thirdparty->tva_intra));
$date = implode(unpack("H*", $datestring));
$pricewithtax = implode(unpack("H*", price2num($pricewithtaxstring, 2)));
$pricetax = implode(unpack("H*", $pricetaxstring));
//var_dump(strlen($this->thirdparty->name));
//var_dump(str_pad(dechex('9'), 2, '0', STR_PAD_LEFT));
//var_dump($this->thirdparty->name);
//var_dump(implode(unpack("H*", $this->thirdparty->name)));
//var_dump(price($this->total_tva, 0, $tmplang, 0, -1, 2));
$s = '01'.str_pad(dechex(strlen($this->thirdparty->name)), 2, '0', STR_PAD_LEFT).$name;
$s .= '02'.str_pad(dechex(strlen($this->thirdparty->tva_intra)), 2, '0', STR_PAD_LEFT).$vatnumber;
$s .= '03'.str_pad(dechex(strlen($datestring)), 2, '0', STR_PAD_LEFT).$date;
$s .= '04'.str_pad(dechex(strlen($pricewithtaxstring)), 2, '0', STR_PAD_LEFT).$pricewithtax;
$s .= '05'.str_pad(dechex(strlen($pricetaxstring)), 2, '0', STR_PAD_LEFT).$pricetax;
$s .= ''; // Hash of xml invoice
$s .= ''; // ecda signature
$s .= ''; // ecda public key
$s .= ''; // ecda signature of public key stamp
*/
$mysocname = $mysoc->name ?? '';
$mysoctva_intra = $mysoc->tva_intra ?? '';
// Using TLV format
$s = pack('C1', 1).pack('C1', strlen($mysocname)).$mysocname;
$s .= pack('C1', 2).pack('C1', strlen($mysoctva_intra)).$mysoctva_intra;
$s .= pack('C1', 3).pack('C1', strlen($datestring)).$datestring;
$s .= pack('C1', 4).pack('C1', strlen($pricewithtaxstring)).$pricewithtaxstring;
$s .= pack('C1', 5).pack('C1', strlen($pricetaxstring)).$pricetaxstring;
$s .= ''; // Hash of xml invoice
$s .= ''; // ecda signature
$s .= ''; // ecda public key
$s .= ''; // ecda signature of public key stamp
$s = base64_encode($s);
return $s;
}
/**
* Build string for QR-Bill (Switzerland)
*
* @return string String for Switzerland QR Code if QR-Bill
*/
public function buildSwitzerlandQRString()
{
global $conf, $mysoc;
$tmplang = new Translate('', $conf);
$tmplang->setDefaultLang('en_US');
$tmplang->load("main");
$pricewithtaxstring = price2num($this->total_ttc, 2, 1);
$pricetaxstring = price2num($this->total_tva, 2, 1);
$complementaryinfo = '';
/*
Example: //S1/10/10201409/11/190512/20/1400.000-53/30/106017086/31/180508/32/7.7/40/2:10;0:30
/10/ Numéro de facture 10201409
/11/ Date de facture 12.05.2019
/20/ Référence client 1400.000-53
/30/ Numéro IDE pour la TVA CHE-106.017.086 TVA
/31/ Date de la prestation pour la comptabilisation de la TVA 08.05.2018
/32/ Taux de TVA sur le montant total de la facture 7.7%
/40/ Conditions 2% descompte à 10 jours, paiement net à 30 jours
*/
$datestring = dol_print_date($this->date, '%y%m%d');
//$pricewithtaxstring = price($this->total_ttc, 0, $tmplang, 0, -1, 2);
//$pricetaxstring = price($this->total_tva, 0, $tmplang, 0, -1, 2);
$complementaryinfo = '//S1/10/'.str_replace('/', '', $this->ref).'/11/'.$datestring;
if ($this->ref_client) {
$complementaryinfo .= '/20/'.$this->ref_client;
}
if ($this->thirdparty->tva_intra) {
$complementaryinfo .= '/30/'.$this->thirdparty->tva_intra;
}
include_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
$bankaccount = new Account($this->db);
// Header
$s = '';
$s .= "SPC\n";
$s .= "0200\n";
$s .= "1\n";
// Info Seller ("Compte / Payable à")
if ($this->fk_account > 0) {
// Bank BAN if country is LI or CH. TODO Add a test to check than IBAN start with CH or LI
$bankaccount->fetch($this->fk_account);
$s .= $bankaccount->iban."\n";
} else {
$s .= "\n";
}
if ($bankaccount->id > 0 && getDolGlobalString('PDF_SWISS_QRCODE_USE_OWNER_OF_ACCOUNT_AS_CREDITOR')) {
// If a bank account is provided and we ask to use it as creditor, we use the bank address
// TODO In a future, we may always use this address, and if name/address/zip/town/country differs from $mysoc, we can use the address of $mysoc into the final seller field ?
$s .= "S\n";
$s .= dol_trunc($bankaccount->owner_name, 70, 'right', 'UTF-8', 1)."\n";
$addresslinearray = explode("\n", $bankaccount->owner_address);
$s .= dol_trunc(empty($addresslinearray[1]) ? '' : $addresslinearray[1], 70, 'right', 'UTF-8', 1)."\n"; // address line 1
$s .= dol_trunc(empty($addresslinearray[2]) ? '' : $addresslinearray[2], 70, 'right', 'UTF-8', 1)."\n"; // address line 2
/*$s .= dol_trunc($mysoc->zip, 16, 'right', 'UTF-8', 1)."\n";
$s .= dol_trunc($mysoc->town, 35, 'right', 'UTF-8', 1)."\n";
$s .= dol_trunc($mysoc->country_code, 2, 'right', 'UTF-8', 1)."\n";*/
} else {
$s .= "S\n";
$s .= dol_trunc((string) $mysoc->name, 70, 'right', 'UTF-8', 1)."\n";
$addresslinearray = explode("\n", $mysoc->address);
$s .= dol_trunc(empty($addresslinearray[1]) ? '' : $addresslinearray[1], 70, 'right', 'UTF-8', 1)."\n"; // address line 1
$s .= dol_trunc(empty($addresslinearray[2]) ? '' : $addresslinearray[2], 70, 'right', 'UTF-8', 1)."\n"; // address line 2
$s .= dol_trunc($mysoc->zip, 16, 'right', 'UTF-8', 1)."\n";
$s .= dol_trunc($mysoc->town, 35, 'right', 'UTF-8', 1)."\n";
$s .= dol_trunc($mysoc->country_code, 2, 'right', 'UTF-8', 1)."\n";
}
// Final seller (Ultimate seller) ("Créancier final" = "En faveur de")
$s .= "\n";
$s .= "\n";
$s .= "\n";
$s .= "\n";
$s .= "\n";
$s .= "\n";
$s .= "\n";
// Amount of payment (to do?)
$s .= price($pricewithtaxstring, 0, 'none', 0, 0, 2)."\n";
$s .= ($this->multicurrency_code ? $this->multicurrency_code : $conf->currency)."\n";
// Buyer
$s .= "S\n";
$s .= dol_trunc((string) $this->thirdparty->name, 70, 'right', 'UTF-8', 1)."\n";
$addresslinearray = explode("\n", $this->thirdparty->address);
$s .= dol_trunc(empty($addresslinearray[1]) ? '' : $addresslinearray[1], 70, 'right', 'UTF-8', 1)."\n"; // address line 1
$s .= dol_trunc(empty($addresslinearray[2]) ? '' : $addresslinearray[2], 70, 'right', 'UTF-8', 1)."\n"; // address line 2
$s .= dol_trunc($this->thirdparty->zip, 16, 'right', 'UTF-8', 1)."\n";
$s .= dol_trunc($this->thirdparty->town, 35, 'right', 'UTF-8', 1)."\n";
$s .= dol_trunc($this->thirdparty->country_code, 2, 'right', 'UTF-8', 1)."\n";
// ID of payment
$s .= "NON\n"; // NON or QRR
$s .= "\n"; // QR Code reference if previous field is QRR
// Free text
if ($complementaryinfo) {
$s .= $complementaryinfo."\n";
} else {
$s .= "\n";
}
$s .= "EPD\n";
// More text, complementary info
if ($complementaryinfo) {
$s .= $complementaryinfo."\n";
}
$s .= "\n";
//var_dump($s);exit;
return $s;
}
}
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobjectline.class.php';
/**
* Parent class of all other business classes for details of elements (invoices, contracts, proposals, orders, ...)
*/
abstract class CommonInvoiceLine extends CommonObjectLine
{
/**
* Custom label of line. Not used by default.
* @deprecated
* @var string
*/
public $label;
/**
* @deprecated Use $product_ref
* @see $product_ref
* @var string
*/
public $ref; // Product ref (deprecated)
/**
* @var string
* @deprecated Use $product_label
* @see $product_label
*/
public $libelle; // Product label (deprecated)
/**
* Type of the product. 0 for product 1 for service
* @var int
*/
public $product_type = 0;
/**
* Product ref
* @var string
*/
public $product_ref;
/**
* Product label
* @var string
*/
public $product_label;
/**
* Product description
* @var string
*/
public $product_desc;
/**
* Quantity
* @var float
*/
public $qty;
/**
* Unit price before taxes
* @var float
*/
public $subprice;
/**
* Unit price before taxes
* @var float
* @deprecated
*/
public $price;
/**
* Id of corresponding product
* @var int
*/
public $fk_product;
/**
* VAT code
* @var string
*/
public $vat_src_code;
/**
* VAT % Vat rate can be like "21.30 (CODE)"
* @var float|string
*/
public $tva_tx;
/**
* Local tax 1 %
* @var float
*/
public $localtax1_tx;
/**
* Local tax 2 %
* @var float
*/
public $localtax2_tx;
/**
* Local tax 1 type
* @var int<0,6> From 1 to 6, or 0 if not found
* @see getLocalTaxesFromRate()
*/
public $localtax1_type;
/**
* Local tax 2 type
* @var int<0,6> From 1 to 6, or 0 if not found
* @see getLocalTaxesFromRate()
*/
public $localtax2_type;
/**
* Percent of discount
* @var float
*/
public $remise_percent;
/**
* Fixed discount
* @var float
* @deprecated
*/
public $remise;
/**
* Total amount before taxes
* @var float
*/
public $total_ht;
/**
* Total VAT amount
* @var float
*/
public $total_tva;
/**
* Total local tax 1 amount
* @var float
*/
public $total_localtax1;
/**
* Total local tax 2 amount
* @var float
*/
public $total_localtax2;
/**
* Total amount with taxes
* @var float
*/
public $total_ttc;
/**
* @var float|null
*/
public $revenuestamp;
/**
* @var int<0,1>
*/
public $date_start_fill; // If set to 1, when invoice is created from a template invoice, it will also auto set the field date_start at creation
/**
* @var int<0,1>
*/
public $date_end_fill; // If set to 1, when invoice is created from a template invoice, it will also auto set the field date_end at creation
/**
* @var float
*/
public $buy_price_ht;
/**
* @var float
* @deprecated For backward compatibility
*/
public $buyprice;
/**
* @var float|int|string
* @deprecated For backward compatibility
*/
public $pa_ht;
/**
* @var string
*/
public $marge_tx;
/**
* @var string
*/
public $marque_tx;
/**
* List of cumulative options:
* Bit 0: 0 for common VAT - 1 if VAT french NPR
* Bit 1: 0 si ligne normal - 1 si bit discount (link to line into llx_remise_except)
* @var int
*/
public $info_bits = 0;
/**
* List of special options to define line:
* 1: shipment cost lines
* 2: ecotaxe
* 3: ??
* id of module: a meaning for the module
* @var int
*/
public $special_code = 0;
/**
* @var int
* @deprecated Use user_creation_id
*/
public $fk_user_author;
/**
* @var int
* @deprecated Use user_modification_id
*/
public $fk_user_modif;
/**
* @var int
*/
public $fk_accounting_account;
}