Initial import from Docker volume

This commit is contained in:
root
2025-12-26 13:11:43 +00:00
commit 4998dc066a
13336 changed files with 1767801 additions and 0 deletions

33
modules/product/__init__.py Executable file
View File

@@ -0,0 +1,33 @@
# 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.pool import Pool
from . import category, configuration, ir, product, uom
from .product import (
ProductDeactivatableMixin, TemplateDeactivatableMixin, price_digits,
round_price)
from .uom import uom_conversion_digits
__all__ = [price_digits, round_price, uom_conversion_digits,
ProductDeactivatableMixin, TemplateDeactivatableMixin]
def register():
Pool.register(
ir.Configuration,
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,
configuration.Configuration,
configuration.ConfigurationDefaultCostPriceMethod,
module='product', type_='model')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

115
modules/product/category.py Executable file
View File

@@ -0,0 +1,115 @@
# 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):
"Product Category"
__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(Category, cls).__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],
'pyson_domain': PYSONEncoder().encode(
[('id', '=', categories[0].id)]),
}
@classmethod
def _new_code(cls):
pool = Pool()
Configuration = pool.get('product.configuration')
config = Configuration(1)
sequence = config.category_sequence
if sequence:
return sequence.get()
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
for values in vlist:
if not values.get('code'):
values['code'] = cls._new_code()
return super().create(vlist)
@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 Executable file
View 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>

View File

@@ -0,0 +1,59 @@
# 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):
'Product Configuration'
__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):
"Product Configuration Default Cost Price Method"
__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'

View 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 Executable file
View 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

View 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

24
modules/product/ir.py Executable file
View 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.config import 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))

1158
modules/product/locale/bg.po Executable file

File diff suppressed because it is too large Load Diff

1061
modules/product/locale/ca.po Executable file

File diff suppressed because it is too large Load Diff

1075
modules/product/locale/cs.po Executable file

File diff suppressed because it is too large Load Diff

1070
modules/product/locale/de.po Executable file

File diff suppressed because it is too large Load Diff

1062
modules/product/locale/es.po Executable file

File diff suppressed because it is too large Load Diff

1081
modules/product/locale/es_419.po Executable file

File diff suppressed because it is too large Load Diff

1067
modules/product/locale/et.po Executable file

File diff suppressed because it is too large Load Diff

1074
modules/product/locale/fa.po Executable file

File diff suppressed because it is too large Load Diff

1070
modules/product/locale/fi.po Executable file

File diff suppressed because it is too large Load Diff

1068
modules/product/locale/fr.po Executable file

File diff suppressed because it is too large Load Diff

1082
modules/product/locale/hu.po Executable file

File diff suppressed because it is too large Load Diff

1049
modules/product/locale/id.po Executable file

File diff suppressed because it is too large Load Diff

1078
modules/product/locale/it.po Executable file

File diff suppressed because it is too large Load Diff

1169
modules/product/locale/lo.po Executable file

File diff suppressed because it is too large Load Diff

1073
modules/product/locale/lt.po Executable file

File diff suppressed because it is too large Load Diff

1064
modules/product/locale/nl.po Executable file

File diff suppressed because it is too large Load Diff

1060
modules/product/locale/pl.po Executable file

File diff suppressed because it is too large Load Diff

1074
modules/product/locale/pt.po Executable file

File diff suppressed because it is too large Load Diff

1068
modules/product/locale/ro.po Executable file

File diff suppressed because it is too large Load Diff

1180
modules/product/locale/ru.po Executable file

File diff suppressed because it is too large Load Diff

1109
modules/product/locale/sl.po Executable file

File diff suppressed because it is too large Load Diff

1121
modules/product/locale/tr.po Executable file

File diff suppressed because it is too large Load Diff

1039
modules/product/locale/uk.po Executable file

File diff suppressed because it is too large Load Diff

1058
modules/product/locale/zh_CN.po Executable file

File diff suppressed because it is too large Load Diff

37
modules/product/message.xml Executable file
View File

@@ -0,0 +1,37 @@
<?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>
</data>
</tryton>

870
modules/product/product.py Executable file
View File

