diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index a7d94a1b403..df5dbfe6b8f 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -688,10 +688,19 @@ class AccountJournal(models.Model): # We simply call the setup bar function. return self.env['res.company'].setting_init_bank_account_action() - def create_document_from_attachment(self, attachment_ids=None): - ''' Create the invoices from files. - :return: A action redirecting to account.move tree/form view. - ''' + def _create_document_from_attachment(self, attachment_ids=None): + """ Create the invoices from files.""" + context_move_type = self._context.get("default_move_type", "entry") + if not self: + if context_move_type in self.env['account.move'].get_sale_types(): + journal_type = "sale" + elif context_move_type in self.env['account.move'].get_purchase_types(): + journal_type = "purchase" + else: + raise UserError(_("The journal in which to upload the invoice is not specified. ")) + self = self.env['account.journal'].search([ + ('company_id', '=', self.env.company.id), ('type', '=', journal_type) + ], limit=1) attachments = self.env['ir.attachment'].browse(attachment_ids) if not attachments: raise UserError(_("No attachment was provided")) @@ -703,14 +712,20 @@ class AccountJournal(models.Model): decoders = self.env['account.move']._get_create_document_from_attachment_decoders() invoice = False for decoder in sorted(decoders, key=lambda d: d[0]): - invoice = decoder[1](attachment) + invoice = decoder[1](attachment, journal=self) if invoice: break if not invoice: invoice = self.env['account.move'].create({}) invoice.with_context(no_new_invoice=True).message_post(attachment_ids=[attachment.id]) invoices += invoice + return invoices + def create_document_from_attachment(self, attachment_ids=None): + """ Create the invoices from files. + :return: A action redirecting to account.move tree/form view. + """ + invoices = self._create_document_from_attachment(attachment_ids=attachment_ids) action_vals = { 'name': _('Generated Documents'), 'domain': [('id', 'in', invoices.ids)], diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index b7b3e4ed8d6..187269c9e26 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -658,7 +658,7 @@ class AccountMove(models.Model): domain = [('company_id', '=', company_id), ('type', '=', journal_type)] m.suitable_journal_ids = self.env['account.journal'].search(domain) - @api.depends('posted_before', 'state', 'journal_id', 'date') + @api.depends('posted_before', 'state', 'journal_id', 'date', 'move_type') def _compute_name(self): self = self.sorted(lambda m: (m.date, m.ref or '', m.id)) highest_name = self[0]._get_last_sequence(lock=False) if self else False @@ -3398,13 +3398,18 @@ class AccountMove(models.Model): action['domain'] = [('id', 'in', moves.ids)] return action - def action_switch_invoice_into_refund_credit_note(self): - if any(move.move_type not in ('in_invoice', 'out_invoice') for move in self): + def action_switch_move_type(self): + if any(move.posted_before for move in self): + raise ValidationError(_("You cannot switch the type of a posted document.")) + if any(move.move_type == "entry" for move in self): raise ValidationError(_("This action isn't available for this document.")) for move in self: + in_out, old_move_type = move.move_type.split('_') + new_move_type = f"{in_out}_{'invoice' if old_move_type == 'refund' else 'refund'}" + move.name = False move.write({ - 'move_type': move.move_type.replace('invoice', 'refund'), + 'move_type': new_move_type, 'partner_bank_id': False, 'currency_id': move.currency_id.id, }) diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py index aadec9275a5..1cfe6b1e2ed 100644 --- a/addons/account/tests/test_account_move_in_invoice.py +++ b/addons/account/tests/test_account_move_in_invoice.py @@ -1534,8 +1534,9 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): with self.assertRaises(RedirectWarning): invoice_2.action_post() - def test_in_invoice_switch_in_refund_1(self): - # Test creating an account_move with an in_invoice_type and switch it in an in_refund. + def test_in_invoice_switch_type_1(self): + # Test creating an account_move with an in_invoice_type and switch it in an in_refund, + # then switching it back to an in_invoice. move = self.env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': self.partner_a.id, @@ -1557,7 +1558,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): }), ], }) - move.action_switch_invoice_into_refund_credit_note() + move.action_switch_move_type() # Switch to refund. self.assertRecordValues(move, [{'move_type': 'in_refund'}]) self.assertInvoiceValues(move, [ @@ -1602,8 +1603,54 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): 'currency_id': self.currency_data['currency'].id, }) - def test_in_invoice_switch_in_refund_2(self): - # Test creating an account_move with an in_invoice_type and switch it in an in_refund and a negative quantity. + move.action_switch_move_type() # Switch back to invoice. + + self.assertRecordValues(move, [{'move_type': 'in_invoice'}]) + self.assertInvoiceValues(move, [ + { + **self.product_line_vals_1, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': 800.0, + 'credit': 0, + 'debit': 400.0, + }, + { + **self.product_line_vals_2, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': 160.0, + 'credit': 0.0, + 'debit': 80, + }, + { + **self.tax_line_vals_1, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': 144.0, + 'credit': 0, + 'debit': 72, + }, + { + **self.tax_line_vals_2, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': 24.0, + 'credit': 0, + 'debit': 12, + }, + { + **self.term_line_vals_1, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': -1128.0, + 'debit': 0, + 'credit': 564, + }, + ], { + **self.move_vals, + 'date': fields.Date.from_string('2019-01-31'), + 'currency_id': self.currency_data['currency'].id, + }) + + def test_in_invoice_switch_type_2(self): + # Test creating an account_move with an in_invoice_type and switch it in an in_refund and a negative quantity, + # then switching it back to an in_invoice. move = self.env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': self.partner_a.id, @@ -1678,7 +1725,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): 'amount_total' : -self.move_vals['amount_total'], 'amount_untaxed' : -self.move_vals['amount_untaxed'], }) - move.action_switch_invoice_into_refund_credit_note() + move.action_switch_move_type() # Switch to refund self.assertRecordValues(move, [{'move_type': 'in_refund'}]) self.assertInvoiceValues(move, [ @@ -1725,6 +1772,53 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): 'amount_total' : self.move_vals['amount_total'], 'amount_untaxed' : self.move_vals['amount_untaxed'], }) + move.action_switch_move_type() # Switch back to invoice + + self.assertRecordValues(move, [{'move_type': 'in_invoice'}]) + self.assertInvoiceValues(move, [ + { + **self.product_line_vals_1, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': 800.0, + 'credit': 0, + 'debit': 400, + }, + { + **self.product_line_vals_2, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': 160.0, + 'credit': 0, + 'debit': 80, + }, + { + **self.tax_line_vals_1, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': 144.0, + 'credit': 0, + 'debit': 72, + }, + { + **self.tax_line_vals_2, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': 24.0, + 'credit': 0, + 'debit': 12, + }, + { + **self.term_line_vals_1, + 'currency_id': self.currency_data['currency'].id, + 'amount_currency': -1128.0, + 'debit': 0, + 'credit': 564, + }, + ], { + **self.move_vals, + 'date': fields.Date.from_string('2019-01-31'), + 'currency_id': self.currency_data['currency'].id, + 'amount_tax': self.move_vals['amount_tax'], + 'amount_total': self.move_vals['amount_total'], + 'amount_untaxed': self.move_vals['amount_untaxed'], + }) def test_in_invoice_change_period_accrual_1(self): move = self.env['account.move'].create({ diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index 61879d19b29..0a2ae3b92c0 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -2380,7 +2380,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): }), ], }) - move.action_switch_invoice_into_refund_credit_note() + move.action_switch_move_type() self.assertRecordValues(move, [{'move_type': 'out_refund'}]) self.assertInvoiceValues(move, [ @@ -2500,7 +2500,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): 'amount_untaxed' : -self.move_vals['amount_untaxed'], }) - move.action_switch_invoice_into_refund_credit_note() + move.action_switch_move_type() self.assertRecordValues(move, [{'move_type': 'out_refund'}]) self.assertInvoiceValues(move, [ diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index d3dcc53a211..42f3ba96628 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -1565,8 +1565,8 @@ - - Switch into refund/credit note + + Switch into invoice/credit note @@ -1574,7 +1574,7 @@ form if records: - action = records.action_switch_invoice_into_refund_credit_note() + action = records.action_switch_move_type() diff --git a/addons/account_edi/models/account_edi_format.py b/addons/account_edi/models/account_edi_format.py index ff7613ea955..0bb5e791a97 100644 --- a/addons/account_edi/models/account_edi_format.py +++ b/addons/account_edi/models/account_edi_format.py @@ -128,11 +128,12 @@ class AccountEdiFormat(models.Model): self.ensure_one() return self.env['account.move'] - def _create_invoice_from_pdf_reader(self, filename, reader): + def _create_invoice_from_pdf_reader(self, filename, reader, journal): """ Create a new invoice with the data inside a pdf. :param filename: The name of the pdf. :param reader: The OdooPdfFileReader of the pdf to import. + :param journal The journal in which we want to create the invoice. :returns: The created invoice. """ # TO OVERRIDE @@ -298,7 +299,7 @@ class AccountEdiFormat(models.Model): return to_process - def _create_document_from_attachment(self, attachment): + def _create_document_from_attachment(self, attachment, journal=None): """Decodes an ir.attachment to create an invoice. :param attachment: An ir.attachment record. @@ -309,9 +310,9 @@ class AccountEdiFormat(models.Model): res = False try: if file_data['type'] == 'xml': - res = edi_format.with_company(self.env.company)._create_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree']) + res = edi_format.with_company(self.env.company)._create_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'], journal) elif file_data['type'] == 'pdf': - res = edi_format.with_company(self.env.company)._create_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader']) + res = edi_format.with_company(self.env.company)._create_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'], journal) file_data['pdf_reader'].stream.close() else: res = edi_format._create_invoice_from_binary(file_data['filename'], file_data['content'], file_data['extension']) diff --git a/addons/account_edi_ubl_cii/models/account_edi_common.py b/addons/account_edi_ubl_cii/models/account_edi_common.py index d6b81b8857b..6ff758fc450 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_common.py +++ b/addons/account_edi_ubl_cii/models/account_edi_common.py @@ -249,7 +249,15 @@ class AccountEdiCommon(models.AbstractModel): def _import_invoice(self, journal, filename, tree, existing_invoice=None): move_type, qty_factor = self._get_import_document_amount_sign(filename, tree) - if not move_type or (existing_invoice and existing_invoice.move_type != move_type): + if not move_type: + return + if journal.type == 'sale': + move_type = 'out_' + move_type + elif journal.type == 'purchase': + move_type = 'in_' + move_type + else: + return + if existing_invoice and existing_invoice.move_type != move_type: return with (existing_invoice or self.env['account.move']).with_context( @@ -351,7 +359,7 @@ class AccountEdiCommon(models.AbstractModel): ('company_id', '=', journal.company_id.id), ('amount', '=', float(tax_categ_percent_el.text)), ('amount_type', '=', 'percent'), - ('type_tax_use', '=', 'purchase'), + ('type_tax_use', '=', journal.type), # Journal type is ensured by _create_invoice_from_xml_tree to be either 'sale' or 'purchase' ], limit=1) if tax: tax_ids += tax.ids diff --git a/addons/account_edi_ubl_cii/models/account_edi_format.py b/addons/account_edi_ubl_cii/models/account_edi_format.py index 1bf74e27cab..ecdd11a974b 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_format.py +++ b/addons/account_edi_ubl_cii/models/account_edi_format.py @@ -177,12 +177,6 @@ class AccountEdiFormat(models.Model): # EXTENDS account_edi self.ensure_one() - if not journal: - # infer the journal - journal = self.env['account.journal'].search([ - ('company_id', '=', self.env.company.id), ('type', '=', 'purchase') - ], limit=1) - if not self._is_ubl_cii_available(journal.company_id) and self.code != 'facturx_1_0_05': return super()._create_invoice_from_xml_tree(filename, tree, journal=journal) diff --git a/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py b/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py index f8dee20b275..21ad9c17322 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py +++ b/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py @@ -216,7 +216,7 @@ class AccountEdiXmlCII(models.AbstractModel): # ==== partner_id ==== - partner_type = invoice.journal_id.type == 'purchase' and 'SellerTradeParty' or 'BuyerTradeParty' + partner_type = invoice.journal_id.type == journal.type and 'SellerTradeParty' or 'BuyerTradeParty' invoice.partner_id = self.env['account.edi.format']._retrieve_partner( name=_find_value(f"//ram:{partner_type}/ram:Name"), mail=_find_value(f"//ram:{partner_type}//ram:URIID[@schemeID='SMTP']"), @@ -352,9 +352,9 @@ class AccountEdiXmlCII(models.AbstractModel): if move_type_code is None: return None, None if move_type_code.text == '381': - return 'in_refund', 1 + return 'refund', 1 if move_type_code.text == '380': amount_node = tree.find('.//{*}SpecifiedTradeSettlementHeaderMonetarySummation/{*}TaxBasisTotalAmount') if amount_node is not None and float(amount_node.text) < 0: - return 'in_refund', -1 - return 'in_invoice', 1 + return 'refund', -1 + return 'invoice', 1 diff --git a/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py b/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py index 40c080521fc..0b2e60a096e 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py +++ b/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py @@ -545,7 +545,7 @@ class AccountEdiXmlUBL20(models.AbstractModel): # ==== invoice_line_ids: InvoiceLine/CreditNoteLine ==== - invoice_line_tag = 'InvoiceLine' if invoice.move_type == 'in_invoice' or qty_factor == -1 else 'CreditNoteLine' + invoice_line_tag = 'InvoiceLine' if invoice.move_type in ('in_invoice', 'out_invoice') or qty_factor == -1 else 'CreditNoteLine' for i, invl_el in enumerate(tree.findall('./{*}' + invoice_line_tag)): invoice_line = invoice.invoice_line_ids.create({'move_id': invoice.id}) invl_logs = self._import_fill_invoice_line_form(journal, invl_el, invoice, invoice_line, qty_factor) @@ -579,7 +579,7 @@ class AccountEdiXmlUBL20(models.AbstractModel): 'gross_price_unit': './{*}Price/{*}AllowanceCharge/{*}BaseAmount', 'rebate': './{*}Price/{*}AllowanceCharge/{*}Amount', 'net_price_unit': './{*}Price/{*}PriceAmount', - 'billed_qty': './{*}InvoicedQuantity' if invoice.move_type == 'in_invoice' or qty_factor == -1 else './{*}CreditedQuantity', + 'billed_qty': './{*}InvoicedQuantity' if invoice.move_type in ('in_invoice', 'out_invoice') or qty_factor == -1 else './{*}CreditedQuantity', 'allowance_charge': './/{*}AllowanceCharge', 'allowance_charge_indicator': './{*}ChargeIndicator', # below allowance_charge node 'allowance_charge_amount': './{*}Amount', # below allowance_charge node @@ -609,10 +609,10 @@ class AccountEdiXmlUBL20(models.AbstractModel): if tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice': amount_node = tree.find('.//{*}LegalMonetaryTotal/{*}TaxExclusiveAmount') if amount_node is not None and float(amount_node.text) < 0: - return 'in_refund', -1 - return 'in_invoice', 1 + return 'refund', -1 + return 'invoice', 1 if tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2}CreditNote': - return 'in_refund', 1 + return 'refund', 1 return None, None def _import_retrieve_partner_map(self, company): diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/common.py b/addons/l10n_account_edi_ubl_cii_tests/tests/common.py index ba1bfaa811d..c56ec871660 100644 --- a/addons/l10n_account_edi_ubl_cii_tests/tests/common.py +++ b/addons/l10n_account_edi_ubl_cii_tests/tests/common.py @@ -74,7 +74,7 @@ class TestUBLCommon(AccountEdiTestCommon): Create an account.move directly from an xml file, asserts the invoice obtained is the same as the expected invoice. """ - self.company_data['default_journal_purchase'].create_document_from_attachment(attachment.ids) + 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) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index b7d884748b3..463b6472f6e 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -612,7 +612,7 @@ class PurchaseOrder(models.Model): # 4) Some moves might actually be refunds: convert them if the total amount is negative # We do this after the moves have been created since we need taxes, etc. to know if the total # is actually negative or not - moves.filtered(lambda m: m.currency_id.round(m.amount_total) < 0).action_switch_invoice_into_refund_credit_note() + moves.filtered(lambda m: m.currency_id.round(m.amount_total) < 0).action_switch_move_type() return self.action_view_invoice(moves) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index dfff1f3e0f8..a6cc3e56160 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -1179,7 +1179,7 @@ class SaleOrder(models.Model): # We do this after the moves have been created since we need taxes, etc. to know if the total # is actually negative or not if final: - moves.sudo().filtered(lambda m: m.amount_total < 0).action_switch_invoice_into_refund_credit_note() + moves.sudo().filtered(lambda m: m.amount_total < 0).action_switch_move_type() for move in moves: move.message_post_with_view( 'mail.message_origin_link',