Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 6913a2aa

Von Cem Aydin vor etwa 1 Jahr hinzugefügt

  • ID 6913a2aadea4a708f413d744bf0f1a160566cb1b
  • Vorgänger a616be07
  • Nachfolger 570414f3

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
50 50
use Data::Dumper;
51 51
use List::Util qw(sum0);
52 52
use strict;
53
use URI::Escape;
53 54

  
54 55
sub post_transaction {
55 56
  my ($self, $myconfig, $form, $provided_dbh, %params) = @_;
......
161 162
                transdate = ?, ordnumber = ?, vendor_id = ?, taxincluded = ?,
162 163
                amount = ?, duedate = ?, deliverydate = ?, tax_point = ?, paid = ?, netamount = ?,
163 164
                currency_id = (SELECT id FROM currencies WHERE name = ?), notes = ?, department_id = ?, storno = ?, storno_id = ?,
164
                globalproject_id = ?, direct_debit = ?, payment_id = ?, transaction_description = ?, intnotes = ?
165
                globalproject_id = ?, direct_debit = ?, payment_id = ?, transaction_description = ?, intnotes = ?,
166
                qrbill_data = ?
165 167
               WHERE id = ?|;
166 168
    @values = ($form->{invnumber}, conv_date($form->{transdate}),
167 169
                  $form->{ordnumber}, conv_i($form->{vendor_id}),
......
174 176
                  $form->{direct_debit} ? 't' : 'f',
175 177
                  conv_i($form->{payment_id}), $form->{transaction_description},
176 178
                  $form->{intnotes},
179
                  $form->{qrbill_data_encoded} ? uri_unescape($form->{qrbill_data_encoded}) : undef,
177 180
                  $form->{id});
178 181
    do_query($form, $dbh, $query, @values);
179 182

  
SL/Controller/ScanQRBill.pm
1
package SL::Controller::ScanQRBill;
2

  
3
use strict;
4
use parent qw(SL::Controller::Base);
5

  
6
use List::Util qw(first);
7
use URI::Escape;
8

  
9
use SL::Helper::QrBillParser;
10
use SL::DB::Vendor;
11
use SL::DB::Chart;
12
use SL::DB::Tax;
13
use SL::DB::ValidityToken;
14

  
15
use Rose::Object::MakeMethods::Generic(
16
  #scalar => [ qw() ],
17
  'scalar --get_set_init' => [ qw(vendors accounts_AP_amount accounts_AP taxcharts) ],
18
);
19

  
20
# check permissions
21
__PACKAGE__->run_before(sub { $::auth->assert('ap_transactions'); });
22

  
23
################ actions #################
24

  
25
sub action_scan_view {
26
  my ($self) = @_;
27

  
28
  $::request->layout->add_javascripts('html5-qrcode.js');
29
  $::request->layout->add_javascripts('kivi.ScanQRBill.js');
30

  
31
  $self->render('scan_qrbill/scan_view',
32
    transaction_success => $::form->{transaction_success} // '0',
33
    invoice_number => $::form->{invnumber} // '',
34
    developer => $::auth->assert('developer', 1) ? '1' : '0',
35
  );
36
}
37

  
38
sub action_handle_scan_result {
39
  my ($self) = @_;
40

  
41
  my $qrtext = $::form->{qrtext};
42

  
43
  # load text into object
44
  $self->{qr_obj} = SL::Helper::QrBillParser->new($qrtext);
45

  
46
  # check if valid qr-bill
47
  if (!$self->{qr_obj}->is_valid) {
48
    return $self->js
49
      ->run('kivi.ScanQRBill.popupInvalidQRBill', $self->{qr_obj}->error)
50
      ->render();
51
  }
52

  
53
  my $vendor_name = $self->{qr_obj}->{creditor}->{name};
54
  $self->{vendor} = first { $_->{name} eq $vendor_name } @{ $self->vendors };
55

  
56
  if (!$self->{vendor}) {
57
    return $self->js
58
      ->run('kivi.ScanQRBill.popupVendorNotFound', $vendor_name)
59
      ->render();
60
  }
61

  
62
  $self->prepare_add_purchase_transaction();
63
}
64

  
65
################# internal ###############
66

  
67
sub prepare_add_purchase_transaction {
68
  my ($self) = @_;
69

  
70
  my $qr_obj = $self->{qr_obj};
71

  
72
  my $token = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_PURCHASE_INVOICE_POST())->token;
73

  
74
  my $html = $self->render('scan_qrbill/_add_purchase_transaction',
75
    { output => 0 },
76
    vendor => {
77
      name => $self->{vendor}->{name},
78
      number => $self->{vendor}->{vendornumber},
79
      id => $self->{vendor}->{id},
80
    },
81
    qrbill => {
82
      unstructured_message => $qr_obj->{additional_information}->{unstructured_message},
83
      reference_type => $qr_obj->{payment_reference}->{reference_type},
84
      reference => $qr_obj->{payment_reference}->{reference},
85
      amount => $qr_obj->{payment_amount_information}->{amount},
86
      currency => $qr_obj->{payment_amount_information}->{currency},
87
      data_encoded => uri_escape($qr_obj->raw_data),
88
    },
89
    accounts_AP_amount => $self->accounts_AP_amount,
90
    accounts_AP => $self->accounts_AP,
91
    taxcharts => $self->taxcharts,
92
    form_validity_token => $token,
93
  );
94

  
95
  $self->js->html('#main-content', $html)->render();
96
}
97

  
98
sub init_vendors {
99
  SL::DB::Manager::Vendor->get_all();
100
}
101

  
102
sub init_accounts_AP_amount {
103
  [ map { {
104
      text => "$_->{accno} - $_->{description}",
105
      accno => $_->{accno},
106
      id => $_->{id},
107
      chart_id => $_->{id},
108
    } } @{ SL::DB::Manager::Chart->get_all(
109
      query   => [ SL::DB::Manager::Chart->link_filter('AP_amount') ],
110
      sort_by => 'id ASC') }
111
  ];
112
}
113

  
114
sub init_accounts_AP {
115
  [ map { {
116
      text => "$_->{accno} - $_->{description}",
117
      accno => $_->{accno},
118
      id => $_->{id},
119
      chart_id => $_->{id},
120
    } } @{ SL::DB::Manager::Chart->get_all(
121
      query   => [ SL::DB::Manager::Chart->link_filter('AP') ],
122
      sort_by => 'id ASC') }
123
  ];
124
}
125

  
126
sub init_taxcharts {
127
  [ map { {
128
      text => "$_->{taxkey} - $_->{taxdescription} " . ($_->{rate} * 100) .' %',
129
      id => "$_->{id}--$_->{rate}",
130
    } } @{ SL::DB::Manager::Tax->get_all(
131
    where   => [ chart_categories => { like => '%E%' }],
132
    sort_by => 'taxkey, rate') }
133
  ];
134
}
135

  
136
1;
137

  
138
__END__
139

  
140
=pod
141

  
142
=encoding utf-8
143

  
144
=head1 NAME
145

  
146
SL::Controller::ScanQRBill - Controller for scanning swiss QR-Bills using the mobile template
147

  
148
=head1 DESCRIPTION
149

  
150
Renders the scan view in the mobile template and handles the scan result.
151

  
152
The scanned QR-Bill data is parsed and the vendor is searched in the database.
153

  
154
If everything is valid an add purchase transaction view is rendered and
155
the QR-Bill can be saved as a purchase transaction.
156

  
157
The post function from ap.pl is used to save the purchase transaction.
158

  
159
The raw data of the QR-Bill is stored with the purchase transaction in the ap table
160
in the field qrbill_data.
161
The data can later be accessed again using the parser module SL::Helper::QrBillParser.
162

  
163
=head1 SECURITY CONSIDERATIONS
164

  
165
In theory an attacker could try to insert a malicious Javascript code into a qr code,
166
that is then scanned, and redisplayed in the browser (XSS).
167

  
168
Therefore it is important to escape any data coming from the qr code when it is rendered
169
in the templates. For this we use the template toolkit html filter: [% qrdata | html %],
170
Jquery's text function: $('#qrdata').text(qrdata);, and URI::Escape; for the raw data.
171

  
172
For database insertion we use prepared statements (AP.pm).
173

  
174
=head1 TESTING
175

  
176
To simplify testing the scan view shows some buttons to send example qr codes, when in
177
developer mode. Sending is implemented in Javascript in js/kivi.ScanQRBill.js.
178

  
179
=head1 URL ACTIONS
180

  
181
=over 4
182

  
183
=item C<scan_view>
184

  
185
Renders the scan view in the mobile template.
186

  
187
=item C<handle_scan_result>
188

  
189
Handles the scan result and renders the add purchase transaction view.
190

  
191
=back
192

  
193
=head1 TODO
194

  
195
=head2 Additional features:
196

  
197
=over 4
198

  
199
=item * automatically extract invoice number and dates etc. from "SWICO-String" if present
200

  
201
=item * Option to add the vendor if not found
202

  
203
=back
204

  
205
=head1 BUGS
206

  
207
Nothing here yet.
208

  
209
=head1 AUTHOR
210

  
211
Cem Aydin E<lt>cem.aydin@revamp-it.chE<gt>
212

  
213
=cut
bin/mozilla/ap.pl
919 919
        SL::Helper::Flash::flash_later('info', $msg);
