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.
Files changed (44) hide show
  1. odoo/addons/ebill_postfinance/README.rst +130 -0
  2. odoo/addons/ebill_postfinance/__init__.py +1 -0
  3. odoo/addons/ebill_postfinance/__manifest__.py +35 -0
  4. odoo/addons/ebill_postfinance/data/ir_cron.xml +14 -0
  5. odoo/addons/ebill_postfinance/data/mail_activity_type.xml +10 -0
  6. odoo/addons/ebill_postfinance/data/transmit.method.xml +10 -0
  7. odoo/addons/ebill_postfinance/i18n/ebill_postfinance.pot +533 -0
  8. odoo/addons/ebill_postfinance/messages/invoice-2003A.jinja +238 -0
  9. odoo/addons/ebill_postfinance/messages/invoice-yellowbill.jinja +227 -0
  10. odoo/addons/ebill_postfinance/messages/ybInvoice_V2.0.4.xsd +1395 -0
  11. odoo/addons/ebill_postfinance/models/__init__.py +5 -0
  12. odoo/addons/ebill_postfinance/models/account_move.py +140 -0
  13. odoo/addons/ebill_postfinance/models/ebill_payment_contract.py +73 -0
  14. odoo/addons/ebill_postfinance/models/ebill_postfinance_invoice_message.py +404 -0
  15. odoo/addons/ebill_postfinance/models/ebill_postfinance_service.py +154 -0
  16. odoo/addons/ebill_postfinance/models/sale_order.py +19 -0
  17. odoo/addons/ebill_postfinance/readme/CONFIGURE.rst +13 -0
  18. odoo/addons/ebill_postfinance/readme/CONTRIBUTORS.rst +1 -0
  19. odoo/addons/ebill_postfinance/readme/DESCRIPTION.rst +1 -0
  20. odoo/addons/ebill_postfinance/readme/INSTALL.rst +2 -0
  21. odoo/addons/ebill_postfinance/readme/ROADMAP.rst +10 -0
  22. odoo/addons/ebill_postfinance/readme/USAGE.rst +5 -0
  23. odoo/addons/ebill_postfinance/security/ir.model.access.csv +5 -0
  24. odoo/addons/ebill_postfinance/static/description/icon.png +0 -0
  25. odoo/addons/ebill_postfinance/static/description/index.html +470 -0
  26. odoo/addons/ebill_postfinance/tests/__init__.py +3 -0
  27. odoo/addons/ebill_postfinance/tests/common.py +194 -0
  28. odoo/addons/ebill_postfinance/tests/examples/credit_note_yb.xml +176 -0
  29. odoo/addons/ebill_postfinance/tests/examples/invoice_qr_yb.xml +182 -0
  30. odoo/addons/ebill_postfinance/tests/examples/yellowbill_qr_iban.xml +178 -0
  31. odoo/addons/ebill_postfinance/tests/fixtures/cassettes/test_ping_service.yaml +1057 -0
  32. odoo/addons/ebill_postfinance/tests/fixtures/cassettes/test_search_invoices.yaml +564 -0
  33. odoo/addons/ebill_postfinance/tests/fixtures/cassettes/test_upload_file.yaml +561 -0
  34. odoo/addons/ebill_postfinance/tests/test_ebill_postfinance.py +50 -0
  35. odoo/addons/ebill_postfinance/tests/test_ebill_postfinance_message_yb.py +65 -0
  36. odoo/addons/ebill_postfinance/tests/test_ebill_postfinance_message_yb_creditnote.py +67 -0
  37. odoo/addons/ebill_postfinance/views/ebill_payment_contract.xml +56 -0
  38. odoo/addons/ebill_postfinance/views/ebill_postfinance_invoice_message.xml +81 -0
  39. odoo/addons/ebill_postfinance/views/ebill_postfinance_service.xml +136 -0
  40. odoo/addons/ebill_postfinance/views/message_template.xml +8 -0
  41. odoo_addon_ebill_postfinance-16.0.1.0.0.3.dist-info/METADATA +149 -0
  42. odoo_addon_ebill_postfinance-16.0.1.0.0.3.dist-info/RECORD +44 -0
  43. odoo_addon_ebill_postfinance-16.0.1.0.0.3.dist-info/WHEEL +5 -0
  44. odoo_addon_ebill_postfinance-16.0.1.0.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ from . import account_move
2
+ from . import ebill_payment_contract
3
+ from . import ebill_postfinance_invoice_message
4
+ from . import ebill_postfinance_service
5
+ from . import sale_order
@@ -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()