class WebsiteResPartner(osv.Model): _name = 'res.partner' _inherit = [ 'res.partner', 'website.seo.metadata', 'website.published.mixin' ] def _get_ids(self, cr, uid, ids, flds, args, context=None): return {i: i for i in ids} def _set_private(self, cr, uid, ids, field_name, value, arg, context=None): return self.write(cr, uid, ids, {'website_published': not value}, context=context) def _get_private(self, cr, uid, ids, field_name, arg, context=None): return dict((rec.id, not rec.website_published) for rec in self.browse(cr, uid, ids, context=context)) def _search_private(self, cr, uid, obj, name, args, context=None): return [('website_published', '=', not args[0][2])] _columns = { 'website_private': fields.function(_get_private, fnct_inv=_set_private, fnct_search=_search_private, type='boolean', string='Private Profile'), 'website_description': fields.html('Website Partner Full Description', strip_style=True), 'website_short_description': fields.text('Website Partner Short Description'), # hack to allow using plain browse record in qweb views 'self': fields.function(_get_ids, type='many2one', relation=_name), } def _website_url(self, cr, uid, ids, field_name, arg, context=None): res = super(WebsiteResPartner, self)._website_url(cr, uid, ids, field_name, arg, context=context) for partner in self.browse(cr, uid, ids, context=context): res[partner.id] = "/partners/%s" % slug(partner) return res _defaults = { 'website_private': True, }
class res_partner(osv.osv): _name = 'res.partner' _inherit = 'res.partner' def _compute_payment_method_count(self, cr, uid, ids, field_names, arg, context=None): result = {} payment_data = self.pool['payment.method'].read_group( cr, uid, [('partner_id', 'in', ids)], ['partner_id'], ['partner_id'], context=context) mapped_data = dict([(payment['partner_id'][0], payment['partner_id_count']) for payment in payment_data]) for partner in self.browse(cr, uid, ids, context=context): result[partner.id] = mapped_data.get(partner.id, 0) return result _columns = { 'payment_method_ids': fields.one2many('payment.method', 'partner_id', 'Payment Methods'), 'payment_method_count': fields.function(_compute_payment_method_count, string='Count Payment Method', type="integer"), }
class TestFunctionNoInfiniteRecursion(osv.Model): _name = 'test_old_api.function_noinfiniterecursion' def _compute_f1(self, cr, uid, ids, fname, arg, context=None): res = {} for tf in self.browse(cr, uid, ids, context=context): res[tf.id] = 'create' in tf.f0 and 'create' or 'write' cntobj = self.pool['test_old_api.function_counter'] cnt_id = self.pool['ir.model.data'].xmlid_to_res_id( cr, uid, 'test_new_api.c1') cntobj.write(cr, uid, cnt_id, {'access': datetime.datetime.now()}, context=context) return res _columns = { 'f0': fields.char('Char Field'), 'f1': fields.function(_compute_f1, type='char', string='Function Field', store=True), }
class product_product(osv.osv): _inherit = "product.product" def _bom_orders_count(self, cr, uid, ids, field_name, arg, context=None): Production = self.pool('mrp.production') res = {} for product_id in ids: res[product_id] = Production.search_count(cr,uid, [('product_id', '=', product_id)], context=context) return res _columns = { 'mo_count': fields.function(_bom_orders_count, string='# Manufacturing Orders', type='integer'), } def action_view_bom(self, cr, uid, ids, context=None): result = self.pool.get("product.template")._get_act_window_dict(cr, uid, 'mrp.product_open_bom', context=context) templates = [product.product_tmpl_id.id for product in self.browse(cr, uid, ids, context=context)] # bom specific to this variant or global to template context = { 'search_default_product_tmpl_id': templates[0], 'search_default_product_id': ids[0], 'default_product_tmpl_id': templates[0], 'default_product_id': ids[0], } result['context'] = str(context) return result
class project_issue(osv.osv): _inherit = 'project.issue' _description = 'project issue' def _hours_get(self, cr, uid, ids, field_names, args, context=None): res = {} for issue in self.browse(cr, uid, ids, context=context): res[issue.id] = {'progress': issue.task_id.progress or 0.0} return res def _get_issue_task(self, cr, uid, task_ids, context=None): return self.pool['project.issue'].search(cr, uid, [('task_id', 'in', task_ids)], context=context) _columns = { 'progress': fields.function(_hours_get, string='Progress (%)', multi='line_id', group_operator="avg", help="Computed as: Time Spent / Total Time.", store={ 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10), 'project.task': (_get_issue_task, ['progress'], 10), }), 'timesheet_ids': fields.one2many('account.analytic.line', 'issue_id', 'Timesheets'), 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'), } def on_change_project(self, cr, uid, ids, project_id, context=None): if not project_id: return {'value': {'analytic_account_id': False}} result = super(project_issue, self).on_change_project(cr, uid, ids, project_id, context=context) project = self.pool.get('project.project').browse(cr, uid, project_id, context=context) if 'value' not in result: result['value'] = {} account = project.analytic_account_id if account: result['value']['analytic_account_id'] = account.id return result
class MassMailingList(osv.Model): """Model of a contact list. """ _name = 'mail.mass_mailing.list' _order = 'name' _description = 'Mailing List' def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None): result = dict.fromkeys(ids, 0) Contacts = self.pool.get('mail.mass_mailing.contact') for group in Contacts.read_group(cr, uid, [('list_id', 'in', ids), ('opt_out', '!=', True)], ['list_id'], ['list_id'], context=context): result[group['list_id'][0]] = group['list_id_count'] return result _columns = { 'name': fields.char('Mailing List', required=True), 'active': fields.boolean('Active'), 'create_date': fields.datetime('Creation Date'), 'contact_nbr': fields.function( _get_contact_nbr, type='integer', string='Number of Contacts', ), 'popup_content': fields.html("Website Popup Content", translate=True, required=True, sanitize=False), 'popup_redirect_url': fields.char("Website Popup Redirect URL"), } def _get_default_popup_content(self, cr, uid, context=None): return """<div class="modal-header text-center"> <h3 class="modal-title mt8">YuanCloud Presents</h3> </div> <div class="o_popup_message"> <font>7</font> <strong>Business Hacks</strong> <span> to<br/>boost your marketing</span> </div> <p class="o_message_paragraph">Join our Marketing newsletter and get <strong>this white paper instantly</strong></p>""" _defaults = { 'active': True, 'popup_content': _get_default_popup_content, 'popup_redirect_url': '/', }
class ResCompany(osv.Model): _inherit = "res.company" def _get_paypal_account(self, cr, uid, ids, name, arg, context=None): Acquirer = self.pool['payment.acquirer'] company_id = self.pool['res.users'].browse( cr, uid, uid, context=context).company_id.id paypal_ids = Acquirer.search(cr, uid, [ ('website_published', '=', True), ('name', 'ilike', 'paypal'), ('company_id', '=', company_id), ], limit=1, context=context) if paypal_ids: paypal = Acquirer.browse(cr, uid, paypal_ids[0], context=context) return dict.fromkeys(ids, paypal.paypal_email_account) return dict.fromkeys(ids, False) def _set_paypal_account(self, cr, uid, id, name, value, arg, context=None): Acquirer = self.pool['payment.acquirer'] company_id = self.pool['res.users'].browse( cr, uid, uid, context=context).company_id.id paypal_account = self.browse(cr, uid, id, context=context).paypal_account paypal_ids = Acquirer.search( cr, uid, [ ('website_published', '=', True), ('paypal_email_account', '=', paypal_account), ('company_id', '=', company_id), ], context=context) if paypal_ids: Acquirer.write(cr, uid, paypal_ids, {'paypal_email_account': value}, context=context) return True _columns = { 'paypal_account': fields.function( _get_paypal_account, fnct_inv=_set_paypal_account, nodrop=True, type='char', string='Paypal Account', help= "Paypal username (usually email) for receiving online payments."), }
class product_template(osv.osv): _inherit = "product.template" def _bom_orders_count(self, cr, uid, ids, field_name, arg, context=None): Bom = self.pool('mrp.bom') res = {} for product_tmpl_id in ids: nb = Bom.search_count(cr, uid, [('product_tmpl_id', '=', product_tmpl_id)], context=context) res[product_tmpl_id] = { 'bom_count': nb, } return res def _bom_orders_count_mo(self, cr, uid, ids, name, arg, context=None): res = {} for product_tmpl_id in self.browse(cr, uid, ids): res[product_tmpl_id.id] = sum([p.mo_count for p in product_tmpl_id.product_variant_ids]) return res _columns = { 'bom_ids': fields.one2many('mrp.bom', 'product_tmpl_id','Bill of Materials'), 'bom_count': fields.function(_bom_orders_count, string='# Bill of Material', type='integer', multi="_bom_order_count"), 'mo_count': fields.function(_bom_orders_count_mo, string='# Manufacturing Orders', type='integer'), 'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days to produce this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added."), } _defaults = { 'produce_delay': 1, } def action_view_mos(self, cr, uid, ids, context=None): products = self._get_products(cr, uid, ids, context=context) result = self._get_act_window_dict(cr, uid, 'mrp.act_product_mrp_production', context=context) if len(ids) == 1 and len(products) == 1: result['context'] = "{'default_product_id': " + str(products[0]) + ", 'search_default_product_id': " + str(products[0]) + "}" else: result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]" result['context'] = "{}" return result
class res_partner(osv.osv): def _task_count(self, cr, uid, ids, field_name, arg, context=None): Task = self.pool['project.task'] return { partner_id: Task.search_count(cr,uid, [('partner_id', '=', partner_id)], context=context) for partner_id in ids } """ Inherits partner and adds Tasks information in the partner form """ _inherit = 'res.partner' _columns = { 'task_ids': fields.one2many('project.task', 'partner_id', 'Tasks'), 'task_count': fields.function(_task_count, string='# Tasks', type='integer'), }
class hr_employee(osv.osv): _name = "hr.employee" _description = "Employee" _inherit = "hr.employee" def attachment_tree_view(self, cr, uid, ids, context): domain = [ '&', ('res_model', '=', 'hr.employee'), ('res_id', 'in', ids) ] res_id = ids and ids[0] or False return { 'name': _('Attachments'), 'domain': domain, 'res_model': 'ir.attachment', 'type': 'ir.actions.act_window', 'view_id': False, 'view_mode': 'kanban,tree,form', 'view_type': 'form', 'limit': 80, 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id) } def _get_attached_docs(self, cr, uid, ids, field_name, arg, context): res = {} attachment = self.pool.get('ir.attachment') for id in ids: employee_attachments = attachment.search( cr, uid, [('res_model', '=', 'hr.employee'), ('res_id', '=', id)], context=context, count=True) res[id] = employee_attachments or 0 return res _columns = { 'doc_count': fields.function(_get_attached_docs, string="Number of documents attached", type='integer') }
class res_partner(osv.osv): def _issue_count(self, cr, uid, ids, field_name, arg, context=None): Issue = self.pool['project.issue'] return { partner_id: Issue.search_count(cr, uid, [('partner_id', '=', partner_id)]) for partner_id in ids } """ Inherits partner and adds Issue information in the partner form """ _inherit = 'res.partner' _columns = { 'issue_count': fields.function(_issue_count, string='# Issues', type='integer'), }
class TestFunctionCounter(osv.Model): _name = 'test_old_api.function_counter' def _compute_cnt(self, cr, uid, ids, fname, arg, context=None): res = {} for cnt in self.browse(cr, uid, ids, context=context): res[cnt.id] = cnt.access and cnt.cnt + 1 or 0 return res _columns = { 'access': fields.datetime('Datetime Field'), 'cnt': fields.function(_compute_cnt, type='integer', string='Function Field', store=True), }
class hr_employee(osv.osv): _name = "hr.employee" _description = "Employee" _inherit = "hr.employee" def _trainings_count(self, cr, uid, ids, field_name, arg, context=None): Training = self.pool['hr.training.record'] sql_str = '''select rel.*,emp.* from training_employee_rel as rel join hr_employee as emp on rel.employee_id = emp.id where emp.id=%s ''' % (ids[0]) # _logger.info('根据培训课程ID,查找讲师=%s' % sql_str) self._cr.execute(sql_str) res = self._cr.fetchall() return {ids[0]:len(res)} _columns = { 'trainings_count': fields.function(_trainings_count, type='integer', string='Trainings'), }
class ir_attachment(osv.osv): _inherit = "ir.attachment" def _local_url_get(self, cr, uid, ids, name, arg, context=None): result = {} for attach in self.browse(cr, uid, ids, context=context): if attach.url: result[attach.id] = attach.url else: result[attach.id] = '/web/image/%s?unique=%s' % ( attach.id, attach.checksum) return result _columns = { 'local_url': fields.function(_local_url_get, string="Attachment URL", type='char'), }
class product_product(Model): _inherit = 'product.product' def _get_supplier_goodies_ids(self, cr, uid, ids, name, arg, context=None): if context is None: context = {} if context.get('date'): date = context['date'] else: date = datetime.today().strftime('%Y-%m-%d') res = {} link_obj = self.pool.get('product.link') for product_id in ids: res[product_id] = link_obj.search( cr, uid, [ ['product_id', '=', ids[0]], ['type', '=', 'goodies'], '|', ['start_date', '<=', date], ['start_date', '=', False], '|', ['end_date', '>=', date], ['end_date', '=', False], ['supplier_goodies', '=', True], ], context=context) return res _columns = { 'supplier_goodies_ids': fields.function(_get_supplier_goodies_ids, type='many2many', relation="product.link"), } def is_purchase_goodies(self, cr, uid, ids, context=None): return self.pool.get('product.link').search( cr, uid, [ ['linked_product_id', '=', ids[0]], ['type', '=', 'goodies'], ['supplier_goodies', '=', True], ], context=context) and True or False
class project(osv.Model): _inherit = "project.project" def _get_alias_models(self, cr, uid, context=None): res = super(project, self)._get_alias_models(cr, uid, context=context) res.append(("project.issue", "Issues")) return res def _issue_count(self, cr, uid, ids, field_name, arg, context=None): Issue = self.pool['project.issue'] return { project_id: Issue.search_count(cr, uid, [('project_id', '=', project_id), ('stage_id.fold', '=', False)], context=context) for project_id in ids } _columns = { 'issue_count': fields.function( _issue_count, type='integer', string="Issues", ), 'issue_ids': fields.one2many('project.issue', 'project_id', string="Issues", domain=[('stage_id.fold', '=', False)]), } @api.multi def write(self, vals): res = super(project, self).write(vals) if 'active' in vals: # archiving/unarchiving a project does it on its issues, too issues = self.with_context(active_test=False).mapped('issue_ids') issues.write({'active': vals['active']}) return res
class website_pricelist(osv.Model): _name = 'website_pricelist' _description = 'Website Pricelist' def _get_display_name(self, cr, uid, ids, name, arg, context=None): result = {} for o in self.browse(cr, uid, ids, context=context): result[o.id] = _("Website Pricelist for %s") % o.pricelist_id.name return result _columns = { 'name': fields.function(_get_display_name, string='Pricelist Name', type="char"), 'website_id': fields.many2one('website', string="Website", required=True), 'selectable': fields.boolean('Selectable', help="Allow the end user to choose this price list"), 'pricelist_id': fields.many2one('product.pricelist', string='Pricelist'), 'country_group_ids': fields.many2many('res.country.group', 'res_country_group_website_pricelist_rel', 'website_pricelist_id', 'res_country_group_id', string='Country Groups'), } def clear_cache(self): # website._get_pl() is cached to avoid to recompute at each request the # list of available pricelists. So, we need to invalidate the cache when # we change the config of website price list to force to recompute. website = self.pool['website'] website._get_pl.clear_cache(website) def create(self, cr, uid, data, context=None): res = super(website_pricelist, self).create(cr, uid, data, context=context) self.clear_cache() return res def write(self, cr, uid, ids, data, context=None): res = super(website_pricelist, self).write(cr, uid, ids, data, context=context) self.clear_cache() return res def unlink(self, cr, uid, ids, context=None): res = super(website_pricelist, self).unlink(cr, uid, ids, context=context) self.clear_cache() return res
class res_partner(osv.osv): _inherit = 'res.partner' def _sale_order_count(self, cr, uid, ids, field_name, arg, context=None): res = dict(map(lambda x: (x, 0), ids)) # The current user may not have access rights for sale orders try: for partner in self.browse(cr, uid, ids, context): res[partner.id] = len(partner.sale_order_ids) + len( partner.mapped('child_ids.sale_order_ids')) except: pass return res _columns = { 'sale_order_count': fields.function(_sale_order_count, string='# of Sales Order', type='integer'), 'sale_order_ids': fields.one2many('sale.order', 'partner_id', 'Sales Order') }
class hr_employee(osv.osv): ''' Employee ''' _inherit = 'hr.employee' _description = 'Employee' def _timesheet_count(self, cr, uid, ids, field_name, arg, context=None): Sheet = self.pool['hr_timesheet_sheet.sheet'] return { employee_id: Sheet.search_count(cr, uid, [('employee_id', '=', employee_id)], context=context) for employee_id in ids } _columns = { 'timesheet_count': fields.function(_timesheet_count, type='integer', string='Timesheets'), }
class mrp_repair_fee(osv.osv, ProductChangeMixin): _name = 'mrp.repair.fee' _description = 'Repair Fees Line' def _amount_line(self, cr, uid, ids, field_name, arg, context=None): """ Calculates amount. @param field_name: Name of field. @param arg: Argument @return: Dictionary of values. """ res = {} tax_obj = self.pool.get('account.tax') cur_obj = self.pool.get('res.currency') for line in self.browse(cr, uid, ids, context=context): if line.to_invoice: cur = line.repair_id.pricelist_id.currency_id taxes = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, cur.id, line.product_uom_qty, line.product_id.id, line.repair_id.partner_id.id) res[line.id] = taxes['total_included'] else: res[line.id] = 0 return res _columns = { 'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', required=True, ondelete='cascade', select=True), 'name': fields.char('Description', select=True, required=True), 'product_id': fields.many2one('product.product', 'Product'), 'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True), 'price_unit': fields.float('Unit Price', required=True), 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True), 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits=0), 'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'), 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True, copy=False), 'to_invoice': fields.boolean('To Invoice'), 'invoiced': fields.boolean('Invoiced', readonly=True, copy=False), } _defaults = { 'to_invoice': lambda *a: True, }
class product_category(osv.osv): _inherit = 'product.category' def calculate_total_routes(self, cr, uid, ids, name, args, context=None): res = {} for categ in self.browse(cr, uid, ids, context=context): categ2 = categ routes = [x.id for x in categ.route_ids] while categ2.parent_id: categ2 = categ2.parent_id routes += [x.id for x in categ2.route_ids] res[categ.id] = routes return res _columns = { 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes', domain="[('product_categ_selectable', '=', True)]"), 'removal_strategy_id': fields.many2one( 'product.removal', 'Force Removal Strategy', help= "Set a specific removal strategy that will be used regardless of the source location for this product category" ), 'total_route_ids': fields.function(calculate_total_routes, relation='stock.location.route', type='many2many', string='Total routes', readonly=True), }
class BlogPost(osv.Model): _name = "blog.post" _description = "Blog Post" _inherit = ['mail.thread', 'website.seo.metadata', 'website.published.mixin'] _order = 'id DESC' _mail_post_access = 'read' def _website_url(self, cr, uid, ids, field_name, arg, context=None): res = super(BlogPost, self)._website_url(cr, uid, ids, field_name, arg, context=context) for blog_post in self.browse(cr, uid, ids, context=context): res[blog_post.id] = "/blog/%s/post/%s" % (slug(blog_post.blog_id), slug(blog_post)) return res def _compute_ranking(self, cr, uid, ids, name, arg, context=None): res = {} for blog_post in self.browse(cr, uid, ids, context=context): age = datetime.now() - datetime.strptime(blog_post.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT) res[blog_post.id] = blog_post.visits * (0.5+random.random()) / max(3, age.days) return res def _default_content(self, cr, uid, context=None): return ''' <div class="container"> <section class="mt16 mb16"> <p class="o_default_snippet_text">''' + _("Start writing here...") + '''</p> </section> </div> ''' _columns = { 'name': fields.char('Title', required=True, translate=True), 'subtitle': fields.char('Sub Title', translate=True), 'author_id': fields.many2one('res.partner', 'Author'), 'cover_properties': fields.text('Cover Properties'), 'blog_id': fields.many2one( 'blog.blog', 'Blog', required=True, ondelete='cascade', ), 'tag_ids': fields.many2many( 'blog.tag', string='Tags', ), 'content': fields.html('Content', translate=True, sanitize=False), 'website_message_ids': fields.one2many( 'mail.message', 'res_id', domain=lambda self: [ '&', '&', ('model', '=', self._name), ('message_type', '=', 'comment'), ('path', '=', False) ], string='Website Messages', help="Website communication history", ), # creation / update stuff 'create_date': fields.datetime( 'Created on', select=True, readonly=True, ), 'create_uid': fields.many2one( 'res.users', 'Author', select=True, readonly=True, ), 'write_date': fields.datetime( 'Last Modified on', select=True, readonly=True, ), 'write_uid': fields.many2one( 'res.users', 'Last Contributor', select=True, readonly=True, ), 'author_avatar': fields.related( 'author_id', 'image_small', string="Avatar", type="binary"), 'visits': fields.integer('No of Views'), 'ranking': fields.function(_compute_ranking, string='Ranking', type='float'), } _defaults = { 'name': '', 'content': _default_content, 'cover_properties': '{"background-image": "none", "background-color": "oe_none", "opacity": "0.6", "resize_class": ""}', 'author_id': lambda self, cr, uid, ctx=None: self.pool['res.users'].browse(cr, uid, uid, context=ctx).partner_id.id, } def html_tag_nodes(self, html, attribute=None, tags=None, context=None): """ Processing of html content to tag paragraphs and set them an unique ID. :return result: (html, mappin), where html is the updated html with ID and mapping is a list of (old_ID, new_ID), where old_ID is None is the paragraph is a new one. """ existing_attributes = [] mapping = [] if not html: return html, mapping if tags is None: tags = ['p'] if attribute is None: attribute = 'data-unique-id' # form a tree root = lxml.html.fragment_fromstring(html, create_parent='div') if not len(root) and root.text is None and root.tail is None: return html, mapping # check all nodes, replace : # - img src -> check URL # - a href -> check URL for node in root.iter(): if node.tag not in tags: continue ancestor_tags = [parent.tag for parent in node.iterancestors()] old_attribute = node.get(attribute) new_attribute = old_attribute if not new_attribute or (old_attribute in existing_attributes): if ancestor_tags: ancestor_tags.pop() counter = random.randint(10000, 99999) ancestor_tags.append('counter_%s' % counter) new_attribute = '/'.join(reversed(ancestor_tags)) node.set(attribute, new_attribute) existing_attributes.append(new_attribute) mapping.append((old_attribute, new_attribute)) html = lxml.html.tostring(root, pretty_print=False, method='html') # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that if html.startswith('<div>') and html.endswith('</div>'): html = html[5:-6] return html, mapping def _postproces_content(self, cr, uid, id, content=None, context=None): if content is None: content = self.browse(cr, uid, id, context=context).content if content is False: return content content, mapping = self.html_tag_nodes(content, attribute='data-chatter-id', tags=['p'], context=context) if id: # not creating existing = [x[0] for x in mapping if x[0]] msg_ids = self.pool['mail.message'].search(cr, SUPERUSER_ID, [ ('res_id', '=', id), ('model', '=', self._name), ('path', 'not in', existing), ('path', '!=', False) ], context=context) self.pool['mail.message'].unlink(cr, SUPERUSER_ID, msg_ids, context=context) return content def _check_for_publication(self, cr, uid, ids, vals, context=None): if vals.get('website_published'): base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') for post in self.browse(cr, uid, ids, context=context): post.blog_id.message_post( body='<p>%(post_publication)s <a href="%(base_url)s/blog/%(blog_slug)s/post/%(post_slug)s">%(post_link)s</a></p>' % { 'post_publication': _('A new post %s has been published on the %s blog.') % (post.name, post.blog_id.name), 'post_link': _('Click here to access the post.'), 'base_url': base_url, 'blog_slug': slug(post.blog_id), 'post_slug': slug(post), }, subtype='website_blog.mt_blog_blog_published') return True return False def create(self, cr, uid, vals, context=None): if context is None: context = {} if 'content' in vals: vals['content'] = self._postproces_content(cr, uid, None, vals['content'], context=context) create_context = dict(context, mail_create_nolog=True) post_id = super(BlogPost, self).create(cr, uid, vals, context=create_context) self._check_for_publication(cr, uid, [post_id], vals, context=context) return post_id def write(self, cr, uid, ids, vals, context=None): if isinstance(ids, (int, long)): ids = [ids] if 'content' in vals: vals['content'] = self._postproces_content(cr, uid, ids[0], vals['content'], context=context) result = super(BlogPost, self).write(cr, uid, ids, vals, context) self._check_for_publication(cr, uid, ids, vals, context=context) return result def get_access_action(self, cr, uid, ids, context=None): """ Override method that generated the link to access the document. Instead of the classic form view, redirect to the post on the website directly """ post = self.browse(cr, uid, ids[0], context=context) return { 'type': 'ir.actions.act_url', 'url': '/blog/%s/post/%s' % (post.blog_id.id, post.id), 'target': 'self', 'res_id': self.id, } def _notification_get_recipient_groups(self, cr, uid, ids, message, recipients, context=None): """ Override to set the access button: everyone can see an access button on their notification email. It will lead on the website view of the post. """ res = super(BlogPost, self)._notification_get_recipient_groups(cr, uid, ids, message, recipients, context=context) access_action = self._notification_link_helper('view', model=message.model, res_id=message.res_id) for category, data in res.iteritems(): res[category]['button_access'] = {'url': access_action, 'title': _('View Blog Post')} return res
class mrp_repair(osv.osv): _name = 'mrp.repair' _inherit = 'mail.thread' _description = 'Repair Order' def _amount_untaxed(self, cr, uid, ids, field_name, arg, context=None): """ Calculates untaxed amount. @param self: The object pointer @param cr: The current row, from the database cursor, @param uid: The current user ID for security checks @param ids: List of selected IDs @param field_name: Name of field. @param arg: Argument @param context: A standard dictionary for contextual values @return: Dictionary of values. """ res = {} cur_obj = self.pool.get('res.currency') for repair in self.browse(cr, uid, ids, context=context): res[repair.id] = 0.0 for line in repair.operations: res[repair.id] += line.price_subtotal for line in repair.fees_lines: res[repair.id] += line.price_subtotal cur = repair.pricelist_id.currency_id res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id]) return res def _amount_tax(self, cr, uid, ids, field_name, arg, context=None): """ Calculates taxed amount. @param field_name: Name of field. @param arg: Argument @return: Dictionary of values. """ res = {} #return {}.fromkeys(ids, 0) cur_obj = self.pool.get('res.currency') tax_obj = self.pool.get('account.tax') for repair in self.browse(cr, uid, ids, context=context): val = 0.0 cur = repair.pricelist_id.currency_id for line in repair.operations: #manage prices with tax included use compute_all instead of compute if line.to_invoice and line.tax_id: tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, cur, line.product_uom_qty, line.product_id.id, repair.partner_id.id) for c in tax_calculate['taxes']: val += c['amount'] for line in repair.fees_lines: if line.to_invoice and line.tax_id: tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, cur, line.product_uom_qty, line.product_id.id, repair.partner_id.id) for c in tax_calculate['taxes']: val += c['amount'] res[repair.id] = cur_obj.round(cr, uid, cur, val) return res def _amount_total(self, cr, uid, ids, field_name, arg, context=None): """ Calculates total amount. @param field_name: Name of field. @param arg: Argument @return: Dictionary of values. """ res = {} untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context=context) tax = self._amount_tax(cr, uid, ids, field_name, arg, context=context) cur_obj = self.pool.get('res.currency') for id in ids: repair = self.browse(cr, uid, id, context=context) cur = repair.pricelist_id.currency_id res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0)) return res def _get_default_address(self, cr, uid, ids, field_name, arg, context=None): res = {} partner_obj = self.pool.get('res.partner') for data in self.browse(cr, uid, ids, context=context): adr_id = False if data.partner_id: adr_id = partner_obj.address_get(cr, uid, [data.partner_id.id], ['contact'])['contact'] res[data.id] = adr_id return res def _get_lines(self, cr, uid, ids, context=None): return self.pool['mrp.repair'].search(cr, uid, [('operations', 'in', ids)], context=context) def _get_fee_lines(self, cr, uid, ids, context=None): return self.pool['mrp.repair'].search(cr, uid, [('fees_lines', 'in', ids)], context=context) _columns = { 'name': fields.char('Repair Reference', required=True, states={'confirmed': [('readonly', True)]}, copy=False), 'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}), 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'partner_id': fields.many2one('res.partner', 'Partner', select=True, help='Choose partner for whom the order will be invoiced and delivered.', states={'confirmed': [('readonly', True)]}), 'address_id': fields.many2one('res.partner', 'Delivery Address', domain="[('parent_id','=',partner_id)]", states={'confirmed': [('readonly', True)]}), 'default_address_id': fields.function(_get_default_address, type="many2one", relation="res.partner"), 'state': fields.selection([ ('draft', 'Quotation'), ('cancel', 'Cancelled'), ('confirmed', 'Confirmed'), ('under_repair', 'Under Repair'), ('ready', 'Ready to Repair'), ('2binvoiced', 'To be Invoiced'), ('invoice_except', 'Invoice Exception'), ('done', 'Repaired') ], 'Status', readonly=True, track_visibility='onchange', copy=False, help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed repair order. \ \n* The \'Confirmed\' status is used when a user confirms the repair order. \ \n* The \'Ready to Repair\' status is used to start to repairing, user can start repairing only after repair order is confirmed. \ \n* The \'To be Invoiced\' status is used to generate the invoice before or after repairing done. \ \n* The \'Done\' status is set when repairing is completed.\ \n* The \'Cancelled\' status is used when user cancel repair order.'), 'location_id': fields.many2one('stock.location', 'Current Location', select=True, required=True, readonly=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}), 'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, required=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}), 'lot_id': fields.many2one('stock.production.lot', 'Repaired Lot', domain="[('product_id','=', product_id)]", help="Products repaired are all belonging to this lot", oldname="prodlot_id"), 'guarantee_limit': fields.date('Warranty Expiration', states={'confirmed': [('readonly', True)]}), 'operations': fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft': [('readonly', False)]}, copy=True), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='Pricelist of the selected partner.'), 'partner_invoice_id': fields.many2one('res.partner', 'Invoicing Address'), 'invoice_method': fields.selection([ ("none", "No Invoice"), ("b4repair", "Before Repair"), ("after_repair", "After Repair") ], "Invoice Method", select=True, required=True, states={'draft': [('readonly', False)]}, readonly=True, help='Selecting \'Before Repair\' or \'After Repair\' will allow you to generate invoice before or after the repair is done respectively. \'No invoice\' means you don\'t want to generate invoice for this repair order.'), 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True, track_visibility="onchange", copy=False), 'move_id': fields.many2one('stock.move', 'Move', readonly=True, help="Move created by the repair order", track_visibility="onchange", copy=False), 'fees_lines': fields.one2many('mrp.repair.fee', 'repair_id', 'Fees', readonly=True, states={'draft': [('readonly', False)]}, copy=True), 'internal_notes': fields.text('Internal Notes'), 'quotation_notes': fields.text('Quotation Notes'), 'company_id': fields.many2one('res.company', 'Company'), 'invoiced': fields.boolean('Invoiced', readonly=True, copy=False), 'repaired': fields.boolean('Repaired', readonly=True, copy=False), 'amount_untaxed': fields.function(_amount_untaxed, string='Untaxed Amount', store={ 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10), 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10), 'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10), }), 'amount_tax': fields.function(_amount_tax, string='Taxes', store={ 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10), 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10), 'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10), }), 'amount_total': fields.function(_amount_total, string='Total', store={ 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10), 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10), 'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10), }), } def _default_stock_location(self, cr, uid, context=None): try: warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0') return warehouse.lot_stock_id.id except: return False _defaults = { 'state': lambda *a: 'draft', 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').next_by_code(cr, uid, 'mrp.repair'), 'invoice_method': lambda *a: 'none', 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.repair', context=context), 'pricelist_id': lambda self, cr, uid, context: self.pool['product.pricelist'].search(cr, uid, [], limit=1)[0], 'product_qty': 1.0, 'location_id': _default_stock_location, } _sql_constraints = [ ('name', 'unique (name)', 'The name of the Repair Order must be unique!'), ] def onchange_product_id(self, cr, uid, ids, product_id=None): """ On change of product sets some values. @param product_id: Changed product @return: Dictionary of values. """ product = False if product_id: product = self.pool.get("product.product").browse(cr, uid, product_id) return {'value': { 'guarantee_limit': False, 'lot_id': False, 'product_uom': product and product.uom_id.id or False, } } def onchange_product_uom(self, cr, uid, ids, product_id, product_uom, context=None): res = {'value': {}} if not product_uom or not product_id: return res product = self.pool.get('product.product').browse(cr, uid, product_id, context=context) uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context) if uom.category_id.id != product.uom_id.category_id.id: res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')} res['value'].update({'product_uom': product.uom_id.id}) return res def onchange_location_id(self, cr, uid, ids, location_id=None): """ On change of location """ return {'value': {'location_dest_id': location_id}} def button_dummy(self, cr, uid, ids, context=None): return True def onchange_partner_id(self, cr, uid, ids, part, address_id): """ On change of partner sets the values of partner address, partner invoice address and pricelist. @param part: Changed id of partner. @param address_id: Address id from current record. @return: Dictionary of values. """ part_obj = self.pool.get('res.partner') pricelist_obj = self.pool.get('product.pricelist') if not part: return {'value': { 'address_id': False, 'partner_invoice_id': False, 'pricelist_id': pricelist_obj.search(cr, uid, [], limit=1)[0] } } addr = part_obj.address_get(cr, uid, [part], ['delivery', 'invoice', 'contact']) partner = part_obj.browse(cr, uid, part) pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False return {'value': { 'address_id': addr['delivery'] or addr['contact'], 'partner_invoice_id': addr['invoice'], 'pricelist_id': pricelist } } def action_cancel_draft(self, cr, uid, ids, *args): """ Cancels repair order when it is in 'Draft' state. @param *arg: Arguments @return: True """ if not len(ids): return False mrp_line_obj = self.pool.get('mrp.repair.line') for repair in self.browse(cr, uid, ids): mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'}) self.write(cr, uid, ids, {'state': 'draft'}) return self.create_workflow(cr, uid, ids) def action_confirm(self, cr, uid, ids, *args): """ Repair order state is set to 'To be invoiced' when invoice method is 'Before repair' else state becomes 'Confirmed'. @param *arg: Arguments @return: True """ mrp_line_obj = self.pool.get('mrp.repair.line') for o in self.browse(cr, uid, ids): if (o.invoice_method == 'b4repair'): self.write(cr, uid, [o.id], {'state': '2binvoiced'}) else: self.write(cr, uid, [o.id], {'state': 'confirmed'}) for line in o.operations: if line.product_id.tracking != 'none' and not line.lot_id: raise UserError(_("Serial number is required for operation line with product '%s'") % (line.product_id.name)) mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'}) return True def action_cancel(self, cr, uid, ids, context=None): """ Cancels repair order. @return: True """ mrp_line_obj = self.pool.get('mrp.repair.line') for repair in self.browse(cr, uid, ids, context=context): if not repair.invoiced: mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'}, context=context) else: raise UserError(_('Repair order is already invoiced.')) return self.write(cr, uid, ids, {'state': 'cancel'}) def wkf_invoice_create(self, cr, uid, ids, *args): self.action_invoice_create(cr, uid, ids) return True def action_invoice_create(self, cr, uid, ids, group=False, context=None): """ Creates invoice(s) for repair order. @param group: It is set to true when group invoice is to be generated. @return: Invoice Ids. """ res = {} invoices_group = {} inv_line_obj = self.pool.get('account.invoice.line') inv_obj = self.pool.get('account.invoice') repair_line_obj = self.pool.get('mrp.repair.line') repair_fee_obj = self.pool.get('mrp.repair.fee') for repair in self.browse(cr, uid, ids, context=context): res[repair.id] = False if repair.state in ('draft', 'cancel') or repair.invoice_id: continue if not (repair.partner_id.id and repair.partner_invoice_id.id): raise UserError(_('You have to select a Partner Invoice Address in the repair form!')) comment = repair.quotation_notes if (repair.invoice_method != 'none'): if group and repair.partner_invoice_id.id in invoices_group: inv_id = invoices_group[repair.partner_invoice_id.id] invoice = inv_obj.browse(cr, uid, inv_id) invoice_vals = { 'name': invoice.name + ', ' + repair.name, 'origin': invoice.origin + ', ' + repair.name, 'comment': (comment and (invoice.comment and invoice.comment + "\n" + comment or comment)) or (invoice.comment and invoice.comment or ''), } inv_obj.write(cr, uid, [inv_id], invoice_vals, context=context) else: if not repair.partner_id.property_account_receivable_id: raise UserError(_('No account defined for partner "%s".') % repair.partner_id.name) account_id = repair.partner_id.property_account_receivable_id.id inv = { 'name': repair.name, 'origin': repair.name, 'type': 'out_invoice', 'account_id': account_id, 'partner_id': repair.partner_invoice_id.id or repair.partner_id.id, 'currency_id': repair.pricelist_id.currency_id.id, 'comment': repair.quotation_notes, 'fiscal_position_id': repair.partner_id.property_account_position_id.id } inv_id = inv_obj.create(cr, uid, inv) invoices_group[repair.partner_invoice_id.id] = inv_id self.write(cr, uid, repair.id, {'invoiced': True, 'invoice_id': inv_id}) for operation in repair.operations: if operation.to_invoice: if group: name = repair.name + '-' + operation.name else: name = operation.name if operation.product_id.property_account_income_id: account_id = operation.product_id.property_account_income_id.id elif operation.product_id.categ_id.property_account_income_categ_id: account_id = operation.product_id.categ_id.property_account_income_categ_id.id else: raise UserError(_('No account defined for product "%s".') % operation.product_id.name) invoice_line_id = inv_line_obj.create(cr, uid, { 'invoice_id': inv_id, 'name': name, 'origin': repair.name, 'account_id': account_id, 'quantity': operation.product_uom_qty, 'invoice_line_tax_ids': [(6, 0, [x.id for x in operation.tax_id])], 'uom_id': operation.product_uom.id, 'price_unit': operation.price_unit, 'price_subtotal': operation.product_uom_qty * operation.price_unit, 'product_id': operation.product_id and operation.product_id.id or False }) repair_line_obj.write(cr, uid, [operation.id], {'invoiced': True, 'invoice_line_id': invoice_line_id}) for fee in repair.fees_lines: if fee.to_invoice: if group: name = repair.name + '-' + fee.name else: name = fee.name if not fee.product_id: raise UserError(_('No product defined on Fees!')) if fee.product_id.property_account_income_id: account_id = fee.product_id.property_account_income_id.id elif fee.product_id.categ_id.property_account_income_categ_id: account_id = fee.product_id.categ_id.property_account_income_categ_id.id else: raise UserError(_('No account defined for product "%s".') % fee.product_id.name) invoice_fee_id = inv_line_obj.create(cr, uid, { 'invoice_id': inv_id, 'name': name, 'origin': repair.name, 'account_id': account_id, 'quantity': fee.product_uom_qty, 'invoice_line_tax_ids': [(6, 0, [x.id for x in fee.tax_id])], 'uom_id': fee.product_uom.id, 'product_id': fee.product_id and fee.product_id.id or False, 'price_unit': fee.price_unit, 'price_subtotal': fee.product_uom_qty * fee.price_unit }) repair_fee_obj.write(cr, uid, [fee.id], {'invoiced': True, 'invoice_line_id': invoice_fee_id}) #inv_obj.button_reset_taxes(cr, uid, inv_id, context=context) res[repair.id] = inv_id return res def action_repair_ready(self, cr, uid, ids, context=None): """ Writes repair order state to 'Ready' @return: True """ for repair in self.browse(cr, uid, ids, context=context): self.pool.get('mrp.repair.line').write(cr, uid, [l.id for l in repair.operations], {'state': 'confirmed'}, context=context) self.write(cr, uid, [repair.id], {'state': 'ready'}) return True def action_repair_start(self, cr, uid, ids, context=None): """ Writes repair order state to 'Under Repair' @return: True """ repair_line = self.pool.get('mrp.repair.line') for repair in self.browse(cr, uid, ids, context=context): repair_line.write(cr, uid, [l.id for l in repair.operations], {'state': 'confirmed'}, context=context) repair.write({'state': 'under_repair'}) return True def action_repair_end(self, cr, uid, ids, context=None): """ Writes repair order state to 'To be invoiced' if invoice method is After repair else state is set to 'Ready'. @return: True """ for order in self.browse(cr, uid, ids, context=context): val = {} val['repaired'] = True if (not order.invoiced and order.invoice_method == 'after_repair'): val['state'] = '2binvoiced' elif (not order.invoiced and order.invoice_method == 'b4repair'): val['state'] = 'ready' else: pass self.write(cr, uid, [order.id], val) return True def wkf_repair_done(self, cr, uid, ids, *args): self.action_repair_done(cr, uid, ids) return True def action_repair_done(self, cr, uid, ids, context=None): """ Creates stock move for operation and stock move for final product of repair order. @return: Move ids of final products """ res = {} move_obj = self.pool.get('stock.move') repair_line_obj = self.pool.get('mrp.repair.line') for repair in self.browse(cr, uid, ids, context=context): move_ids = [] for move in repair.operations: move_id = move_obj.create(cr, uid, { 'name': move.name, 'product_id': move.product_id.id, 'restrict_lot_id': move.lot_id.id, 'product_uom_qty': move.product_uom_qty, 'product_uom': move.product_uom.id, 'partner_id': repair.address_id and repair.address_id.id or False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, }) move_ids.append(move_id) repair_line_obj.write(cr, uid, [move.id], {'move_id': move_id, 'state': 'done'}, context=context) move_id = move_obj.create(cr, uid, { 'name': repair.name, 'product_id': repair.product_id.id, 'product_uom': repair.product_uom.id or repair.product_id.uom_id.id, 'product_uom_qty': repair.product_qty, 'partner_id': repair.address_id and repair.address_id.id or False, 'location_id': repair.location_id.id, 'location_dest_id': repair.location_dest_id.id, 'restrict_lot_id': repair.lot_id.id, }) move_ids.append(move_id) move_obj.action_done(cr, uid, move_ids, context=context) self.write(cr, uid, [repair.id], {'state': 'done', 'move_id': move_id}, context=context) res[repair.id] = move_id return res
class mrp_repair_line(osv.osv, ProductChangeMixin): _name = 'mrp.repair.line' _description = 'Repair Line' def _amount_line(self, cr, uid, ids, field_name, arg, context=None): """ Calculates amount. @param field_name: Name of field. @param arg: Argument @return: Dictionary of values. """ res = {} tax_obj = self.pool.get('account.tax') # cur_obj = self.pool.get('res.currency') for line in self.browse(cr, uid, ids, context=context): if line.to_invoice: cur = line.repair_id.pricelist_id.currency_id taxes = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, cur.id, line.product_uom_qty, line.product_id.id, line.repair_id.partner_id.id) #res[line.id] = cur_obj.round(cr, uid, cur, taxes['total']) res[line.id] = taxes['total_included'] else: res[line.id] = 0 return res _columns = { 'name': fields.char('Description', required=True), 'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', ondelete='cascade', select=True), 'type': fields.selection([('add', 'Add'), ('remove', 'Remove')], 'Type', required=True), 'to_invoice': fields.boolean('To Invoice'), 'product_id': fields.many2one('product.product', 'Product', required=True), 'invoiced': fields.boolean('Invoiced', readonly=True, copy=False), 'price_unit': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price')), 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits=0), 'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'), 'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True), 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True), 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True, copy=False), 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True), 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True), 'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True, copy=False), 'lot_id': fields.many2one('stock.production.lot', 'Lot'), 'state': fields.selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', required=True, readonly=True, copy=False, help=' * The \'Draft\' status is set automatically as draft when repair order in draft status. \ \n* The \'Confirmed\' status is set automatically as confirm when repair order in confirm status. \ \n* The \'Done\' status is set automatically when repair order is completed.\ \n* The \'Cancelled\' status is set automatically when user cancel repair order.'), } _defaults = { 'state': lambda *a: 'draft', 'product_uom_qty': lambda *a: 1, } def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit, company_id=False, context=None): """ On change of operation type it sets source location, destination location and to invoice field. @param product: Changed operation type. @param guarantee_limit: Guarantee limit of current record. @return: Dictionary of values. """ if not type: return {'value': { 'location_id': False, 'location_dest_id': False }} location_obj = self.pool.get('stock.location') warehouse_obj = self.pool.get('stock.warehouse') location_id = location_obj.search(cr, uid, [('usage', '=', 'production')], context=context) location_id = location_id and location_id[0] or False if type == 'add': # TOCHECK: Find stock location for user's company warehouse or # repair order's company's warehouse (company_id field is added in fix of lp:831583) args = company_id and [('company_id', '=', company_id)] or [] warehouse_ids = warehouse_obj.search(cr, uid, args, context=context) stock_id = False if warehouse_ids: stock_id = warehouse_obj.browse(cr, uid, warehouse_ids[0], context=context).lot_stock_id.id to_invoice = (guarantee_limit and datetime.strptime(guarantee_limit, '%Y-%m-%d') < datetime.now()) return {'value': { 'to_invoice': to_invoice, 'location_id': stock_id, 'location_dest_id': location_id }} scrap_location_ids = location_obj.search(cr, uid, [('scrap_location', '=', True)], context=context) return {'value': { 'to_invoice': False, 'location_id': location_id, 'location_dest_id': scrap_location_ids and scrap_location_ids[0] or False, }}
class hr_attendance(osv.osv): _inherit = "hr.attendance" def _get_default_date(self, cr, uid, context=None): if context is None: context = {} if 'name' in context: return context['name'] + time.strftime(' %H:%M:%S') return time.strftime('%Y-%m-%d %H:%M:%S') def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None): attendance_ids = [] for ts in self.browse(cr, uid, ids, context=context): cr.execute( """ SELECT a.id FROM hr_attendance a INNER JOIN hr_employee e INNER JOIN resource_resource r ON (e.resource_id = r.id) ON (a.employee_id = e.id) LEFT JOIN res_users u ON r.user_id = u.id LEFT JOIN res_partner p ON u.partner_id = p.id WHERE %(date_to)s >= date_trunc('day', a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC')) AND %(date_from)s <= date_trunc('day', a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC')) AND %(user_id)s = r.user_id GROUP BY a.id""", { 'date_from': ts.date_from, 'date_to': ts.date_to, 'user_id': ts.employee_id.user_id.id, }) attendance_ids.extend([row[0] for row in cr.fetchall()]) return attendance_ids def _get_attendance_employee_tz(self, cr, uid, employee_id, date, context=None): """ Simulate timesheet in employee timezone Return the attendance date in string format in the employee tz converted from utc timezone as we consider date of employee timesheet is in employee timezone """ employee_obj = self.pool['hr.employee'] tz = False if employee_id: employee = employee_obj.browse(cr, uid, employee_id, context=context) tz = employee.user_id.partner_id.tz if not date: date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) att_tz = timezone(tz or 'utc') attendance_dt = datetime.strptime(date, DEFAULT_SERVER_DATETIME_FORMAT) att_tz_dt = pytz.utc.localize(attendance_dt) att_tz_dt = att_tz_dt.astimezone(att_tz) # We take only the date omiting the hours as we compare with timesheet # date_from which is a date format thus using hours would lead to # be out of scope of timesheet att_tz_date_str = datetime.strftime(att_tz_dt, DEFAULT_SERVER_DATE_FORMAT) return att_tz_date_str def _get_current_sheet(self, cr, uid, employee_id, date=False, context=None): sheet_obj = self.pool['hr_timesheet_sheet.sheet'] if not date: date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) att_tz_date_str = self._get_attendance_employee_tz(cr, uid, employee_id, date=date, context=context) sheet_ids = sheet_obj.search(cr, uid, [('date_from', '<=', att_tz_date_str), ('date_to', '>=', att_tz_date_str), ('employee_id', '=', employee_id)], limit=1, context=context) return sheet_ids and sheet_ids[0] or False def _sheet(self, cursor, user, ids, name, args, context=None): res = {}.fromkeys(ids, False) for attendance in self.browse(cursor, user, ids, context=context): res[attendance.id] = self._get_current_sheet( cursor, user, attendance.employee_id.id, attendance.name, context=context) return res _columns = { 'sheet_id': fields.function( _sheet, string='Sheet', type='many2one', relation='hr_timesheet_sheet.sheet', store={ 'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10), 'hr.attendance': (lambda self, cr, uid, ids, context=None: ids, ['employee_id', 'name', 'day'], 10), }, ) } _defaults = { 'name': _get_default_date, } def create(self, cr, uid, vals, context=None): if context is None: context = {} sheet_id = context.get('sheet_id') or self._get_current_sheet( cr, uid, vals.get('employee_id'), vals.get('name'), context=context) if sheet_id: att_tz_date_str = self._get_attendance_employee_tz( cr, uid, vals.get('employee_id'), date=vals.get('name'), context=context) ts = self.pool.get('hr_timesheet_sheet.sheet').browse( cr, uid, sheet_id, context=context) if ts.state not in ('draft', 'new'): raise UserError( _('You can not enter an attendance in a submitted timesheet. Ask your manager to reset it before adding attendance.' )) elif ts.date_from > att_tz_date_str or ts.date_to < att_tz_date_str: raise UserError( _('You can not enter an attendance date outside the current timesheet dates.' )) return super(hr_attendance, self).create(cr, uid, vals, context=context) def unlink(self, cr, uid, ids, *args, **kwargs): if isinstance(ids, (int, long)): ids = [ids] self._check(cr, uid, ids) return super(hr_attendance, self).unlink(cr, uid, ids, *args, **kwargs) def write(self, cr, uid, ids, vals, context=None): if context is None: context = {} if isinstance(ids, (int, long)): ids = [ids] self._check(cr, uid, ids) res = super(hr_attendance, self).write(cr, uid, ids, vals, context=context) if 'sheet_id' in context: for attendance in self.browse(cr, uid, ids, context=context): if context['sheet_id'] != attendance.sheet_id.id: raise UserError(_('You cannot enter an attendance ' \ 'date outside the current timesheet dates.')) return res def _check(self, cr, uid, ids): for att in self.browse(cr, uid, ids): if att.sheet_id and att.sheet_id.state not in ('draft', 'new'): raise UserError( _('You cannot modify an entry in a confirmed timesheet')) return True
class task(osv.osv): _inherit = "project.task" # Compute: effective_hours, total_hours, progress def _hours_get(self, cr, uid, ids, field_names, args, context=None): res = {} tasks_data = self.pool['account.analytic.line'].read_group( cr, uid, [('task_id', 'in', ids)], ['task_id', 'unit_amount'], ['task_id'], context=context) for data in tasks_data: task = self.browse(cr, uid, data['task_id'][0], context=context) res[data['task_id'][0]] = { 'effective_hours': data.get('unit_amount', 0.0), 'remaining_hours': task.planned_hours - data.get('unit_amount', 0.0) } res[data['task_id'][0]]['total_hours'] = res[ data['task_id'][0]]['remaining_hours'] + data.get( 'unit_amount', 0.0) res[data['task_id'][0]]['delay_hours'] = res[ data['task_id'][0]]['total_hours'] - task.planned_hours res[data['task_id'][0]]['progress'] = 0.0 if (task.planned_hours > 0.0 and data.get('unit_amount', 0.0)): res[data['task_id'][0]]['progress'] = round( min( 100.0 * data.get('unit_amount', 0.0) / task.planned_hours, 99.99), 2) # TDE CHECK: if task.state in ('done','cancelled'): if task.stage_id and task.stage_id.fold: res[data['task_id'][0]]['progress'] = 100.0 return res def _get_task(self, cr, uid, id, context=None): res = [] for line in self.pool.get('account.analytic.line').search_read( cr, uid, [('task_id', '!=', False), ('id', 'in', id)], context=context): res.append(line['task_id'][0]) return res def _get_total_hours(self): return super(task, self)._get_total_hours() + self.effective_hours _columns = { 'remaining_hours': fields.function( _hours_get, string='Remaining Hours', multi='line_id', help= "Total remaining time, can be re-estimated periodically by the assignee of the task.", store={ 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), }), 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='line_id', help="Computed using the sum of the task work done.", store={ 'project.task': (lambda self, cr, uid, ids, c={}: ids, [ 'timesheet_ids', 'remaining_hours', 'planned_hours' ], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), }), 'total_hours': fields.function(_hours_get, string='Total', multi='line_id', help="Computed as: Time Spent + Remaining Time.", store={ 'project.task': (lambda self, cr, uid, ids, c={}: ids, [ 'timesheet_ids', 'remaining_hours', 'planned_hours' ], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), }), 'progress': fields.function( _hours_get, string='Working Time Progress (%)', multi='line_id', group_operator="avg", help= "If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time", store={ 'project.task': (lambda self, cr, uid, ids, c={}: ids, [ 'timesheet_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id' ], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), }), 'delay_hours': fields.function( _hours_get, string='Delay Hours', multi='line_id', help= "Computed as difference between planned hours by the project manager and the total hours of the task.", store={ 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10), 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10), }), 'timesheet_ids': fields.one2many('account.analytic.line', 'task_id', 'Timesheets'), 'analytic_account_id': fields.related('project_id', 'analytic_account_id', type='many2one', relation='account.analytic.account', string='Analytic Account', store=True), } _defaults = { 'progress': 0, } def _prepare_delegate_values(self, cr, uid, ids, delegate_data, context=None): vals = super(task, self)._prepare_delegate_values(cr, uid, ids, delegate_data, context) for task in self.browse(cr, uid, ids, context=context): vals[task.id]['planned_hours'] += task.effective_hours return vals def onchange_project(self, cr, uid, ids, project_id, context=None): result = super(task, self).onchange_project(cr, uid, ids, project_id, context=context) if not project_id: return result if 'value' not in result: result['value'] = {} project = self.pool['project.project'].browse(cr, uid, project_id, context=context) return result
class mrp_operations_operation(osv.osv): _name = "mrp_operations.operation" def _order_date_search_production(self, cr, uid, ids, context=None): """ Finds operations for a production order. @return: List of ids """ operation_ids = self.pool.get('mrp_operations.operation').search( cr, uid, [('production_id', '=', ids[0])], context=context) return operation_ids def _get_order_date(self, cr, uid, ids, field_name, arg, context=None): """ Calculates planned date for an operation. @return: Dictionary of values """ res = {} operation_obj = self.browse(cr, uid, ids, context=context) for operation in operation_obj: res[operation.id] = operation.production_id.date_planned return res def calc_delay(self, cr, uid, vals): """ Calculates delay of work order. @return: Delay """ code_lst = [] time_lst = [] code_ids = self.pool.get('mrp_operations.operation.code').search( cr, uid, [('id', '=', vals['code_id'])]) code = self.pool.get('mrp_operations.operation.code').browse( cr, uid, code_ids)[0] oper_ids = self.search(cr, uid, [('production_id', '=', vals['production_id']), ('workcenter_id', '=', vals['workcenter_id'])]) oper_objs = self.browse(cr, uid, oper_ids) for oper in oper_objs: code_lst.append(oper.code_id.start_stop) time_lst.append(oper.date_start) code_lst.append(code.start_stop) time_lst.append(vals['date_start']) diff = 0 for i in range(0, len(code_lst)): if code_lst[i] == 'pause' or code_lst[i] == 'done' or code_lst[ i] == 'cancel': if not i: continue if code_lst[i - 1] not in ('resume', 'start'): continue a = datetime.strptime(time_lst[i - 1], '%Y-%m-%d %H:%M:%S') b = datetime.strptime(time_lst[i], '%Y-%m-%d %H:%M:%S') diff += (b - a).days * 24 diff += (b - a).seconds / float(60 * 60) return diff def check_operation(self, cr, uid, vals): """ Finds which operation is called ie. start, pause, done, cancel. @param vals: Dictionary of values. @return: True or False """ code_ids = self.pool.get('mrp_operations.operation.code').search( cr, uid, [('id', '=', vals['code_id'])]) code = self.pool.get('mrp_operations.operation.code').browse( cr, uid, code_ids)[0] code_lst = [] oper_ids = self.search(cr, uid, [('production_id', '=', vals['production_id']), ('workcenter_id', '=', vals['workcenter_id'])]) oper_objs = self.browse(cr, uid, oper_ids) if not oper_objs: if code.start_stop != 'start': raise UserError(_('Operation is not started yet!')) return False else: for oper in oper_objs: code_lst.append(oper.code_id.start_stop) if code.start_stop == 'start': if 'start' in code_lst: raise UserError( _('Operation has already started! You can either Pause/Finish/Cancel the operation.' )) return False if code.start_stop == 'pause': if code_lst[len(code_lst) - 1] != 'resume' and code_lst[ len(code_lst) - 1] != 'start': raise UserError( _('In order to Pause the operation, it must be in the Start or Resume state!' )) return False if code.start_stop == 'resume': if code_lst[len(code_lst) - 1] != 'pause': raise UserError( _('In order to Resume the operation, it must be in the Pause state!' )) return False if code.start_stop == 'done': if code_lst[len(code_lst) - 1] != 'start' and code_lst[ len(code_lst) - 1] != 'resume': raise UserError( _('In order to Finish the operation, it must be in the Start or Resume state!' )) return False if 'cancel' in code_lst: raise UserError(_('Operation is Already Cancelled!')) return False if code.start_stop == 'cancel': if not 'start' in code_lst: raise UserError(_('No operation to cancel.')) return False if 'done' in code_lst: raise UserError(_('Operation is already finished!')) return False return True def write(self, cr, uid, ids, vals, context=None): oper_objs = self.browse(cr, uid, ids, context=context)[0] vals['production_id'] = oper_objs.production_id.id vals['workcenter_id'] = oper_objs.workcenter_id.id if 'code_id' in vals: self.check_operation(cr, uid, vals) if 'date_start' in vals: vals['date_start'] = vals['date_start'] vals['code_id'] = oper_objs.code_id.id delay = self.calc_delay(cr, uid, vals) wc_op_id = self.pool.get('mrp.production.workcenter.line').search( cr, uid, [('workcenter_id', '=', vals['workcenter_id']), ('production_id', '=', vals['production_id'])]) self.pool.get('mrp.production.workcenter.line').write( cr, uid, wc_op_id, {'delay': delay}) return super(mrp_operations_operation, self).write(cr, uid, ids, vals, context=context) def create(self, cr, uid, vals, context=None): workcenter_pool = self.pool.get('mrp.production.workcenter.line') code_ids = self.pool.get('mrp_operations.operation.code').search( cr, uid, [('id', '=', vals['code_id'])]) code = self.pool.get('mrp_operations.operation.code').browse( cr, uid, code_ids, context=context)[0] wc_op_id = workcenter_pool.search( cr, uid, [('workcenter_id', '=', vals['workcenter_id']), ('production_id', '=', vals['production_id'])]) if code.start_stop in ('start', 'done', 'pause', 'cancel', 'resume'): if not wc_op_id: production_obj = self.pool.get('mrp.production').browse( cr, uid, vals['production_id'], context=context) wc_op_id.append( workcenter_pool.create( cr, uid, { 'production_id': vals['production_id'], 'name': production_obj.product_id.name, 'workcenter_id': vals['workcenter_id'] })) if code.start_stop == 'start': workcenter_pool.action_start_working(cr, uid, wc_op_id) workcenter_pool.signal_workflow(cr, uid, [wc_op_id[0]], 'button_start_working') if code.start_stop == 'done': workcenter_pool.action_done(cr, uid, wc_op_id) workcenter_pool.signal_workflow(cr, uid, [wc_op_id[0]], 'button_done') self.pool.get('mrp.production').write( cr, uid, vals['production_id'], { 'date_finished': datetime.now().strftime('%Y-%m-%d %H:%M:%S') }) if code.start_stop == 'pause': workcenter_pool.action_pause(cr, uid, wc_op_id) workcenter_pool.signal_workflow(cr, uid, [wc_op_id[0]], 'button_pause') if code.start_stop == 'resume': workcenter_pool.action_resume(cr, uid, wc_op_id) workcenter_pool.signal_workflow(cr, uid, [wc_op_id[0]], 'button_resume') if code.start_stop == 'cancel': workcenter_pool.action_cancel(cr, uid, wc_op_id) workcenter_pool.signal_workflow(cr, uid, [wc_op_id[0]], 'button_cancel') if not self.check_operation(cr, uid, vals): return delay = self.calc_delay(cr, uid, vals) line_vals = {} line_vals['delay'] = delay if vals.get('date_start', False): if code.start_stop == 'done': line_vals['date_finished'] = vals['date_start'] elif code.start_stop == 'start': line_vals['date_start'] = vals['date_start'] self.pool.get('mrp.production.workcenter.line').write(cr, uid, wc_op_id, line_vals, context=context) return super(mrp_operations_operation, self).create(cr, uid, vals, context=context) def initialize_workflow_instance(self, cr, uid, context=None): mrp_production_workcenter_line = self.pool.get( 'mrp.production.workcenter.line') line_ids = mrp_production_workcenter_line.search(cr, uid, [], context=context) mrp_production_workcenter_line.create_workflow(cr, uid, line_ids) return True _columns = { 'production_id': fields.many2one('mrp.production', 'Production', required=True), 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True), 'code_id': fields.many2one('mrp_operations.operation.code', 'Code', required=True), 'date_start': fields.datetime('Start Date'), 'date_finished': fields.datetime('End Date'), 'order_date': fields.function(_get_order_date, string='Order Date', type='date', store={ 'mrp.production': (_order_date_search_production, ['date_planned'], 10) }), } _defaults = { 'date_start': lambda *a: datetime.now().strftime('%Y-%m-%d %H:%M:%S') }
class mrp_production_workcenter_line(osv.osv): def _get_date_end(self, cr, uid, ids, field_name, arg, context=None): """ Finds ending date. @return: Dictionary of values. """ ops = self.browse(cr, uid, ids, context=context) date_and_hours_by_cal = [(op.date_planned, op.hour, op.workcenter_id.calendar_id.id) for op in ops if op.date_planned] intervals = self.pool.get('resource.calendar').interval_get_multi( cr, uid, date_and_hours_by_cal) res = {} for op in ops: res[op.id] = False if op.date_planned: i = intervals.get((op.date_planned, op.hour, op.workcenter_id.calendar_id.id)) if i: res[op.id] = i[-1][1].strftime('%Y-%m-%d %H:%M:%S') else: res[op.id] = op.date_planned return res def onchange_production_id(self, cr, uid, ids, production_id, context=None): if not production_id: return {} production = self.pool.get('mrp.production').browse(cr, uid, production_id, context=None) result = { 'product': production.product_id.id, 'qty': production.product_qty, 'uom': production.product_uom.id, } return {'value': result} _inherit = 'mrp.production.workcenter.line' _order = "sequence, date_planned" _columns = { 'state': fields.selection([('draft','Draft'),('cancel','Cancelled'),('pause','Pending'),('startworking', 'In Progress'),('done','Finished')],'Status', readonly=True, copy=False, help="* When a work order is created it is set in 'Draft' status.\n" \ "* When user sets work order in start mode that time it will be set in 'In Progress' status.\n" \ "* When work order is in running mode, during that time if user wants to stop or to make changes in order then can set in 'Pending' status.\n" \ "* When the user cancels the work order it will be set in 'Canceled' status.\n" \ "* When order is completely processed that time it is set in 'Finished' status."), 'date_planned': fields.datetime('Scheduled Date', select=True), 'date_planned_end': fields.function(_get_date_end, string='End Date', type='datetime'), 'date_start': fields.datetime('Start Date'), 'date_finished': fields.datetime('End Date'), 'delay': fields.float('Working Hours',help="The elapsed time between operation start and stop in this Work Center",readonly=True), 'production_state':fields.related('production_id','state', type='selection', selection=[('draft','Draft'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','In Production'),('cancel','Canceled'),('done','Done')], string='Production Status', readonly=True), 'product':fields.related('production_id','product_id',type='many2one',relation='product.product',string='Product', readonly=True), 'qty':fields.related('production_id','product_qty',type='float',string='Qty',readonly=True, store=True), 'uom':fields.related('production_id','product_uom',type='many2one',relation='product.uom',string='Unit of Measure',readonly=True), } _defaults = {'state': 'draft', 'delay': 0.0, 'production_state': 'draft'} def modify_production_order_state(self, cr, uid, ids, action): """ Modifies production order state if work order state is changed. @param action: Action to perform. @return: Nothing """ prod_obj_pool = self.pool.get('mrp.production') oper_obj = self.browse(cr, uid, ids)[0] prod_obj = oper_obj.production_id if action == 'start': if prod_obj.state == 'confirmed': prod_obj_pool.force_production(cr, uid, [prod_obj.id]) prod_obj_pool.signal_workflow(cr, uid, [prod_obj.id], 'button_produce') elif prod_obj.state == 'ready': prod_obj_pool.signal_workflow(cr, uid, [prod_obj.id], 'button_produce') elif prod_obj.state == 'in_production': return else: raise UserError( _('Manufacturing order cannot be started in state "%s"!') % (prod_obj.state, )) else: open_count = self.search_count( cr, uid, [('production_id', '=', prod_obj.id), ('state', '!=', 'done')]) flag = not bool(open_count) if flag: button_produce_done = True for production in prod_obj_pool.browse(cr, uid, [prod_obj.id], context=None): if production.move_lines or production.move_created_ids: moves = production.move_lines + production.move_created_ids # If tracking is activated, we want to make sure the user will enter the # serial numbers. if moves.filtered( lambda r: r.product_id.tracking != 'none'): button_produce_done = False else: prod_obj_pool.action_produce( cr, uid, production.id, production.product_qty, 'consume_produce', context=None) if button_produce_done: prod_obj_pool.signal_workflow(cr, uid, [oper_obj.production_id.id], 'button_produce_done') return def write(self, cr, uid, ids, vals, context=None, update=True): result = super(mrp_production_workcenter_line, self).write(cr, uid, ids, vals, context=context) prod_obj = self.pool.get('mrp.production') if vals.get('date_planned', False) and update: for prod in self.browse(cr, uid, ids, context=context): if prod.production_id.workcenter_lines: dstart = min( vals['date_planned'], prod.production_id.workcenter_lines[0]['date_planned']) prod_obj.write(cr, uid, [prod.production_id.id], {'date_start': dstart}, context=context, mini=False) return result def action_draft(self, cr, uid, ids, context=None): """ Sets state to draft. @return: True """ return self.write(cr, uid, ids, {'state': 'draft'}, context=context) def action_start_working(self, cr, uid, ids, context=None): """ Sets state to start working and writes starting date. @return: True """ self.modify_production_order_state(cr, uid, ids, 'start') self.write(cr, uid, ids, { 'state': 'startworking', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S') }, context=context) return True def action_done(self, cr, uid, ids, context=None): """ Sets state to done, writes finish date and calculates delay. @return: True """ delay = 0.0 date_now = time.strftime('%Y-%m-%d %H:%M:%S') obj_line = self.browse(cr, uid, ids[0]) date_start = datetime.strptime(obj_line.date_start, '%Y-%m-%d %H:%M:%S') date_finished = datetime.strptime(date_now, '%Y-%m-%d %H:%M:%S') delay += (date_finished - date_start).days * 24 delay += (date_finished - date_start).seconds / float(60 * 60) self.write(cr, uid, ids, { 'state': 'done', 'date_finished': date_now, 'delay': delay }, context=context) self.modify_production_order_state(cr, uid, ids, 'done') return True def action_cancel(self, cr, uid, ids, context=None): """ Sets state to cancel. @return: True """ return self.write(cr, uid, ids, {'state': 'cancel'}, context=context) def action_pause(self, cr, uid, ids, context=None): """ Sets state to pause. @return: True """ return self.write(cr, uid, ids, {'state': 'pause'}, context=context) def action_resume(self, cr, uid, ids, context=None): """ Sets state to startworking. @return: True """ return self.write(cr, uid, ids, {'state': 'startworking'}, context=context)
class gamification_goal_definition(osv.Model): """Goal definition A goal definition contains the way to evaluate an objective Each module wanting to be able to set goals to the users needs to create a new gamification_goal_definition """ _name = 'gamification.goal.definition' _description = 'Gamification goal definition' def _get_suffix(self, cr, uid, ids, field_name, arg, context=None): res = dict.fromkeys(ids, '') for goal in self.browse(cr, uid, ids, context=context): if goal.suffix and not goal.monetary: res[goal.id] = goal.suffix elif goal.monetary: # use the current user's company currency user = self.pool.get('res.users').browse(cr, uid, uid, context) if goal.suffix: res[goal.id] = "%s %s" % ( user.company_id.currency_id.symbol, goal.suffix) else: res[goal.id] = user.company_id.currency_id.symbol else: res[goal.id] = "" return res _columns = { 'name': fields.char('Goal Definition', required=True, translate=True), 'description': fields.text('Goal Description'), 'monetary': fields.boolean( 'Monetary Value', help= "The target and current value are defined in the company currency." ), 'suffix': fields.char('Suffix', help="The unit of the target and current values", translate=True), 'full_suffix': fields.function(_get_suffix, type="char", string="Full Suffix", help="The currency and suffix field"), 'computation_mode': fields.selection( [ ('manually', 'Recorded manually'), ('count', 'Automatic: number of records'), ('sum', 'Automatic: sum on a field'), ('python', 'Automatic: execute a specific Python code'), ], string="Computation Mode", help= "Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.", required=True), 'display_mode': fields.selection([ ('progress', 'Progressive (using numerical values)'), ('boolean', 'Exclusive (done or not-done)'), ], string="Displayed as", required=True), 'model_id': fields.many2one('ir.model', string='Model', help='The model object for the field to evaluate'), 'model_inherited_model_ids': fields.related('model_id', 'inherited_model_ids', type="many2many", obj="ir.model", string="Inherited models", readonly="True"), 'field_id': fields.many2one('ir.model.fields', string='Field to Sum', help='The field containing the value to evaluate'), 'field_date_id': fields.many2one('ir.model.fields', string='Date Field', help='The date to use for the time period evaluated'), 'domain': fields.char( "Filter Domain", help= "Domain for filtering records. General rule, not user depending, e.g. [('state', '=', 'done')]. The expression can contain reference to 'user' which is a browse record of the current user if not in batch mode.", required=True), 'batch_mode': fields.boolean( 'Batch Mode', help= "Evaluate the expression in batch instead of once for each user"), 'batch_distinctive_field': fields.many2one( 'ir.model.fields', string="Distinctive field for batch user", help= "In batch mode, this indicates which field distinct one user form the other, e.g. user_id, partner_id..." ), 'batch_user_expression': fields.char( "Evaluted expression for batch mode", help= "The value to compare with the distinctive field. The expression can contain reference to 'user' which is a browse record of the current user, e.g. user.id, user.partner_id.id..." ), 'compute_code': fields.text( 'Python Code', help= "Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id." ), 'condition': fields.selection( [('higher', 'The higher the better'), ('lower', 'The lower the better')], string='Goal Performance', help= 'A goal is considered as completed when the current value is compared to the value to reach', required=True), 'action_id': fields.many2one( 'ir.actions.act_window', string="Action", help="The action that will be called to update the goal value."), 'res_id_field': fields.char( "ID Field of user", help= "The field name on the user profile (res.users) containing the value for res_id for action." ), } _defaults = { 'condition': 'higher', 'computation_mode': 'manually', 'domain': "[]", 'monetary': False, 'display_mode': 'progress', } def number_following(self, cr, uid, model_name="mail.thread", context=None): """Return the number of 'model_name' objects the user is following The model specified in 'model_name' must inherit from mail.thread """ user = self.pool.get('res.users').browse(cr, uid, uid, context=context) return self.pool.get('mail.followers').search( cr, uid, [('res_model', '=', model_name), ('partner_id', '=', user.partner_id.id)], count=True, context=context) def _check_domain_validity(self, cr, uid, ids, context=None): # take admin as should always be present superuser = self.pool['res.users'].browse(cr, uid, SUPERUSER_ID, context=context) for definition in self.browse(cr, uid, ids, context=context): if definition.computation_mode not in ('count', 'sum'): continue obj = self.pool[definition.model_id.model] try: domain = safe_eval(definition.domain, {'user': superuser}) # demmy search to make sure the domain is valid obj.search(cr, uid, domain, context=context, count=True) except (ValueError, SyntaxError), e: msg = e.message or (e.msg + '\n' + e.text) raise UserError( _("The domain for the definition %s seems incorrect, please check it.\n\n%s" % (definition.name, msg))) return True
class gamification_goal(osv.Model): """Goal instance for a user An individual goal for a user on a specified time period""" _name = 'gamification.goal' _description = 'Gamification goal instance' def _get_completion(self, cr, uid, ids, field_name, arg, context=None): """Return the percentage of completeness of the goal, between 0 and 100""" res = dict.fromkeys(ids, 0.0) for goal in self.browse(cr, uid, ids, context=context): if goal.definition_condition == 'higher': if goal.current >= goal.target_goal: res[goal.id] = 100.0 else: res[goal.id] = round( 100.0 * goal.current / goal.target_goal, 2) elif goal.current < goal.target_goal: # a goal 'lower than' has only two values possible: 0 or 100% res[goal.id] = 100.0 else: res[goal.id] = 0.0 return res def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None): goal_definition = self.pool.get('gamification.goal.definition') if not definition_id: return {'value': {'definition_id': False}} goal_definition = goal_definition.browse(cr, uid, definition_id, context=context) return { 'value': { 'computation_mode': goal_definition.computation_mode, 'definition_condition': goal_definition.condition } } _columns = { 'definition_id': fields.many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade"), 'user_id': fields.many2one('res.users', string='User', required=True, auto_join=True, ondelete="cascade"), 'line_id': fields.many2one('gamification.challenge.line', string='Challenge Line', ondelete="cascade"), 'challenge_id': fields.related( 'line_id', 'challenge_id', string="Challenge", type='many2one', relation='gamification.challenge', store=True, readonly=True, help= "Challenge that generated the goal, assign challenge to users to generate goals with a value in this field." ), 'start_date': fields.date('Start Date'), 'end_date': fields.date('End Date'), # no start and end = always active 'target_goal': fields.float('To Reach', required=True, track_visibility='always'), # no goal = global index 'current': fields.float('Current Value', required=True, track_visibility='always'), 'completeness': fields.function(_get_completion, type='float', string='Completeness'), 'state': fields.selection([ ('draft', 'Draft'), ('inprogress', 'In progress'), ('reached', 'Reached'), ('failed', 'Failed'), ('canceled', 'Canceled'), ], string='State', required=True, track_visibility='always'), 'to_update': fields.boolean('To update'), 'closed': fields.boolean('Closed goal', help="These goals will not be recomputed."), 'computation_mode': fields.related('definition_id', 'computation_mode', type='char', string="Computation mode"), 'remind_update_delay': fields.integer( 'Remind delay', help= "The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified." ), 'last_update': fields.date( 'Last Update', help= "In case of manual goal, reminders are sent if the goal as not been updated for a while (defined in challenge). Ignored in case of non-manual goal or goal not linked to a challenge." ), 'definition_description': fields.related('definition_id', 'description', type='char', string='Definition Description', readonly=True), 'definition_condition': fields.related('definition_id', 'condition', type='char', string='Definition Condition', readonly=True), 'definition_suffix': fields.related('definition_id', 'full_suffix', type="char", string="Suffix", readonly=True), 'definition_display': fields.related('definition_id', 'display_mode', type="char", string="Display Mode", readonly=True), } _defaults = { 'current': 0, 'state': 'draft', 'start_date': fields.date.today, } _order = 'start_date desc, end_date desc, definition_id, id' def _check_remind_delay(self, cr, uid, goal, context=None): """Verify if a goal has not been updated for some time and send a reminder message of needed. :return: data to write on the goal object """ temp_obj = self.pool['mail.template'] if goal.remind_update_delay and goal.last_update: delta_max = timedelta(days=goal.remind_update_delay) last_update = datetime.strptime(goal.last_update, DF).date() if date.today() - last_update > delta_max: # generate a remind report temp_obj = self.pool.get('mail.template') template_id = self.pool['ir.model.data'].get_object_reference( cr, uid, 'gamification', 'email_template_goal_reminder')[0] template = temp_obj.get_email_template(cr, uid, template_id, goal.id, context=context) body_html = temp_obj.render_template(cr, uid, template.body_html, 'gamification.goal', goal.id, context=template._context) self.pool['mail.thread'].message_post( cr, uid, 0, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment') return {'to_update': True} return {} def _get_write_values(self, cr, uid, goal, new_value, context=None): """Generate values to write after recomputation of a goal score""" if new_value == goal.current: # avoid useless write if the new value is the same as the old one return {} result = {goal.id: {'current': new_value}} if (goal.definition_id.condition == 'higher' and new_value >= goal.target_goal) \ or (goal.definition_id.condition == 'lower' and new_value <= goal.target_goal): # success, do no set closed as can still change result[goal.id]['state'] = 'reached' elif goal.end_date and fields.date.today() > goal.end_date: # check goal failure result[goal.id]['state'] = 'failed' result[goal.id]['closed'] = True return result def update(self, cr, uid, ids, context=None): """Update the goals to recomputes values and change of states If a manual goal is not updated for enough time, the user will be reminded to do so (done only once, in 'inprogress' state). If a goal reaches the target value, the status is set to reached If the end date is passed (at least +1 day, time not considered) without the target value being reached, the goal is set as failed.""" if context is None: context = {} commit = context.get('commit_gamification', False) goals_by_definition = {} for goal in self.browse(cr, uid, ids, context=context): goals_by_definition.setdefault(goal.definition_id, []).append(goal) for definition, goals in goals_by_definition.items(): goals_to_write = dict((goal.id, {}) for goal in goals) if definition.computation_mode == 'manually': for goal in goals: goals_to_write[goal.id].update( self._check_remind_delay(cr, uid, goal, context)) elif definition.computation_mode == 'python': # TODO batch execution for goal in goals: # execute the chosen method cxt = { 'self': self.pool.get('gamification.goal'), 'object': goal, 'pool': self.pool, 'cr': cr, 'context': dict(context ), # copy context to prevent side-effects of eval 'uid': uid, 'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time } code = definition.compute_code.strip() safe_eval(code, cxt, mode="exec", nocopy=True) # the result of the evaluated codeis put in the 'result' local variable, propagated to the context result = cxt.get('result') if result is not None and type(result) in (float, int, long): goals_to_write.update( self._get_write_values(cr, uid, goal, result, context=context)) else: _logger.exception( _('Invalid return content from the evaluation of code for definition %s' ) % definition.name) else: # count or sum obj = self.pool.get(definition.model_id.model) field_date_name = definition.field_date_id and definition.field_date_id.name or False if definition.computation_mode == 'count' and definition.batch_mode: # batch mode, trying to do as much as possible in one request general_domain = safe_eval(definition.domain) field_name = definition.batch_distinctive_field.name subqueries = {} for goal in goals: start_date = field_date_name and goal.start_date or False end_date = field_date_name and goal.end_date or False subqueries.setdefault( (start_date, end_date), {}).update({ goal.id: safe_eval(definition.batch_user_expression, {'user': goal.user_id}) }) # the global query should be split by time periods (especially for recurrent goals) for (start_date, end_date), query_goals in subqueries.items(): subquery_domain = list(general_domain) subquery_domain.append( (field_name, 'in', list(set(query_goals.values())))) if start_date: subquery_domain.append( (field_date_name, '>=', start_date)) if end_date: subquery_domain.append( (field_date_name, '<=', end_date)) if field_name == 'id': # grouping on id does not work and is similar to search anyway user_ids = obj.search(cr, uid, subquery_domain, context=context) user_values = [{ 'id': user_id, 'id_count': 1 } for user_id in user_ids] else: user_values = obj.read_group(cr, uid, subquery_domain, fields=[field_name], groupby=[field_name], context=context) # user_values has format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...] for goal in [ g for g in goals if g.id in query_goals.keys() ]: for user_value in user_values: queried_value = field_name in user_value and user_value[ field_name] or False if isinstance(queried_value, tuple) and len( queried_value) == 2 and isinstance( queried_value[0], (int, long)): queried_value = queried_value[0] if queried_value == query_goals[goal.id]: new_value = user_value.get( field_name + '_count', goal.current) goals_to_write.update( self._get_write_values( cr, uid, goal, new_value, context=context)) else: for goal in goals: # eval the domain with user replaced by goal user object domain = safe_eval(definition.domain, {'user': goal.user_id}) # add temporal clause(s) to the domain if fields are filled on the goal if goal.start_date and field_date_name: domain.append( (field_date_name, '>=', goal.start_date)) if goal.end_date and field_date_name: domain.append( (field_date_name, '<=', goal.end_date)) if definition.computation_mode == 'sum': field_name = definition.field_id.name # TODO for master: group on user field in batch mode res = obj.read_group(cr, uid, domain, [field_name], [], context=context) new_value = res and res[0][field_name] or 0.0 else: # computation mode = count new_value = obj.search(cr, uid, domain, context=context, count=True) goals_to_write.update( self._get_write_values(cr, uid, goal, new_value, context=context)) for goal_id, value in goals_to_write.items(): if not value: continue self.write(cr, uid, [goal_id], value, context=context) if commit: cr.commit() return True def action_start(self, cr, uid, ids, context=None): """Mark a goal as started. This should only be used when creating goals manually (in draft state)""" self.write(cr, uid, ids, {'state': 'inprogress'}, context=context) return self.update(cr, uid, ids, context=context) def action_reach(self, cr, uid, ids, context=None): """Mark a goal as reached. If the target goal condition is not met, the state will be reset to In Progress at the next goal update until the end date.""" return self.write(cr, uid, ids, {'state': 'reached'}, context=context) def action_fail(self, cr, uid, ids, context=None): """Set the state of the goal to failed. A failed goal will be ignored in future checks.""" return self.write(cr, uid, ids, {'state': 'failed'}, context=context) def action_cancel(self, cr, uid, ids, context=None): """Reset the completion after setting a goal as reached or failed. This is only the current state, if the date and/or target criterias match the conditions for a change of state, this will be applied at the next goal update.""" return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context) def create(self, cr, uid, vals, context=None): """Overwrite the create method to add a 'no_remind_goal' field to True""" context = dict(context or {}) context['no_remind_goal'] = True return super(gamification_goal, self).create(cr, uid, vals, context=context) def write(self, cr, uid, ids, vals, context=None): """Overwrite the write method to update the last_update field to today If the current value is changed and the report frequency is set to On change, a report is generated """ if context is None: context = {} vals['last_update'] = fields.date.today() result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context) for goal in self.browse(cr, uid, ids, context=context): if goal.state != "draft" and ('definition_id' in vals or 'user_id' in vals): # avoid drag&drop in kanban view raise UserError( _('Can not modify the configuration of a started goal')) if vals.get('current'): if 'no_remind_goal' in context: # new goals should not be reported continue if goal.challenge_id and goal.challenge_id.report_message_frequency == 'onchange': self.pool.get('gamification.challenge').report_progress( cr, SUPERUSER_ID, goal.challenge_id, users=[goal.user_id], context=context) return result def get_action(self, cr, uid, goal_id, context=None): """Get the ir.action related to update the goal In case of a manual goal, should return a wizard to update the value :return: action description in a dictionnary """ goal = self.browse(cr, uid, goal_id, context=context) if goal.definition_id.action_id: # open a the action linked to the goal action = goal.definition_id.action_id.read()[0] if goal.definition_id.res_id_field: current_user = self.pool.get('res.users').browse( cr, uid, uid, context=context) action['res_id'] = safe_eval(goal.definition_id.res_id_field, {'user': current_user}) # if one element to display, should see it in form mode if possible action['views'] = [(view_id, mode) for (view_id, mode) in action['views'] if mode == 'form'] or action['views'] return action if goal.computation_mode == 'manually': # open a wizard window to update the value manually action = { 'name': _("Update %s") % goal.definition_id.name, 'id': goal_id, 'type': 'ir.actions.act_window', 'views': [[False, 'form']], 'target': 'new', 'context': { 'default_goal_id': goal_id, 'default_current': goal.current }, 'res_model': 'gamification.goal.wizard' } return action return False