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:
Eduard Wisch 2026-03-10 15:52:20 +01:00
parent 33ecb49fc4
commit b552b90303
5 changed files with 261 additions and 2 deletions

147
card.php
View file

@ -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 ' &nbsp; ';
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)

View file

@ -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

View file

@ -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';

View file

@ -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

View file

@ -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