Skip to content

Commit

Permalink
[13.0][ADD] l10n_ch_qr_bill_scan
Browse files Browse the repository at this point in the history
A new module to scan QR-bill or to import PDF files
  • Loading branch information
yvaucher committed Jun 26, 2020
1 parent fbf2035 commit 6d55a1e
Show file tree
Hide file tree
Showing 23 changed files with 642 additions and 0 deletions.
Empty file added l10n_ch_qr_bill_scan/README.rst
Empty file.
2 changes: 2 additions & 0 deletions l10n_ch_qr_bill_scan/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import wizard
from . import tools
16 changes: 16 additions & 0 deletions l10n_ch_qr_bill_scan/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

{
"name": "Switzerland - QR-bill scan",
"summary": "Scan QR-bill to create vendor bills",
"version": "13.0.1.0.0",
"author": "Camptocamp,Odoo Community Association (OCA)",
"category": "Localization",
"website": "https://github.com/OCA/l10n-switzerland",
"license": "AGPL-3",
"depends": ["l10n_ch", "account_invoice_import"],
"data": ["wizard/account_invoice_import_view.xml"],
"auto_install": False,
"installable": True,
}
Binary file added l10n_ch_qr_bill_scan/data/CH-Cross_7mm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added l10n_ch_qr_bill_scan/data/QR-patch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions l10n_ch_qr_bill_scan/models/business_document_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2020 Camptocamp
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, models


class BusinessDocumentImport(models.AbstractModel):
_name = "business.document.import"
_description = "Common methods to import business documents"

@api.model
def _hook_match_partner(
self, partner_dict, chatter_msg, domain, partner_type_label
):
# TODO search by iban
return False
25 changes: 25 additions & 0 deletions l10n_ch_qr_bill_scan/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
In order to configure the type of invoice created per partner.

Got to:
- `Accounting` or `Invoicing`
- `Configuration`
- `Import Vendor Bill`
- `Import Bills`

you will find the configuration of each partner.


In order to automatically create bank accounts on QR-bill import:

Go to:

- `Accounting` or `Invoicing`
- `Configuration`
- `Settings`
- `Invoice Import`

And activate `Auto-create Bank`

.. figure:: ../static/description/auto-create-bank.png
:alt: auto create bank
:width: 600 px
1 change: 1 addition & 0 deletions l10n_ch_qr_bill_scan/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Yannick Vaucher <[email protected]>
3 changes: 3 additions & 0 deletions l10n_ch_qr_bill_scan/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Import Vendor bills with a PayEye scanner.

The QR bills are introduced in Switzerland since the 30th of June 2020.
11 changes: 11 additions & 0 deletions l10n_ch_qr_bill_scan/readme/INSTALL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
This module depends on `account_invoice_import` which is located in `edi` and has dependencies on `community-data-files`.
So the repository `edi` and `community-data-files` must be present on the system.

Optionally you can also import Vendor bills directly from a PDF.
To enable this feature those additional dependencies must be
installed on your Odoo server:

pip install pyzbar,numpy,pdf2image,opencv-python

If you are looking to generate QR-bill, this feature is already available
in Odoo in the Switzerland localization module (`l10n_ch`).
4 changes: 4 additions & 0 deletions l10n_ch_qr_bill_scan/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
TODO:

- Read the Swico standard from Billing info
- Read Alternative parameters
9 changes: 9 additions & 0 deletions l10n_ch_qr_bill_scan/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
To import your invoices Launch the wizard `Invoicing` -> `Vendors` -> `Import Vendor Bill`.

In there you can either directly the QR code of the QR-bill or you can import a PDF/image file.

If it's the first time you import a Bill from a Vendor the wizard will ask you to create the partner and to set a import configuration for that partner
A configuration let you set which expense account you want the invoice to be imported with.


To quickly import a serie of invoice, use the button `Import Next Invoice` from the Vendor bill form.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions l10n_ch_qr_bill_scan/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_scan_qrbill
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added l10n_ch_qr_bill_scan/tests/data/qr-bill.pdf
Binary file not shown.
256 changes: 256 additions & 0 deletions l10n_ch_qr_bill_scan/tests/test_scan_qrbill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# Copyright 2020 Camptocamp (http://www.camptocamp.com/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import base64

from odoo import tools
from odoo.exceptions import UserError
from odoo.modules.module import get_resource_path
from odoo.tests import common

