odoo-addon-ebill-postfinance 16.0.1.0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/ebill_postfinance/README.rst +130 -0
- odoo/addons/ebill_postfinance/__init__.py +1 -0
- odoo/addons/ebill_postfinance/__manifest__.py +35 -0
- odoo/addons/ebill_postfinance/data/ir_cron.xml +14 -0
- odoo/addons/ebill_postfinance/data/mail_activity_type.xml +10 -0
- odoo/addons/ebill_postfinance/data/transmit.method.xml +10 -0
- odoo/addons/ebill_postfinance/i18n/ebill_postfinance.pot +533 -0
- odoo/addons/ebill_postfinance/messages/invoice-2003A.jinja +238 -0
- odoo/addons/ebill_postfinance/messages/invoice-yellowbill.jinja +227 -0
- odoo/addons/ebill_postfinance/messages/ybInvoice_V2.0.4.xsd +1395 -0
- odoo/addons/ebill_postfinance/models/__init__.py +5 -0
- odoo/addons/ebill_postfinance/models/account_move.py +140 -0
- odoo/addons/ebill_postfinance/models/ebill_payment_contract.py +73 -0
- odoo/addons/ebill_postfinance/models/ebill_postfinance_invoice_message.py +404 -0
- odoo/addons/ebill_postfinance/models/ebill_postfinance_service.py +154 -0
- odoo/addons/ebill_postfinance/models/sale_order.py +19 -0
- odoo/addons/ebill_postfinance/readme/CONFIGURE.rst +13 -0
- odoo/addons/ebill_postfinance/readme/CONTRIBUTORS.rst +1 -0
- odoo/addons/ebill_postfinance/readme/DESCRIPTION.rst +1 -0
- odoo/addons/ebill_postfinance/readme/INSTALL.rst +2 -0
- odoo/addons/ebill_postfinance/readme/ROADMAP.rst +10 -0
- odoo/addons/ebill_postfinance/readme/USAGE.rst +5 -0
- odoo/addons/ebill_postfinance/security/ir.model.access.csv +5 -0
- odoo/addons/ebill_postfinance/static/description/icon.png +0 -0
- odoo/addons/ebill_postfinance/static/description/index.html +470 -0
- odoo/addons/ebill_postfinance/tests/__init__.py +3 -0
- odoo/addons/ebill_postfinance/tests/common.py +194 -0
- odoo/addons/ebill_postfinance/tests/examples/credit_note_yb.xml +176 -0
- odoo/addons/ebill_postfinance/tests/examples/invoice_qr_yb.xml +182 -0
- odoo/addons/ebill_postfinance/tests/examples/yellowbill_qr_iban.xml +178 -0
- odoo/addons/ebill_postfinance/tests/fixtures/cassettes/test_ping_service.yaml +1057 -0
- odoo/addons/ebill_postfinance/tests/fixtures/cassettes/test_search_invoices.yaml +564 -0
- odoo/addons/ebill_postfinance/tests/fixtures/cassettes/test_upload_file.yaml +561 -0
- odoo/addons/ebill_postfinance/tests/test_ebill_postfinance.py +50 -0
- odoo/addons/ebill_postfinance/tests/test_ebill_postfinance_message_yb.py +65 -0
- odoo/addons/ebill_postfinance/tests/test_ebill_postfinance_message_yb_creditnote.py +67 -0
- odoo/addons/ebill_postfinance/views/ebill_payment_contract.xml +56 -0
- odoo/addons/ebill_postfinance/views/ebill_postfinance_invoice_message.xml +81 -0
- odoo/addons/ebill_postfinance/views/ebill_postfinance_service.xml +136 -0
- odoo/addons/ebill_postfinance/views/message_template.xml +8 -0
- odoo_addon_ebill_postfinance-16.0.1.0.0.3.dist-info/METADATA +149 -0
- odoo_addon_ebill_postfinance-16.0.1.0.0.3.dist-info/RECORD +44 -0
- odoo_addon_ebill_postfinance-16.0.1.0.0.3.dist-info/WHEEL +5 -0
- odoo_addon_ebill_postfinance-16.0.1.0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Copyright 2019 Camptocamp SA
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import odoo
|
|
8
|
+
from odoo import _, api, fields, models
|
|
9
|
+
from odoo.exceptions import UserError
|
|
10
|
+
from odoo.tools.pdf import merge_pdf
|
|
11
|
+
|
|
12
|
+
_logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AccountMove(models.Model):
|
|
16
|
+
|
|
17
|
+
_inherit = "account.move"
|
|
18
|
+
|
|
19
|
+
@api.onchange("transmit_method_id")
|
|
20
|
+
def _onchange_transmit_method(self):
|
|
21
|
+
if self.move_type not in ("out_invoice", "out_refund"):
|
|
22
|
+
return
|
|
23
|
+
postfinance_method = self.env.ref(
|
|
24
|
+
"ebill_postfinance.postfinance_transmit_method"
|
|
25
|
+
)
|
|
26
|
+
if self.transmit_method_id == postfinance_method:
|
|
27
|
+
contract = self.partner_id.get_active_contract(self.transmit_method_id)
|
|
28
|
+
if contract:
|
|
29
|
+
self.partner_bank_id = contract.postfinance_service_id.partner_bank_id
|
|
30
|
+
|
|
31
|
+
def _export_invoice(self):
|
|
32
|
+
"""Export invoice with the help of account_invoice_export module."""
|
|
33
|
+
postfinance_method = self.env.ref(
|
|
34
|
+
"ebill_postfinance.postfinance_transmit_method"
|
|
35
|
+
)
|
|
36
|
+
if self.transmit_method_id != postfinance_method:
|
|
37
|
+
return super()._export_invoice()
|
|
38
|
+
message = self.create_postfinance_ebill()
|
|
39
|
+
if not message:
|
|
40
|
+
raise UserError(_("Error generating postfinance eBill"))
|
|
41
|
+
message.send_to_postfinance()
|
|
42
|
+
self.invoice_exported = True
|
|
43
|
+
return "Postfinance invoice generated and in state {}".format(message.state)
|
|
44
|
+
|
|
45
|
+
def create_postfinance_ebill(self):
|
|
46
|
+
"""Generate the message record for an invoice."""
|
|
47
|
+
self.ensure_one()
|
|
48
|
+
contract = self.partner_id.get_active_contract(self.transmit_method_id)
|
|
49
|
+
if not contract:
|
|
50
|
+
return
|
|
51
|
+
# Generate PDf to be send
|
|
52
|
+
pdf_data = []
|
|
53
|
+
# When test are run, pdf are not generated, so use an empty pdf
|
|
54
|
+
pdf = b""
|
|
55
|
+
report_names = ["account.report_invoice"]
|
|
56
|
+
payment_type = ""
|
|
57
|
+
if self.move_type == "out_invoice":
|
|
58
|
+
payment_type = "iban"
|
|
59
|
+
if contract.payment_type == "qr":
|
|
60
|
+
report_names.append("l10n_ch.qr_report_main")
|
|
61
|
+
elif self.move_type == "out_refund":
|
|
62
|
+
payment_type = "credit"
|
|
63
|
+
for report_name in report_names:
|
|
64
|
+
# r = self.env["ir.actions.report"]._get_report_from_name(report_name)
|
|
65
|
+
pdf_content, _ = self.env["ir.actions.report"]._render(
|
|
66
|
+
report_name, [self.id]
|
|
67
|
+
)
|
|
68
|
+
# pdf_content, _ = r._render([self.id])
|
|
69
|
+
pdf_data.append(pdf_content)
|
|
70
|
+
if not odoo.tools.config["test_enable"]:
|
|
71
|
+
if len(pdf_data) > 1:
|
|
72
|
+
pdf = merge_pdf(pdf_data)
|
|
73
|
+
elif len(pdf_data) == 1:
|
|
74
|
+
pdf = pdf_data[0]
|
|
75
|
+
message = self.env["ebill.postfinance.invoice.message"].create(
|
|
76
|
+
{
|
|
77
|
+
"service_id": contract.postfinance_service_id.id,
|
|
78
|
+
"invoice_id": self.id,
|
|
79
|
+
"ebill_account_number": contract.postfinance_billerid,
|
|
80
|
+
"payment_type": payment_type,
|
|
81
|
+
"ebill_payment_contract_id": contract.id,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
attachment = self.env["ir.attachment"].create(
|
|
85
|
+
{
|
|
86
|
+
"name": "postfinance ebill",
|
|
87
|
+
"type": "binary",
|
|
88
|
+
"datas": base64.b64encode(pdf).decode("ascii"),
|
|
89
|
+
"res_model": "ebill.postfinance.invoice.message",
|
|
90
|
+
"res_id": message.id,
|
|
91
|
+
"mimetype": "application/x-pdf",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
message.attachment_id = attachment.id
|
|
95
|
+
return message
|
|
96
|
+
|
|
97
|
+
def postfinance_invoice_line_ids(self):
|
|
98
|
+
"""Filter invoice line to be included in XML message.
|
|
99
|
+
|
|
100
|
+
Invoicing line that are UX based (notes, sections) are removed.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
self.ensure_one()
|
|
104
|
+
return self.invoice_line_ids.filtered(lambda r: r.display_type == "product")
|
|
105
|
+
|
|
106
|
+
def get_postfinance_other_reference(self):
|
|
107
|
+
"""Allows glue module to insert <OTHER-REFERENCE> in the <HEADER>
|
|
108
|
+
|
|
109
|
+
Add to the list ref, object strucutred like this:
|
|
110
|
+
|
|
111
|
+
{'type': other reference allowed types,
|
|
112
|
+
'no': the content of <Reference-No> desired
|
|
113
|
+
}
|
|
114
|
+
"""
|
|
115
|
+
self.ensure_one()
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
def log_invoice_accepted_by_system(self):
|
|
119
|
+
""" """
|
|
120
|
+
self.activity_feedback(
|
|
121
|
+
["ebill_postfinance.mail_activity_dws_error"],
|
|
122
|
+
feedback="It worked on a later try",
|
|
123
|
+
)
|
|
124
|
+
self.message_post(body=_("Invoice accepted by the Postfinance system"))
|
|
125
|
+
self.invoice_export_confirmed = True
|
|
126
|
+
|
|
127
|
+
def log_invoice_refused_by_system(self):
|
|
128
|
+
""" """
|
|
129
|
+
activity_type = "ebill_postfinance.mail_activity_dws_error"
|
|
130
|
+
activity = self.activity_reschedule(
|
|
131
|
+
[activity_type], date_deadline=fields.Date.today()
|
|
132
|
+
)
|
|
133
|
+
values = {}
|
|
134
|
+
if not activity:
|
|
135
|
+
message = self.env.ref("ebill_postfinance.rejected_invoice")._render(
|
|
136
|
+
values=values
|
|
137
|
+
)
|
|
138
|
+
activity = self.activity_schedule(
|
|
139
|
+
activity_type, summary="Invoice rejected by Postfinance", note=message
|
|
140
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright 2019 Camptocamp SA
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import _, api, fields, models
|
|
5
|
+
from odoo.exceptions import ValidationError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EbillPaymentContract(models.Model):
|
|
9
|
+
_inherit = "ebill.payment.contract"
|
|
10
|
+
|
|
11
|
+
postfinance_billerid = fields.Char(
|
|
12
|
+
string="Contract ID", size=20, help="The PayerID of the customer"
|
|
13
|
+
)
|
|
14
|
+
is_postfinance_contract = fields.Boolean(
|
|
15
|
+
compute="_compute_is_postfinance_contract", store=False
|
|
16
|
+
)
|
|
17
|
+
postfinance_service_id = fields.Many2one(
|
|
18
|
+
comodel_name="ebill.postfinance.service",
|
|
19
|
+
string="Service",
|
|
20
|
+
ondelete="restrict",
|
|
21
|
+
)
|
|
22
|
+
is_postfinance_method_on_partner = fields.Boolean(
|
|
23
|
+
compute="_compute_is_postfinance_method_on_partner"
|
|
24
|
+
)
|
|
25
|
+
payment_type = fields.Selection(
|
|
26
|
+
selection=[("qr", "QR"), ("isr", "ISR")],
|
|
27
|
+
string="Payment method",
|
|
28
|
+
default="qr",
|
|
29
|
+
help="Payment type to use for the invoices sent,"
|
|
30
|
+
" PDF will be generated and attached accordingly.",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@api.depends("transmit_method_id")
|
|
34
|
+
def _compute_is_postfinance_contract(self):
|
|
35
|
+
transmit_method = self.env.ref("ebill_postfinance.postfinance_transmit_method")
|
|
36
|
+
for record in self:
|
|
37
|
+
record.is_postfinance_contract = (
|
|
38
|
+
record.transmit_method_id == transmit_method
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@api.depends("transmit_method_id", "partner_id", "postfinance_service_id")
|
|
42
|
+
def _compute_is_postfinance_method_on_partner(self):
|
|
43
|
+
transmit_method = self.env.ref("ebill_postfinance.postfinance_transmit_method")
|
|
44
|
+
for record in self:
|
|
45
|
+
record.is_postfinance_method_on_partner = (
|
|
46
|
+
record.partner_id.customer_invoice_transmit_method_id == transmit_method
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def set_postfinance_method_on_partner(self):
|
|
50
|
+
transmit_method = self.env.ref("ebill_postfinance.postfinance_transmit_method")
|
|
51
|
+
for record in self:
|
|
52
|
+
if record.partner_id:
|
|
53
|
+
record.partner_id.customer_invoice_transmit_method_id = transmit_method
|
|
54
|
+
|
|
55
|
+
@api.constrains("transmit_method_id", "postfinance_billerid")
|
|
56
|
+
def _check_postfinance_biller_id(self):
|
|
57
|
+
for contract in self:
|
|
58
|
+
if not contract.is_postfinance_contract:
|
|
59
|
+
continue
|
|
60
|
+
if not contract.postfinance_billerid:
|
|
61
|
+
raise ValidationError(
|
|
62
|
+
_(
|
|
63
|
+
"The Postfinacnce Account ID is required for a Postfinance contract."
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@api.constrains("transmit_method_id", "postfinance_service_id")
|
|
68
|
+
def _check_postfinance_service_id(self):
|
|
69
|
+
for contract in self:
|
|
70
|
+
if contract.is_postfinance_contract and not contract.postfinance_service_id:
|
|
71
|
+
raise ValidationError(
|
|
72
|
+
_("A Postfinance service is required for a Postfinance contract.")
|
|
73
|
+
)
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Copyright 2019-2022 Camptocamp SA
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
import pytz
|
|
9
|
+
from jinja2 import Environment, FileSystemLoader
|
|
10
|
+
from lxml import etree
|
|
11
|
+
|
|
12
|
+
from odoo import _, api, fields, models
|
|
13
|
+
from odoo.exceptions import UserError
|
|
14
|
+
from odoo.modules.module import get_module_root
|
|
15
|
+
|
|
16
|
+
from odoo.addons.base.models.res_bank import sanitize_account_number
|
|
17
|
+
|
|
18
|
+
_logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
MODULE_PATH = get_module_root(os.path.dirname(__file__))
|
|
21
|
+
INVOICE_TEMPLATE_2003 = "invoice-2003A.jinja"
|
|
22
|
+
INVOICE_TEMPLATE_YB = "invoice-yellowbill.jinja"
|
|
23
|
+
TEMPLATE_DIR = [MODULE_PATH + "/messages"]
|
|
24
|
+
XML_SCHEMA_YB = MODULE_PATH + "/messages/ybInvoice_V2.0.4.xsd"
|
|
25
|
+
|
|
26
|
+
DOCUMENT_TYPE = {"out_invoice": "EFD", "out_refund": "EGS"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EbillPostfinanceInvoiceMessage(models.Model):
|
|
30
|
+
_name = "ebill.postfinance.invoice.message"
|
|
31
|
+
_description = "Postfinance message send to service"
|
|
32
|
+
|
|
33
|
+
service_id = fields.Many2one(
|
|
34
|
+
comodel_name="ebill.postfinance.service",
|
|
35
|
+
string="Service used",
|
|
36
|
+
required=True,
|
|
37
|
+
ondelete="restrict",
|
|
38
|
+
readonly=True,
|
|
39
|
+
)
|
|
40
|
+
ebill_payment_contract_id = fields.Many2one(comodel_name="ebill.payment.contract")
|
|
41
|
+
invoice_id = fields.Many2one(comodel_name="account.move", ondelete="restrict")
|
|
42
|
+
transaction_id = fields.Char()
|
|
43
|
+
file_type_used = fields.Char()
|
|
44
|
+
submitted_on = fields.Datetime(string="Submitted on")
|
|
45
|
+
attachment_id = fields.Many2one("ir.attachment", "PDF")
|
|
46
|
+
state = fields.Selection(
|
|
47
|
+
selection=[
|
|
48
|
+
("draft", "Draft"),
|
|
49
|
+
("sent", "Sent"),
|
|
50
|
+
("error", "Error"),
|
|
51
|
+
("processing", "Processing"),
|
|
52
|
+
("reject", "Reject"),
|
|
53
|
+
("done", "Done"),
|
|
54
|
+
],
|
|
55
|
+
default="draft",
|
|
56
|
+
)
|
|
57
|
+
server_state = fields.Selection(
|
|
58
|
+
selection=[
|
|
59
|
+
("invalid", "Invalid"),
|
|
60
|
+
("processing", "Processing"),
|
|
61
|
+
("unsigned", "Unsigned"),
|
|
62
|
+
("open", "Open"),
|
|
63
|
+
("paid", "Paid"),
|
|
64
|
+
# Not encountered states
|
|
65
|
+
("rejected", "Rejected"),
|
|
66
|
+
("incomplete", "Incomplete"),
|
|
67
|
+
("deleted", "Deleted"),
|
|
68
|
+
],
|
|
69
|
+
)
|
|
70
|
+
server_reason_code = fields.Integer(string="Error code")
|
|
71
|
+
server_reason_text = fields.Char(string="Error text")
|
|
72
|
+
|
|
73
|
+
# Set with invoice_id.number but also with returned data from server ?
|
|
74
|
+
ref = fields.Char("Reference No.", size=35)
|
|
75
|
+
ebill_account_number = fields.Char("Payer Id", size=20)
|
|
76
|
+
payload = fields.Text("Payload sent")
|
|
77
|
+
payload_size = fields.Float("Payload Size (MB)", digits=(6, 3), readonly=True)
|
|
78
|
+
response = fields.Text()
|
|
79
|
+
payment_type = fields.Selection(
|
|
80
|
+
selection=[
|
|
81
|
+
("iban", "IBAN"),
|
|
82
|
+
("credit", "CREDIT"),
|
|
83
|
+
("other", "OTHER"),
|
|
84
|
+
("dd", "DD"),
|
|
85
|
+
("esr", "ESR"),
|
|
86
|
+
],
|
|
87
|
+
default="iban",
|
|
88
|
+
readonly=True,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@api.model
|
|
92
|
+
def _get_payload_size(self, payload):
|
|
93
|
+
size_in_bytes = len(payload)
|
|
94
|
+
if size_in_bytes > 0:
|
|
95
|
+
size_in_bytes = size_in_bytes / 1000000
|
|
96
|
+
return size_in_bytes
|
|
97
|
+
|
|
98
|
+
def set_transaction_id(self):
|
|
99
|
+
self.ensure_one()
|
|
100
|
+
self.transaction_id = "-".join(
|
|
101
|
+
[
|
|
102
|
+
fields.Datetime.now().strftime("%y%m%d%H%M%S"),
|
|
103
|
+
self.invoice_id.name.replace("/", "").replace("_", ""),
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def update_message_from_server_data(self, data):
|
|
108
|
+
"""Update the invoice message with data received from the server.
|
|
109
|
+
|
|
110
|
+
Keyword arguments:
|
|
111
|
+
data -- Structure from the api
|
|
112
|
+
Example:
|
|
113
|
+
{
|
|
114
|
+
'BillerId': '41101000001021209',
|
|
115
|
+
'TransactionId': 'INV_2022_03_0001_2022_03_26_08_31_xml',
|
|
116
|
+
'eBillAccountId': '123412341234',
|
|
117
|
+
'Amount': Decimal('0'),
|
|
118
|
+
'State': 'Invalid',
|
|
119
|
+
'PaymentType': None,
|
|
120
|
+
'ESRReferenceNbr': None,
|
|
121
|
+
'DeliveryDate': datetime.datetime(2022, 3, 26, 0, 0),
|
|
122
|
+
'PaymentDueDate': None,
|
|
123
|
+
'ReasonCode': '16',
|
|
124
|
+
'ReasonText': 'some good reason'
|
|
125
|
+
}
|
|
126
|
+
"""
|
|
127
|
+
self.ensure_one()
|
|
128
|
+
self.server_state = data.State.lower()
|
|
129
|
+
self.server_reason_code = data.ReasonCode
|
|
130
|
+
self.server_reason_text = data.ReasonText
|
|
131
|
+
if self.server_state in ["invalid"]:
|
|
132
|
+
self.state = "error"
|
|
133
|
+
elif self.server_state == "processing":
|
|
134
|
+
self.state = "processing"
|
|
135
|
+
elif self.server_state == "paid":
|
|
136
|
+
self.set_as_paid(data)
|
|
137
|
+
|
|
138
|
+
def set_as_paid(self, data):
|
|
139
|
+
for record in self:
|
|
140
|
+
if record.state != "done":
|
|
141
|
+
record.state = "done"
|
|
142
|
+
record.invoice_id.message_post(body=_("Invoice paid through eBilling"))
|
|
143
|
+
|
|
144
|
+
@api.model
|
|
145
|
+
def _remove_pdf_data_from_payload(self, data):
|
|
146
|
+
"""Minimize payload size to be kept.
|
|
147
|
+
|
|
148
|
+
Remove the node containing the pdf data from the xml.
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
start_node = "<Appendix>"
|
|
152
|
+
end_node = "</Appendix>"
|
|
153
|
+
start = data.find(start_node)
|
|
154
|
+
if start < 0:
|
|
155
|
+
return data
|
|
156
|
+
end = data.find(end_node, start)
|
|
157
|
+
return data[0:start] + data[end + len(end_node) :]
|
|
158
|
+
|
|
159
|
+
def send_to_postfinance(self):
|
|
160
|
+
# TODO: Could sent multiple with one call
|
|
161
|
+
for message in self:
|
|
162
|
+
message.file_type_used = message.service_id.file_type_to_use
|
|
163
|
+
message.set_transaction_id()
|
|
164
|
+
payload = message._generate_payload()
|
|
165
|
+
data = payload.encode("utf-8")
|
|
166
|
+
message.payload = self._remove_pdf_data_from_payload(payload)
|
|
167
|
+
message.payload_size = self._get_payload_size(payload)
|
|
168
|
+
try:
|
|
169
|
+
# TODO: Handle file type from service configuation
|
|
170
|
+
res = message.service_id.upload_file(
|
|
171
|
+
message.transaction_id, message.file_type_used, data
|
|
172
|
+
)
|
|
173
|
+
response = res[0]
|
|
174
|
+
if response.ProcessingState == "OK":
|
|
175
|
+
message.state = "sent"
|
|
176
|
+
submit_date_utc = response.SubmitDate.astimezone(pytz.utc)
|
|
177
|
+
message.submitted_on = submit_date_utc.replace(tzinfo=None)
|
|
178
|
+
message.response = response
|
|
179
|
+
else:
|
|
180
|
+
message.state = "error"
|
|
181
|
+
message.server_reason_code = "NOK"
|
|
182
|
+
message.server_reason_text = "Could not be sent to sftp"
|
|
183
|
+
except Exception as ex:
|
|
184
|
+
message.response = "Exception sending to Postfinance"
|
|
185
|
+
message.state = "error"
|
|
186
|
+
raise ex
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def format_date(date_string=None):
|
|
190
|
+
"""Format a date in the Jinja template."""
|
|
191
|
+
if not date_string:
|
|
192
|
+
date_string = datetime.now()
|
|
193
|
+
return date_string.strftime("%Y%m%d")
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def format_date_yb(date_string=None):
|
|
197
|
+
"""Format a date in the Jinja template."""
|
|
198
|
+
if not date_string:
|
|
199
|
+
date_string = datetime.now()
|
|
200
|
+
return date_string.strftime("%Y-%m-%d")
|
|
201
|
+
|
|
202
|
+
def _get_payload_params(self):
|
|
203
|
+
bank_account = ""
|
|
204
|
+
if self.payment_type == "iban":
|
|
205
|
+
bank_account = sanitize_account_number(
|
|
206
|
+
self.invoice_id.partner_bank_id.l10n_ch_qr_iban
|
|
207
|
+
or self.invoice_id.partner_bank_id.acc_number
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
bank_account = self.invoice_id.partner_bank_id.l10n_ch_isr_subscription_chf
|
|
211
|
+
if bank_account:
|
|
212
|
+
account_parts = bank_account.split("-")
|
|
213
|
+
bank_account = (
|
|
214
|
+
account_parts[0] + account_parts[1].rjust(6, "0") + account_parts[2]
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
bank_account = ""
|
|
218
|
+
|
|
219
|
+
params = {
|
|
220
|
+
"client_pid": self.service_id.biller_id,
|
|
221
|
+
"invoice": self.invoice_id,
|
|
222
|
+
"invoice_lines": self.invoice_id.postfinance_invoice_line_ids(),
|
|
223
|
+
"biller": self.invoice_id.company_id,
|
|
224
|
+
"customer": self.invoice_id.partner_id,
|
|
225
|
+
"delivery": self.invoice_id.partner_shipping_id,
|
|
226
|
+
"pdf_data": self.attachment_id.datas.decode("ascii"),
|
|
227
|
+
"bank": self.invoice_id.partner_bank_id,
|
|
228
|
+
"bank_account": bank_account,
|
|
229
|
+
"transaction_id": self.transaction_id,
|
|
230
|
+
"payment_type": self.payment_type,
|
|
231
|
+
"document_type": DOCUMENT_TYPE[self.invoice_id.move_type],
|
|
232
|
+
"format_date": self.format_date,
|
|
233
|
+
"ebill_account_number": self.ebill_account_number,
|
|
234
|
+
"discount_template": "",
|
|
235
|
+
"discount": {},
|
|
236
|
+
}
|
|
237
|
+
amount_by_group = []
|
|
238
|
+
# Get the percentage of the tax from the name of the group
|
|
239
|
+
# Could be improve by searching in the account_tax linked to the group
|
|
240
|
+
for taxgroup in self.invoice_id.amount_by_group:
|
|
241
|
+
rate = taxgroup[0].split()[-1:][0][:-1]
|
|
242
|
+
amount_by_group.append(
|
|
243
|
+
(
|
|
244
|
+
rate or "0",
|
|
245
|
+
taxgroup[1],
|
|
246
|
+
taxgroup[2],
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
params["amount_by_group"] = amount_by_group
|
|
250
|
+
# Get the invoice due date
|
|
251
|
+
date_due = None
|
|
252
|
+
if self.invoice_id.invoice_payment_term_id:
|
|
253
|
+
terms = self.invoice_id.invoice_payment_term_id.compute(
|
|
254
|
+
self.invoice_id.amount_total
|
|
255
|
+
)
|
|
256
|
+
if terms:
|
|
257
|
+
# Returns all payment and their date like [('2020-12-07', 430.37), ...]
|
|
258
|
+
# Get the last payment date in the format "202021207"
|
|
259
|
+
date_due = terms[-1][0].replace("-", "")
|
|
260
|
+
if not date_due:
|
|
261
|
+
date_due = self.format_date(
|
|
262
|
+
self.invoice_id.invoice_date_due or self.invoice_id.invoice_date
|
|
263
|
+
)
|
|
264
|
+
params["date_due"] = date_due
|
|
265
|
+
return params
|
|
266
|
+
|
|
267
|
+
def _get_payload_params_yb(self):
|
|
268
|
+
bank_account = ""
|
|
269
|
+
if self.payment_type == "iban":
|
|
270
|
+
bank_account = sanitize_account_number(
|
|
271
|
+
self.invoice_id.partner_bank_id.l10n_ch_qr_iban
|
|
272
|
+
or self.invoice_id.partner_bank_id.acc_number
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
bank_account = self.invoice_id.partner_bank_id.l10n_ch_isr_subscription_chf
|
|
276
|
+
if bank_account:
|
|
277
|
+
account_parts = bank_account.split("-")
|
|
278
|
+
bank_account = (
|
|
279
|
+
account_parts[0] + account_parts[1].rjust(6, "0") + account_parts[2]
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
bank_account = ""
|
|
283
|
+
|
|
284
|
+
delivery = (
|
|
285
|
+
self.invoice_id.partner_shipping_id
|
|
286
|
+
if self.invoice_id.partner_shipping_id != self.invoice_id.partner_id
|
|
287
|
+
else False
|
|
288
|
+
)
|
|
289
|
+
orders = self.invoice_id.line_ids.sale_line_ids.mapped("order_id")
|
|
290
|
+
params = {
|
|
291
|
+
"invoice": self.invoice_id,
|
|
292
|
+
"saleorder": orders,
|
|
293
|
+
"message": self,
|
|
294
|
+
"client_pid": self.service_id.biller_id,
|
|
295
|
+
"invoice_lines": self.invoice_id.postfinance_invoice_line_ids(),
|
|
296
|
+
"biller": self.invoice_id.company_id,
|
|
297
|
+
"customer": self.invoice_id.partner_id,
|
|
298
|
+
"delivery": delivery,
|
|
299
|
+
"pdf_data": self.attachment_id.datas.decode("ascii"),
|
|
300
|
+
"bank": self.invoice_id.partner_bank_id,
|
|
301
|
+
"bank_account": bank_account,
|
|
302
|
+
"transaction_id": self.transaction_id,
|
|
303
|
+
"payment_type": self.payment_type,
|
|
304
|
+
"amount_sign": -1 if self.payment_type == "credit" else 1,
|
|
305
|
+
"document_type": DOCUMENT_TYPE[self.invoice_id.move_type],
|
|
306
|
+
"format_date": self.format_date_yb,
|
|
307
|
+
"ebill_account_number": self.ebill_account_number,
|
|
308
|
+
"discount_template": "",
|
|
309
|
+
"discount": {},
|
|
310
|
+
"invoice_line_stock_template": "",
|
|
311
|
+
}
|
|
312
|
+
amount_by_group = []
|
|
313
|
+
# Get the percentage of the tax from the name of the group
|
|
314
|
+
# Could be improve by searching in the account_tax linked to the group
|
|
315
|
+
for __, tax_group in self.invoice_id.tax_totals["groups_by_subtotal"].items():
|
|
316
|
+
for taxgroup in tax_group:
|
|
317
|
+
rate = taxgroup["tax_group_name"].split()[-1:][0][:-1]
|
|
318
|
+
amount_by_group.append(
|
|
319
|
+
(
|
|
320
|
+
rate or "0",
|
|
321
|
+
taxgroup["tax_group_amount"],
|
|
322
|
+
taxgroup["tax_group_base_amount"],
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
params["amount_by_group"] = amount_by_group
|
|
326
|
+
# Get the invoice due date
|
|
327
|
+
date_due = None
|
|
328
|
+
if self.invoice_id.invoice_payment_term_id:
|
|
329
|
+
terms = self.invoice_id.invoice_payment_term_id.compute(
|
|
330
|
+
self.invoice_id.amount_total
|
|
331
|
+
)
|
|
332
|
+
if terms:
|
|
333
|
+
# Get the last payment date
|
|
334
|
+
date_due = terms[-1][0]
|
|
335
|
+
if not date_due:
|
|
336
|
+
date_due = self.format_date_yb(
|
|
337
|
+
self.invoice_id.invoice_date_due or self.invoice_id.invoice_date
|
|
338
|
+
)
|
|
339
|
+
params["date_due"] = date_due
|
|
340
|
+
return params
|
|
341
|
+
|
|
342
|
+
def _get_jinja_env(self, template_dir):
|
|
343
|
+
jinja_env = Environment(
|
|
344
|
+
loader=FileSystemLoader(template_dir),
|
|
345
|
+
autoescape=True,
|
|
346
|
+
)
|
|
347
|
+
# Force the truncate filter to be exact
|
|
348
|
+
jinja_env.policies["truncate.leeway"] = 0
|
|
349
|
+
return jinja_env
|
|
350
|
+
|
|
351
|
+
def _get_template(self, jinja_env):
|
|
352
|
+
return jinja_env.get_template(INVOICE_TEMPLATE_2003)
|
|
353
|
+
|
|
354
|
+
def _get_template_yb(self, jinja_env):
|
|
355
|
+
return jinja_env.get_template(INVOICE_TEMPLATE_YB)
|
|
356
|
+
|
|
357
|
+
def _generate_payload(self):
|
|
358
|
+
self.ensure_one()
|
|
359
|
+
assert self.state in ("draft", "error")
|
|
360
|
+
if self.service_id.file_type_to_use == "XML":
|
|
361
|
+
if self.service_id.use_file_type_xml_paynet:
|
|
362
|
+
return self._generate_payload_paynet()
|
|
363
|
+
else:
|
|
364
|
+
return self._generate_payload_yb()
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
def _generate_payload_paynet(self):
|
|
368
|
+
"""Generates the xml in the paynet format."""
|
|
369
|
+
params = self._get_payload_params()
|
|
370
|
+
jinja_env = self._get_jinja_env(TEMPLATE_DIR)
|
|
371
|
+
jinja_template = self._get_template(jinja_env)
|
|
372
|
+
return jinja_template.render(params)
|
|
373
|
+
|
|
374
|
+
def _generate_payload_yb(self):
|
|
375
|
+
"""Generates the xml in the yellowbill format."""
|
|
376
|
+
params = self._get_payload_params_yb()
|
|
377
|
+
jinja_env = self._get_jinja_env(TEMPLATE_DIR)
|
|
378
|
+
jinja_template = self._get_template_yb(jinja_env)
|
|
379
|
+
return jinja_template.render(params)
|
|
380
|
+
|
|
381
|
+
def validate_xml_payload(self):
|
|
382
|
+
"""Check the validity of yellowbill xml."""
|
|
383
|
+
schema = etree.XMLSchema(file=XML_SCHEMA_YB)
|
|
384
|
+
parser = etree.XMLParser(schema=schema)
|
|
385
|
+
try:
|
|
386
|
+
etree.fromstring(self.payload.encode("utf-8"), parser)
|
|
387
|
+
except etree.XMLSyntaxError as ex:
|
|
388
|
+
raise UserError(ex.error_log) from ex
|
|
389
|
+
return {
|
|
390
|
+
"type": "ir.actions.client",
|
|
391
|
+
"tag": "display_notification",
|
|
392
|
+
"params": {
|
|
393
|
+
"title": _("The payload is valid."),
|
|
394
|
+
"sticky": False,
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
def update_invoice_status(self):
|
|
399
|
+
"""Update the export status in the chatter."""
|
|
400
|
+
for message in self:
|
|
401
|
+
if message.state == "done":
|
|
402
|
+
message.invoice_id.log_invoice_accepted_by_system()
|
|
403
|
+
elif message.state in ["reject", "error"]:
|
|
404
|
+
message.invoice_id.log_invoice_refused_by_system()
|