Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[13.0] update upstream 038354cb17d #3926

Merged
merged 12 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def _find_additional_data(self, currency_code, account_number):

# If importing into an existing journal, its currency must be the same as the bank statement
if journal:
journal_currency = journal.currency_id
journal_currency = journal.currency_id or journal.company_id.currency_id
if currency is None:
currency = journal_currency
if currency and currency != journal_currency:
Expand Down
3 changes: 2 additions & 1 deletion addons/base_address_extended/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@

def _update_street_format(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
env['res.partner'].search([])._split_street()
specific_countries = env['res.country'].search([('street_format', '!=', '%(street_number)s/%(street_number2)s %(street_name)s')])
env['res.partner'].search([('country_id', 'in', specific_countries.ids)])._split_street()
28 changes: 18 additions & 10 deletions addons/mrp/models/product.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import collections
from datetime import timedelta
import operator as py_operator
from odoo import api, fields, models
Expand Down Expand Up @@ -125,21 +126,28 @@ def _compute_quantities_dict(self, lot_id, owner_id, package_id, from_date=False
qties = self.env.context.get("mrp_compute_quantities", {})
qties.update(res)
for product in bom_kits:
boms, bom_sub_lines = bom_kits[product].explode(product, 1)
bom_sub_lines = bom_kits[product].explode(product, 1)[1]
# group lines by component
bom_sub_lines_grouped = collections.defaultdict(list)
for info in bom_sub_lines:
bom_sub_lines_grouped[info[0].product_id].append(info)
ratios_virtual_available = []
ratios_qty_available = []
ratios_incoming_qty = []
ratios_outgoing_qty = []
ratios_free_qty = []
for bom_line, bom_line_data in bom_sub_lines:
component = bom_line.product_id.with_context(mrp_compute_quantities=qties)
if component.type != 'product' or float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding):
# As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those
# to avoid a division by zero. The same logic is applied to non-storable products as those
# products have 0 qty available.
continue
uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty']
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, round=False, raise_if_failure=False)

for component, bom_sub_lines in bom_sub_lines_grouped.items():
component = component.with_context(mrp_compute_quantities=qties)
qty_per_kit = 0
for bom_line, bom_line_data in bom_sub_lines:
if component.type != 'product' or float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding):
# As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those
# to avoid a division by zero. The same logic is applied to non-storable products as those
# products have 0 qty available.
continue
uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty']
qty_per_kit += bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, round=False, raise_if_failure=False)
if not qty_per_kit:
continue
rounding = component.uom_id.rounding
Expand Down
29 changes: 29 additions & 0 deletions addons/mrp/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,32 @@ def setUpClass(cls):
(0, 0, {'product_id': cls.product_4.id, 'product_qty': 8}),
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 12})
]})

@classmethod
def make_prods(cls, n):
return [
cls.env["product.product"].create(
{"name": f"p{k + 1}", "type": "product"}
)
for k in range(n)
]

@classmethod
def make_bom(cls, p, *cs):
return cls.env["mrp.bom"].create(
{
"product_tmpl_id": p.product_tmpl_id.id,
"product_id": p.id,
"product_qty": 1,
"type": "phantom",
"product_uom_id": cls.uom_unit.id,
"bom_line_ids": [
(0, 0, {
"product_id": c.id,
"product_qty": 1,
"product_uom_id": cls.uom_unit.id
})
for c in cs
],
}
)
12 changes: 12 additions & 0 deletions addons/mrp/tests/test_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,3 +895,15 @@ def test_validate_bom_line_with_variant_of_bom_product(self):
line.product_uom_id = uom_unit
line.product_qty = 5
bom_finished = bom_finished.save()

def test_bom_kit_with_sub_kit(self):
p1, p2, p3, p4 = self.make_prods(4)
self.make_bom(p1, p2, p3)
self.make_bom(p2, p3, p4)

