19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:07 +01:00
parent ba20ce7443
commit 768b70e05e
2357 changed files with 1057103 additions and 712486 deletions

View file

@ -1,122 +1,85 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.stock_account.tests.test_stockvaluation import _create_accounting_data
from odoo.tests.common import tagged, Form
from odoo.addons.stock_account.tests.common import TestStockValuationCommon
from odoo.exceptions import UserError
from odoo.tests import Form, tagged
from odoo import fields, Command
class TestAccountMoveStockCommon(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
(
cls.stock_input_account,
cls.stock_output_account,
cls.stock_valuation_account,
cls.expense_account,
cls.stock_journal,
) = _create_accounting_data(cls.env)
# `all_categ` should not be altered, so we can test the `post_init` hook of `stock_account`
cls.all_categ = cls.env.ref('product.product_category_all')
cls.auto_categ = cls.env['product.category'].create({
'name': 'child_category',
'parent_id': cls.all_categ.id,
"property_stock_account_input_categ_id": cls.stock_input_account.id,
"property_stock_account_output_categ_id": cls.stock_output_account.id,
"property_stock_valuation_account_id": cls.stock_valuation_account.id,
"property_stock_journal": cls.stock_journal.id,
"property_valuation": "real_time",
"property_cost_method": "standard",
})
cls.product_A = cls.env["product.product"].create(
{
"name": "Product A",
"type": "product",
"default_code": "prda",
"categ_id": cls.auto_categ.id,
"taxes_id": [(5, 0, 0)],
"supplier_taxes_id": [(5, 0, 0)],
"lst_price": 100.0,
"standard_price": 10.0,
"property_account_income_id": cls.company_data["default_account_revenue"].id,
"property_account_expense_id": cls.company_data["default_account_expense"].id,
}
)
@tagged("post_install", "-at_install")
class TestAccountMove(TestAccountMoveStockCommon):
class TestAccountMove(TestStockValuationCommon):
def test_standard_perpetual_01_mc_01(self):
rate = self.currency_data["rates"].sorted()[0].rate
product = self.product_standard_auto
self._use_multi_currencies([('2017-01-01', 2.0)])
rate = self.other_currency.rate_ids.rate
move_form = Form(self.env["account.move"].with_context(default_move_type="out_invoice"))
move_form.partner_id = self.partner_a
move_form.currency_id = self.currency_data["currency"]
move_form.partner_id = self.partner
move_form.currency_id = self.other_currency
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.product_A
line_form.product_id = product
line_form.tax_ids.clear()
invoice = move_form.save()
self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
self.assertAlmostEqual(product.lst_price * rate, invoice.amount_total)
self.assertEqual(len(invoice.mapped("line_ids")), 2)
self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 1)
invoice._post()
self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_residual)
self.assertAlmostEqual(product.lst_price * rate, invoice.amount_total)
self.assertAlmostEqual(product.lst_price * rate, invoice.amount_residual)
self.assertEqual(len(invoice.mapped("line_ids")), 4)
self.assertEqual(len(invoice.mapped("line_ids").filtered(lambda l: l.display_type == 'cogs')), 2)
self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 2)
def test_fifo_perpetual_01_mc_01(self):
self.product_A.categ_id.property_cost_method = "fifo"
rate = self.currency_data["rates"].sorted()[0].rate
product = self.product_fifo_auto
self._use_multi_currencies([('2017-01-01', 2.0)])
rate = self.other_currency.rate_ids.rate
move_form = Form(self.env["account.move"].with_context(default_move_type="out_invoice"))
move_form.partner_id = self.partner_a
move_form.currency_id = self.currency_data["currency"]
move_form.partner_id = self.partner
move_form.currency_id = self.other_currency
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.product_A
line_form.product_id = product
line_form.tax_ids.clear()
invoice = move_form.save()
self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
self.assertAlmostEqual(product.lst_price * rate, invoice.amount_total)
self.assertEqual(len(invoice.mapped("line_ids")), 2)
self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 1)
invoice._post()
self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_residual)
self.assertAlmostEqual(product.lst_price * rate, invoice.amount_total)
self.assertAlmostEqual(product.lst_price * rate, invoice.amount_residual)
self.assertEqual(len(invoice.mapped("line_ids")), 4)
self.assertEqual(len(invoice.mapped("line_ids").filtered(lambda l: l.display_type == 'cogs')), 2)
self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 2)
def test_average_perpetual_01_mc_01(self):
self.product_A.categ_id.property_cost_method = "average"
rate = self.currency_data["rates"].sorted()[0].rate
product = self.product_avco_auto
self._use_multi_currencies([('2017-01-01', 2.0)])
rate = self.other_currency.rate_ids.rate
move_form = Form(self.env["account.move"].with_context(default_move_type="out_invoice"))
move_form.partner_id = self.partner_a
move_form.currency_id = self.currency_data["currency"]
move_form.partner_id = self.partner
move_form.currency_id = self.other_currency
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.product_A
line_form.product_id = product
line_form.tax_ids.clear()
invoice = move_form.save()
self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
self.assertAlmostEqual(product.lst_price * rate, invoice.amount_total)
self.assertEqual(len(invoice.mapped("line_ids")), 2)
self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 1)
invoice._post()
self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_residual)
self.assertAlmostEqual(product.lst_price * rate, invoice.amount_total)
self.assertAlmostEqual(product.lst_price * rate, invoice.amount_residual)
self.assertEqual(len(invoice.mapped("line_ids")), 4)
self.assertEqual(len(invoice.mapped("line_ids").filtered(lambda l: l.display_type == 'cogs')), 2)
self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 2)
@ -125,34 +88,42 @@ class TestAccountMove(TestAccountMoveStockCommon):
"""Storno accounting uses negative numbers on debit/credit to cancel other moves.
This test checks that we do the same for the anglosaxon lines when storno is enabled.
"""
self._use_multi_currencies([('2017-01-01', 2.0)])
product = self.product_standard_auto
self.env.company.account_storno = True
self.env.company.anglo_saxon_accounting = True
move = self.env['account.move'].create({
'move_type': 'out_refund',
'invoice_date': fields.Date.from_string('2019-01-01'),
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'partner_id': self.partner.id,
'currency_id': self.other_currency.id,
'invoice_line_ids': [
(0, None, {'product_id': self.product_A.id}),
(0, None, {'product_id': product.id}),
]
})
move.action_post()
stock_output_line = move.line_ids.filtered(lambda l: l.account_id == self.stock_output_account)
stock_output_line = move.line_ids.filtered(lambda l: l.account_id == self.account_stock_valuation)
self.assertEqual(stock_output_line.debit, 0)
self.assertEqual(stock_output_line.credit, -10)
expense_line = move.line_ids.filtered(lambda l: l.account_id == self.product_A.property_account_expense_id)
expense_line = move.line_ids.filtered(lambda l: l.account_id == product._get_product_accounts()['expense'])
self.assertEqual(expense_line.debit, -10)
self.assertEqual(expense_line.credit, 0)
def test_standard_manual_tax_edit(self):
''' Test manually editing tax amount, cogs creation should not reset tax amount '''
product = self.product_standard_auto
product.lst_price = 100
move_form = Form(self.env["account.move"].with_context(default_move_type="out_invoice"))
move_form.partner_id = self.partner_a
move_form.partner_id = self.partner
self.company.income_account_id.write({
'tax_ids': [(6, 0, [self.env.company.account_sale_tax_id.id])]
})
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.product_A
line_form.product_id = product
invoice = move_form.save()
self.assertEqual(invoice.amount_total, 115)
@ -160,36 +131,14 @@ class TestAccountMove(TestAccountMoveStockCommon):
self.assertEqual(invoice.amount_tax, 15)
# simulate manual tax edit via widget
vals = {
'tax_totals': {
'amount_untaxed': 100,
'amount_total': 114,
'formatted_amount_total': '$\xa0114.00',
'formatted_amount_untaxed': '$\xa0100.00',
'groups_by_subtotal': {
'Untaxed Amount': [{
'group_key': 2,
'tax_group_id': 2,
'tax_group_name': 'Tax 15%',
'tax_group_amount': 14,
'tax_group_base_amount': 100,
'formatted_tax_group_amount': '$\xa014.00',
'formatted_tax_group_base_amount': '$\xa0100.00'
}]
},
'subtotals': [{
'name': 'Untaxed Amount',
'amount': 100,
'formatted_amount': '$\xa0100.00'
}],
'subtotals_order': ['Untaxed Amount'],
'display_tax_base': False,
}
}
invoice.write(vals)
tax_totals = invoice.tax_totals
tax_totals['subtotals'][0]['tax_groups'][0]['tax_amount_currency'] = 14.0
invoice.tax_totals = tax_totals
self.assertEqual(len(invoice.mapped("line_ids")), 3)
self.assertAlmostEqual(114.0, invoice.amount_total)
self.assertEqual(invoice.amount_total, 114)
self.assertEqual(invoice.amount_untaxed, 100)
self.assertEqual(invoice.amount_tax, 14)
invoice._post()
@ -207,108 +156,40 @@ class TestAccountMove(TestAccountMoveStockCommon):
the post-install hook)
- One created after the module installation
"""
first_company = self.env['res.company'].browse(1)
self.env.user.company_ids |= first_company
basic_product = self.env['product.product'].create({
'name': 'SuperProduct',
'type': 'product',
'categ_id': self.all_categ.id,
})
self.env.user.company_ids |= self.other_company
for company in (self.env.company | first_company):
for company in (self.company | self.other_company):
bill_form = Form(self.env['account.move'].with_company(company.id).with_context(default_move_type='in_invoice'))
bill_form.partner_id = self.partner_a
bill_form.partner_id = self.partner
bill_form.invoice_date = fields.Date.today()
with bill_form.invoice_line_ids.new() as line:
line.product_id = basic_product
line.product_id = self.product_standard
line.price_unit = 100
bill = bill_form.save()
bill.action_post()
product_accounts = basic_product.product_tmpl_id.with_company(company.id).get_product_accounts()
product_accounts = self.product_standard.product_tmpl_id.with_company(company.id).get_product_accounts()
self.assertEqual(bill.invoice_line_ids.account_id, product_accounts['expense'])
def test_product_valuation_method_change_to_automated_negative_on_hand_qty(self):
"""
We have a product whose category has manual valuation and on-hand quantity is negative:
Upon switching to an automated valuation method for the product category, the following
entries should be generated in the stock journal:
1. CREDIT to valuation account
2. DEBIT to stock output account
"""
stock_location = self.env['stock.warehouse'].search([
('company_id', '=', self.env.company.id),
], limit=1).lot_stock_id
categ = self.env['product.category'].create({'name': 'categ'})
product = self.product_a
product.write({
'type': 'product',
'categ_id': categ.id,
})
out_picking = self.env['stock.picking'].create({
'location_id': stock_location.id,
'location_dest_id': self.ref('stock.stock_location_customers'),
'picking_type_id': stock_location.warehouse_id.out_type_id.id,
})
sm = self.env['stock.move'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'location_id': out_picking.location_id.id,
'location_dest_id': out_picking.location_dest_id.id,
'picking_id': out_picking.id,
})
out_picking.action_confirm()
sm.quantity_done = 1
out_picking.button_validate()
categ.write({
'property_valuation': 'real_time',
'property_stock_account_input_categ_id': self.stock_input_account.id,
'property_stock_account_output_categ_id': self.stock_output_account.id,
'property_stock_valuation_account_id': self.stock_valuation_account.id,
'property_stock_journal': self.stock_journal.id,
})
amls = self.env['account.move.line'].search([('product_id', '=', product.id)])
if amls[0].account_id == self.stock_valuation_account:
stock_valuation_line = amls[0]
output_line = amls[1]
else:
output_line = amls[0]
stock_valuation_line = amls[1]
expected_valuation_line = {
'account_id': self.stock_valuation_account.id,
'credit': product.standard_price,
'debit': 0,
}
expected_output_line = {
'account_id': self.stock_output_account.id,
'credit': 0,
'debit': product.standard_price,
}
self.assertRecordValues(
[stock_valuation_line, output_line],
[expected_valuation_line, expected_output_line]
)
def test_cogs_analytic_accounting(self):
"""Check analytic distribution is correctly propagated to COGS lines"""
self.env.company.anglo_saxon_accounting = True
default_plan = self.env['account.analytic.plan'].create({'name': 'Default', 'company_id': False})
analytic_account = self.env['account.analytic.account'].create({'name': 'Account 1', 'plan_id': default_plan.id})
product = self.product_standard_auto
default_plan = self.env['account.analytic.plan'].create({
'name': 'Default',
})
analytic_account = self.env['account.analytic.account'].create({
'name': 'Account 1',
'plan_id': default_plan.id,
'company_id': False,
})
move = self.env['account.move'].create({
'move_type': 'out_refund',
'invoice_date': fields.Date.from_string('2019-01-01'),
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'partner_id': self.partner.id,
'invoice_line_ids': [
Command.create({
'product_id': self.product_A.id,
'product_id': product.id,
'analytic_distribution': {
analytic_account.id: 100,
},
@ -317,5 +198,144 @@ class TestAccountMove(TestAccountMoveStockCommon):
})
move.action_post()
cogs_line = move.line_ids.filtered(lambda l: l.account_id == self.product_A.property_account_expense_id)
cogs_line = move.line_ids.filtered(lambda l: l.account_id == product._get_product_accounts()['expense'])
self.assertEqual(cogs_line.analytic_distribution, {str(analytic_account.id): 100})
def test_cogs_account_branch_company(self):
"""Check branch company accounts are selected"""
product = self.product_standard_auto
branch = self.branch
test_account = self.env['account.account'].with_company(branch.id).create({
'name': 'STCK Test Account',
'code': '100119',
'reconcile': True,
'account_type': 'asset_current',
})
self.category_standard_auto.with_company(branch.id).property_valuation = "real_time"
self.category_standard_auto.with_company(branch.id).property_stock_valuation_account_id = test_account
bill = self.env['account.move'].with_company(branch.id).with_context(default_move_type='in_invoice').create({
'partner_id': self.partner.id,
'invoice_date': fields.Date.today(),
'company_id': branch.id,
'invoice_line_ids': [
Command.create({
'product_id': product.id,
'price_unit': 100,
}),
],
})
self.assertEqual(bill.invoice_line_ids.account_id, test_account)
def test_apply_inventory_adjustment_on_multiple_quants_simultaneously(self):
product = self.product_standard_auto
product_b = product.copy()
products = product + product_b
self._use_inventory_location_accounting()
self.env['stock.quant']._update_available_quantity(product, self.stock_location, 5)
self.env['stock.quant']._update_available_quantity(product_b, self.stock_location, 15)
quants = products.stock_quant_ids
quants.inventory_quantity = 10.0
wizard = self.env['stock.inventory.adjustment.name'].create({'quant_ids': quants})
wizard.action_apply()
inv_adjustment_journal_items = self.env['account.move.line'].search([('product_id', 'in', products.ids)], order='id asc', limit=4)
prod_a_accounts = product.product_tmpl_id.get_product_accounts()
prod_b_accounts = product_b.product_tmpl_id.get_product_accounts()
self.assertRecordValues(
inv_adjustment_journal_items,
[
{'account_id': self.account_inventory.id, 'product_id': product.id},
{'account_id': prod_a_accounts['stock_valuation'].id, 'product_id': product.id},
{'account_id': prod_b_accounts['stock_valuation'].id, 'product_id': product_b.id},
{'account_id': self.account_inventory.id, 'product_id': product_b.id},
]
)
@freeze_time("2020-01-22")
def test_backdate_picking_with_lock_date(self):
"""
Check that pickings can not be backdate or validated prior to the
fiscal and hard lock date.
"""
self.env['account.lock_exception'].search([]).sudo().unlink()
lock_date = fields.Date.from_string('2011-01-01')
prior_to_lock_date = fields.Datetime.add(lock_date, days=-1)
post_to_lock_date = fields.Datetime.add(lock_date, days=+1)
self.env['stock.quant']._update_available_quantity(self.product_standard, self.stock_location, 10)
receipts = receipt, receipt_done = self.env['stock.picking'].create([
{
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'picking_type_id': self.picking_type_in.id,
'owner_id': self.env.company.partner_id.id,
'move_ids': [Command.create({
'product_id': self.product_standard.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_qty': 1.0,
})]
} for _ in range(2)
])
receipts.action_confirm()
receipt_done.button_validate()
# Check that the purchase, sale and tax lock dates do not impose any restrictions
self.env.company.write({
'sale_lock_date': lock_date,
'purchase_lock_date': lock_date,
'tax_lock_date': lock_date,
})
# Receipts can be backdated
receipt.scheduled_date = prior_to_lock_date
receipt_done.date_done = prior_to_lock_date
# Check that the fiscal year lock date imposes restrictions
self.env.company.write({
'sale_lock_date': False,
'purchase_lock_date': False,
'tax_lock_date': False,
'fiscalyear_lock_date': lock_date,
})
# Receipts can not be backdated prior to lock date
receipt.scheduled_date = post_to_lock_date
receipt_done.date_done = post_to_lock_date
# Lock dates should not affect un-validated scheduled_date
receipt.scheduled_date = prior_to_lock_date
with self.assertRaises(UserError):
receipt_done.date_done = prior_to_lock_date
# Check that the hard lock date imposes restrictions
self.env.company.write({
'fiscalyear_lock_date': False,
'hard_lock_date': lock_date,
})
# Receipts can not be backdated prior to lock date
receipt.scheduled_date = post_to_lock_date
receipt_done.date_done = post_to_lock_date
# Lock dates should not affect un-validated scheduled_date
receipt.scheduled_date = prior_to_lock_date
with self.assertRaises(UserError):
receipt_done.date_done = prior_to_lock_date
def test_invoice_with_journal_item_without_label(self):
"""Test posting an invoice whose invoice lines have no label.
The 'name' field is optional on account.move.line and should be
handled safely when generating accounting entries.
"""
move = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner.id,
'invoice_line_ids': [
Command.create({
'product_id': self.product_standard.id,
'name': False,
}),
],
})
move.action_post()
# name should remain falsy on the invoice line
self.assertFalse(move.invoice_line_ids.name)
# ensure the invoice is posted successfully
self.assertEqual(move.state, 'posted')