class Template(ModelSQL, ModelView): "Product Template" __name__ = "product.template" name = fields.Char('Name', size=None, required=True, translate=True, select=True, states=STATES, depends=DEPENDS) type = fields.Selection(TYPES, 'Type', required=True, states=STATES, depends=DEPENDS) consumable = fields.Boolean('Consumable', states={ 'readonly': ~Eval('active', True), 'invisible': Eval('type', 'goods') != 'goods', }, depends=['active', 'type']) list_price = fields.Property(fields.Numeric('List Price', states=STATES, digits=price_digits, depends=DEPENDS, required=True)) cost_price = fields.Property(fields.Numeric('Cost Price', states=STATES, digits=price_digits, depends=DEPENDS, required=True)) cost_price_method = fields.Property(fields.Selection(COST_PRICE_METHODS, 'Cost Method', required=True, states=STATES, depends=DEPENDS)) default_uom = fields.Many2One('product.uom', 'Default UOM', required=True, states=STATES, depends=DEPENDS) default_uom_category = fields.Function( fields.Many2One('product.uom.category', 'Default UOM Category'), 'on_change_with_default_uom_category', searcher='search_default_uom_category') active = fields.Boolean('Active', select=True) categories = fields.Many2Many('product.template-product.category', 'template', 'category', 'Categories', states=STATES, depends=DEPENDS) products = fields.One2Many('product.product', 'template', 'Variants', states=STATES, depends=DEPENDS) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') cursor = Transaction().connection.cursor() sql_table = cls.__table__() super(Template, cls).__register__(module_name) table = TableHandler(cls, module_name) # Migration from 2.2: category is no more required table.not_null_action('category', 'remove') # Migration from 2.2: new types cursor.execute(*sql_table.update( columns=[sql_table.consumable], values=[True], where=sql_table.type == 'consumable')) cursor.execute(*sql_table.update( columns=[sql_table.type], values=['goods'], where=sql_table.type.in_(['stockable', 'consumable']))) # Migration from 3.8: rename category into categories if table.column_exist('category'): logger.warning( 'The column "category" on table "%s" must be dropped manually', cls._table) @staticmethod def default_active(): return True @staticmethod def default_type(): return 'goods' @staticmethod def default_consumable(): return False @staticmethod def default_products(): if Transaction().user == 0: return [] return [{}] @fields.depends('default_uom') def on_change_with_default_uom_category(self, name=None): if self.default_uom: return self.default_uom.category.id @classmethod def search_default_uom_category(cls, name, clause): return [('default_uom.category',) + tuple(clause[1:])] @classmethod def create(cls, vlist): vlist = [v.copy() for v in vlist] for values in vlist: values.setdefault('products', None) return super(Template, cls).create(vlist) @classmethod def search_global(cls, text): for record, rec_name, icon in super(Template, cls).search_global(text): icon = icon or 'tryton-product' yield record, rec_name, icon
class Production(Workflow, ModelSQL, ModelView): "Production" __name__ = 'production' _rec_name = 'number' number = fields.Char('Number', select=True, readonly=True) reference = fields.Char('Reference', select=1, states={ 'readonly': ~Eval('state').in_(['request', 'draft']), }, depends=['state']) planned_date = fields.Date('Planned Date', states={ 'readonly': ~Eval('state').in_(['request', 'draft']), }, depends=['state']) effective_date = fields.Date('Effective Date', states={ 'readonly': Eval('state').in_(['cancel', 'done']), }, depends=['state']) planned_start_date = fields.Date( 'Planned Start Date', states={ 'readonly': ~Eval('state').in_(['request', 'draft']), 'required': Bool(Eval('planned_date')), }, depends=['state', 'planned_date']) effective_start_date = fields.Date('Effective Start Date', states={ 'readonly': Eval('state').in_( ['cancel', 'running', 'done']), }, depends=['state']) company = fields.Many2One('company.company', 'Company', required=True, states={ 'readonly': ~Eval('state').in_(['request', 'draft']), }, depends=['state']) warehouse = fields.Many2One( 'stock.location', 'Warehouse', required=True, domain=[ ('type', '=', 'warehouse'), ], states={ 'readonly': (~Eval('state').in_(['request', 'draft']) | Eval('inputs', True) | Eval('outputs', True)), }, depends=['state']) location = fields.Many2One( 'stock.location', 'Location', required=True, domain=[ ('type', '=', 'production'), ], states={ 'readonly': (~Eval('state').in_(['request', 'draft']) | Eval('inputs', True) | Eval('outputs', True)), }, depends=['state']) product = fields.Many2One('product.product', 'Product', domain=[ ('producible', '=', True), ], states={ 'readonly': ~Eval('state').in_(['request', 'draft']), }) bom = fields.Many2One('production.bom', 'BOM', domain=[ ('output_products', '=', Eval('product', 0)), ], states={ 'readonly': (~Eval('state').in_(['request', 'draft']) | ~Eval('warehouse', 0) | ~Eval('location', 0)), 'invisible': ~Eval('product'), }, depends=['product']) uom_category = fields.Function( fields.Many2One('product.uom.category', 'Uom Category'), 'on_change_with_uom_category') uom = fields.Many2One('product.uom', 'Uom', domain=[ ('category', '=', Eval('uom_category')), ], states={ 'readonly': ~Eval('state').in_(['request', 'draft']), 'required': Bool(Eval('bom')), 'invisible': ~Eval('product'), }, depends=['uom_category']) unit_digits = fields.Function(fields.Integer('Unit Digits'), 'on_change_with_unit_digits') quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)), states={ 'readonly': ~Eval('state').in_(['request', 'draft']), 'required': Bool(Eval('bom')), 'invisible': ~Eval('product'), }, depends=['unit_digits']) cost = fields.Function( fields.Numeric('Cost', digits=price_digits, readonly=True), 'get_cost') inputs = fields.One2Many( 'stock.move', 'production_input', 'Inputs', domain=[ ('shipment', '=', None), ('from_location', 'child_of', [Eval('warehouse')], 'parent'), ('to_location', '=', Eval('location')), ('company', '=', Eval('company', -1)), ], states={ 'readonly': (~Eval('state').in_(['request', 'draft', 'waiting']) | ~Eval('warehouse') | ~Eval('location')), }, depends=['warehouse', 'location', 'company']) outputs = fields.One2Many('stock.move', 'production_output', 'Outputs', domain=[ ('shipment', '=', None), ('from_location', '=', Eval('location')), ('to_location', 'child_of', [Eval('warehouse')], 'parent'), ('company', '=', Eval('company', -1)), ], states={ 'readonly': (Eval('state').in_(['done', 'cancel']) | ~Eval('warehouse') | ~Eval('location')), }, depends=['warehouse', 'location', 'company']) state = fields.Selection([ ('request', 'Request'), ('draft', 'Draft'), ('waiting', 'Waiting'), ('assigned', 'Assigned'), ('running', 'Running'), ('done', 'Done'), ('cancel', 'Canceled'), ], 'State', readonly=True) @classmethod def __setup__(cls): super(Production, cls).__setup__() cls._error_messages.update({ 'uneven_costs': ('The costs of the outputs (%(outputs)s) of ' 'production "%(production)s" do not match the cost of the ' 'production (%(costs)s).') }) cls._transitions |= set(( ('request', 'draft'), ('draft', 'waiting'), ('waiting', 'assigned'), ('assigned', 'running'), ('running', 'done'), ('running', 'waiting'), ('assigned', 'waiting'), ('waiting', 'waiting'), ('waiting', 'draft'), ('request', 'cancel'), ('draft', 'cancel'), ('waiting', 'cancel'), ('assigned', 'cancel'), ('cancel', 'draft'), )) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['request', 'draft', 'assigned']), }, 'draft': { 'invisible': ~Eval('state').in_(['request', 'waiting', 'cancel']), 'icon': If( Eval('state') == 'cancel', 'tryton-clear', If( Eval('state') == 'request', 'tryton-go-next', 'tryton-go-previous')), }, 'reset_bom': { 'invisible': (~Eval('bom') | ~Eval('state').in_(['request', 'draft', 'waiting'])), }, 'wait': { 'invisible': ~Eval('state').in_(['draft', 'assigned', 'waiting', 'running' ]), 'icon': If( Eval('state').in_(['assigned', 'running']), 'tryton-go-previous', If( Eval('state') == 'waiting', 'tryton-clear', 'tryton-go-next')), }, 'run': { 'invisible': Eval('state') != 'assigned', }, 'done': { 'invisible': Eval('state') != 'running', }, 'assign_wizard': { 'invisible': Eval('state') != 'waiting', }, 'assign_try': {}, 'assign_force': {}, }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') table_h = TableHandler(cls, module_name) table = cls.__table__() # Migration from 3.8: rename code into number if table_h.column_exist('code'): table_h.column_rename('code', 'number') super(Production, cls).__register__(module_name) # Migration from 4.0: fill planned_start_date cursor = Transaction().connection.cursor() cursor.execute( *table.update([table.planned_start_date], [table.planned_date], where=(table.planned_start_date == Null) & (table.planned_date != Null))) @staticmethod def default_state(): return 'draft' @classmethod def default_warehouse(cls): Location = Pool().get('stock.location') locations = Location.search(cls.warehouse.domain) if len(locations) == 1: return locations[0].id @classmethod def default_location(cls): Location = Pool().get('stock.location') warehouse_id = cls.default_warehouse() if warehouse_id: warehouse = Location(warehouse_id) return warehouse.production_location.id @staticmethod def default_company(): return Transaction().context.get('company') @fields.depends('planned_date', 'product', 'bom') def on_change_with_planned_start_date(self, pattern=None): if self.planned_date and self.product: if pattern is None: pattern = {} pattern.setdefault('bom', self.bom.id if self.bom else None) for line in self.product.lead_times: if line.match(pattern): if line.lead_time: return self.planned_date - line.lead_time else: return self.planned_date return self.planned_date def _move(self, from_location, to_location, company, product, uom, quantity): Move = Pool().get('stock.move') move = Move( product=product, uom=uom, quantity=quantity, from_location=from_location, to_location=to_location, company=company, currency=company.currency if company else None, state='draft', ) return move def _explode_move_values(self, from_location, to_location, company, bom_io, quantity): move = self._move(from_location, to_location, company, bom_io.product, bom_io.uom, quantity) move.from_location = from_location.id if from_location else None move.to_location = to_location.id if to_location else None move.unit_price_required = move.on_change_with_unit_price_required() return move def explode_bom(self): pool = Pool() Uom = pool.get('product.uom') Move = pool.get('stock.move') if not (self.bom and self.product and self.uom): return self.cost = Decimal(0) if self.warehouse: storage_location = self.warehouse.storage_location else: storage_location = None factor = self.bom.compute_factor(self.product, self.quantity or 0, self.uom) inputs = [] for input_ in self.bom.inputs: quantity = input_.compute_quantity(factor) move = self._explode_move_values(storage_location, self.location, self.company, input_, quantity) if move: inputs.append(move) quantity = Uom.compute_qty(input_.uom, quantity, input_.product.default_uom, round=False) self.cost += (Decimal(str(quantity)) * input_.product.cost_price) self.inputs = inputs digits = self.__class__.cost.digits self.cost = self.cost.quantize(Decimal(str(10**-digits[1]))) digits = Move.unit_price.digits digit = Decimal(str(10**-digits[1])) outputs = [] for output in self.bom.outputs: quantity = output.compute_quantity(factor) move = self._explode_move_values(self.location, storage_location, self.company, output, quantity) if move: move.unit_price = Decimal(0) if output.product == move.product and quantity: move.unit_price = Decimal( self.cost / Decimal(str(quantity))).quantize(digit) outputs.append(move) self.outputs = outputs @fields.depends('warehouse') def on_change_warehouse(self): self.location = None if self.warehouse: self.location = self.warehouse.production_location @fields.depends(*BOM_CHANGES) def on_change_product(self): if self.product: category = self.product.default_uom.category if not self.uom or self.uom.category != category: self.uom = self.product.default_uom self.unit_digits = self.product.default_uom.digits else: self.bom = None self.uom = None self.unit_digits = 2 self.explode_bom() @fields.depends('product') def on_change_with_uom_category(self, name=None): if self.product: return self.product.default_uom.category.id @fields.depends('uom') def on_change_with_unit_digits(self, name=None): if self.uom: return self.uom.digits return 2 @fields.depends(*BOM_CHANGES) def on_change_bom(self): self.explode_bom() @fields.depends(*BOM_CHANGES) def on_change_uom(self): self.explode_bom() @fields.depends(*BOM_CHANGES) def on_change_quantity(self): self.explode_bom() @ModelView.button_change(*BOM_CHANGES) def reset_bom(self): self.explode_bom() def get_cost(self, name): cost = Decimal(0) for input_ in self.inputs: if input_.cost_price is not None: cost_price = input_.cost_price else: cost_price = input_.product.cost_price cost += (Decimal(str(input_.internal_quantity)) * cost_price) digits = self.__class__.cost.digits return cost.quantize(Decimal(str(10**-digits[1]))) @fields.depends('inputs') def on_change_with_cost(self): Uom = Pool().get('product.uom') cost = Decimal(0) if not self.inputs: return cost for input_ in self.inputs: if (input_.product is None or input_.uom is None or input_.quantity is None): continue product = input_.product quantity = Uom.compute_qty(input_.uom, input_.quantity, product.default_uom) cost += Decimal(str(quantity)) * product.cost_price return cost def set_moves(self): pool = Pool() Move = pool.get('stock.move') storage_location = self.warehouse.storage_location location = self.location company = self.company if not self.bom: if self.product: move = self._move(location, storage_location, company, self.product, self.uom, self.quantity) if move: move.production_output = self move.unit_price = Decimal(0) move.save() self._set_move_planned_date() return factor = self.bom.compute_factor(self.product, self.quantity, self.uom) cost = Decimal(0) for input_ in self.bom.inputs: quantity = input_.compute_quantity(factor) product = input_.product move = self._move(storage_location, location, company, product, input_.uom, quantity) if move: move.production_input = self move.save() cost += (Decimal(str(move.internal_quantity)) * product.cost_price) digits = self.__class__.cost.digits cost = cost.quantize(Decimal(str(10**-digits[1]))) digits = Move.unit_price.digits digit = Decimal(str(10**-digits[1])) for output in self.bom.outputs: quantity = output.compute_quantity(factor) product = output.product move = self._move(location, storage_location, company, product, output.uom, quantity) if move: move.production_output = self if product == self.product: move.unit_price = Decimal( cost / Decimal(str(quantity))).quantize(digit) else: move.unit_price = Decimal(0) move.save() self._set_move_planned_date() @classmethod def validate(cls, productions): super(Production, cls).validate(productions) for production in productions: production.check_cost() def check_cost(self): if self.state != 'done': return cost_price = Decimal(0) for output in self.outputs: cost_price += (Decimal(str(output.quantity)) * output.unit_price) if not self.company.currency.is_zero(self.cost - cost_price): self.raise_user_error( 'uneven_costs', { 'production': self.rec_name, 'costs': self.cost, 'outputs': cost_price, }) @classmethod def create(cls, vlist): Sequence = Pool().get('ir.sequence') Config = Pool().get('production.configuration') vlist = [x.copy() for x in vlist] config = Config(1) for values in vlist: if values.get('number') is None: values['number'] = Sequence.get_id( config.production_sequence.id) productions = super(Production, cls).create(vlist) for production in productions: production._set_move_planned_date() return productions @classmethod def write(cls, *args): super(Production, cls).write(*args) for production in sum(args[::2], []): production._set_move_planned_date() @classmethod def copy(cls, productions, default=None): if default is None: default = {} default = default.copy() default.setdefault('number', None) return super(Production, cls).copy(productions, default=default) def _get_move_planned_date(self): "Return the planned dates for input and output moves" return self.planned_start_date, self.planned_date def _set_move_planned_date(self): "Set planned date of moves for the shipments" pool = Pool() Move = pool.get('stock.move') dates = self._get_move_planned_date() input_date, output_date = dates Move.write([ m for m in self.inputs if m.state not in ('assigned', 'done', 'cancel') ], { 'planned_date': input_date, }) Move.write([ m for m in self.outputs if m.state not in ('assigned', 'done', 'cancel') ], { 'planned_date': output_date, }) @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, productions): pool = Pool() Move = pool.get('stock.move') Move.cancel([m for p in productions for m in p.inputs + p.outputs]) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, productions): pool = Pool() Move = pool.get('stock.move') Move.draft([m for p in productions for m in p.inputs + p.outputs]) @classmethod @ModelView.button @Workflow.transition('waiting') def wait(cls, productions): pool = Pool() Move = pool.get('stock.move') Move.draft([m for p in productions for m in p.inputs + p.outputs]) @classmethod @Workflow.transition('assigned') def assign(cls, productions): pass @classmethod @ModelView.button @Workflow.transition('running') def run(cls, productions): pool = Pool() Move = pool.get('stock.move') Date = pool.get('ir.date') Move.do([m for p in productions for m in p.inputs]) cls.write([p for p in productions if not p.effective_start_date], { 'effective_start_date': Date.today(), }) @classmethod @ModelView.button @Workflow.transition('done') def done(cls, productions): pool = Pool() Move = pool.get('stock.move') Date = pool.get('ir.date') Move.do([m for p in productions for m in p.outputs]) cls.write([p for p in productions if not p.effective_date], { 'effective_date': Date.today(), }) @classmethod @ModelView.button_action('production.wizard_assign') def assign_wizard(self, productions): pass @classmethod @ModelView.button def assign_try(cls, productions): pool = Pool() Move = pool.get('stock.move') if Move.assign_try([m for p in productions for m in p.inputs]): cls.assign(productions) return True else: return False @classmethod @ModelView.button def assign_force(cls, productions): pool = Pool() Move = pool.get('stock.move') Move.assign([m for p in productions for m in p.inputs]) cls.assign(productions)
class Product(ModelSQL, ModelView): "Product Variant" __name__ = "product.product" _order_name = 'rec_name' template = fields.Many2One('product.template', 'Product Template', required=True, ondelete='CASCADE', select=True, states=STATES, depends=DEPENDS) code = fields.Char("Code", size=None, select=True, states=STATES, depends=DEPENDS) description = fields.Text("Description", translate=True, states=STATES, depends=DEPENDS) active = fields.Boolean('Active', select=True) list_price_uom = fields.Function(fields.Numeric('List Price', digits=price_digits), 'get_price_uom') cost_price_uom = fields.Function(fields.Numeric('Cost Price', digits=price_digits), 'get_price_uom') @classmethod def __setup__(cls): pool = Pool() Template = pool.get('product.template') if not hasattr(cls, '_no_template_field'): cls._no_template_field = set() cls._no_template_field.update(['products']) super(Product, cls).__setup__() for attr in dir(Template): tfield = getattr(Template, attr) if not isinstance(tfield, fields.Field): continue if attr in cls._no_template_field: continue field = getattr(cls, attr, None) if not field or isinstance(field, TemplateFunction): setattr(cls, attr, TemplateFunction(copy.deepcopy(tfield))) order_method = getattr(cls, 'order_%s' % attr, None) if (not order_method and not isinstance(tfield, ( fields.Function, fields.One2Many, fields.Many2Many))): order_method = TemplateFunction.order(attr) setattr(cls, 'order_%s' % attr, order_method) @fields.depends('template') def on_change_template(self): for name, field in self._fields.iteritems(): if isinstance(field, TemplateFunction): if self.template: value = getattr(self.template, name, None) else: value = None setattr(self, name, value) def get_template(self, name): value = getattr(self.template, name) if isinstance(value, Model): return value.id elif (isinstance(value, (list, tuple)) and value and isinstance(value[0], Model)): return [r.id for r in value] else: return value @classmethod def search_template(cls, name, clause): return [('template.%s' % name,) + tuple(clause[1:])] @classmethod def order_rec_name(cls, tables): pool = Pool() Template = pool.get('product.template') product, _ = tables[None] if 'template' not in tables: template = Template.__table__() tables['template'] = { None: (template, product.template == template.id), } else: template = tables['template'] return [product.code] + Template.name.convert_order('name', tables['template'], Template) @staticmethod def default_active(): return True def get_rec_name(self, name): if self.code: return '[' + self.code + '] ' + self.name else: return self.name @classmethod def search_rec_name(cls, name, clause): if clause[1].startswith('!') or clause[1].startswith('not '): bool_op = 'AND' else: bool_op = 'OR' return [bool_op, ('code',) + tuple(clause[1:]), ('template.name',) + tuple(clause[1:]), ] @staticmethod def get_price_uom(products, name): Uom = Pool().get('product.uom') res = {} field = name[:-4] if Transaction().context.get('uom'): to_uom = Uom(Transaction().context['uom']) for product in products: res[product.id] = Uom.compute_price( product.default_uom, getattr(product, field), to_uom) else: for product in products: res[product.id] = getattr(product, field) return res @classmethod def search_global(cls, text): for id_, rec_name, icon in super(Product, cls).search_global(text): icon = icon or 'tryton-product' yield id_, rec_name, icon
class View(ModelSQL, ModelView): "View" __name__ = 'ir.ui.view' _rec_name = 'model' model = fields.Char('Model', select=True, states={ 'required': Eval('type').in_([None, 'tree', 'form', 'graph']), }) priority = fields.Integer('Priority', required=True, select=True) type = fields.Selection([ (None, ''), ('tree', 'Tree'), ('form', 'Form'), ('graph', 'Graph'), ('calendar', 'Calendar'), ('board', 'Board'), ], 'View Type', select=True, domain=[ If(Bool(Eval('inherit')), ('type', '=', None), ('type', '!=', None)), ], depends=['inherit']) data = fields.Text('Data') name = fields.Char('Name', states={ 'invisible': ~(Eval('module') & Eval('name')), }, depends=['module'], readonly=True) arch = fields.Function(fields.Text('View Architecture', states={ 'readonly': Bool(Eval('name')), }, depends=['name']), 'get_arch', setter='set_arch') inherit = fields.Many2One('ir.ui.view', 'Inherited View', select=True, ondelete='CASCADE') field_childs = fields.Char('Children Field', states={ 'invisible': Eval('type') != 'tree', }, depends=['type']) module = fields.Char('Module', states={ 'invisible': ~Eval('module'), }, readonly=True) domain = fields.Char('Domain', states={ 'invisible': ~Eval('inherit'), }, depends=['inherit']) _get_rng_cache = Cache('ir_ui_view.get_rng') @classmethod def __setup__(cls): super(View, cls).__setup__() cls._error_messages.update({ 'invalid_xml': 'Invalid XML for view "%s".', }) cls._order.insert(0, ('priority', 'ASC')) cls._buttons.update({ 'show': { 'readonly': Eval('type') != 'form', }, }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') table = TableHandler(cls, module_name) # Migration from 2.4 arch moved into data if table.column_exist('arch'): table.column_rename('arch', 'data') super(View, cls).__register__(module_name) # New instance to refresh definition table = TableHandler(cls, module_name) # Migration from 1.0 arch no more required table.not_null_action('arch', action='remove') # Migration from 2.4 model no more required table.not_null_action('model', action='remove') @staticmethod def default_priority(): return 16 @staticmethod def default_module(): return Transaction().context.get('module') or '' @classmethod @ModelView.button_action('ir.act_view_show') def show(cls, views): pass @classmethod def get_rng(cls, type_): key = (cls.__name__, type_) rng = cls._get_rng_cache.get(key) if rng is None: if sys.version_info < (3, ): filename = __file__.decode(sys.getfilesystemencoding()) else: filename = __file__ rng_name = os.path.join(os.path.dirname(filename), type_ + '.rng') with open(rng_name, 'rb') as fp: rng = etree.fromstring(fp.read()) cls._get_rng_cache.set(key, rng) return rng @property def rng_type(self): if self.inherit: return self.inherit.rng_type return self.type @classmethod def validate(cls, views): super(View, cls).validate(views) cls.check_xml(views) @classmethod def check_xml(cls, views): "Check XML" for view in views: if not view.arch: continue xml = view.arch.strip() if not xml: continue tree = etree.fromstring(xml) if hasattr(etree, 'RelaxNG'): validator = etree.RelaxNG(etree=cls.get_rng(view.rng_type)) if not validator.validate(tree): error_log = '\n'.join( map(str, validator.error_log.filter_from_errors())) logger.error('Invalid XML view %s:\n%s\n%s', view.rec_name, error_log, xml) cls.raise_user_error('invalid_xml', (view.rec_name, ), error_log) root_element = tree.getroottree().getroot() # validate pyson attributes validates = { 'states': fields.states_validate, } def encode(element): for attr in ('states', 'domain', 'spell'): if not element.get(attr): continue try: value = PYSONDecoder().decode(element.get(attr)) validates.get(attr, lambda a: True)(value) except Exception, e: error_log = '%s: <%s %s="%s"/>' % ( e, element.get('id') or element.get('name'), attr, element.get(attr)) logger.error('Invalid XML view %s:\n%s\n%s', view.rec_name, error_log, xml) cls.raise_user_error('invalid_xml', (view.rec_name, ), error_log) for child in element: encode(child) encode(root_element)
class ViewTreeState(ModelSQL, ModelView): 'View Tree State' __name__ = 'ir.ui.view_tree_state' _rec_name = 'model' model = fields.Char('Model', required=True) domain = fields.Char('Domain', required=True) user = fields.Many2One('res.user', 'User', required=True, ondelete='CASCADE') child_name = fields.Char('Child Name') nodes = fields.Text('Expanded Nodes') selected_nodes = fields.Text('Selected Nodes') @classmethod def __setup__(cls): super(ViewTreeState, cls).__setup__() cls.__rpc__.update({ 'set': RPC(readonly=False, check_access=False), 'get': RPC(check_access=False), }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') table = TableHandler(cls, module_name) # Migration from 2.8: table name changed table.table_rename('ir_ui_view_tree_expanded_state', cls._table) super(ViewTreeState, cls).__register__(module_name) table = TableHandler(cls, module_name) table.index_action(['model', 'domain', 'user', 'child_name'], 'add') @staticmethod def default_nodes(): return '[]' @staticmethod def default_selected_nodes(): return '[]' @classmethod def set(cls, model, domain, child_name, nodes, selected_nodes): # Normalize the json domain domain = json.dumps(json.loads(domain), separators=(',', ':')) current_user = Transaction().user records = cls.search([ ('user', '=', current_user), ('model', '=', model), ('domain', '=', domain), ('child_name', '=', child_name), ]) cls.delete(records) cls.create([{ 'user': current_user, 'model': model, 'domain': domain, 'child_name': child_name, 'nodes': nodes, 'selected_nodes': selected_nodes, }]) @classmethod def get(cls, model, domain, child_name): # Normalize the json domain domain = json.dumps(json.loads(domain), separators=(',', ':')) current_user = Transaction().user try: expanded_info, = cls.search([ ('user', '=', current_user), ('model', '=', model), ('domain', '=', domain), ('child_name', '=', child_name), ], limit=1) except ValueError: return (cls.default_nodes(), cls.default_selected_nodes()) state = cls(expanded_info) return (state.nodes or cls.default_nodes(), state.selected_nodes or cls.default_selected_nodes())
class Menu(ModelSQL, ModelView): "Nereid CMS Menu" __name__ = 'nereid.cms.menu' name = fields.Char('Name', required=True, on_change=['name', 'unique_identifier'], depends=['name', 'unique_identifier']) unique_identifier = fields.Char('Unique Identifier', required=True, select=True) description = fields.Text('Description') website = fields.Many2One('nereid.website', 'WebSite') active = fields.Boolean('Active') model = fields.Many2One('ir.model', 'Tryton Model', required=True) children_field = fields.Many2One('ir.model.field', 'Children', depends=['model'], domain=[('model', '=', Eval('model')), ('ttype', '=', 'one2many')], required=True) uri_field = fields.Many2One('ir.model.field', 'URI Field', depends=['model'], domain=[('model', '=', Eval('model')), ('ttype', '=', 'char')], required=True) title_field = fields.Many2One('ir.model.field', 'Title Field', depends=['model'], domain=[('model', '=', Eval('model')), ('ttype', '=', 'char')], required=True) identifier_field = fields.Many2One('ir.model.field', 'Identifier Field', depends=['model'], domain=[('model', '=', Eval('model')), ('ttype', '=', 'char')], required=True) @staticmethod def default_active(): """ By Default the Menu is active """ return True @classmethod def __setup__(cls): super(Menu, cls).__setup__() cls._sql_constraints += [ ('unique_identifier', 'UNIQUE(unique_identifier, website)', 'The Unique Identifier of the Menu must be unique.'), ] def _menu_item_to_dict(self, menu_item): """ :param menu_item: Active record of the menu item """ if hasattr(menu_item, 'reference') and getattr(menu_item, 'reference'): model, id = getattr(menu_item, 'reference').split(',') if int(id): reference, = Pool().get(model)(int(id)) uri = url_for('%s.render' % reference.__name__, uri=reference.uri) else: uri = getattr(menu_item, self.uri_field.name) else: uri = getattr(menu_item, self.uri_field.name) return { 'name': getattr(menu_item, self.title_field.name), 'uri': uri, } def _generate_menu_tree(self, menu_item): """ :param menu_item: Active record of the root menu_item """ result = {'children': []} result.update(self._menu_item_to_dict(menu_item)) # If children exist iteratively call _generate_.. children = getattr(menu_item, self.children_field.name) if children: for child in children: result['children'].append(self._generate_menu_tree(child)) return result @classmethod def menu_for(cls, identifier, ident_field_value, objectified=False): """ Returns a dictionary of menu tree :param identifier: The unique identifier from which the menu has to be chosen :param ident_field_value: The value of the field that has to be looked up on model with search on ident_field :param objectified: The value returned is the active record of the menu identified rather than a tree. """ # First pick up the menu through identifier try: menu, = cls.search([ ('unique_identifier', '=', identifier), ('website', '=', request.nereid_website.id), ]) except ValueError: current_app.logger.error("Menu %s could not be identified" % identifier) abort(404) # Get the data from the model MenuItem = Pool().get(menu.model.model) try: root_menu_item, = MenuItem.search( [(menu.identifier_field.name, '=', ident_field_value)], limit=1) except ValueError: current_app.logger.error("Menu %s could not be identified" % ident_field_value) abort(500) if objectified: return root_menu_item cache_key = key_from_list([ Transaction().cursor.dbname, Transaction().user, Transaction().language, identifier, ident_field_value, 'nereid.cms.menu.menu_for', ]) rv = cache.get(cache_key) if rv is None: rv = menu._generate_menu_tree(root_menu_item) cache.set(cache_key, rv, 60 * 60) return rv def on_change_name(self): res = {} if self.name and not self.unique_identifier: res['unique_identifier'] = slugify(self.name) return res @classmethod def context_processor(cls): """This function will be called by nereid to update the template context. Must return a dictionary that the context will be updated with. This function is registered with nereid.template.context_processor in xml code """ return {'menu_for': cls.menu_for}
class AdvancePaymentCondition(ModelSQL, ModelView): "Advance Payment Condition" __name__ = 'sale.advance_payment.condition' _rec_name = 'description' _states = { 'readonly': Eval('sale_state') != 'draft', } _depends = ['sale_state'] sale = fields.Many2One('sale.sale', 'Sale', required=True, ondelete='CASCADE', select=True, states={ 'readonly': ((Eval('sale_state') != 'draft') & Bool(Eval('sale'))), }, depends=['sale_state']) description = fields.Char( "Description", required=True, states=_states, depends=_depends) amount = fields.Numeric( "Amount", digits=(16, Eval('_parent_sale', {}).get('currency_digits', 2)), states=_states, depends=_depends) account = fields.Many2One( 'account.account', "Account", required=True, domain=[ ('kind', '=', 'revenue'), ('company', '=', Eval('sale_company')), ], states=_states, depends=_depends + ['sale_company']) block_supply = fields.Boolean( "Block Supply", states=_states, depends=_depends) block_shipping = fields.Boolean( "Block Shipping", states=_states, depends=_depends) invoice_delay = fields.TimeDelta( "Invoice Delay", states=_states, depends=_depends) invoice_lines = fields.One2Many( 'account.invoice.line', 'origin', "Invoice Lines", readonly=True) completed = fields.Function(fields.Boolean("Completed"), 'get_completed') sale_state = fields.Function(fields.Selection( 'get_sale_states', "Sale State"), 'on_change_with_sale_state') sale_company = fields.Function(fields.Many2One( 'company.company', "Company"), 'on_change_with_sale_company') del _states del _depends @classmethod def __setup__(cls): super(AdvancePaymentCondition, cls).__setup__() cls._order.insert(0, ('amount', 'ASC')) @classmethod def get_sale_states(cls): pool = Pool() Sale = pool.get('sale.sale') return Sale.fields_get(['state'])['state']['selection'] @fields.depends('sale', '_parent_sale.state') def on_change_with_sale_state(self, name=None): if self.sale: return self.sale.state @fields.depends('sale', '_parent_sale.company') def on_change_with_sale_company(self, name=None): if self.sale and self.sale.company: return self.sale.company.id @classmethod def copy(cls, conditions, default=None): if default is None: default = {} default['invoice_lines'] = [] return super(AdvancePaymentCondition, cls).copy(conditions, default) def create_invoice(self): pool = Pool() Invoice = pool.get('account.invoice') invoice = self.sale._get_invoice_sale() invoice.invoice_date = self.sale.sale_date if self.invoice_delay: invoice.invoice_date += self.invoice_delay invoice.payment_term = None invoice_lines = self.get_invoice_advance_payment_lines(invoice) if not invoice_lines: return None invoice.lines = invoice_lines invoice.save() Invoice.update_taxes([invoice]) return invoice def get_invoice_advance_payment_lines(self, invoice): pool = Pool() InvoiceLine = pool.get('account.invoice.line') advance_amount = self._get_advance_amount() advance_amount += self._get_ignored_amount() if advance_amount >= self.amount: return [] invoice_line = InvoiceLine() invoice_line.invoice = invoice invoice_line.company = invoice.company invoice_line.type = 'line' invoice_line.quantity = 1 invoice_line.account = self.account invoice_line.unit_price = self.amount - advance_amount invoice_line.description = self.description invoice_line.origin = self invoice_line.company = self.sale.company # Set taxes invoice_line.on_change_account() return [invoice_line] def _get_advance_amount(self): return sum(l.amount for l in self.invoice_lines if l.invoice.state != 'cancel') def _get_ignored_amount(self): skips = {l for i in self.sale.invoices_recreated for l in i.lines} return sum(l.amount for l in self.invoice_lines if l.invoice.state == 'cancel' and l not in skips) def get_completed(self, name): advance_amount = 0 lines_ignored = set(l for i in self.sale.invoices_ignored for l in i.lines) for l in self.invoice_lines: if l.invoice.state == 'paid' or l in lines_ignored: advance_amount += l.amount return advance_amount >= self.amount
class Article(Workflow, ModelSQL, ModelView): "CMS Articles" __name__ = 'nereid.cms.article' _rec_name = 'uri' uri = fields.Char('URI', required=True, select=True, translate=True) title = fields.Char('Title', required=True, select=True, translate=True) content = fields.Text('Content', required=True, translate=True) template = fields.Char('Template', required=True) active = fields.Boolean('Active', select=True) category = fields.Many2One('nereid.cms.article.category', 'Category', required=True, select=True) image = fields.Many2One('nereid.static.file', 'Image') employee = fields.Many2One('company.employee', 'Employee') author = fields.Many2One('nereid.user', 'Author') published_on = fields.Date('Published On') publish_date = fields.Function(fields.Char('Publish Date'), 'get_publish_date') sequence = fields.Integer('Sequence', required=True, select=True) reference = fields.Reference('Reference', selection='links_get') description = fields.Text('Short Description') attributes = fields.One2Many('nereid.cms.article.attribute', 'article', 'Attributes') # Article can have a banner banner = fields.Many2One('nereid.cms.banner', 'Banner') state = fields.Selection([('draft', 'Draft'), ('published', 'Published'), ('archived', 'Archived')], 'State', required=True, select=True, readonly=True) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') cursor = Transaction().cursor table = TableHandler(cursor, cls, module_name) if not table.column_exist('employee'): table.column_rename('author', 'employee') super(Article, cls).__register__(module_name) @classmethod def __setup__(cls): super(Article, cls).__setup__() cls._order.insert(0, ('sequence', 'ASC')) cls._transitions |= set(( ('draft', 'published'), ('archived', 'draft'), ('published', 'archived'), )) cls._buttons.update({ 'archive': { 'invisible': Eval('state') != 'published', }, 'publish': { 'invisible': Eval('state') == 'published', } }) @classmethod @ModelView.button @Workflow.transition('archived') def archive(cls, articles): pass @classmethod @ModelView.button @Workflow.transition('published') def publish(cls, articles): pass @staticmethod def links_get(): CMSLink = Pool().get('nereid.cms.link') return [('', '')] + [(x.model, x.name) for x in CMSLink.search([])] @staticmethod def default_active(): return True def on_change_title(self): res = {} if self.title and not self.uri: res['uri'] = slugify(self.title) return res @staticmethod def default_template(): return 'article.jinja' @staticmethod def default_employee(): User = Pool().get('res.user') if 'employee' in Transaction().context: return Transaction().context['employee'] user = User(Transaction().user) if user.employee: return user.employee.id if has_request_context() and request.nereid_user.employee: return request.nereid_user.employee.id @staticmethod def default_author(): if has_request_context(): return request.nereid_user.id @staticmethod def default_published_on(): Date = Pool().get('ir.date') return Date.today() @classmethod @route('/article/<uri>') def render(cls, uri): """ Renders the template """ try: article, = cls.search([('uri', '=', uri)]) except ValueError: abort(404) return render_template(article.template, article=article) @classmethod @route('/sitemaps/article-index.xml') def sitemap_index(cls): index = SitemapIndex(cls, []) return index.render() @classmethod @route('/sitemaps/article-<int:page>.xml') def sitemap(cls, page): sitemap_section = SitemapSection(cls, [], page) sitemap_section.changefreq = 'daily' return sitemap_section.render() @classmethod def get_publish_date(cls, records, name): """ Return publish date to render on view """ res = {} for record in records: res[record.id] = str(record.published_on) return res def get_absolute_url(self, **kwargs): return url_for('nereid.cms.article.render', uri=self.uri, **kwargs) @staticmethod def default_state(): if 'published' in Transaction().context: return 'published' return 'draft'
class ArticleCategory(ModelSQL, ModelView): "Article Categories" __name__ = 'nereid.cms.article.category' _rec_name = 'title' per_page = 10 title = fields.Char('Title', size=100, translate=True, required=True, on_change=['title', 'unique_name'], select=True) unique_name = fields.Char('Unique Name', required=True, select=True, help='Unique Name is used as the uri.') active = fields.Boolean('Active', select=True) description = fields.Text('Description', translate=True) template = fields.Char('Template', required=True) articles = fields.One2Many('nereid.cms.article', 'category', 'Articles', context={'published': True}) # Article Category can have a banner banner = fields.Many2One('nereid.cms.banner', 'Banner') sort_order = fields.Selection([ ('older_first', 'Older First'), ('recent_first', 'Recent First'), ], 'Sort Order') published_articles = fields.Function( fields.One2Many('nereid.cms.article', 'category', 'Published Articles'), 'get_published_articles') @staticmethod def default_sort_order(): return 'recent_first' @staticmethod def default_active(): 'Return True' return True @staticmethod def default_template(): return 'article-category.jinja' @classmethod def __setup__(cls): super(ArticleCategory, cls).__setup__() cls._sql_constraints += [ ('unique_name', 'UNIQUE(unique_name)', 'The Unique Name of the Category must be unique.'), ] def on_change_title(self): res = {} if self.title and not self.unique_name: res['unique_name'] = slugify(self.title) return res @classmethod @route('/article-category/<uri>/') @route('/article-category/<uri>/<int:page>') def render(cls, uri, page=1): """ Renders the category """ Article = Pool().get('nereid.cms.article') # Find in cache or load from DB try: category, = cls.search([('unique_name', '=', uri)]) except ValueError: abort(404) order = [] if category.sort_order == 'recent_first': order.append(('write_date', 'DESC')) elif category.sort_order == 'older_first': order.append(('write_date', 'ASC')) articles = Pagination(Article, [('category', '=', category.id)], page, cls.per_page, order=order) return render_template(category.template, category=category, articles=articles) @classmethod def get_article_category(cls, uri, silent=True): """Returns the browse record of the article category given by uri """ category = cls.search([('unique_name', '=', uri)], limit=1) if not category and not silent: raise RuntimeError("Article category %s not found" % uri) return category[0] if category else None @classmethod def context_processor(cls): """This function will be called by nereid to update the template context. Must return a dictionary that the context will be updated with. This function is registered with nereid.template.context_processor in xml code """ return {'get_article_category': cls.get_article_category} @classmethod @route('/sitemaps/article-category-index.xml') def sitemap_index(cls): index = SitemapIndex(cls, []) return index.render() @classmethod @route('/sitemaps/article-category-<int:page>.xml') def sitemap(cls, page): sitemap_section = SitemapSection(cls, [], page) sitemap_section.changefreq = 'daily' return sitemap_section.render() def get_absolute_url(self, **kwargs): return url_for('nereid.cms.article.category.render', uri=self.unique_name, **kwargs) def get_published_articles(self, name): """ Get the published articles. """ NereidArticle = Pool().get('nereid.cms.article') articles = NereidArticle.search([('state', '=', 'published'), ('category', '=', self.id)]) return map(int, articles)
class Banner(Workflow, ModelSQL, ModelView): """Banner for CMS.""" __name__ = 'nereid.cms.banner' name = fields.Char('Name', required=True, select=True) description = fields.Text('Description') category = fields.Many2One('nereid.cms.banner.category', 'Category', required=True, select=True) sequence = fields.Integer('Sequence', select=True) # Type related data type = fields.Selection([ ('image', 'Image'), ('remote_image', 'Remote Image'), ('custom_code', 'Custom Code'), ], 'Type', required=True) file = fields.Many2One('nereid.static.file', 'File', states={ 'required': Equal(Eval('type'), 'image'), 'invisible': Not(Equal(Eval('type'), 'image')) }) remote_image_url = fields.Char('Remote Image URL', states={ 'required': Equal(Eval('type'), 'remote_image'), 'invisible': Not(Equal(Eval('type'), 'remote_image')) }) custom_code = fields.Text('Custom Code', translate=True, states={ 'required': Equal(Eval('type'), 'custom_code'), 'invisible': Not(Equal(Eval('type'), 'custom_code')) }) # Presentation related Data height = fields.Integer( 'Height', states={'invisible': Not(In(Eval('type'), ['image', 'remote_image']))}) width = fields.Integer( 'Width', states={'invisible': Not(In(Eval('type'), ['image', 'remote_image']))}) alternative_text = fields.Char( 'Alternative Text', translate=True, states={'invisible': Not(In(Eval('type'), ['image', 'remote_image']))}) click_url = fields.Char( 'Click URL', translate=True, states={'invisible': Not(In(Eval('type'), ['image', 'remote_image']))}) state = fields.Selection([('draft', 'Draft'), ('published', 'Published'), ('archived', 'Archived')], 'State', required=True, select=True, readonly=True) reference = fields.Reference('Reference', selection='links_get') @classmethod def __setup__(cls): super(Banner, cls).__setup__() cls._order.insert(0, ('sequence', 'ASC')) cls._transitions |= set(( ('draft', 'published'), ('archived', 'published'), ('published', 'archived'), )) cls._buttons.update({ 'archive': { 'invisible': Eval('state') != 'published', }, 'publish': { 'invisible': Eval('state') == 'published', } }) @classmethod @ModelView.button @Workflow.transition('archived') def archive(cls, banners): pass @classmethod @ModelView.button @Workflow.transition('published') def publish(cls, banners): pass def get_html(self): """Return the HTML content""" StaticFile = Pool().get('nereid.static.file') banner = self.read([self], [ 'type', 'click_url', 'file', 'remote_image_url', 'custom_code', 'height', 'width', 'alternative_text', 'click_url' ])[0] if banner['type'] == 'image': # replace the `file` in the dictionary with the complete url # that is required to render the image based on static file file = StaticFile(banner['file']) banner['file'] = file.url image = Template(u'<a href="$click_url">' u'<img src="$file" alt="$alternative_text"' u' width="$width" height="$height"/>' u'</a>') return image.substitute(**banner) elif banner['type'] == 'remote_image': image = Template( u'<a href="$click_url">' u'<img src="$remote_image_url" alt="$alternative_text"' u' width="$width" height="$height"/>' u'</a>') return image.substitute(**banner) elif banner['type'] == 'custom_code': return banner['custom_code'] @staticmethod def links_get(): CMSLink = Pool().get('nereid.cms.link') return [('', '')] + [(x.model, x.name) for x in CMSLink.search([])] @staticmethod def default_type(): return 'image' @staticmethod def default_state(): if 'published' in Transaction().context: return 'published' return 'draft'
class MenuItem(ModelSQL, ModelView): "Nereid CMS Menuitem" __name__ = 'nereid.cms.menuitem' _rec_name = 'unique_name' title = fields.Char('Title', required=True, on_change=['title', 'unique_name'], select=True, translate=True) unique_name = fields.Char('Unique Name', required=True, select=True) link = fields.Char('Link') use_url_builder = fields.Boolean('Use URL Builder') url_for_build = fields.Many2One( 'nereid.url_rule', 'Rule', depends=['use_url_builder'], states={ 'required': Equal(Bool(Eval('use_url_builder')), True), 'invisible': Not(Equal(Bool(Eval('use_url_builder')), True)), }) values_to_build = fields.Char( 'Values', depends=['use_url_builder'], states={ 'required': Equal(Bool(Eval('use_url_builder')), True), 'invisible': Not(Equal(Bool(Eval('use_url_builder')), True)), }) full_url = fields.Function(fields.Char('Full URL'), 'get_full_url') parent = fields.Many2One( 'nereid.cms.menuitem', 'Parent Menuitem', ) child = fields.One2Many('nereid.cms.menuitem', 'parent', string='Child Menu Items') active = fields.Boolean('Active') sequence = fields.Integer('Sequence', required=True, select=True) reference = fields.Reference('Reference', selection='links_get') def get_full_url(self, name): # TODO return '' @staticmethod def links_get(): CMSLink = Pool().get('nereid.cms.link') links = [(x.model, x.name) for x in CMSLink.search([])] links.append([None, '']) return links @staticmethod def default_active(): return True @staticmethod def default_values_to_build(): return '{ }' @classmethod def __setup__(cls): super(MenuItem, cls).__setup__() cls._error_messages.update({ 'wrong_recursion': 'Error ! You can not create recursive menuitems.', }) cls._order.insert(0, ('sequence', 'ASC')) @classmethod def validate(cls, menus): super(MenuItem, cls).validate(menus) cls.check_recursion(menus) def on_change_title(self): res = {} if self.title and not self.unique_name: res['unique_name'] = slugify(self.title) return res def get_rec_name(self, name): def _name(menuitem): if menuitem.parent: return _name(menuitem.parent) + ' / ' + menuitem.title else: return menuitem.title return _name(self)
class Account(ModelSQL, ModelView): 'Analytic Account' __name__ = 'analytic_account.account' name = fields.Char('Name', required=True, translate=True, select=True) code = fields.Char('Code', select=True) active = fields.Boolean('Active', 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') display_balance = fields.Selection([ ('debit-credit', 'Debit - Credit'), ('credit-debit', 'Credit - Debit'), ], 'Display Balance', required=True) mandatory = fields.Boolean( 'Mandatory', states={ 'invisible': Eval('type') != 'root', }, depends=['type'], help="Make this account mandatory when filling documents") 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')) cls._error_messages.update({ 'invalid_distribution': ('The distribution sum of account "%(account)s" ' 'is not 100%.'), }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') super(Account, cls).__register__(module_name) table = TableHandler(cls, module_name) # Migration from 4.0: remove currency table.not_null_action('currency', action='remove') @staticmethod def default_active(): return True @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_type(): return 'normal' @staticmethod def default_state(): return 'draft' @staticmethod def default_display_balance(): return 'credit-debit' @staticmethod def default_mandatory(): return False @classmethod def validate(cls, accounts): super(Account, cls).validate(accounts) cls.check_recursion(accounts) cls.check_recursion(accounts, parent='distribution_parents') 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: self.raise_user_error('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 = {}.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.debit, 0) - Coalesce(line.credit, 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] if account.display_balance == 'credit-debit' and balance: balance *= -1 balances[account.id] = account.currency.round(balance) 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: result[name][account.id] = account.currency.round( result[name][account.id]) return result def get_rec_name(self, name): if self.code: return self.code + ' - ' + unicode(self.name) else: return unicode(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