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',