first commit
This commit is contained in:
18
modules/product/__init__.py
Normal file
18
modules/product/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
__all__ = [
|
||||
'price_digits', 'round_price', 'uom_conversion_digits',
|
||||
'ProductDeactivatableMixin', 'TemplateDeactivatableMixin']
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name == 'uom_conversion_digits':
|
||||
from .uom import uom_conversion_digits
|
||||
return uom_conversion_digits
|
||||
elif name in {
|
||||
'price_digits', 'round_price',
|
||||
'ProductDeactivatableMixin', 'TemplateDeactivatableMixin'}:
|
||||
from . import product
|
||||
return getattr(product, name)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
BIN
modules/product/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
modules/product/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/product/__pycache__/category.cpython-311.pyc
Normal file
BIN
modules/product/__pycache__/category.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/product/__pycache__/configuration.cpython-311.pyc
Normal file
BIN
modules/product/__pycache__/configuration.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/product/__pycache__/exceptions.cpython-311.pyc
Normal file
BIN
modules/product/__pycache__/exceptions.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/product/__pycache__/ir.cpython-311.pyc
Normal file
BIN
modules/product/__pycache__/ir.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/product/__pycache__/product.cpython-311.pyc
Normal file
BIN
modules/product/__pycache__/product.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/product/__pycache__/uom.cpython-311.pyc
Normal file
BIN
modules/product/__pycache__/uom.cpython-311.pyc
Normal file
Binary file not shown.
112
modules/product/category.py
Normal file
112
modules/product/category.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from sql.conditionals import NullIf
|
||||
from sql.functions import CharLength
|
||||
from sql.operators import Equal
|
||||
|
||||
from trytond.model import Exclude, ModelSQL, ModelView, fields, tree
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval, PYSONEncoder
|
||||
from trytond.tools import is_full_text, lstrip_wildcard
|
||||
|
||||
|
||||
class Category(tree(separator=' / '), ModelSQL, ModelView):
|
||||
__name__ = "product.category"
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
code = fields.Char(
|
||||
"Code",
|
||||
states={
|
||||
'readonly': Eval('code_readonly', False),
|
||||
})
|
||||
code_readonly = fields.Function(
|
||||
fields.Boolean("Code Readonly"), 'get_code_readonly')
|
||||
parent = fields.Many2One(
|
||||
'product.category', "Parent",
|
||||
help="Used to add structure above the category.")
|
||||
childs = fields.One2Many(
|
||||
'product.category', 'parent', string="Children",
|
||||
help="Used to add structure below the category.")
|
||||
templates = fields.Many2Many(
|
||||
'product.template-product.category', 'category', 'template',
|
||||
"Products")
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
cls.code.search_unaccented = False
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_constraints += [
|
||||
('code_unique', Exclude(t, (NullIf(t.code, ''), Equal)),
|
||||
'product.msg_category_code_unique'),
|
||||
]
|
||||
cls._order.insert(0, ('name', 'ASC'))
|
||||
cls._buttons.update({
|
||||
'add_products': {
|
||||
'icon': 'tryton-add',
|
||||
},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def default_code_readonly(cls):
|
||||
pool = Pool()
|
||||
Configuration = pool.get('product.configuration')
|
||||
config = Configuration(1)
|
||||
return bool(config.category_sequence)
|
||||
|
||||
def get_code_readonly(self, name):
|
||||
return self.default_code_readonly()
|
||||
|
||||
@classmethod
|
||||
def order_code(cls, tables):
|
||||
table, _ = tables[None]
|
||||
if cls.default_code_readonly():
|
||||
return [CharLength(table.code), table.code]
|
||||
else:
|
||||
return [table.code]
|
||||
|
||||
@classmethod
|
||||
def search_rec_name(cls, name, clause):
|
||||
_, operator, operand, *extra = clause
|
||||
if operator.startswith('!') or operator.startswith('not '):
|
||||
bool_op = 'AND'
|
||||
else:
|
||||
bool_op = 'OR'
|
||||
code_value = operand
|
||||
if operator.endswith('like') and is_full_text(operand):
|
||||
code_value = lstrip_wildcard(operand)
|
||||
return [bool_op,
|
||||
('name', operator, operand, *extra),
|
||||
('code', operator, code_value, *extra),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@ModelView.button_action('product.act_category_product')
|
||||
def add_products(cls, categories):
|
||||
return {
|
||||
'res_id': [categories[0].id if categories else None],
|
||||
'pyson_domain': PYSONEncoder().encode(
|
||||
[('id', '=', categories[0].id if categories else None)]),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _code_sequence(cls):
|
||||
pool = Pool()
|
||||
Configuration = pool.get('product.configuration')
|
||||
config = Configuration(1)
|
||||
return config.category_sequence
|
||||
|
||||
@classmethod
|
||||
def preprocess_values(cls, mode, values):
|
||||
values = super().preprocess_values(mode, values)
|
||||
if mode == 'create' and not values.get('code'):
|
||||
if sequence := cls._code_sequence():
|
||||
values['code'] = sequence.get()
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def copy(cls, categories, default=None):
|
||||
default = default.copy() if default is not None else {}
|
||||
default.setdefault('templates')
|
||||
default.setdefault('code', None)
|
||||
return super().copy(categories, default=default)
|
||||
118
modules/product/category.xml
Normal file
118
modules/product/category.xml
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="category_view_list">
|
||||
<field name="model">product.category</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="name">category_list</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="category_view_tree">
|
||||
<field name="model">product.category</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="field_childs">childs</field>
|
||||
<field name="name">category_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="category_view_form">
|
||||
<field name="model">product.category</field>
|
||||
<field name="type">form</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="name">category_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="category_view_form_product">
|
||||
<field name="model">product.category</field>
|
||||
<field name="type">form</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="name">category_product_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_category_tree">
|
||||
<field name="name">Categories</field>
|
||||
<field name="res_model">product.category</field>
|
||||
<field name="domain" eval="[('parent', '=', None)]" pyson="1"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_category_tree_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="category_view_tree"/>
|
||||
<field name="act_window" ref="act_category_tree"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_category_tree_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="category_view_form"/>
|
||||
<field name="act_window" ref="act_category_tree"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
parent="menu_main_product"
|
||||
action="act_category_tree"
|
||||
sequence="20"
|
||||
id="menu_category_tree"/>
|
||||
|
||||
<record model="ir.action.act_window" id="act_category_list">
|
||||
<field name="name">Categories</field>
|
||||
<field name="res_model">product.category</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_category_list_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="category_view_list"/>
|
||||
<field name="act_window" ref="act_category_list"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_category_list_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="category_view_form"/>
|
||||
<field name="act_window" ref="act_category_list"/>
|
||||
</record>
|
||||
<menuitem
|
||||
parent="menu_category_tree"
|
||||
action="act_category_list"
|
||||
sequence="10"
|
||||
id="menu_category_list"/>
|
||||
|
||||
<record model="ir.action.act_window" id="act_category_product">
|
||||
<field name="name">Add products</field>
|
||||
<field name="res_model">product.category</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_category_product_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="category_view_form_product"/>
|
||||
<field name="act_window" ref="act_category_product"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.button" id="category_add_products_button">
|
||||
<field name="model">product.category</field>
|
||||
<field name="name">add_products</field>
|
||||
<field name="string">Add products</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_product_category">
|
||||
<field name="model">product.category</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_product_category_admin">
|
||||
<field name="model">product.category</field>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.sequence.type" id="sequence_type_category">
|
||||
<field name="name">Category</field>
|
||||
</record>
|
||||
<record model="ir.sequence.type-res.group" id="sequence_type_category_group_admin">
|
||||
<field name="sequence_type" ref="sequence_type_category"/>
|
||||
<field name="group" ref="res.group_admin"/>
|
||||
</record>
|
||||
<record model="ir.sequence.type-res.group" id="sequence_type_category_group_product_admin">
|
||||
<field name="sequence_type" ref="sequence_type_category"/>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
57
modules/product/configuration.py
Normal file
57
modules/product/configuration.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from trytond.model import (
|
||||
ModelSingleton, ModelSQL, ModelView, MultiValueMixin, ValueMixin, fields)
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Id
|
||||
|
||||
default_cost_price_method = fields.Selection(
|
||||
'get_cost_price_methods', "Default Cost Method",
|
||||
help="The default cost price method for new products.")
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_cost_price_methods(cls):
|
||||
pool = Pool()
|
||||
Template = pool.get('product.template')
|
||||
field_name = 'cost_price_method'
|
||||
return (Template.fields_get([field_name])[field_name]['selection']
|
||||
+ [(None, '')])
|
||||
|
||||
|
||||
class Configuration(ModelSingleton, ModelSQL, ModelView, MultiValueMixin):
|
||||
__name__ = 'product.configuration'
|
||||
|
||||
default_cost_price_method = fields.MultiValue(default_cost_price_method)
|
||||
get_cost_price_methods = get_cost_price_methods
|
||||
product_sequence = fields.Many2One('ir.sequence', "Variant Sequence",
|
||||
domain=[
|
||||
('sequence_type', '=', Id('product', 'sequence_type_product')),
|
||||
],
|
||||
help="Used to generate the last part of the product code.")
|
||||
template_sequence = fields.Many2One('ir.sequence', "Product Sequence",
|
||||
domain=[
|
||||
('sequence_type', '=', Id('product', 'sequence_type_template')),
|
||||
],
|
||||
help="Used to generate the first part of the product code.")
|
||||
category_sequence = fields.Many2One(
|
||||
'ir.sequence', "Category Sequence",
|
||||
domain=[
|
||||
('sequence_type', '=', Id('product', 'sequence_type_category')),
|
||||
],
|
||||
help="Used to generate the category code.")
|
||||
|
||||
@classmethod
|
||||
def default_default_cost_price_method(cls, **pattern):
|
||||
return cls.multivalue_model(
|
||||
'default_cost_price_method').default_default_cost_price_method()
|
||||
|
||||
|
||||
class ConfigurationDefaultCostPriceMethod(ModelSQL, ValueMixin):
|
||||
__name__ = 'product.configuration.default_cost_price_method'
|
||||
default_cost_price_method = default_cost_price_method
|
||||
get_cost_price_methods = get_cost_price_methods
|
||||
|
||||
@classmethod
|
||||
def default_default_cost_price_method(cls):
|
||||
return 'fixed'
|
||||
57
modules/product/configuration.xml
Normal file
57
modules/product/configuration.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="product_configuration_view_form">
|
||||
<field name="model">product.configuration</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">configuration_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window"
|
||||
id="act_product_configuration_form">
|
||||
<field name="name">Configuration</field>
|
||||
<field name="res_model">product.configuration</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_product_configuration_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="product_configuration_view_form"/>
|
||||
<field name="act_window" ref="act_product_configuration_form"/>
|
||||
</record>
|
||||
<menuitem
|
||||
parent="menu_configuration"
|
||||
action="act_product_configuration_form"
|
||||
sequence="10"
|
||||
id="menu_product_configuration"
|
||||
icon="tryton-list"/>
|
||||
<record model="ir.ui.menu-res.group"
|
||||
id="menu_product_configuration_group_product_admin">
|
||||
<field name="menu" ref="menu_product_configuration"/>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_product_configuration">
|
||||
<field name="model">product.configuration</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_product_configuration_product_admin">
|
||||
<field name="model">product.configuration</field>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record model="product.configuration.default_cost_price_method"
|
||||
id="cost_price_method">
|
||||
<field name="default_cost_price_method">fixed</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
24
modules/product/exceptions.py
Normal file
24
modules/product/exceptions.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from trytond.model.exceptions import AccessError, ValidationError
|
||||
|
||||
|
||||
class TemplateValidationError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class ProductValidationError(TemplateValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class UOMValidationError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class UOMAccessError(AccessError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidIdentifierCode(ValidationError):
|
||||
pass
|
||||
4
modules/product/icons/tryton-product.svg
Normal file
4
modules/product/icons/tryton-product.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M13 13v8h8v-8h-8zM3 21h8v-8H3v8zM3 3v8h8V3H3zm13.66-1.31L11 7.34 16.66 13l5.66-5.66-5.66-5.65z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 244 B |
36
modules/product/ir.py
Normal file
36
modules/product/ir.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
import trytond.config as config
|
||||
from trytond.model import fields
|
||||
from trytond.pool import PoolMeta
|
||||
|
||||
price_decimal = config.getint('product', 'price_decimal', default=4)
|
||||
|
||||
|
||||
class Configuration(metaclass=PoolMeta):
|
||||
__name__ = 'ir.configuration'
|
||||
product_price_decimal = fields.Integer("Product Price Decimal")
|
||||
|
||||
@classmethod
|
||||
def default_product_price_decimal(cls):
|
||||
return price_decimal
|
||||
|
||||
def check(self):
|
||||
super().check()
|
||||
if self.product_price_decimal != price_decimal:
|
||||
raise ValueError(
|
||||
"The price_decimal %s in [product] configuration section "
|
||||
"is different from the value %s in 'ir.configuration'." % (
|
||||
price_decimal, self.product_price_decimal))
|
||||
|
||||
|
||||
class Cron(metaclass=PoolMeta):
|
||||
__name__ = 'ir.cron'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.method.selection.append(
|
||||
('product.product|deactivate_replaced',
|
||||
"Deactivate Replaced Products"))
|
||||
1274
modules/product/locale/bg.po
Normal file
1274
modules/product/locale/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
1164
modules/product/locale/ca.po
Normal file
1164
modules/product/locale/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
1188
modules/product/locale/cs.po
Normal file
1188
modules/product/locale/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
1174
modules/product/locale/de.po
Normal file
1174
modules/product/locale/de.po
Normal file
File diff suppressed because it is too large
Load Diff
1165
modules/product/locale/es.po
Normal file
1165
modules/product/locale/es.po
Normal file
File diff suppressed because it is too large
Load Diff
1196
modules/product/locale/es_419.po
Normal file
1196
modules/product/locale/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
1185
modules/product/locale/et.po
Normal file
1185
modules/product/locale/et.po
Normal file
File diff suppressed because it is too large
Load Diff
1192
modules/product/locale/fa.po
Normal file
1192
modules/product/locale/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
1183
modules/product/locale/fi.po
Normal file
1183
modules/product/locale/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
1171
modules/product/locale/fr.po
Normal file
1171
modules/product/locale/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
1201
modules/product/locale/hu.po
Normal file
1201
modules/product/locale/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
1168
modules/product/locale/id.po
Normal file
1168
modules/product/locale/id.po
Normal file
File diff suppressed because it is too large
Load Diff
1195
modules/product/locale/it.po
Normal file
1195
modules/product/locale/it.po
Normal file
File diff suppressed because it is too large
Load Diff
1286
modules/product/locale/lo.po
Normal file
1286
modules/product/locale/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
1195
modules/product/locale/lt.po
Normal file
1195
modules/product/locale/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
1167
modules/product/locale/nl.po
Normal file
1167
modules/product/locale/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
1185
modules/product/locale/pl.po
Normal file
1185
modules/product/locale/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
1170
modules/product/locale/pt.po
Normal file
1170
modules/product/locale/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
1188
modules/product/locale/ro.po
Normal file
1188
modules/product/locale/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
1297
modules/product/locale/ru.po
Normal file
1297
modules/product/locale/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
1226
modules/product/locale/sl.po
Normal file
1226
modules/product/locale/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
1237
modules/product/locale/tr.po
Normal file
1237
modules/product/locale/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
1143
modules/product/locale/uk.po
Normal file
1143
modules/product/locale/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
1183
modules/product/locale/zh_CN.po
Normal file
1183
modules/product/locale/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
40
modules/product/message.xml
Normal file
40
modules/product/message.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data grouped="1">
|
||||
<record model="ir.message" id="msg_uom_modify_factor">
|
||||
<field name="text">You cannot modify the factor of the unit of measure "%(uom)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_uom_modify_rate">
|
||||
<field name="text">You cannot modify the rate of the unit of measure"%(uom)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_uom_modify_category">
|
||||
<field name="text">You cannot modify the category of the unit of measure "%(uom)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_uom_decrease_digits">
|
||||
<field name="text">You cannot decrease the digits of the unit of measure "%(uom)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_uom_modify_options">
|
||||
<field name="text">If the unit of measure is still not used, you can delete it otherwise you can deactivate it and create a new one.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_uom_incompatible_factor_rate">
|
||||
<field name="text">Incompatible factor and rate values on unit of measure"%(uom)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_uom_no_zero_factor_rate">
|
||||
<field name="text">Rate and factor can not be both equal to zero.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_invalid_code">
|
||||
<field name="text">The %(type)s "%(code)s" for product "%(product)s" is not valid.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_product_code_unique">
|
||||
<field name="text">Code of active product must be unique.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_category_code_unique">
|
||||
<field name="text">The code on product category must be unique.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_product_change_template">
|
||||
<field name="text">You cannot change the template of the product "%(product)s".</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
1049
modules/product/product.py
Normal file
1049
modules/product/product.py
Normal file
File diff suppressed because it is too large
Load Diff
276
modules/product/product.xml
Normal file
276
modules/product/product.xml
Normal file
@@ -0,0 +1,276 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
|
||||
<record model="res.group" id="group_product_admin">
|
||||
<field name="name">Product Administration</field>
|
||||
</record>
|
||||
<record model="res.user-res.group" id="user_admin_group_product_admin">
|
||||
<field name="user" ref="res.user_admin"/>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.icon" id="product_icon">
|
||||
<field name="name">tryton-product</field>
|
||||
<field name="path">icons/tryton-product.svg</field>
|
||||
</record>
|
||||
<menuitem
|
||||
name="Products"
|
||||
sequence="30"
|
||||
id="menu_main_product"
|
||||
icon="tryton-product"/>
|
||||
|
||||
<menuitem
|
||||
name="Configuration"
|
||||
parent="menu_main_product"
|
||||
sequence="0"
|
||||
id="menu_configuration"
|
||||
icon="tryton-settings"/>
|
||||
<record model="ir.ui.menu-res.group"
|
||||
id="menu_product_group_product_admin">
|
||||
<field name="menu" ref="menu_configuration"/>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Reporting"
|
||||
parent="menu_main_product"
|
||||
sequence="100"
|
||||
id="menu_reporting"/>
|
||||
|
||||
<record model="ir.ui.view" id="template_view_tree">
|
||||
<field name="model">product.template</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">template_tree</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="template_view_form">
|
||||
<field name="model">product.template</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">template_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_template_form">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.template</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_template_list_view">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="template_view_tree"/>
|
||||
<field name="act_window" ref="act_template_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_template_form_view">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="template_view_form"/>
|
||||
<field name="act_window" ref="act_template_form"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
parent="menu_main_product"
|
||||
action="act_template_form"
|
||||
sequence="10"
|
||||
id="menu_template"/>
|
||||
|
||||
<record model="ir.action.act_window" id="act_template_by_category">
|
||||
<field name="name">Product by Category</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="context"
|
||||
eval="{'categories': [Eval('active_id')]}" pyson="1"/>
|
||||
<field name="domain"
|
||||
eval="[('categories_all','child_of', [Eval('active_id')], 'parent')]"
|
||||
pyson="1"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_template_by_category_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="template_view_tree"/>
|
||||
<field name="act_window" ref="act_template_by_category"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_template_by_category_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="template_view_form"/>
|
||||
<field name="act_window" ref="act_template_by_category"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="act_template_by_category_keyword1">
|
||||
<field name="keyword">tree_open</field>
|
||||
<field name="model">product.category,-1</field>
|
||||
<field name="action" ref="act_template_by_category"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_view_tree">
|
||||
<field name="model">product.product</field>
|
||||
<field name="type" eval="None"/>
|
||||
<field name="inherit" ref="template_view_tree"/>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="name">product_tree</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_view_tree_simple">
|
||||
<field name="model">product.product</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="name">product_tree_simple</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_view_tree_simple_sequence">
|
||||
<field name="model">product.product</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="name">product_tree_simple_sequence</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_view_form">
|
||||
<field name="model">product.product</field>
|
||||
<field name="type" eval="None"/>
|
||||
<field name="inherit" ref="template_view_form"/>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="name">product_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_view_form_simple">
|
||||
<field name="model">product.product</field>
|
||||
<field name="type">form</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="name">product_form_simple</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_product_form">
|
||||
<field name="name">Variants</field>
|
||||
<field name="res_model">product.product</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_product_list_view">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="product_view_tree"/>
|
||||
<field name="act_window" ref="act_product_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_product_form_view">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="product_view_form"/>
|
||||
<field name="act_window" ref="act_product_form"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
parent="menu_template"
|
||||
action="act_product_form"
|
||||
sequence="10"
|
||||
id="menu_product"
|
||||
icon="tryton-list"/>
|
||||
|
||||
<record model="ir.model.access" id="access_product_template">
|
||||
<field name="model">product.template</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_product_template_admin">
|
||||
<field name="model">product.template</field>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_product_from_template">
|
||||
<field name="name">Variants</field>
|
||||
<field name="res_model">product.product</field>
|
||||
<field name="domain" pyson="1"
|
||||
eval="[If(Eval('active_ids', []) == [Eval('active_id')], ('template', '=', Eval('active_id')), ('template', 'in', Eval('active_ids')))]"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_product_from_template_list_view">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="product_view_tree"/>
|
||||
<field name="act_window" ref="act_product_from_template"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_productfrom_template_form_view">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="product_view_form"/>
|
||||
<field name="act_window" ref="act_product_from_template"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword"
|
||||
id="act_product_from_template_keyword1">
|
||||
<field name="keyword">form_relate</field>
|
||||
<field name="model">product.template,-1</field>
|
||||
<field name="action" ref="act_product_from_template"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="identifier_view_form">
|
||||
<field name="model">product.identifier</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">identifier_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="identifier_view_list">
|
||||
<field name="model">product.identifier</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="name">identifier_list</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="identifier_view_list_sequence">
|
||||
<field name="model">product.identifier</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="name">identifier_list_sequence</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.wizard" id="wizard_product_replace">
|
||||
<field name="name">Replace</field>
|
||||
<field name="wiz_name">product.product.replace</field>
|
||||
<field name="model">product.product</field>
|
||||
</record>
|
||||
<record model="ir.action-res.group" id="wizard_product_replace-group_product_admin">
|
||||
<field name="action" ref="wizard_product_replace"/>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="wizard_product_replace_keyword1">
|
||||
<field name="keyword">form_action</field>
|
||||
<field name="model">product.product,-1</field>
|
||||
<field name="action" ref="wizard_product_replace"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_replace_ask_view_form">
|
||||
<field name="model">product.product.replace.ask</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">product_replace_ask_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.sequence.type" id="sequence_type_product">
|
||||
<field name="name">Variant</field>
|
||||
</record>
|
||||
<record model="ir.sequence.type-res.group"
|
||||
id="sequence_type_product_group_admin">
|
||||
<field name="sequence_type" ref="sequence_type_product"/>
|
||||
<field name="group" ref="res.group_admin"/>
|
||||
</record>
|
||||
<record model="ir.sequence.type-res.group"
|
||||
id="sequence_type_product_group_product_admin">
|
||||
<field name="sequence_type" ref="sequence_type_product"/>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
</record>
|
||||
<record model="ir.sequence.type" id="sequence_type_template">
|
||||
<field name="name">Product</field>
|
||||
</record>
|
||||
<record model="ir.sequence.type-res.group"
|
||||
id="sequence_type_template_group_admin">
|
||||
<field name="sequence_type" ref="sequence_type_template"/>
|
||||
<field name="group" ref="res.group_admin"/>
|
||||
</record>
|
||||
<record model="ir.sequence.type-res.group"
|
||||
id="sequence_type_template_group_template_admin">
|
||||
<field name="sequence_type" ref="sequence_type_template"/>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
<record model="ir.cron" id="cron_product_deactivate_replaced">
|
||||
<field name="method">product.product|deactivate_replaced</field>
|
||||
<field name="interval_number" eval="1"/>
|
||||
<field name="interval_type">days</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
2
modules/product/tests/__init__.py
Normal file
2
modules/product/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
BIN
modules/product/tests/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
modules/product/tests/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/product/tests/__pycache__/test_module.cpython-311.pyc
Normal file
BIN
modules/product/tests/__pycache__/test_module.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modules/product/tests/__pycache__/test_scenario.cpython-311.pyc
Normal file
BIN
modules/product/tests/__pycache__/test_scenario.cpython-311.pyc
Normal file
Binary file not shown.
47
modules/product/tests/scenario_product_identifier.rst
Normal file
47
modules/product/tests/scenario_product_identifier.rst
Normal file
@@ -0,0 +1,47 @@
|
||||
===========================
|
||||
Product Identifier Scenario
|
||||
===========================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from proteus import Model
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('product')
|
||||
|
||||
Create product::
|
||||
|
||||
>>> ProductUom = Model.get('product.uom')
|
||||
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
|
||||
>>> ProductTemplate = Model.get('product.template')
|
||||
>>> template = ProductTemplate()
|
||||
>>> template.name = 'product'
|
||||
>>> template.default_uom = unit
|
||||
>>> template.type = 'service'
|
||||
>>> template.save()
|
||||
>>> product, = template.products
|
||||
|
||||
The identifier code is computed when set::
|
||||
|
||||
>>> identifier = product.identifiers.new()
|
||||
>>> identifier.type = 'ean'
|
||||
>>> identifier.code = '123 456 7890 123'
|
||||
>>> identifier.code
|
||||
'1234567890123'
|
||||
|
||||
An Error is raised for invalid code::
|
||||
|
||||
>>> product.save()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
InvalidIdentifierCode: ...
|
||||
|
||||
Valid codes are saved correctly::
|
||||
|
||||
>>> identifier.code = '978-0-471-11709-4'
|
||||
>>> product.save()
|
||||
>>> identifier, = product.identifiers
|
||||
>>> identifier.code
|
||||
'9780471117094'
|
||||
85
modules/product/tests/scenario_product_replace.rst
Normal file
85
modules/product/tests/scenario_product_replace.rst
Normal file
@@ -0,0 +1,85 @@
|
||||
========================
|
||||
Product Replace Scenario
|
||||
========================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from proteus import Model, Wizard
|
||||
>>> from trytond.tests.tools import activate_modules, assertEqual, assertFalse
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('product')
|
||||
|
||||
>>> Cron = Model.get('ir.cron')
|
||||
>>> ProductTemplate = Model.get('product.template')
|
||||
>>> UoM = Model.get('product.uom')
|
||||
|
||||
Get units::
|
||||
|
||||
>>> unit, = UoM.find([('name', '=', "Unit")])
|
||||
>>> kg, = UoM.find([('name', '=', "Kilogram")])
|
||||
|
||||
Create a product::
|
||||
|
||||
>>> template = ProductTemplate()
|
||||
>>> template.name = "Product"
|
||||
>>> template.type = 'goods'
|
||||
>>> template.default_uom = kg
|
||||
>>> template.save()
|
||||
>>> product1, = template.products
|
||||
|
||||
Create a second product::
|
||||
|
||||
>>> template = ProductTemplate()
|
||||
>>> template.name = "Product"
|
||||
>>> template.type = 'service'
|
||||
>>> template.default_uom = unit
|
||||
>>> template.save()
|
||||
>>> product2, = template.products
|
||||
|
||||
Try to replace goods with service::
|
||||
|
||||
>>> replace = Wizard('product.product.replace', models=[product1])
|
||||
>>> assertEqual(replace.form.source, product1)
|
||||
>>> replace.form.destination = product2
|
||||
>>> replace.execute('replace')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
DomainValidationError: ...
|
||||
|
||||
Try to replace with different categories of unit of measure::
|
||||
|
||||
>>> product2.template.type = 'goods'
|
||||
>>> product2.template.save()
|
||||
|
||||
>>> replace = Wizard('product.product.replace', models=[product1])
|
||||
>>> replace.form.destination = product2
|
||||
>>> replace.execute('replace')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
DomainValidationError: ...
|
||||
|
||||
Replace product::
|
||||
|
||||
>>> product2.template.default_uom = kg
|
||||
>>> product2.template.save()
|
||||
|
||||
>>> replace = Wizard('product.product.replace', models=[product1])
|
||||
>>> replace.form.destination = product2
|
||||
>>> replace.execute('replace')
|
||||
>>> assertEqual(product1.replaced_by, product2)
|
||||
>>> assertFalse(product1.active)
|
||||
|
||||
Cron task deactivate replaced product::
|
||||
|
||||
>>> product1.active = True
|
||||
>>> product1.save()
|
||||
|
||||
>>> deactivate_replaced, = Cron.find([
|
||||
... ('method', '=', 'product.product|deactivate_replaced'),
|
||||
... ])
|
||||
>>> deactivate_replaced.click('run_once')
|
||||
|
||||
>>> product1.reload()
|
||||
>>> assertFalse(product1.active)
|
||||
114
modules/product/tests/scenario_product_variant.rst
Normal file
114
modules/product/tests/scenario_product_variant.rst
Normal file
@@ -0,0 +1,114 @@
|
||||
========================
|
||||
Product Variant Scenario
|
||||
========================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from decimal import Decimal
|
||||
|
||||
>>> from proteus import Model
|
||||
>>> from trytond.modules.company.tests.tools import create_company
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
|
||||
Activate modules::
|
||||
|
||||
>>> config = activate_modules('product', create_company)
|
||||
|
||||
Create a template::
|
||||
|
||||
>>> ProductUom = Model.get('product.uom')
|
||||
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
|
||||
>>> ProductTemplate = Model.get('product.template')
|
||||
>>> template = ProductTemplate()
|
||||
>>> template.name = "Product"
|
||||
>>> template.default_uom = unit
|
||||
>>> template.list_price = Decimal('42.0000')
|
||||
>>> template.code = "PROD"
|
||||
>>> template.save()
|
||||
>>> len(template.products)
|
||||
1
|
||||
>>> product1, = template.products
|
||||
>>> product1.code
|
||||
'PROD'
|
||||
>>> product1.suffix_code = "001"
|
||||
>>> product1.save()
|
||||
>>> product1.code
|
||||
'PROD001'
|
||||
|
||||
Create a variant::
|
||||
|
||||
>>> Product = Model.get('product.product')
|
||||
>>> product2 = Product()
|
||||
>>> product2.template = template
|
||||
>>> product2.name
|
||||
'Product'
|
||||
>>> product2.suffix_code = "002"
|
||||
>>> product2.save()
|
||||
>>> product2.list_price_used
|
||||
Decimal('42.0000')
|
||||
>>> product2.code
|
||||
'PROD002'
|
||||
|
||||
Change variant list price::
|
||||
|
||||
>>> product2.list_price = Decimal('50.0000')
|
||||
>>> product2.save()
|
||||
>>> product2.list_price
|
||||
Decimal('50.0000')
|
||||
>>> product2.list_price_used
|
||||
Decimal('50.0000')
|
||||
|
||||
>>> product1.reload()
|
||||
>>> product1.list_price
|
||||
>>> product1.list_price_used
|
||||
Decimal('42.0000')
|
||||
|
||||
>>> template.reload()
|
||||
>>> template.list_price
|
||||
Decimal('42.0000')
|
||||
|
||||
Change product list price::
|
||||
|
||||
>>> template.list_price = Decimal('40.0000')
|
||||
>>> template.save()
|
||||
|
||||
>>> product2.reload()
|
||||
>>> product2.list_price
|
||||
Decimal('50.0000')
|
||||
>>> product2.list_price_used
|
||||
Decimal('50.0000')
|
||||
|
||||
>>> product1.reload()
|
||||
>>> product1.list_price
|
||||
>>> product1.list_price_used
|
||||
Decimal('40.0000')
|
||||
|
||||
Change template code::
|
||||
|
||||
>>> template.code = "PRD"
|
||||
>>> template.save()
|
||||
>>> sorted([p.code for p in template.products])
|
||||
['PRD001', 'PRD002']
|
||||
|
||||
Create template with trailing space in code::
|
||||
|
||||
>>> template = ProductTemplate()
|
||||
>>> template.name = "Product"
|
||||
>>> template.code = "TRAILING "
|
||||
>>> template.default_uom = unit
|
||||
>>> template.save()
|
||||
>>> product, = template.products
|
||||
>>> product.code
|
||||
'TRAILING'
|
||||
|
||||
Create product with leading space in code::
|
||||
|
||||
>>> template = ProductTemplate()
|
||||
>>> template.name = "Product"
|
||||
>>> template.default_uom = unit
|
||||
>>> product, = template.products
|
||||
>>> product.suffix_code = " LEADING"
|
||||
>>> template.save()
|
||||
>>> product, = template.products
|
||||
>>> product.code
|
||||
'LEADING'
|
||||
618
modules/product/tests/test_module.py
Normal file
618
modules/product/tests/test_module.py
Normal file
@@ -0,0 +1,618 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
import io
|
||||
import unittest
|
||||
from decimal import Decimal
|
||||
|
||||
from trytond.modules.company.tests import CompanyTestMixin
|
||||
from trytond.modules.product import round_price
|
||||
from trytond.modules.product.exceptions import UOMAccessError
|
||||
from trytond.modules.product.product import barcode
|
||||
from trytond.pool import Pool
|
||||
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class ProductTestCase(CompanyTestMixin, ModuleTestCase):
|
||||
'Test Product module'
|
||||
module = 'product'
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_non_zero_rate_factor(self):
|
||||
'Test uom non_zero_rate_factor constraint'
|
||||
pool = Pool()
|
||||
UomCategory = pool.get('product.uom.category')
|
||||
Uom = pool.get('product.uom')
|
||||
transaction = Transaction()
|
||||
category, = UomCategory.create([{'name': 'Test'}])
|
||||
|
||||
self.assertRaises(Exception, Uom.create, [{
|
||||
'name': 'Test',
|
||||
'symbol': 'T',
|
||||
'category': category.id,
|
||||
'rate': 0,
|
||||
'factor': 0,
|
||||
}])
|
||||
transaction.rollback()
|
||||
|
||||
def create():
|
||||
category, = UomCategory.create([{'name': 'Test'}])
|
||||
return Uom.create([{
|
||||
'name': 'Test',
|
||||
'symbol': 'T',
|
||||
'category': category.id,
|
||||
'rate': 1.0,
|
||||
'factor': 1.0,
|
||||
}])[0]
|
||||
|
||||
uom = create()
|
||||
self.assertRaises(Exception, Uom.write, [uom], {
|
||||
'rate': 0.0,
|
||||
})
|
||||
transaction.rollback()
|
||||
|
||||
uom = create()
|
||||
self.assertRaises(Exception, Uom.write, [uom], {
|
||||
'factor': 0.0,
|
||||
})
|
||||
transaction.rollback()
|
||||
|
||||
uom = create()
|
||||
self.assertRaises(Exception, Uom.write, [uom], {
|
||||
'rate': 0.0,
|
||||
'factor': 0.0,
|
||||
})
|
||||
transaction.rollback()
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_check_factor_and_rate(self):
|
||||
'Test uom check_factor_and_rate constraint'
|
||||
pool = Pool()
|
||||
UomCategory = pool.get('product.uom.category')
|
||||
Uom = pool.get('product.uom')
|
||||
transaction = Transaction()
|
||||
category, = UomCategory.create([{'name': 'Test'}])
|
||||
|
||||
self.assertRaises(Exception, Uom.create, [{
|
||||
'name': 'Test',
|
||||
'symbol': 'T',
|
||||
'category': category.id,
|
||||
'rate': 2,
|
||||
'factor': 2,
|
||||
}])
|
||||
transaction.rollback()
|
||||
|
||||
def create():
|
||||
category, = UomCategory.create([{'name': 'Test'}])
|
||||
return Uom.create([{
|
||||
'name': 'Test',
|
||||
'symbol': 'T',
|
||||
'category': category.id,
|
||||
'rate': 1.0,
|
||||
'factor': 1.0,
|
||||
}])[0]
|
||||
|
||||
uom = create()
|
||||
self.assertRaises(Exception, Uom.write, [uom], {
|
||||
'rate': 2.0,
|
||||
})
|
||||
transaction.rollback()
|
||||
|
||||
uom = create()
|
||||
self.assertRaises(Exception, Uom.write, [uom], {
|
||||
'factor': 2.0,
|
||||
})
|
||||
transaction.rollback()
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_select_accurate_field(self):
|
||||
'Test uom select_accurate_field function'
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
tests = [
|
||||
('Meter', 'factor'),
|
||||
('Kilometer', 'factor'),
|
||||
('Centimeter', 'rate'),
|
||||
('Foot', 'factor'),
|
||||
]
|
||||
for name, result in tests:
|
||||
uom, = Uom.search([
|
||||
('name', '=', name),
|
||||
], limit=1)
|
||||
self.assertEqual(result, uom.accurate_field)
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_compute_qty(self):
|
||||
'Test uom compute_qty function'
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
tests = [
|
||||
('Kilogram', 100, 'Gram', 100000, 100000),
|
||||
('Gram', 1, 'Pound', 0.0022046226218487759, 0.0),
|
||||
('Ounce', 1 / 7, 'Ounce', 1 / 7, 0.14),
|
||||
('Second', 5, 'Minute', 0.083333333333333343, 0.08),
|
||||
('Second', 25, 'Hour', 0.0069444444444444441, 0.01),
|
||||
('Millimeter', 3, 'Inch', 0.11811023622047245, 0.12),
|
||||
('Millimeter', 0, 'Inch', 0, 0),
|
||||
('Millimeter', None, 'Inch', None, None),
|
||||
]
|
||||
for from_name, qty, to_name, result, rounded_result in tests:
|
||||
from_uom, = Uom.search([
|
||||
('name', '=', from_name),
|
||||
], limit=1)
|
||||
to_uom, = Uom.search([
|
||||
('name', '=', to_name),
|
||||
], limit=1)
|
||||
self.assertEqual(result, Uom.compute_qty(
|
||||
from_uom, qty, to_uom, False))
|
||||
self.assertEqual(rounded_result, Uom.compute_qty(
|
||||
from_uom, qty, to_uom, True))
|
||||
self.assertEqual(0.2, Uom.compute_qty(None, 0.2, None, False))
|
||||
self.assertEqual(0.2, Uom.compute_qty(None, 0.2, None, True))
|
||||
|
||||
tests_exceptions = [
|
||||
('Millimeter', 3, 'Pound', ValueError),
|
||||
('Kilogram', 'not a number', 'Pound', TypeError),
|
||||
]
|
||||
for from_name, qty, to_name, exception in tests_exceptions:
|
||||
from_uom, = Uom.search([
|
||||
('name', '=', from_name),
|
||||
], limit=1)
|
||||
to_uom, = Uom.search([
|
||||
('name', '=', to_name),
|
||||
], limit=1)
|
||||
self.assertRaises(exception, Uom.compute_qty,
|
||||
from_uom, qty, to_uom, False)
|
||||
self.assertRaises(exception, Uom.compute_qty,
|
||||
from_uom, qty, to_uom, True)
|
||||
self.assertRaises(ValueError, Uom.compute_qty,
|
||||
None, qty, to_uom, True)
|
||||
self.assertRaises(ValueError, Uom.compute_qty,
|
||||
from_uom, qty, None, True)
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_compute_qty_category(self):
|
||||
"Test uom compute_qty with different category"
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
g, = Uom.search([
|
||||
('name', '=', "Gram"),
|
||||
], limit=1)
|
||||
m3, = Uom.search([
|
||||
('name', '=', "Cubic meter"),
|
||||
], limit=1)
|
||||
|
||||
for quantity, result, keys in [
|
||||
(10000, 0.02, dict(factor=2)),
|
||||
(20000, 0.01, dict(rate=2)),
|
||||
(30000, 0.01, dict(rate=3, factor=0.333333, round=False)),
|
||||
]:
|
||||
msg = 'quantity: %r, keys: %r' % (quantity, keys)
|
||||
self.assertEqual(
|
||||
Uom.compute_qty(g, quantity, m3, **keys), result,
|
||||
msg=msg)
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_compute_price(self):
|
||||
'Test uom compute_price function'
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
tests = [
|
||||
('Kilogram', Decimal('100'), 'Gram', Decimal('0.1')),
|
||||
('Gram', Decimal('1'), 'Pound', Decimal('453.59237')),
|
||||
('Ounce', Decimal(1 / 7), 'Ounce', Decimal(1 / 7)),
|
||||
('Second', Decimal('5'), 'Minute', Decimal('300')),
|
||||
('Second', Decimal('25'), 'Hour', Decimal('90000')),
|
||||
('Millimeter', Decimal('3'), 'Inch', Decimal('76.2')),
|
||||
('Millimeter', Decimal('0'), 'Inch', Decimal('0')),
|
||||
('Millimeter', None, 'Inch', None),
|
||||
]
|
||||
for from_name, price, to_name, result in tests:
|
||||
from_uom, = Uom.search([
|
||||
('name', '=', from_name),
|
||||
], limit=1)
|
||||
to_uom, = Uom.search([
|
||||
('name', '=', to_name),
|
||||
], limit=1)
|
||||
self.assertEqual(result, Uom.compute_price(
|
||||
from_uom, price, to_uom))
|
||||
self.assertEqual(Decimal('0.2'), Uom.compute_price(
|
||||
None, Decimal('0.2'), None))
|
||||
|
||||
tests_exceptions = [
|
||||
('Millimeter', Decimal('3'), 'Pound', ValueError),
|
||||
('Kilogram', 'not a number', 'Pound', TypeError),
|
||||
]
|
||||
for from_name, price, to_name, exception in tests_exceptions:
|
||||
from_uom, = Uom.search([
|
||||
('name', '=', from_name),
|
||||
], limit=1)
|
||||
to_uom, = Uom.search([
|
||||
('name', '=', to_name),
|
||||
], limit=1)
|
||||
self.assertRaises(exception, Uom.compute_price,
|
||||
from_uom, price, to_uom)
|
||||
self.assertRaises(ValueError, Uom.compute_price,
|
||||
None, price, to_uom)
|
||||
self.assertRaises(ValueError, Uom.compute_price,
|
||||
from_uom, price, None)
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_compute_price_category(self):
|
||||
"Test uom compute_price with different category"
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
g, = Uom.search([
|
||||
('name', '=', "Gram"),
|
||||
], limit=1)
|
||||
m3, = Uom.search([
|
||||
('name', '=', "Cubic meter"),
|
||||
], limit=1)
|
||||
|
||||
for price, result, keys in [
|
||||
(Decimal('0.001'), Decimal('500'), dict(factor=2)),
|
||||
(Decimal('0.002'), Decimal('4000'), dict(rate=2)),
|
||||
(Decimal('0.003'), Decimal('9000'), dict(
|
||||
rate=3, factor=0.333333)),
|
||||
]:
|
||||
msg = 'price: %r, keys: %r' % (price, keys)
|
||||
self.assertEqual(
|
||||
Uom.compute_price(g, price, m3, **keys), result,
|
||||
msg=msg)
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_modify_factor_rate(self):
|
||||
"Test can not modify factor or rate of uom"
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
g, = Uom.search([('name', '=', "Gram")])
|
||||
|
||||
g.factor = 1
|
||||
g.rate = 1
|
||||
|
||||
with self.assertRaises(UOMAccessError):
|
||||
g.save()
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_modify_category(self):
|
||||
"Test can not modify category of uom"
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
Category = pool.get('product.uom.category')
|
||||
g, = Uom.search([('name', '=', "Gram")])
|
||||
units, = Category.search([('name', '=', "Units")])
|
||||
|
||||
g.category = units
|
||||
|
||||
with self.assertRaises(UOMAccessError):
|
||||
g.save()
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_increase_digits(self):
|
||||
"Test can increase digits of uom"
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
g, = Uom.search([('name', '=', "Gram")])
|
||||
|
||||
g.digits += 1
|
||||
|
||||
g.save()
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_decrease_digits(self):
|
||||
"Test can not decrease digits of uom"
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
g, = Uom.search([('name', '=', "Gram")])
|
||||
|
||||
g.digits -= 1
|
||||
g.rounding = 1
|
||||
|
||||
with self.assertRaises(UOMAccessError):
|
||||
g.save()
|
||||
|
||||
@with_transaction()
|
||||
def test_product_search_domain(self):
|
||||
'Test product.product search_domain function'
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
Template = pool.get('product.template')
|
||||
Product = pool.get('product.product')
|
||||
|
||||
kilogram, = Uom.search([
|
||||
('name', '=', 'Kilogram'),
|
||||
], limit=1)
|
||||
millimeter, = Uom.search([
|
||||
('name', '=', 'Millimeter'),
|
||||
])
|
||||
pt1, pt2 = Template.create([{
|
||||
'name': 'P1',
|
||||
'type': 'goods',
|
||||
'default_uom': kilogram.id,
|
||||
'products': [('create', [{
|
||||
'code': '1',
|
||||
}])]
|
||||
}, {
|
||||
'name': 'P2',
|
||||
'type': 'goods',
|
||||
'default_uom': millimeter.id,
|
||||
'products': [('create', [{
|
||||
'code': '2',
|
||||
}])]
|
||||
}])
|
||||
p, = Product.search([
|
||||
('default_uom.name', '=', 'Kilogram'),
|
||||
])
|
||||
self.assertEqual(p, pt1.products[0])
|
||||
p, = Product.search([
|
||||
('default_uom.name', '=', 'Millimeter'),
|
||||
])
|
||||
self.assertEqual(p, pt2.products[0])
|
||||
|
||||
@with_transaction()
|
||||
def test_search_domain_conversion(self):
|
||||
'Test the search domain conversion'
|
||||
pool = Pool()
|
||||
Category = pool.get('product.category')
|
||||
Template = pool.get('product.template')
|
||||
Product = pool.get('product.product')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
category1, = Category.create([{'name': 'Category1'}])
|
||||
category2, = Category.create([{'name': 'Category2'}])
|
||||
uom, = Uom.search([], limit=1)
|
||||
values1 = {
|
||||
'name': 'Some product-1',
|
||||
'categories': [('add', [category1.id])],
|
||||
'type': 'goods',
|
||||
'default_uom': uom.id,
|
||||
'products': [('create', [{}])],
|
||||
}
|
||||
values2 = {
|
||||
'name': 'Some product-2',
|
||||
'categories': [('add', [category2.id])],
|
||||
'type': 'goods',
|
||||
'default_uom': uom.id,
|
||||
'products': [('create', [{}])],
|
||||
}
|
||||
|
||||
# This is a false positive as there is 1 product with the
|
||||
# template 1 and the same product with category 1. If you do not
|
||||
# create two categories (or any other relation on the template
|
||||
# model) you wont be able to check as in most cases the
|
||||
# id of the template and the related model would be same (1).
|
||||
# So two products have been created with same category. So that
|
||||
# domain ('template.categories', '=', 1) will return 2 records which
|
||||
# it supposed to be.
|
||||
template1, template2, template3, template4 = Template.create(
|
||||
[values1, values1.copy(), values2, values2.copy()]
|
||||
)
|
||||
self.assertEqual(Product.search([], count=True), 4)
|
||||
self.assertEqual(
|
||||
Product.search([
|
||||
('categories', '=', category1.id),
|
||||
], count=True), 2)
|
||||
|
||||
self.assertEqual(
|
||||
Product.search([
|
||||
('template.categories', '=', category1.id),
|
||||
], count=True), 2)
|
||||
|
||||
self.assertEqual(
|
||||
Product.search([
|
||||
('categories', '=', category2.id),
|
||||
], count=True), 2)
|
||||
self.assertEqual(
|
||||
Product.search([
|
||||
('template.categories', '=', category2.id),
|
||||
], count=True), 2)
|
||||
|
||||
@with_transaction()
|
||||
def test_uom_rounding(self):
|
||||
'Test uom rounding functions'
|
||||
pool = Pool()
|
||||
Uom = pool.get('product.uom')
|
||||
tests = [
|
||||
(2.53, .1, 2.5, 2.6, 2.5),
|
||||
(3.8, .1, 3.8, 3.8, 3.8),
|
||||
(3.7, .1, 3.7, 3.7, 3.7),
|
||||
(1.3, .5, 1.5, 1.5, 1.0),
|
||||
(1.1, .3, 1.2, 1.2, 0.9),
|
||||
(17, 10, 20, 20, 10),
|
||||
(7, 10, 10, 10, 0),
|
||||
(4, 10, 0, 10, 0),
|
||||
(17, 15, 15, 30, 15),
|
||||
(2.5, 1.4, 2.8, 2.8, 1.4),
|
||||
]
|
||||
for number, precision, round, ceil, floor in tests:
|
||||
uom = Uom(rounding=precision)
|
||||
self.assertEqual(uom.round(number), round)
|
||||
self.assertEqual(uom.ceil(number), ceil)
|
||||
self.assertEqual(uom.floor(number), floor)
|
||||
|
||||
@with_transaction()
|
||||
def test_product_order(self):
|
||||
'Test product field order'
|
||||
pool = Pool()
|
||||
Template = pool.get('product.template')
|
||||
Product = pool.get('product.product')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
uom, = Uom.search([], limit=1)
|
||||
values1 = {
|
||||
'name': 'Product A',
|
||||
'type': 'assets',
|
||||
'default_uom': uom.id,
|
||||
'products': [('create', [{'suffix_code': 'AA'}])],
|
||||
}
|
||||
values2 = {
|
||||
'name': 'Product B',
|
||||
'type': 'goods',
|
||||
'default_uom': uom.id,
|
||||
'products': [('create', [{'suffix_code': 'BB'}])],
|
||||
}
|
||||
|
||||
template1, template2 = Template.create([values1, values2])
|
||||
product1, product2 = Product.search([])
|
||||
|
||||
# Non-inherited field.
|
||||
self.assertEqual(
|
||||
Product.search([], order=[('code', 'ASC')]), [product1, product2])
|
||||
self.assertEqual(
|
||||
Product.search([], order=[('code', 'DESC')]), [product2, product1])
|
||||
self.assertEqual(Product.search(
|
||||
[('name', 'like', '%')], order=[('code', 'ASC')]),
|
||||
[product1, product2])
|
||||
self.assertEqual(Product.search(
|
||||
[('name', 'like', '%')], order=[('code', 'DESC')]),
|
||||
[product2, product1])
|
||||
|
||||
# Inherited field with custom order.
|
||||
self.assertEqual(
|
||||
Product.search([], order=[('name', 'ASC')]), [product1, product2])
|
||||
self.assertEqual(
|
||||
Product.search([], order=[('name', 'DESC')]), [product2, product1])
|
||||
self.assertEqual(Product.search(
|
||||
[('name', 'like', '%')], order=[('name', 'ASC')]),
|
||||
[product1, product2])
|
||||
self.assertEqual(Product.search(
|
||||
[('name', 'like', '%')], order=[('name', 'DESC')]),
|
||||
[product2, product1])
|
||||
|
||||
# Inherited field without custom order.
|
||||
self.assertEqual(
|
||||
Product.search([], order=[('type', 'ASC')]), [product1, product2])
|
||||
self.assertEqual(
|
||||
Product.search([], order=[('type', 'DESC')]), [product2, product1])
|
||||
self.assertEqual(Product.search(
|
||||
[('name', 'like', '%')], order=[('type', 'ASC')]),
|
||||
[product1, product2])
|
||||
self.assertEqual(Product.search(
|
||||
[('name', 'like', '%')], order=[('type', 'DESC')]),
|
||||
[product2, product1])
|
||||
|
||||
def test_round_price(self):
|
||||
for value, result in [
|
||||
(Decimal('1'), Decimal('1.0000')),
|
||||
(Decimal('1.12345'), Decimal('1.1234')),
|
||||
(1, Decimal('1')),
|
||||
]:
|
||||
with self.subTest(value=value):
|
||||
self.assertEqual(round_price(value), result)
|
||||
|
||||
@with_transaction()
|
||||
def test_product_identifier_get_single_type(self):
|
||||
"Test identifier get with a single type"
|
||||
pool = Pool()
|
||||
Identifier = pool.get('product.identifier')
|
||||
Product = pool.get('product.product')
|
||||
Template = pool.get('product.template')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
uom, = Uom.search([], limit=1)
|
||||
template = Template(name="Product", default_uom=uom)
|
||||
template.save()
|
||||
product = Product(template=template)
|
||||
product.identifiers = [
|
||||
Identifier(code='FOO'),
|
||||
Identifier(type='ean', code='978-0-471-11709-4'),
|
||||
]
|
||||
product.save()
|
||||
|
||||
self.assertEqual(
|
||||
product.identifier_get('ean').code,
|
||||
'978-0-471-11709-4')
|
||||
|
||||
@with_transaction()
|
||||
def test_product_identifier_get_many_types(self):
|
||||
"Test identifier get with many types"
|
||||
pool = Pool()
|
||||
Identifier = pool.get('product.identifier')
|
||||
Product = pool.get('product.product')
|
||||
Template = pool.get('product.template')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
uom, = Uom.search([], limit=1)
|
||||
template = Template(name="Product", default_uom=uom)
|
||||
template.save()
|
||||
product = Product(template=template)
|
||||
product.identifiers = [
|
||||
Identifier(code='FOO'),
|
||||
Identifier(type='isbn', code='0-6332-4980-7'),
|
||||
Identifier(type='ean', code='978-0-471-11709-4'),
|
||||
]
|
||||
product.save()
|
||||
|
||||
self.assertEqual(
|
||||
product.identifier_get({'ean', 'isbn'}).code,
|
||||
'0-6332-4980-7')
|
||||
|
||||
@with_transaction()
|
||||
def test_product_identifier_get_any(self):
|
||||
"Test identifier get for any type"
|
||||
pool = Pool()
|
||||
Identifier = pool.get('product.identifier')
|
||||
Product = pool.get('product.product')
|
||||
Template = pool.get('product.template')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
uom, = Uom.search([], limit=1)
|
||||
template = Template(name="Product", default_uom=uom)
|
||||
template.save()
|
||||
product = Product(template=template)
|
||||
product.identifiers = [
|
||||
Identifier(code='FOO'),
|
||||
]
|
||||
product.save()
|
||||
|
||||
self.assertEqual(product.identifier_get(None).code, 'FOO')
|
||||
|
||||
@with_transaction()
|
||||
def test_product_identifier_get_unknown_type(self):
|
||||
"Test identifier get with a unknown type"
|
||||
pool = Pool()
|
||||
Identifier = pool.get('product.identifier')
|
||||
Product = pool.get('product.product')
|
||||
Template = pool.get('product.template')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
uom, = Uom.search([], limit=1)
|
||||
template = Template(name="Product", default_uom=uom)
|
||||
template.save()
|
||||
product = Product(template=template)
|
||||
product.identifiers = [
|
||||
Identifier(code='FOO'),
|
||||
]
|
||||
product.save()
|
||||
|
||||
self.assertEqual(product.identifier_get('ean'), None)
|
||||
|
||||
@unittest.skipUnless(barcode, 'required barcode')
|
||||
@with_transaction()
|
||||
def test_product_identifier_barcode(self):
|
||||
"Test identifier barcode"
|
||||
pool = Pool()
|
||||
Identifier = pool.get('product.identifier')
|
||||
Product = pool.get('product.product')
|
||||
Template = pool.get('product.template')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
uom, = Uom.search([], limit=1)
|
||||
template = Template(name="Product", default_uom=uom)
|
||||
template.save()
|
||||
product = Product(template=template)
|
||||
product.identifiers = [
|
||||
Identifier(type='ean', code='978-0-471-11709-4'),
|
||||
]
|
||||
product.save()
|
||||
identifier, = product.identifiers
|
||||
|
||||
image = identifier.barcode()
|
||||
self.assertIsInstance(image, io.BytesIO)
|
||||
self.assertIsNotNone(image.getvalue())
|
||||
|
||||
|
||||
del ModuleTestCase
|
||||
8
modules/product/tests/test_scenario.py
Normal file
8
modules/product/tests/test_scenario.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from trytond.tests.test_tryton import load_doc_tests
|
||||
|
||||
|
||||
def load_tests(*args, **kwargs):
|
||||
return load_doc_tests(__name__, __file__, *args, **kwargs)
|
||||
34
modules/product/tryton.cfg
Normal file
34
modules/product/tryton.cfg
Normal file
@@ -0,0 +1,34 @@
|
||||
[tryton]
|
||||
version=7.8.1
|
||||
depends:
|
||||
company
|
||||
ir
|
||||
res
|
||||
xml:
|
||||
product.xml
|
||||
category.xml
|
||||
uom.xml
|
||||
configuration.xml
|
||||
message.xml
|
||||
|
||||
[register]
|
||||
model:
|
||||
ir.Configuration
|
||||
ir.Cron
|
||||
uom.UomCategory
|
||||
uom.Uom
|
||||
category.Category
|
||||
product.Template
|
||||
product.Product
|
||||
product.ProductIdentifier
|
||||
product.ProductListPrice
|
||||
# before ProductCostPrice for migration
|
||||
product.ProductCostPriceMethod
|
||||
product.ProductCostPrice
|
||||
product.TemplateCategory
|
||||
product.TemplateCategoryAll
|
||||
product.ProductReplaceAsk
|
||||
configuration.Configuration
|
||||
configuration.ConfigurationDefaultCostPriceMethod
|
||||
wizard:
|
||||
product.ProductReplace
|
||||
324
modules/product/uom.py
Normal file
324
modules/product/uom.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from decimal import Decimal
|
||||
from math import ceil, floor, log10
|
||||
|
||||
import trytond.config as config
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
Check, DeactivableMixin, DigitsMixin, ModelSQL, ModelView, SymbolMixin,
|
||||
fields)
|
||||
from trytond.pyson import Eval, If
|
||||
|
||||
from .exceptions import UOMAccessError, UOMValidationError
|
||||
|
||||
__all__ = ['uom_conversion_digits']
|
||||
|
||||
uom_conversion_digits = (
|
||||
config.getint('product', 'uom_conversion_decimal', default=12),) * 2
|
||||
|
||||
|
||||
class UomCategory(ModelSQL, ModelView):
|
||||
__name__ = 'product.uom.category'
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
uoms = fields.One2Many('product.uom', 'category', "Units of Measure")
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._order.insert(0, ('name', 'ASC'))
|
||||
|
||||
|
||||
class Uom(SymbolMixin, DigitsMixin, DeactivableMixin, ModelSQL, ModelView):
|
||||
__name__ = 'product.uom'
|
||||
name = fields.Char("Name", size=None, required=True, translate=True)
|
||||
symbol = fields.Char(
|
||||
"Symbol", size=10, required=True, translate=True,
|
||||
help="The symbol that represents the unit of measure.")
|
||||
category = fields.Many2One(
|
||||
'product.uom.category', "Category", required=True, ondelete='RESTRICT',
|
||||
help="The category that contains the unit of measure.\n"
|
||||
"Conversions between different units of measure can be done if they "
|
||||
"are in the same category.")
|
||||
rate = fields.Float(
|
||||
"Rate", digits=uom_conversion_digits, required=True,
|
||||
domain=[
|
||||
If(Eval('factor', 0) == 0, ('rate', '!=', 0), ()),
|
||||
],
|
||||
help="The coefficient for the formula:\n"
|
||||
"1 (base unit) = coef (this unit)")
|
||||
factor = fields.Float(
|
||||
"Factor", digits=uom_conversion_digits, required=True,
|
||||
domain=[
|
||||
If(Eval('rate', 0) == 0, ('factor', '!=', 0), ()),
|
||||
],
|
||||
help="The coefficient for the formula:\n"
|
||||
"coefficient (base unit) = 1 (this unit)")
|
||||
rounding = fields.Float(
|
||||
"Rounding Precision",
|
||||
digits=(None, Eval('digits', None)), required=True,
|
||||
domain=[
|
||||
('rounding', '>', 0),
|
||||
],
|
||||
help="The accuracy to which values are rounded.")
|
||||
digits = fields.Integer(
|
||||
"Display Digits", required=True,
|
||||
help="The number of digits to display after the decimal separator.")
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_constraints += [
|
||||
('non_zero_rate_factor', Check(t, (t.rate != 0) | (t.factor != 0)),
|
||||
'product.msg_uom_no_zero_factor_rate')
|
||||
]
|
||||
cls._order.insert(0, ('name', 'ASC'))
|
||||
|
||||
@staticmethod
|
||||
def default_rate():
|
||||
return 1.0
|
||||
|
||||
@staticmethod
|
||||
def default_factor():
|
||||
return 1.0
|
||||
|
||||
@staticmethod
|
||||
def default_rounding():
|
||||
return 0.01
|
||||
|
||||
@staticmethod
|
||||
def default_digits():
|
||||
return 2
|
||||
|
||||
@fields.depends('factor')
|
||||
def on_change_factor(self):
|
||||
if (self.factor or 0.0) == 0.0:
|
||||
self.rate = 0.0
|
||||
else:
|
||||
self.rate = round(1.0 / self.factor, uom_conversion_digits[1])
|
||||
|
||||
@fields.depends('rate')
|
||||
def on_change_rate(self):
|
||||
if (self.rate or 0.0) == 0.0:
|
||||
self.factor = 0.0
|
||||
else:
|
||||
self.factor = round(
|
||||
1.0 / self.rate, uom_conversion_digits[1])
|
||||
|
||||
@classmethod
|
||||
def search_rec_name(cls, name, clause):
|
||||
if clause[1].startswith('!') or clause[1].startswith('not '):
|
||||
bool_op = 'AND'
|
||||
else:
|
||||
bool_op = 'OR'
|
||||
return [bool_op,
|
||||
(cls._rec_name,) + tuple(clause[1:]),
|
||||
('symbol',) + tuple(clause[1:]),
|
||||
]
|
||||
|
||||
def round(self, number):
|
||||
return _round(self, number, func=round)
|
||||
|
||||
def ceil(self, number):
|
||||
return _round(self, number, func=ceil)
|
||||
|
||||
def floor(self, number):
|
||||
return _round(self, number, func=floor)
|
||||
|
||||
@classmethod
|
||||
def validate_fields(cls, uoms, field_names):
|
||||
super().validate_fields(uoms, field_names)
|
||||
cls.check_factor_and_rate(uoms, field_names)
|
||||
|
||||
@classmethod
|
||||
def check_factor_and_rate(cls, uoms, field_names=None):
|
||||
"Check coherence between factor and rate"
|
||||
if field_names and not (field_names & {'rate', 'factor'}):
|
||||
return
|
||||
for uom in uoms:
|
||||
if uom.rate == uom.factor == 0.0:
|
||||
continue
|
||||
if (uom.rate != round(
|
||||
1.0 / uom.factor, uom_conversion_digits[1])
|
||||
and uom.factor != round(
|
||||
1.0 / uom.rate, uom_conversion_digits[1])):
|
||||
raise UOMValidationError(
|
||||
gettext('product.msg_uom_incompatible_factor_rate',
|
||||
uom=uom.rec_name))
|
||||
|
||||
@classmethod
|
||||
def check_modification(cls, mode, uoms, values=None, external=False):
|
||||
super().check_modification(
|
||||
mode, uoms, values=values, external=external)
|
||||
if (mode == 'write'
|
||||
and values.keys() & {'factor', 'rate', 'category', 'digits'}):
|
||||
for uom in uoms:
|
||||
for field_name in values.keys() & {'factor', 'rate'}:
|
||||
if values[field_name] != getattr(uom, field_name):
|
||||
raise UOMAccessError(gettext(
|
||||
'product.msg_uom_modify_%s' % field_name,
|
||||
uom=uom.rec_name),
|
||||
gettext('product.msg_uom_modify_options'))
|
||||
if 'category' in values:
|
||||
if values['category'] != uom.category.id:
|
||||
raise UOMAccessError(gettext(
|
||||
'product.msg_uom_modify_category',
|
||||
uom=uom.rec_name),
|
||||
gettext('product.msg_uom_modify_options'))
|
||||
if 'digits' in values:
|
||||
if values['digits'] < uom.digits:
|
||||
raise UOMAccessError(gettext(
|
||||
'product.msg_uom_decrease_digits',
|
||||
uom=uom.rec_name),
|
||||
gettext('product.msg_uom_modify_options'))
|
||||
|
||||
@property
|
||||
def accurate_field(self):
|
||||
"""
|
||||
Select the more accurate field.
|
||||
It chooses the field that has the least decimal.
|
||||
"""
|
||||
return _accurate_operator(self.factor, self.rate)
|
||||
|
||||
@classmethod
|
||||
def compute_qty(cls, from_uom, qty, to_uom, round=True,
|
||||
factor=None, rate=None):
|
||||
"""
|
||||
Convert quantity for given uom's.
|
||||
|
||||
When converting between uom's from different categories the factor and
|
||||
rate provide the ratio to use to convert between the category's base
|
||||
uom's.
|
||||
"""
|
||||
if not qty or (from_uom is None and to_uom is None):
|
||||
return qty
|
||||
if from_uom is None:
|
||||
raise ValueError("missing from_UoM")
|
||||
if to_uom is None:
|
||||
raise ValueError("missing to_UoM")
|
||||
if from_uom.category.id != to_uom.category.id:
|
||||
if not factor and not rate:
|
||||
raise ValueError(
|
||||
"cannot convert between %s and %s without a factor or rate"
|
||||
% (from_uom.category.name, to_uom.category.name))
|
||||
elif factor or rate:
|
||||
raise ValueError("factor and rate not allowed for same category")
|
||||
|
||||
if from_uom != to_uom:
|
||||
if from_uom.accurate_field == 'factor':
|
||||
amount = qty * from_uom.factor
|
||||
else:
|
||||
amount = qty / from_uom.rate
|
||||
|
||||
if factor and rate:
|
||||
if _accurate_operator(factor, rate) == 'rate':
|
||||
factor = None
|
||||
else:
|
||||
rate = None
|
||||
if factor:
|
||||
amount *= factor
|
||||
elif rate:
|
||||
amount /= rate
|
||||
|
||||
if to_uom.accurate_field == 'factor':
|
||||
amount = amount / to_uom.factor
|
||||
else:
|
||||
amount = amount * to_uom.rate
|
||||
else:
|
||||
amount = qty
|
||||
|
||||
if round:
|
||||
amount = to_uom.round(amount)
|
||||
|
||||
return amount
|
||||
|
||||
@classmethod
|
||||
def compute_price(cls, from_uom, price, to_uom, factor=None, rate=None):
|
||||
"""
|
||||
Convert price for given uom's.
|
||||
|
||||
When converting between uom's from different categories the factor and
|
||||
rate provide the ratio to use to convert between the category's base
|
||||
uom's.
|
||||
"""
|
||||
if not price or (from_uom is None and to_uom is None):
|
||||
return price
|
||||
if from_uom is None:
|
||||
raise ValueError("missing from_UoM")
|
||||
if to_uom is None:
|
||||
raise ValueError("missing to_UoM")
|
||||
if from_uom.category.id != to_uom.category.id:
|
||||
if not factor and not rate:
|
||||
raise ValueError(
|
||||
"cannot convert between %s and %s without a factor or rate"
|
||||
% (from_uom.category.name, to_uom.category.name))
|
||||
elif factor or rate:
|
||||
raise ValueError("factor and rate not allow for same category")
|
||||
|
||||
if from_uom != to_uom:
|
||||
format_ = '%%.%df' % uom_conversion_digits[1]
|
||||
|
||||
if from_uom.accurate_field == 'factor':
|
||||
new_price = price / Decimal(format_ % from_uom.factor)
|
||||
else:
|
||||
new_price = price * Decimal(format_ % from_uom.rate)
|
||||
|
||||
if factor and rate:
|
||||
if _accurate_operator(factor, rate) == 'rate':
|
||||
factor = None
|
||||
else:
|
||||
rate = None
|
||||
if factor:
|
||||
new_price /= Decimal(factor)
|
||||
elif rate:
|
||||
new_price *= Decimal(rate)
|
||||
|
||||
if to_uom.accurate_field == 'factor':
|
||||
new_price = new_price * Decimal(format_ % to_uom.factor)
|
||||
else:
|
||||
new_price = new_price / Decimal(format_ % to_uom.rate)
|
||||
else:
|
||||
new_price = price
|
||||
|
||||
return new_price
|
||||
|
||||
|
||||
def _round(uom, number, func=round):
|
||||
if not number:
|
||||
# Avoid unnecessary computation
|
||||
return number
|
||||
precision = uom.rounding
|
||||
# Convert precision into an integer greater than 1 to avoid precision lost.
|
||||
# This works for most cases because rounding is often: n * 10**i
|
||||
if precision < 1:
|
||||
exp = -floor(log10(precision))
|
||||
factor = 10 ** exp
|
||||
number *= factor
|
||||
precision *= factor
|
||||
else:
|
||||
factor = 1
|
||||
# Divide by factor which is an integer to avoid precision lost due to
|
||||
# multiplication by float < 1.
|
||||
# example:
|
||||
# >>> 3 * 0.1
|
||||
# 0.30000000000000004
|
||||
# >>> 3 / 10.
|
||||
# 0.3
|
||||
return func(number / precision) * precision / factor
|
||||
|
||||
|
||||
def _accurate_operator(factor, rate):
|
||||
lengths = {}
|
||||
for name, value in [('rate', rate), ('factor', factor)]:
|
||||
format_ = '%%.%df' % uom_conversion_digits[1]
|
||||
lengths[name] = len((format_ % value).split('.')[1].rstrip('0'))
|
||||
if lengths['rate'] < lengths['factor']:
|
||||
return 'rate'
|
||||
elif lengths['factor'] < lengths['rate']:
|
||||
return 'factor'
|
||||
elif factor >= 1.0:
|
||||
return 'factor'
|
||||
else:
|
||||
return 'rate'
|
||||
377
modules/product/uom.xml
Normal file
377
modules/product/uom.xml
Normal file
@@ -0,0 +1,377 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="uom_view_tree">
|
||||
<field name="model">product.uom</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">uom_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="uom_view_form">
|
||||
<field name="model">product.uom</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">uom_form</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_uom_form">
|
||||
<field name="name">Units of Measure</field>
|
||||
<field name="res_model">product.uom</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_uom_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="uom_view_tree"/>
|
||||
<field name="act_window" ref="act_uom_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_uom_form_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="uom_view_form"/>
|
||||
<field name="act_window" ref="act_uom_form"/>
|
||||
</record>
|
||||
<menuitem
|
||||
parent="menu_configuration"
|
||||
action="act_uom_form"
|
||||
sequence="50"
|
||||
id="menu_uom_form"/>
|
||||
|
||||
<record model="ir.ui.view" id="uom_category_view_tree">
|
||||
<field name="model">product.uom.category</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">uom_category_tree</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="uom_category_view_form">
|
||||
<field name="model">product.uom.category</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">uom_category_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_uom_category_form">
|
||||
<field name="name">Categories</field>
|
||||
<field name="res_model">product.uom.category</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_uom_category_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="uom_category_view_tree"/>
|
||||
<field name="act_window" ref="act_uom_category_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_uom_category_form_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="uom_category_view_form"/>
|
||||
<field name="act_window" ref="act_uom_category_form"/>
|
||||
</record>
|
||||
<menuitem
|
||||
parent="menu_uom_form"
|
||||
action="act_uom_category_form"
|
||||
sequence="20"
|
||||
id="menu_uom_category_form"/>
|
||||
|
||||
<record model="ir.model.access" id="access_uom">
|
||||
<field name="model">product.uom</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_uom_admin">
|
||||
<field name="model">product.uom</field>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_uom_category">
|
||||
<field name="model">product.uom.category</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_uom_category_admin">
|
||||
<field name="model">product.uom.category</field>
|
||||
<field name="group" ref="group_product_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record model="product.uom.category" id="uom_cat_unit">
|
||||
<field name="name">Units</field>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_unit">
|
||||
<field name="name">Unit</field>
|
||||
<field name="symbol">u</field>
|
||||
<field name="category" ref="uom_cat_unit"/>
|
||||
<field name="rate" eval="1."/>
|
||||
<field name="factor" eval="1."/>
|
||||
<field name="rounding" eval="1."/>
|
||||
<field name="digits" eval="0"/>
|
||||
</record>
|
||||
|
||||
<record model="product.uom.category" id="uom_cat_weight">
|
||||
<field name="name">Weight</field>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_kilogram">
|
||||
<field name="name">Kilogram</field>
|
||||
<field name="symbol">kg</field>
|
||||
<field name="category" ref="uom_cat_weight"/>
|
||||
<field name="factor" eval="1."/>
|
||||
<field name="rate" eval="1."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_gram">
|
||||
<field name="name">Gram</field>
|
||||
<field name="symbol">g</field>
|
||||
<field name="category" ref="uom_cat_weight"/>
|
||||
<field name="factor" eval="0.001"/>
|
||||
<field name="rate" eval="1000."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_carat">
|
||||
<field name="name">Carat</field>
|
||||
<field name="symbol">c</field>
|
||||
<field name="category" ref="uom_cat_weight"/>
|
||||
<field name="factor" eval="0.0002"/>
|
||||
<field name="rate" eval="5000."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_pound">
|
||||
<field name="name">Pound</field>
|
||||
<field name="symbol">lb</field>
|
||||
<field name="category" ref="uom_cat_weight"/>
|
||||
<field name="factor" eval="0.45359237"/>
|
||||
<field name="rate" eval="round(1.0 / 0.45359237, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_ounce">
|
||||
<field name="name">Ounce</field>
|
||||
<field name="symbol">oz</field>
|
||||
<field name="category" ref="uom_cat_weight"/>
|
||||
<field name="factor" eval="0.028349523125"/>
|
||||
<field name="rate" eval="round(1.0 / 0.028349523125, 12)"/>
|
||||
</record>
|
||||
|
||||
<record model="product.uom.category" id="uom_cat_time">
|
||||
<field name="name">Time</field>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_second">
|
||||
<field name="name">Second</field>
|
||||
<field name="symbol">s</field>
|
||||
<field name="category" ref="uom_cat_time"/>
|
||||
<field name="factor" eval="round(1.0 / 3600, 12)"/>
|
||||
<field name="rate" eval="3600."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_minute">
|
||||
<field name="name">Minute</field>
|
||||
<field name="symbol">min</field>
|
||||
<field name="category" ref="uom_cat_time"/>
|
||||
<field name="factor" eval="round(1.0 / 60, 12)"/>
|
||||
<field name="rate" eval="60."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_hour">
|
||||
<field name="name">Hour</field>
|
||||
<field name="symbol">h</field>
|
||||
<field name="category" ref="uom_cat_time"/>
|
||||
<field name="factor" eval="1."/>
|
||||
<field name="rate" eval="1."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_day">
|
||||
<field name="name">Day</field>
|
||||
<field name="symbol">d</field>
|
||||
<field name="category" ref="uom_cat_time"/>
|
||||
<field name="factor" eval="24."/>
|
||||
<field name="rate" eval="round(1.0 / 24, 12)"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record model="product.uom.category" id="uom_cat_length">
|
||||
<field name="name">Length</field>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_meter">
|
||||
<field name="name">Meter</field>
|
||||
<field name="symbol">m</field>
|
||||
<field name="category" ref="uom_cat_length"/>
|
||||
<field name="factor" eval="1."/>
|
||||
<field name="rate" eval="1."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_kilometer">
|
||||
<field name="name">Kilometer</field>
|
||||
<field name="symbol">km</field>
|
||||
<field name="category" ref="uom_cat_length"/>
|
||||
<field name="factor" eval="1000."/>
|
||||
<field name="rate" eval="0.001"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_centimeter">
|
||||
<field name="name">Centimeter</field>
|
||||
<field name="symbol">cm</field>
|
||||
<field name="category" ref="uom_cat_length"/>
|
||||
<field name="factor" eval="0.01"/>
|
||||
<field name="rate" eval="100."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_millimeter">
|
||||
<field name="name">Millimeter</field>
|
||||
<field name="symbol">mm</field>
|
||||
<field name="category" ref="uom_cat_length"/>
|
||||
<field name="factor" eval="0.001"/>
|
||||
<field name="rate" eval="1000."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_foot">
|
||||
<field name="name">Foot</field>
|
||||
<field name="symbol">ft</field>
|
||||
<field name="category" ref="uom_cat_length"/>
|
||||
<field name="factor" eval="0.3048"/>
|
||||
<field name="rate" eval="round(1.0 / 0.3048, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_yard">
|
||||
<field name="name">Yard</field>
|
||||
<field name="symbol">yd</field>
|
||||
<field name="category" ref="uom_cat_length"/>
|
||||
<field name="factor" eval="0.9144"/>
|
||||
<field name="rate" eval="round(1.0/ 0.9144, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_inch">
|
||||
<field name="name">Inch</field>
|
||||
<field name="symbol">in</field>
|
||||
<field name="category" ref="uom_cat_length"/>
|
||||
<field name="factor" eval="0.0254"/>
|
||||
<field name="rate" eval="round(1.0 / 0.0254, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_mile">
|
||||
<field name="name">Mile</field>
|
||||
<field name="symbol">mi</field>
|
||||
<field name="category" ref="uom_cat_length"/>
|
||||
<field name="factor" eval="1609.344"/>
|
||||
<field name="rate" eval="round(1.0 / 1609.344, 12)"/>
|
||||
</record>
|
||||
|
||||
<record model="product.uom.category" id="uom_cat_volume">
|
||||
<field name="name">Volume</field>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_cubic_meter">
|
||||
<field name="name">Cubic meter</field>
|
||||
<field name="symbol">m³</field>
|
||||
<field name="category" ref="uom_cat_volume"/>
|
||||
<field name="factor" eval="1000."/>
|
||||
<field name="rate" eval="0.001"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_liter">
|
||||
<field name="name">Liter</field>
|
||||
<field name="symbol">l</field>
|
||||
<field name="category" ref="uom_cat_volume"/>
|
||||
<field name="factor" eval="1."/>
|
||||
<field name="rate" eval="1."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_cubic_centimeter">
|
||||
<field name="name">Cubic centimeter</field>
|
||||
<field name="symbol">cm³</field>
|
||||
<field name="category" ref="uom_cat_volume"/>
|
||||
<field name="factor" eval="0.001"/>
|
||||
<field name="rate" eval="1000."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_cubic_inch">
|
||||
<field name="name">Cubic inch</field>
|
||||
<field name="symbol">in³</field>
|
||||
<field name="category" ref="uom_cat_volume"/>
|
||||
<field name="factor" eval="0.016387064"/>
|
||||
<field name="rate" eval="round(1.0 / 0.016387064, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_cubic_foot">
|
||||
<field name="name">Cubic foot</field>
|
||||
<field name="symbol">ft³</field>
|
||||
<field name="category" ref="uom_cat_volume"/>
|
||||
<field name="factor" eval="28.316846592"/>
|
||||
<field name="rate" eval="round(1.0 / 28.316846592, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_gallon">
|
||||
<field name="name">Gallon</field>
|
||||
<field name="symbol">gal</field>
|
||||
<field name="category" ref="uom_cat_volume"/>
|
||||
<field name="factor" eval="3.785411784"/>
|
||||
<field name="rate" eval="round(1.0 / 3.785411784, 12)"/>
|
||||
</record>
|
||||
|
||||
<record model="product.uom.category" id="uom_cat_surface">
|
||||
<field name="name">Surface</field>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_square_meter">
|
||||
<field name="name">Square meter</field>
|
||||
<field name="symbol">m²</field>
|
||||
<field name="category" ref="uom_cat_surface"/>
|
||||
<field name="factor" eval="1."/>
|
||||
<field name="rate" eval="1."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_square_centimeter">
|
||||
<field name="name">Square centimeter</field>
|
||||
<field name="symbol">cm²</field>
|
||||
<field name="category" ref="uom_cat_surface"/>
|
||||
<field name="factor" eval="0.0001"/>
|
||||
<field name="rate" eval="10000."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_are">
|
||||
<field name="name">Are</field>
|
||||
<field name="symbol">a</field>
|
||||
<field name="category" ref="uom_cat_surface"/>
|
||||
<field name="factor" eval="100."/>
|
||||
<field name="rate" eval="0.01"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_hectare">
|
||||
<field name="name">Hectare</field>
|
||||
<field name="symbol">ha</field>
|
||||
<field name="category" ref="uom_cat_surface"/>
|
||||
<field name="factor" eval="10000."/>
|
||||
<field name="rate" eval="0.0001"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_square_inch">
|
||||
<field name="name">Square inch</field>
|
||||
<field name="symbol">in²</field>
|
||||
<field name="category" ref="uom_cat_surface"/>
|
||||
<field name="factor" eval="0.00064516"/>
|
||||
<field name="rate" eval="round(1.0 / 0.00064516, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_square_foot">
|
||||
<field name="name">Square foot</field>
|
||||
<field name="symbol">ft²</field>
|
||||
<field name="category" ref="uom_cat_surface"/>
|
||||
<field name="factor" eval="0.09290304"/>
|
||||
<field name="rate" eval="round(1.0 / 0.09290304, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_square_yard">
|
||||
<field name="name">Square yard</field>
|
||||
<field name="symbol">yd²</field>
|
||||
<field name="category" ref="uom_cat_surface"/>
|
||||
<field name="factor" eval="0.83612736"/>
|
||||
<field name="rate" eval="round(1.0 / 0.83612736, 12)"/>
|
||||
</record>
|
||||
|
||||
<record model="product.uom.category" id="uom_cat_energy">
|
||||
<field name="name">Energy</field>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_energy_joule">
|
||||
<field name="name">Joule</field>
|
||||
<field name="symbol">J</field>
|
||||
<field name="category" ref="uom_cat_energy"/>
|
||||
<field name="factor" eval="1."/>
|
||||
<field name="rate" eval="1."/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_energy_megajoule">
|
||||
<field name="name">Megajoule</field>
|
||||
<field name="symbol">MJ</field>
|
||||
<field name="category" ref="uom_cat_energy"/>
|
||||
<field name="factor" eval="1_000_000."/>
|
||||
<field name="rate" eval="round(1.0 / 1_000_000, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_energy_kwh">
|
||||
<field name="name">Kilowatt-hour</field>
|
||||
<field name="symbol">kW⋅h</field>
|
||||
<field name="category" ref="uom_cat_energy"/>
|
||||
<field name="factor" eval="3_600_000"/>
|
||||
<field name="rate" eval="round(1.0 / 3_600_000, 12)"/>
|
||||
</record>
|
||||
<record model="product.uom" id="uom_energy_mwh">
|
||||
<field name="name">Megawatt-hour</field>
|
||||
<field name="symbol">MW⋅h</field>
|
||||
<field name="category" ref="uom_cat_energy"/>
|
||||
<field name="factor" eval="3_600_000_000"/>
|
||||
<field name="rate" eval="round(1.0 / 3_600_000_000, 12)"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
22
modules/product/view/category_form.xml
Normal file
22
modules/product/view/category_form.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="6">
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<label name="code"/>
|
||||
<field name="code"/>
|
||||
<group colspan="2" col="-1" id="checkboxes">
|
||||
<!-- Add here some checkboxes ! -->
|
||||
</group>
|
||||
|
||||
<label name="parent"/>
|
||||
<field name="parent" colspan="3"/>
|
||||
|
||||
<button name="add_products" colspan="6"/>
|
||||
<notebook colspan="6">
|
||||
<page string="Children" col="1" id="childs">
|
||||
<field name="childs"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
8
modules/product/view/category_list.xml
Normal file
8
modules/product/view/category_list.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="code" expand="1" optional="1"/>
|
||||
<field name="rec_name" expand="2"/>
|
||||
<field name="name" tree_invisible="1"/>
|
||||
</tree>
|
||||
8
modules/product/view/category_product_form.xml
Normal file
8
modules/product/view/category_product_form.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="2">
|
||||
<label name="rec_name"/>
|
||||
<field name="rec_name" readonly="1"/>
|
||||
<field name="templates" colspan="2"/>
|
||||
</form>
|
||||
9
modules/product/view/category_tree.xml
Normal file
9
modules/product/view/category_tree.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree keyword_open="1">
|
||||
<field name="code" expand="1" optional="1"/>
|
||||
<field name="name" expand="2"/>
|
||||
<field name="parent" tree_invisible="1"/>
|
||||
<field name="childs" tree_invisible="1"/>
|
||||
</tree>
|
||||
15
modules/product/view/configuration_form.xml
Normal file
15
modules/product/view/configuration_form.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="default_cost_price_method"/>
|
||||
<field name="default_cost_price_method"/>
|
||||
<separator id="sequences" string="Sequences" colspan="4"/>
|
||||
<label name="template_sequence"/>
|
||||
<field name="template_sequence"/>
|
||||
<label name="product_sequence"/>
|
||||
<field name="product_sequence"/>
|
||||
|
||||
<label name="category_sequence"/>
|
||||
<field name="category_sequence"/>
|
||||
</form>
|
||||
13
modules/product/view/identifier_form.xml
Normal file
13
modules/product/view/identifier_form.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="product"/>
|
||||
<field name="product"/>
|
||||
<label name="sequence"/>
|
||||
<field name="sequence"/>
|
||||
<label name="type"/>
|
||||
<field name="type"/>
|
||||
<label name="code"/>
|
||||
<field name="code"/>
|
||||
</form>
|
||||
8
modules/product/view/identifier_list.xml
Normal file
8
modules/product/view/identifier_list.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="product"/>
|
||||
<field name="type"/>
|
||||
<field name="code" expand="1"/>
|
||||
</tree>
|
||||
8
modules/product/view/identifier_list_sequence.xml
Normal file
8
modules/product/view/identifier_list_sequence.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree sequence="sequence">
|
||||
<field name="product"/>
|
||||
<field name="type"/>
|
||||
<field name="code" expand="1"/>
|
||||
</tree>
|
||||
33
modules/product/view/product_form.xml
Normal file
33
modules/product/view/product_form.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="/form/field[@name='code']" position="replace">
|
||||
<group col="-1" name="code" string="">
|
||||
<field name="prefix_code"/>
|
||||
<field name="suffix_code"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='active']" position="after">
|
||||
<label name="replaced_by"/>
|
||||
<field name="replaced_by" colspan="3"/>
|
||||
<newline/>
|
||||
</xpath>
|
||||
<xpath expr="/form/notebook/page[@id='general']/label[@name='type']"
|
||||
position="before">
|
||||
<label name="template"/>
|
||||
<field name="template" colspan="3"/>
|
||||
</xpath>
|
||||
<xpath expr="/form/notebook/page/field[@name='products']" position="replace">
|
||||
<group id="description" colspan="2" col="1" yexpand="1" yfill="1">
|
||||
<separator name="description"/>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="/form/notebook/page[@id='general']" position="after">
|
||||
<page name="identifiers" col="1">
|
||||
<field name="identifiers" pre_validate="1"
|
||||
view_ids="product.identifier_view_list_sequence"/>
|
||||
</page>
|
||||
</xpath>
|
||||
</data>
|
||||
27
modules/product/view/product_form_simple.xml
Normal file
27
modules/product/view/product_form_simple.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="code"/>
|
||||
<group col="-1" name="code" string="">
|
||||
<field name="prefix_code"/>
|
||||
<field name="suffix_code"/>
|
||||
</group>
|
||||
<label name="active"/>
|
||||
<field name="active"/>
|
||||
|
||||
<label name="template"/>
|
||||
<field name="template"/>
|
||||
<label name="position"/>
|
||||
<field name="position"/>
|
||||
|
||||
<label name="list_price"/>
|
||||
<field name="list_price"/>
|
||||
<newline/>
|
||||
|
||||
<label name="cost_price"/>
|
||||
<field name="cost_price"/>
|
||||
<field name="identifiers" colspan="4" pre_validate="1" view_ids="product.identifier_view_list_sequence"/>
|
||||
<separator name="description" colspan="4"/>
|
||||
<field name="description" colspan="4"/>
|
||||
</form>
|
||||
9
modules/product/view/product_replace_ask_form.xml
Normal file
9
modules/product/view/product_replace_ask_form.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="source" string="Product"/>
|
||||
<field name="source"/>
|
||||
<label name="destination" string="Replaced By"/>
|
||||
<field name="destination"/>
|
||||
</form>
|
||||
10
modules/product/view/product_tree.xml
Normal file
10
modules/product/view/product_tree.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="//field[@name='products']" position="replace">
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='list_price']" position="replace">
|
||||
<field name="list_price_used"/>
|
||||
</xpath>
|
||||
</data>
|
||||
6
modules/product/view/product_tree_simple.xml
Normal file
6
modules/product/view/product_tree_simple.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="rec_name" expand="1"/>
|
||||
</tree>
|
||||
6
modules/product/view/product_tree_simple_sequence.xml
Normal file
6
modules/product/view/product_tree_simple_sequence.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree sequence="position">
|
||||
<field name="rec_name" expand="1"/>
|
||||
</tree>
|
||||
38
modules/product/view/template_form.xml
Normal file
38
modules/product/view/template_form.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="6">
|
||||
<label name="name"/>
|
||||
<field name="name" xexpand="1"/>
|
||||
<label name="code"/>
|
||||
<field name="code"/>
|
||||
<label name="active"/>
|
||||
<field name="active" xexpand="0" width="100"/>
|
||||
|
||||
<notebook colspan="6">
|
||||
<page string="General" id="general">
|
||||
<label name="type"/>
|
||||
<field name="type"/>
|
||||
<group colspan="2" col="-1" id="checkboxes">
|
||||
<label name="consumable"/>
|
||||
<field name="consumable" xexpand="0" width="25"/>
|
||||
</group>
|
||||
<label name="default_uom"/>
|
||||
<field name="default_uom" />
|
||||
<newline/>
|
||||
<label name="list_price"/>
|
||||
<field name="list_price"/>
|
||||
<newline/>
|
||||
<label name="cost_price"/>
|
||||
<field name="cost_price"/>
|
||||
<label name="cost_price_method"/>
|
||||
<field name="cost_price_method"/>
|
||||
<newline/>
|
||||
<field name="products" mode="form,tree" colspan="2"
|
||||
view_ids="product.product_view_form_simple,product.product_view_tree_simple_sequence"/>
|
||||
<group id="categories" colspan="2" col="2" yexpand="1" yfill="1">
|
||||
<field name="categories" colspan="2" yexpand="1" yfill="1" view_ids="product.category_view_list"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
12
modules/product/view/template_tree.xml
Normal file
12
modules/product/view/template_tree.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="code" expand="1"/>
|
||||
<field name="name" expand="2"/>
|
||||
<field name="list_price" optional="0"/>
|
||||
<field name="cost_price" optional="0"/>
|
||||
<field name="default_uom" optional="0"/>
|
||||
<field name="type" optional="0"/>
|
||||
<field name="products" optional="1"/>
|
||||
</tree>
|
||||
7
modules/product/view/uom_category_form.xml
Normal file
7
modules/product/view/uom_category_form.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="6">
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
</form>
|
||||
6
modules/product/view/uom_category_tree.xml
Normal file
6
modules/product/view/uom_category_tree.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="name" expand="1"/>
|
||||
</tree>
|
||||
21
modules/product/view/uom_form.xml
Normal file
21
modules/product/view/uom_form.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="4">
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<label name="category"/>
|
||||
<field name="category"/>
|
||||
<label name="symbol"/>
|
||||
<field name="symbol"/>
|
||||
<label name="active"/>
|
||||
<field name="active"/>
|
||||
<label name="factor"/>
|
||||
<field name="factor"/>
|
||||
<label name="rate"/>
|
||||
<field name="rate"/>
|
||||
<label name="rounding"/>
|
||||
<field name="rounding"/>
|
||||
<label name="digits"/>
|
||||
<field name="digits"/>
|
||||
</form>
|
||||
11
modules/product/view/uom_tree.xml
Normal file
11
modules/product/view/uom_tree.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="name" expand="2"/>
|
||||
<field name="symbol" optional="0"/>
|
||||
<field name="category" expand="1" optional="0"/>
|
||||
<field name="factor" optional="1"/>
|
||||
<field name="rate" optional="1"/>
|
||||
<field name="rounding" optional="1"/>
|
||||
</tree>
|
||||
Reference in New Issue
Block a user