Initial commit: Test packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:52 +02:00
commit 080accd21c
338 changed files with 32413 additions and 0 deletions

View file

@ -0,0 +1,47 @@
# test xlsx export
A module to test xlsx export.
## Installation
```bash
pip install odoo-bringout-oca-ocb-test_xlsx_export
```
## Dependencies
This addon depends on:
- web
- test_mail
## Manifest Information
- **Name**: test xlsx export
- **Version**: 0.1
- **Category**: Hidden/Tests
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `test_xlsx_export`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Test_xlsx_export Module - test_xlsx_export
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for test_xlsx_export. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,6 @@
# Dependencies
This addon depends on:
- [web](../../odoo-bringout-oca-ocb-web)
- [test_mail](../../odoo-bringout-oca-ocb-test_mail)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon test_xlsx_export or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-ocb-test_xlsx_export"
# or
uv pip install odoo-bringout-oca-ocb-test_xlsx_export"
```

View file

@ -0,0 +1,11 @@
# Models
Detected core models and extensions in test_xlsx_export.
```mermaid
classDiagram
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: test_xlsx_export. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon test_xlsx_export
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,8 @@
# Security
This module does not define custom security rules or access controls beyond Odoo defaults.
Default Odoo security applies:
- Base user access through standard groups
- Model access inherited from dependencies
- No custom row-level security rules

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon test_xlsx_export
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,43 @@
[project]
name = "odoo-bringout-oca-ocb-test_xlsx_export"
version = "16.0.0"
description = "test xlsx export - Odoo addon"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-web>=16.0.0",
"odoo-bringout-oca-ocb-test_mail>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["test_xlsx_export"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'test xlsx export',
'version': '0.1',
'category': 'Hidden/Tests',
'description': """A module to test xlsx export.""",
'depends': ['web', 'test_mail'],
'data': ['ir.model.access.csv'],
'installable': True,
'license': 'LGPL-3',
}

View file

@ -0,0 +1,4 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
access_export_group_operator,access_export_group_operator,model_export_group_operator,,1,1,1,1
access_export_group_operator_one2many,access_export_group_operator_one2many,model_export_group_operator_one2many,,1,1,1,1
access_export_integer,access_export_integer,model_export_integer,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_export_group_operator access_export_group_operator model_export_group_operator 1 1 1 1
3 access_export_group_operator_one2many access_export_group_operator_one2many model_export_group_operator_one2many 1 1 1 1
4 access_export_integer access_export_integer model_export_integer 1 1 1 1

View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class NewModel(models.Model):
_name = 'export.integer'
_description = 'Export: Integer'
value = fields.Integer(default=4)
def name_get(self):
return [(record.id, "%s:%s" % (self._name, record.value)) for record in self]
class GroupOperator(models.Model):
_name = 'export.group_operator'
_description = 'Export Group Operator'
int_sum = fields.Integer(group_operator='sum')
int_max = fields.Integer(group_operator='max')
float_min = fields.Float(group_operator='min')
float_avg = fields.Float(group_operator='avg')
float_monetary = fields.Monetary(currency_field='currency_id', group_operator='sum')
currency_id = fields.Many2one('res.currency')
date_max = fields.Date(group_operator='max')
bool_and = fields.Boolean(group_operator='bool_and')
bool_or = fields.Boolean(group_operator='bool_or')
many2one = fields.Many2one('export.integer')
one2many = fields.One2many('export.group_operator.one2many', 'parent_id')
active = fields.Boolean(default=True)
class GroupOperatorO2M(models.Model):
_name = 'export.group_operator.one2many'
_description = 'Export Group Operator One2Many'
parent_id = fields.Many2one('export.group_operator')
value = fields.Integer()

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_export

View file