from odoo.addons.l10n_ch_qr_bill_scan.tools import QR

# QR Reference
QRR = "210000000003139471430009017"

# Creditor Reference
CF = "RF18539007547034"

CH_IBAN = "CH15 3881 5158 3845 3843 7"
QR_IBAN = "CH21 3080 8001 2345 6782 7"


SCAN_DATA = [
"SPC",
"0200",
"1",
CH_IBAN.replace(" ", ""),
"S",
"Camptocamp",
"EPFL Innovation Park",
"Bldg A",
"1015",
"Lausanne",
"CH",
"",
"",
"",
"",
"",
"",
"",
"1949.75",
"CHF",
"S",
"Your company",
"Bahnhofplatz",
"10",
"3011",
"Bern",
"CH",
"{ref_type}",
"{ref}",
"Instruction of 15.09.2019",
"EPD",
"",
"",
"",
]


class TestScanQRBill(common.SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.company_id.invoice_import_create_bank_account = True
cls.supplier = cls.env["res.partner"].create(
{
"name": "Camptocamp",
"street": "EPFL Innovation Park",
"street2": "Bldg A",
"zip": "1015",
"city": "Lausanne",
"country_id": cls.env.ref("base.ch").id,
}
)
cls.supplier.supplier_rank = 1

cls.expense_account = cls.env["account.account"].create(
{
"code": "612AII",
"name": "expense account invoice import",
"user_type_id": cls.env.ref("account.data_account_type_expenses").id,
}
)
cls.env["account.invoice.import.config"].create(
{
"name": "Camptocamp - one line no product",
"partner_id": cls.supplier.id,
"invoice_line_method": "1line_no_product",
"account_id": cls.expense_account.id,
}
)

def wiz_import_invoice_file(self, file_path, file_name):
""" Import a file of a vendor bill """
with tools.file_open(file_path, "rb") as f:
invoice_file = base64.b64encode(f.read())
wiz = self.env["account.invoice.import"].create({})
wiz.invoice_file = invoice_file
wiz.invoice_filename = file_name
return wiz

def import_invoice_file(self, file_path, file_name):
""" Import scanned data from a vendor bill
And return the created invoice
"""
wiz = self.wiz_import_invoice_file(file_path, file_name)
res = wiz.import_invoice()
if res.get("res_model") == "account.move":
invoice = self.env["account.move"].browse(res["res_id"])
return invoice
return None

def wiz_import_invoice_scan(self, invoice_scan):
""" Import scanned data from a vendor bill """
wiz = self.env["account.invoice.import"].create({})
wiz.invoice_scan = invoice_scan
return wiz

def import_invoice_scan(self, invoice_scan):
""" Import scanned data from a vendor bill
And return the created invoice
"""
wiz = self.wiz_import_invoice_scan(invoice_scan)
res = wiz.import_invoice()
if res.get("res_model") == "account.move":
invoice = self.env["account.move"].browse(res["res_id"])
return invoice
return None

def test_scan_QR_free_ref(self):
scan_data = SCAN_DATA[:]
scan_data = "\n".join(scan_data).format(ref_type="NON", ref="")
invoice = self.import_invoice_scan(scan_data)

self.assertEqual(invoice.partner_id, self.supplier)
self.assertFalse(invoice.invoice_payment_ref)
self.assertEqual(invoice.state, "draft")
iban = invoice.invoice_partner_bank_id.acc_number
self.assertEqual(iban, CH_IBAN)
self.assertEqual(invoice.amount_total, 1949.75)

def test_scan_QR_QRR(self):
scan_data = SCAN_DATA[:]
scan_data[QR.IBAN] = QR_IBAN.replace(" ", "")
scan_data = "\n".join(scan_data).format(ref_type="QRR", ref=QRR)
invoice = self.import_invoice_scan(scan_data)

self.assertEqual(invoice.partner_id, self.supplier)
self.assertEqual(invoice.invoice_payment_ref, QRR)
self.assertEqual(invoice.state, "draft")
iban = invoice.invoice_partner_bank_id.acc_number
self.assertEqual(iban, QR_IBAN)
self.assertEqual(invoice.amount_total, 1949.75)

def test_scan_QR_CF(self):
scan_data = SCAN_DATA[:]
scan_data = "\n".join(scan_data).format(ref_type="SCOR", ref=CF)
invoice = self.import_invoice_scan(scan_data)

self.assertEqual(invoice.partner_id, self.supplier)
self.assertEqual(invoice.invoice_payment_ref, CF)
self.assertEqual(invoice.state, "draft")
iban = invoice.invoice_partner_bank_id.acc_number
self.assertEqual(iban, CH_IBAN)
self.assertEqual(invoice.amount_total, 1949.75)

def test_scan_QR_new_partner(self):
scan_data = SCAN_DATA
scan_data[QR.CREDITOR_NAME] = "New Vendor"
scan_data = "\n".join(scan_data).format(ref_type="NON", ref="")
wiz = self.wiz_import_invoice_scan(scan_data)
wiz.import_invoice()

self.assertEqual(wiz.state, "select-partner")
self.assertEqual(wiz.partner_name, "New Vendor")
self.assertEqual(wiz.partner_street, "EPFL Innovation Park Bldg A")
self.assertEqual(wiz.partner_zip, "1015")
self.assertEqual(wiz.partner_city, "Lausanne")
self.assertEqual(wiz.partner_country_id, self.env.ref("base.ch"))

def test_scan_QR_swico(self):
self.assertTrue(False)

def test_scan_QR_wrong_swico(self):
self.assertTrue(False)

def test_scan_QR_extra_first_lines(self):
scan_data = ["", ""] + SCAN_DATA[:]
scan_data = "\n".join(scan_data).format(ref_type="NON", ref="")

invoice = self.import_invoice_scan(scan_data)
self.assertEqual(invoice.partner_id, self.supplier)
self.assertFalse(invoice.invoice_payment_ref)
self.assertEqual(invoice.state, "draft")
iban = invoice.invoice_partner_bank_id.acc_number
self.assertEqual(iban, CH_IBAN)
self.assertEqual(invoice.amount_total, 1949.75)

def test_scan_QR_missing_lines(self):
scan_data = SCAN_DATA[:-10]
scan_data = "\n".join(scan_data).format(ref_type="NON", ref="")

with self.assertRaises(UserError):
self.import_invoice_scan(scan_data)

def test_scan_QR_wrong_size(self):
scan_data = SCAN_DATA[:15] + SCAN_DATA[20:]
scan_data = "\n".join(scan_data).format(ref_type="NON", ref="")

with self.assertRaises(UserError):
self.import_invoice_scan(scan_data)

def test_scan_QR_gibberish(self):
scan_data = "saoeutheSPCoauhoess" + "\n" * 30

with self.assertRaises(UserError):
self.import_invoice_scan(scan_data)

def test_scan_QR_bad_amount(self):
scan_data = SCAN_DATA[:]
scan_data[QR.AMOUNT] = "not a number"
scan_data = "\n".join(scan_data).format(ref_type="NON", ref="")

with self.assertRaises(UserError):
self.import_invoice_scan(scan_data)

def test_import_QR_pdf(self):
partner = self.env["res.partner"].create(
{
"name": "My Company", # this was generated from Odoo
"street": "addr 1",
"street2": "",
"zip": "2074",
"city": "Marin",
"country_id": self.env.ref("base.ch").id,
}
)
partner.supplier_rank = 1
self.env["account.invoice.import.config"].create(
{
"name": "Camptocamp - one line no product",
"partner_id": partner.id,
"invoice_line_method": "1line_no_product",
"account_id": self.expense_account.id,
}
)

invoice_fp = get_resource_path(
"l10n_ch_qr_bill_scan", "tests", "data", "qr-bill.pdf"
)
invoice = self.import_invoice_file(invoice_fp, "qr-bill.pdf")
self.assertTrue(invoice)
self.assertEqual(invoice.invoice_payment_ref, "000000000000000000202000058")
self.assertEqual(invoice.amount_total, 1.0)
18 changes: 18 additions & 0 deletions l10n_ch_qr_bill_scan/tools/QR.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import re

# Positions
SPC = 0
VERSION = 1
IBAN = 3
CREDITOR = 4 # Till 10
CREDITOR_NAME = 5
AMOUNT = 18
CURRENCY = 19
REF = 28
MSG = 29
EPD = 30
BILL_INFO = 31

ADR_LEN = 7

valid_re = re.compile(r"SPC\n(.*\n){29}EPD")
1 change: 1 addition & 0 deletions l10n_ch_qr_bill_scan/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import QR
1 change: 1 addition & 0 deletions l10n_ch_qr_bill_scan/wizard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import account_invoice_import
Loading

0 comments on commit 6d55a1e

Please sign in to comment.