Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 61fd623b

Von Cem Aydin vor mehr als 1 Jahr hinzugefügt

  • ID 61fd623b958483cc57c328c3f3305c633b824473
  • Vorgänger e1e09249
  • Nachfolger 73126531

Schweizer QR-Rechnung: Scan Funktion in mobile design eingebaut

Schweizer QR-Rechnungen können via mobile design eingescannt werden.

Die eingescannten Daten werden in einer Maske angezeigt und können
dann als Kreditorbuchung gespeichert werden.

Zum speichern wird die post funktion aus ap.pl verwendet.

- Menüpunkt im mobile design hinzugefügt
- Neue mobile Templates und controller hinzugefügt
- ap.pl: Anpassung redirect nach post
- AP.pm: QR-Code Daten in Kreditorenbuchung speichern (Verwendung von
URI::Escape, da es sich um mehrzeilige Daten mit Sonderzeichen handelt,
welche über die form weitergegeben werden.)

Unterschiede anzeigen:

SL/AP.pm
use Data::Dumper;
use List::Util qw(sum0);
use strict;
use URI::Escape;
sub post_transaction {
my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
......
transdate = ?, ordnumber = ?, vendor_id = ?, taxincluded = ?,
amount = ?, duedate = ?, deliverydate = ?, tax_point = ?, paid = ?, netamount = ?,
currency_id = (SELECT id FROM currencies WHERE name = ?), notes = ?, department_id = ?, storno = ?, storno_id = ?,
globalproject_id = ?, direct_debit = ?, payment_id = ?, transaction_description = ?, intnotes = ?
globalproject_id = ?, direct_debit = ?, payment_id = ?, transaction_description = ?, intnotes = ?,
qrbill_data = ?
WHERE id = ?|;
@values = ($form->{invnumber}, conv_date($form->{transdate}),
$form->{ordnumber}, conv_i($form->{vendor_id}),
......
$form->{direct_debit} ? 't' : 'f',
conv_i($form->{payment_id}), $form->{transaction_description},
$form->{intnotes},
$form->{qrbill_data_encoded} ? uri_unescape($form->{qrbill_data_encoded}) : undef,
$form->{id});
do_query($form, $dbh, $query, @values);
SL/Controller/ScanQRBill.pm
package SL::Controller::ScanQRBill;
use strict;
use parent qw(SL::Controller::Base);
use List::Util qw(first);
use URI::Escape;
use SL::Helper::QrBillParser;
use SL::DB::Vendor;
use SL::DB::Chart;
use SL::DB::Tax;
use SL::DB::ValidityToken;
use Rose::Object::MakeMethods::Generic(
#scalar => [ qw() ],
'scalar --get_set_init' => [ qw(vendors accounts_AP_amount accounts_AP taxcharts) ],
);
# check permissions
__PACKAGE__->run_before(sub { $::auth->assert('ap_transactions'); });
################ actions #################
sub action_scan_view {
my ($self) = @_;
$::request->layout->add_javascripts('html5-qrcode.js');
$::request->layout->add_javascripts('kivi.ScanQRBill.js');
$self->render('scan_qrbill/scan_view',
transaction_success => $::form->{transaction_success} // '0',
invoice_number => $::form->{invnumber} // '',
developer => $::auth->assert('developer', 1) ? '1' : '0',
);
}
sub action_handle_scan_result {
my ($self) = @_;
my $qrtext = $::form->{qrtext};
# load text into object
$self->{qr_obj} = SL::Helper::QrBillParser->new($qrtext);
# check if valid qr-bill
if (!$self->{qr_obj}->is_valid) {
return $self->js
->run('kivi.ScanQRBill.popupInvalidQRBill', $self->{qr_obj}->error)
->render();
}
my $vendor_name = $self->{qr_obj}->{creditor}->{name};
$self->{vendor} = first { $_->{name} eq $vendor_name } @{ $self->vendors };
if (!$self->{vendor}) {
return $self->js
->run('kivi.ScanQRBill.popupVendorNotFound', $vendor_name)
->render();
}
$self->prepare_add_purchase_transaction();
}
################# internal ###############
sub prepare_add_purchase_transaction {
my ($self) = @_;
my $qr_obj = $self->{qr_obj};
my $token = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_PURCHASE_INVOICE_POST())->token;
my $html = $self->render('scan_qrbill/_add_purchase_transaction',
{ output => 0 },
vendor => {
name => $self->{vendor}->{name},
number => $self->{vendor}->{vendornumber},
id => $self->{vendor}->{id},
},
qrbill => {
unstructured_message => $qr_obj->{additional_information}->{unstructured_message},
reference_type => $qr_obj->{payment_reference}->{reference_type},
reference => $qr_obj->{payment_reference}->{reference},
amount => $qr_obj->{payment_amount_information}->{amount},
currency => $qr_obj->{payment_amount_information}->{currency},
data_encoded => uri_escape($qr_obj->raw_data),
},
accounts_AP_amount => $self->accounts_AP_amount,
accounts_AP => $self->accounts_AP,
taxcharts => $self->taxcharts,
form_validity_token => $token,
);
$self->js->html('#main-content', $html)->render();
}
sub init_vendors {
SL::DB::Manager::Vendor->get_all();
}
sub init_accounts_AP_amount {
[ map { {
text => "$_->{accno} - $_->{description}",
accno => $_->{accno},
id => $_->{id},
chart_id => $_->{id},
} } @{ SL::DB::Manager::Chart->get_all(
query => [ SL::DB::Manager::Chart->link_filter('AP_amount') ],
sort_by => 'id ASC') }
];
}
sub init_accounts_AP {
[ map { {
text => "$_->{accno} - $_->{description}",
accno => $_->{accno},
id => $_->{id},
chart_id => $_->{id},
} } @{ SL::DB::Manager::Chart->get_all(
query => [ SL::DB::Manager::Chart->link_filter('AP') ],
sort_by => 'id ASC') }
];
}
sub init_taxcharts {
[ map { {
text => "$_->{taxkey} - $_->{taxdescription} " . ($_->{rate} * 100) .' %',
id => "$_->{id}--$_->{rate}",
} } @{ SL::DB::Manager::Tax->get_all(
where => [ chart_categories => { like => '%E%' }],
sort_by => 'taxkey, rate') }
];
}
1;
__END__
=pod
=encoding utf-8
=head1 NAME
SL::Controller::ScanQRBill - Controller for scanning swiss QR-Bills using the mobile template
=head1 DESCRIPTION
Renders the scan view in the mobile template and handles the scan result.
The scanned QR-Bill data is parsed and the vendor is searched in the database.
If everything is valid an add purchase transaction view is rendered and
the QR-Bill can be saved as a purchase transaction.
The post function from ap.pl is used to save the purchase transaction.
The raw data of the QR-Bill is stored with the purchase transaction in the ap table
in the field qrbill_data.
The data can later be accessed again using the parser module SL::Helper::QrBillParser.
=head1 SECURITY CONSIDERATIONS
In theory an attacker could try to insert a malicious Javascript code into a qr code,
that is then scanned, and redisplayed in the browser (XSS).
Therefore it is important to escape any data coming from the qr code when it is rendered
in the templates. For this we use the template toolkit html filter: [% qrdata | html %],
Jquery's text function: $('#qrdata').text(qrdata);, and URI::Escape; for the raw data.
For database insertion we use prepared statements (AP.pm).
=head1 TESTING
To simplify testing the scan view shows some buttons to send example qr codes, when in
developer mode. Sending is implemented in Javascript in js/kivi.ScanQRBill.js.
=head1 URL ACTIONS
=over 4
=item C<scan_view>
Renders the scan view in the mobile template.
=item C<handle_scan_result>
Handles the scan result and renders the add purchase transaction view.
=back
=head1 TODO
=head2 Additional features:
=over 4
=item * automatically extract invoice number and dates etc. from "SWICO-String" if present
=item * Option to add the vendor if not found
=back
=head1 BUGS
Nothing here yet.
=head1 AUTHOR
Cem Aydin E<lt>cem.aydin@revamp-it.chE<gt>
=cut
bin/mozilla/ap.pl
SL::Helper::Flash::flash_later('info', $msg);
print $form->redirect_header($form->{callback});
$::dispatcher->end_request;
} elsif ($form->{callback} =~ /ScanQRCode/) {
# callback/redirect when coming from mobile view (swiss qr bill scan)
print $form->redirect_header(build_std_url(
"script=controller.pl",
'action=ScanQRBill/scan_view',
'transaction_success=1',
'invnumber=' . E($form->{invnumber})
));
$::dispatcher->end_request;
} elsif ('doc-tab' eq $form->{after_action}) {
# Redirect with callback containing a fragment does not work (by now)
# because the callback info is stored in the session an parsing the
js/kivi.ScanQRBill.js
namespace('kivi.ScanQRBill', function(ns) {
ns.onScanSuccess = async (decodedText, decodedResult) => {
// stop camera
await html5Qrcode.stop();
// send the scanned text to the server
const data = [];
data.push({ name: 'qrtext', value: decodedText });
data.push({ name: 'action', value: 'ScanQRBill/handle_scan_result' });
$.post("controller.pl", data, kivi.eval_json_result);
}
ns.onScanFailure = (error) => {
// handle scan failure, usually better to ignore and keep scanning.
//console.warn(`Code scan error = ${error}`);
}
ns.popupInvalidQRBill = (error) => {
console.warn('popupInvalidQRBill', error);
$('#qr_code_invalid_error').text(error);
$('#qr_code_invalid_modal').modal('open');
}
ns.popupVendorNotFound = (vn) => {
//console.warn('popupVendorNotFound', vn);
$('#vendor_name').text(vn);
$('#vendor_not_found_error').modal('open');
}
ns.sendTestCode = async (code) => {
// function to easily send code without scanning
// use for testing only
if (html5Qrcode.isScanning) {
// stop camera
await html5Qrcode.stop();
}
const data = [];
const codes = [
"SPC\r\n0200\r\n1\r\nCH4431999123000889012\r\nS\r\nMax Muster & Söhne\r\nMusterstrasse\r\n123\r\n8000\r\nSeldwyla\r\nCH\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n1949.75\r\nCHF\r\nS\r\nSimon Muster\r\nMusterstrasse\r\n1\r\n8000\r\nSeldwyla\r\nCH\r\nQRR\r\n210000000003139471430009017\r\nOrder from 15.10.2020\r\nEPD\r\n//S1/10/1234/11/201021/30/102673386/32/7.7/40/0:30\r\nName AV1: UV;UltraPay005;12345\r\nName AV2: XY;XYService;54321",
"SPC\n0200\n1\nCH5800791123000889012\nS\nMuster Krankenkasse\nMusterstrasse\n12\n8000\nSeldwyla\nCH\n\n\n\n\n\n\n\n211.00\nCHF\nS\nSarah Beispiel\nMusterstrasse\n1\n8000\nSeldwyla\nCH\nSCOR\nRF240191230100405JSH0438\n\nEPD\n",
"SPC\n0200\n1\nCH5800791123000889012\nS\nMax Muster & Söhne\nMusterstrasse\n123\n8000\nSeldwyla\nCH\n\n\n\n\n\n\n\n199.95\nCHF\nS\nSarah Beispiel\nMusterstrasse\n1\n78462\nKonstanz\nDE\nSCOR\nRF18539007547034\n\nEPD\n",
// for testing XSS
"SPC\r\n0200\r\n1\r\nCH4431999123000889012\r\nS\r\nMax Muster & Söhne\r\nMusterstrasse\r\n123\r\n8000\r\nSeldwyla\r\nCH\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n1949.75\r\nCHF\r\nS\r\nSimon Muster\r\nMusterstrasse\r\n1\r\n8000\r\nSeldwyla\r\nCH\r\nQRR\r\n210000000003139471430009017\r\nOrder from 15.10.2020\r\nEPD\r\n//S1/10/1234/11/201021/30/102673386/32/7.7/40/0:30<script>alert('XSS!');</script>\r\nName AV1: UV;UltraPay005;12345\r\nName AV2: XY;XYService;54321",
"<script>alert('XSS!');</script>",
];
data.push({ name: 'qrtext', value: codes[code] });
data.push({ name: 'action', value: 'ScanQRBill/handle_scan_result' });
$.post("controller.pl", data, kivi.eval_json_result);
}
});
menus/mobile/00-erp.yaml
params:
action: ImageUpload/upload_image
object_type: sales_delivery_order
- id: scan_qrbill
name: Scan swiss QR bill
order: 200
access: ap_transactions
params:
action: ScanQRBill/scan_view
- id: component_test
name: Component Test
order: 200
order: 300
access: developer
params:
action: MaterializeTest/components
- id: modal_test
name: Modal Test
order: 300
order: 400
access: developer
params:
action: MaterializeTest/modal
templates/mobile_webpages/scan_qrbill/_add_purchase_transaction.html
[% USE LxERP %]
[% USE L %]
[% USE HTML %]
[% USE P %]
[% USE T8 %]
<div class="container">
<div class="row">
<form method="post" action="ap.pl?action=post" id="form" class="col s12">
[% L.hidden_tag("rowcount", "1") %]
[% L.hidden_tag('form_validity_token', form_validity_token) %]
[% L.hidden_tag('callback', 'ScanQRCode') %]
[% L.hidden_tag('paidaccounts', '1') %]
[% L.hidden_tag('taxincluded', '1') %]
[% L.hidden_tag('qrbill_data_encoded', qrbill.data_encoded) %]
<h4>[% 'Add Accounts Payables Transaction' | $T8 %]</h4>
<div class="card">
<div class="card-content">
<span class="card-title">[% 'Vendor & Order' | $T8 %]</span>
<p>
[% 'Vendor' | $T8 %]: [% vendor.number | html %] [% vendor.name | html %]
[% L.hidden_tag("vendor_id", vendor.id) %]
[% L.hidden_tag("previous_vendor_id", vendor.id) %]
</p>
<p>
[% 'Currency' | $T8 %]: [% qrbill.currency | html %]
[% L.hidden_tag("currency", qrbill.currency) %]
</p>
</div>
</div>
<h5>[% 'Notes' | $T8 %]</h5>
<div class="row">
[% P.M.textarea_tag("notes", notes, label=LxERP.t8('Notes'), class="col s12") %]
[% P.M.textarea_tag("intnotes", intnotes, label=LxERP.t8('Internal Notes'), class="col s12") %]
</div>
<h5>[% 'Numbers & Dates' | $T8 %]</h5>
<div class="row">
[% P.M.input_tag("invnumber", qrbill.unstructured_message, label=LxERP.t8('Invoice Number'), class="col s12") %]
[% P.M.input_tag("ordnumber", '', label=LxERP.t8('Order Number'), class="col s12") %]
[% P.M.date_tag('transdate', '', label=LxERP.t8('Invoice Date'), icon="date_range", class="col s12") %]
[% P.M.date_tag('duedate', '', label=LxERP.t8('Due Date'), icon="date_range", class="col s12") %]
</div>
<h5>[% 'Transactions' | $T8 %]</h5>
<div class="row">
[% P.M.select_tag("AP_amount_chart_id_1", accounts_AP_amount, title_key='text'
label=LxERP.t8('Account'), class="col s12") %]
[% L.hidden_tag("AP_amount_chart_id_1_type", "AP_amount") %]
[% P.M.input_tag("amount_1", qrbill.amount, label=LxERP.t8('Amount'), class="col s12") %]
[% P.M.select_tag("taxchart_1", taxcharts, title_key='text'
label=LxERP.t8('Taxkey'), class="col s12") %]
[% P.M.select_tag("AP_chart_id", accounts_AP, title_key='text'
label=LxERP.t8('Contra Account'), class="col s12") %]
[% L.hidden_tag("AP_chart_id_type", "AP") %]
</div>
<div class="row">
[% P.M.submit_tag("", LxERP.t8('Post'), class="col s12") %]
</div>
<div class="row">
[% P.M.button_tag("", LxERP.t8('Cancel'), class="col s12", flat=1, small=1, href="controller.pl?action=ScanQRBill/scan_view") %]
</div>
</form>
</div>
</div>
templates/mobile_webpages/scan_qrbill/scan_view.html
[% USE LxERP %]
[% USE L %]
[% USE HTML %]
[% USE P %]
[% USE T8 %]
<div id="main-content">
<div class="container">
<div class="row">
<h4>[% 'Scan swiss QR bill' | $T8 %]</h4>
<div id="QRreader" width="600px"></div>
[% IF developer %]
<p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(0);", "sendTestCode 0" ) %]</p>
<p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(1);", "sendTestCode 1" ) %]</p>
<p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(2);", "sendTestCode 2" ) %]</p>
<p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(3);", "sendTestCode 3 XSS" ) %]</p>
<p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(4);", "sendTestCode 4 XSS (invalid)" ) %]</p>
[% END %]
</div>
</div>
<hr>
</div>
<div id="qr_code_invalid_modal" class="modal">
<div class="modal-content">
<h4>[% 'QR-Code invalid' | $T8 %]</h4>
<p>[% 'The scanned code is not a valid QR bill.' | $T8 %]</p>
<p>[% 'Error' | $T8 %]: <span id="qr_code_invalid_error"></span></p>
</div>
<div class="modal-footer">
[% P.M.button_tag("startCamera();", LxERP.t8("Try again"), class="modal-close") %]
</div>
</div>
<div id="vendor_not_found_error" class="modal">
<div class="modal-content">
<h4>[% 'Vendor not found' | $T8 %]</h4>
<p>[% 'The vendor could not be found. Please register the vendor with the exact name from the QR bill as shown below.' | $T8 %]</p>
<p>[% 'Vendor Name' | $T8 %]: '<span id="vendor_name"></span>'</p>
</div>
<div class="modal-footer">
[% P.M.button_tag("startCamera();", LxERP.t8("Try again"), class="modal-close") %]
</div>
</div>
<div id="transaction_successful_modal" class="modal">
<div class="modal-content">
<h4>[% 'AP transaction posted successfully' | $T8 %]</h4>
<p>[% 'Invoice number' | $T8 %]: [% invoice_number | html %]</p>
</div>
<div class="modal-footer">
[% P.M.button_tag("", LxERP.t8("Ok"), class="modal-close") %]
</div>
</div>
[% L.hidden_tag("transaction_success", transaction_success) %]
<script>
const html5Qrcode = new Html5Qrcode("QRreader",
{ formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE ] });
const startCamera = () => {
html5Qrcode.start({ facingMode: "environment" },
{ fps: 10, qrbox: { width: 250, height: 250 } },
kivi.ScanQRBill.onScanSuccess, kivi.ScanQRBill.onScanFailure
);
};
window.onload = () => {
// using $(document).ready didn't work here
//$(document).ready(() => {
if ($('#transaction_success').val() === '1') {
$('#transaction_successful_modal').modal('open');
}
startCamera();
};
</script>

Auch abrufbar als: Unified diff