class MPTT(DeactivableMixin, tree(), ModelSQL, ModelView): 'Modified Preorder Tree Traversal' __name__ = 'test.mptt' name = fields.Char('Name', required=True) parent = fields.Many2One('test.mptt', "Parent", select=True, left="left", right="right") left = fields.Integer('Left', required=True, select=True) right = fields.Integer('Right', required=True, select=True) childs = fields.One2Many('test.mptt', 'parent', 'Children') @staticmethod def order_sequence(tables): table, _ = tables[None] return [Case((table.sequence == Null, 0), else_=1), table.sequence] @staticmethod def default_left(): return 0 @staticmethod def default_right(): return 0
class Group(tree(separator=' / '), ModelSQL, ModelView): 'Domum Group' __name__ = 'domum.group' company = fields.Many2One('company.company', 'Company', required=True, states={ 'readonly': True, }, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], select=True) name = fields.Char('Name', required=True) description = fields.Char('Description') parent = fields.Many2One('domum.group', 'Parent', select=True, domain=[ ('company', '=', Eval('company')) ], depends=['company']) childs = fields.One2Many('domum.group', 'parent', string='Childs', domain=[ ('company', '=', Eval('company')) ], depends=['company']) units = fields.One2Many('domum.unit', 'group', 'Units') order = fields.Integer('Order') @classmethod def __setup__(cls): super(Group, cls).__setup__() cls._order = [ ('order', 'ASC'), ('name', 'ASC'), ] @staticmethod def default_company(): return Transaction().context.get('company')
class Tree(tree(separator=' / '), ModelSQL): "Tree" __name__ = 'test.tree' name = fields.Char("Name") parent = fields.Many2One('test.tree', "Parent")
class Polytree(tree(parent='parents'), ModelSQL): "PolyTree" __name__ = 'test.polytree' name = fields.Char("Name") parents = fields.Many2Many('test.polytree.edge', 'parent', 'child', "Parents")
class TmiGroup(ActivePeriodMixin, tree(), ModelView, ModelSQL): 'TMI Group' __name__ = 'tmi.group' name = fields.Function(fields.Char('Name'), 'get_name', searcher='search_meta_field') code = fields.Function(fields.Char('Code'), 'get_code', searcher='search_meta_field') #active = fields.Boolean('Active') meta = fields.Many2One('tmi.meta.group', 'Meta', ondelete="RESTRICT", required=True, domain=[ ('company', '=', Eval('company')), ('type','in',['church','small_group']) ], depends=['','company']) type = fields.Function(fields.Selection( [ ('church','Church'), ('small_group','Small Group'), ] ,'Type'), 'get_type', searcher='search_meta_field') parent_type = fields.Function(fields.Char('Parent Type'), 'get_parent_type', searcher='search_meta_field') parent = fields.Function(fields.Many2One('tmi.meta.group', 'Parent', ondelete="RESTRICT", domain=[ #'OR',[ ('company','=',Eval('company',-1)), # ('company', 'in',Eval('company.childs',[])), # ], ('type','=',Eval('parent_type',-1)) ], depends=['parent_type','company']), 'get_parent', searcher='search_meta_field') childs = fields.Function(fields.One2Many('tmi.group', 'parent', 'Children', domain=[ 'OR',[ ('company','=',Eval('company',-1)), ('company', 'in',Eval('company.childs',[])), ], ], depends=['company']), 'get_childs', searcher='search_meta_field') company = fields.Function(fields.Many2One('company.company', 'Company', required=True, ondelete="RESTRICT"), 'get_company', searcher='search_meta_field') currency = fields.Function(fields.Many2One('currency.currency', 'Currency'), 'get_currency') currency_digits = fields.Function(fields.Integer('Currency Digits'), 'get_currency_digits') baptism = fields.Function(fields.Numeric('Baptism', digits=(16,0)), 'get_balance') small_group = fields.Function(fields.Numeric('Small Group', digits=(16,0)), 'get_balance') tithe = fields.Function(fields.Numeric('Tithe', digits=(16,Eval('currency_digits',2))), 'get_balance') offering = fields.Function(fields.Numeric('Offering', digits=(16,Eval('currency_digits',2))), 'get_balance') praise_thanksgiving = fields.Function(fields.Numeric('Praise and Thanksgiving', digits=(16,Eval('currency_digits',2))), 'get_balance') gathering = fields.Function(fields.Numeric('Gathering', digits=(16,Eval('currency_digits',2))), 'get_balance') church_planting = fields.Function(fields.Numeric('Church Planting', digits=(16,0)), 'get_balance') organizing_church = fields.Function(fields.Numeric('Organizing Church', digits=(16,0)), 'get_balance') @classmethod def __setup__(cls): super(TmiGroup, cls).__setup__() t = cls.__table__() cls._order.insert(0, ('meta', 'ASC')) cls._sql_constraints = [ ('meta_uniq', Unique(t, t.meta), 'The meta group must be unique.') ] def get_rec_name(self, name): if self.code: return self.name + ' - ' + self.code + ' - ' + self.meta.parent.name else: return self.name + ' - ' + self.meta.parent.name def get_name(self, name=None): if self.meta: return self.meta.name def get_code(self, name=None): if self.meta: return self.meta.code def get_type(self, name=None): if self.meta: return self.meta.type ''' def get_type(self, value=None): if value=='small_group': return 'Small Group' if value=='church': return 'Church' if value=='district': return 'District' if value=='zone': return 'Zone' if value=='field': return 'Field' if value=='union': return 'Union' if value=='division': return 'Division' if value=='conference': return 'Conference' return None def get_rec_name(self, name): if self.code: return self.get_type(self.type) + ' - ' + self.name + ' - ' + self.code else: return self.get_type(self.type) + ' - ' + self.name ''' def get_parent(self, name=None): if self.meta: if self.meta.parent: return self.meta.parent.id return None def get_childs(self, name=None): if self.meta: pool = Pool() Group = pool.get('tmi.group') MetaGroup = pool.get('tmi.meta.group') meta_groups = MetaGroup.search([('id','=',self.meta.id)]) meta_childs = [] if len(meta_groups)==1: meta_childs = meta_groups[0].childs if meta_childs is not []: childs = [] for meta in meta_childs: groups = Group.search([('meta','=',meta.id)]) if len(groups)==1: childs.append(groups[0].id) return childs return [] else: return [] def get_company(self, name=None): if self.meta: return self.meta.company.id @classmethod def search_meta_field(cls, name, clause): return [('meta.' + clause[0],) + tuple(clause[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, ('code',) + tuple(clause[1:]), ('type',) + tuple(clause[1:]), ('name',) + tuple(clause[1:]), ('meta.parent',) + tuple(clause[1:]), (cls._rec_name,) + tuple(clause[1:]), ] @fields.depends('meta','code','type','parent','childs','company') def on_change_meta(self, name=None): self.name = None self.code = None self.type = None self.parent = None self.parent_type = None self.childs = [] self.company = Transaction().context.get('company') if self.meta: pool = Pool() Group = pool.get('tmi.group') MetaGroup = pool.get('tmi.meta.group') self.name = self.meta.name self.code = self.meta.code self.type = self.meta.type self.parent_type = self.meta.parent_type self.company = self.meta.company groups = Group.search([('meta','=',self.meta.parent.id)]) if len(groups)==1: self.parent = groups[0].id meta_groups = MetaGroup.search([('id','=',self.meta.id)]) meta_childs = [] if len(meta_groups)==1: meta_childs = meta_groups[0].childs if meta_childs is not []: childs = [] for meta in meta_childs: groups = Group.search([('meta','=',meta.id)]) if len(groups)==1: childs.append(groups[0].id) self.childs = childs def get_currency(self, name): return self.company.currency.id def get_currency_digits(self, name): return self.company.currency.digits @fields.depends('company','currency','currency_digits') def on_change_company(self, name=None): if self.company: self.currency = self.company.currency.id self.currency_digits = self.company.currency.digits def get_parent_type(self, name): if self.type=='small_group': return 'church' if self.type=='church': return 'district' if self.type=='district': return 'zone' if self.type=='zone': return 'field' if self.type=='field': return 'union' if self.type=='union': return 'division' if self.type=='division': return 'conference' return None @fields.depends('type','parent_type') def on_change_type(self): if self.type=='small_group': self.parent_type = 'church' if self.type=='church': self.parent_type = 'district' if self.type=='district': self.parent_type = 'zone' if self.type=='zone': self.parent_type = 'field' if self.type=='field': self.parent_type = 'union' if self.type=='union': self.parent_type = 'division' if self.type=='division': self.parent_type = 'conference' return None @staticmethod def default_active(): return True @staticmethod def default_type(): return 'small_group' @staticmethod def default_company(): return Transaction().context.get('company') @classmethod def get_group_baptism(cls, groups, names): ''' Function to compute baptism for TMI Group. ''' pool = Pool() MoveLine = pool.get('tmi.move.line') cursor = Transaction().connection.cursor() result = {} ids = [a.id for a in groups] for name in names: if name not in {'baptism'}: raise ValueError('Unknown name: %s' % name) result[name] = dict((i, Decimal(0)) for i in ids) table = cls.__table__() line = MoveLine.__table__() line_query, fiscalyear_ids = MoveLine.query_get(line) columns = [table.id] for name in names: columns.append(Sum(Coalesce(Column(line, name), 0))) for sub_ids in grouped_slice(ids): red_sql = reduce_ids(table.id, sub_ids) cursor.execute(*table.join(line, 'LEFT', condition=line.group == table.id ).select(*columns, where=red_sql & line_query, group_by=table.id)) for row in cursor.fetchall(): group_id = row[0] for i, name in enumerate(names, 1): # SQLite uses float for SUM if not isinstance(row[i], Decimal): result[name][group_id] = Decimal(str(row[i])) else: result[name][group_id] = row[i] for group in groups: for name in names: exp = Decimal(str(10.0 ** -group.currency_digits)) result[name][group.id] = ( result[name][group.id].quantize(exp)) return result @classmethod def get_balance(cls, groups, names): ''' Function to compute tithe, baptism, church_planting, gathering, small_group, organizing_church, praise_thanksgiving, offering for TMI Group. ''' pool = Pool() MoveLine = pool.get('tmi.move.line') cursor = Transaction().connection.cursor() result = {} ids = [a.id for a in groups] for name in names: if name not in {'baptism','tithe', 'church_planting', \ 'gathering','small_group', 'organizing_church', \ 'praise_thanksgiving', 'offering'}: raise ValueError('Unknown name: %s' % name) result[name] = dict((i, Decimal(0)) for i in ids) table = cls.__table__() line = MoveLine.__table__() #line_query = MoveLine.query_get(line) line_query, fiscalyear_ids = MoveLine.query_get(line) columns = [table.id] for name in names: columns.append(Sum(Coalesce(Column(line, name), 0))) for sub_ids in grouped_slice(ids): red_sql = reduce_ids(table.id, sub_ids) cursor.execute(*table.join(line, 'LEFT', condition=line.group == table.id ).select(*columns, where=red_sql & line_query, group_by=table.id)) for row in cursor.fetchall(): group_id = row[0] for i, name in enumerate(names, 1): # SQLite uses float for SUM if not isinstance(row[i], Decimal): result[name][group_id] = Decimal(str(row[i])) else: result[name][group_id] = row[i] for group in groups: for name in names: exp = Decimal(str(10.0 ** -group.currency_digits)) result[name][group.id] = ( result[name][group.id].quantize(exp)) return result
class Package(tree(), ModelSQL, ModelView): 'Stock Package' __name__ = 'stock.package' _rec_name = 'code' code = fields.Char('Code', select=True, readonly=True, required=True) type = fields.Many2One('stock.package.type', 'Type', required=True) shipment = fields.Reference('Shipment', selection='get_shipment', select=True) moves = fields.One2Many('stock.move', 'package', 'Moves', domain=[ ('shipment', '=', Eval('shipment')), ('to_location.type', 'in', ['customer', 'supplier']), ('state', '!=', 'cancel'), ], add_remove=[ ('package', '=', None), ], depends=['shipment']) parent = fields.Many2One('stock.package', 'Parent', select=True, ondelete='CASCADE', domain=[('shipment', '=', Eval('shipment'))], depends=['shipment']) children = fields.One2Many('stock.package', 'parent', 'Children', domain=[('shipment', '=', Eval('shipment'))], depends=['shipment']) @staticmethod def _get_shipment(): 'Return list of Model names for shipment Reference' return [ 'stock.shipment.out', 'stock.shipment.in.return', ] @classmethod def get_shipment(cls): pool = Pool() Model = pool.get('ir.model') models = cls._get_shipment() models = Model.search([ ('model', 'in', models), ]) return [(None, '')] + [(m.model, m.name) for m in models] @classmethod def create(cls, vlist): pool = Pool() Sequence = pool.get('ir.sequence') Config = pool.get('stock.configuration') vlist = [v.copy() for v in vlist] config = Config(1) for values in vlist: values['code'] = Sequence.get_id(config.package_sequence) return super(Package, cls).create(vlist)
class Group(DeactivableMixin, tree(), ModelSQL, ModelView): "Group" __name__ = "res.group" name = fields.Char('Name', required=True, select=True, translate=True) users = fields.Many2Many('res.user-res.group', 'group', 'user', 'Users') parent = fields.Many2One( 'res.group', "Parent", help="The group to inherit accesses from.") model_access = fields.One2Many('ir.model.access', 'group', 'Access Model') field_access = fields.One2Many('ir.model.field.access', 'group', 'Access Field') buttons = fields.Many2Many( 'ir.model.button-res.group', 'group', 'button', "Buttons") rule_groups = fields.Many2Many('ir.rule.group-res.group', 'group', 'rule_group', 'Rules', domain=[('global_p', '!=', True), ('default_p', '!=', True)]) menu_access = MenuMany2Many('ir.ui.menu-res.group', 'group', 'menu', 'Access Menu') @classmethod def __setup__(cls): super(Group, cls).__setup__() table = cls.__table__() cls._sql_constraints += [ ('name_uniq', Unique(table, table.name), 'The name of the group must be unique!') ] cls._order.insert(0, ('name', 'ASC')) @classmethod def write(cls, *args): super().write(*args) pool = Pool() # Restart the cache on the domain_get method pool.get('ir.rule')._domain_get_cache.clear() # Restart the cache for get_groups pool.get('res.user')._get_groups_cache.clear() # Restart the cache for model access and view pool.get('ir.model.access')._get_access_cache.clear() pool.get('ir.model.field.access')._get_access_cache.clear() ModelView._fields_view_get_cache.clear() @classmethod def copy(cls, groups, default=None): if default is None: default = {} default = default.copy() new_groups = [] for group in groups: i = 1 while True: name = '%s (%d)' % (group.name, i) if not cls.search([('name', '=', name)], order=[]): break i += 1 default['name'] = name new_groups.extend(super(Group, cls).copy([group], default=default)) return new_groups @classmethod def group_parent_all_cte(cls): group = cls.__table__() parents = With('id', 'parent', recursive=True) parents.query = group.select(group.id, group.parent) parents.query |= group.select(group.id, group.id) parents.query |= (group .join(parents, condition=group.parent == parents.id) .select(group.id, parents.parent)) return parents
class Todo(Workflow, ModelSQL, ModelView, sequence_ordered(), tree(separator=' / ')): 'TODO task' __name__ = 'todo.todo' _states = { 'readonly': Equal(Eval('state'), 'done'), } _depends = ['state'] name = fields.Char('Name', required=True, states=_states, depends=_depends) date = fields.Function(fields.DateTime('Date'), 'get_date') limit_date = fields.DateTime('Limit Date', states=_states, depends=_depends) limit_state = fields.Function(fields.Integer('Limit state'), 'get_limit_state') finish_date = fields.DateTime('Finish Date', states={ 'readonly': Equal(Eval('state'), 'done'), 'invisible': Equal(Eval('state'), 'open') }, depends=_depends) user = fields.Function(fields.Many2One('res.user', 'User'), 'get_user') parent = fields.Many2One('todo.todo', 'Parent', select=True, domain=[('create_uid', '=', Eval('create_uid'))], states=_states, depends=_depends + ['create_uid']) childs = fields.One2Many('todo.todo', 'parent', string='Childs', states=_states, depends=_depends) childs_open = fields.One2Many('todo.todo', 'parent', string='Childs Open', states=_states, depends=_depends, filter=[('state', '=', 'open')]) description = fields.Text('Description', states=_states, depends=_depends) state = fields.Selection([ ('open', 'Open'), ('done', 'Done'), ], 'State', readonly=True, required=True) del _states, _depends @classmethod def __setup__(cls): super(Todo, cls).__setup__() cls._order = [ ('sequence', 'ASC'), ('create_date', 'DESC'), ('id', 'DESC'), ] cls._transitions |= set(( ('open', 'done'), ('done', 'open'), )) cls._buttons.update({ 'done': { 'invisible': In(Eval('state'), ['done']), }, 'open': { 'invisible': In(Eval('state'), ['open']), }, }) @classmethod def view_attributes(cls): return [ ('/tree/field[@name="name"]', 'visual', If(And(Eval('limit_state', 0) > 0, Eval('state', '') == 'open'), If(Eval('limit_state', 0) > 1, 'danger', 'warning'), '')), ] @staticmethod def default_state(): return 'open' def get_limit_state(self, name): pool = Pool() Company = pool.get('company.company') timezone_str = None res = 0 if self.limit_date: company_id = Transaction().context.get('company') if company_id: timezone_str = Company(company_id).timezone date = self.limit_date.astimezone(timezone(timezone_str)) curr_date = datetime.datetime.now(timezone(timezone_str)) date = date.date() curr_date = curr_date.date() if date == curr_date: res = 1 # Warning elif date < curr_date: res = 2 # Danger return res def get_date(self, name): return self.create_date.replace(microsecond=0) def get_user(self, name): return self.create_uid.id @classmethod def search_date(cls, name, clause): return [('create_date', ) + tuple(clause[1:])] @classmethod @ModelView.button @Workflow.transition('open') def open(cls, todos): pass @classmethod @ModelView.button @Workflow.transition('done') def done(cls, todos): finish_date = datetime.datetime.now() to_done = cls._set_done(todos, finish_date, False) cls.save(to_done) @classmethod def _set_done(cls, todos, finish_date, done_childs): pool = Pool() Warning = pool.get('res.user.warning') to_done = [] for todo in todos: todo.state = 'done' todo.finish_date = finish_date to_done.append(todo) if todo.childs: if not done_childs: msg_id = 'todo_done_childs_' + str(todo.id) if Warning.check(msg_id): raise UserWarning( msg_id, gettext('todo.msg_todo_done_childs', todo=todo.rec_name)) to_done += cls._set_done(todo.childs, finish_date, True) return to_done
class Package(tree(), ModelSQL, ModelView): 'Stock Package' __name__ = 'stock.package' _rec_name = 'code' code = fields.Char('Code', select=True, readonly=True, required=True) type = fields.Many2One('stock.package.type', "Type", required=True, states={ 'readonly': Eval('state') == 'closed', }, depends=['state']) shipment = fields.Reference("Shipment", selection='get_shipment', select=True, states={ 'readonly': Eval('state') == 'closed', }, depends=['state']) moves = fields.One2Many('stock.move', 'package', 'Moves', domain=[ ('shipment', '=', Eval('shipment')), ('to_location.type', 'in', ['customer', 'supplier']), ('state', '!=', 'cancel'), ], add_remove=[ ('package', '=', None), ], states={ 'readonly': Eval('state') == 'closed', }, depends=['shipment', 'state']) parent = fields.Many2One('stock.package', "Parent", select=True, ondelete='CASCADE', domain=[ ('shipment', '=', Eval('shipment')), ], states={ 'readonly': Eval('state') == 'closed', }, depends=['shipment', 'state']) children = fields.One2Many('stock.package', 'parent', 'Children', domain=[ ('shipment', '=', Eval('shipment')), ], states={ 'readonly': Eval('state') == 'closed', }, depends=['shipment', 'state']) state = fields.Function( fields.Selection([ ('open', "Open"), ('closed', "Closed"), ], "State"), 'on_change_with_state') @staticmethod def _get_shipment(): 'Return list of Model names for shipment Reference' return [ 'stock.shipment.out', 'stock.shipment.in.return', ] @classmethod def get_shipment(cls): pool = Pool() Model = pool.get('ir.model') models = cls._get_shipment() models = Model.search([ ('model', 'in', models), ]) return [(None, '')] + [(m.model, m.name) for m in models] @fields.depends('shipment') def on_change_with_state(self, name=None): if (self.shipment and self.shipment.state in {'packed', 'done', 'cancel'}): return 'closed' return 'open' @classmethod def create(cls, vlist): pool = Pool() Sequence = pool.get('ir.sequence') Config = pool.get('stock.configuration') vlist = [v.copy() for v in vlist] config = Config(1) for values in vlist: values['code'] = Sequence.get_id(config.package_sequence) return super(Package, cls).create(vlist)
class WorkCenter(DeactivableMixin, tree(separator=' / '), ModelSQL, ModelView): 'Work Center' __name__ = 'production.work.center' name = fields.Char('Name', required=True, translate=True) parent = fields.Many2One('production.work.center', 'Parent', select=True, domain=[ ('company', '=', Eval('company', -1)), ('warehouse', '=', Eval('warehouse', -1)), ], depends=['company', 'warehouse']) children = fields.One2Many('production.work.center', 'parent', 'Children', domain=[ ('company', '=', Eval('company', -1)), ('warehouse', '=', Eval('warehouse', -1)), ], depends=['company', 'warehouse']) category = fields.Many2One('production.work.center.category', 'Category') cost_price = fields.Numeric('Cost Price', digits=price_digits, states={ 'required': Bool(Eval('cost_method')), }, depends=['cost_method']) cost_method = fields.Selection([ ('', ''), ('cycle', 'Per Cycle'), ('hour', 'Per Hour'), ], 'Cost Method', states={ 'required': Bool(Eval('cost_price')), }, depends=['cost_price']) company = fields.Many2One('company.company', 'Company', required=True, select=True) warehouse = fields.Many2One('stock.location', 'Warehouse', required=True, domain=[ ('type', '=', 'warehouse'), ]) @classmethod def __setup__(cls): super(WorkCenter, cls).__setup__() cls._order.insert(0, ('name', 'ASC')) @classmethod def default_company(cls): return Transaction().context.get('company') @classmethod def default_warehouse(cls): Location = Pool().get('stock.location') return Location.get_default_warehouse() @classmethod def get_picker(cls): """Return a method that picks a work center for the category and the parent""" cache = {} def picker(parent, category): key = (parent, category) if key not in cache: work_centers = cls.search([ ('parent', 'child_of', [parent.id]), ('category', '=', category.id), ]) if not work_centers: raise PickerError( gettext('production_work.msg_missing_work_center', category=category.rec_name, parent=parent.rec_name)) cache[key] = work_centers return random.choice(cache[key]) return picker
class TreeWildcard(tree(separator='\\'), ModelSQL): "Tree separator wildcard" __name__ = 'test.tree_wildcard' name = fields.Char("Name") parent = fields.Many2One('test.tree_wildcard', "Parent")
class BudgetMixin(BalanceMixin, tree(), ModelSQL, ModelView): name = fields.Char("Name", required=True, help="The main identifier of this budget.") company = fields.Many2One('company.company', "Company", required=True, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], select=True, help="Make the budget belong to the company.") left = fields.Integer("Left", required=True, select=True) right = fields.Integer("Right", required=True, select=True) @classmethod def _childs_domain(cls): return [('company', '=', Eval('company'))] @classmethod def _childs_depends(cls): return ['company'] @classmethod def __setup__(cls): if not hasattr(cls, 'parent'): domain = cls._childs_domain() depends = cls._childs_depends() cls.parent = fields.Many2One(cls.__name__, "Parent", select=True, help="Add the budget below the parent.", left='left', right='right', ondelete='RESTRICT', domain=domain, depends=depends) cls.children = fields.One2Many(cls.__name__, 'parent', "Children", help="Add children below the budget.", domain=domain, depends=depends) super(BudgetMixin, cls).__setup__() cls._buttons.update({ 'copy_budget': { }, }) cls._error_messages.update({ 'invalid_children_amount': ( 'The children amount "%(children_amount)s" of budget ' '"%(budget)s" can not be higher than its own amount ' '"%(amount)s".' ), }) @classmethod def default_company(cls): return Transaction().context.get('company') @classmethod def default_left(cls): return 0 @classmethod def default_right(cls): return 0 @fields.depends('company') def on_change_with_currency(self, name=None): if self.company: return self.company.currency.id def get_rec_name(self, name): return self.name ''' def get_rec_name(self, name): if self.parent: return self.parent.get_rec_name(name) + '\\' + self.name else: return self.name ''' ''' @classmethod def search_rec_name(cls, name, clause): if isinstance(clause[2], basestring): values = clause[2].split('\\') values.reverse() domain = [] field = 'name' for name in values: domain.append((field, clause[1], name.strip())) field = 'parent.' + field else: domain = [('name',) + tuple(clause[1:])] ids = [w.id for w in cls.search(domain, order=[])] return [('parent', 'child_of', ids)] ''' @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, ('name',) + tuple(clause[1:]), (cls._rec_name,) + tuple(clause[1:]), ] @classmethod def copy_budget(cls, budgets): raise NotImplementedError @classmethod def copy(cls, records, default=None): if default is None: default = {} default.setdefault('left', 0) default.setdefault('right', 0) return super(BudgetMixin, cls).copy(records, default=default) @classmethod def validate(cls, records): super(BudgetMixin, cls).validate(records) for record in records: record.check_amounts() def check_amounts(self): children_amount = sum((c.amount for c in self.children), Decimal(0)) if abs(children_amount) > abs(self.amount): self.raise_user_error('invalid_children_amount', { 'children_amount': children_amount, 'amount': self.amount, 'budget': self.rec_name, })
class ClassificationTreeMixin(tree(separator=' / '), ClassificationMixin): __slots__ = () parent = fields.Many2One(name, 'Parent', select=True) childs = fields.One2Many(name, 'parent', 'Children')
class Account(DeactivableMixin, ModelSQL, ModelView, tree(separator='\\')): 'Analytic Account' __name__ = 'analytic_account.account' template = fields.Many2One('analytic_account.account.template', 'Template') is_consolidated = fields.Boolean('Consolidated Indicator') is_current_capital = fields.Boolean('Current Capital') is_current_asset = fields.Boolean('Current Asset') is_recommended_capital = fields.Boolean('Recommended Capital') is_cash = fields.Boolean('Cash and Banks') is_current_liability = fields.Boolean('Current Liability') is_revenue = fields.Boolean('Revenue') is_expense = fields.Boolean('Expense') financial_indicator = fields.Function( fields.Numeric('Financial Indicator', digits=(16, Eval('currency_digits', 2))), 'get_financial_indicator') custom_balance = fields.Function( fields.Numeric('Custom Balance', digits=(16, Eval('currency_digits', 1)), depends=['currency_digits']), 'get_custom_balance') @classmethod def __setup__(cls): super(Account, cls).__setup__() cls.name.translate = False def get_current_capital(self): pool = Pool() Date = pool.get('ir.date') AccountType = pool.get('account.account.type') today = Date.today() company = Transaction().context.get('company') current_capital = current_liability = Decimal('0.0') current_capitals = AccountType.search([('company', '=', company), ('name', '=', '1) ACTIVOS CORRIENTES')]) if len(current_capitals) == 1: current_capital = current_capitals[0].amount * Decimal('1.0') current_liabilities = AccountType.search([('company', '=', company), ('name', '=', '3) PASIVOS CORRIENTES')]) if len(current_liabilities) == 1: current_liability = current_liabilities[0].amount * Decimal('1.0') balance = (current_capital - current_liability) * Decimal('1.0') return balance def get_cash(self): pool = Pool() Date = pool.get('ir.date') AccountType = pool.get('account.account.type') today = Date.today() company = Transaction().context.get('company') balance = Decimal('0.0') transaction = Transaction() context = Transaction().context total_cash = Decimal('0.0') if self.is_consolidated: companies = context.get('companies', []) for company in context.get('companies', []): with transaction.set_context(company=company['id']): cash = Decimal('0.0') accounts = AccountType.search([ ('company', '=', company['id']), ('name', '=', '10. Efectivo y Equivalencias de Efectivo') ]) if len(accounts) == 1: cash = accounts[0].amount * Decimal('1.0') total_cash += cash return total_cash else: accounts = AccountType.search([ ('company', '=', company), ('name', '=', '10. Efectivo y Equivalencias de Efectivo') ]) if len(accounts) == 1: balance = accounts[0].amount * Decimal('1.0') return balance def get_current_asset(self): pool = Pool() Date = pool.get('ir.date') AccountType = pool.get('account.account.type') today = Date.today() company = Transaction().context.get('company') transaction = Transaction() context = Transaction().context total_current_asset = current_asset = Decimal('0.0') today = Date.today() company = Transaction().context.get('company') to_date = Transaction().context.get('to_date') if self.is_consolidated: companies = context.get('companies', []) date = today if to_date is None else to_date for company in context.get('companies', []): with transaction.set_context(company=company['id'], posted=True, cumulate=True, date=date, to_date=date, from_date=None): current_asset = Decimal('0.0') current_assets = AccountType.search([ ('company', '=', company['id']), ('name', '=', '1) ACTIVOS CORRIENTES') ]) if len(current_assets) == 1: current_asset = current_assets[0].amount * Decimal( '1.0') total_current_asset += current_asset return total_current_asset else: date = today if to_date is None else to_date with transaction.set_context( posted=True, cumulate=True, date=date, to_date=date, from_date=None, ): current_assets = AccountType.search([ ('company', '=', company), ('name', '=', '1) ACTIVOS CORRIENTES') ]) if len(current_assets) == 1: current_asset = current_assets[0].amount * Decimal('1.0') return current_asset def get_current_liability(self): pool = Pool() Date = pool.get('ir.date') AccountType = pool.get('account.account.type') today = Date.today() liability = Decimal('0.0') transaction = Transaction() context = Transaction().context total_liability = liability = Decimal('0.0') company = Transaction().context.get('company') to_date = Transaction().context.get('to_date') if self.is_consolidated: companies = context.get('companies', []) date = today if to_date is None else to_date for company in context.get('companies', []): with transaction.set_context( company=company['id'], posted=True, cumulate=True, date=date, to_date=date, from_date=None, ): liability = Decimal('0.0') liabilities = AccountType.search([ ('company', '=', company['id']), ('name', '=', '3) PASIVOS CORRIENTES') ]) if len(liabilities) == 1: liability = liabilities[0].amount * Decimal('1.0') total_liability += liability return total_liability else: current_liability = Decimal('0.0') date = today if to_date is None else to_date with transaction.set_context( posted=True, cumulate=True, date=date, to_date=date, from_date=None, ): current_liabilities = AccountType.search([ ('company', '=', company), ('name', '=', '3) PASIVOS CORRIENTES') ]) if len(current_liabilities) == 1: current_liability = current_liabilities[ 0].amount * Decimal('1.0') return current_liability def get_revenues(self): pool = Pool() Date = pool.get('ir.date') AccountType = pool.get('account.account.type') today = Date.today() revenue = Decimal('0.0') transaction = Transaction() context = Transaction().context total_revenue = revenue = Decimal('0.0') if self.is_consolidated: companies = context.get('companies', []) for company in context.get('companies', []): with transaction.set_context(company=company['id']): revenue = Decimal('0.0') revenues = AccountType.search([ ('company', '=', company['id']), ('name', '=', 'INGRESOS FINANCIEROS') ]) if len(revenues) == 1: revenue = revenues[0].amount * Decimal('1.0') total_revenue += revenue return total_revenue else: revenue = Decimal('0.0') company = Transaction().context.get('company') revenues = AccountType.search([('company', '=', company), ('name', '=', 'INGRESOS FINANCIEROS')]) if len(revenues) == 1: revenue = revenues[0].amount * Decimal('1.0') return revenue def get_expenses(self): pool = Pool() Date = pool.get('ir.date') AccountType = pool.get('account.account.type') today = Date.today() transaction = Transaction() context = Transaction().context total_expense = expense = Decimal('0.0') if self.is_consolidated: companies = context.get('companies', []) for company in context.get('companies', []): with transaction.set_context(company=company['id']): expense = Decimal('0.0') expenses = AccountType.search([ ('company', '=', company['id']), ('name', '=', 'GASTOS FINANCIEROS') ]) if len(expenses) == 1: expense = expenses[0].amount * Decimal('1.0') total_expense += expense return total_expense else: company = Transaction().context.get('company') expense = Decimal('0.0') expenses = AccountType.search([('company', '=', company), ('name', '=', 'GASTOS FINANCIEROS') ]) if len(expenses) == 1: expense = expenses[0].amount * Decimal('1.0') return expense def get_recommended_capital(self): pool = Pool() Date = pool.get('ir.date') Fiscalyear = pool.get('account.fiscalyear') Budget = pool.get('account.budget') today = Date.today() transaction = Transaction() context = Transaction().context company = context.get('company') balance = Decimal('0.0') if self.is_consolidated: companies = context.get('companies', []) for company in context.get('companies', []): total_amount = Decimal('0.0') with transaction.set_context(company=company['id']): fiscalyears = Fiscalyear.search([ ('company', '=', company['id']), ('start_date', '<=', today), ('end_date', '>=', today) ]) fiscalyear = None if len(fiscalyears) == 1: fiscalyear = fiscalyears[0].id budgets = Budget.search([('fiscalyear', '=', fiscalyear), ('company', '=', company['id']), ('parent', '=', None)]) if len(budgets) == 1: budget = Budget(budgets[0].id) balance += budget.children[1].amount * Decimal('0.15') #balance *= -1 else: fiscalyear = Transaction().context.get('fiscalyear') if fiscalyear is not None: fiscalyears = Fiscalyear.search([('company', '=', company), ('id', '=', fiscalyear)]) else: fiscalyears = Fiscalyear.search([('company', '=', company), ('start_date', '<=', today), ('end_date', '>=', today)]) if len(fiscalyears) == 1: fiscalyear = fiscalyears[0].id budgets = Budget.search([('fiscalyear', '=', fiscalyear), ('company', '=', company), ('parent', '=', None)]) if len(budgets) == 1: budget = Budget(budgets[0].id) print("BUDGET: ", str(budget)) balance = budget.children[0].amount * Decimal('0.15') print("BALANCE: ", str(balance)) #balance *= -1 return balance def get_difference_between_childs(self): balance = first_child = second_child = 0 if self.childs[0] is not None and self.childs[1] is not None: first_child = self.childs[0].custom_balance second_child = self.childs[1].custom_balance balance = first_child - second_child return balance @classmethod def get_custom_balance(cls, accounts, name): balances = {} for account in accounts: balance = Decimal() if account.is_current_capital == True: balance = account.get_difference_between_childs() elif account.is_recommended_capital == True: balance = account.get_recommended_capital() elif account.is_cash == True: balance = account.get_cash() elif account.is_current_liability == True: balance = account.get_current_liability() elif account.is_current_asset == True: balance = account.get_current_asset() elif account.is_revenue == True: balance = account.get_revenues() elif account.is_expense == True: balance = account.get_expenses() elif account.type == 'root': balance = account.get_difference_between_childs() if account.display_balance == 'credit-debit' and balance: balance *= -1 exp = Decimal(str(10.0**-account.currency_digits)) balances[account.id] = balance.quantize(exp) return balances @classmethod def get_credit_debit(cls, accounts, names): pool = Pool() Line = pool.get('analytic_account.line') MoveLine = pool.get('account.move.line') cursor = Transaction().connection.cursor() table = cls.__table__() line = Line.__table__() move_line = MoveLine.__table__() result = {} ids = [a.id for a in accounts] for name in names: if name not in ('credit', 'debit'): raise Exception('Bad argument') result[name] = {}.fromkeys(ids, Decimal('0.0')) id2account = {} for account in accounts: id2account[account.id] = account line_query = Line.query_get(line) columns = [table.id] for name in names: columns.append(Sum(Coalesce(Column(line, name), 0))) cursor.execute( *table.join(line, 'LEFT', condition=table.id == line.account).join( move_line, 'LEFT', condition=move_line.id == line.move_line).select(*columns, where=(table.type != 'view') & table.id.in_(ids) & (table.active == True) & line_query, group_by=table.id)) for row in cursor.fetchall(): account_id = row[0] for i, name in enumerate(names, 1): value = row[i] # SQLite uses float for SUM if not isinstance(value, Decimal): value = Decimal(str(value)) result[name][account_id] += value for account in accounts: for name in names: exp = Decimal(str(10.0**-account.currency_digits)) result[name][account.id] = ( result[name][account.id].quantize(exp)) return result def get_financial_indicator(self, name): if self.type == 'root': first_child = second_child = quotient = 0 if self.childs is not None: first_child = Decimal(str(self.childs[0].custom_balance)) second_child = Decimal(str(self.childs[1].custom_balance)) if second_child != 0: quotient = first_child / second_child * 100 return quotient credit = self.credit if self.credit else 0 debit = self.debit if self.debit else 0 if debit is not 0: return credit / debit return 0 @staticmethod def default_is_consolidated(): return False def update_analytic_account(self, template2account=None): ''' Update recursively types based on template. template2type is a dictionary with template id as key and type id as value, used to convert template id into type. The dictionary is filled with new types ''' if template2account is None: template2account = {} values = [] childs = [self] while childs: for child in childs: if child.template: vals = child.template._get_account_value() if vals: values.append([child]) values.append(vals) template2account[child.template.id] = child.id childs = sum((c.childs for c in childs), ()) if values: self.write(*values)
class Location(DeactivableMixin, tree(), ModelSQL, ModelView): "Stock Location" __name__ = 'stock.location' _default_warehouse_cache = Cache('stock.location.default_warehouse', context=False) name = fields.Char("Name", size=None, required=True, translate=True) code = fields.Char("Code", size=None, select=True, help="The internal identifier used for the location.") address = fields.Many2One('party.address', "Address", states={ 'invisible': Eval('type') != 'warehouse', }) type = fields.Selection([ ('supplier', 'Supplier'), ('customer', 'Customer'), ('lost_found', 'Lost and Found'), ('warehouse', 'Warehouse'), ('storage', 'Storage'), ('production', 'Production'), ('drop', 'Drop'), ('view', 'View'), ], "Location type") type_string = type.translated('type') parent = fields.Many2One("stock.location", "Parent", select=True, left="left", right="right", help="Used to add structure above the location.") left = fields.Integer('Left', required=True, select=True) right = fields.Integer('Right', required=True, select=True) childs = fields.One2Many("stock.location", "parent", "Children", help="Used to add structure below the location.") flat_childs = fields.Boolean( "Flat Children", help="Check to enforce a single level of children with no " "grandchildren.") warehouse = fields.Function(fields.Many2One('stock.location', 'Warehouse'), 'get_warehouse') input_location = fields.Many2One("stock.location", "Input", states={ 'invisible': Eval('type') != 'warehouse', 'required': Eval('type') == 'warehouse', }, domain=[ ('type', '=', 'storage'), [ 'OR', ('parent', 'child_of', [Eval('id', -1)]), ('parent', '=', None), ], ], help="Where incoming stock is received.") output_location = fields.Many2One( "stock.location", "Output", states={ 'invisible': Eval('type') != 'warehouse', 'required': Eval('type') == 'warehouse', }, domain=[('type', '=', 'storage'), [ 'OR', ('parent', 'child_of', [Eval('id', -1)]), ('parent', '=', None) ]], help="Where outgoing stock is sent from.") storage_location = fields.Many2One( "stock.location", "Storage", states={ 'invisible': Eval('type') != 'warehouse', 'required': Eval('type') == 'warehouse', }, domain=[('type', 'in', ['storage', 'view']), [ 'OR', ('parent', 'child_of', [Eval('id', -1)]), ('parent', '=', None) ]], help="The top level location where stock is stored.") picking_location = fields.Many2One( 'stock.location', 'Picking', states={ 'invisible': Eval('type') != 'warehouse', }, domain=[ ('type', '=', 'storage'), ('parent', 'child_of', [Eval('storage_location', -1)]), ], help="Where stock is picked from.\n" "Leave empty to use the storage location.") lost_found_location = fields.Many2One( 'stock.location', "Lost and Found", states={ 'invisible': Eval('type') != 'warehouse', 'readonly': ~Eval('active'), }, domain=[ ('type', '=', 'lost_found'), ], help="Used, by inventories, when correcting stock levels " "in the warehouse.") waste_locations = fields.Many2Many( 'stock.location.waste', 'warehouse', 'location', "Waste Locations", states={ 'invisible': Eval('type') != 'warehouse', }, domain=[ ('type', '=', 'lost_found'), ], help="The locations used for waste products from the warehouse.") waste_warehouses = fields.Many2Many( 'stock.location.waste', 'location', 'warehouse', "Waste Warehouses", states={ 'invisible': Eval('type') != 'lost_found', }, domain=[ ('type', '=', 'warehouse'), ], help="The warehouses that use the location for waste products.") quantity = fields.Function(fields.Float( "Quantity", digits=(16, Eval('quantity_uom_digits', 2)), help="The amount of stock in the location."), 'get_quantity', searcher='search_quantity') forecast_quantity = fields.Function(fields.Float( "Forecast Quantity", digits=(16, Eval('quantity_uom_digits', 2)), help="The amount of stock expected to be in the location."), 'get_quantity', searcher='search_quantity') quantity_uom = fields.Function( fields.Many2One('product.uom', "Quantity UOM"), 'get_quantity_uom') quantity_uom_digits = fields.Function( fields.Integer("Quantity UOM Digits"), 'get_quantity_uom') cost_value = fields.Function( fields.Numeric("Cost Value", digits=price_digits, help="The value of the stock in the location."), 'get_cost_value') @classmethod def __setup__(cls): super(Location, cls).__setup__() cls._order.insert(0, ('name', 'ASC')) parent_domain = [[ 'OR', ('parent.flat_childs', '=', False), ('parent', '=', None), ]] childs_domain = [ If(Eval('flat_childs', False), ('childs', '=', None), ()), ] childs_mapping = cls._childs_domain() for type_, allowed_parents in cls._parent_domain().items(): parent_domain.append( If(Eval('type') == type_, ('type', 'in', allowed_parents), ())) childs_domain.append( If( Eval('type') == type_, ('type', 'in', childs_mapping[type_]), ())) cls.parent.domain = parent_domain cls.childs.domain = childs_domain @classmethod def _parent_domain(cls): '''Returns a dict with location types as keys and a list of allowed parent location types as values''' return { 'customer': ['customer'], 'supplier': ['supplier'], 'production': ['production'], 'lost_found': ['lost_found'], 'view': ['warehouse', 'view', 'storage'], 'storage': ['warehouse', 'view', 'storage'], 'warehouse': ['view'], } @classmethod def _childs_domain(cls): childs_domain = {} for type_, allowed_parents in cls._parent_domain().items(): for parent in allowed_parents: childs_domain.setdefault(parent, []) childs_domain[parent].append(type_) return childs_domain @classmethod def __register__(cls, module_name): super(Location, cls).__register__(module_name) table = cls.__table_handler__(module_name) table.index_action(['left', 'right'], 'add') @classmethod def validate_fields(cls, locations, field_names): super().validate_fields(locations, field_names) inactives = [] for location in locations: location.check_type_for_moves(field_names) if 'active' in field_names and not location.active: inactives.append(location) cls.check_inactive(inactives) def check_type_for_moves(self, field_names=None): """ Check locations with moves have types compatible with moves. """ pool = Pool() Move = pool.get('stock.move') if field_names and 'type' not in field_names: return invalid_move_types = ['warehouse', 'view'] if self.type in invalid_move_types: # Use root to compute for all companies with Transaction().set_user(0): moves = Move.search([ [ 'OR', ('to_location', '=', self.id), ('from_location', '=', self.id), ], ('state', 'not in', ['staging', 'draft']), ], order=[], limit=1) if moves: raise LocationValidationError( gettext('stock.msg_location_invalid_type_for_moves', location=self.rec_name, type=self.type_string)) @classmethod def check_inactive(cls, locations): "Check inactive location are empty" assert all(not l.active for l in locations) empty = cls.get_empty_locations(locations) non_empty = set(locations) - set(empty) if non_empty: raise LocationValidationError( gettext('stock.msg_location_inactive_not_empty', location=next(iter(non_empty)).rec_name)) @classmethod def get_empty_locations(cls, locations=None): pool = Pool() Move = pool.get('stock.move') if locations is None: locations = cls.search([]) if not locations: return [] location_ids = list(map(int, locations)) # Use root to compute for all companies # and ensures inactive locations are in the query with Transaction().set_user(0), \ Transaction().set_context(active_test=False): query = Move.compute_quantities_query(location_ids, with_childs=True) quantities = Move.compute_quantities(query, location_ids, with_childs=True) empty = set(location_ids) for (location_id, product), quantity in quantities.items(): if quantity: empty.discard(location_id) for sub_ids in grouped_slice(list(empty)): sub_ids = list(sub_ids) moves = Move.search([ ('state', 'not in', ['done', 'cancelled']), [ 'OR', ('from_location', 'in', sub_ids), ('to_location', 'in', sub_ids), ], ]) for move in moves: for location in [move.from_location, move.to_location]: empty.discard(location.id) return cls.browse(empty) @staticmethod def default_left(): return 0 @staticmethod def default_right(): return 0 @classmethod def default_flat_childs(cls): return False @staticmethod def default_type(): return 'storage' @classmethod def check_xml_record(cls, records, values): return True def get_warehouse(self, name): # Order by descending left to get the first one in the tree with Transaction().set_context(active_test=False): locations = self.search([ ('parent', 'parent_of', [self.id]), ('type', '=', 'warehouse'), ], order=[('left', 'DESC')]) if locations: return locations[0].id @classmethod def get_default_warehouse(cls): warehouse = Transaction().context.get('warehouse') if warehouse: return warehouse warehouse = cls._default_warehouse_cache.get(None, -1) if warehouse == -1: warehouses = cls.search([ ('type', '=', 'warehouse'), ], limit=2) if len(warehouses) == 1: warehouse = warehouses[0].id else: warehouse = None cls._default_warehouse_cache.set(None, warehouse) return warehouse @property def lost_found_used(self): if self.warehouse: return self.warehouse.lost_found_location @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:]), ('code', ) + tuple(clause[1:]), ] @classmethod def _get_quantity_grouping(cls): context = Transaction().context grouping, grouping_filter, key = (), (), [] if context.get('product') is not None: grouping = ('product', ) grouping_filter = ([context['product']], ) key = (context['product'], ) elif context.get('product_template') is not None: grouping = ('product.template', ) grouping_filter = ([context['product_template']], ) key = (context['product_template'], ) return grouping, grouping_filter, key @classmethod def get_quantity(cls, locations, name): pool = Pool() Product = pool.get('product.product') Date_ = pool.get('ir.date') trans_context = Transaction().context def valid_context(name): return (trans_context.get(name) is not None and isinstance(trans_context[name], int)) context = {} if (name == 'quantity' and (trans_context.get( 'stock_date_end', datetime.date.max) > Date_.today())): context['stock_date_end'] = Date_.today() if name == 'forecast_quantity': context['forecast'] = True if not trans_context.get('stock_date_end'): context['stock_date_end'] = datetime.date.max grouping, grouping_filter, key = cls._get_quantity_grouping() if not grouping: return {loc.id: None for loc in locations} pbl = {} for sub_locations in grouped_slice(locations): location_ids = [l.id for l in sub_locations] with Transaction().set_context(context): pbl.update( Product.products_by_location( location_ids, grouping=grouping, grouping_filter=grouping_filter, with_childs=trans_context.get('with_childs', True))) return dict( (loc.id, pbl.get((loc.id, ) + key, 0)) for loc in locations) @classmethod def search_quantity(cls, name, domain): _, operator_, operand = domain operator_ = { '=': operator.eq, '>=': operator.ge, '>': operator.gt, '<=': operator.le, '<': operator.lt, '!=': operator.ne, 'in': lambda v, l: v in l, 'not in': lambda v, l: v not in l, }.get(operator_, lambda v, l: False) ids = [] for location in cls.search([]): if operator_(getattr(location, name), operand): ids.append(location.id) return [('id', 'in', ids)] @classmethod def get_quantity_uom(cls, locations, name): pool = Pool() Product = pool.get('product.product') Template = pool.get('product.template') context = Transaction().context value = None uom = None if context.get('product') is not None: product = Product(context['product']) uom = product.default_uom elif context.get('product_template') is not None: template = Template(context['product_template']) uom = template.default_uom if uom: if name == 'quantity_uom': value = uom.id elif name == 'quantity_uom_digits': value = uom.digits return {l.id: value for l in locations} @classmethod def get_cost_value(cls, locations, name): pool = Pool() Product = pool.get('product.product') Template = pool.get('product.template') trans_context = Transaction().context cost_values = {l.id: None for l in locations} def valid_context(name): return (trans_context.get(name) is not None and isinstance(trans_context[name], int)) if not any(map(valid_context, ['product', 'product_template'])): return cost_values def get_record(): if trans_context.get('product') is not None: return Product(trans_context['product']) else: return Template(trans_context['product_template']) context = {} if 'stock_date_end' in trans_context: # Use the last cost_price of the day context['_datetime'] = datetime.datetime.combine( trans_context['stock_date_end'], datetime.time.max) # The date could be before the product creation record = get_record() if record.create_date > context['_datetime']: return cost_values with Transaction().set_context(context): cost_price = get_record().cost_price # The template may have more than one product if cost_price is not None: for location in locations: cost_values[location.id] = round_price( Decimal(str(location.quantity)) * cost_price) return cost_values @classmethod def _set_warehouse_parent(cls, locations): ''' Set the parent of child location of warehouse if not set ''' to_update = set() to_save = [] for location in locations: if location.type == 'warehouse': if not location.input_location.parent: to_update.add(location.input_location) if not location.output_location.parent: to_update.add(location.output_location) if not location.storage_location.parent: to_update.add(location.storage_location) if to_update: for child_location in to_update: child_location.parent = location to_save.append(child_location) to_update.clear() cls.save(to_save) @classmethod def create(cls, vlist): locations = super(Location, cls).create(vlist) cls._set_warehouse_parent(locations) cls._default_warehouse_cache.clear() return locations @classmethod def write(cls, *args): super(Location, cls).write(*args) locations = sum(args[::2], []) cls._set_warehouse_parent(locations) cls._default_warehouse_cache.clear() ids = [l.id for l in locations] warehouses = cls.search([('type', '=', 'warehouse'), [ 'OR', ('storage_location', 'in', ids), ('input_location', 'in', ids), ('output_location', 'in', ids), ]]) fields = ('storage_location', 'input_location', 'output_location') wh2childs = {} for warehouse in warehouses: in_out_sto = (getattr(warehouse, f).id for f in fields) for location in locations: if location.id not in in_out_sto: continue childs = wh2childs.setdefault( warehouse.id, cls.search([ ('parent', 'child_of', warehouse.id), ])) if location not in childs: raise LocationValidationError( gettext('stock.msg_location_child_of_warehouse', location=location.rec_name, warehouse=warehouse.rec_name)) @classmethod def delete(cls, *args): super().delete(*args) cls._default_warehouse_cache.clear() @classmethod def copy(cls, locations, default=None): if default is None: default = {} else: default = default.copy() res = [] for location in locations: if location.type == 'warehouse': wh_default = default.copy() wh_default['type'] = 'view' wh_default['input_location'] = None wh_default['output_location'] = None wh_default['storage_location'] = None wh_default['childs'] = None new_location, = super(Location, cls).copy([location], default=wh_default) with Transaction().set_context( cp_warehouse_locations={ 'input_location': location.input_location.id, 'output_location': location.output_location.id, 'storage_location': location.storage_location.id, }, cp_warehouse_id=new_location.id): cls.copy(location.childs, default={'parent': new_location.id}) cls.write([new_location], { 'type': 'warehouse', }) else: new_location, = super(Location, cls).copy([location], default=default) warehouse_locations = Transaction().context.get( 'cp_warehouse_locations') or {} if location.id in warehouse_locations.values(): cp_warehouse = cls( Transaction().context['cp_warehouse_id']) for field, loc_id in warehouse_locations.items(): if loc_id == location.id: cls.write([cp_warehouse], { field: new_location.id, }) res.append(new_location) return res @classmethod def view_attributes(cls): storage_types = Eval('type').in_(['storage', 'warehouse', 'view']) return super().view_attributes() + [ ('/tree/field[@name="quantity"]', 'visual', If(storage_types & (Eval('quantity', 0) < 0), 'danger', ''), ['type']), ('/tree/field[@name="forecast_quantity"]', 'visual', If(storage_types & (Eval('forecast_quantity', 0) < 0), 'warning', ''), ['type']), ]
class Work(tree(parent='successors'), metaclass=PoolMeta): __name__ = 'project.work' predecessors = fields.Many2Many('project.predecessor_successor', 'successor', 'predecessor', 'Predecessors', domain=[ ('parent', '=', Eval('parent')), ('id', '!=', Eval('id')), ], depends=['parent', 'id']) successors = fields.Many2Many('project.predecessor_successor', 'predecessor', 'successor', 'Successors', domain=[ ('parent', '=', Eval('parent')), ('id', '!=', Eval('id')), ], depends=['parent', 'id']) leveling_delay = fields.Float("Leveling Delay", required=True) back_leveling_delay = fields.Float("Back Leveling Delay", required=True) allocations = fields.One2Many('project.allocation', 'work', 'Allocations', states={ 'invisible': Eval('type') != 'task', }, depends=['type']) duration = fields.Function( fields.TimeDelta('Duration', 'company_work_time'), 'get_function_fields') early_start_time = fields.DateTime("Early Start Time", readonly=True) late_start_time = fields.DateTime("Late Start Time", readonly=True) early_finish_time = fields.DateTime("Early Finish Time", readonly=True) late_finish_time = fields.DateTime("Late Finish Time", readonly=True) actual_start_time = fields.DateTime("Actual Start Time") actual_finish_time = fields.DateTime("Actual Finish Time") constraint_start_time = fields.DateTime("Constraint Start Time") constraint_finish_time = fields.DateTime("Constraint Finish Time") early_start_date = fields.Function(fields.Date('Early Start'), 'get_function_fields') late_start_date = fields.Function(fields.Date('Late Start'), 'get_function_fields') early_finish_date = fields.Function(fields.Date('Early Finish'), 'get_function_fields') late_finish_date = fields.Function(fields.Date('Late Finish'), 'get_function_fields') actual_start_date = fields.Function(fields.Date('Actual Start'), 'get_function_fields', setter='set_function_fields') actual_finish_date = fields.Function(fields.Date('Actual Finish'), 'get_function_fields', setter='set_function_fields') constraint_start_date = fields.Function(fields.Date('Constraint Start', depends=['type']), 'get_function_fields', setter='set_function_fields') constraint_finish_date = fields.Function(fields.Date('Constraint Finish', depends=['type']), 'get_function_fields', setter='set_function_fields') @classmethod def __setup__(cls): super(Work, cls).__setup__() @staticmethod def default_leveling_delay(): return 0.0 @staticmethod def default_back_leveling_delay(): return 0.0 @classmethod def get_function_fields(cls, works, names): ''' Function to compute function fields ''' res = {} ids = [w.id for w in works] if 'duration' in names: all_works = cls.search([ ('parent', 'child_of', ids), ]) all_works = set(all_works) durations = {} id2work = {} leafs = set() for work in all_works: id2work[work.id] = work if not work.children: leafs.add(work.id) total_allocation = 0 effort = work.effort_duration or datetime.timedelta() if not work.allocations: durations[work.id] = effort continue for allocation in work.allocations: total_allocation += allocation.percentage durations[work.id] = datetime.timedelta( seconds=effort.total_seconds() / (total_allocation / 100.0)) while leafs: for work_id in leafs: work = id2work[work_id] all_works.remove(work) if work.parent and work.parent.id in durations: durations[work.parent.id] += durations[work_id] next_leafs = set(w.id for w in all_works) for work in all_works: if not work.parent: continue if work.parent.id in next_leafs and work.parent in works: next_leafs.remove(work.parent.id) leafs = next_leafs res['duration'] = durations fun_fields = ('early_start_date', 'early_finish_date', 'late_start_date', 'late_finish_date', 'actual_start_date', 'actual_finish_date', 'constraint_start_date', 'constraint_finish_date') db_fields = ('early_start_time', 'early_finish_time', 'late_start_time', 'late_finish_time', 'actual_start_time', 'actual_finish_time', 'constraint_start_time', 'constraint_finish_time') for fun_field, db_field in zip(fun_fields, db_fields): if fun_field in names: values = {} for work in works: values[work.id] = getattr(work, db_field) \ and getattr(work, db_field).date() or None res[fun_field] = values return res @classmethod def set_function_fields(cls, works, name, value): fun_fields = ('actual_start_date', 'actual_finish_date', 'constraint_start_date', 'constraint_finish_date') db_fields = ('actual_start_time', 'actual_finish_time', 'constraint_start_time', 'constraint_finish_time') for fun_field, db_field in zip(fun_fields, db_fields): if fun_field == name: cls.write( works, { db_field: (value and datetime.datetime.combine(value, datetime.time()) or None), }) break @property def hours(self): if not self.duration: return 0 return self.duration.total_seconds() / 60 / 60 @classmethod def add_minutes(cls, company, date, minutes): minutes = int(round(minutes)) minutes = date.minute + minutes hours = minutes // 60 if hours: date = cls.add_hours(company, date, hours) minutes = minutes % 60 date = datetime.datetime(date.year, date.month, date.day, date.hour, minutes, date.second) return date @classmethod def add_hours(cls, company, date, hours): while hours: if hours != intfloor(hours): minutes = (hours - intfloor(hours)) * 60 date = cls.add_minutes(company, date, minutes) hours = intfloor(hours) hours = date.hour + hours days = hours // company.hours_per_work_day if days: date = cls.add_days(company, date, days) hours = hours % company.hours_per_work_day date = datetime.datetime(date.year, date.month, date.day, intfloor(hours), date.minute, date.second) hours = hours - intfloor(hours) return date @classmethod def add_days(cls, company, date, days): day_per_week = company.hours_per_work_week / company.hours_per_work_day while days: if days != intfloor(days): hours = (days - intfloor(days)) * company.hours_per_work_day date = cls.add_hours(company, date, hours) days = intfloor(days) days = date.weekday() + days weeks = days // day_per_week days = days % day_per_week if weeks: date = cls.add_weeks(company, date, weeks) date += datetime.timedelta(days=-date.weekday() + intfloor(days)) days = days - intfloor(days) return date @classmethod def add_weeks(cls, company, date, weeks): day_per_week = company.hours_per_work_week / company.hours_per_work_day if weeks != intfloor(weeks): days = (weeks - intfloor(weeks)) * day_per_week if days: date = cls.add_days(company, date, days) date += datetime.timedelta(days=7 * intfloor(weeks)) return date def compute_dates(self): values = {} get_early_finish = lambda work: values.get(work, {}).get( 'early_finish_time', work.early_finish_time) get_late_start = lambda work: values.get(work, {}).get( 'late_start_time', work.late_start_time) maxdate = lambda x, y: x and y and max(x, y) or x or y mindate = lambda x, y: x and y and min(x, y) or x or y # propagate constraint_start_time constraint_start = reduce(maxdate, (pred.early_finish_time for pred in self.predecessors), None) if constraint_start is None and self.parent: constraint_start = self.parent.early_start_time constraint_start = maxdate(constraint_start, self.constraint_start_time) works = deque([(self, constraint_start)]) work2children = {} parent = None while works or parent: if parent: work = parent parent = None # Compute early_finish if work.children: early_finish_time = reduce( maxdate, map(get_early_finish, work.children), None) else: early_finish_time = None if values[work]['early_start_time']: early_finish_time = self.add_hours( work.company, values[work]['early_start_time'], work.hours) values[work]['early_finish_time'] = early_finish_time # Propagate constraint_start on successors for w in work.successors: works.append((w, early_finish_time)) if not work.parent: continue # housecleaning work2children if work.parent not in work2children: work2children[work.parent] = set() work2children[work.parent].update(work.successors) if work in work2children[work.parent]: work2children[work.parent].remove(work) # if no sibling continue to walk up the tree if not work2children.get(work.parent): if work.parent not in values: values[work.parent] = {} parent = work.parent continue work, constraint_start = works.popleft() # take constraint define on the work into account constraint_start = maxdate(constraint_start, work.constraint_start_time) if constraint_start: early_start = self.add_hours(work.company, constraint_start, work.leveling_delay) else: early_start = None # update values if work not in values: values[work] = {} values[work]['early_start_time'] = early_start # Loop on children if they exist if work.children and work not in work2children: work2children[work] = set(work.children) # Propagate constraint_start on children for w in work.children: if w.predecessors: continue works.append((w, early_start)) else: parent = work # propagate constraint_finish_time constraint_finish = reduce(mindate, (succ.late_start_time for succ in self.successors), None) if constraint_finish is None and self.parent: constraint_finish = self.parent.late_finish_time constraint_finish = mindate(constraint_finish, self.constraint_finish_time) works = deque([(self, constraint_finish)]) work2children = {} parent = None while works or parent: if parent: work = parent parent = None # Compute late_start if work.children: reduce(mindate, map(get_late_start, work.children), None) else: late_start_time = None if values[work]['late_finish_time']: late_start_time = self.add_hours( work.company, values[work]['late_finish_time'], -work.hours) values[work]['late_start_time'] = late_start_time # Propagate constraint_finish on predecessors for w in work.predecessors: works.append((w, late_start_time)) if not work.parent: continue # housecleaning work2children if work.parent not in work2children: work2children[work.parent] = set() work2children[work.parent].update(work.predecessors) if work in work2children[work.parent]: work2children[work.parent].remove(work) # if no sibling continue to walk up the tree if not work2children.get(work.parent): if work.parent not in values: values[work.parent] = {} parent = work.parent continue work, constraint_finish = works.popleft() # take constraint define on the work into account constraint_finish = mindate(constraint_finish, work.constraint_finish_time) if constraint_finish: late_finish = self.add_hours(work.company, constraint_finish, -work.back_leveling_delay) else: late_finish = None # update values if work not in values: values[work] = {} values[work]['late_finish_time'] = late_finish # Loop on children if they exist if work.children and work not in work2children: work2children[work] = set(work.children) # Propagate constraint_start on children for w in work.children: if w.successors: continue works.append((w, late_finish)) else: parent = work # write values write_fields = ('early_start_time', 'early_finish_time', 'late_start_time', 'late_finish_time') to_write = [] for work, val in values.items(): write_cond = False for field in write_fields: if field in val and getattr(work, field) != val[field]: write_cond = True break if write_cond: to_write.extend(([work], val)) if to_write: self.write(*to_write) def reset_leveling(self): get_key = lambda w: (set(p.id for p in w.predecessors), set(s.id for s in w.successors)) if not self.parent: return siblings = self.search([('parent', '=', self.parent.id)]) to_clean = [] ref_key = get_key(self) for sibling in siblings: if sibling.leveling_delay == sibling.back_leveling_delay == 0: continue if get_key(sibling) == ref_key: to_clean.append(sibling) if to_clean: self.write(to_clean, { 'leveling_delay': 0, 'back_leveling_delay': 0, }) def create_leveling(self): # define some helper functions get_key = lambda w: (set(p.id for p in w.predecessors), set(s.id for s in w.successors)) over_alloc = lambda current_alloc, work: (reduce( lambda res, alloc: (res or (current_alloc[alloc.employee.id] + alloc.percentage) > 100 ), work.allocations, False)) def sum_allocs(current_alloc, work): res = defaultdict(float) for alloc in work.allocations: empl = alloc.employee.id res[empl] = current_alloc[empl] + alloc.percentage return res def compute_delays(siblings): # time_line is a list [[end_delay, allocations], ...], this # mean that allocations is valid between the preceding end_delay # (or 0 if it doesn't exist) and the current end_delay. timeline = [] for sibling in siblings: delay = 0 ignored = [] overloaded = [] item = None while timeline: # item is [end_delay, allocations] item = heappop(timeline) if over_alloc(item[1], sibling): ignored.extend(overloaded) ignored.append(item) delay = item[0] continue elif item[1] >= delay + sibling.duration: overloaded.append(item) else: # Succes! break heappush(timeline, [ delay + sibling.duration, sum_allocs(defaultdict(float), sibling), sibling.id ]) for i in ignored: heappush(timeline, i) for i in overloaded: i[1] = sum_allocs(i[1], sibling) heappush(timeline, i) yield sibling, delay siblings = self.search([('parent', '=', self.parent.id if self.parent else None)]) refkey = get_key(self) siblings = [s for s in siblings if get_key(s) == refkey] for sibling, delay in compute_delays(siblings): sibling.leveling_delay = delay siblings.reverse() for sibling, delay in compute_delays(siblings): sibling.back_leveling_delay = delay self.__class__.save(siblings) if self.parent: self.parent.compute_dates() @classmethod def write(cls, *args): super(Work, cls).write(*args) actions = iter(args) for works, values in zip(actions, actions): if 'effort' in values: for work in works: work.reset_leveling() fields = ('constraint_start_time', 'constraint_finish_time', 'effort') if reduce(lambda x, y: x or y in values, fields, False): for work in works: work.compute_dates() @classmethod def create(cls, vlist): works = super(Work, cls).create(vlist) for work in works: work.reset_leveling() work.compute_dates() return works @classmethod def delete(cls, works): to_update = set() for work in works: if work.parent and work.parent not in works: to_update.add(work.parent) to_update.update(c for c in work.parent.children if c not in works) super(Work, cls).delete(works) for work in to_update: work.reset_leveling() work.compute_dates()
class Account(DeactivableMixin, tree('distribution_parents'), tree(), ModelSQL, ModelView): 'Analytic Account' __name__ = 'analytic_account.account' name = fields.Char('Name', required=True, translate=True, select=True) code = fields.Char('Code', select=True) company = fields.Many2One('company.company', 'Company', required=True) currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') type = fields.Selection([ ('root', 'Root'), ('view', 'View'), ('normal', 'Normal'), ('distribution', 'Distribution'), ], 'Type', required=True) root = fields.Many2One('analytic_account.account', 'Root', select=True, domain=[ ('company', '=', Eval('company', -1)), ('parent', '=', None), ('type', '=', 'root'), ], states={ 'invisible': Eval('type') == 'root', 'required': Eval('type') != 'root', }, depends=['company', 'type']) parent = fields.Many2One('analytic_account.account', 'Parent', select=True, domain=[ 'OR', ('root', '=', Eval('root', -1)), ('parent', '=', None), ], states={ 'invisible': Eval('type') == 'root', 'required': Eval('type') != 'root', }, depends=['root', 'type']) childs = fields.One2Many('analytic_account.account', 'parent', 'Children', states={ 'invisible': Eval('id', -1) < 0, }, domain=[ ('company', '=', Eval('company', -1)), ], depends=['company']) balance = fields.Function( fields.Numeric('Balance', digits=(16, Eval('currency_digits', 1)), depends=['currency_digits']), 'get_balance') credit = fields.Function( fields.Numeric('Credit', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_credit_debit') debit = fields.Function( fields.Numeric('Debit', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_credit_debit') state = fields.Selection([ ('draft', 'Draft'), ('opened', 'Opened'), ('closed', 'Closed'), ], 'State', required=True) note = fields.Text('Note') distributions = fields.One2Many('analytic_account.account.distribution', 'parent', "Distributions", states={ 'invisible': Eval('type') != 'distribution', 'required': Eval('type') == 'distribution', }, depends=['type']) distribution_parents = fields.Many2Many( 'analytic_account.account.distribution', 'account', 'parent', "Distribution Parents", readonly=True) @classmethod def __setup__(cls): super(Account, cls).__setup__() cls._order.insert(0, ('code', 'ASC')) cls._order.insert(1, ('name', 'ASC')) @classmethod def __register__(cls, module_name): super(Account, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 4.0: remove currency table.not_null_action('currency', action='remove') # Migration from 5.0: remove display_balance table.drop_column('display_balance') @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_type(): return 'normal' @staticmethod def default_state(): return 'draft' @classmethod def validate(cls, accounts): super(Account, cls).validate(accounts) for account in accounts: account.check_distribution() def check_distribution(self): if self.type != 'distribution': return if sum((d.ratio for d in self.distributions)) != 1: raise AccountValidationError( gettext('analytic_account.msg_invalid_distribution', account=self.rec_name)) @fields.depends('company') def on_change_with_currency(self, name=None): if self.company: return self.company.currency.id @fields.depends('company') def on_change_with_currency_digits(self, name=None): if self.company: return self.company.currency.digits return 2 @fields.depends('parent', 'type', '_parent_parent.root', '_parent_parent.type') def on_change_parent(self): if self.parent and self.type != 'root': if self.parent.type == 'root': self.root = self.parent else: self.root = self.parent.root else: self.root = None @classmethod def get_balance(cls, accounts, name): pool = Pool() Line = pool.get('analytic_account.line') MoveLine = pool.get('account.move.line') cursor = Transaction().connection.cursor() table = cls.__table__() line = Line.__table__() move_line = MoveLine.__table__() ids = [a.id for a in accounts] childs = cls.search([('parent', 'child_of', ids)]) all_ids = list({}.fromkeys(ids + [c.id for c in childs]).keys()) id2account = {} all_accounts = cls.browse(all_ids) for account in all_accounts: id2account[account.id] = account line_query = Line.query_get(line) cursor.execute( *table.join(line, 'LEFT', condition=table.id == line.account).join( move_line, 'LEFT', condition=move_line.id == line.move_line). select(table.id, Sum(Coalesce(line.credit, 0) - Coalesce(line.debit, 0)), where=(table.type != 'view') & table.id.in_(all_ids) & (table.active == True) & line_query, group_by=table.id)) account_sum = defaultdict(Decimal) for account_id, value in cursor.fetchall(): account_sum.setdefault(account_id, Decimal('0.0')) # SQLite uses float for SUM if not isinstance(value, Decimal): value = Decimal(str(value)) account_sum[account_id] += value balances = {} for account in accounts: balance = Decimal() childs = cls.search([ ('parent', 'child_of', [account.id]), ]) for child in childs: balance += account_sum[child.id] exp = Decimal(str(10.0**-account.currency_digits)) balances[account.id] = balance.quantize(exp) return balances @classmethod def get_credit_debit(cls, accounts, names): pool = Pool() Line = pool.get('analytic_account.line') MoveLine = pool.get('account.move.line') cursor = Transaction().connection.cursor() table = cls.__table__() line = Line.__table__() move_line = MoveLine.__table__() result = {} ids = [a.id for a in accounts] for name in names: if name not in ('credit', 'debit'): raise Exception('Bad argument') result[name] = {}.fromkeys(ids, Decimal('0.0')) id2account = {} for account in accounts: id2account[account.id] = account line_query = Line.query_get(line) columns = [table.id] for name in names: columns.append(Sum(Coalesce(Column(line, name), 0))) cursor.execute( *table.join(line, 'LEFT', condition=table.id == line.account).join( move_line, 'LEFT', condition=move_line.id == line.move_line).select(*columns, where=(table.type != 'view') & table.id.in_(ids) & (table.active == True) & line_query, group_by=table.id)) for row in cursor.fetchall(): account_id = row[0] for i, name in enumerate(names, 1): value = row[i] # SQLite uses float for SUM if not isinstance(value, Decimal): value = Decimal(str(value)) result[name][account_id] += value for account in accounts: for name in names: exp = Decimal(str(10.0**-account.currency_digits)) result[name][account.id] = ( result[name][account.id].quantize(exp)) return result def get_rec_name(self, name): if self.code: return self.code + ' - ' + str(self.name) else: return str(self.name) @classmethod def search_rec_name(cls, name, clause): accounts = cls.search([('code', ) + tuple(clause[1:])], limit=1) if accounts: return [('code', ) + tuple(clause[1:])] else: return [(cls._rec_name, ) + tuple(clause[1:])] def distribute(self, amount): "Return a list of (account, amount) distribution" assert self.type in {'normal', 'distribution'} if self.type == 'normal': return [(self, amount)] else: result = [] remainder = amount for distribution in self.distributions: account = distribution.account ratio = distribution.ratio current_amount = self.currency.round(amount * ratio) remainder -= current_amount result.extend(account.distribute(current_amount)) if remainder: i = 0 while remainder: account, amount = result[i] rounding = self.currency.rounding.copy_sign(remainder) result[i] = (account, amount - rounding) remainder -= rounding i = (i + 1) % len(result) return result
class Location(DeactivableMixin, tree(), ModelSQL, ModelView): "Stock Location" __name__ = 'stock.location' name = fields.Char("Name", size=None, required=True, states=STATES, depends=DEPENDS, translate=True) code = fields.Char("Code", size=None, states=STATES, depends=DEPENDS, select=True) address = fields.Many2One("party.address", "Address", states={ 'invisible': Eval('type') != 'warehouse', 'readonly': ~Eval('active'), }, depends=['type', 'active']) type = fields.Selection([ ('supplier', 'Supplier'), ('customer', 'Customer'), ('lost_found', 'Lost and Found'), ('warehouse', 'Warehouse'), ('storage', 'Storage'), ('production', 'Production'), ('drop', 'Drop'), ('view', 'View'), ], 'Location type', states=STATES, depends=DEPENDS) parent = fields.Many2One("stock.location", "Parent", select=True, left="left", right="right", states={ 'invisible': Eval('type') == 'warehouse', }, depends=['type']) left = fields.Integer('Left', required=True, select=True) right = fields.Integer('Right', required=True, select=True) childs = fields.One2Many("stock.location", "parent", "Children") flat_childs = fields.Boolean( "Flat Children", help="Check to restrict to one level of children.") warehouse = fields.Function(fields.Many2One('stock.location', 'Warehouse'), 'get_warehouse') input_location = fields.Many2One("stock.location", "Input", states={ 'invisible': Eval('type') != 'warehouse', 'readonly': ~Eval('active'), 'required': Eval('type') == 'warehouse', }, domain=[ ('type', '=', 'storage'), [ 'OR', ('parent', 'child_of', [Eval('id')]), ('parent', '=', None), ], ], depends=['type', 'active', 'id']) output_location = fields.Many2One("stock.location", "Output", states={ 'invisible': Eval('type') != 'warehouse', 'readonly': ~Eval('active'), 'required': Eval('type') == 'warehouse', }, domain=[('type', '=', 'storage'), [ 'OR', ('parent', 'child_of', [Eval('id')]), ('parent', '=', None) ]], depends=['type', 'active', 'id']) storage_location = fields.Many2One( "stock.location", "Storage", states={ 'invisible': Eval('type') != 'warehouse', 'readonly': ~Eval('active'), 'required': Eval('type') == 'warehouse', }, domain=[('type', 'in', ['storage', 'view']), [ 'OR', ('parent', 'child_of', [Eval('id')]), ('parent', '=', None) ]], depends=['type', 'active', 'id']) picking_location = fields.Many2One( 'stock.location', 'Picking', states={ 'invisible': Eval('type') != 'warehouse', 'readonly': ~Eval('active'), }, domain=[ ('type', '=', 'storage'), ('parent', 'child_of', [Eval('storage_location', -1)]), ], depends=['type', 'active', 'storage_location'], help='If empty the Storage is used') quantity = fields.Function(fields.Float('Quantity'), 'get_quantity', searcher='search_quantity') forecast_quantity = fields.Function(fields.Float('Forecast Quantity'), 'get_quantity', searcher='search_quantity') cost_value = fields.Function(fields.Numeric('Cost Value'), 'get_cost_value') @classmethod def __setup__(cls): super(Location, cls).__setup__() cls._order.insert(0, ('name', 'ASC')) cls._error_messages.update({ 'invalid_type_for_moves': ('Location "%s" with existing moves ' 'cannot be changed to a type that does not support moves.'), 'child_of_warehouse': ('Location "%(location)s" must be a ' 'child of warehouse "%(warehouse)s".'), 'inactive_location_with_moves': ("The location '%(location)s' must be empty " "to be deactivated."), }) parent_domain = [[ 'OR', ('parent.flat_childs', '=', False), ('parent', '=', None), ]] childs_domain = [ If(Eval('flat_childs', False), ('childs', '=', None), ()), ] childs_mapping = cls._childs_domain() for type_, allowed_parents in cls._parent_domain().items(): parent_domain.append( If(Eval('type') == type_, ('type', 'in', allowed_parents), ())) childs_domain.append( If( Eval('type') == type_, ('type', 'in', childs_mapping[type_]), ())) cls.parent.domain = parent_domain cls.childs.domain = childs_domain cls.childs.depends.extend(['flat_childs', 'type']) @classmethod def _parent_domain(cls): '''Returns a dict with location types as keys and a list of allowed parent location types as values''' return { 'customer': ['customer'], 'supplier': ['supplier'], 'production': ['production'], 'lost_found': ['lost_found'], 'view': ['warehouse', 'view', 'storage'], 'storage': ['warehouse', 'view', 'storage'], 'warehouse': [''], } @classmethod def _childs_domain(cls): childs_domain = {} for type_, allowed_parents in cls._parent_domain().items(): for parent in allowed_parents: childs_domain.setdefault(parent, []) childs_domain[parent].append(type_) return childs_domain @classmethod def __register__(cls, module_name): super(Location, cls).__register__(module_name) table = cls.__table_handler__(module_name) table.index_action(['left', 'right'], 'add') @classmethod def validate(cls, locations): super(Location, cls).validate(locations) inactives = [] for location in locations: location.check_type_for_moves() if not location.active: inactives.append(location) cls.check_inactive(inactives) def check_type_for_moves(self): """ Check locations with moves have types compatible with moves. """ invalid_move_types = ['warehouse', 'view'] Move = Pool().get('stock.move') if self.type in invalid_move_types: # Use root to compute for all companies with Transaction().set_user(0): moves = Move.search([ [ 'OR', ('to_location', '=', self.id), ('from_location', '=', self.id), ], ('state', 'not in', ['staging', 'draft']), ]) if moves: self.raise_user_error('invalid_type_for_moves', (self.rec_name, )) @classmethod def check_inactive(cls, locations): "Check inactive location are empty" assert all(not l.active for l in locations) empty = cls.get_empty_locations(locations) non_empty = set(locations) - set(empty) if non_empty: cls.raise_user_error('inactive_location_with_moves', { 'location': next(iter(non_empty)).rec_name, }) @classmethod def get_empty_locations(cls, locations=None): pool = Pool() Move = pool.get('stock.move') if locations is None: locations = cls.search([]) if not locations: return [] location_ids = list(map(int, locations)) # Use root to compute for all companies # and ensures inactive locations are in the query with Transaction().set_user(0), \ Transaction().set_context(active_test=False): query = Move.compute_quantities_query(location_ids, with_childs=True) quantities = Move.compute_quantities(query, location_ids, with_childs=True) empty = set(location_ids) for (location_id, product), quantity in quantities.items(): if quantity: empty.discard(location_id) for sub_ids in grouped_slice(list(empty)): sub_ids = list(sub_ids) moves = Move.search([ ('state', 'not in', ['done', 'cancel']), [ 'OR', ('from_location', 'in', sub_ids), ('to_location', 'in', sub_ids), ], ]) for move in moves: for location in [move.from_location, move.to_location]: empty.discard(location.id) return cls.browse(empty) @staticmethod def default_left(): return 0 @staticmethod def default_right(): return 0 @classmethod def default_flat_childs(cls): return False @staticmethod def default_type(): return 'storage' @classmethod def check_xml_record(cls, records, values): return True def get_warehouse(self, name): # Order by descending left to get the first one in the tree with Transaction().set_context(active_test=False): locations = self.search([ ('parent', 'parent_of', [self.id]), ('type', '=', 'warehouse'), ], order=[('left', 'DESC')]) if locations: return locations[0].id @classmethod def search_rec_name(cls, name, clause): locations = cls.search([ ('code', '=', clause[2]), ], order=[]) if locations: return [('id', 'in', [l.id for l in locations])] return [(cls._rec_name, ) + tuple(clause[1:])] @classmethod def get_quantity(cls, locations, name): pool = Pool() Product = pool.get('product.product') Date_ = pool.get('ir.date') trans_context = Transaction().context def valid_context(name): return (trans_context.get(name) is not None and isinstance(trans_context[name], int)) if not any(map(valid_context, ['product', 'product_template'])): return {l.id: None for l in locations} context = {} if (name == 'quantity' and (trans_context.get( 'stock_date_end', datetime.date.max) > Date_.today())): context['stock_date_end'] = Date_.today() if name == 'forecast_quantity': context['forecast'] = True if not trans_context.get('stock_date_end'): context['stock_date_end'] = datetime.date.max if trans_context.get('product') is not None: grouping = ('product', ) grouping_filter = ([trans_context['product']], ) key = trans_context['product'] else: grouping = ('product.template', ) grouping_filter = ([trans_context['product_template']], ) key = trans_context['product_template'] pbl = {} for sub_locations in grouped_slice(locations): location_ids = [l.id for l in sub_locations] with Transaction().set_context(context): pbl.update( Product.products_by_location( location_ids, grouping=grouping, grouping_filter=grouping_filter, with_childs=trans_context.get('with_childs', True))) return dict((loc.id, pbl.get((loc.id, key), 0)) for loc in locations) @classmethod def search_quantity(cls, name, domain): _, operator_, operand = domain operator_ = { '=': operator.eq, '>=': operator.ge, '>': operator.gt, '<=': operator.le, '<': operator.lt, '!=': operator.ne, 'in': lambda v, l: v in l, 'not in': lambda v, l: v not in l, }.get(operator_, lambda v, l: False) ids = [] for location in cls.search([]): if operator_(getattr(location, name), operand): ids.append(location.id) return [('id', 'in', ids)] @classmethod def get_cost_value(cls, locations, name): pool = Pool() Product = pool.get('product.product') Template = pool.get('product.template') trans_context = Transaction().context cost_values = {l.id: None for l in locations} def valid_context(name): return (trans_context.get(name) is not None and isinstance(trans_context[name], int)) if not any(map(valid_context, ['product', 'product_template'])): return cost_values def get_record(): if trans_context.get('product') is not None: return Product(trans_context['product']) else: return Template(trans_context['product_template']) context = {} if 'stock_date_end' in trans_context: # Use the last cost_price of the day context['_datetime'] = datetime.datetime.combine( trans_context['stock_date_end'], datetime.time.max) # The date could be before the product creation record = get_record() if record.create_date > context['_datetime']: return cost_values with Transaction().set_context(context): cost_price = get_record().cost_price # The template may have more than one product if cost_price is not None: for location in locations: cost_values[location.id] = (Decimal(str(location.quantity)) * cost_price) return cost_values @classmethod def _set_warehouse_parent(cls, locations): ''' Set the parent of child location of warehouse if not set ''' to_update = set() to_save = [] for location in locations: if location.type == 'warehouse': if not location.input_location.parent: to_update.add(location.input_location) if not location.output_location.parent: to_update.add(location.output_location) if not location.storage_location.parent: to_update.add(location.storage_location) if to_update: for child_location in to_update: child_location.parent = location to_save.append(child_location) to_update.clear() cls.save(to_save) @classmethod def create(cls, vlist): locations = super(Location, cls).create(vlist) cls._set_warehouse_parent(locations) return locations @classmethod def write(cls, *args): super(Location, cls).write(*args) locations = sum(args[::2], []) cls._set_warehouse_parent(locations) ids = [l.id for l in locations] warehouses = cls.search([('type', '=', 'warehouse'), [ 'OR', ('storage_location', 'in', ids), ('input_location', 'in', ids), ('output_location', 'in', ids), ]]) fields = ('storage_location', 'input_location', 'output_location') wh2childs = {} for warehouse in warehouses: in_out_sto = (getattr(warehouse, f).id for f in fields) for location in locations: if location.id not in in_out_sto: continue childs = wh2childs.setdefault( warehouse.id, cls.search([ ('parent', 'child_of', warehouse.id), ])) if location not in childs: cls.raise_user_error( 'child_of_warehouse', { 'location': location.rec_name, 'warehouse': warehouse.rec_name, }) @classmethod def copy(cls, locations, default=None): if default is None: default = {} else: default = default.copy() res = [] for location in locations: if location.type == 'warehouse': wh_default = default.copy() wh_default['type'] = 'view' wh_default['input_location'] = None wh_default['output_location'] = None wh_default['storage_location'] = None wh_default['childs'] = None new_location, = super(Location, cls).copy([location], default=wh_default) with Transaction().set_context( cp_warehouse_locations={ 'input_location': location.input_location.id, 'output_location': location.output_location.id, 'storage_location': location.storage_location.id, }, cp_warehouse_id=new_location.id): cls.copy(location.childs, default={'parent': new_location.id}) cls.write([new_location], { 'type': 'warehouse', }) else: new_location, = super(Location, cls).copy([location], default=default) warehouse_locations = Transaction().context.get( 'cp_warehouse_locations') or {} if location.id in warehouse_locations.values(): cp_warehouse = cls( Transaction().context['cp_warehouse_id']) for field, loc_id in warehouse_locations.items(): if loc_id == location.id: cls.write([cp_warehouse], { field: new_location.id, }) res.append(new_location) return res
class Type(sequence_ordered(), ModelSQL, ModelView, tree(separator='\\')): 'Account Meta Type' __name__ = 'account.account.meta.type' _states = { 'readonly': (Bool(Eval('template', -1)) & ~Eval('template_override', False)), } name = fields.Char('Name', size=None, required=True, states=_states) parent = fields.Many2One('account.account.meta.type', 'Parent', ondelete="RESTRICT", states=_states, domain=[ ('company', '=', Eval('company')), ], depends=['company']) childs = fields.One2Many('account.account.meta.type', 'parent', 'Children', domain=[ ('company', '=', Eval('company')), ], depends=['company']) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'get_currency_digits') amount = fields.Function( fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_amount') amount_cmp = fields.Function( fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_amount_cmp') balance_sheet = fields.Boolean('Balance Sheet', states=_states) income_statement = fields.Boolean('Income Statement', states=_states) display_balance = fields.Selection([ ('debit-credit', 'Debit - Credit'), ('credit-debit', 'Credit - Debit'), ], 'Display Balance', required=True, states=_states) company = fields.Many2One('company.company', 'Company', required=True, ondelete="RESTRICT") template = fields.Many2One('account.account.meta.type.template', 'Template') template_override = fields.Boolean( 'Override Template', help="Check to override template definition", states={ 'invisible': ~Bool(Eval('template', -1)), }, depends=['template']) level = fields.Function(fields.Numeric('Level', digits=(2, 0)), '_get_level') type_display_balance = fields.Selection([('debit', 'Debit'), ('credit', 'Credit')], 'Type') custom_amount = fields.Function( fields.Numeric('Custom Amount', digits=(2, 0)), '_get_custom_amount') del _states def _get_level(self, parent=None): level = 0 if self.parent: level = self.parent.level + 1 return level def _get_custom_amount(self, name): amount = 0 if self.type_display_balance == 'credit': amount = -self.amount else: amount = self.amount return amount def _get_childs_by_order(self, res=None): '''Returns the records of all the children computed recursively, and sorted by sequence. Ready for the printing''' Account = Pool().get('account.account.meta.type') if res is None: res = [] childs = Account.search([('parent', '=', self.id)], order=[('sequence', 'ASC')]) if len(childs) >= 1: for child in childs: res.append(Account(child.id)) child._get_childs_by_order(res=res) return res #@classmethod #def validate(cls, types): # super(Type, cls).validate(types) # cls.check_recursion(types, rec_name='name') @staticmethod def default_balance_sheet(): return False @staticmethod def default_income_statement(): return False @staticmethod def default_display_balance(): return 'debit-credit' @staticmethod def default_type_display_balance(): return 'debit' @classmethod def default_template_override(cls): return False def get_currency_digits(self, name): return self.company.currency.digits @classmethod def get_amount(cls, types, name): pool = Pool() Account = pool.get('account.account') GeneralLedger = pool.get('account.general_ledger.account') transaction = Transaction() context = transaction.context res = {} for type_ in types: res[type_.id] = Decimal('0.0') childs = cls.search([ ('parent', 'child_of', [t.id for t in types]), ]) type_sum = {} for type_ in childs: type_sum[type_.id] = Decimal('0.0') start_period_ids = GeneralLedger.get_period_ids('start_%s' % name) end_period_ids = GeneralLedger.get_period_ids('end_%s' % name) period_ids = list( set(end_period_ids).difference(set(start_period_ids))) for company in context.get('companies', []): with transaction.set_context(company=company['id'], posted=True, cumulate=True): accounts = Account.search([ ('company', '=', company['id']), ('type.meta_type', 'in', [t.id for t in childs]), ('kind', '!=', 'view'), ]) for account in accounts: key = account.type.meta_type.id type_sum[key] += (account.debit - account.credit) for type_ in types: childs = cls.search([ ('parent', 'child_of', [type_.id]), ]) for child in childs: res[type_.id] += type_sum[child.id] exp = Decimal(str(10.0**-type_.currency_digits)) res[type_.id] = res[type_.id].quantize(exp) if type_.display_balance == 'credit-debit': res[type_.id] = -res[type_.id] return res @classmethod def get_amount_cmp(cls, types, name): transaction = Transaction() current = transaction.context if not current.get('comparison'): return dict.fromkeys([t.id for t in types], None) new = {} for key, value in current.iteritems(): if key.endswith('_cmp'): new[key[:-4]] = value with transaction.set_context(new): return cls.get_amount(types, name) @classmethod def view_attributes(cls): return [ ('/tree/field[@name="amount_cmp"]', 'tree_invisible', ~Eval('comparison', False)), ] def get_rec_name(self, name): #if self.parent: # return self.parent.get_rec_name(name) + '\\' + self.name #else: # return self.name return self.name @classmethod def delete(cls, types): types = cls.search([ ('parent', 'child_of', [t.id for t in types]), ]) super(Type, cls).delete(types) def update_type(self, template2type=None): ''' Update recursively types based on template. template2type is a dictionary with template id as key and type id as value, used to convert template id into type. The dictionary is filled with new types ''' if template2type is None: template2type = {} values = [] childs = [self] while childs: for child in childs: if child.template and not child.template_override: vals = child.template._get_type_value(type=child) if vals: values.append([child]) values.append(vals) template2type[child.template.id] = child.id childs = sum((c.childs for c in childs), ()) if values: self.write(*values)
class TypeTemplate(tree(separator='\\'), sequence_ordered(), ModelSQL, ModelView): 'Account Meta Type Template' __name__ = 'account.account.meta.type.template' name = fields.Char('Name', required=True) parent = fields.Many2One('account.account.meta.type.template', 'Parent', ondelete="RESTRICT") childs = fields.One2Many('account.account.meta.type.template', 'parent', 'Children') balance_sheet = fields.Boolean('Balance Sheet') income_statement = fields.Boolean('Income Statement') display_balance = fields.Selection([ ('debit-credit', 'Debit - Credit'), ('credit-debit', 'Credit - Debit'), ], 'Display Balance', required=True) type_display_balance = fields.Selection([('debit', 'Debit'), ('credit', 'Credit')], 'Type') #@classmethod #def validate(cls, records): # super(TypeTemplate, cls).validate(records) # cls.check_recursion(records, rec_name='name') @staticmethod def default_balance_sheet(): return False @staticmethod def default_income_statement(): return False @staticmethod def default_display_balance(): return 'debit-credit' @staticmethod def default_type_display_balance(): return 'debit' def get_rec_name(self, name): if self.parent: return self.parent.get_rec_name(name) + '\\' + self.name else: return self.name def _get_type_value(self, type=None): ''' Set the values for account creation. ''' res = {} if not type or type.name != self.name: res['name'] = self.name if not type or type.sequence != self.sequence: res['sequence'] = self.sequence if not type or type.balance_sheet != self.balance_sheet: res['balance_sheet'] = self.balance_sheet if not type or type.income_statement != self.income_statement: res['income_statement'] = self.income_statement if not type or type.display_balance != self.display_balance: res['display_balance'] = self.display_balance if not type or type.type_display_balance != self.type_display_balance: res['type_display_balance'] = self.type_display_balance if not type or type.template != self: res['template'] = self.id return res def create_type(self, company_id, template2type=None): ''' Create recursively types based on template. template2type is a dictionary with template id as key and type id as value, used to convert template id into type. The dictionary is filled with new types. ''' pool = Pool() Type = pool.get('account.account.meta.type') assert self.parent is None if template2type is None: template2type = {} def create(templates): values = [] created = [] for template in templates: if template.id not in template2type: vals = template._get_type_value() vals['company'] = company_id if template.parent: vals['parent'] = template2type[template.parent.id] else: vals['parent'] = None values.append(vals) created.append(template) types = Type.create(values) for template, type_ in zip(created, types): template2type[template.id] = type_.id childs = [self] while childs: create(childs) childs = sum((c.childs for c in childs), ())
class UIMenu(DeactivableMixin, sequence_ordered(), tree(separator=' / '), ModelSQL, ModelView): "UI menu" __name__ = 'ir.ui.menu' name = fields.Char('Menu', required=True, translate=True) childs = fields.One2Many('ir.ui.menu', 'parent', 'Children') parent = fields.Many2One('ir.ui.menu', 'Parent Menu', select=True, ondelete='CASCADE') groups = fields.Many2Many('ir.ui.menu-res.group', 'menu', 'group', 'Groups') complete_name = fields.Function(fields.Char('Complete Name'), 'get_rec_name', searcher='search_rec_name') icon = fields.Selection('list_icons', 'Icon', translate=False) action = fields.Function(fields.Reference( 'Action', selection=[ ('', ''), ('ir.action.report', 'ir.action.report'), ('ir.action.act_window', 'ir.action.act_window'), ('ir.action.wizard', 'ir.action.wizard'), ('ir.action.url', 'ir.action.url'), ], translate=False), 'get_action', setter='set_action') action_keywords = fields.One2Many('ir.action.keyword', 'model', 'Action Keywords') favorite = fields.Function(fields.Boolean('Favorite'), 'get_favorite') @classmethod def order_complete_name(cls, tables): return cls.name.convert_order('name', tables, cls) @staticmethod def default_icon(): return 'tryton-folder' @staticmethod def default_sequence(): return 10 @staticmethod def list_icons(): pool = Pool() Icon = pool.get('ir.ui.icon') return sorted(CLIENT_ICONS + [(name, name) for _, name in Icon.list_icons()]) @classmethod def search_global(cls, text): # TODO improve search clause for record in cls.search([ ('rec_name', 'ilike', '%%%s%%' % text), ]): if record.action: yield record, record.rec_name, record.icon @classmethod def search(cls, domain, offset=0, limit=None, order=None, count=False, query=False): menus = super(UIMenu, cls).search(domain, offset=offset, limit=limit, order=order, count=False, query=query) if query: return menus if menus: parent_ids = {x.parent.id for x in menus if x.parent} parents = set() for sub_parent_ids in grouped_slice(parent_ids): parents.update( cls.search([ ('id', 'in', list(sub_parent_ids)), ])) # Re-browse to avoid side-cache access menus = cls.browse([ x.id for x in menus if (x.parent and x.parent in parents) or not x.parent ]) if count: return len(menus) return menus @classmethod def get_action(cls, menus, name): pool = Pool() actions = dict((m.id, None) for m in menus) with Transaction().set_context(active_test=False): menus = cls.browse(menus) action_keywords = sum((list(m.action_keywords) for m in menus), []) key = lambda k: k.action.type action_keywords.sort(key=key) for type, action_keywords in groupby(action_keywords, key=key): action_keywords = list(action_keywords) for action_keyword in action_keywords: model = action_keyword.model actions[model.id] = '%s,-1' % type Action = pool.get(type) action2keyword = {k.action.id: k for k in action_keywords} with Transaction().set_context(active_test=False): factions = Action.search([ ('action', 'in', list(action2keyword.keys())), ]) for action in factions: model = action2keyword[action.id].model actions[model.id] = str(action) return actions @classmethod def set_action(cls, menus, name, value): pool = Pool() ActionKeyword = pool.get('ir.action.keyword') action_keywords = [] transaction = Transaction() for i in range(0, len(menus), transaction.database.IN_MAX): sub_menus = menus[i:i + transaction.database.IN_MAX] action_keywords += ActionKeyword.search([ ('keyword', '=', 'tree_open'), ('model', 'in', [str(menu) for menu in sub_menus]), ]) if action_keywords: with Transaction().set_context(_timestamp=False): ActionKeyword.delete(action_keywords) if not value: return if isinstance(value, str): action_type, action_id = value.split(',') else: action_type, action_id = value if int(action_id) <= 0: return Action = pool.get(action_type) action = Action(int(action_id)) to_create = [] for menu in menus: with Transaction().set_context(_timestamp=False): to_create.append({ 'keyword': 'tree_open', 'model': str(menu), 'action': action.action.id, }) if to_create: ActionKeyword.create(to_create) @classmethod def get_favorite(cls, menus, name): pool = Pool() Favorite = pool.get('ir.ui.menu.favorite') user = Transaction().user favorites = Favorite.search([ ('menu', 'in', [m.id for m in menus]), ('user', '=', user), ]) menu2favorite = dict( (m.id, False if m.action else None) for m in menus) menu2favorite.update(dict((f.menu.id, True) for f in favorites)) return menu2favorite
class TmiMetaGroup( tree(separator='\\'), sequence_ordered(), ModelSQL, ModelView): 'Meta Group' __name__ = 'tmi.meta.group' name = fields.Char('Name', size=None, required=True) code = fields.Char('Code') active = fields.Boolean('Active') company = fields.Many2One('company.company', 'Company', required=True) currency = fields.Function(fields.Many2One('currency.currency', 'Currency'), 'get_currency') currency_digits = fields.Function(fields.Integer('Currency Digits'), 'get_currency_digits') parent = fields.Many2One('tmi.meta.group', 'Parent', ondelete="RESTRICT", domain=[ 'OR',[ ('company','=',Eval('company',-1)), ('company', 'in',Eval('company.childs',[])), ], ('type', '=', Eval('parent_type',None)), ], depends=['type','company']) childs = fields.One2Many('tmi.meta.group', 'parent', 'Children', domain=[ 'OR',[ ('company','=',Eval('company',-1)), ('company', 'in',Eval('company.childs',[])), ], ], depends=['company']) baptism = fields.Function(fields.Numeric('Baptism', digits=(16,Eval('currency_digits',2))), 'get_balance') small_group = fields.Function(fields.Numeric('Small Group', digits=(16,Eval('currency_digits',2))), 'get_balance') tithe = fields.Function(fields.Numeric('Tithe', digits=(16,Eval('currency_digits',2))), 'get_balance') offering = fields.Function(fields.Numeric('Offering', digits=(16,Eval('currency_digits',2))), 'get_balance') praise_thanksgiving = fields.Function(fields.Numeric('Praise and Thanksgiving', digits=(16,Eval('currency_digits',2))), 'get_balance') gathering = fields.Function(fields.Numeric('Gathering', digits=(16,Eval('currency_digits',2))), 'get_balance') church_planting = fields.Function(fields.Numeric('Church Planting', digits=(16,Eval('currency_digits',2))), 'get_balance') organizing_church = fields.Function(fields.Numeric('Organizing Church', digits=(16,Eval('currency_digits',2))), 'get_balance') type = fields.Selection( [ ('conference','Conference'), ('division','Division'), ('union','Union'), ('field','Field'), ('zone','Zone'), ('district','District'), ('church','Church'), ('small_group','Small Group'), ] ,'Type', required=True, sort=False) parent_type = fields.Function(fields.Selection( [ ('conference','Conference'), ('division','Division'), ('union','Union'), ('field','Field'), ('zone','Zone'), ('district','District'), ('church','Church'), ('small_group','Small Group'), ], 'Parent Type'), 'get_parent_type') company = fields.Many2One('company.company', 'Company', required=True, ondelete="RESTRICT") child_value = fields.Function(fields.Numeric('Child Value'), 'get_child_value') tmi_baptism_target = fields.Function(fields.Numeric('Baptism Target', digits=(16,0 )), 'get_target') tmi_small_group_target = fields.Function(fields.Numeric('Small Group Target', digits=(16, 0)), 'get_target') tmi_tithe_target = fields.Function(fields.Numeric('Tithe Target', digits=(16,Eval('currency_digits',2))), 'get_target') tmi_offering_target = fields.Function(fields.Numeric('Offering Target', digits=(16,Eval('currency_digits',2))), 'get_target') tmi_praise_thanksgiving_target = fields.Function(fields.Numeric('Praise and Thanksgiving Target', digits=(16,Eval('currency_digits',2))), 'get_target') tmi_gathering_target = fields.Function(fields.Numeric('Gathering Target', digits=(16,Eval('currency_digits',2))), 'get_target') tmi_church_planting_target = fields.Function(fields.Numeric('Church Planting Target', digits=(16,0)), 'get_target') tmi_organizing_church_target = fields.Function(fields.Numeric('Organizing Church Target', digits=(16,0)), 'get_target') tmi_baptism_difference = fields.Function(fields.Numeric('Baptism Difference', digits=(16,0 )), 'get_difference') tmi_small_group_difference = fields.Function(fields.Numeric('Small Group Difference', digits=(16, 0)), 'get_difference') tmi_tithe_difference = fields.Function(fields.Numeric('Tithe Difference', digits=(16,Eval('currency_digits',2))), 'get_difference') tmi_offering_difference = fields.Function(fields.Numeric('Offering Difference', digits=(16,Eval('currency_digits',2))), 'get_difference') tmi_praise_thanksgiving_difference = fields.Function(fields.Numeric('Praise and Thanksgiving Difference', digits=(16,Eval('currency_digits',2))), 'get_difference') tmi_gathering_difference = fields.Function(fields.Numeric('Gathering Difference', digits=(16,Eval('currency_digits',2))), 'get_difference') tmi_church_planting_difference = fields.Function(fields.Numeric('Church Planting Difference', digits=(16,0)), 'get_difference') tmi_organizing_church_difference = fields.Function(fields.Numeric('Organizing Church Difference', digits=(16,0)), 'get_difference') tmi_baptism_percentage = fields.Function(fields.Numeric('Baptism Percentage', digits=(16,2)), 'get_percentage') tmi_small_group_percentage = fields.Function(fields.Numeric('Small Group Percentage', digits=(16,2)), 'get_percentage') tmi_tithe_percentage = fields.Function(fields.Numeric('Tithe Percentage', digits=(16,2)), 'get_percentage') tmi_offering_percentage = fields.Function(fields.Numeric('Offering Percentage', digits=(16,2)), 'get_percentage') tmi_praise_thanksgiving_percentage = fields.Function(fields.Numeric('Praise and Thanksgiving Percentage', digits=(16,2)), 'get_percentage') tmi_gathering_percentage = fields.Function(fields.Numeric('Gathering Percentage', digits=(16,2)), 'get_percentage') tmi_church_planting_percentage = fields.Function(fields.Numeric('Church Planting Percentage', digits=(16,2)), 'get_percentage') tmi_organizing_church_percentage = fields.Function(fields.Numeric('Organizing Church Percentage', digits=(16,2)), 'get_percentage') church = fields.Function(fields.Many2One('tmi.meta.group', 'Church', domain=[ 'OR',[ ('company','=',Eval('company',-1)), ('company', 'in',Eval('company.childs',[])), ], ('type', '=', 'church'), ], depends=['type','company']), 'get_church', searcher='search_church', ) district = fields.Function(fields.Many2One('tmi.meta.group', 'District', domain=[ 'OR',[ ('company','=',Eval('company',-1)), ('company', 'in',Eval('company.childs',[])), ], ('type', '=', 'district'), ], depends=['type','company']), 'get_district', searcher='search_district', ) level = fields.Function(fields.Numeric('Level',digits=(2,0)), '_get_level') @classmethod def __setup__(cls): super(TmiMetaGroup, cls).__setup__() #cls._order.insert(0, ('baptism', 'ASC')) cls._order.insert(0, ('name', 'ASC')) cls._order.insert(0, ('code', 'ASC')) def get_parent_type(self, name): if self.type=='small_group': return 'church' if self.type=='church': return 'district' if self.type=='district': return 'zone' if self.type=='zone': return 'field' if self.type=='field': return 'union' if self.type=='union': return 'division' if self.type=='division': return 'conference' return None def _get_level(self, parent=None): level = 0 if self.parent: level = self.parent.level + 1 return level def _get_childs_by_order(self, res=None, _order=None): '''Returns the records of all the children computed recursively, and sorted by sequence. Ready for the printing''' Group = Pool().get('tmi.meta.group') if res is None: res = [] if _order is None: _order = 'baptism' childs = Group.search([('parent', '=', self.id)]) if len(childs)>=1: for child in childs: res.append(Group(child.id)) child._get_childs_by_order(res=res) return res @fields.depends('type') def get_church(self, name=None): if self.type == 'small_group' and self.parent: return self.parent.id return None @fields.depends('type') def get_district(self, name=None): if self.type == 'small_group' and self.parent: if self.parent.parent: return self.parent.parent.id if self.type == 'church' and self.parent: return self.parent.id return None @classmethod def search_church(cls, name, clause): return [('parent.name' + clause[0].lstrip(name),) + tuple(clause[1:])] @classmethod def search_district(cls, name, clause): return [('parent.name' + clause[0].lstrip(name),) + tuple(clause[1:])] @fields.depends('type','parent_type') def on_change_type(self): if self.type=='small_group': self.parent_type = 'church' if self.type=='church': self.parent_type = 'district' if self.type=='district': self.parent_type = 'zone' if self.type=='zone': self.parent_type = 'field' if self.type=='field': self.parent_type = 'union' if self.type=='union': self.parent_type = 'division' if self.type=='division': self.parent_type = 'conference' return None def get_type(self,value=None): if value=='small_group': return 'Small Group' if value=='church': return 'Church' if value=='district': return 'District' if value=='zone': return 'Zone' if value=='field': return 'Field' if value=='union': return 'Union' if value=='division': return 'Division' if value=='conference': return 'Conference' return None def get_rec_name(self, name): if self.code and self.parent: return self.name + ' - ' + self.code + ' - ' + self.parent.name elif self.code and not self.parent: return self.name + ' - ' + self.code elif self.parent and not self.code: return self.name + ' - ' + self.parent.name else: return self.name @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, ('code',) + tuple(clause[1:]), ('type',) + tuple(clause[1:]), ('name',) + tuple(clause[1:]), (cls._rec_name,) + tuple(clause[1:]), ] def get_currency(self, name): return self.company.currency.id def get_currency_digits(self, name): return self.company.currency.digits def get_child_value(self, name): if self.childs is not None and self.type !='small_group': return sum(x.child_value for x in self.childs) elif self.childs is None and self.type =='small_group': return 1 else: children_sum = 1 for child in self.childs: children_sum = sum(x.child_value for x in self.childs if x.type=='small_group') return children_sum return 0 @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_active(): return True @staticmethod def default_type(): return 'small_group' @classmethod def get_baptism(cls, metas, name): pool = Pool() Group = pool.get('tmi.group') Period = pool.get('tmi.period') res = {} for meta in metas: res[meta.id] = Decimal('0.0') childs = cls.search([ ('parent', 'child_of', [m.id for m in metas]), ]) meta_sum = {} for meta in childs: meta_sum[meta.id] = Decimal('0.0') start_period_ids = Period.get_period_ids('start_%s' % name) end_period_ids = Period.get_period_ids('end_%s' % name) period_ids = list( set(end_period_ids).difference(set(start_period_ids))) with Transaction().set_context(periods=period_ids): groups = Group.search([ ('meta', 'in', [m.id for m in childs]), ]) for group in groups: meta_sum[group.meta.id] += (group.baptism) for meta in metas: childs = cls.search([ ('parent', 'child_of', [meta.id]), ]) for child in childs: res[meta.id] += meta_sum[child.id] exp = Decimal(str(10.0 ** -meta.currency_digits)) res[meta.id] = res[meta.id].quantize(exp) return res @classmethod def get_balance(cls, metas, name): pool = Pool() Group = pool.get('tmi.group') Period = pool.get('tmi.period') res = {} for meta in metas: res[meta.id] = Decimal('0.0') childs = cls.search([ ('parent', 'child_of', [m.id for m in metas]), ]) meta_sum = {} for meta in childs: meta_sum[meta.id] = Decimal('0.0') start_period_ids = Period.get_period_ids('start_%s' % name) end_period_ids = Period.get_period_ids('end_%s' % name) period_ids = list( set(end_period_ids).difference(set(start_period_ids))) with Transaction().set_context(periods=period_ids): groups = Group.search([ ('meta', 'in', [m.id for m in childs]), ]) for group in groups: meta_sum[group.meta.id] += (getattr(group,name)) for meta in metas: childs = cls.search([ ('parent', 'child_of', [meta.id]), ]) for child in childs: res[meta.id] += meta_sum[child.id] exp = Decimal(str(10.0 ** -meta.currency_digits)) res[meta.id] = res[meta.id].quantize(exp) return res @staticmethod def order_baptism(tables): pool = Pool() Group = pool.get('tmi.meta.group') group = Group.__table__() table, _ = tables[None] return [CharLength(table.name), table.name] def get_baptism_target(self, name=None): pool = Pool() Configuration = pool.get('tmi.configuration') config = Configuration(1) target = config.get_multivalue('tmi_baptism_target') context = Transaction().context start_date = context.get('start_date') end_date = context.get('end_date') value = self.child_value if start_date and end_date: months = diff_month(end_date, start_date) value = value * months total = 0 if target and value: total = target * value return total def get_target(self, name): pool = Pool() Configuration = pool.get('tmi.configuration') if name not in {'tmi_baptism_target','tmi_tithe_target','tmi_offering_target','tmi_church_planting_target', 'tmi_gathering_target','tmi_small_group_target','tmi_organizing_church_target', 'tmi_praise_thanksgiving_target'}: raise ValueError('Unknown name: %s' % name) config = Configuration(1) field = str(name) target = config.get_multivalue(field) context = Transaction().context start_date = context.get('start_date') end_date = context.get('end_date') value = self.child_value if start_date and end_date: months = diff_month(end_date, start_date) value = value * months total = 0 if target and value: total = target * value return total def get_difference(self, name): pool = Pool() if name not in {'tmi_baptism_difference','tmi_tithe_difference','tmi_offering_difference','tmi_church_planting_difference', 'tmi_gathering_difference','tmi_small_group_difference','tmi_organizing_church_difference', 'tmi_praise_thanksgiving_difference'}: raise ValueError('Unknown name: %s' % name) field = str(name) target_field = field.replace('difference','target') base_field = field.replace('tmi_','') base_field = base_field.replace('_difference','') target_field_value = getattr(self, target_field,None) base_field_value = getattr(self, base_field,None) difference = target_field_value - base_field_value return difference def get_percentage(self, name): pool = Pool() if name not in {'tmi_baptism_percentage','tmi_tithe_percentage','tmi_offering_percentage','tmi_church_planting_percentage', 'tmi_gathering_percentage','tmi_small_group_percentage','tmi_organizing_church_percentage', 'tmi_praise_thanksgiving_percentage'}: raise ValueError('Unknown name: %s' % name) field = str(name) target_field = field.replace('percentage','target') base_field = field.replace('tmi_','') base_field = base_field.replace('_percentage','') target_field_value = getattr(self, target_field,None) base_field_value = getattr(self, base_field,None) difference = 0 if target_field_value and target_field_value != 0: difference = base_field_value / target_field_value difference = round(difference, 2) return difference
class Work(sequence_ordered(), tree(separator='\\'), ModelSQL, ModelView): 'Work Effort' __name__ = 'project.work' name = fields.Char('Name', required=True, select=True) type = fields.Selection([ ('project', 'Project'), ('task', 'Task') ], 'Type', required=True, select=True) company = fields.Many2One('company.company', 'Company', required=True, select=True) party = fields.Many2One('party.party', 'Party', states={ 'invisible': Eval('type') != 'project', }, context={ 'company': Eval('company', -1), }, depends={'company'}) party_address = fields.Many2One('party.address', 'Contact Address', domain=[('party', '=', Eval('party'))], states={ 'invisible': Eval('type') != 'project', }) timesheet_works = fields.One2Many( 'timesheet.work', 'origin', 'Timesheet Works', readonly=True, size=1) timesheet_available = fields.Function( fields.Boolean('Available on timesheets'), 'get_timesheet_available', setter='set_timesheet_available') timesheet_start_date = fields.Function(fields.Date('Timesheet Start', states={ 'invisible': ~Eval('timesheet_available'), }), 'get_timesheet_date', setter='set_timesheet_date') timesheet_end_date = fields.Function(fields.Date('Timesheet End', states={ 'invisible': ~Eval('timesheet_available'), }), 'get_timesheet_date', setter='set_timesheet_date') timesheet_duration = fields.Function(fields.TimeDelta('Duration', 'company_work_time', help="Total time spent on this work and the sub-works."), 'get_total') effort_duration = fields.TimeDelta('Effort', 'company_work_time', help="Estimated Effort for this work.") total_effort = fields.Function(fields.TimeDelta('Total Effort', 'company_work_time', help="Estimated total effort for this work and the sub-works."), 'get_total') progress = fields.Float('Progress', domain=['OR', ('progress', '=', None), [ ('progress', '>=', 0), ('progress', '<=', 1), ], ], help='Estimated progress for this work.') total_progress = fields.Function(fields.Float('Total Progress', digits=(16, 4), help='Estimated total progress for this work and the sub-works.', states={ 'invisible': ( Eval('total_progress', None) == None), # noqa: E711 }), 'get_total') comment = fields.Text('Comment') parent = fields.Many2One( 'project.work', 'Parent', path='path', ondelete='RESTRICT', domain=[ ('company', '=', Eval('company', -1)), ]) path = fields.Char("Path", select=True) children = fields.One2Many('project.work', 'parent', 'Children', domain=[ ('company', '=', Eval('company', -1)), ]) status = fields.Many2One( 'project.work.status', "Status", required=True, select=True, domain=[If(Bool(Eval('type')), ('types', 'in', Eval('type')), ())]) @staticmethod def default_type(): return 'task' @classmethod def default_company(cls): return Transaction().context.get('company') @classmethod def default_status(cls): pool = Pool() WorkStatus = pool.get('project.work.status') return WorkStatus.get_default_status(cls.default_type()) @classmethod def __register__(cls, module_name): TimesheetWork = Pool().get('timesheet.work') transaction = Transaction() cursor = transaction.connection.cursor() update = transaction.connection.cursor() table_project_work = cls.__table_handler__(module_name) project = cls.__table__() timesheet = TimesheetWork.__table__() work_exist = table_project_work.column_exist('work') add_parent = (not table_project_work.column_exist('parent') and work_exist) add_company = (not table_project_work.column_exist('company') and work_exist) add_name = (not table_project_work.column_exist('name') and work_exist) super(Work, cls).__register__(module_name) # Migration from 3.4: change effort into timedelta effort_duration if table_project_work.column_exist('effort'): cursor.execute(*project.select(project.id, project.effort, where=project.effort != Null)) for id_, effort in cursor: duration = datetime.timedelta(hours=effort) update.execute(*project.update( [project.effort_duration], [duration], where=project.id == id_)) table_project_work.drop_column('effort') # Migration from 3.6: add parent, company, drop required on work, # fill name if add_parent: second_project = cls.__table__() query = project.join(timesheet, condition=project.work == timesheet.id ).join(second_project, condition=timesheet.parent == second_project.work ).select(project.id, second_project.id) cursor.execute(*query) for id_, parent in cursor: update.execute(*project.update( [project.parent], [parent], where=project.id == id_)) cls._rebuild_tree('parent', None, 0) if add_company: cursor.execute(*project.join(timesheet, condition=project.work == timesheet.id ).select(project.id, timesheet.company)) for id_, company in cursor: update.execute(*project.update( [project.company], [company], where=project.id == id_)) table_project_work.not_null_action('work', action='remove') if add_name: cursor.execute(*project.join(timesheet, condition=project.work == timesheet.id ).select(project.id, timesheet.name)) for id_, name in cursor: update.execute(*project.update( [project.name], [name], where=project.id == id_)) # Migration from 4.0: remove work if work_exist: table_project_work.drop_constraint('work_uniq') cursor.execute(*project.select(project.id, project.work, where=project.work != Null)) for project_id, work_id in cursor: update.execute(*timesheet.update( [timesheet.origin, timesheet.name], ['%s,%s' % (cls.__name__, project_id), Null], where=timesheet.id == work_id)) table_project_work.drop_column('work') # Migration from 5.4: replace state by status table_project_work.not_null_action('state', action='remove') # Migration from 6.0: remove left and right table_project_work.drop_column('left') table_project_work.drop_column('right') @fields.depends('type', 'status') def on_change_type(self): pool = Pool() WorkState = pool.get('project.work.status') if (self.type and (not self.status or self.type not in self.status.types)): self.status = WorkState.get_default_status(self.type) @fields.depends('status', 'progress') def on_change_status(self): if (self.status and self.status.progress is not None and self.status.progress > (self.progress or -1.0)): self.progress = self.status.progress @classmethod def index_set_field(cls, name): index = super(Work, cls).index_set_field(name) if name in {'timesheet_start_date', 'timesheet_end_date'}: index = cls.index_set_field('timesheet_available') + 1 return index @classmethod def validate(cls, works): super(Work, cls).validate(works) for work in works: work.check_work_progress() def check_work_progress(self): pool = Pool() progress = -1 if self.progress is None else self.progress if (self.status.progress is not None and progress < self.status.progress): Lang = pool.get('ir.lang') lang = Lang.get() raise WorkProgressValidationError( gettext('project.msg_work_invalid_progress_status', work=self.rec_name, progress=lang.format('%.2f%%', self.status.progress * 100), status=self.status.rec_name)) if (self.status.progress == 1.0 and not all(c.progress == 1.0 for c in self.children)): raise WorkProgressValidationError( gettext('project.msg_work_children_progress', work=self.rec_name, status=self.status.rec_name)) if (self.parent and self.parent.progress == 1.0 and not self.progress == 1.0): raise WorkProgressValidationError( gettext('project.msg_work_parent_progress', work=self.rec_name, parent=self.parent.rec_name)) @property def effort_hours(self): if not self.effort_duration: return 0 return self.effort_duration.total_seconds() / 60 / 60 @property def total_effort_hours(self): if not self.total_effort: return 0 return self.total_effort.total_seconds() / 60 / 60 @property def timesheet_duration_hours(self): if not self.timesheet_duration: return 0 return self.timesheet_duration.total_seconds() / 60 / 60 @classmethod def default_timesheet_available(cls): return False def get_timesheet_available(self, name): return bool(self.timesheet_works) @classmethod def set_timesheet_available(cls, projects, name, value): pool = Pool() Timesheet = pool.get('timesheet.work') to_create = [] to_delete = [] for project in projects: if not project.timesheet_works and value: to_create.append({ 'origin': str(project), 'company': project.company.id, }) elif project.timesheet_works and not value: to_delete.extend(project.timesheet_works) if to_create: Timesheet.create(to_create) if to_delete: Timesheet.delete(to_delete) def get_timesheet_date(self, name): if self.timesheet_works: func = { 'timesheet_start_date': min, 'timesheet_end_date': max, }[name] return func(getattr(w, name) for w in self.timesheet_works) @classmethod def set_timesheet_date(cls, projects, name, value): pool = Pool() Timesheet = pool.get('timesheet.work') timesheets = [w for p in projects for w in p.timesheet_works] if timesheets: Timesheet.write(timesheets, { name: value, }) @classmethod def sum_tree(cls, works, values, parents): result = values.copy() works = set((w.id for w in works)) leafs = works - set(parents.values()) while leafs: for work in leafs: works.remove(work) parent = parents.get(work) if parent in result: result[parent] += result[work] next_leafs = set(works) for work in works: parent = parents.get(work) if not parent: continue if parent in next_leafs and parent in works: next_leafs.remove(parent) leafs = next_leafs return result @classmethod def get_total(cls, works, names): cursor = Transaction().connection.cursor() table = cls.__table__() works = cls.search([ ('parent', 'child_of', [w.id for w in works]), ]) work_ids = [w.id for w in works] parents = {} for sub_ids in grouped_slice(work_ids): where = reduce_ids(table.id, sub_ids) cursor.execute(*table.select(table.id, table.parent, where=where)) parents.update(cursor) if 'total_progress' in names and 'total_effort' not in names: names = list(names) names.append('total_effort') result = {} for name in names: values = getattr(cls, '_get_%s' % name)(works) result[name] = cls.sum_tree(works, values, parents) if 'total_progress' in names: digits = cls.total_progress.digits[1] total_progress = result['total_progress'] total_effort = result['total_effort'] for work in works: if total_effort[work.id]: total_progress[work.id] = round(total_progress[work.id] / (total_effort[work.id].total_seconds() / 60 / 60), digits) else: total_effort[work.id] = None return result @classmethod def _get_total_effort(cls, works): return {w.id: w.effort_duration or datetime.timedelta() for w in works} @classmethod def _get_timesheet_duration(cls, works): durations = {} for work in works: value = datetime.timedelta() for timesheet_work in work.timesheet_works: if timesheet_work.duration: value += timesheet_work.duration durations[work.id] = value return durations @classmethod def _get_total_progress(cls, works): return {w.id: w.effort_hours * (w.progress or 0) for w in works} @classmethod def copy(cls, project_works, default=None): pool = Pool() WorkStatus = pool.get('project.work.status') if default is None: default = {} else: default = default.copy() default.setdefault('children', None) default.setdefault('progress', None) default.setdefault( 'status', lambda data: WorkStatus.get_default_status(data['type'])) return super().copy(project_works, default=default) @classmethod def delete(cls, project_works): TimesheetWork = Pool().get('timesheet.work') # Get the timesheet works linked to the project works timesheet_works = [ w for pw in project_works for w in pw.timesheet_works] super(Work, cls).delete(project_works) if timesheet_works: with Transaction().set_context(_check_access=False): TimesheetWork.delete(timesheet_works) @classmethod def search_global(cls, text): for record, rec_name, icon in super(Work, cls).search_global(text): icon = icon or 'tryton-project' yield record, rec_name, icon
class Category(DeactivableMixin, tree(separator=' / '), ModelSQL, ModelView): "Category" __name__ = 'party.category' name = fields.Char('Name', required=True, states=STATES, translate=True, depends=DEPENDS, help="The main identifier of the category.") parent = fields.Many2One('party.category', 'Parent', select=True, states=STATES, depends=DEPENDS, help="Add the category below the parent.") childs = fields.One2Many('party.category', 'parent', 'Children', states=STATES, depends=DEPENDS, help="Add children below the category.") @classmethod def __setup__(cls): super(Category, cls).__setup__() t = cls.__table__() cls._sql_constraints = [ ('name_parent_exclude', Exclude(t, (t.name, Equal), (Coalesce(t.parent, -1), Equal)), 'party.msg_category_name_unique'), ] cls._order.insert(0, ('name', 'ASC')) @classmethod def __register__(cls, module_name): super(Category, cls).__register__(module_name) table_h = cls.__table_handler__(module_name) # Migration from 4.6: replace unique by exclude table_h.drop_constraint('name_parent_uniq') @classmethod def validate(cls, categories): super(Category, cls).validate(categories) cls.check_recursion(categories) for category in categories: category.check_name() def check_name(self): if SEPARATOR in self.name: self.raise_user_error('wrong_name', (self.name, )) def get_rec_name(self, name): if self.parent: return self.parent.get_rec_name(name) + SEPARATOR + self.name return self.name @classmethod def search_rec_name(cls, name, clause): if isinstance(clause[2], str): values = clause[2].split(SEPARATOR) values.reverse() domain = [] field = 'name' for name in values: domain.append((field, clause[1], name)) field = 'parent.' + field return domain # TODO Handle list return [('name', ) + tuple(clause[1:])]