920 920
        print $form->redirect_header($form->{callback});
921 921
        $::dispatcher->end_request;
922

  
922
      } elsif ($form->{callback} =~ /ScanQRCode/) {
923
        # callback/redirect when coming from mobile view (swiss qr bill scan)
924
        print $form->redirect_header(build_std_url(
925
          "script=controller.pl",
926
          'action=ScanQRBill/scan_view',
927
          'transaction_success=1',
928
          'invnumber=' . E($form->{invnumber})
929
        ));
930
        $::dispatcher->end_request;
923 931
      } elsif ('doc-tab' eq $form->{after_action}) {
924 932
        # Redirect with callback containing a fragment does not work (by now)
925 933
        # because the callback info is stored in the session an parsing the
js/kivi.ScanQRBill.js
1
namespace('kivi.ScanQRBill', function(ns) {
2

  
3
  ns.onScanSuccess = async (decodedText, decodedResult) => {
4
    // stop camera
5
    await html5Qrcode.stop();
6

  
7
    // send the scanned text to the server
8
    const data = [];
9
    data.push({ name: 'qrtext', value: decodedText });
10
    data.push({ name: 'action', value: 'ScanQRBill/handle_scan_result' });
11
    $.post("controller.pl", data, kivi.eval_json_result);
12
  }
13

  
14
  ns.onScanFailure = (error) => {
15
    // handle scan failure, usually better to ignore and keep scanning.
16
    //console.warn(`Code scan error = ${error}`);
17
  }
18

  
19
  ns.popupInvalidQRBill = (error) => {
20
    console.warn('popupInvalidQRBill', error);
21
    $('#qr_code_invalid_error').text(error);
22
    $('#qr_code_invalid_modal').modal('open');
23
  }
24

  
25
  ns.popupVendorNotFound = (vn) => {
26
    //console.warn('popupVendorNotFound', vn);
27
    $('#vendor_name').text(vn);
28
    $('#vendor_not_found_error').modal('open');
29
  }
30

  
31
  ns.sendTestCode = async (code) => {
32
    // function to easily send code without scanning
33
    // use for testing only
34
    if (html5Qrcode.isScanning) {
35
      // stop camera
36
      await html5Qrcode.stop();
37
    }
38
    const data = [];
39
    const codes = [
40
      "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",
41
      "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",
42
      "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",
43
      // for testing XSS
44
      "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",
45
      "<script>alert('XSS!');</script>",
46
    ];
47
    data.push({ name: 'qrtext', value: codes[code] });
48
    data.push({ name: 'action', value: 'ScanQRBill/handle_scan_result' });
49
    $.post("controller.pl", data, kivi.eval_json_result);
50
  }
51

  
52
});
menus/mobile/00-erp.yaml
10 10
  params:
11 11
    action: ImageUpload/upload_image
12 12
    object_type: sales_delivery_order
13
- id: scan_qrbill
14
  name: Scan swiss QR bill
15
  order: 200
16
  access: ap_transactions
17
  params:
18
    action: ScanQRBill/scan_view
13 19
- id: component_test
14 20
  name: Component Test
15
  order: 200
21
  order: 300
16 22
  access: developer
17 23
  params:
18 24
    action: MaterializeTest/components
19 25
- id: modal_test
20 26
  name: Modal Test
21
  order: 300
27
  order: 400
22 28
  access: developer
23 29
  params:
24 30
    action: MaterializeTest/modal
templates/mobile_webpages/scan_qrbill/_add_purchase_transaction.html
1
[% USE LxERP %]
2
[% USE L %]
3
[% USE HTML %]
4
[% USE P %]
5
[% USE T8 %]
6

  
7
<div class="container">
8
  <div class="row">
9

  
10
    <form method="post" action="ap.pl?action=post" id="form" class="col s12">
11
      [% L.hidden_tag("rowcount", "1") %]
12
      [% L.hidden_tag('form_validity_token', form_validity_token) %]
13
      [% L.hidden_tag('callback', 'ScanQRCode') %]
14
      [% L.hidden_tag('paidaccounts', '1') %]
15
      [% L.hidden_tag('taxincluded', '1') %]
16

  
17
      [% L.hidden_tag('qrbill_data_encoded', qrbill.data_encoded) %]
18

  
19
      <h4>[% 'Add Accounts Payables Transaction' | $T8 %]</h4>
20

  
21
      <div class="card">
22
        <div class="card-content">
23
          <span class="card-title">[% 'Vendor & Order' | $T8 %]</span>
24
          <p>
25
            [% 'Vendor' | $T8 %]: [% vendor.number | html %] [% vendor.name | html %]
26
            [% L.hidden_tag("vendor_id", vendor.id) %]
27
            [% L.hidden_tag("previous_vendor_id", vendor.id) %]
28
          </p>
29

  
30
          <p>
31
            [% 'Currency' | $T8 %]: [% qrbill.currency | html %]
32
            [% L.hidden_tag("currency", qrbill.currency) %]
33
          </p>
34
        </div>
35
      </div>
36

  
37
      <h5>[% 'Notes' | $T8 %]</h5>
38
      <div class="row">
39
        [% P.M.textarea_tag("notes", notes, label=LxERP.t8('Notes'), class="col s12") %]
40
        [% P.M.textarea_tag("intnotes", intnotes, label=LxERP.t8('Internal Notes'), class="col s12") %]
41
      </div>
42
      <h5>[% 'Numbers & Dates' | $T8 %]</h5>
43
      <div class="row">
44
        [% P.M.input_tag("invnumber", qrbill.unstructured_message, label=LxERP.t8('Invoice Number'), class="col s12") %]
45
        [% P.M.input_tag("ordnumber", '', label=LxERP.t8('Order Number'), class="col s12") %]
46
        [% P.M.date_tag('transdate', '', label=LxERP.t8('Invoice Date'), icon="date_range", class="col s12") %]
47
        [% P.M.date_tag('duedate', '', label=LxERP.t8('Due Date'), icon="date_range", class="col s12") %]
48
      </div>
49
      <h5>[% 'Transactions' | $T8 %]</h5>
50
      <div class="row">
51
        [% P.M.select_tag("AP_amount_chart_id_1", accounts_AP_amount, title_key='text'
52
                          label=LxERP.t8('Account'), class="col s12") %]
53
        [% L.hidden_tag("AP_amount_chart_id_1_type", "AP_amount") %]
54

  
55
        [% P.M.input_tag("amount_1", qrbill.amount, label=LxERP.t8('Amount'), class="col s12") %]
56
        [% P.M.select_tag("taxchart_1", taxcharts, title_key='text'
57
                            label=LxERP.t8('Taxkey'), class="col s12") %]
58

  
59
        [% P.M.select_tag("AP_chart_id", accounts_AP, title_key='text'
60
                          label=LxERP.t8('Contra Account'), class="col s12") %]
61
        [% L.hidden_tag("AP_chart_id_type", "AP") %]
62
      </div>
63

  
64
      <div class="row">
65
        [% P.M.submit_tag("", LxERP.t8('Post'), class="col s12") %]
66
      </div>
67
      <div class="row">
68
        [% P.M.button_tag("", LxERP.t8('Cancel'), class="col s12", flat=1, small=1, href="controller.pl?action=ScanQRBill/scan_view") %]
69
      </div>
70
    </form>
71

  
72
  </div>
73
</div>
templates/mobile_webpages/scan_qrbill/scan_view.html
1
[% USE LxERP %]
2
[% USE L %]
3
[% USE HTML %]
4
[% USE P %]
5
[% USE T8 %]
6

  
7
<div id="main-content">
8
  <div class="container">
9
    <div class="row">
10

  
11
      <h4>[% 'Scan swiss QR bill' | $T8 %]</h4>
12

  
13
      <div id="QRreader" width="600px"></div>
14

  
15
      [% IF developer %]
16
        <p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(0);", "sendTestCode 0" ) %]</p>
17
        <p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(1);", "sendTestCode 1" ) %]</p>
18
        <p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(2);", "sendTestCode 2" ) %]</p>
19
        <p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(3);", "sendTestCode 3 XSS" ) %]</p>
20
        <p>[% P.M.button_tag("kivi.ScanQRBill.sendTestCode(4);", "sendTestCode 4 XSS (invalid)" ) %]</p>
21
      [% END %]
22
    </div>
23
  </div>
24
  <hr>
25
</div>
26

  
27
<div id="qr_code_invalid_modal" class="modal">
28
  <div class="modal-content">
29
    <h4>[% 'QR-Code invalid' | $T8 %]</h4>
30

  
31
    <p>[% 'The scanned code is not a valid QR bill.' | $T8 %]</p>
32
    <p>[% 'Error' | $T8 %]: <span id="qr_code_invalid_error"></span></p>
33

  
34
  </div>
35
  <div class="modal-footer">
36
    [% P.M.button_tag("startCamera();", LxERP.t8("Try again"), class="modal-close") %]
37
  </div>
38
</div>
39

  
40
<div id="vendor_not_found_error" class="modal">
41
  <div class="modal-content">
42
    <h4>[% 'Vendor not found' | $T8 %]</h4>
43

  
44
    <p>[% 'The vendor could not be found. Please register the vendor with the exact name from the QR bill as shown below.' | $T8 %]</p>
45
    <p>[% 'Vendor Name' | $T8 %]: '<span id="vendor_name"></span>'</p>
46

  
47
  </div>
48
  <div class="modal-footer">
49
    [% P.M.button_tag("startCamera();", LxERP.t8("Try again"), class="modal-close") %]
50
  </div>
51
</div>
52

  
53
<div id="transaction_successful_modal" class="modal">
54
  <div class="modal-content">
55
    <h4>[% 'AP transaction posted successfully' | $T8 %]</h4>
56

  
57
    <p>[% 'Invoice number' | $T8 %]: [% invoice_number | html %]</p>
58
  </div>
59
  <div class="modal-footer">
60
    [% P.M.button_tag("", LxERP.t8("Ok"), class="modal-close") %]
61
  </div>
62
</div>
63

  
64
[% L.hidden_tag("transaction_success", transaction_success) %]
65

  
66
<script>
67
  const html5Qrcode = new Html5Qrcode("QRreader",
68
    { formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE ] });
69

  
70
  const startCamera = () => {
71
    html5Qrcode.start({ facingMode: "environment" },
72
      { fps: 10, qrbox: { width: 250, height: 250 } },
73
      kivi.ScanQRBill.onScanSuccess, kivi.ScanQRBill.onScanFailure
74
    );
75
  };
76

  
77
  window.onload = () => {
78
    // using $(document).ready didn't work here
79
    //$(document).ready(() => {
80
    if ($('#transaction_success').val() === '1') {
81
      $('#transaction_successful_modal').modal('open');
82
    }
83

  
84
    startCamera();
85
  };
86
</script>

Auch abrufbar als: Unified diff