1 package SL::Helper::QrBillFunctions;
3 use List::Util qw(first);
10 use Exporter qw(import);
12 get_street_name_from_address_line
13 get_building_number_from_address_line
14 get_postal_code_from_address_line
15 get_town_name_from_address_line
18 get_ref_number_formatted
24 REGEX_STREET_NAME_FROM_ADDRESS_LINE => qr{^([^\d]+)\s*},
25 REGEX_BUILDING_NUMBER_FROM_ADDRESS_LINE => qr{(\d+.*)},
26 REGEX_POSTAL_CODE_FROM_ADDRESS_LINE => qr{^(\d+).*$},
27 REGEX_TOWN_FROM_ADDRESS_LINE => qr{^\d+\s(.*)$},
30 sub get_street_name_from_address_line {
31 my $address_line = $_[0];
33 my ($street_name) = $address_line =~ REGEX_STREET_NAME_FROM_ADDRESS_LINE;
35 return trim($street_name) // '';
38 sub get_building_number_from_address_line {
39 my $address_line = $_[0];
41 my ($building_number) = $address_line =~ REGEX_BUILDING_NUMBER_FROM_ADDRESS_LINE;
43 return trim($building_number) // '';
46 sub get_postal_code_from_address_line {
47 my $address_line = $_[0];
49 my ($postal_code) = $address_line =~ REGEX_POSTAL_CODE_FROM_ADDRESS_LINE;
51 return trim($postal_code) // '';
54 sub get_town_name_from_address_line {
55 my $address_line = $_[0];
57 my ($town_name) = $address_line =~ REGEX_TOWN_FROM_ADDRESS_LINE;
59 return trim($town_name) // '';
62 sub get_qrbill_account {
63 $main::lxdebug->enter_sub();
67 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted;
69 $qr_account = first { $_->use_for_qrbill } @{ $bank_accounts };
72 return undef, $::locale->text('No bank account flagged for QRBill usage was found.');
75 $main::lxdebug->leave_sub();
76 return $qr_account, undef;
79 sub assemble_ref_number {
80 $main::lxdebug->enter_sub();
83 my $customer_number = $_[1];
84 my $invoice_number = $_[2] // "0";
86 # check values (analog to checks in makro)
88 # in-/output: a string containing a 6 digit number
89 if (!($bank_id =~ /^\d*$/) || length($bank_id) != 6) {
90 return undef, $::locale->text('Bank account id number invalid. Must be 6 digits.');
94 # input: a string containing up to 6 digits [0-9]
95 # output: non-digits removed, 6 digits, filled with leading zeros
96 $customer_number = remove_non_digits($customer_number);
97 if (!check_digits_and_max_length($customer_number, 6)) {
98 return undef, $::locale->text('Customer number invalid. Must be less then or equal to 6 digits after non-digits removed.');
101 $customer_number = sprintf "%06d", $customer_number;
104 # input: a string containing up to 14 digits, may be zero
105 # output: non-digits removed, 14 digits, filled with leading zeros
106 $invoice_number = remove_non_digits($invoice_number);
107 if (!check_digits_and_max_length($invoice_number, 14)) {
108 return undef, $::locale->text('Invoice number invalid. Must be less then or equal to 14 digits after non-digits removed.');
111 $invoice_number = sprintf "%014d", $invoice_number;
113 # assemble ref. number
114 my $ref_number = $bank_id . $customer_number . $invoice_number;
116 # calculate check digit
117 my $ref_number_cpl = $ref_number . calculate_check_digit($ref_number);
119 $main::lxdebug->leave_sub();
120 return $ref_number_cpl, undef;
123 sub get_ref_number_formatted {
124 $main::lxdebug->enter_sub();
126 my $ref_number = $_[0];
128 # create ref. number in format:
129 # 'XX XXXXX XXXXX XXXXX XXXXX XXXXX' (2 digits + 5 x 5 digits)
130 my $ref_number_spaced = substr($ref_number, 0, 2) . ' ' .
131 substr($ref_number, 2, 5) . ' ' .
132 substr($ref_number, 7, 5) . ' ' .
133 substr($ref_number, 12, 5) . ' ' .
134 substr($ref_number, 17, 5) . ' ' .
135 substr($ref_number, 22, 5);
137 $main::lxdebug->leave_sub();
138 return $ref_number_spaced;
141 sub get_iban_formatted {
142 $main::lxdebug->enter_sub();
146 # create iban number in format:
147 # 'XXXX XXXX XXXX XXXX XXXX X' (5 x 4 + 1digits)
148 my $iban_spaced = substr($iban, 0, 4) . ' ' .
149 substr($iban, 4, 4) . ' ' .
150 substr($iban, 8, 4) . ' ' .
151 substr($iban, 12, 4) . ' ' .
152 substr($iban, 16, 4) . ' ' .
153 substr($iban, 20, 1);
155 $main::lxdebug->leave_sub();
159 sub get_amount_formatted {
160 $main::lxdebug->enter_sub();
164 # parameter should be a string containing a number
165 # with 2 digits after the pointi'm also getting in the town
166 unless ($amount =~ /^\d+\.\d{2}$/) {
170 my $r = reverse $amount;
171 # this matches the digits left of the '.'
173 # '\G' continuous the search where the last stopped,
174 # matches three digits and substitutes with a space
175 $r =~ s/\G(\d{3})(?=\d)/$1 /g;
178 $main::lxdebug->leave_sub();
182 ### internal functions
184 sub remove_non_digits {
190 sub check_digits_and_max_length {
194 return 0 if (!($s =~ /^\d*$/) || length($s) > $length);
198 sub calculate_check_digit {
199 # calculate ESR check digit using algorithm: "modulo 10, recursive"
200 my $ref_number_str = $_[0];
202 my @m = (0, 9, 4, 6, 8, 2, 7, 1, 3, 5);
205 my @ref_number_split = map int($_), split(//, $ref_number_str);
207 for my $v (@ref_number_split) {
208 $carry = @m[($carry + $v) % 10];
211 return (10 - $carry) % 10;
222 SL::Helper::QrBillFunctions - Additional helper functions for the swiss QR bill
226 use SL::Helper::QrBillFunctions qw(get_qrbill_account assemble_ref_number
227 get_ref_number_formatted get_iban_formatted get_amount_formatted);
229 # get qr-account data
230 my ($qr_account, $error) = get_qrbill_account();
232 my ($ref_number, $error) = assemble_ref_number(
233 $qr_account->{'bank_account_id'},
234 $form->{'customernumber'},
235 $form->{'invnumber'},
238 # get ref. number/iban formatted with spaces and set into form for template
240 $form->{'ref_number_formatted'} = get_ref_number_formatted($ref_number);
241 $form->{'iban_formatted'} = get_iban_formatted($qr_account->{'iban'});
243 # format amount for template
244 my $amount = sprintf("%.2f", $form->parse_amount(\%::myconfig, $form->{'total'}));
245 my $amount_formatted = get_amount_formatted($amount);
249 Helper functions moved from SL::Template::OpenDocument.
255 =item C<get_street_name_from_address_line>
257 Returns the street name from a combined street name and number.
259 =item C<get_street_number_from_address_line>
261 Returns the street number from a combined street name and number.
263 =item C<get_postal_code_from_address_line>
265 Returns the postal code from an combined postal code and town.
267 =item C<get_town_name_from_address_line>
269 Returns the town name from an combined postal code and town.
271 =item C<get_qrbill_account>
273 Return the bank account flagged for the QR bill. And a string containing an
274 error message as second return value or undef if no error occurred.
276 =item C<assemble_ref_number>
278 Assembles and returns the Swiss reference number. 27 digits, formed
279 from the parameters plus one check digit. And a string containing an error
280 message as second return value or undef if no error occurred.
282 Non-digits will be removed and remaining numbers filled up with leading zeros.
290 "Bankkonto Identifikationsnummer". A string containing a 6 digit number.
292 =item C<customer_number>
294 Kivitendo customer number. A string containing up to 6 digits.
296 =item C<invoice_number>
298 Kivitendo invoice number. A string containing up to 14 digits, may be zero.
302 =item C<get_ref_number_formatted>
304 Given a reference number, return it in format:
306 'XX XXXXX XXXXX XXXXX XXXXX XXXXX' (2 digits + 5 x 5 digits)
308 =item C<get_iban_formatted>
310 Given a IBAN number, return it in format:
312 'XXXX XXXX XXXX XXXX XXXX X' (5 x 4 + 1digits)
314 =item C<get_amount_formatted>
316 Given an amount, return it in format: 'X XXX.XX'
317 Or undef if an error occurred.
321 =head1 ERROR HANDLING
323 The functions C<get_qrbill_account> and C<assemble_ref_number> return
324 undef when an error occurs and a string containing an error message as
327 The function C<get_amount_formatted> returns undef if an error occurred.
329 The other functions always return a result.
333 Tests for functions see t/helper/qrbill_functions.t.
335 Run: C<t/test.pl t/helper/qrbill_functions.t>
339 Cem Aydin E<lt>cem.aydin@gmx.chE<gt>