# 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.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=(14, 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') @classmethod def __setup__(cls): super(Category, cls).__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(Category, cls).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(Category, cls).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): "Category Account" __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): 'Category - Customer Tax' __name__ = 'product.category-customer-account.tax' _table = 'product_category_customer_taxes_rel' 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') class CategorySupplierTax(ModelSQL): 'Category - Supplier Tax' __name__ = 'product.category-supplier-account.tax' _table = 'product_category_supplier_taxes_rel' 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') 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): "Template - Account Category" __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.create_uid.as_('create_uid'), template.create_date.as_('create_date'), template.write_uid.as_('write_uid'), template.write_date.as_('write_date'), 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(TemplateCategoryAll, cls).union_models() + [ 'product.template-product.category.account']