loc = self.env.ref("stock.stock_location_stock")
self.env["stock.quant"]._update_available_quantity(p3, loc, 10)
self.env["stock.quant"]._update_available_quantity(p4, loc, 10)
self.assertEqual(p1.qty_available, 5.0)
self.assertEqual(p2.qty_available, 10.0)
self.assertEqual(p3.qty_available, 10.0)
24 changes: 13 additions & 11 deletions addons/point_of_sale/models/account_tax.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,29 @@

from odoo import _, api, models
from odoo.exceptions import UserError
from odoo.tools import split_every


class AccountTax(models.Model):
_inherit = 'account.tax'

def write(self, vals):
forbidden_fields = set([
forbidden_fields = {
'amount_type', 'amount', 'type_tax_use', 'tax_group_id', 'price_include',
'include_base_amount'
])
}
if forbidden_fields & set(vals.keys()):
tax_ids = self.env['pos.order.line'].sudo().search([
lines = self.env['pos.order.line'].sudo().search([
('order_id.session_id.state', '!=', 'closed')
]).read(['tax_ids'])
# Flatten the list of taxes, see https://stackoverflow.com/questions/952914
tax_ids = set([i for sl in [t['tax_ids'] for t in tax_ids] for i in sl])
if tax_ids & set(self.ids):
raise UserError(_(
'It is forbidden to modify a tax used in a POS order not posted. ' +
'You must close the POS sessions before modifying the tax.'
))
])
self_ids = set(self.ids)
for lines_chunk in map(self.env['pos.order.line'].browse, split_every(100000, lines.ids)):
if any(tid in self_ids for ts in lines_chunk.read(['tax_ids']) for tid in ts['tax_ids']):
raise UserError(_(
'It is forbidden to modify a tax used in a POS order not posted. ' +
'You must close the POS sessions before modifying the tax.'
))
lines_chunk.invalidate_cache(['tax_ids'], lines_chunk.ids)
return super(AccountTax, self).write(vals)

def get_real_tax_amount(self):
Expand Down
14 changes: 12 additions & 2 deletions addons/snailmail/models/snailmail_letter.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,22 @@ def _snailmail_create(self, route):
documents = []