@@ -0,0 +1,870 @@
# 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 copy
import logging
from decimal import Decimal
from importlib import import_module
import stdnum
import stdnum.exceptions
from sql import Literal
from sql.conditionals import Coalesce
from sql.functions import CharLength
from sql.operators import Equal
from trytond.i18n import gettext
from trytond.model import (
DeactivableMixin, Exclude, Index, Model, ModelSQL, ModelView, UnionMixin,
fields, sequence_ordered)
from trytond.modules.company.model import (
CompanyMultiValueMixin, CompanyValueMixin)
from trytond.pool import Pool
from trytond.pyson import Eval, Get
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction
try:
from trytond.tools import barcode
except ImportError:
barcode = None
from .exceptions import InvalidIdentifierCode
from .ir import price_decimal
__all__ = ['price_digits', 'round_price', 'TemplateFunction']
logger = logging.getLogger(__name__)
TYPES = [
('goods', 'Goods'),
('assets', 'Assets'),
('service', 'Service'),
]
COST_PRICE_METHODS = [
('fixed', 'Fixed'),
('average', 'Average'),
]
price_digits = (16, price_decimal)
def round_price(value, rounding=None):
"Round price using the price digits"
if isinstance(value, int):
return Decimal(value)
return value.quantize(
Decimal(1) / 10 ** price_digits[1], rounding=rounding)
class Template(
DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):
"Product Template"
__name__ = "product.template"
_order_name = 'rec_name'
name = fields.Char(
"Name", size=None, required=True, translate=True)
code_readonly = fields.Function(
fields.Boolean("Code Readonly"), 'get_code_readonly')
code = fields.Char(
"Code", strip='leading',
states={
'readonly': Eval('code_readonly', False),
})
type = fields.Selection(TYPES, "Type", required=True)
consumable = fields.Boolean('Consumable',
states={
'invisible': Eval('type', 'goods') != 'goods',
},
help="Check to allow stock moves to be assigned "
"regardless of stock level.")
list_price = fields.MultiValue(fields.Numeric(
"List Price", digits=price_digits,
states={
'readonly': ~Eval('context', {}).get('company'),
},
help="The standard price the product is sold at."))
list_prices = fields.One2Many(
'product.list_price', 'template', "List Prices")
cost_price = fields.Function(fields.Numeric(
"Cost Price", digits=price_digits,
help="The amount it costs to purchase or make the product, "
"or carry out the service."),
'get_cost_price')
cost_price_method = fields.MultiValue(fields.Selection(
COST_PRICE_METHODS, "Cost Price Method", required=True,
help="The method used to calculate the cost price."))
cost_price_methods = fields.One2Many(
'product.cost_price_method', 'template', "Cost Price Methods")
default_uom = fields.Many2One(
'product.uom', "Default UoM", required=True,
help="The standard Unit of Measure for the product.\n"
"Used internally when calculating the stock levels of goods "
"and assets.")
default_uom_category = fields.Function(
fields.Many2One(
'product.uom.category', "Default UoM Category",
help="The category of the default Unit of Measure."),
'on_change_with_default_uom_category',
searcher='search_default_uom_category')
categories = fields.Many2Many(
'product.template-product.category', 'template', 'category',
"Categories",
help="The categories that the product is in.\n"
"Used to group similar products together.")
categories_all = fields.Many2Many(
'product.template-product.category.all',
'template', 'category', "Categories", readonly=True)
products = fields.One2Many(
'product.product', 'template', "Variants",
help="The different variants the product comes in.")
@classmethod
def __setup__(cls):
cls.code.search_unaccented = False
super().__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(t, (t.code, Index.Similarity())),
})
cls._order.insert(0, ('rec_name', 'ASC'))
types_cost_method = cls._cost_price_method_domain_per_type()
cls.cost_price_method.domain = [
Get(types_cost_method, Eval('type'), []),
]
@classmethod
def _cost_price_method_domain_per_type(cls):
return {'service': [('cost_price_method', '=', 'fixed')]}
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field == 'list_price':
return pool.get('product.list_price')
elif field == 'cost_price_method':
return pool.get('product.cost_price_method')
return super(Template, cls).multivalue_model(field)
@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 order_rec_name(cls, tables):
table, _ = tables[None]
return cls.order_code(tables) + [table.name]
def get_rec_name(self, name):
if self.code:
return '[' + self.code + '] ' + self.name
else:
return self.name
@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),
('products.code', operator, code_value, *extra),
('products.identifiers.code', operator, code_value, *extra),
]
@staticmethod
def default_type():
return 'goods'
@staticmethod
def default_consumable():
return False
def get_cost_price(self, name):
if len(self.products) == 1:
product, = self.products
return product.cost_price
@classmethod
def default_cost_price_method(cls, **pattern):
pool = Pool()
Configuration = pool.get('product.configuration')
return Configuration(1).get_multivalue(
'default_cost_price_method', **pattern)
@classmethod
def default_products(cls):
transaction = Transaction()
if (transaction.user == 0
or not transaction.context.get('default_products', True)):
return []
return [{}]
@classmethod
def default_code_readonly(cls):
pool = Pool()
Configuration = pool.get('product.configuration')
config = Configuration(1)
return bool(config.template_sequence)
def get_code_readonly(self, name):
return self.default_code_readonly()
@fields.depends('type', 'cost_price_method')
def on_change_type(self):
if self.type == 'service':
self.cost_price_method = 'fixed'
@fields.depends('default_uom')
def on_change_with_default_uom_category(self, name=None):
return self.default_uom.category if self.default_uom else None
@classmethod
def search_default_uom_category(cls, name, clause):
return [('default_uom.category' + clause[0][len(name):], *clause[1:])]
@classmethod
def _new_code(cls):
pool = Pool()
Configuration = pool.get('product.configuration')
config = Configuration(1)
sequence = config.template_sequence
if sequence:
return sequence.get()
@classmethod
def create(cls, vlist):
pool = Pool()
Product = pool.get('product.product')
vlist = [v.copy() for v in vlist]
for values in vlist:
values.setdefault('products', None)
if not values.get('code'):
values['code'] = cls._new_code()
templates = super(Template, cls).create(vlist)
products = sum((t.products for t in templates), ())
Product.sync_code(products)
return templates
@classmethod
def write(cls, *args):
pool = Pool()
Product = pool.get('product.product')
super().write(*args)
templates = sum(args[0:None:2], [])
products = sum((t.products for t in templates), ())
Product.sync_code(products)
@classmethod
def copy(cls, templates, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('code', None)
return super().copy(templates, default=default)
@classmethod
def search_global(cls, text):
for record, rec_name, icon in super(Template, cls).search_global(text):
icon = icon or 'tryton-product'
yield record, rec_name, icon
class TemplateFunction(fields.Function):
def __init__(self, field):
super(TemplateFunction, self).__init__(
field, 'get_template', searcher='search_template')
# Disable on_change as it is managed by on_change_template
self.on_change = set()
self.on_change_with = set()
def __copy__(self):
return TemplateFunction(copy.copy(self._field))
def __deepcopy__(self, memo):
return TemplateFunction(copy.deepcopy(self._field, memo))
@staticmethod
def order(name):
@classmethod
def order(cls, tables):
pool = Pool()
Template = pool.get('product.template')
product, _ = tables[None]
if 'template' not in tables:
template = Template.__table__()
tables['template'] = {
None: (template, product.template == template.id),
}
return getattr(Template, name).convert_order(
name, tables['template'], Template)
return order
def definition(self, model, language):
pool = Pool()
Template = pool.get('product.template')
definition = super().definition(model, language)
definition['searchable'] = self._field.definition(
Template, language)['searchable']
return definition
class TemplateDeactivatableMixin(DeactivableMixin):
__slots__ = ()
@classmethod
def _active_expression(cls, tables):
pool = Pool()
Template = pool.get('product.template')
table, _ = tables[None]
if 'template' not in tables:
template = Template.__table__()
tables['template'] = {
None: (template, table.template == template.id),
}
else:
template, _ = tables['template'][None]
return table.active & template.active
@classmethod
def domain_active(cls, domain, tables):
expression = cls._active_expression(tables)
_, operator, value = domain
if operator in {'=', '!='}:
if (operator == '=') != value:
expression = ~expression
elif operator in {'in', 'not in'}:
if True in value and False not in value:
pass
elif False in value and True not in value:
expression = ~expression
else:
expression = Literal(True)
else:
expression = Literal(True)
return expression
class ProductDeactivatableMixin(TemplateDeactivatableMixin):
__slots__ = ()
@classmethod
def _active_expression(cls, tables):
pool = Pool()
Product = pool.get('product.product')
table, _ = tables[None]
if 'product' not in tables:
product = Product.__table__()
tables['product'] = {
None: (product, table.product == product.id),
}
else:
product, _ = tables['product'][None]
expression = super()._active_expression(tables)
return expression & Coalesce(product.active, expression)
class Product(
TemplateDeactivatableMixin, ModelSQL, ModelView,
CompanyMultiValueMixin):
"Product Variant"
__name__ = "product.product"
_order_name = 'rec_name'
template = fields.Many2One(
'product.template', "Product Template",
required=True, ondelete='CASCADE',
search_context={'default_products': False},
help="The product that defines the common properties "
"inherited by the variant.")
code_readonly = fields.Function(fields.Boolean('Code Readonly'),
'get_code_readonly')
prefix_code = fields.Function(fields.Char(
"Prefix Code",
states={
'invisible': ~Eval('prefix_code'),
}),
'on_change_with_prefix_code')
suffix_code = fields.Char(
"Suffix Code", strip='trailing',
states={
'readonly': Eval('code_readonly', False),
},
help="The unique identifier for the product (aka SKU).")
code = fields.Char(
"Code", readonly=True,
help="A unique identifier for the variant.")
identifiers = fields.One2Many(
'product.identifier', 'product', "Identifiers",
help="Other identifiers associated with the variant.")
cost_price = fields.MultiValue(fields.Numeric(
"Cost Price", digits=price_digits,
states={
'readonly': ~Eval('context', {}).get('company'),
},
help="The amount it costs to purchase or make the variant, "
"or carry out the service."))
cost_prices = fields.One2Many(
'product.cost_price', 'product', "Cost Prices")
description = fields.Text("Description", translate=True)
list_price_uom = fields.Function(fields.Numeric('List Price',
digits=price_digits), 'get_price_uom')
cost_price_uom = fields.Function(fields.Numeric('Cost Price',
digits=price_digits), 'get_price_uom')
@classmethod
def __setup__(cls):
pool = Pool()
Template = pool.get('product.template')
if not hasattr(cls, '_no_template_field'):
cls._no_template_field = set()
cls._no_template_field.update(['products'])
cls.suffix_code.search_unaccented = False
cls.code.search_unaccented = False
super(Product, cls).__setup__()
cls.__access__.add('template')
cls._order.insert(0, ('rec_name', 'ASC'))
t = cls.__table__()
cls._sql_constraints = [
('code_exclude', Exclude(t, (t.code, Equal),
where=(t.active == Literal(True))
& (t.code != '')),
'product.msg_product_code_unique'),
]
cls._sql_indexes.add(
Index(t, (t.code, Index.Similarity())))
for attr in dir(Template):
tfield = getattr(Template, attr)
if not isinstance(tfield, fields.Field):
continue
if attr in cls._no_template_field:
continue
field = getattr(cls, attr, None)
if not field or isinstance(field, TemplateFunction):
tfield = copy.deepcopy(tfield)
if hasattr(tfield, 'field'):
tfield.field = None
invisible_state = ~Eval('template')
if 'invisible' in tfield.states:
tfield.states['invisible'] |= invisible_state
else:
tfield.states['invisible'] = invisible_state
setattr(cls, attr, TemplateFunction(tfield))
order_method = getattr(cls, 'order_%s' % attr, None)
if (not order_method
and not isinstance(tfield, (
fields.Function,
fields.One2Many,
fields.Many2Many))):
order_method = TemplateFunction.order(attr)
setattr(cls, 'order_%s' % attr, order_method)
if isinstance(tfield, fields.One2Many):
getattr(cls, attr).setter = '_set_template_function'
@classmethod
def __register__(cls, module):
table = cls.__table__()
table_h = cls.__table_handler__(module)
fill_suffix_code = (
table_h.column_exist('code')
and not table_h.column_exist('suffix_code'))
super().__register__(module)
cursor = Transaction().connection.cursor()
# Migration from 5.4: split code into prefix/suffix
if fill_suffix_code:
cursor.execute(*table.update(
[table.suffix_code],
[table.code]))
@classmethod
def _set_template_function(cls, products, name, value):
# Prevent NotImplementedError for One2Many
pass
@classmethod
def order_suffix_code(cls, tables):
table, _ = tables[None]
if cls.default_code_readonly():
return [CharLength(table.suffix_code), table.suffix_code]
else:
return [table.suffix_code]
@classmethod
def order_code(cls, tables):
pool = Pool()
Template = pool.get('product.template')
table, _ = tables[None]
if cls.default_code_readonly() or Template.default_code_readonly():
return [CharLength(table.code), table.code]
else:
return [table.code]
@fields.depends('template', '_parent_template.id')
def on_change_template(self):
for name, field in self._fields.items():
if isinstance(field, TemplateFunction):
if self.template:
value = getattr(self.template, name, None)
else:
value = None
setattr(self, name, value)
def get_template(self, name):
value = getattr(self.template, name)
if isinstance(value, Model):
field = getattr(self.__class__, name)
if field._type == 'reference':
return str(value)
return value.id
elif (isinstance(value, (list, tuple))
and value and isinstance(value[0], Model)):
return [r.id for r in value]
else:
return value
@fields.depends('template', '_parent_template.code')
def on_change_with_prefix_code(self, name=None):
if self.template:
return self.template.code
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field == 'cost_price':
return pool.get('product.cost_price')
return super(Product, cls).multivalue_model(field)
def set_multivalue(self, name, value, save=True, **pattern):
context = Transaction().context
if name in {'cost_price', 'list_price'} and not value:
if not pattern.get('company', context.get('company')):
return []
return super().set_multivalue(name, value, save=save, **pattern)
def get_multivalue(self, name, **pattern):
if isinstance(self._fields[name], TemplateFunction):
return self.template.get_multivalue(name, **pattern)
else:
return super().get_multivalue(name, **pattern)
@classmethod
def default_cost_price(cls, **pattern):
context = Transaction().context
if pattern.get('company', context.get('company')):
return Decimal(0)
@classmethod
def search_template(cls, name, clause):
return [('template.' + clause[0],) + tuple(clause[1:])]
@classmethod
def order_rec_name(cls, tables):
pool = Pool()
Template = pool.get('product.template')
product, _ = tables[None]
if 'template' not in tables:
template = Template.__table__()
tables['template'] = {
None: (template, product.template == template.id),
}
return cls.order_code(tables) + Template.name.convert_order('name',
tables['template'], Template)
def get_rec_name(self, name):
if self.code:
return '[' + self.code + '] ' + self.name
else:
return self.name
@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,
('code', operator, code_value, *extra),
('identifiers.code', operator, code_value, *extra),
('template.name', operator, operand, *extra),
('template.code', operator, code_value, *extra),
]
@staticmethod
def get_price_uom(products, name):
Uom = Pool().get('product.uom')
res = {}
field = name[:-4]
if Transaction().context.get('uom'):
to_uom = Uom(Transaction().context['uom'])
else:
to_uom = None
for product in products:
price = getattr(product, field)
if to_uom and product.default_uom.category == to_uom.category:
res[product.id] = Uom.compute_price(
product.default_uom, price, to_uom)
else:
res[product.id] = price
return res
@classmethod
def search_global(cls, text):
for id_, rec_name, icon in super(Product, cls).search_global(text):
icon = icon or 'tryton-product'
yield id_, rec_name, icon
@classmethod
def default_code_readonly(cls):
pool = Pool()
Configuration = pool.get('product.configuration')
config = Configuration(1)
return bool(config.product_sequence)
def get_code_readonly(self, name):
return self.default_code_readonly()
def identifier_get(self, types=None):
"Return the first identifier for the given types"
if isinstance(types, str) or types is None:
types = {types}
for identifier in self.identifiers:
if identifier.type in types:
return identifier
@classmethod
def _new_suffix_code(cls):
pool = Pool()
Configuration = pool.get('product.configuration')
config = Configuration(1)
sequence = config.product_sequence
if sequence:
return sequence.get()
@classmethod
def create(cls, vlist):
vlist = [x.copy() for x in vlist]
for values in vlist:
if not values.get('suffix_code'):
values['suffix_code'] = cls._new_suffix_code()
products = super().create(vlist)
cls.sync_code(products)
return products
@classmethod
def write(cls, *args):
super().write(*args)
products = sum(args[0:None:2], [])
cls.sync_code(products)
@classmethod
def copy(cls, products, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('suffix_code', None)
default.setdefault('code', None)
return super().copy(products, default=default)
@property
def list_price_used(self):
transaction = Transaction()
with transaction.reset_context(), \
transaction.set_context(self._context):
return self.template.get_multivalue('list_price')
@classmethod
def sync_code(cls, products):
for product in products:
code = ''.join(filter(None, [
product.prefix_code, product.suffix_code]))
if cls.code.strip:
if cls.code.strip == 'leading':
code = code.lstrip()
elif cls.code.strip == 'trailing':
code = code.rstrip()
else:
code = code.strip()
if not code:
code = None
if code != product.code:
product.code = code
cls.save(products)
class ProductListPrice(ModelSQL, CompanyValueMixin):
"Product List Price"
__name__ = 'product.list_price'
template = fields.Many2One(
'product.template', "Template", ondelete='CASCADE',
context={
'company': Eval('company', -1),
},
depends={'company'})
list_price = fields.Numeric("List Price", digits=price_digits)
@classmethod
def __setup__(cls):
super().__setup__()
cls.company.required = True
class ProductCostPriceMethod(ModelSQL, CompanyValueMixin):
"Product Cost Price Method"
__name__ = 'product.cost_price_method'
template = fields.Many2One(
'product.template', "Template", ondelete='CASCADE',
context={
'company': Eval('company', -1),
},
depends={'company'})
cost_price_method = fields.Selection(
'get_cost_price_methods', "Cost Price Method")
@classmethod
def get_cost_price_methods(cls):
pool = Pool()
Template = pool.get('product.template')
field_name = 'cost_price_method'
methods = Template.fields_get([field_name])[field_name]['selection']
methods.append((None, ''))
return methods
class ProductCostPrice(ModelSQL, CompanyValueMixin):
"Product Cost Price"
__name__ = 'product.cost_price'
product = fields.Many2One(
'product.product', "Product", ondelete='CASCADE',
context={
'company': Eval('company', -1),
},
depends={'company'})
cost_price = fields.Numeric(
"Cost Price", required=True, digits=price_digits)
@classmethod
def __setup__(cls):
super().__setup__()
cls.company.required = True
class TemplateCategory(ModelSQL):
'Template - Category'
__name__ = 'product.template-product.category'
template = fields.Many2One(
'product.template', "Template", ondelete='CASCADE', required=True)
category = fields.Many2One(
'product.category', "Category", ondelete='CASCADE', required=True)
class TemplateCategoryAll(UnionMixin, ModelSQL):
"Template - Category All"
__name__ = 'product.template-product.category.all'
template = fields.Many2One('product.template', "Template")
category = fields.Many2One('product.category', "Category")
@classmethod
def union_models(cls):
return ['product.template-product.category']
class ProductIdentifier(sequence_ordered(), ModelSQL, ModelView):
"Product Identifier"
__name__ = 'product.identifier'
_rec_name = 'code'
product = fields.Many2One(
'product.product', "Product", ondelete='CASCADE', required=True,
help="The product identified by the code.")
type = fields.Selection([
(None, ''),
('ean', "International Article Number"),
('isan', "International Standard Audiovisual Number"),
('isbn', "International Standard Book Number"),
('isil', "International Standard Identifier for Libraries"),
('isin', "International Securities Identification Number"),
('ismn', "International Standard Music Number"),
('brand', "Brand"),
('mpn', "Manufacturer Part Number"),
], "Type")
type_string = type.translated('type')
code = fields.Char("Code", required=True)
@classmethod
def __setup__(cls):
cls.code.search_unaccented = False
super().__setup__()
cls.__access__.add('product')
t = cls.__table__()
cls._sql_indexes.update({
Index(
t,
(t.product, Index.Equality()),
(t.code, Index.Similarity())),
})
@property
@fields.depends('type')
def _is_stdnum(self):
return self.type in {'ean', 'isan', 'isbn', 'isil', 'isin', 'ismn'}
@fields.depends('type', 'code', methods=['_is_stdnum'])
def on_change_with_code(self):
if self._is_stdnum:
try:
module = import_module('stdnum.%s' % self.type)
return module.compact(self.code)
except ImportError:
pass
except stdnum.exceptions.ValidationError:
pass
return self.code
def pre_validate(self):
super().pre_validate()
self.check_code()
@fields.depends('type', 'product', 'code', methods=['_is_stdnum'])
def check_code(self):
if self._is_stdnum:
try:
module = import_module('stdnum.%s' % self.type)
except ModuleNotFoundError:
return
if not module.is_valid(self.code):
if self.product and self.product.id > 0:
product = self.product.rec_name
else:
product = ''
raise InvalidIdentifierCode(
gettext('product.msg_invalid_code',
type=self.type_string,
code=self.code,
product=product))
def barcode(self, format='svg', type=None, **options):
if type is None:
type = self.type
if barcode and type in barcode.BARCODES:
generator = getattr(barcode, 'generate_%s' % format)
return generator(type, self.on_change_with_code(), **options)

269
modules/product/product.xml Executable file
View File

@@ -0,0 +1,269 @@
<?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">
<field name="name">Product</field>
</record>
<record model="res.group" id="group_product_admin">
<field name="name">Product Administration</field>
<field name="parent" ref="group_product"/>
</record>
<record model="res.user-res.group" id="user_admin_group_product">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_product"/>
</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>
<record model="ir.ui.menu-res.group" id="menu_product_group_product">
<field name="menu" ref="menu_main_product"/>
<field name="group" ref="group_product"/>
</record>
<record model="ir.model.access" id="access_bank_bank">
<field name="model">product.product</field>
<field name="group" ref="group_product"/>
<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>
<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_tree2">
<field name="model">product.template</field>
<field name="type">tree</field>
<field name="name">template_tree2</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_tree2"/>
<field name="act_window" ref="act_template_form"/>
</record>
<record model="ir.action.act_window.view" id="act_template_list_view3">
<field name="sequence" eval="20"/>
<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="30"/>
<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_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.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>
</tryton>

View 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.

Binary file not shown.

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

View File

@@ -0,0 +1,86 @@
========================
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_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
>>> product, = template.products
>>> product.code
'PROD'
>>> product.suffix_code = "001"
>>> product.save()
>>> product.code
'PROD001'
Create a variant::
>>> Product = Model.get('product.product')
>>> product = Product()
>>> product.template = template
>>> product.name
'Product'
>>> product.list_price
Decimal('42.0000')
>>> product.suffix_code = "002"
>>> product.save()
>>> product.list_price
Decimal('42.0000')
>>> product.code
'PROD002'
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'

View File

@@ -0,0 +1,616 @@
# 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),
('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')),
('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

View 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)

