Remove accounts from products

The edition of accounting categories are limited to accounting administrators.
If a product does not have an accounting category, the default value defined
on the configuration are used.

issue3805
review45111002
This commit is contained in:
Cédric Krier
2018-06-20 12:09:04 +02:00
parent 4344dc7c08
commit dc6e017e2d
12 changed files with 91 additions and 409 deletions
+2
View File
@@ -1,3 +1,5 @@
* Remove accounts from products
Version 4.8.0 - 2018-04-23
* Bug fixes (see mercurial logs for details)
+12 -16
View File
@@ -2,28 +2,24 @@
# this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from .product import *
from .configuration import *
from . import product
from . import configuration
from . import account
def register():
Pool.register(
ProductConfiguration,
Category,
CategoryAccount,
CategoryCustomerTax,
CategorySupplierTax,
product.Category,
product.CategoryAccount,
product.CategoryCustomerTax,
product.CategorySupplierTax,
account.CreateChartProperties,
Template,
TemplateAccount,
TemplateCustomerTax,
TemplateSupplierTax,
Product,
TemplateAccountCategory,
TemplateCategoryAll,
Configuration,
ConfigurationDefaultAccount,
product.Template,
product.Product,
product.TemplateAccountCategory,
product.TemplateCategoryAll,
configuration.Configuration,
configuration.ConfigurationDefaultAccount,
module='account_product', type_='model')
Pool.register(
account.CreateChart,
+3 -16
View File
@@ -12,20 +12,6 @@ class CreateChartProperties:
__metaclass__ = PoolMeta
__name__ = 'account.create_chart.properties'
product_account_expense = fields.Many2One(
'account.account', 'Default Expense Account',
domain=[
('kind', '=', 'expense'),
('company', '=', Eval('company')),
],
depends=['company'])
product_account_revenue = fields.Many2One(
'account.account', 'Default Revenue Account',
domain=[
('kind', '=', 'revenue'),
('company', '=', Eval('company')),
],
depends=['company'])
category_account_expense = fields.Many2One(
'account.account', 'Default Expense Account',
domain=[
@@ -53,8 +39,9 @@ class CreateChart:
with Transaction().set_context(company=self.properties.company.id):
config = Configuration(1)
for name in ['product_account_expense', 'product_account_revenue',
'category_account_expense', 'category_account_revenue']:
for name in [
'category_account_expense',
'category_account_revenue']:
setattr(config, 'default_%s' % name,
getattr(self.properties, name, None))
config.save()
+2 -63
View File
@@ -3,29 +3,15 @@
from trytond.pool import PoolMeta, Pool
from trytond.model import fields
from trytond.pyson import Eval
from trytond import backend
__all__ = ['Configuration', 'ConfigurationDefaultAccount',
'ProductConfiguration']
__all__ = ['Configuration', 'ConfigurationDefaultAccount']
class Configuration:
__metaclass__ = PoolMeta
__name__ = 'account.configuration'
default_product_account_expense = fields.MultiValue(fields.Many2One(
'account.account', 'Default Account Expense',
domain=[
('kind', '=', 'expense'),
('company', '=', Eval('context', {}).get('company', -1)),
]))
default_product_account_revenue = fields.MultiValue(fields.Many2One(
'account.account', 'Default Account Revenue',
domain=[
('kind', '=', 'revenue'),
('company', '=', Eval('context', {}).get('company', -1)),
]))
default_category_account_expense = fields.MultiValue(fields.Many2One(
'account.account', 'Default Account Expense',
domain=[
@@ -42,9 +28,7 @@ class Configuration:
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field in {'default_product_account_expense',
'default_product_account_revenue',
'default_category_account_expense',
if field in {'default_category_account_expense',
'default_category_account_revenue'}:
return pool.get('account.configuration.default_account')
return super(Configuration, cls).multivalue_model(field)
@@ -53,20 +37,6 @@ class Configuration:
class ConfigurationDefaultAccount:
__metaclass__ = PoolMeta
__name__ = 'account.configuration.default_account'
default_product_account_expense = fields.Many2One(
'account.account', "Default Account Expense",
domain=[
('kind', '=', 'expense'),
('company', '=', Eval('company', -1)),
],
depends=['company'])
default_product_account_revenue = fields.Many2One(
'account.account', "Default Account Revenue",
domain=[
('kind', '=', 'revenue'),
('company', '=', Eval('company', -1)),
],
depends=['company'])
default_category_account_expense = fields.Many2One(
'account.account', "Default Account Expense",
domain=[
@@ -81,34 +51,3 @@ class ConfigurationDefaultAccount:
('company', '=', Eval('company', -1)),
],
depends=['company'])
class ProductConfiguration:
__metaclass__ = PoolMeta
__name__ = 'product.configuration'
default_accounts_category = fields.Boolean(
'Use Category\'s accounts by default')
default_taxes_category = fields.Boolean(
'Use Category\' taxes by default')
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
# Migration from 3.8: rename default_account_category into
# default_accounts_category
table = TableHandler(cls, module_name)
if table.column_exist('default_account_category'):
table.column_rename(
'default_account_category', 'default_accounts_category')
super(ProductConfiguration, cls).__register__(module_name)
@classmethod
def default_default_accounts_category(cls):
return False
@classmethod
def default_default_taxes_category(cls):
return False
-6
View File
@@ -8,11 +8,5 @@ this repository contains the full copyright notices and license terms. -->
<field name="inherit" ref="account.configuration_view_form"/>
<field name="name">configuration_form</field>
</record>
<record model="ir.ui.view" id="product_configuration_view_form">
<field name="model">product.configuration</field>
<field name="inherit" ref="product.product_configuration_view_form"/>
<field name="name">product_configuration_form</field>
</record>
</data>
</tryton>
+42 -220
View File
@@ -15,13 +15,14 @@ from trytond.modules.company.model import (
__all__ = ['Category', 'CategoryAccount',
'CategoryCustomerTax', 'CategorySupplierTax',
'Template', 'TemplateAccount',
'TemplateCustomerTax', 'TemplateSupplierTax',
'Product', 'account_used', 'template_property',
'Template', 'Product', 'account_used', 'template_property',
'TemplateAccountCategory', 'TemplateCategoryAll']
def account_used(field_name):
def account_used(field_name, field_string=None):
if field_string is None:
field_string = field_name
def decorator(func):
@wraps(func)
def wrapper(self):
@@ -30,10 +31,10 @@ def account_used(field_name):
account = self.get_account(field_name + '_used')
# Allow empty values on on_change
if not account and not Transaction().readonly:
field_string = (
self.fields_get([field_name])[field_name]['string'])
field = (
self.fields_get([field_string])[field_string]['string'])
self.raise_user_error('missing_account', {
'field': field_string,
'field': field,
'name': self.rec_name,
})
return account
@@ -310,68 +311,7 @@ class Template(CompanyMultiValueMixin):
account_category = fields.Many2One('product.category', 'Account Category',
domain=[
('accounting', '=', True),
],
states={
'required': (Eval('accounts_category', False)
| Eval('taxes_category', False)),
},
depends=['accounts_category', 'taxes_category'])
accounts_category = fields.Boolean('Use Category\'s accounts',
help="Check to use the accounts defined on the account category.")
accounts = fields.One2Many(
'product.template.account', 'template', "Accounts")
account_expense = fields.MultiValue(fields.Many2One('account.account',
'Account Expense', domain=[
('kind', '=', 'expense'),
('company', '=', Eval('context', {}).get('company', -1)),
],
states={
'invisible': (~Eval('context', {}).get('company')
| Eval('accounts_category')),
},
help=("The account to use instead of the one defined on the "
"account category."), depends=['accounts_category']))
account_revenue = fields.MultiValue(fields.Many2One('account.account',
'Account Revenue', domain=[
('kind', '=', 'revenue'),
('company', '=', Eval('context', {}).get('company', -1)),
],
states={
'invisible': (~Eval('context', {}).get('company')
| Eval('accounts_category')),
},
help=("The account to use instead of the one defined on the "
"account category."), depends=['accounts_category']))
taxes_category = fields.Boolean('Use Category\'s Taxes',
help="Check to use the taxes defined on the account category.")
customer_taxes = fields.Many2Many('product.template-customer-account.tax',
'product', 'tax', 'Customer Taxes',
order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')],
domain=[('parent', '=', None), ['OR',
('group', '=', None),
('group.kind', 'in', ['sale', 'both'])],
],
states={
'invisible': (~Eval('context', {}).get('company')
| Eval('taxes_category')),
}, depends=['taxes_category'],
help="The taxes to apply when selling the product.")
supplier_taxes = fields.Many2Many('product.template-supplier-account.tax',
'product', 'tax', 'Supplier Taxes',
order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')],
domain=[('parent', '=', None), ['OR',
('group', '=', None),
('group.kind', 'in', ['purchase', 'both'])],
],
states={
'invisible': (~Eval('context', {}).get('company')
| Eval('taxes_category')),
}, depends=['taxes_category'],
help="The taxes to apply when purchasing the product.")
customer_taxes_used = fields.Function(fields.One2Many('account.tax', None,
'Customer Taxes Used'), 'get_taxes')
supplier_taxes_used = fields.Function(fields.One2Many('account.tax', None,
'Supplier Taxes Used'), 'get_taxes')
])
@classmethod
def __setup__(cls):
@@ -379,169 +319,53 @@ class Template(CompanyMultiValueMixin):
cls._error_messages.update({
'missing_account': ('There is no '
'"%(field)s" defined on the product "%(name)s"'),
'missing_taxes': ('There is no account category defined '
'on the product "%(name)s"'),
})
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
cursor = Transaction().connection.cursor()
pool = Pool()
Category = pool.get('product.category')
sql_table = cls.__table__()
category = Category.__table__()
table = TableHandler(cls, module_name)
category_exists = table.column_exist('category')
# Migration from 3.8: rename account_category into accounts_category
if (table.column_exist('account_category')
and not table.column_exist('accounts_category')):
table.column_rename('account_category', 'accounts_category')
super(Template, cls).__register__(module_name)
# Migration from 3.8: duplicate category into account_category
if category_exists:
# Only accounting category until now
cursor.execute(*category.update([category.accounting], [True]))
cursor.execute(*sql_table.update(
[sql_table.account_category],
[sql_table.category]))
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field in {'account_expense', 'account_revenue'}:
return pool.get('product.template.account')
return super(Template, cls).multivalue_model(field)
@classmethod
def default_account_expense(cls, **pattern):
pool = Pool()
Configuration = pool.get('account.configuration')
config = Configuration(1)
account = config.get_multivalue(
'default_product_account_expense', **pattern)
return account.id if account else None
@classmethod
def default_account_revenue(cls, **pattern):
pool = Pool()
Configuration = pool.get('account.configuration')
config = Configuration(1)
account = config.get_multivalue(
'default_product_account_revenue', **pattern)
return account.id if account else None
@classmethod
def default_accounts_category(cls):
pool = Pool()
Config = pool.get('product.configuration')
return Config(1).default_accounts_category
@classmethod
def default_taxes_category(cls):
pool = Pool()
Config = pool.get('product.configuration')
return Config(1).default_taxes_category
def get_account(self, name, **pattern):
if self.accounts_category:
if self.account_category:
return self.account_category.get_account(name, **pattern)
else:
return self.get_multivalue(name[:-5], **pattern)
def get_taxes(self, name):
if self.taxes_category:
return [x.id for x in getattr(self.account_category, name)]
else:
return [x.id for x in getattr(self, name[:-5])]
@fields.depends('account_category', 'account_expense')
def on_change_account_expense(self):
if not self.account_category:
if self.account_expense:
self.supplier_taxes = self.account_expense.taxes
else:
self.supplier_taxes = []
@fields.depends('account_category', 'account_revenue')
def on_change_account_revenue(self):
if not self.account_category:
if self.account_revenue:
self.customer_taxes = self.account_revenue.taxes
else:
self.customer_taxes = []
if self.account_category:
return getattr(self.account_category, name)
@property
@account_used('account_expense')
@account_used('account_expense', 'account_category')
def account_expense_used(self):
pass
@property
@account_used('account_revenue')
@account_used('account_revenue', 'account_category')
def account_revenue_used(self):
pass
@property
def customer_taxes_used(self):
taxes = self.get_taxes('customer_taxes_used')
if taxes is None:
account = self.account_revenue_used
if account:
taxes = account.taxes
if taxes is None:
self.raise_user_error('missing_taxes', {
'name': self.rec_name,
})
return taxes
class TemplateAccount(ModelSQL, CompanyValueMixin):
"Product Template Account"
__name__ = 'product.template.account'
template = fields.Many2One(
'product.template', "Template", ondelete='CASCADE', select=True)
account_expense = fields.Many2One(
'account.account', "Account Expense",
domain=[
('kind', '=', 'expense'),
('company', '=', Eval('company', -1)),
],
depends=['company'])
account_revenue = fields.Many2One(
'account.account', "Account Revenue",
domain=[
('kind', '=', 'revenue'),
('company', '=', Eval('company', -1)),
],
depends=['company'])
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
exist = TableHandler.table_exist(cls._table)
super(TemplateAccount, cls).__register__(module_name)
if not exist:
cls._migrate_property([], [], [])
@classmethod
def _migrate_property(cls, field_names, value_names, fields):
field_names.extend(['account_expense', 'account_revenue'])
value_names.extend(['account_expense', 'account_revenue'])
fields.append('company')
migrate_property(
'product.template', field_names, cls, value_names,
parent='template', fields=fields)
class TemplateCustomerTax(ModelSQL):
'Product Template - Customer Tax'
__name__ = 'product.template-customer-account.tax'
_table = 'product_customer_taxes_rel'
product = fields.Many2One('product.template', 'Product Template',
ondelete='CASCADE', select=True, required=True)
tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT',
required=True)
class TemplateSupplierTax(ModelSQL):
'Product Template - Supplier Tax'
__name__ = 'product.template-supplier-account.tax'
_table = 'product_supplier_taxes_rel'
product = fields.Many2One('product.template', 'Product Template',
ondelete='CASCADE', select=True, required=True)
tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT',
required=True)
@property
def supplier_taxes_used(self):
taxes = self.get_taxes('supplier_taxes_used')
if taxes is None:
account = self.account_expense_used
if account:
taxes = account.taxes
if taxes is None:
self.raise_user_error('missing_taxes', {
'name': self.rec_name,
})
return taxes
class Product:
@@ -549,10 +373,8 @@ class Product:
__name__ = 'product.product'
account_expense_used = template_property('account_expense_used')
account_revenue_used = template_property('account_revenue_used')
customer_taxes_used = fields.Function(fields.One2Many('account.tax', None,
'Customer Taxes Used'), 'get_template')
supplier_taxes_used = fields.Function(fields.One2Many('account.tax', None,
'Supplier Taxes Used'), 'get_template')
customer_taxes_used = template_property('customer_taxes_used')
supplier_taxes_used = template_property('supplier_taxes_used')
class TemplateAccountCategory(ModelSQL):
+25 -24
View File
@@ -10,6 +10,31 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">category_form</field>
</record>
<record model="ir.rule.group" id="rule_group_category_default">
<field name="model" search="[('model', '=', 'product.category')]"/>
<field name="default_p" eval="True"/>
<field name="global_p" eval="False"/>
<field name="perm_read" eval="False"/>
</record>
<record model="ir.rule" id="rule_category_default_non_accounting">
<field name="domain" eval="[('accounting', '=', False)]" pyson="1"/>
<field name="rule_group" ref="rule_group_category_default"/>
</record>
<record model="ir.rule.group" id="rule_group_category_accounting">
<field name="model" search="[('model', '=', 'product.category')]"/>
<field name="global_p" eval="False"/>
<field name="perm_read" eval="False"/>
</record>
<record model="ir.rule.group-res.group" id="rule_group_category_accounting-account_admin">
<field name="rule_group" ref="rule_group_category_accounting"/>
<field name="group" ref="account.group_account_admin"/>
</record>
<record model="ir.rule" id="rule_category_accounting_accounting">
<field name="domain" eval="[('accounting', '=', True)]" pyson="1"/>
<field name="rule_group" ref="rule_group_category_accounting"/>
</record>
<record model="ir.ui.view" id="template_view_form">
<field name="model">product.template</field>
<field name="inherit" ref="product.template_view_form"/>
@@ -46,29 +71,5 @@ this repository contains the full copyright notices and license terms. -->
<field name="rule_group" ref="rule_group_category_supplier_taxes"/>
</record>
<record model="ir.rule.group" id="rule_group_template_customer_taxes">
<field name="model"
search="[('model', '=', 'product.template-customer-account.tax')]"/>
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_template_customer_taxes">
<field name="domain"
eval="[('tax.company', '=', Eval('user', {}).get('company', None))]"
pyson="1"/>
<field name="rule_group" ref="rule_group_template_customer_taxes"/>
</record>
<record model="ir.rule.group" id="rule_group_template_supplier_taxes">
<field name="model"
search="[('model', '=', 'product.template-supplier-account.tax')]"/>
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_template_supplier_taxes">
<field name="domain"
eval="[('tax.company', '=', Eval('user', {}).get('company', None))]"
pyson="1"/>
<field name="rule_group" ref="rule_group_template_supplier_taxes"/>
</record>
</data>
</tryton>
+5 -28
View File
@@ -45,34 +45,16 @@ class AccountProductTestCase(ModuleTestCase):
)
template.save()
self.assertIsNone(template.account_expense)
with self.assertRaises(UserError):
template.account_expense_used
# with account
template = ProductTemplate(
name='test account used',
list_price=Decimal(10),
default_uom=unit.id,
account_expense=account_expense.id,
products=[],
)
template.save()
self.assertEqual(template.account_expense, account_expense)
self.assertEqual(template.account_expense_used, account_expense)
# with account on category
category = ProductCategory(name='test account used',
account_expense=account_expense, accounting=True)
category.save()
template.account_expense = None
template.accounts_category = True
template.account_category = category
template.save()
self.assertIsNone(template.account_expense)
self.assertEqual(template.account_expense_used, account_expense)
# with account on grant category
@@ -84,30 +66,25 @@ class AccountProductTestCase(ModuleTestCase):
category.parent = parent_category
category.save()
self.assertIsNone(category.account_expense)
self.assertEqual(template.account_expense_used, account_expense)
self.assertEqual(category.account_expense_used, account_expense)
# raise only at direct usage
templates = ProductTemplate.create([{
categories = ProductCategory.create([{
'name': 'test with account',
'list_price': Decimal(10),
'default_uom': unit.id,
'accounting': True,
'account_expense': account_expense.id,
'products': [],
}, {
'name': 'test without account',
'list_price': Decimal(10),
'default_uom': unit.id,
'accounting': True,
'account_expense': None,
'products': [],
}])
self.assertEqual(templates[0].account_expense_used.id,
self.assertEqual(categories[0].account_expense_used.id,
account_expense.id)
with self.assertRaises(UserError):
templates[1].account_expense_used
categories[1].account_expense_used
def suite():
-5
View File
@@ -4,11 +4,6 @@ this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/field[@name='default_account_payable']"
position="after">
<separator id="product" string="Product" colspan="4"/>
<label name="default_product_account_revenue"/>
<field name="default_product_account_revenue"/>
<label name="default_product_account_expense"/>
<field name="default_product_account_expense"/>
<separator id="category" string="Category" colspan="4"/>
<label name="default_category_account_revenue"/>
<field name="default_category_account_revenue"/>
-5
View File
@@ -3,11 +3,6 @@
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/field[@name='account_payable']" position="after">
<separator id="product" string="Product" colspan="4"/>
<label name="product_account_revenue"/>
<field name="product_account_revenue"/>
<label name="product_account_expense"/>
<field name="product_account_expense"/>
<separator id="category" string="Category" colspan="4"/>
<label name="category_account_revenue"/>
<field name="category_account_revenue"/>
-12
View File
@@ -1,12 +0,0 @@
<?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" position="inside">
<newline/>
<label name="default_accounts_category"/>
<field name="default_accounts_category"/>
<label name="default_taxes_category"/>
<field name="default_taxes_category"/>
</xpath>
</data>
-14
View File
@@ -8,20 +8,6 @@ this repository contains the full copyright notices and license terms. -->
<page string="Accounting" col="4" id="accounting">
<label name="account_category"/>
<field name="account_category"/>
<separator string="Accounts" id="accounts" colspan="4"/>
<label name="accounts_category"/>
<field name="accounts_category"/>
<newline/>
<label name="account_revenue"/>
<field name="account_revenue"/>
<label name="account_expense"/>
<field name="account_expense"/>
<separator string="Taxes" id="taxes" colspan="4"/>
<label name="taxes_category"/>
<field name="taxes_category"/>
<newline/>
<field name="customer_taxes" colspan="2"/>
<field name="supplier_taxes" colspan="2"/>
</page>
</xpath>
</data>