feat: PaymentVarious - Zahlung anlegen für Transaktionen ohne Rechnung
Neuer Button "Zahlung anlegen" auf der Transaktions-Detailseite (card.php) für Bankbuchungen ohne zugehörige Rechnung (z.B. Steuer-Erstattungen, private Umbuchungen, sonstige Zahlungen). - Inline-Formular mit Buchungskonto (Select2), Nebenbuchkonto, Buchungsseite (Soll/Haben) und Label - Buchungsseite wird automatisch aus Vorzeichen ermittelt - Erstellt PaymentVarious mit Bank-Eintrag und bank_url-Verknüpfung - Transaktion wird auf MATCHED gesetzt - Anzeige der sonstigen Zahlung mit Link bei gematchten Transaktionen - Fix: update() speichert jetzt fk_user_match und date_match korrekt - Fix: Leeres Nebenbuchkonto wird nicht mehr als -1 gespeichert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
33ecb49fc4
commit
b552b90303
5 changed files with 261 additions and 2 deletions
147
card.php
147
card.php
|
|
@ -337,6 +337,32 @@ if ($action == 'searchinvoice' && $object->id > 0) {
|
|||
}
|
||||
}
|
||||
|
||||
// Create various payment (for transactions without invoices)
|
||||
if ($action == 'confirmcreatevarious' && $object->id > 0 && $object->status == BankImportTransaction::STATUS_NEW) {
|
||||
$accountancyCode = GETPOST('accountancy_code', 'alpha');
|
||||
$subledgerAccount = GETPOST('subledger_account', 'alpha');
|
||||
$sens = GETPOSTINT('sens');
|
||||
$variousLabel = GETPOST('various_label', 'alphanohtml');
|
||||
|
||||
if (empty($accountancyCode)) {
|
||||
setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentities("AccountingAccount")), null, 'errors');
|
||||
} else {
|
||||
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
if (empty($bankAccountId)) {
|
||||
setEventMessages($langs->trans("ErrorNoBankAccountConfigured"), null, 'errors');
|
||||
} else {
|
||||
$result = $object->createVariousPayment($user, $bankAccountId, $accountancyCode, $sens, $variousLabel, $subledgerAccount);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans("VariousPaymentCreated"), null, 'mesgs');
|
||||
header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id);
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($object->error, null, 'errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
|
@ -563,6 +589,36 @@ if ($object->id > 0) {
|
|||
}
|
||||
}
|
||||
|
||||
// Various payment (no invoice, linked via bank_url)
|
||||
if (empty($object->fk_paiement) && empty($object->fk_paiementfourn) && $object->fk_bank > 0) {
|
||||
$sql = "SELECT url_id FROM ".MAIN_DB_PREFIX."bank_url WHERE fk_bank = ".((int) $object->fk_bank)." AND type = 'payment_various'";
|
||||
$resql = $db->query($sql);
|
||||
if ($resql && $db->num_rows($resql) > 0) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/paymentvarious.class.php';
|
||||
$various = new PaymentVarious($db);
|
||||
$various->fetch($obj->url_id);
|
||||
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans("Payment").'</td>';
|
||||
print '<td>';
|
||||
print '<a href="'.DOL_URL_ROOT.'/compta/bank/various_payment/card.php?id='.$various->id.'">';
|
||||
print img_picto('', 'payment', 'class="pictofixedwidth"');
|
||||
print $langs->trans("VariousPayment").' #'.$various->id;
|
||||
print '</a>';
|
||||
print ' <span class="opacitymedium">('.dol_print_date($various->datep, 'day').' - '.price($various->amount, 0, $langs, 1, -1, 2, 'EUR').')</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
if (!empty($various->accountancy_code)) {
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans("AccountingAccount").'</td>';
|
||||
print '<td>'.dol_escape_htmltag($various->accountancy_code).'</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no payment link but invoice link exists (edge case)
|
||||
if (empty($object->fk_paiement) && empty($object->fk_paiementfourn)) {
|
||||
if ($object->fk_facture_fourn > 0) {
|
||||
|
|
@ -677,6 +733,9 @@ if ($object->id > 0) {
|
|||
// Find matches button
|
||||
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=findmatches&token='.newToken().'">'.$langs->trans("FindMatches").'</a>';
|
||||
|
||||
// Create various payment button
|
||||
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=createvarious&token='.newToken().'">'.$langs->trans("CreateVariousPayment").'</a>';
|
||||
|
||||
// Set as ignored
|
||||
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=setstatus&status='.BankImportTransaction::STATUS_IGNORED.'&token='.newToken().'">'.$langs->trans("SetAsIgnored").'</a>';
|
||||
}
|
||||
|
|
@ -693,6 +752,94 @@ if ($object->id > 0) {
|
|||
|
||||
print '</div>';
|
||||
|
||||
// Various payment inline form
|
||||
if ($action == 'createvarious' && $object->status == BankImportTransaction::STATUS_NEW) {
|
||||
// Auto-detect sens from amount sign: positive = credit (1), negative = debit (0)
|
||||
$defaultSens = ($object->amount >= 0) ? 1 : 0;
|
||||
$defaultLabel = $object->name.' - '.dol_trunc($object->description, 80);
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formaccounting.class.php';
|
||||
$formaccounting = new FormAccounting($db);
|
||||
|
||||
print '<br>';
|
||||
print load_fiche_titre($langs->trans("CreateVariousPayment"), '', 'object_payment');
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="id" value="'.$object->id.'">';
|
||||
print '<input type="hidden" name="action" value="confirmcreatevarious">';
|
||||
|
||||
print '<table class="border centpercent">';
|
||||
|
||||
// Amount (read-only)
|
||||
print '<tr>';
|
||||
print '<td class="titlefield fieldrequired">'.$langs->trans("Amount").'</td>';
|
||||
print '<td>';
|
||||
$amountColor = ($object->amount >= 0) ? 'green' : 'red';
|
||||
print '<span style="font-weight: bold; color: '.$amountColor.';">'.price($object->amount, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Date (read-only)
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans("Date").'</td>';
|
||||
print '<td>'.dol_print_date($object->date_trans, 'day').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Counterparty (read-only)
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans("Counterparty").'</td>';
|
||||
print '<td>'.dol_escape_htmltag($object->name).'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Accounting account (Select2 search)
|
||||
print '<tr>';
|
||||
print '<td class="fieldrequired">'.$langs->trans("AccountingAccount").'</td>';
|
||||
print '<td>';
|
||||
print $formaccounting->select_account(GETPOST('accountancy_code', 'alpha'), 'accountancy_code', 1, array(), 0, 0, 'minwidth200 maxwidth500', '', 1);
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Subledger account (Select2 search)
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans("SubledgerAccount").'</td>';
|
||||
print '<td>';
|
||||
print $formaccounting->select_auxaccount(GETPOST('subledger_account', 'alpha'), 'subledger_account', 1, 'minwidth200 maxwidth500');
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Debit/Credit (auto-selected)
|
||||
print '<tr>';
|
||||
print '<td class="fieldrequired">'.$langs->trans("DebitCredit").'</td>';
|
||||
print '<td>';
|
||||
$sensValue = GETPOSTISSET('sens') ? GETPOSTINT('sens') : $defaultSens;
|
||||
print '<select name="sens" class="flat minwidth100">';
|
||||
print '<option value="0"'.($sensValue == 0 ? ' selected' : '').'>'.$langs->trans("Debit").' ('.$langs->trans("Expense").')</option>';
|
||||
print '<option value="1"'.($sensValue == 1 ? ' selected' : '').'>'.$langs->trans("Credit").' ('.$langs->trans("Income").')</option>';
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Label (editable, pre-filled)
|
||||
print '<tr>';
|
||||
print '<td class="fieldrequired">'.$langs->trans("Label").'</td>';
|
||||
print '<td>';
|
||||
$labelValue = GETPOSTISSET('various_label') ? GETPOST('various_label', 'alphanohtml') : $defaultLabel;
|
||||
print '<input type="text" name="various_label" class="flat minwidth300 maxwidth500" value="'.dol_escape_htmltag($labelValue).'">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print '<div class="center" style="margin-top: 10px;">';
|
||||
print '<input type="submit" class="button button-save" value="'.$langs->trans("Save").'">';
|
||||
print ' ';
|
||||
print '<a class="button button-cancel" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'">'.$langs->trans("Cancel").'</a>';
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
}
|
||||
|
||||
// Manual invoice selection (only for new transactions)
|
||||
if ($object->status == BankImportTransaction::STATUS_NEW) {
|
||||
// Determine if this is likely a supplier payment (negative amount) or customer (positive)
|
||||
|
|
|
|||
|
|
@ -455,7 +455,9 @@ class BankImportTransaction extends CommonObject
|
|||
$sql .= " status = ".((int) $this->status).",";
|
||||
$sql .= " fk_user_modif = ".((int) $user->id).",";
|
||||
$sql .= " note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").",";
|
||||
$sql .= " note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
|
||||
$sql .= " note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL").",";
|
||||
$sql .= " fk_user_match = ".($this->fk_user_match > 0 ? ((int) $this->fk_user_match) : "NULL").",";
|
||||
$sql .= " date_match = ".($this->date_match ? "'".$this->db->idate($this->date_match)."'" : "NULL");
|
||||
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||
|
||||
dol_syslog(get_class($this)."::update", LOG_DEBUG);
|
||||
|
|
@ -1538,6 +1540,88 @@ class BankImportTransaction extends CommonObject
|
|||
return -4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a various payment (PaymentVarious) for this transaction
|
||||
* Used for transactions without invoices (tax refunds, internal transfers, etc.)
|
||||
*
|
||||
* @param User $user User performing the action
|
||||
* @param int $bankAccountId Dolibarr bank account ID
|
||||
* @param string $accountancyCode Accounting account code (e.g. '1780')
|
||||
* @param int $sens 0=debit, 1=credit
|
||||
* @param string $label Payment label
|
||||
* @param string $subledgerAccount Subledger account (optional)
|
||||
* @param int $typePayment Payment type ID (0 = auto-detect VIR)
|
||||
* @return int >0 if OK (PaymentVarious ID), <0 if error
|
||||
*/
|
||||
public function createVariousPayment($user, $bankAccountId, $accountancyCode, $sens, $label, $subledgerAccount = '', $typePayment = 0)
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
$error = 0;
|
||||
$this->db->begin();
|
||||
|
||||
// Look up payment type ID for 'VIR' (bank transfer) if not provided
|
||||
if (empty($typePayment)) {
|
||||
$sql = "SELECT id FROM ".MAIN_DB_PREFIX."c_paiement WHERE code = 'VIR' AND active = 1";
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$typePayment = (int) $obj->id;
|
||||
}
|
||||
if (empty($typePayment)) {
|
||||
$this->error = 'Payment type VIR not found in c_paiement';
|
||||
$this->db->rollback();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/paymentvarious.class.php';
|
||||
|
||||
$various = new PaymentVarious($this->db);
|
||||
$various->datep = $this->date_trans;
|
||||
$various->datev = $this->date_value ?: $this->date_trans;
|
||||
$various->sens = $sens;
|
||||
$various->amount = abs($this->amount);
|
||||
$various->type_payment = $typePayment;
|
||||
$various->num_payment = $this->end_to_end_id ?: $this->ref;
|
||||
$various->label = $label;
|
||||
$various->accountancy_code = $accountancyCode;
|
||||
$various->subledger_account = !empty($subledgerAccount) ? $subledgerAccount : '';
|
||||
$various->fk_account = $bankAccountId;
|
||||
$various->note = $langs->trans("PaymentCreatedByBankImport").' - '.$this->name.' - '.dol_trunc($this->description, 100);
|
||||
|
||||
$variousId = $various->create($user);
|
||||
if ($variousId < 0) {
|
||||
$this->error = $various->error;
|
||||
$this->errors = $various->errors;
|
||||
$error++;
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
// Get the bank line ID created by PaymentVarious
|
||||
$sql = "SELECT fk_bank FROM ".MAIN_DB_PREFIX."payment_various WHERE rowid = ".((int) $variousId);
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$this->fk_bank = (int) $obj->fk_bank;
|
||||
}
|
||||
|
||||
// Update transaction status to MATCHED
|
||||
$this->status = self::STATUS_MATCHED;
|
||||
$this->fk_user_match = $user->id;
|
||||
$this->date_match = dol_now();
|
||||
$this->update($user);
|
||||
}
|
||||
|
||||
if ($error) {
|
||||
$this->db->rollback();
|
||||
return -2;
|
||||
}
|
||||
|
||||
$this->db->commit();
|
||||
return $variousId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm payment for multiple invoices (batch payment)
|
||||
* Creates a single payment that covers multiple invoices
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class modBankImport extends DolibarrModules
|
|||
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@bankimport'
|
||||
|
||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||
$this->version = '3.5';
|
||||
$this->version = '3.7';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
|
|||
|
|
@ -404,3 +404,17 @@ StatementsNotPdfFormat = Kontoauszüge empfangen, aber nicht im PDF-Format
|
|||
StatementsUsingHKEKA = Nutze HKEKA (generischer Kontoauszug) statt HKEKP
|
||||
StatementsUsingHKEKP = Nutze HKEKP (PDF-Kontoauszug)
|
||||
NeitherHKEKPnorHKEKA = Die Bank unterstützt weder HKEKP noch HKEKA für elektronische Kontoauszüge
|
||||
|
||||
#
|
||||
# Sonstige Zahlung (PaymentVarious)
|
||||
#
|
||||
CreateVariousPayment = Zahlung anlegen
|
||||
AccountingAccount = Buchungskonto
|
||||
SubledgerAccount = Nebenbuchkonto
|
||||
DebitCredit = Buchungsseite
|
||||
Debit = Soll
|
||||
Credit = Haben
|
||||
Expense = Ausgabe
|
||||
Income = Einnahme
|
||||
VariousPaymentCreated = Sonstige Zahlung erfolgreich erstellt
|
||||
VariousPayment = Sonstige Zahlung
|
||||
|
|
|
|||
|
|
@ -301,3 +301,17 @@ StatementsUsingHKEKP = Using HKEKP (PDF statement)
|
|||
NeitherHKEKPnorHKEKA = The bank supports neither HKEKP nor HKEKA for electronic statements
|
||||
BankImportAutoFetch = Automatic bank import (transactions)
|
||||
BankImportFetchPdfStatements = Automatic PDF statement retrieval
|
||||
|
||||
#
|
||||
# Various Payment (PaymentVarious)
|
||||
#
|
||||
CreateVariousPayment = Create Payment
|
||||
AccountingAccount = Accounting Account
|
||||
SubledgerAccount = Subledger Account
|
||||
DebitCredit = Debit/Credit
|
||||
Debit = Debit
|
||||
Credit = Credit
|
||||
Expense = Expense
|
||||
Income = Income
|
||||
VariousPaymentCreated = Various payment created successfully
|
||||
VariousPayment = Various Payment
|
||||
|
|
|
|||
Loading…
Reference in a new issue