12
modules/product/tryton.cfg Executable file
View File

@@ -0,0 +1,12 @@
[tryton]
version=7.2.1
depends:
company
ir
res
xml:
product.xml
category.xml
uom.xml
configuration.xml
message.xml

318
modules/product/uom.py Executable file
View File

@@ -0,0 +1,318 @@
# 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
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import (
Check, DeactivableMixin, DigitsMixin, ModelSQL, ModelView, SymbolMixin,
fields)
from trytond.pyson import Eval
from trytond.transaction import Transaction
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):
"Unit of Measure Category"
__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(UomCategory, cls).__setup__()
cls._order.insert(0, ('name', 'ASC'))
class Uom(SymbolMixin, DigitsMixin, DeactivableMixin, ModelSQL, ModelView):
"Unit of Measure"
__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,
help="The coefficient for the formula:\n"
"1 (base unit) = coef (this unit)")
factor = fields.Float(
"Factor", digits=uom_conversion_digits, required=True,
help="The coefficient for the formula:\n"
"coefficient (base unit) = 1 (this unit)")
rounding = fields.Float(
"Rounding Precision", digits=(12, Eval('digits', 12)), 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(Uom, cls).__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'))
@classmethod
def check_xml_record(cls, records, values):
pass
@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 write(cls, *args):
if Transaction().user == 0:
super(Uom, cls).write(*args)
return
all_uoms = sum(args[0:None:2], [])
old_uom = {
uom.id: (uom.factor, uom.rate, uom.category) for uom in all_uoms}
old_digits = {uom.id: uom.digits for uom in all_uoms}
super(Uom, cls).write(*args)
for uom in all_uoms:
for i, field in enumerate(['factor', 'rate', 'category']):
if getattr(uom, field) != old_uom[uom.id][i]:
raise UOMAccessError(
gettext('product.msg_uom_modify_%s' % field,
uom=uom.rec_name),
gettext('product.msg_uom_modify_options'))
if uom.digits < old_digits[uom.id]:
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.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
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")
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)
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 Executable file
View 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"></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"></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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,28 @@
<?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="/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>

View File

@@ -0,0 +1,20 @@
<?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="template"/>
<field name="template"/>
<newline/>
<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="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>

View 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. -->
<data>
<xpath expr="//field[@name='products']" position="replace">
</xpath>
</data>

View 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>

View 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"/>
<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>

View 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>

View 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. -->
<tree>
<field name="code" expand="1"/>
<field name="name" expand="2"/>
</tree>

View 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>

View 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>

View 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>

View 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>