@ -0,0 +1,423 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from datetime import date
from unittest.mock import patch
from odoo import http
from odoo.tests import common, tagged
from odoo.tools.misc import get_lang
from odoo.addons.web.controllers.export import ExportXlsxWriter, Export
from odoo.addons.mail.tests.common import mail_new_test_user
class XlsxCreatorCase(common.HttpCase):
model_name = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model = None
def setUp(self):
super().setUp()
self.model = self.env[self.model_name]
mail_new_test_user(self.env, login='fof', password='123456789', groups='base.group_user,base.group_allow_export')
self.session = self.authenticate('fof', '123456789')
self.worksheet = {} # mock worksheet
self.default_params = {
'domain': [],
'fields': [{'name': field.name, 'label': field.string} for field in self.model._fields.values()],
'groupby': [],
'ids': False,
'import_compat': False,
'model': self.model._name,
}
def _mock_write(self, row, column, value, style=None):
if isinstance(value, float):
decimal_places = style.num_format[::-1].find('.')
style_format = "{:." + str(decimal_places) + "f}"
self.worksheet[row, column] = style_format.format(value)
else:
self.worksheet[row, column] = str(value)
def make(self, values, context=None):
return self.model.with_context(**(context or {})).create(values)
def export(self, values, fields=[], params={}, context=None):
self.worksheet = {}
self.make(values, context=context)
if fields and 'fields' not in params:
params['fields'] = [{
'name': self.model._fields[f].name,
'label': self.model._fields[f].string,
'type': self.model._fields[f].type,
} for f in fields]
with patch.object(ExportXlsxWriter, 'write', self._mock_write):
self.url_open('/web/export/xlsx', data={
'data': json.dumps(dict(self.default_params, **params)),
'csrf_token': http.Request.csrf_token(self),
})
return self.worksheet
def assertExportEqual(self, value, expected):
for row in range(len(expected)):
for column in range(len(expected[row])):
cell_value = value.pop((row, column), '')
expected_value = expected[row][column]
self.assertEqual(cell_value, expected_value, "Cell %s, %s have a wrong value" % (row, column))
self.assertFalse(value, "There are unexpected cells in the export")
@tagged('-at_install', 'post_install')
class TestGroupedExport(XlsxCreatorCase):
model_name = 'export.group_operator'
# pylint: disable=bad-whitespace
def test_archived_groupped(self):
""" The decimal separator of the language used shouldn't impact the float representation in the exported xlsx """
get_lang(self.env).decimal_point = ','
get_lang(self.env).thousands_sep = '.'
values = [
{'int_sum': 1, 'active': False},
]
export = self.export(values, fields=['int_sum', 'active'], params={'groupby': ['int_sum']})
self.assertExportEqual(export, [
['Int Sum', 'Active'],
['1 (1)', ''],
])
def test_int_sum_max(self):
values = [
{'int_sum': 10, 'int_max': 20},
{'int_sum': 10, 'int_max': 50},
{'int_sum': 20,'int_max': 30},
]
export = self.export(values, fields=['int_sum', 'int_max'], params={'groupby': ['int_sum', 'int_max']})
self.assertExportEqual(export, [
['Int Sum' ,'Int Max'],
['10 (2)' ,'50'],
[' 20 (1)' ,'20'],
['10' ,'20'],
[' 50 (1)' ,'50'],
['10' ,'50'],
['20 (1)' ,'30'],
[' 30 (1)' ,'30'],
['20' ,'30'],
])
export = self.export([], fields=['int_max', 'int_sum'], params={'groupby': ['int_sum', 'int_max']})
self.assertExportEqual(export, [
['Int Max' ,'Int Sum'],
['10 (2)' ,'20'],
[' 20 (1)' ,'10'],
['20' ,'10'],
[' 50 (1)' ,'10'],
['50' ,'10'],
['20 (1)' ,'20'],
[' 30 (1)' ,'20'],
['30' ,'20'],
])
def test_float_min(self):
values = [
{'int_sum': 10, 'float_min': 111.0},
{'int_sum': 10, 'float_min': 222.0},
{'int_sum': 20, 'float_min': 333.0},
]
export = self.export(values, fields=['int_sum', 'float_min'], params={'groupby': ['int_sum', 'float_min']})
self.assertExportEqual(export, [
['Int Sum' ,'Float Min'],
['10 (2)' ,'111.00'],
[' 111.0 (1)','111.00'],
['10' ,'111.00'],
[' 222.0 (1)','222.00'],
['10' ,'222.00'],
['20 (1)' ,'333.00'],
[' 333.0 (1)','333.00'],
['20' ,'333.00'],
])
def test_float_avg(self):
values = [
{'int_sum': 10, 'float_avg': 100.0},
{'int_sum': 10, 'float_avg': 200.0},
{'int_sum': 20, 'float_avg': 300.0},
]
export = self.export(values, fields=['int_sum', 'float_avg'], params={'groupby': ['int_sum', 'float_avg']})
self.assertExportEqual(export, [
['Int Sum' ,'Float Avg'],
['10 (2)' ,'150.00'],
[' 100.0 (1)','100.00'],
['10' ,'100.00'],
[' 200.0 (1)','200.00'],
['10' ,'200.00'],
['20 (1)' ,'300.00'],
[' 300.0 (1)','300.00'],
['20' ,'300.00'],
])
def test_float_avg_nested(self):
""" With more than one nested level (avg aggregation) """
values = [
{'int_sum': 10, 'int_max': 30, 'float_avg': 100.0},
{'int_sum': 10, 'int_max': 30, 'float_avg': 200.0},
{'int_sum': 10, 'int_max': 20, 'float_avg': 600.0},
]
export = self.export(values, fields=['int_sum', 'float_avg'], params={'groupby': ['int_sum', 'int_max', 'float_avg']})
self.assertExportEqual(export, [
['Int Sum' ,'Float Avg'],
['10 (3)' ,'300.00'],
[' 20 (1)' ,'600.00'],
[' 600.0 (1)','600.00'],
['10' ,'600.00'],
[' 30 (2)' ,'150.00'],
[' 100.0 (1)','100.00'],
['10' ,'100.00'],
[' 200.0 (1)','200.00'],
['10' ,'200.00'],
])
def test_float_avg_nested_no_value(self):
""" With more than one nested level (avg aggregation is done on 0, not False) """
values = [
{'int_sum': 10, 'int_max': 20, 'float_avg': False},
{'int_sum': 10, 'int_max': 30, 'float_avg': False},
{'int_sum': 10, 'int_max': 30, 'float_avg': False},
]
export = self.export(values, fields=['int_sum', 'float_avg'], params={'groupby': ['int_sum', 'int_max', 'float_avg']})
self.assertExportEqual(export, [
['Int Sum' ,'Float Avg'],
['10 (3)' ,'0.00'],
[' 20 (1)' ,'0.00'],
[' Undefined (1)','0.00'],
['10' ,'0.00'],
[' 30 (2)' ,'0.00'],
[' Undefined (2)','0.00'],
['10' ,'0.00'],
['10' ,'0.00'],
])
def test_date_max(self):
values = [
{'int_sum': 10, 'date_max': date(2019, 1, 1)},
{'int_sum': 10, 'date_max': date(2000, 1, 1)},
{'int_sum': 20, 'date_max': date(1980, 1, 1)},
]
export = self.export(values, fields=['int_sum', 'date_max'], params={'groupby': ['int_sum', 'date_max:month']})
self.assertExportEqual(export, [
['Int Sum' ,'Date Max'],
['10 (2)' ,'2019-01-01'],
[' January 2000 (1)' ,'2000-01-01'],
['10' ,'2000-01-01'],
[' January 2019 (1)' ,'2019-01-01'],
['10' ,'2019-01-01'],
['20 (1)' ,'1980-01-01'],
[' January 1980 (1)' ,'1980-01-01'],
['20' ,'1980-01-01'],
])
def test_bool_and(self):
values = [
{'int_sum': 10, 'bool_and': True},
{'int_sum': 10, 'bool_and': True},
{'int_sum': 20, 'bool_and': True},
{'int_sum': 20, 'bool_and': False},
]
export = self.export(values, fields=['int_sum', 'bool_and'], params={'groupby': ['int_sum', 'bool_and']})
self.assertExportEqual(export, [
['Int Sum' ,'Bool And'],
['10 (2)' ,'True'],
[' True (2)' ,'True'],
['10' ,'True'],
['10' ,'True'],
['20 (2)' ,'False'],
[' False (1)' ,'False'],
['20' ,'False'],
[' True (1)' ,'True'],
['20' ,'True'],
])
def test_bool_or(self):
values = [
{'int_sum': 10, 'bool_or': True},
{'int_sum': 10, 'bool_or': False},
{'int_sum': 20, 'bool_or': False},
{'int_sum': 20, 'bool_or': False},
]
export = self.export(values, fields=['int_sum', 'bool_or'], params={'groupby': ['int_sum', 'bool_or']})
self.assertExportEqual(export, [
['Int Sum' ,'Bool Or'],
['10 (2)' ,'True'],
[' False (1)' ,'False'],
['10' ,'False'],
[' True (1)' ,'True'],
['10' ,'True'],
['20 (2)' ,'False'],
[' False (2)' ,'False'],
['20' ,'False'],
['20' ,'False'],
])
def test_many2one(self):
values = [
{'int_sum': 10, 'many2one': self.env['export.integer'].create({}).id},
{'int_sum': 10},
]
export = self.export(values, fields=['int_sum', 'many2one'], params={'groupby': ['int_sum', 'many2one']})
self.assertExportEqual(export, [
['Int Sum' ,'Many2One'],
['10 (2)' ,''],
[' export.integer:4 (1)' ,''],
['10' ,'export.integer:4'],
[' Undefined (1)' ,''],
['10' ,''],
])
def test_nested_records(self):
"""
aggregated values currently not supported for nested record export, but it should not crash
e.g. export 'many2one/const'
"""
values = [{'int_sum': 10,
'date_max': date(2019, 1, 1),
'many2one': self.env['export.integer'].create({}).id,
}, {
'int_sum': 10,
'date_max': date(2000, 1, 1),
'many2one': self.env['export.integer'].create({}).id,
},]
export = self.export(values,
params={
'groupby': ['int_sum', 'date_max:month'],
'fields': [
{'name': 'int_sum', 'label': 'Int Sum'},
{'name': 'date_max', 'label': 'Date Max'},
{'name': 'many2one/value', 'label': 'Many2One/Value'},
]
})
self.assertExportEqual(export, [
['Int Sum' ,'Date Max' ,'Many2One/Value'],
['10 (2)' ,'2019-01-01' ,''],
[' January 2000 (1)' ,'2000-01-01' ,''],
['10' ,'2000-01-01' ,'4'],
[' January 2019 (1)' ,'2019-01-01' ,''],
['10' ,'2019-01-01' ,'4'],
])
def test_one2many(self):
values = [{
'int_sum': 10,
'one2many': [
(0, 0, {'value': 8}),
(0, 0, {'value': 9}),
],
}]
export = self.export(values,
params={
'groupby': ['int_sum',],
'fields': [
{'name': 'int_sum', 'label': 'Int Sum'},
{'name': 'one2many/value', 'label': 'One2many/Value'},
]
})
self.assertExportEqual(export, [
['Int Sum' ,'One2many/Value'],
['10 (1)' ,''],
['10' ,'8'],
['' ,'9'],
])
def test_unset_date_values(self):
values = [
{'int_sum': 10, 'date_max': date(2019, 1, 1)},
{'int_sum': 10, 'date_max': False},
]
# Group and aggregate by date, but date fields are not set for all records
export = self.export(values, fields=['int_sum', 'date_max'], params={'groupby': ['int_sum', 'date_max:month']})
self.assertExportEqual(export, [
['Int Sum' ,'Date Max'],
['10 (2)' ,'2019-01-01'],
[' January 2019 (1)' ,'2019-01-01'],
['10' ,'2019-01-01'],
[' Undefined (1)' ,''],
['10' ,''],
])
def test_float_representation(self):
currency = self.env['res.currency'].create({
'name': "bottlecap",
'symbol': "b",
'rounding': 0.001,
'decimal_places': 3,
})
values = [
{'int_sum': 1, 'currency_id': currency.id, 'float_monetary': 60739.2000000004},
{'int_sum': 2, 'currency_id': currency.id, 'float_monetary': 2.0},
{'int_sum': 3, 'currency_id': currency.id, 'float_monetary': 999.9995999},
]
export = self.export(values, fields=['int_sum', 'float_monetary'], params={'groupby': ['int_sum', 'float_monetary']})
self.assertExportEqual(export, [
['Int Sum', 'Float Monetary'],
['1 (1)', '60739.200'],
[' 60739.2 (1)', '60739.200'],
['1', '60739.20'],
['2 (1)', '2.000'],
[' 2.0 (1)', '2.000'],
['2', '2.00'],
['3 (1)', '1000.000'],
[' 1000.0 (1)', '1000.000'],
['3', '1000.00'],
])
def test_decimal_separator(self):
""" The decimal separator of the language used shouldn't impact the float representation in the exported xlsx """
get_lang(self.env).decimal_point = ','
get_lang(self.env).thousands_sep = '.'
values = [
{'int_sum': 1, 'float_min': 86420.864},
]
export = self.export(values, fields=['int_sum', 'float_min'], params={'groupby': ['int_sum', 'float_min']})
self.assertExportEqual(export, [
['Int Sum' ,'Float Min'],
['1 (1)' ,'86420.86'],
[' 86420.864 (1)','86420.86'],
['1' ,'86420.86'],
])
@tagged('-at_install', 'post_install')
class TestExport(common.HttpCase):
def test_properties_type_fields_not_selectable_with_import_compat(self):
with patch.object(Export, 'fields_get', return_value={
'id': {'string': 'ID', 'type': 'integer'},
'name': {'string': 'Name', 'type': 'char'},
'properties': {'string': 'Properties', 'type': 'properties'},
'properties_definition': {'string': 'Properties Definition', 'type': 'properties_definition'}
}):
fields = Export().get_fields("mock_model", import_compat=True)
field_names = [field['id'] for field in fields]
self.assertNotIn('properties', field_names)
self.assertNotIn('properties_definition', field_names)