# 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 functools import wraps from sql import Null from trytond import backend from trytond.i18n import gettext from trytond.model import ModelSQL, fields from trytond.modules.company.model import ( CompanyMultiValueMixin, CompanyValueMixin) from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval, Or from trytond.transaction import Transaction from .exceptions import AccountError, TaxError def account_used(field_name, field_string=None): def decorator(func): @wraps(func) def wrapper(self): account = func(self) if not account: account = self.get_account(field_name + '_used') # Allow empty values on on_change if not account and not Transaction().readonly: Model = self.__class__ field = field_name if field_string: if getattr(self, field_string, None): Model = getattr(self, field_string).__class__ else: field = field_string field = ( Model.fields_get([field])[field]['string']) raise AccountError( gettext('account_product.msg_missing_account', field=field, name=self.rec_name)) if account: return account.current() return wrapper return decorator def template_property(field_name): @property @fields.depends('template') def prop(self): return getattr(self.template, field_name) return prop class Category(CompanyMultiValueMixin, metaclass=PoolMeta): __name__ = 'product.category' accounting = fields.Boolean( "Accounting", states={ 'readonly': Bool(Eval('childs', [0])) | Bool(Eval('parent')), }, help="Check to indicate the category is used for accounting.") account_parent = fields.Boolean('Use Parent\'s accounts', states={ 'invisible': ~Eval('accounting', False), }, help="Use the accounts defined on the parent category.") accounts = fields.One2Many( 'product.category.account', 'category', "Accounts") account_expense = fields.MultiValue(fields.Many2One('account.account', 'Account Expense', domain=[ ('closed', '!=', True), ('type.expense', '=', True), ('company', '=', Eval('context', {}).get('company', -1)), ], states={ 'invisible': (~Eval('context', {}).get('company') | Eval('account_parent') | ~Eval('accounting', False)), })) account_revenue = fields.MultiValue(fields.Many2One('account.account', 'Account Revenue', domain=[ ('closed', '!=', True), ('type.revenue', '=', True), ('company', '=', Eval('context', {}).get('company', -1)), ], states={ 'invisible': (~Eval('context', {}).get('company') | Eval('account_parent') | ~Eval('accounting', False)), })) taxes_parent = fields.Boolean('Use the Parent\'s Taxes', states={ 'invisible': ~Eval('accounting', False), }, help="Use the taxes defined on the parent category.") customer_taxes = fields.Many2Many('product.category-customer-account.tax', 'category', '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_parent') | ~Eval('accounting', False)), }, help="The taxes to apply when selling products of this category.") supplier_taxes = fields.Many2Many('product.category-supplier-account.tax', 'category', '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_parent') | ~Eval('accounting', False)), }, help="The taxes to apply when purchasing products of this category.") supplier_taxes_deductible_rate = fields.Numeric( "Supplier Taxes Deductible Rate", digits=(None, 10), domain=[ ('supplier_taxes_deductible_rate', '>=', 0), ('supplier_taxes_deductible_rate', '<=', 1), ], states={ 'invisible': ( Eval('taxes_parent') | ~Eval('accounting', False)), }) customer_taxes_used = fields.Function(fields.Many2Many( 'account.tax', None, None, "Customer Taxes Used"), 'get_taxes') supplier_taxes_used = fields.Function(fields.Many2Many( 'account.tax', None, None, "Supplier Taxes Used"), 'get_taxes') accounting_templates = fields.One2Many( 'product.template', 'account_category', "Accounting Products", states={ 'invisible': ~Eval('accounting', False), }, help="The products for which the accounting category applies.") @classmethod def __setup__(cls): super().__setup__() cls.parent.domain = [ ('accounting', '=', Eval('accounting', False)), cls.parent.domain or []] cls.parent.states['required'] = Or( cls.parent.states.get('required', False), Eval('account_parent', False) | Eval('taxes_parent', False)) @classmethod def multivalue_model(cls, field): pool = Pool() if field in {'account_expense', 'account_revenue'}: return pool.get('product.category.account') return super().multivalue_model(field) @classmethod def default_accounting(cls): return False @classmethod def default_account_expense(cls, **pattern): pool = Pool() Configuration = pool.get('account.configuration') config = Configuration(1) account = config.get_multivalue( 'default_category_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_category_account_revenue', **pattern) return account.id if account else None @classmethod def default_supplier_taxes_deductible_rate(cls): return 1 def get_account(self, name, **pattern): if self.account_parent: return self.parent.get_account(name, **pattern) else: transaction = Transaction() with transaction.reset_context(), \ transaction.set_context(self._context): return self.get_multivalue(name[:-5], **pattern) def get_taxes(self, name): company = Transaction().context.get('company') if self.taxes_parent: return [x.id for x in getattr(self.parent, name)] else: return [x.id for x in getattr(self, name[:-5]) if x.company.id == company] @fields.depends('parent', '_parent_parent.accounting', 'accounting') def on_change_with_accounting(self): if self.parent: return self.parent.accounting return self.accounting @fields.depends( 'accounting', 'account_parent', 'account_expense', 'account_revenue', 'taxes_parent', 'customer_taxes', 'supplier_taxes') def on_change_accounting(self): if not self.accounting: self.account_parent = None self.account_expense = None self.account_revenue = None self.taxes_parent = None self.customer_taxes = None self.supplier_taxes = None @fields.depends('account_expense', 'supplier_taxes') def on_change_account_expense(self): if self.account_expense: self.supplier_taxes = self.account_expense.taxes else: self.supplier_taxes = [] @fields.depends('account_revenue', 'customer_taxes') def on_change_account_revenue(self): if self.account_revenue: self.customer_taxes = self.account_revenue.taxes else: self.customer_taxes = [] @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/form/notebook/page[@id="accounting"]', 'states', { 'invisible': ~Eval('accounting', False), }), ] @property @account_used('account_expense') def account_expense_used(self): pass @property @account_used('account_revenue') def account_revenue_used(self): pass @property def supplier_taxes_deductible_rate_used(self): if self.taxes_parent: return self.parent.supplier_taxes_deductible_rate_used else: return self.supplier_taxes_deductible_rate class CategoryAccount(ModelSQL, CompanyValueMixin): __name__ = 'product.category.account' category = fields.Many2One( 'product.category', "Category", ondelete='CASCADE', context={ 'company': Eval('company', -1), }, depends={'company'}) account_expense = fields.Many2One( 'account.account', "Account Expense", domain=[ ('type.expense', '=', True), ('company', '=', Eval('company', -1)), ]) account_revenue = fields.Many2One( 'account.account', "Account Revenue", domain=[ ('type.revenue', '=', True), ('company', '=', Eval('company', -1)), ]) class CategoryCustomerTax(ModelSQL): __name__ = 'product.category-customer-account.tax' category = fields.Many2One( 'product.category', "Category", ondelete='CASCADE', required=True) tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT', required=True) @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('tax') @classmethod def __register__(cls, module): # Migration from 7.0: rename to standard name backend.TableHandler.table_rename( 'product_category_customer_taxes_rel', cls._table) super().__register__(module) class CategorySupplierTax(ModelSQL): __name__ = 'product.category-supplier-account.tax' category = fields.Many2One( 'product.category', "Category", ondelete='CASCADE', required=True) tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT', required=True) @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('tax') @classmethod def __register__(cls, module): # Migration from 7.0: rename to standard name backend.TableHandler.table_rename( 'product_category_supplier_taxes_rel', cls._table) super().__register__(module) class Template(CompanyMultiValueMixin, metaclass=PoolMeta): __name__ = 'product.template' account_category = fields.Many2One('product.category', 'Account Category', domain=[ ('accounting', '=', True), ]) @fields.depends('account_category') def get_account(self, name, **pattern): if self.account_category: return self.account_category.get_account(name, **pattern) @fields.depends('account_category') def get_taxes(self, name): if self.account_category: return getattr(self.account_category, name) @property @fields.depends('account_category', methods=['get_account']) @account_used('account_expense', 'account_category') def account_expense_used(self): pass @property @fields.depends('account_category', methods=['get_account']) @account_used('account_revenue', 'account_category') def account_revenue_used(self): pass @property @fields.depends(methods=['get_taxes', 'account_revenue_used']) 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: # Allow empty values on on_change if Transaction().readonly: taxes = [] else: raise TaxError( gettext('account_product.msg_missing_taxes', name=self.rec_name)) return taxes @property @fields.depends(methods=['get_taxes', 'account_expense_used']) 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: # Allow empty values on on_change if Transaction().readonly: taxes = [] else: raise TaxError( gettext('account_product.msg_missing_taxes', name=self.rec_name)) return taxes @property @fields.depends(methods=['get_taxes']) def supplier_taxes_deductible_rate_used(self): return self.get_taxes('supplier_taxes_deductible_rate_used') @classmethod def copy(cls, templates, default=None): default = default.copy() if default else {} if Transaction().check_access: default.setdefault( 'account_category', cls.default_get( ['account_category'], with_rec_name=False).get('account_category')) return super().copy(templates, default=default) class Product(metaclass=PoolMeta): __name__ = 'product.product' account_expense_used = template_property('account_expense_used') account_revenue_used = template_property('account_revenue_used') customer_taxes_used = template_property('customer_taxes_used') supplier_taxes_used = template_property('supplier_taxes_used') supplier_taxes_deductible_rate_used = template_property( 'supplier_taxes_deductible_rate_used') class TemplateAccountCategory(ModelSQL): __name__ = 'product.template-product.category.account' template = fields.Many2One('product.template', 'Template') category = fields.Many2One('product.category', 'Category') @classmethod def table_query(cls): pool = Pool() Template = pool.get('product.template') template = Template.__table__() return template.select( template.id.as_('id'), template.id.as_('template'), template.account_category.as_('category'), where=template.account_category != Null) class TemplateCategoryAll(metaclass=PoolMeta): __name__ = 'product.template-product.category.all' @classmethod def union_models(cls): return super().union_models() + [ 'product.template-product.category.account']