for letter in self:
recipient_name = letter.partner_id.name or letter.partner_id.parent_id and letter.partner_id.parent_id.name
if not recipient_name:
letter.write({
'info_msg': _('Invalid recipient name.'),
'state': 'error',
'error_code': 'MISSING_REQUIRED_FIELDS'
})
continue
document = {
# generic informations to send
'letter_id': letter.id,
'res_model': letter.model,
'res_id': letter.res_id,
'contact_address': letter.partner_id.with_context(snailmail_layout=True, show_address=True).name_get()[0][1],
'address': {
'name': letter.partner_id.name,
'name': recipient_name,
'street': letter.partner_id.street,
'street2': letter.partner_id.street2,
'zip': letter.partner_id.zip,
Expand Down Expand Up @@ -414,7 +422,9 @@ def _format_snailmail_failures(self):
return failures_infos

def _append_cover_page(self, invoice_bin: bytes):
address = self.partner_id.with_context(show_address=True, lang='en_US')._get_name().replace('\n', '<br/>')
address_split = self.partner_id.with_context(show_address=True, lang='en_US')._get_name().split('\n')
address_split[0] = self.partner_id.name or self.partner_id.parent_id and self.partner_id.parent_id.name or address_split[0]
address = '<br/>'.join(address_split)
address_x = 118 * mm
address_y = 60 * mm
frame_width = 85.5 * mm
Expand Down
20 changes: 16 additions & 4 deletions odoo/modules/migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,24 @@ def _get_migration_files(pkg, version, stage):
parsed_installed_version = parse_version(installed_version)
current_version = parse_version(convert_version(pkg.data['version']))

versions = _get_migration_versions(pkg, stage)
def compare(version):
if version == "0.0.0" and parsed_installed_version < current_version:
return True

for version in versions:
if ((version == "0.0.0" and parsed_installed_version < current_version)
or parsed_installed_version < parse_version(convert_version(version)) <= current_version):
full_version = convert_version(version)
majorless_version = (version != full_version)

if majorless_version:
# We should not re-execute major-less scripts when upgrading to new Odoo version
# a module in `9.0.2.0` should not re-execute a `2.0` script when upgrading to `10.0.2.0`.
# In which case we must compare just the module version
return parsed_installed_version[2:] < parse_version(full_version)[2:] <= current_version[2:]

return parsed_installed_version < parse_version(full_version) <= current_version

versions = _get_migration_versions(pkg, stage)
for version in versions:
if compare(version):
strfmt = {'addon': pkg.name,
'stage': stage,
'version': stageformat[stage] % version,
Expand Down
24 changes: 22 additions & 2 deletions odoo/modules/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import warnings
from operator import itemgetter
from os.path import join as opj
from pathlib import Path

import odoo
import odoo.tools as tools
Expand Down Expand Up @@ -158,7 +159,7 @@ def initialize_sys_path():
legacy_upgrade_path = os.path.join(base_path, 'base', 'maintenance', 'migrations')
for up in (tools.config['upgrade_path'] or legacy_upgrade_path).split(','):
up = os.path.normcase(os.path.abspath(tools.ustr(up.strip())))
if up not in upgrade.__path__:
if os.path.isdir(up) and up not in upgrade.__path__:
upgrade.__path__.append(up)

# create decrecated module alias from odoo.addons.base.maintenance.migrations to odoo.upgrade
Expand Down Expand Up @@ -472,7 +473,7 @@ def get_test_modules(module, openupgrade_prefix=None):
except ImportError:
pass
else:
results += _get_tests_modules('odoo.upgrade', module, openupgrade_prefix)
results += list(_get_upgrade_test_modules(module, openupgrade_prefix))

return results

Expand Down Expand Up @@ -504,6 +505,25 @@ def _get_tests_modules(path, module, openupgrade_prefix=None):
if name.startswith('test_')]
return result

def _get_upgrade_test_modules(module, openupgrade_prefix=None):
if openupgrade_prefix is None:
openupgrade_prefix = ''
name = openupgrade_prefix + '.tests'
upg = importlib.import_module("odoo.upgrade")
for path in map(Path, upg.__path__):
if openupgrade_prefix:
tests = (path / module / openupgrade_prefix[1:] / "tests").glob("test_*.py")
else:
tests = (path / module / "tests").glob("test_*.py")
for test in tests:
spec = importlib.util.spec_from_file_location(f"odoo.upgrade.{module}{name}.{test.stem}", test)
if not spec:
continue
pymod = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = pymod
spec.loader.exec_module(pymod)
yield pymod


class OdooTestResult(unittest.result.TestResult):
"""
Expand Down
2 changes: 1 addition & 1 deletion odoo/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ def _json_command(self, command, timeout=3, get_key=None):
raise unittest.SkipTest("Error during Chrome headless connection")

def _open_websocket(self):
self.ws = websocket.create_connection(self.ws_url)
self.ws = websocket.create_connection(self.ws_url, suppress_origin=True)
if self.ws.getstatus() != 101:
raise unittest.SkipTest("Cannot connect to chrome dev tools")
self.ws.settimeout(0.01)
Expand Down
5 changes: 5 additions & 0 deletions odoo/tools/cloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

STANDARD_MODULES = ['web', 'web_enterprise', 'website_animate', 'base']
MAX_FILE_SIZE = 25 * 2**20 # 25 MB
MAX_LINE_SIZE = 100000

class Cloc(object):
def __init__(self):
Expand Down Expand Up @@ -61,6 +62,10 @@ def parse_js(self, s):
# Based on https://stackoverflow.com/questions/241327
s = s.strip() + "\n"
total = s.count("\n")
# To avoid to use too much memory we don't try to count file
# with very large line, usually minified file
if max(len(l) for l in s.split('\n')) > MAX_LINE_SIZE:
return -1, "Max line size exceeded"
def replacer(match):
s = match.group(0)
return " " if s.startswith('/') else s
Expand Down