class ProductPackaging(models.Model): _inherit = 'product.packaging' height = fields.Integer('Height') width = fields.Integer('Width') length = fields.Integer('Length') max_weight = fields.Float( 'Max Weight', help='Maximum weight shippable in this packaging') shipper_package_code = fields.Char('Package Code') package_carrier_type = fields.Selection( [('none', 'No carrier integration')], string='Carrier', default='none') _sql_constraints = [ ('positive_height', 'CHECK(height>=0)', 'Height must be positive'), ('positive_width', 'CHECK(width>=0)', 'Width must be positive'), ('positive_length', 'CHECK(length>=0)', 'Length must be positive'), ('positive_max_weight', 'CHECK(max_weight>=0.0)', 'Max Weight must be positive'), ]
class FleetVehicleModel(models.Model): _inherit = 'fleet.vehicle.model' default_recurring_cost_amount_depreciated = fields.Float( string="Cost (Depreciated)", help= "Default recurring cost amount that should be applied to a new car from this model" ) default_co2 = fields.Float(string="CO2 emissions") default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle') default_car_value = fields.Float(string="Catalog Value (VAT Incl.)") can_be_requested = fields.Boolean( string="Can be requested", help="Can be requested on a contract as a new car") default_atn = fields.Float(compute='_compute_atn', string="ATN") default_total_depreciated_cost = fields.Float( compute='_compute_default_total_depreciated_cost', string="Total Cost (Depreciated)") co2_fee = fields.Float(compute='_compute_co2_fee', string="CO2 fee") @api.depends('default_car_value', 'default_co2', 'default_fuel_type') def _compute_atn(self): now = Datetime.now() for model in self: model.default_atn = self.env['fleet.vehicle']._get_car_atn( now, model.default_car_value, model.default_fuel_type, model.default_co2) @api.depends('co2_fee', 'default_recurring_cost_amount_depreciated') def _compute_default_total_depreciated_cost(self): for model in self: model.default_total_depreciated_cost = model.co2_fee + model.default_recurring_cost_amount_depreciated @api.depends('default_co2') def _compute_co2_fee(self): for model in self: model.co2_fee = self.env['fleet.vehicle']._get_co2_fee( model.default_co2)
class MailActivityType(models.Model): """ Activity Types are used to categorize activities. Each type is a different kind of activity e.g. call, mail, meeting. An activity can be generic i.e. available for all models using activities; or specific to a model in which case res_model_id field should be used. """ _name = 'mail.activity.type' _description = 'Activity Type' _rec_name = 'name' _order = 'sequence, id' name = fields.Char('Name', required=True, translate=True) summary = fields.Char('Summary', translate=True) sequence = fields.Integer('Sequence', default=10) days = fields.Integer( '# Days', default=0, help= 'Number of days before executing the action. It allows to plan the action deadline.' ) icon = fields.Char('Icon', help="Font awesome icon e.g. fa-tasks") res_model_id = fields.Many2one( 'ir.model', 'Model', index=True, help='Specify a model if the activity should be specific to a model' ' and not available when managing activities for other models.') next_type_ids = fields.Many2many('mail.activity.type', 'mail_activity_rel', 'activity_id', 'recommended_id', string='Recommended Next Activities') previous_type_ids = fields.Many2many('mail.activity.type', 'mail_activity_rel', 'recommended_id', 'activity_id', string='Preceding Activities') category = fields.Selection( [('default', 'Other')], default='default', string='Category', help= 'Categories may trigger specific behavior like opening calendar view')
class ChannelPartner(models.Model): _name = 'mail.channel.partner' _description = 'Listeners of a Channel' _table = 'mail_channel_partner' _rec_name = 'partner_id' partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade') partner_email = fields.Char('Email', related='partner_id.email') channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade') seen_message_id = fields.Many2one('mail.message', string='Last Seen') fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open') is_minimized = fields.Boolean("Conversation is minimized") is_pinned = fields.Boolean("Is pinned on the interface", default=True)
class mother(models.Model): _inherit = 'test.inherit.mother' field_in_mother = fields.Char() partner_id = fields.Many2one('res.partner') # extend the name field: make it required and change its default value name = fields.Char(required=True, default='Bar') # extend the selection of the state field, and discard its default value state = fields.Selection(selection_add=[('c', 'C')], default=None) # override the computed field, and extend its dependencies @api.one @api.depends('field_in_mother') def _compute_surname(self): if self.field_in_mother: self.surname = self.field_in_mother else: super(mother, self)._compute_surname()
class IrModelField(models.Model): _inherit = 'ir.model.fields' track_visibility = fields.Selection( [('onchange', "On Change"), ('always', "Always")], string="Tracking", help= "When set, every modification to this field will be tracked in the chatter.", ) def _reflect_field_params(self, field): vals = super(IrModelField, self)._reflect_field_params(field) vals['track_visibility'] = getattr(field, 'track_visibility', None) return vals def _instanciate_attrs(self, field_data): attrs = super(IrModelField, self)._instanciate_attrs(field_data) if attrs and field_data.get('track_visibility'): attrs['track_visibility'] = field_data['track_visibility'] return attrs
class TemplatePreview(models.TransientModel): _inherit = "mail.template" _name = "email_template.preview" _description = "Email Template Preview" @api.model def _get_records(self): """ Return Records of particular Email Template's Model """ template_id = self._context.get('template_id') default_res_id = self._context.get('default_res_id') if not template_id: return [] template = self.env['mail.template'].browse(int(template_id)) records = self.env[template.model_id.model].search([], limit=10) records |= records.browse(default_res_id) return records.name_get() @api.model def default_get(self, fields): result = super(TemplatePreview, self).default_get(fields) if 'res_id' in fields and not result.get('res_id'): records = self._get_records() result['res_id'] = records and records[0][0] or False # select first record as a Default if self._context.get('template_id') and 'model_id' in fields and not result.get('model_id'): result['model_id'] = self.env['mail.template'].browse(self._context['template_id']).model_id.id return result res_id = fields.Selection(_get_records, 'Sample Document') partner_ids = fields.Many2many('res.partner', string='Recipients') @api.onchange('res_id') @api.multi def on_change_res_id(self): mail_values = {} if self.res_id and self._context.get('template_id'): template = self.env['mail.template'].browse(self._context['template_id']) self.name = template.name mail_values = template.generate_email(self.res_id) for field in ['email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to', 'partner_ids', 'attachment_ids']: setattr(self, field, mail_values.get(field, False))
class MrpWorkcenterProductivity(models.Model): _name = "mrp.workcenter.productivity" _description = "Workcenter Productivity Log" _order = "id desc" _rec_name = "loss_id" workcenter_id = fields.Many2one('mrp.workcenter', "Work Center", required=True) workorder_id = fields.Many2one('mrp.workorder', 'Work Order') user_id = fields.Many2one( 'res.users', "User", default=lambda self: self.env.uid) loss_id = fields.Many2one( 'mrp.workcenter.productivity.loss', "Loss Reason", ondelete='restrict', required=True) loss_type = fields.Selection( "Effectiveness", related='loss_id.loss_type', store=True) description = fields.Text('Description') date_start = fields.Datetime('Start Date', default=fields.Datetime.now, required=True) date_end = fields.Datetime('End Date') duration = fields.Float('Duration', compute='_compute_duration', store=True) @api.depends('date_end', 'date_start') def _compute_duration(self): for blocktime in self: if blocktime.date_end: d1 = fields.Datetime.from_string(blocktime.date_start) d2 = fields.Datetime.from_string(blocktime.date_end) diff = d2 - d1 if (blocktime.loss_type not in ('productive', 'performance')) and blocktime.workcenter_id.resource_calendar_id: r = blocktime.workcenter_id.resource_calendar_id.get_work_hours_count(d1, d2, blocktime.workcenter_id.resource_id.id) blocktime.duration = round(r * 60, 2) else: blocktime.duration = round(diff.total_seconds() / 60.0, 2) else: blocktime.duration = 0.0 @api.multi def button_block(self): self.ensure_one() self.workcenter_id.order_ids.end_all()
class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' security_lead = fields.Float(related='company_id.security_lead', string="Security Lead Time") group_route_so_lines = fields.Boolean( "Order-Specific Routes", implied_group='sale_stock.group_route_so_lines') module_sale_order_dates = fields.Boolean("Delivery Date") group_display_incoterm = fields.Boolean( "Incoterms", implied_group='sale_stock.group_display_incoterm') use_security_lead = fields.Boolean( string="Security Lead Time for Sales", oldname='default_new_security_lead', help= "Margin of error for dates promised to customers. Products will be scheduled for delivery that many days earlier than the actual promised date, to cope with unexpected delays in the supply chain." ) default_picking_policy = fields.Selection( [('direct', 'Ship products as soon as available, with back orders'), ('one', 'Ship all products at once')], "Shipping Management", default='direct', default_model="sale.order", required=True) @api.onchange('use_security_lead') def _onchange_use_security_lead(self): if not self.use_security_lead: self.security_lead = 0.0 def get_values(self): res = super(ResConfigSettings, self).get_values() res.update(use_security_lead=self.env['ir.config_parameter'].sudo(). get_param('sale_stock.use_security_lead')) return res def set_values(self): super(ResConfigSettings, self).set_values() self.env['ir.config_parameter'].sudo().set_param( 'sale_stock.use_security_lead', self.use_security_lead)
class ResourceCalendarAttendance(models.Model): _name = "resource.calendar.attendance" _description = "Work Detail" _order = 'dayofweek, hour_from' name = fields.Char(required=True) dayofweek = fields.Selection([ ('0', 'Monday'), ('1', 'Tuesday'), ('2', 'Wednesday'), ('3', 'Thursday'), ('4', 'Friday'), ('5', 'Saturday'), ('6', 'Sunday') ], 'Day of Week', required=True, index=True, default='0') date_from = fields.Date(string='Starting Date') date_to = fields.Date(string='End Date') hour_from = fields.Float(string='Work from', required=True, index=True, help="Start and End time of working.\n" "A specific value of 24:00 is interpreted as 23:59:59.999999.") hour_to = fields.Float(string='Work to', required=True) calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True, ondelete='cascade')
class ChallengeLine(models.Model): """Gamification challenge line Predefined goal for 'gamification_challenge' These are generic list of goals with only the target goal defined Should only be created for the gamification.challenge object """ _name = 'gamification.challenge.line' _description = 'Gamification generic goal for challenge' _order = "sequence, id" challenge_id = fields.Many2one('gamification.challenge', string='Challenge', required=True, ondelete="cascade") definition_id = fields.Many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade") sequence = fields.Integer('Sequence', help='Sequence number for ordering', default=1) target_goal = fields.Float('Target Value to Reach', required=True) name = fields.Char("Name", related='definition_id.name') condition = fields.Selection("Condition", related='definition_id.condition', readonly=True) definition_suffix = fields.Char("Unit", related='definition_id.suffix', readonly=True) definition_monetary = fields.Boolean("Monetary", related='definition_id.monetary', readonly=True) definition_full_suffix = fields.Char("Suffix", related='definition_id.full_suffix', readonly=True)
class res_partner(models.Model): _name = 'res.partner' _inherit = 'res.partner' @api.multi def _purchase_invoice_count(self): PurchaseOrder = self.env['purchase.order'] Invoice = self.env['account.invoice'] for partner in self: partner.purchase_order_count = PurchaseOrder.search_count([('partner_id', 'child_of', partner.id)]) partner.supplier_invoice_count = Invoice.search_count([('partner_id', 'child_of', partner.id), ('type', '=', 'in_invoice')]) @api.model def _commercial_fields(self): return super(res_partner, self)._commercial_fields() property_purchase_currency_id = fields.Many2one( 'res.currency', string="Supplier Currency", company_dependent=True, help="This currency will be used, instead of the default one, for purchases from the current partner") purchase_order_count = fields.Integer(compute='_purchase_invoice_count', string='# of Purchase Order') supplier_invoice_count = fields.Integer(compute='_purchase_invoice_count', string='# Vendor Bills') purchase_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order', help=WARNING_HELP, required=True, default="no-message") purchase_warn_msg = fields.Text('Message for Purchase Order')
class IrServerObjectLines(models.Model): _name = 'ir.server.object.lines' _description = 'Server Action value mapping' _sequence = 'ir_actions_id_seq' server_id = fields.Many2one('ir.actions.server', string='Related Server Action', ondelete='cascade') col1 = fields.Many2one('ir.model.fields', string='Field', required=True) value = fields.Text( required=True, help="Expression containing a value specification. \n" "When Formula type is selected, this field may be a Python expression " " that can use the same values as for the code field on the server action.\n" "If Value type is selected, the value will be used directly without evaluation." ) type = fields.Selection([('value', 'Value'), ('equation', 'Python expression')], 'Evaluation Type', default='value', required=True, change_default=True) @api.multi def eval_value(self, eval_context=None): result = dict.fromkeys(self.ids, False) for line in self: expr = line.value if line.type == 'equation': expr = safe_eval(line.value, eval_context) elif line.col1.ttype in ['many2one', 'integer']: try: expr = int(line.value) except Exception: pass result[line.id] = expr return result
class StockQuantityHistory(models.TransientModel): _name = 'stock.quantity.history' _description = 'Stock Quantity History' compute_at_date = fields.Selection( [(0, 'Current Inventory'), (1, 'At a Specific Date')], string="Compute", help= "Choose to analyze the current inventory or from a specific date in the past." ) date = fields.Datetime( 'Inventory at Date', help="Choose a date to get the inventory at that date", default=fields.Datetime.now) def open_table(self): self.ensure_one() if self.compute_at_date: tree_view_id = self.env.ref('stock.view_stock_product_tree').id form_view_id = self.env.ref( 'stock.product_form_view_procurement_button').id # We pass `to_date` in the context so that `qty_available` will be computed across # moves until date. action = { 'type': 'ir.actions.act_window', 'views': [(tree_view_id, 'tree'), (form_view_id, 'form')], 'view_mode': 'tree,form', 'name': _('Products'), 'res_model': 'product.product', 'context': dict(self.env.context, to_date=self.date), } return action else: self.env['stock.quant']._merge_quants() return self.env.ref('stock.quantsact').read()[0]
class AccountPrintJournal(models.TransientModel): _inherit = "account.common.journal.report" _name = "account.print.journal" _description = "Account Print Journal" sort_selection = fields.Selection([ ('date', 'Date'), ('move_name', 'Journal Entry Number'), ], 'Entries Sorted by', required=True, default='move_name') journal_ids = fields.Many2many( 'account.journal', string='Journals', required=True, default=lambda self: self.env['account.journal'].search([( 'type', 'in', ['sale', 'purchase'])])) def _print_report(self, data): data = self.pre_print_report(data) data['form'].update({'sort_selection': self.sort_selection}) return self.env.ref('account.action_report_journal').with_context( landscape=True).report_action(self, data=data)
class Partner(models.Model): _inherit = 'res.partner' property_stock_customer = fields.Many2one( 'stock.location', string="Customer Location", company_dependent=True, help= "This stock location will be used, instead of the default one, as the destination location for goods you send to this partner" ) property_stock_supplier = fields.Many2one( 'stock.location', string="Vendor Location", company_dependent=True, help= "This stock location will be used, instead of the default one, as the source location for goods you receive from the current partner" ) picking_warn = fields.Selection(WARNING_MESSAGE, 'Stock Picking', help=WARNING_HELP, default='no-message', required=True) # TDE FIXME: expand this message / help picking_warn_msg = fields.Text('Message for Stock Picking')
class Company(models.Model): _name = "res.company" _description = 'Companies' _order = 'sequence, name' @api.multi def copy(self, default=None): raise UserError( _('Duplicating a company is not allowed. Please create a new company instead.' )) def _get_logo(self): return base64.b64encode( open( os.path.join(tools.config['root_path'], 'addons', 'base', 'res', 'res_company_logo.png'), 'rb').read()) @api.model def _get_euro(self): return self.env['res.currency.rate'].search([('rate', '=', 1)], limit=1).currency_id @api.model def _get_user_currency(self): currency_id = self.env['res.users'].browse( self._uid).company_id.currency_id return currency_id or self._get_euro() name = fields.Char(related='partner_id.name', string='Company Name', required=True, store=True) sequence = fields.Integer( help='Used to order Companies in the company switcher', default=10) parent_id = fields.Many2one('res.company', string='Parent Company', index=True) child_ids = fields.One2many('res.company', 'parent_id', string='Child Companies') partner_id = fields.Many2one('res.partner', string='Partner', required=True) report_header = fields.Text( string='Company Tagline', help= "Appears by default on the top right corner of your printed documents (report header)." ) report_footer = fields.Text( string='Report Footer', translate=True, help="Footer text displayed at the bottom of all reports.") logo = fields.Binary(related='partner_id.image', default=_get_logo, string="Company Logo") # logo_web: do not store in attachments, since the image is retrieved in SQL for # performance reasons (see addons/web/controllers/main.py, Binary.company_logo) logo_web = fields.Binary(compute='_compute_logo_web', store=True) currency_id = fields.Many2one( 'res.currency', string='Currency', required=True, default=lambda self: self._get_user_currency()) user_ids = fields.Many2many('res.users', 'res_company_users_rel', 'cid', 'user_id', string='Accepted Users') account_no = fields.Char(string='Account No.') street = fields.Char(compute='_compute_address', inverse='_inverse_street') street2 = fields.Char(compute='_compute_address', inverse='_inverse_street2') zip = fields.Char(compute='_compute_address', inverse='_inverse_zip') city = fields.Char(compute='_compute_address', inverse='_inverse_city') state_id = fields.Many2one('res.country.state', compute='_compute_address', inverse='_inverse_state', string="Fed. State") bank_ids = fields.One2many('res.partner.bank', 'company_id', string='Bank Accounts', help='Bank accounts related to this company') country_id = fields.Many2one('res.country', compute='_compute_address', inverse='_inverse_country', string="Country") email = fields.Char(related='partner_id.email', store=True) phone = fields.Char(related='partner_id.phone', store=True) website = fields.Char(related='partner_id.website') vat = fields.Char(related='partner_id.vat', string="TIN") company_registry = fields.Char() paperformat_id = fields.Many2one( 'report.paperformat', 'Paper format', default=lambda self: self.env.ref('base.paperformat_euro', raise_if_not_found=False)) external_report_layout = fields.Selection([ ('background', 'Background'), ('boxed', 'Boxed'), ('clean', 'Clean'), ('standard', 'Standard'), ], string='Document Template') _sql_constraints = [('name_uniq', 'unique (name)', 'The company name must be unique !')] @api.model_cr def init(self): for company in self.search([('paperformat_id', '=', False)]): paperformat_euro = self.env.ref('base.paperformat_euro', False) if paperformat_euro: company.write({'paperformat_id': paperformat_euro.id}) sup = super(Company, self) if hasattr(sup, 'init'): sup.init() def _get_company_address_fields(self, partner): return { 'street': partner.street, 'street2': partner.street2, 'city': partner.city, 'zip': partner.zip, 'state_id': partner.state_id, 'country_id': partner.country_id, } # TODO @api.depends(): currently now way to formulate the dependency on the # partner's contact address def _compute_address(self): for company in self.filtered(lambda company: company.partner_id): address_data = company.partner_id.sudo().address_get( adr_pref=['contact']) if address_data['contact']: partner = company.partner_id.browse( address_data['contact']).sudo() company.update(company._get_company_address_fields(partner)) def _inverse_street(self): for company in self: company.partner_id.street = company.street def _inverse_street2(self): for company in self: company.partner_id.street2 = company.street2 def _inverse_zip(self): for company in self: company.partner_id.zip = company.zip def _inverse_city(self): for company in self: company.partner_id.city = company.city def _inverse_state(self): for company in self: company.partner_id.state_id = company.state_id def _inverse_country(self): for company in self: company.partner_id.country_id = company.country_id @api.depends('partner_id', 'partner_id.image') def _compute_logo_web(self): for company in self: company.logo_web = tools.image_resize_image( company.partner_id.image, (180, None)) @api.onchange('state_id') def _onchange_state(self): self.country_id = self.state_id.country_id @api.multi def on_change_country(self, country_id): # This function is called from account/models/chart_template.py, hence decorated with `multi`. self.ensure_one() currency_id = self._get_user_currency() if country_id: currency_id = self.env['res.country'].browse( country_id).currency_id return {'value': {'currency_id': currency_id.id}} @api.onchange('country_id') def _onchange_country_id_wrapper(self): res = {'domain': {'state_id': []}} if self.country_id: res['domain']['state_id'] = [('country_id', '=', self.country_id.id)] values = self.on_change_country(self.country_id.id)['value'] for fname, value in values.items(): setattr(self, fname, value) return res @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): context = dict(self.env.context) newself = self if context.pop('user_preference', None): # We browse as superuser. Otherwise, the user would be able to # select only the currently visible companies (according to rules, # which are probably to allow to see the child companies) even if # she belongs to some other companies. companies = self.env.user.company_id + self.env.user.company_ids args = (args or []) + [('id', 'in', companies.ids)] newself = newself.sudo() return super(Company, newself.with_context(context)).name_search( name=name, args=args, operator=operator, limit=limit) @api.model @api.returns('self', lambda value: value.id) def _company_default_get(self, object=False, field=False): """ Returns the default company (usually the user's company). The 'object' and 'field' arguments are ignored but left here for backward compatibility and potential override. """ return self.env['res.users']._get_company() @api.model @tools.ormcache('self.env.uid', 'company') def _get_company_children(self, company=None): if not company: return [] return self.search([('parent_id', 'child_of', [company])]).ids @api.multi def _get_partner_hierarchy(self): self.ensure_one() parent = self.parent_id if parent: return parent._get_partner_hierarchy() else: return self._get_partner_descendance([]) @api.multi def _get_partner_descendance(self, descendance): self.ensure_one() descendance.append(self.partner_id.id) for child_id in self._get_company_children(self.id): if child_id != self.id: descendance = self.browse(child_id)._get_partner_descendance( descendance) return descendance # deprecated, use clear_caches() instead def cache_restart(self): self.clear_caches() @api.model def create(self, vals): if not vals.get('name') or vals.get('partner_id'): self.clear_caches() return super(Company, self).create(vals) partner = self.env['res.partner'].create({ 'name': vals['name'], 'is_company': True, 'image': vals.get('logo'), 'customer': False, 'email': vals.get('email'), 'phone': vals.get('phone'), 'website': vals.get('website'), 'vat': vals.get('vat'), }) vals['partner_id'] = partner.id self.clear_caches() company = super(Company, self).create(vals) partner.write({'company_id': company.id}) return company @api.multi def write(self, values): self.clear_caches() return super(Company, self).write(values) @api.constrains('parent_id') def _check_parent_id(self): if not self._check_recursion(): raise ValidationError( _('Error ! You cannot create recursive companies.')) @api.multi def open_company_edit_report(self): self.ensure_one() return self.env['res.config.settings'].open_company() @api.multi def write_company_and_print_report(self, values): res = self.write(values) report_name = values.get('default_report_name') active_ids = values.get('active_ids') active_model = values.get('active_model') if report_name and active_ids and active_model: docids = self.env[active_model].browse(active_ids) return (self.env['ir.actions.report'].search( [('report_name', '=', report_name)], limit=1).with_context(values).report_action(docids)) else: return res
class mother(models.Model): _inherit = 'test.inherit.mother' # extend again the selection of the state field state = fields.Selection(selection_add=[('d', 'D')])
class Lead2OpportunityPartner(models.TransientModel): _name = 'crm.lead2opportunity.partner' _description = 'Lead To Opportunity Partner' _inherit = 'crm.partner.binding' @api.model def default_get(self, fields): """ Default get for name, opportunity_ids. If there is an exisitng partner link to the lead, find all existing opportunities links with this partner to merge all information together """ result = super(Lead2OpportunityPartner, self).default_get(fields) if self._context.get('active_id'): tomerge = {int(self._context['active_id'])} partner_id = result.get('partner_id') lead = self.env['crm.lead'].browse(self._context['active_id']) email = lead.partner_id.email if lead.partner_id else lead.email_from tomerge.update(self._get_duplicated_leads(partner_id, email, include_lost=True).ids) if 'action' in fields and not result.get('action'): result['action'] = 'exist' if partner_id else 'create' if 'partner_id' in fields: result['partner_id'] = partner_id if 'name' in fields: result['name'] = 'merge' if len(tomerge) >= 2 else 'convert' if 'opportunity_ids' in fields and len(tomerge) >= 2: result['opportunity_ids'] = list(tomerge) if lead.user_id: result['user_id'] = lead.user_id.id if lead.team_id: result['team_id'] = lead.team_id.id if not partner_id and not lead.contact_name: result['action'] = 'nothing' return result name = fields.Selection([ ('convert', 'Convert to opportunity'), ('merge', 'Merge with existing opportunities') ], 'Conversion Action', required=True) opportunity_ids = fields.Many2many('crm.lead', string='Opportunities') user_id = fields.Many2one('res.users', 'Salesperson', index=True) team_id = fields.Many2one('crm.team', 'Sales Channel', oldname='section_id', index=True) @api.onchange('action') def onchange_action(self): if self.action == 'exist': self.partner_id = self._find_matching_partner() else: self.partner_id = False @api.onchange('user_id') def _onchange_user(self): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ if self.user_id: if self.team_id: user_in_team = self.env['crm.team'].search_count([('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id)]) else: user_in_team = False if not user_in_team: values = self.env['crm.lead']._onchange_user_values(self.user_id.id if self.user_id else False) self.team_id = values.get('team_id', False) @api.model def _get_duplicated_leads(self, partner_id, email, include_lost=False): """ Search for opportunities that have the same partner and that arent done or cancelled """ return self.env['crm.lead']._get_duplicated_leads_by_emails(partner_id, email, include_lost=include_lost) # NOTE JEM : is it the good place to test this ? @api.model def view_init(self, fields): """ Check some preconditions before the wizard executes. """ for lead in self.env['crm.lead'].browse(self._context.get('active_ids', [])): if lead.probability == 100: raise UserError(_("Closed/Dead leads cannot be converted into opportunities.")) return False @api.multi def _convert_opportunity(self, vals): self.ensure_one() res = False leads = self.env['crm.lead'].browse(vals.get('lead_ids')) for lead in leads: self_def_user = self.with_context(default_user_id=self.user_id.id) partner_id = self_def_user._create_partner( lead.id, self.action, vals.get('partner_id') or lead.partner_id.id) res = lead.convert_opportunity(partner_id, [], False) user_ids = vals.get('user_ids') leads_to_allocate = leads if self._context.get('no_force_assignation'): leads_to_allocate = leads_to_allocate.filtered(lambda lead: not lead.user_id) if user_ids: leads_to_allocate.allocate_salesman(user_ids, team_id=(vals.get('team_id'))) return res @api.multi def action_apply(self): """ Convert lead to opportunity or merge lead and opportunity and open the freshly created opportunity view. """ self.ensure_one() values = { 'team_id': self.team_id.id, } if self.partner_id: values['partner_id'] = self.partner_id.id if self.name == 'merge': leads = self.with_context(active_test=False).opportunity_ids.merge_opportunity() if not leads.active: leads.write({'active': True, 'activity_type_id': False, 'lost_reason': False}) if leads.type == "lead": values.update({'lead_ids': leads.ids, 'user_ids': [self.user_id.id]}) self.with_context(active_ids=leads.ids)._convert_opportunity(values) elif not self._context.get('no_force_assignation') or not leads.user_id: values['user_id'] = self.user_id.id leads.write(values) else: leads = self.env['crm.lead'].browse(self._context.get('active_ids', [])) values.update({'lead_ids': leads.ids, 'user_ids': [self.user_id.id]}) self._convert_opportunity(values) return leads[0].redirect_opportunity_view() def _create_partner(self, lead_id, action, partner_id): """ Create partner based on action. :return dict: dictionary organized as followed: {lead_id: partner_assigned_id} """ #TODO this method in only called by Lead2OpportunityPartner #wizard and would probably diserve to be refactored or at least #moved to a better place if action == 'each_exist_or_create': partner_id = self.with_context(active_id=lead_id)._find_matching_partner() action = 'create' result = self.env['crm.lead'].browse(lead_id).handle_partner_assignation(action, partner_id) return result.get(lead_id)
class Lead2OpportunityMassConvert(models.TransientModel): _name = 'crm.lead2opportunity.partner.mass' _description = 'Mass Lead To Opportunity Partner' _inherit = 'crm.lead2opportunity.partner' @api.model def default_get(self, fields): res = super(Lead2OpportunityMassConvert, self).default_get(fields) if 'partner_id' in fields: # avoid forcing the partner of the first lead as default res['partner_id'] = False if 'action' in fields: res['action'] = 'each_exist_or_create' if 'name' in fields: res['name'] = 'convert' if 'opportunity_ids' in fields: res['opportunity_ids'] = False return res user_ids = fields.Many2many('res.users', string='Salesmen') team_id = fields.Many2one('crm.team', 'Sales Channel', index=True, oldname='section_id') deduplicate = fields.Boolean('Apply deduplication', default=True, help='Merge with existing leads/opportunities of each partner') action = fields.Selection([ ('each_exist_or_create', 'Use existing partner or create'), ('nothing', 'Do not link to a customer') ], 'Related Customer', required=True) force_assignation = fields.Boolean('Force assignation', help='If unchecked, this will leave the salesman of duplicated opportunities') @api.onchange('action') def _onchange_action(self): if self.action != 'exist': self.partner_id = False @api.onchange('deduplicate') def _onchange_deduplicate(self): active_leads = self.env['crm.lead'].browse(self._context['active_ids']) partner_ids = [(lead.partner_id.id, lead.partner_id and lead.partner_id.email or lead.email_from) for lead in active_leads] partners_duplicated_leads = {} for partner_id, email in partner_ids: duplicated_leads = self._get_duplicated_leads(partner_id, email) if len(duplicated_leads) > 1: partners_duplicated_leads.setdefault((partner_id, email), []).extend(duplicated_leads) leads_with_duplicates = [] for lead in active_leads: lead_tuple = (lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from) if len(partners_duplicated_leads.get(lead_tuple, [])) > 1: leads_with_duplicates.append(lead.id) self.opportunity_ids = self.env['crm.lead'].browse(leads_with_duplicates) @api.multi def _convert_opportunity(self, vals): """ When "massively" (more than one at a time) converting leads to opportunities, check the salesteam_id and salesmen_ids and update the values before calling super. """ self.ensure_one() salesteam_id = self.team_id.id if self.team_id else False salesmen_ids = [] if self.user_ids: salesmen_ids = self.user_ids.ids vals.update({'user_ids': salesmen_ids, 'team_id': salesteam_id}) return super(Lead2OpportunityMassConvert, self)._convert_opportunity(vals) @api.multi def mass_convert(self): self.ensure_one() if self.name == 'convert' and self.deduplicate: merged_lead_ids = set() remaining_lead_ids = set() lead_selected = self._context.get('active_ids', []) for lead_id in lead_selected: if lead_id not in merged_lead_ids: lead = self.env['crm.lead'].browse(lead_id) duplicated_leads = self._get_duplicated_leads(lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from) if len(duplicated_leads) > 1: lead = duplicated_leads.merge_opportunity() merged_lead_ids.update(duplicated_leads.ids) remaining_lead_ids.add(lead.id) active_ids = set(self._context.get('active_ids', {})) active_ids = (active_ids - merged_lead_ids) | remaining_lead_ids self = self.with_context(active_ids=list(active_ids)) # only update active_ids when there are set no_force_assignation = self._context.get('no_force_assignation', not self.force_assignation) return self.with_context(no_force_assignation=no_force_assignation).action_apply()
class Badge(models.Model): _inherit = 'gamification.badge' level = fields.Selection([('bronze', 'bronze'), ('silver', 'silver'), ('gold', 'gold')], string='Forum Badge Level')
class account_financial_report(models.Model): _name = "account.financial.report" _description = "Account Report" @api.multi @api.depends('parent_id', 'parent_id.level') def _get_level(self): '''Returns a dictionary with key=the ID of a record and value = the level of this record in the tree structure.''' for report in self: level = 0 if report.parent_id: level = report.parent_id.level + 1 report.level = level def _get_children_by_order(self): '''returns a recordset of all the children computed recursively, and sorted by sequence. Ready for the printing''' res = self children = self.search([('parent_id', 'in', self.ids)], order='sequence ASC') if children: for child in children: res += child._get_children_by_order() return res name = fields.Char('Report Name', required=True, translate=True) parent_id = fields.Many2one('account.financial.report', 'Parent') children_ids = fields.One2many('account.financial.report', 'parent_id', 'Account Report') sequence = fields.Integer('Sequence') level = fields.Integer(compute='_get_level', string='Level', store=True) type = fields.Selection([ ('sum', 'View'), ('accounts', 'Accounts'), ('account_type', 'Account Type'), ('account_report', 'Report Value'), ], 'Type', default='sum') account_ids = fields.Many2many('account.account', 'account_account_financial_report', 'report_line_id', 'account_id', 'Accounts') account_report_id = fields.Many2one('account.financial.report', 'Report Value') account_type_ids = fields.Many2many( 'account.account.type', 'account_account_financial_report_type', 'report_id', 'account_type_id', 'Account Types') sign = fields.Selection( [(-1, 'Reverse balance sign'), (1, 'Preserve balance sign')], 'Sign on Reports', required=True, default=1, help= 'For accounts that are typically more debited than credited and that you would like to print as negative amounts in your reports, you should reverse the sign of the balance; e.g.: Expense account. The same applies for accounts that are typically more credited than debited and that you would like to print as positive amounts in your reports; e.g.: Income account.' ) display_detail = fields.Selection( [('no_detail', 'No detail'), ('detail_flat', 'Display children flat'), ('detail_with_hierarchy', 'Display children with hierarchy')], 'Display details', default='detail_flat') style_overwrite = fields.Selection( [ (0, 'Automatic formatting'), (1, 'Main Title 1 (bold, underlined)'), (2, 'Title 2 (bold)'), (3, 'Title 3 (bold, smaller)'), (4, 'Normal Text'), (5, 'Italic Text (smaller)'), (6, 'Smallest Text'), ], 'Financial Report Style', default=0, help= "You can set up here the format you want this record to be displayed. If you leave the automatic formatting, it will be computed based on the financial reports hierarchy (auto-computed field 'level')." )
class IrTranslation(models.Model): _inherit = "ir.translation" gengo_comment = fields.Text("Comments & Activity Linked to Gengo") order_id = fields.Char('Gengo Order ID') gengo_translation = fields.Selection( [('machine', 'Translation By Machine'), ('standard', 'Standard'), ('pro', 'Pro'), ('ultra', 'Ultra')], "Gengo Translation Service Level", help= 'You can select here the service level you want for an automatic translation using Gengo.' ) @api.model def _get_all_supported_languages(self): flag, gengo = self.env['base.gengo.translations'].gengo_authentication( ) if not flag: raise UserError(gengo) supported_langs = {} lang_pair = gengo.getServiceLanguagePairs(lc_src='en') if lang_pair['opstat'] == 'ok': for g_lang in lang_pair['response']: if g_lang['lc_tgt'] not in supported_langs: supported_langs[g_lang['lc_tgt']] = [] supported_langs[g_lang['lc_tgt']] += [g_lang['tier']] return supported_langs def _get_gengo_corresponding_language(self, lang): return lang in LANG_CODE_MAPPING and LANG_CODE_MAPPING[lang][0] or lang @api.model def _get_source_query(self, name, types, lang, source, res_id): query, params = super(IrTranslation, self)._get_source_query(name, types, lang, source, res_id) query += """ ORDER BY CASE WHEN gengo_translation=%s then 10 WHEN gengo_translation=%s then 20 WHEN gengo_translation=%s then 30 WHEN gengo_translation=%s then 40 ELSE 0 END DESC """ params += ( 'machine', 'standard', 'ultra', 'pro', ) return (query, params) @api.model def _get_terms_query(self, field, records): query, params = super(IrTranslation, self)._get_terms_query(field, records) # order translations from worst to best query += """ ORDER BY CASE WHEN gengo_translation=%s then 10 WHEN gengo_translation=%s then 20 WHEN gengo_translation=%s then 30 WHEN gengo_translation=%s then 40 ELSE 0 END ASC """ params += ('machine', 'standard', 'ultra', 'pro') return query, params
class PartnerBinding(models.TransientModel): """ Handle the partner binding or generation in any CRM wizard that requires such feature, like the lead2opportunity wizard, or the phonecall2opportunity wizard. Try to find a matching partner from the CRM model's information (name, email, phone number, etc) or create a new one on the fly. Use it like a mixin with the wizard of your choice. """ _name = 'crm.partner.binding' _description = 'Handle partner binding or generation in CRM wizards.' @api.model def default_get(self, fields): res = super(PartnerBinding, self).default_get(fields) partner_id = self._find_matching_partner() if 'action' in fields and not res.get('action'): res['action'] = 'exist' if partner_id else 'create' if 'partner_id' in fields: res['partner_id'] = partner_id return res action = fields.Selection([ ('exist', 'Link to an existing customer'), ('create', 'Create a new customer'), ('nothing', 'Do not link to a customer') ], 'Related Customer', required=True) partner_id = fields.Many2one('res.partner', 'Customer') @api.model def _find_matching_partner(self): """ Try to find a matching partner regarding the active model data, like the customer's name, email, phone number, etc. :return int partner_id if any, False otherwise """ # active model has to be a lead if self._context.get('active_model') != 'crm.lead' or not self._context.get('active_id'): return False lead = self.env['crm.lead'].browse(self._context.get('active_id')) # find the best matching partner for the active model Partner = self.env['res.partner'] if lead.partner_id: # a partner is set already return lead.partner_id.id if lead.email_from: # search through the existing partners based on the lead's email partner = Partner.search([('email', '=', lead.email_from)], limit=1) return partner.id if lead.partner_name: # search through the existing partners based on the lead's partner or contact name partner = Partner.search([('name', 'ilike', '%' + lead.partner_name + '%')], limit=1) return partner.id if lead.contact_name: partner = Partner.search([('name', 'ilike', '%' + lead.contact_name+'%')], limit=1) return partner.id return False
class ReportProjectTaskUser(models.Model): _name = "report.project.task.user" _description = "Tasks by user and project" _order = 'name desc, project_id' _auto = False name = fields.Char(string='Task Title', readonly=True) user_id = fields.Many2one('res.users', string='Assigned To', readonly=True) date_start = fields.Datetime(string='Assignation Date', readonly=True) date_end = fields.Datetime(string='Ending Date', readonly=True) date_deadline = fields.Date(string='Deadline', readonly=True) date_last_stage_update = fields.Datetime(string='Last Stage Update', readonly=True) project_id = fields.Many2one('project.project', string='Project', readonly=True) working_days_close = fields.Float(string='# Working Days to Close', digits=(16,2), readonly=True, group_operator="avg", help="Number of Working Days to close the task") working_days_open = fields.Float(string='# Working Days to Assign', digits=(16,2), readonly=True, group_operator="avg", help="Number of Working Days to Open the task") delay_endings_days = fields.Float(string='# Days to Deadline', digits=(16,2), readonly=True) nbr = fields.Integer('# of Tasks', readonly=True) # TDE FIXME master: rename into nbr_tasks priority = fields.Selection([ ('0', 'Low'), ('1', 'Normal'), ('2', 'High') ], size=1, readonly=True, string="Priority") state = fields.Selection([ ('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage') ], string='Kanban State', readonly=True) company_id = fields.Many2one('res.company', string='Company', readonly=True) partner_id = fields.Many2one('res.partner', string='Contact', readonly=True) stage_id = fields.Many2one('project.task.type', string='Stage', readonly=True) def _select(self): select_str = """ SELECT (select 1 ) AS nbr, t.id as id, t.date_start as date_start, t.date_end as date_end, t.date_last_stage_update as date_last_stage_update, t.date_deadline as date_deadline, t.user_id, t.project_id, t.priority, t.name as name, t.company_id, t.partner_id, t.stage_id as stage_id, t.kanban_state as state, t.working_days_close as working_days_close, t.working_days_open as working_days_open, (extract('epoch' from (t.date_deadline-(now() at time zone 'UTC'))))/(3600*24) as delay_endings_days """ return select_str def _group_by(self): group_by_str = """ GROUP BY t.id, create_date, write_date, date_start, date_end, date_deadline, date_last_stage_update, t.user_id, t.project_id, t.priority, name, t.company_id, t.partner_id, stage_id """ return group_by_str def init(self): tools.drop_view_if_exists(self._cr, self._table) self._cr.execute(""" CREATE view %s as %s FROM project_task t WHERE t.active = 'true' %s """ % (self._table, self._select(), self._group_by()))
class FleetVehicleLogContract(models.Model): _inherit = ['mail.thread'] _inherits = {'fleet.vehicle.cost': 'cost_id'} _name = 'fleet.vehicle.log.contract' _description = 'Contract information on a vehicle' _order = 'state desc,expiration_date' def compute_next_year_date(self, strdate): oneyear = relativedelta(years=1) start_date = fields.Date.from_string(strdate) return fields.Date.to_string(start_date + oneyear) @api.model def default_get(self, default_fields): res = super(FleetVehicleLogContract, self).default_get(default_fields) contract = self.env.ref('fleet.type_contract_leasing', raise_if_not_found=False) res.update({ 'date': fields.Date.context_today(self), 'cost_subtype_id': contract and contract.id or False, 'cost_type': 'contract' }) return res name = fields.Text(compute='_compute_contract_name', store=True) active = fields.Boolean(default=True) start_date = fields.Date( 'Contract Start Date', default=fields.Date.context_today, help='Date when the coverage of the contract begins') expiration_date = fields.Date( 'Contract Expiration Date', default=lambda self: self.compute_next_year_date( fields.Date.context_today(self)), help= 'Date when the coverage of the contract expirates (by default, one year after begin date)' ) days_left = fields.Integer(compute='_compute_days_left', string='Warning Date') insurer_id = fields.Many2one('res.partner', 'Vendor') purchaser_id = fields.Many2one( 'res.partner', 'Contractor', default=lambda self: self.env.user.partner_id.id, help='Person to which the contract is signed for') ins_ref = fields.Char('Contract Reference', size=64, copy=False) state = fields.Selection( [('futur', 'Incoming'), ('open', 'In Progress'), ('expired', 'Expired'), ('diesoon', 'Expiring Soon'), ('closed', 'Closed')], 'Status', default='open', readonly=True, help='Choose whether the contract is still valid or not', track_visibility="onchange", copy=False) notes = fields.Text( 'Terms and Conditions', help= 'Write here all supplementary information relative to this contract', copy=False) cost_generated = fields.Float( 'Recurring Cost Amount', help="Costs paid at regular intervals, depending on the cost frequency. " "If the cost frequency is set to unique, the cost will be logged at the start date" ) cost_frequency = fields.Selection([('no', 'No'), ('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('yearly', 'Yearly')], 'Recurring Cost Frequency', default='no', help='Frequency of the recuring cost', required=True) generated_cost_ids = fields.One2many('fleet.vehicle.cost', 'contract_id', 'Generated Costs') sum_cost = fields.Float(compute='_compute_sum_cost', string='Indicative Costs Total') cost_id = fields.Many2one('fleet.vehicle.cost', 'Cost', required=True, ondelete='cascade') # we need to keep this field as a related with store=True because the graph view doesn't support # (1) to address fields from inherited table # (2) fields that aren't stored in database cost_amount = fields.Float(related='cost_id.amount', string='Amount', store=True) odometer = fields.Float( string='Odometer at creation', help= 'Odometer measure of the vehicle at the moment of the contract creation' ) @api.depends('vehicle_id', 'cost_subtype_id', 'date') def _compute_contract_name(self): for record in self: name = record.vehicle_id.name if record.cost_subtype_id.name: name += ' / ' + record.cost_subtype_id.name if record.date: name += ' / ' + record.date record.name = name @api.depends('expiration_date', 'state') def _compute_days_left(self): """return a dict with as value for each contract an integer if contract is in an open state and is overdue, return 0 if contract is in a closed state, return -1 otherwise return the number of days before the contract expires """ for record in self: if (record.expiration_date and (record.state == 'open' or record.state == 'expired')): today = fields.Date.from_string(fields.Date.today()) renew_date = fields.Date.from_string(record.expiration_date) diff_time = (renew_date - today).days record.days_left = diff_time > 0 and diff_time or 0 else: record.days_left = -1 @api.depends('cost_ids.amount') def _compute_sum_cost(self): for contract in self: contract.sum_cost = sum(contract.cost_ids.mapped('amount')) @api.onchange('vehicle_id') def _onchange_vehicle(self): if self.vehicle_id: self.odometer_unit = self.vehicle_id.odometer_unit @api.multi def contract_close(self): for record in self: record.state = 'closed' @api.multi def contract_open(self): for record in self: record.state = 'open' @api.multi def act_renew_contract(self): assert len( self.ids ) == 1, "This operation should only be done for 1 single contract at a time, as it it suppose to open a window as result" for element in self: # compute end date startdate = fields.Date.from_string(element.start_date) enddate = fields.Date.from_string(element.expiration_date) diffdate = (enddate - startdate) default = { 'date': fields.Date.context_today(self), 'start_date': fields.Date.to_string( fields.Date.from_string(element.expiration_date) + relativedelta(days=1)), 'expiration_date': fields.Date.to_string(enddate + diffdate), } newid = element.copy(default).id return { 'name': _("Renew Contract"), 'view_mode': 'form', 'view_id': self.env.ref('fleet.fleet_vehicle_log_contract_view_form').id, 'view_type': 'tree,form', 'res_model': 'fleet.vehicle.log.contract', 'type': 'ir.actions.act_window', 'domain': '[]', 'res_id': newid, 'context': { 'active_id': newid }, } @api.model def scheduler_manage_auto_costs(self): # This method is called by a cron task # It creates costs for contracts having the "recurring cost" field setted, depending on their frequency # For example, if a contract has a reccuring cost of 200 with a weekly frequency, this method creates a cost of 200 on the # first day of each week, from the date of the last recurring costs in the database to today # If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today # The created costs are associated to a contract thanks to the many2one field contract_id # If the contract has no start_date, no cost will be created, even if the contract has recurring costs VehicleCost = self.env['fleet.vehicle.cost'] deltas = { 'yearly': relativedelta(years=+1), 'monthly': relativedelta(months=+1), 'weekly': relativedelta(weeks=+1), 'daily': relativedelta(days=+1) } contracts = self.env['fleet.vehicle.log.contract'].search( [('state', '!=', 'closed')], offset=0, limit=None, order=None) for contract in contracts: if not contract.start_date or contract.cost_frequency == 'no': continue found = False last_cost_date = contract.start_date if contract.generated_cost_ids: last_autogenerated_cost = VehicleCost.search( [('contract_id', '=', contract.id), ('auto_generated', '=', True)], offset=0, limit=1, order='date desc') if last_autogenerated_cost: found = True last_cost_date = last_autogenerated_cost.date startdate = fields.Date.from_string(last_cost_date) if found: startdate += deltas.get(contract.cost_frequency) today = fields.Date.from_string(fields.Date.context_today(self)) while (startdate <= today) & (startdate <= fields.Date.from_string( contract.expiration_date)): data = { 'amount': contract.cost_generated, 'date': fields.Date.context_today(self), 'vehicle_id': contract.vehicle_id.id, 'cost_subtype_id': contract.cost_subtype_id.id, 'contract_id': contract.id, 'auto_generated': True } self.env['fleet.vehicle.cost'].create(data) startdate += deltas.get(contract.cost_frequency) return True @api.model def scheduler_manage_contract_expiration(self): # This method is called by a cron task # It manages the state of a contract, possibly by posting a message on the vehicle concerned and updating its status date_today = fields.Date.from_string(fields.Date.today()) in_fifteen_days = fields.Date.to_string(date_today + relativedelta(days=+15)) nearly_expired_contracts = self.search([('state', '=', 'open'), ('expiration_date', '<', in_fifteen_days)]) res = {} for contract in nearly_expired_contracts: if contract.vehicle_id.id in res: res[contract.vehicle_id.id] += 1 else: res[contract.vehicle_id.id] = 1 Vehicle = self.env['fleet.vehicle'] for vehicle, value in res.items(): Vehicle.browse(vehicle).message_post(body=_( '%s contract(s) will expire soon and should be renewed and/or closed!' ) % value) nearly_expired_contracts.write({'state': 'diesoon'}) expired_contracts = self.search([('state', '!=', 'expired'), ('expiration_date', '<', fields.Date.today())]) expired_contracts.write({'state': 'expired'}) futur_contracts = self.search([ ('state', 'not in', ['futur', 'closed']), ('start_date', '>', fields.Date.today()) ]) futur_contracts.write({'state': 'futur'}) now_running_contracts = self.search([('state', '=', 'futur'), ('start_date', '<=', fields.Date.today())]) now_running_contracts.write({'state': 'open'}) @api.model def run_scheduler(self): self.scheduler_manage_auto_costs() self.scheduler_manage_contract_expiration()
class MrpWorkorder(models.Model): _name = 'mrp.workorder' _description = 'Work Order' _inherit = ['mail.thread'] name = fields.Char( 'Work Order', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) workcenter_id = fields.Many2one( 'mrp.workcenter', 'Work Center', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) working_state = fields.Selection( 'Workcenter Status', related='workcenter_id.working_state', help='Technical: used in views only') production_id = fields.Many2one( 'mrp.production', 'Manufacturing Order', index=True, ondelete='cascade', required=True, track_visibility='onchange', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) product_id = fields.Many2one( 'product.product', 'Product', related='production_id.product_id', readonly=True, help='Technical: used in views only.', store=True) product_uom_id = fields.Many2one( 'product.uom', 'Unit of Measure', related='production_id.product_uom_id', readonly=True, help='Technical: used in views only.') production_availability = fields.Selection( 'Stock Availability', readonly=True, related='production_id.availability', store=True, help='Technical: used in views and domains only.') production_state = fields.Selection( 'Production State', readonly=True, related='production_id.state', help='Technical: used in views only.') product_tracking = fields.Selection( 'Product Tracking', related='production_id.product_id.tracking', help='Technical: used in views only.') qty_production = fields.Float('Original Production Quantity', readonly=True, related='production_id.product_qty') qty_remaining = fields.Float('Quantity To Be Produced', compute='_compute_qty_remaining', digits=dp.get_precision('Product Unit of Measure')) qty_produced = fields.Float( 'Quantity', default=0.0, readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="The number of products already handled by this work order") qty_producing = fields.Float( 'Currently Produced Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) is_produced = fields.Boolean(string="Has Been Produced", compute='_compute_is_produced') state = fields.Selection([ ('pending', 'Pending'), ('ready', 'Ready'), ('progress', 'In Progress'), ('done', 'Finished'), ('cancel', 'Cancelled')], string='Status', default='pending') date_planned_start = fields.Datetime( 'Scheduled Date Start', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) date_planned_finished = fields.Datetime( 'Scheduled Date Finished', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) date_start = fields.Datetime( 'Effective Start Date', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) date_finished = fields.Datetime( 'Effective End Date', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) duration_expected = fields.Float( 'Expected Duration', digits=(16, 2), states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Expected duration (in minutes)") duration = fields.Float( 'Real Duration', compute='_compute_duration', readonly=True, store=True) duration_unit = fields.Float( 'Duration Per Unit', compute='_compute_duration', readonly=True, store=True) duration_percent = fields.Integer( 'Duration Deviation (%)', compute='_compute_duration', group_operator="avg", readonly=True, store=True) operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Operation') # Should be used differently as BoM can change in the meantime worksheet = fields.Binary( 'Worksheet', related='operation_id.worksheet', readonly=True) move_raw_ids = fields.One2many( 'stock.move', 'workorder_id', 'Moves') move_line_ids = fields.One2many( 'stock.move.line', 'workorder_id', 'Moves to Track', domain=[('done_wo', '=', True)], help="Inventory moves for which you must scan a lot number at this work order") active_move_line_ids = fields.One2many( 'stock.move.line', 'workorder_id', domain=[('done_wo', '=', False)]) final_lot_id = fields.Many2one( 'stock.production.lot', 'Lot/Serial Number', domain="[('product_id', '=', product_id)]", states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) tracking = fields.Selection(related='production_id.product_id.tracking') time_ids = fields.One2many( 'mrp.workcenter.productivity', 'workorder_id') is_user_working = fields.Boolean( 'Is the Current User Working', compute='_compute_is_user_working', help="Technical field indicating whether the current user is working. ") production_messages = fields.Html('Workorder Message', compute='_compute_production_messages') next_work_order_id = fields.Many2one('mrp.workorder', "Next Work Order") scrap_ids = fields.One2many('stock.scrap', 'workorder_id') scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move') production_date = fields.Datetime('Production Date', related='production_id.date_planned_start', store=True) color = fields.Integer('Color', compute='_compute_color') capacity = fields.Float( 'Capacity', default=1.0, help="Number of pieces that can be produced in parallel.") @api.multi def name_get(self): return [(wo.id, "%s - %s - %s" % (wo.production_id.name, wo.product_id.name, wo.name)) for wo in self] @api.one @api.depends('production_id.product_qty', 'qty_produced') def _compute_is_produced(self): rounding = self.production_id.product_uom_id.rounding self.is_produced = float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0 @api.one @api.depends('time_ids.duration', 'qty_produced') def _compute_duration(self): self.duration = sum(self.time_ids.mapped('duration')) self.duration_unit = round(self.duration / max(self.qty_produced, 1), 2) # rounding 2 because it is a time if self.duration_expected: self.duration_percent = 100 * (self.duration_expected - self.duration) / self.duration_expected else: self.duration_percent = 0 def _compute_is_user_working(self): """ Checks whether the current user is working """ for order in self: if order.time_ids.filtered(lambda x: (x.user_id.id == self.env.user.id) and (not x.date_end) and (x.loss_type in ('productive', 'performance'))): order.is_user_working = True else: order.is_user_working = False @api.depends('production_id', 'workcenter_id', 'production_id.bom_id') def _compute_production_messages(self): ProductionMessage = self.env['mrp.message'] for workorder in self: domain = [ ('valid_until', '>=', fields.Date.today()), '|', ('workcenter_id', '=', False), ('workcenter_id', '=', workorder.workcenter_id.id), '|', '|', '|', ('product_id', '=', workorder.product_id.id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', workorder.product_id.product_tmpl_id.id), ('bom_id', '=', workorder.production_id.bom_id.id), ('routing_id', '=', workorder.operation_id.routing_id.id)] messages = ProductionMessage.search(domain).mapped('message') workorder.production_messages = "<br/>".join(messages) or False @api.multi def _compute_scrap_move_count(self): data = self.env['stock.scrap'].read_group([('workorder_id', 'in', self.ids)], ['workorder_id'], ['workorder_id']) count_data = dict((item['workorder_id'][0], item['workorder_id_count']) for item in data) for workorder in self: workorder.scrap_count = count_data.get(workorder.id, 0) @api.multi @api.depends('date_planned_finished', 'production_id.date_planned_finished') def _compute_color(self): late_orders = self.filtered(lambda x: x.production_id.date_planned_finished and x.date_planned_finished > x.production_id.date_planned_finished) for order in late_orders: order.color = 4 for order in (self - late_orders): order.color = 2 @api.onchange('qty_producing') def _onchange_qty_producing(self): """ Update stock.move.lot records, according to the new qty currently produced. """ moves = self.move_raw_ids.filtered(lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id.id != self.production_id.product_id.id) for move in moves: move_lots = self.active_move_line_ids.filtered(lambda move_lot: move_lot.move_id == move) if not move_lots: continue rounding = move.product_uom.rounding new_qty = float_round(move.unit_factor * self.qty_producing, precision_rounding=rounding) if move.product_id.tracking == 'lot': move_lots[0].product_qty = new_qty move_lots[0].qty_done = new_qty elif move.product_id.tracking == 'serial': # Create extra pseudo record qty_todo = float_round(new_qty - sum(move_lots.mapped('qty_done')), precision_rounding=rounding) if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: while float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: self.active_move_line_ids += self.env['stock.move.line'].new({ 'move_id': move.id, 'product_id': move.product_id.id, 'lot_id': False, 'product_uom_qty': 0.0, 'product_uom_id': move.product_uom.id, 'qty_done': min(1.0, qty_todo), 'workorder_id': self.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, 'date': move.date, }) qty_todo -= 1 elif float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: qty_todo = abs(qty_todo) for move_lot in move_lots: if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break if not move_lot.lot_id and float_compare(qty_todo, move_lot.qty_done, precision_rounding=rounding) >= 0: qty_todo = float_round(qty_todo - move_lot.qty_done, precision_rounding=rounding) self.active_move_line_ids -= move_lot # Difference operator else: #move_lot.product_qty = move_lot.product_qty - qty_todo if float_compare(move_lot.qty_done - qty_todo, 0, precision_rounding=rounding) == 1: move_lot.qty_done = move_lot.qty_done - qty_todo else: move_lot.qty_done = 0 qty_todo = 0 @api.multi def write(self, values): if ('date_planned_start' in values or 'date_planned_finished' in values) and any(workorder.state == 'done' for workorder in self): raise UserError(_('You can not change the finished work order.')) return super(MrpWorkorder, self).write(values) def _generate_lot_ids(self): """ Generate stock move lines """ self.ensure_one() MoveLine = self.env['stock.move.line'] tracked_moves = self.move_raw_ids.filtered( lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id != self.production_id.product_id and move.bom_line_id) for move in tracked_moves: qty = move.unit_factor * self.qty_producing if move.product_id.tracking == 'serial': while float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0: MoveLine.create({ 'move_id': move.id, 'product_uom_qty': 0, 'product_uom_id': move.product_uom.id, 'qty_done': min(1, qty), 'production_id': self.production_id.id, 'workorder_id': self.id, 'product_id': move.product_id.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, }) qty -= 1 else: MoveLine.create({ 'move_id': move.id, 'product_uom_qty': 0, 'product_uom_id': move.product_uom.id, 'qty_done': qty, 'product_id': move.product_id.id, 'production_id': self.production_id.id, 'workorder_id': self.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, }) def _assign_default_final_lot_id(self): self.final_lot_id = self.env['stock.production.lot'].search([('use_next_on_work_order_id', '=', self.id)], order='create_date, id', limit=1) def _get_byproduct_move_line(self, by_product_move, quantity): return { 'move_id': by_product_move.id, 'product_id': by_product_move.product_id.id, 'product_uom_qty': quantity, 'product_uom_id': by_product_move.product_uom.id, 'qty_done': quantity, 'workorder_id': self.id, 'location_id': by_product_move.location_id.id, 'location_dest_id': by_product_move.location_dest_id.id, } @api.multi def record_production(self): self.ensure_one() if self.qty_producing <= 0: raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.')) if (self.production_id.product_id.tracking != 'none') and not self.final_lot_id and self.move_raw_ids: raise UserError(_('You should provide a lot/serial number for the final product')) # Update quantities done on each raw material line # For each untracked component without any 'temporary' move lines, # (the new workorder tablet view allows registering consumed quantities for untracked components) # we assume that only the theoretical quantity was used for move in self.move_raw_ids: if move.has_tracking == 'none' and (move.state not in ('done', 'cancel')) and move.bom_line_id\ and move.unit_factor and not move.move_line_ids.filtered(lambda ml: not ml.done_wo): rounding = move.product_uom.rounding if self.product_id.tracking != 'none': qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) move._generate_consumed_move_line(qty_to_add, self.final_lot_id) elif len(move._get_move_lines()) < 2: move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) else: move._set_quantity_done(move.quantity_done + float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)) # Transfer quantities from temporary to final move lots or make them final for move_line in self.active_move_line_ids: # Check if move_line already exists if move_line.qty_done <= 0: # rounding... move_line.sudo().unlink() continue if move_line.product_id.tracking != 'none' and not move_line.lot_id: raise UserError(_('You should provide a lot/serial number for a component')) # Search other move_line where it could be added: lots = self.move_line_ids.filtered(lambda x: (x.lot_id.id == move_line.lot_id.id) and (not x.lot_produced_id) and (not x.done_move) and (x.product_id == move_line.product_id)) if lots: lots[0].qty_done += move_line.qty_done lots[0].lot_produced_id = self.final_lot_id.id move_line.sudo().unlink() else: move_line.lot_produced_id = self.final_lot_id.id move_line.done_wo = True # One a piece is produced, you can launch the next work order if self.next_work_order_id.state == 'pending': self.next_work_order_id.state = 'ready' self.move_line_ids.filtered( lambda move_line: not move_line.done_move and not move_line.lot_produced_id and move_line.qty_done > 0 ).write({ 'lot_produced_id': self.final_lot_id.id, 'lot_produced_qty': self.qty_producing }) # If last work order, then post lots used # TODO: should be same as checking if for every workorder something has been done? if not self.next_work_order_id: production_move = self.production_id.move_finished_ids.filtered( lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))) if production_move.product_id.tracking != 'none': move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id) if move_line: move_line.product_uom_qty += self.qty_producing move_line.qty_done += self.qty_producing else: move_line.create({'move_id': production_move.id, 'product_id': production_move.product_id.id, 'lot_id': self.final_lot_id.id, 'product_uom_qty': self.qty_producing, 'product_uom_id': production_move.product_uom.id, 'qty_done': self.qty_producing, 'workorder_id': self.id, 'location_id': production_move.location_id.id, 'location_dest_id': production_move.location_dest_id.id, }) else: production_move.quantity_done += self.qty_producing if not self.next_work_order_id: for by_product_move in self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))): if by_product_move.has_tracking != 'serial': values = self._get_byproduct_move_line(by_product_move, self.qty_producing * by_product_move.unit_factor) self.env['stock.move.line'].create(values) elif by_product_move.has_tracking == 'serial': qty_todo = by_product_move.product_uom._compute_quantity(self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id) for i in range(0, int(float_round(qty_todo, precision_digits=0))): values = self._get_byproduct_move_line(by_product_move, 1) self.env['stock.move.line'].create(values) # Update workorder quantity produced self.qty_produced += self.qty_producing if self.final_lot_id: self.final_lot_id.use_next_on_work_order_id = self.next_work_order_id self.final_lot_id = False # Set a qty producing rounding = self.production_id.product_uom_id.rounding if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: self.qty_producing = 0 elif self.production_id.product_id.tracking == 'serial': self._assign_default_final_lot_id() self.qty_producing = 1.0 self._generate_lot_ids() else: self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding) self._generate_lot_ids() if self.next_work_order_id and self.production_id.product_id.tracking != 'none': self.next_work_order_id._assign_default_final_lot_id() if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: self.button_finish() return True @api.multi def button_start(self): self.ensure_one() # As button_start is automatically called in the new view if self.state in ('done', 'cancel'): return True # Need a loss in case of the real time exceeding the expected timeline = self.env['mrp.workcenter.productivity'] if self.duration < self.duration_expected: loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1) if not len(loss_id): raise UserError(_("You need to define at least one productivity loss in the category 'Productivity'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses.")) else: loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','performance')], limit=1) if not len(loss_id): raise UserError(_("You need to define at least one productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses.")) for workorder in self: if workorder.production_id.state != 'progress': workorder.production_id.write({ 'state': 'progress', 'date_start': datetime.now(), }) timeline.create({ 'workorder_id': workorder.id, 'workcenter_id': workorder.workcenter_id.id, 'description': _('Time Tracking: ')+self.env.user.name, 'loss_id': loss_id[0].id, 'date_start': datetime.now(), 'user_id': self.env.user.id }) return self.write({'state': 'progress', 'date_start': datetime.now(), }) @api.multi def button_finish(self): self.ensure_one() self.end_all() return self.write({'state': 'done', 'date_finished': fields.Datetime.now()}) @api.multi def end_previous(self, doall=False): """ @param: doall: This will close all open time lines on the open work orders when doall = True, otherwise only the one of the current user """ # TDE CLEANME timeline_obj = self.env['mrp.workcenter.productivity'] domain = [('workorder_id', 'in', self.ids), ('date_end', '=', False)] if not doall: domain.append(('user_id', '=', self.env.user.id)) not_productive_timelines = timeline_obj.browse() for timeline in timeline_obj.search(domain, limit=None if doall else 1): wo = timeline.workorder_id if wo.duration_expected <= wo.duration: if timeline.loss_type == 'productive': not_productive_timelines += timeline timeline.write({'date_end': fields.Datetime.now()}) else: maxdate = fields.Datetime.from_string(timeline.date_start) + relativedelta(minutes=wo.duration_expected - wo.duration) enddate = datetime.now() if maxdate > enddate: timeline.write({'date_end': enddate}) else: timeline.write({'date_end': maxdate}) not_productive_timelines += timeline.copy({'date_start': maxdate, 'date_end': enddate}) if not_productive_timelines: loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'performance')], limit=1) if not len(loss_id): raise UserError(_("You need to define at least one unactive productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses.")) not_productive_timelines.write({'loss_id': loss_id.id}) return True @api.multi def end_all(self): return self.end_previous(doall=True) @api.multi def button_pending(self): self.end_previous() return True @api.multi def button_unblock(self): for order in self: order.workcenter_id.unblock() return True @api.multi def action_cancel(self): return self.write({'state': 'cancel'}) @api.multi def button_done(self): if any([x.state in ('done', 'cancel') for x in self]): raise UserError(_('A Manufacturing Order is already done or cancelled!')) self.end_all() return self.write({'state': 'done', 'date_finished': datetime.now()}) @api.multi def button_scrap(self): self.ensure_one() return { 'name': _('Scrap'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.scrap', 'view_id': self.env.ref('stock.stock_scrap_form_view2').id, 'type': 'ir.actions.act_window', 'context': {'default_workorder_id': self.id, 'default_production_id': self.production_id.id, 'product_ids': (self.production_id.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) | self.production_id.move_finished_ids.filtered(lambda x: x.state == 'done')).mapped('product_id').ids}, # 'context': {'product_ids': self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')).mapped('product_id').ids + [self.production_id.product_id.id]}, 'target': 'new', } @api.multi def action_see_move_scrap(self): self.ensure_one() action = self.env.ref('stock.action_stock_scrap').read()[0] action['domain'] = [('workorder_id', '=', self.id)] return action @api.multi @api.depends('qty_production', 'qty_produced') def _compute_qty_remaining(self): for wo in self: wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding)
class FleetVehicleCost(models.Model): _name = 'fleet.vehicle.cost' _description = 'Cost related to a vehicle' _order = 'date desc, vehicle_id asc' name = fields.Char(related='vehicle_id.name', string='Name', store=True) vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', required=True, help='Vehicle concerned by this log') cost_subtype_id = fields.Many2one( 'fleet.service.type', 'Type', help='Cost type purchased with this cost') amount = fields.Float('Total Price') cost_type = fields.Selection([('contract', 'Contract'), ('services', 'Services'), ('fuel', 'Fuel'), ('other', 'Other')], 'Category of the cost', default="other", help='For internal purpose only', required=True) parent_id = fields.Many2one('fleet.vehicle.cost', 'Parent', help='Parent cost to this current cost') cost_ids = fields.One2many('fleet.vehicle.cost', 'parent_id', 'Included Services', copy=True) odometer_id = fields.Many2one( 'fleet.vehicle.odometer', 'Odometer', help='Odometer measure of the vehicle at the moment of this log') odometer = fields.Float( compute="_get_odometer", inverse='_set_odometer', string='Odometer Value', help='Odometer measure of the vehicle at the moment of this log') odometer_unit = fields.Selection(related='vehicle_id.odometer_unit', string="Unit", readonly=True) date = fields.Date(help='Date when the cost has been executed') contract_id = fields.Many2one('fleet.vehicle.log.contract', 'Contract', help='Contract attached to this cost') auto_generated = fields.Boolean('Automatically Generated', readonly=True) description = fields.Char("Cost Description") def _get_odometer(self): for record in self: if record.odometer_id: record.odometer = record.odometer_id.value def _set_odometer(self): for record in self: if not record.odometer: raise UserError( _('Emptying the odometer value of a vehicle is not allowed.' )) odometer = self.env['fleet.vehicle.odometer'].create({ 'value': record.odometer, 'date': record.date or fields.Date.context_today(record), 'vehicle_id': record.vehicle_id.id }) self.odometer_id = odometer @api.model def create(self, data): # make sure that the data are consistent with values of parent and contract records given if 'parent_id' in data and data['parent_id']: parent = self.browse(data['parent_id']) data['vehicle_id'] = parent.vehicle_id.id data['date'] = parent.date data['cost_type'] = parent.cost_type if 'contract_id' in data and data['contract_id']: contract = self.env['fleet.vehicle.log.contract'].browse( data['contract_id']) data['vehicle_id'] = contract.vehicle_id.id data['cost_subtype_id'] = contract.cost_subtype_id.id data['cost_type'] = contract.cost_type if 'odometer' in data and not data['odometer']: # if received value for odometer is 0, then remove it from the # data as it would result to the creation of a # odometer log with 0, which is to be avoided del data['odometer'] return super(FleetVehicleCost, self).create(data)
class Challenge(models.Model): _inherit = 'gamification.challenge' category = fields.Selection(selection_add=[('forum', 'Website / Forum')])
class MailChannel(models.Model): """ Chat Session Reprensenting a conversation between users. It extends the base method for anonymous usage. """ _name = 'mail.channel' _inherit = ['mail.channel', 'rating.mixin'] anonymous_name = fields.Char('Anonymous Name') create_date = fields.Datetime('Create Date', required=True) channel_type = fields.Selection(selection_add=[('livechat', 'Livechat Conversation')]) livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel') @api.multi def _channel_message_notifications(self, message): """ When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when clicking on livechat button). So when the anonymous person is sending its FIRST message, the channel header should be added to the notification, since the user cannot be listining to the channel. """ notifications = super(MailChannel, self)._channel_message_notifications(message) message_values_dict = notifications[0][1] if len( notifications) else dict(message.message_format()[0]) for channel in self: # add uuid for private livechat channels to allow anonymous to listen if channel.channel_type == 'livechat': notifications.append([channel.uuid, message_values_dict]) if not message.author_id: unpinned_channel_partner = self.mapped( 'channel_last_seen_partner_ids').filtered( lambda cp: not cp.is_pinned) if unpinned_channel_partner: unpinned_channel_partner.write({'is_pinned': True}) notifications = self._channel_channel_notifications( unpinned_channel_partner.mapped( 'partner_id').ids) + notifications return notifications @api.multi def channel_info(self, extra_info=False): """ Extends the channel header by adding the livechat operator and the 'anonymous' profile :rtype : list(dict) """ channel_infos = super(MailChannel, self).channel_info(extra_info) # add the operator id if self.env.context.get('im_livechat_operator_partner_id'): partner_name = self.env['res.partner'].browse( self.env.context.get( 'im_livechat_operator_partner_id')).name_get()[0] for channel_info in channel_infos: channel_info['operator_pid'] = partner_name channel_infos_dict = dict((c['id'], c) for c in channel_infos) for channel in self: # add the anonymous name if channel.anonymous_name: channel_infos_dict[ channel.id]['anonymous_name'] = channel.anonymous_name # add the last message date if channel.channel_type == 'livechat': last_msg = self.env['mail.message'].search( [("channel_ids", "in", [channel.id])], limit=1) if last_msg: channel_infos_dict[ channel.id]['last_message_date'] = last_msg.date return list(channel_infos_dict.values()) @api.model def channel_fetch_slot(self): values = super(MailChannel, self).channel_fetch_slot() pinned_channels = self.env['mail.channel.partner'].search([ ('partner_id', '=', self.env.user.partner_id.id), ('is_pinned', '=', True) ]).mapped('channel_id') values['channel_livechat'] = self.search([ ('channel_type', '=', 'livechat'), ('id', 'in', pinned_channels.ids) ]).channel_info() return values @api.model def remove_empty_livechat_sessions(self): hours = 1 # never remove empty session created within the last hour self.env.cr.execute( """ SELECT id as id FROM mail_channel C WHERE NOT EXISTS ( SELECT * FROM mail_message_mail_channel_rel R WHERE R.mail_channel_id = C.id ) AND C.channel_type = 'livechat' AND livechat_channel_id IS NOT NULL AND COALESCE(write_date, create_date, (now() at time zone 'UTC'))::timestamp < ((now() at time zone 'UTC') - interval %s)""", ("%s hours" % hours, )) empty_channel_ids = [item['id'] for item in self.env.cr.dictfetchall()] self.browse(empty_channel_ids).unlink() @api.model def get_empty_list_help(self, help): if help: return '<p>%s</p>' % (help) return super(MailChannel, self).get_empty_list_help(help) def _define_command_history(self): return { 'channel_types': ['livechat'], 'help': _('See 15 last visited pages') } def _execute_command_history(self, **kwargs): notification = [] notification_values = { '_type': 'history_command', } notification.append([self.uuid, dict(notification_values)]) return self.env['bus.bus'].sendmany(notification) def _send_history_message(self, pid, page_history): message_body = _('No history found') if page_history: html_links = [ '<li><a href="%s" target="_blank">%s</a></li>' % (page, page) for page in page_history ] message_body = '<span class="o_mail_notification"><ul>%s</ul></span>' % ( ''.join(html_links)) self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', pid), { 'body': message_body, 'channel_ids': self.ids, 'info': 'transient_message', }) # Rating Mixin def rating_get_parent_model_name(self, values): return 'im_livechat.channel' def rating_get_parent_id(self): return self.livechat_channel_id.id