File: common.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (242 lines) | stat: -rw-r--r-- 10,603 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# -*- coding: utf-8 -*-
import base64

from freezegun import freeze_time
from os.path import join as opj

from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo import fields
from odoo.tools import misc

from lxml import etree


class TestUBLCommon(AccountTestInvoicingCommon):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        cls.other_currency = cls.setup_other_currency('USD', rounding=0.001)

        # Required for `product_uom_id` to be visible in the form views
        cls.env.user.groups_id += cls.env.ref('uom.group_uom')

        # remove this tax, otherwise, at import, this tax with children taxes can be selected and the total is wrong
        cls.tax_armageddon.children_tax_ids.unlink()
        cls.tax_armageddon.unlink()

        cls.move_template = cls.env['mail.template'].create({
            'auto_delete': True,
            'body_html': '<p>TemplateBody for <t t-out="object.name"></t><t t-out="object.invoice_user_id.signature or \'\'"></t></p>',
            'description': 'Sent to customers with their invoices in attachment',
            'email_from': "{{ (object.invoice_user_id.email_formatted or user.email_formatted) }}",
            'model_id': cls.env['ir.model']._get_id('account.move'),
            'name': "Invoice: Test Sending",
            'partner_to': "{{ object.partner_id.id }}",
            'subject': "{{ object.company_id.name }} Invoice (Ref {{ object.name or 'n/a' }})",
            'report_template_ids': [(4, cls.env.ref('account.account_invoices').id)],
            'lang': "{{ object.partner_id.lang }}",
        })

        # Fixed Taxes
        cls.recupel = cls.env['account.tax'].create({
            'name': "RECUPEL",
            'amount_type': 'fixed',
            'amount': 1,
            'include_base_amount': True,
            'sequence': 1,
        })
        cls.auvibel = cls.env['account.tax'].create({
            'name': "AUVIBEL",
            'amount_type': 'fixed',
            'amount': 1,
            'include_base_amount': True,
            'sequence': 2,
        })

    def assert_same_invoice(self, invoice1, invoice2, **invoice_kwargs):
        self.assertEqual(len(invoice1.invoice_line_ids), len(invoice2.invoice_line_ids))
        self.assertRecordValues(invoice2, [{
            'partner_id': invoice1.partner_id.id,
            'invoice_date': fields.Date.from_string(invoice1.date),
            'currency_id': invoice1.currency_id.id,
            'amount_untaxed': invoice1.amount_untaxed,
            'amount_tax': invoice1.amount_tax,
            'amount_total': invoice1.amount_total,
            **invoice_kwargs,
        }])

        default_invoice_line_kwargs_list = [{}] * len(invoice1.invoice_line_ids)
        invoice_line_kwargs_list = invoice_kwargs.get('invoice_line_ids', default_invoice_line_kwargs_list)
        self.assertRecordValues(invoice2.invoice_line_ids, [{
            'quantity': line.quantity,
            'price_unit': line.price_unit,
            'discount': line.discount,
            'product_id': line.product_id.id,
            'product_uom_id': line.product_uom_id.id,
            **invoice_line_kwargs,
        } for line, invoice_line_kwargs in zip(invoice1.invoice_line_ids, invoice_line_kwargs_list)])

    # -------------------------------------------------------------------------
    # IMPORT HELPERS
    # -------------------------------------------------------------------------

    @freeze_time('2017-01-01')
    def _assert_imported_invoice_from_etree(self, invoice, attachment):
        """
        Create an account.move directly from an xml file, asserts the invoice obtained is the same as the expected
        invoice.
        """
        # /!\ use the same journal as the invoice's one to import the attachment !
        invoice.journal_id.create_document_from_attachment(attachment.ids)
        new_invoice = self.env['account.move'].search([], order='id desc', limit=1)

        self.assertTrue(new_invoice)
        self.assert_same_invoice(invoice, new_invoice)

    def _update_invoice_from_file(self, module_name, subfolder, filename, invoice):
        """ Create an attachment from a file and post it on the invoice
        """
        file_path = opj(module_name, subfolder, filename)
        with misc.file_open(file_path, 'rb', filter_ext=('.xml',)) as file:
            attachment = self.env['ir.attachment'].create({
                'name': filename,
                'datas': base64.encodebytes(file.read()),
                'res_id': invoice.id,
                'res_model': 'account.move',
            })
            invoice.message_post(attachment_ids=[attachment.id])

    def _assert_imported_invoice_from_file(self, subfolder, filename, invoice_vals, move_type='in_invoice'):
        """ Create an empty account.move, update the xml file, and then check the invoice values. """
        if move_type in self.env['account.move'].get_purchase_types():
            journal = self.company_data['default_journal_purchase']
        else:
            journal = self.company_data['default_journal_sale']
        invoice = self.env['account.move'].create({'move_type': move_type, 'journal_id': journal.id})
        self._update_invoice_from_file(
            module_name='l10n_account_edi_ubl_cii_tests',
            subfolder=subfolder,
            filename=filename,
            invoice=invoice,
        )
        invoice_vals = invoice_vals.copy()
        invoice_lines = invoice_vals.pop('invoice_lines', False)
        self.assertRecordValues(invoice, [invoice_vals])
        if invoice_lines:
            self.assertRecordValues(invoice.invoice_line_ids, invoice_lines)

    # -------------------------------------------------------------------------
    # EXPORT HELPERS
    # -------------------------------------------------------------------------

    @freeze_time('2017-01-01')
    def _generate_move(self, seller, buyer, send=True, **invoice_kwargs):
        """
        Create and post an account.move.
        """

        # Setup the seller.
        self.env.company.write({
            'partner_id': seller.id,
            'name': seller.name,
            'street': seller.street,
            'zip': seller.zip,
            'city': seller.city,
            'vat': seller.vat,
            'country_id': seller.country_id.id,
        })

        move_type = invoice_kwargs['move_type']
        account_move = self.env['account.move'].create({
            'partner_id': buyer.id,
            'partner_bank_id': (seller if move_type == 'out_invoice' else buyer).bank_ids[:1].id,
            'invoice_payment_term_id': self.pay_terms_b.id,
            'invoice_date': '2017-01-01',
            'date': '2017-01-01',
            'currency_id': self.other_currency.id,
            'narration': 'test narration',
            'ref': 'ref_move',
            **invoice_kwargs,
            'invoice_line_ids': [
                (0, 0, {
                    'sequence': i,
                    **invoice_line_kwargs,
                })
                for i, invoice_line_kwargs in enumerate(invoice_kwargs.get('invoice_line_ids', []))
            ],
        })

        account_move.action_post()
        if send:
            # will set the right UBL format by default thanks to the partner's compute
            account_move._generate_and_send(sending_methods=['manual'])
        return account_move

    def _assert_invoice_attachment(self, attachment, xpaths, expected_file_path):
        """
        Get attachment from a posted account.move, and asserts it's the same as the expected xml file.
        """
        self.assertTrue(attachment)

        xml_content = base64.b64decode(attachment.with_context(bin_size=False).datas)
        xml_etree = self.get_xml_tree_from_string(xml_content)

        expected_file_full_path = misc.file_path(f'{self.test_module}/tests/test_files/{expected_file_path}')
        expected_etree = etree.parse(expected_file_full_path).getroot()

        modified_etree = self.with_applied_xpath(
            expected_etree,
            xpaths
        )

        self.assertXmlTreeEqual(
            xml_etree,
            modified_etree,
        )

        return attachment

    def _test_import_partner(self, attachment, seller, buyer):
        """
        Given a buyer and seller in an EDI attachment.
        * Uploading the attachment as an invoice should create an invoice with the partner = buyer.
        * Uploading the attachment as a vendor bill should create a bill with the partner = seller.
        """
        # Import attachment as an invoice
        new_invoice = self.company_data['default_journal_sale']._create_document_from_attachment(attachment.ids)
        self.assertEqual(buyer, new_invoice.partner_id)

        # Import attachment as a vendor bill
        new_invoice = self.company_data['default_journal_purchase']._create_document_from_attachment(attachment.ids)
        self.assertEqual(seller, new_invoice.partner_id)

    def _test_import_in_journal(self, attachment):
        """
        If the context contains the info about the current default journal, we should use it
        instead of infering the journal from the move type.
        """
        journal2 = self.company_data['default_journal_sale'].copy()
        journal2.default_account_id = self.company_data['default_account_revenue'].id
        journal3 = journal2.copy()
        journal3.default_account_id = self.company_data['default_account_revenue'].id  # Not copied

        # Use the journal if it's set
        new_invoice = journal2._create_document_from_attachment(attachment.id)
        self.assertEqual(new_invoice.journal_id, journal2)

        # If no journal, fallback on the context
        new_invoice2 = self.env['account.journal'].with_context(default_journal_id=journal3.id)._create_document_from_attachment(attachment.id)
        self.assertEqual(new_invoice2.journal_id, journal3)

        # If no journal and no journal in the context, fallback on the move type
        new_invoice3 = self.env['account.journal'].with_context(default_move_type='out_invoice')._create_document_from_attachment(attachment.id)
        self.assertEqual(new_invoice3.journal_id, self.company_data['default_journal_sale'])

    def _test_encoding_in_attachment(self, attachment, filename):
        """
        Generate an invoice, assert that the tag '<?xml version='1.0' encoding='UTF-8'?>' is present in the attachment
        """
        self.assertTrue(filename in attachment.name)
        self.assertIn(b"<?xml version='1.0' encoding='UTF-8'?>", attachment.raw)